1818import signal
1919import atexit
2020import json
21+ import shlex
2122from pathlib import Path
2223
2324
@@ -44,9 +45,13 @@ def _load_config(self):
4445 line = line [7 :]
4546 if "=" in line :
4647 key , value = line .split ("=" , 1 )
47- value = value .strip (" \" '" )
48+ value = value .strip ()
4849 if value .startswith ("(" ) and value .endswith (")" ):
49- value = value [1 :- 1 ].strip ("\" '" )
50+ # Remove parentheses and split using shlex to respect quotes
51+ value = value [1 :- 1 ].strip ()
52+ value = shlex .split (value )
53+ else :
54+ value = value .strip ("\" '" )
5055 config [key ] = value
5156 except Exception as e :
5257 print (f"Warning: Could not load Hyde config: { e } " , file = sys .stderr )
@@ -63,7 +68,7 @@ class CavaDataParser:
6368
6469 @staticmethod
6570 def format_data (line , bar_chars = "βββββ
βββ" , width = None , standby_mode = "" ):
66- """Format cava data with custom bar characters"""
71+ """Format cava data with custom bar characters (list or string) """
6772 line = line .strip ()
6873 if not line :
6974 return CavaDataParser ._handle_standby_mode (standby_mode , bar_chars , width )
@@ -107,6 +112,7 @@ def format_data(line, bar_chars="βββββ
βββ", width=None, standby_
107112 char_index = bar_length - 1
108113 else :
109114 char_index = value
115+ # bar_chars can be a list or string
110116 result += bar_chars [char_index ]
111117
112118 return result
@@ -257,7 +263,7 @@ def _check_auto_shutdown(self):
257263 while not self .should_shutdown :
258264 time .sleep (1 )
259265 with self .clients_lock :
260- if not self .clients and time .time () - self .last_client_time > 5 :
266+ if not self .clients and time .time () - self .last_client_time > 1 :
261267 print ("No clients connected for 5 seconds, shutting down..." )
262268 self .should_shutdown = True
263269 break
@@ -285,19 +291,88 @@ def _broadcast_data(self, data):
285291 self .last_client_time = time .time ()
286292
287293 def _handle_client_connections (self ):
288- """Handle incoming client connections"""
294+ """Handle incoming client connections and listen for reload command """
289295 while True :
290296 try :
291297 conn , addr = self .server_socket .accept ()
292298 print ("New client connected" )
299+ threading .Thread (target = self ._client_command_listener , args = (conn ,), daemon = True ).start ()
293300 with self .clients_lock :
294301 self .clients .append (conn )
295302 self .last_client_time = time .time ()
296303 except OSError :
297304 break
298305
299- def _create_cava_config (self , bars = 16 , range_val = 15 ):
300- """Create cava configuration file"""
306+ def _client_command_listener (self , conn ):
307+ """Listen for special commands from a client (e.g., reload)"""
308+ try :
309+ conn .settimeout (0.1 )
310+ data = b""
311+ while True :
312+ try :
313+ chunk = conn .recv (1024 )
314+ if not chunk :
315+ break
316+ data += chunk
317+ if b"\n " in data :
318+ line , data = data .split (b"\n " , 1 )
319+ if line .strip () == b"CMD:RELOAD" :
320+ print ("Received reload command from client." )
321+ self ._reload_cava_process ()
322+ except socket .timeout :
323+ break
324+ except Exception :
325+ pass
326+
327+ def _reload_cava_process (self ):
328+ """Restart cava process and reload config with latest values"""
329+ print ("Reloading cava process..." )
330+ if self .cava_process and self .cava_process .poll () is None :
331+ self .cava_process .terminate ()
332+ try :
333+ self .cava_process .wait (timeout = 2 )
334+ except subprocess .TimeoutExpired :
335+ self .cava_process .kill ()
336+ # Always use latest config values
337+ hyde_config = HydeConfig ()
338+ bars = int (hyde_config .get_value ("CAVA_BARS" , 16 ))
339+ range_val = int (hyde_config .get_value ("CAVA_RANGE" , 15 ))
340+ channels = hyde_config .get_value ("CAVA_CHANNELS" , "stereo" )
341+ reverse = hyde_config .get_value ("CAVA_REVERSE" , 0 )
342+ try :
343+ reverse = int (reverse )
344+ except Exception :
345+ reverse = 1 if str (reverse ).lower () in ("true" , "yes" , "on" ) else 0
346+ self ._create_cava_config (bars , range_val , channels , reverse )
347+ try :
348+ self .cava_process = subprocess .Popen (
349+ ["cava" , "-p" , str (self .config_file )],
350+ stdout = subprocess .PIPE ,
351+ stderr = subprocess .PIPE ,
352+ text = True ,
353+ )
354+ print ("Cava process restarted." )
355+ except FileNotFoundError :
356+ print ("Error: cava not found. Please install cava." )
357+
358+ def _create_cava_config (self , bars = 16 , range_val = 15 , channels = "stereo" , reverse = 0 , prefix = "" ):
359+ """Create cava configuration file with channels and reverse support, using HydeConfig with or without prefix as appropriate"""
360+ hyde_config = HydeConfig ()
361+
362+ if prefix :
363+ config_channels = hyde_config .get_value (f"CAVA_{ prefix } _CHANNELS" )
364+ config_reverse = hyde_config .get_value (f"CAVA_{ prefix } _REVERSE" )
365+ else :
366+ config_channels = hyde_config .get_value ("CAVA_CHANNELS" )
367+ config_reverse = hyde_config .get_value ("CAVA_REVERSE" )
368+ if config_channels in ("mono" , "stereo" ):
369+ channels = config_channels
370+ if config_reverse is not None :
371+ try :
372+ reverse = int (config_reverse )
373+ except ValueError :
374+ reverse = 1 if str (config_reverse ).lower () in ("true" , "yes" , "on" ) else 0
375+
301376 self .temp_dir .mkdir (parents = True , exist_ok = True )
302377
303378 config_content = f"""[general]
@@ -313,12 +388,14 @@ def _create_cava_config(self, bars=16, range_val=15):
313388raw_target = /dev/stdout
314389data_format = ascii
315390ascii_max_range = { range_val }
391+ channels = { channels }
392+ reverse = { reverse }
316393"""
317394
318395 with open (self .config_file , "w" ) as f :
319396 f .write (config_content )
320397
321- def start (self , bars = 16 , range_val = 15 ):
398+ def start (self , bars = 16 , range_val = 15 , channels = "stereo" , reverse = 0 ):
322399 """Start the cava server"""
323400 try :
324401 self .temp_dir .mkdir (parents = True , exist_ok = True )
@@ -366,7 +443,7 @@ def start(self, bars=16, range_val=15):
366443
367444 self ._write_pid_file ()
368445
369- self ._create_cava_config (bars , range_val )
446+ self ._create_cava_config (bars , range_val , channels , reverse )
370447
371448 print (f"Starting cava with config: { self .config_file } " )
372449 try :
@@ -566,13 +643,24 @@ def parse_command_config(hyde_config, command, args):
566643 """Parse configuration for a specific command type"""
567644 prefix = f"CAVA_{ command .upper ()} "
568645
569- bar_chars = args .bar or hyde_config .get_value (f"{ prefix } _BAR" , "βββββ
βββ" )
646+ # Prefer --bar-array if present
647+ if hasattr (args , "bar_array" ) and args .bar_array :
648+ bar_chars = args .bar_array
649+ else :
650+ # Prefer BAR_ARRAY from config if present and is a list
651+ bar_array = hyde_config .get_value (f"{ prefix } _BAR_ARRAY" )
652+ if bar_array and isinstance (bar_array , list ):
653+ bar_chars = bar_array
654+ else :
655+ bar_chars = args .bar or hyde_config .get_value (f"{ prefix } _BAR" , "βββββ
βββ" )
656+ if isinstance (bar_chars , str ):
657+ bar_chars = list (bar_chars )
658+
570659 width = (
571660 args .width
572661 if args .width is not None
573662 else int (hyde_config .get_value (f"{ prefix } _WIDTH" , "0" ) or 0 )
574663 )
575-
576664 if not width :
577665 width = len (bar_chars ) if bar_chars else 8
578666
@@ -584,26 +672,46 @@ def parse_command_config(hyde_config, command, args):
584672 standby_mode = hyde_config .get_value (f"{ prefix } _STANDBY" , "0" )
585673 if standby_mode is None or standby_mode == "" :
586674 standby_mode = "\n "
587- elif standby_mode .isdigit ():
675+ elif isinstance ( standby_mode , str ) and standby_mode .isdigit ():
588676 standby_mode = int (standby_mode )
589677
590678 return bar_chars , width , standby_mode
591679
592680
681+ class CavaReloadClient :
682+ """Minimal client to send reload command to the server"""
683+ def __init__ (self ):
684+ self .runtime_dir = os .getenv ("XDG_RUNTIME_DIR" , os .path .join ("/run/user" , str (os .getuid ())))
685+ self .socket_file = os .path .join (self .runtime_dir , "hyde" , "cava.sock" )
686+
687+ def reload (self ):
688+ if not os .path .exists (self .socket_file ):
689+ print ("Cava manager is not running." )
690+ sys .exit (1 )
691+ try :
692+ s = socket .socket (socket .AF_UNIX , socket .SOCK_STREAM )
693+ s .connect (self .socket_file )
694+ s .sendall (b"CMD:RELOAD\n " )
695+ s .close ()
696+ print ("Reload command sent." )
697+ except Exception as e :
698+ print (f"Failed to send reload command: { e } " )
699+ sys .exit (1 )
700+
701+
593702def create_client_parser (subparsers , name , help_text ):
594703 """Create a client parser with common arguments"""
595704 parser = subparsers .add_parser (name , help = help_text )
596705 parser .add_argument ("--bar" , default = None , help = "Bar characters" )
706+ parser .add_argument ("--bar-array" , nargs = "+" , help = "Bar characters as an array (e.g. --bar-array '<span color=red>#</span>' '<span color=green>#</span>')" )
597707 parser .add_argument ("--width" , type = int , help = "Bar width" )
598708 parser .add_argument (
599709 "--stb" ,
600710 default = None ,
601711 help = 'Standby mode (0-3 or string): 0=clean (totally hides the module), 1=blank (makes module expand as spaces), 2=full (occupies the module with full bar), 3=low (makes the module display the lowest set bar), ""=displays nothing and compresses the module, string=displays the custom string' ,
602712 )
603-
604713 if name == "waybar" :
605714 parser .add_argument ("--json" , action = "store_true" , help = "Output JSON format for waybar tooltips" )
606-
607715 return parser
608716
609717
@@ -615,12 +723,15 @@ def main():
615723 manager_parser = subparsers .add_parser ("manager" , help = "Start cava manager" )
616724 manager_parser .add_argument ("--bars" , type = int , default = 16 , help = "Number of bars" )
617725 manager_parser .add_argument ("--range" , type = int , default = 15 , help = "ASCII range" )
726+ manager_parser .add_argument ("--channels" , choices = ["mono" , "stereo" ], default = "stereo" , help = "Audio channels: mono or stereo" )
727+ manager_parser .add_argument ("--reverse" , type = int , choices = [0 , 1 ], default = 0 , help = "Reverse frequency order: 0=normal, 1=reverse" )
618728
619729 create_client_parser (subparsers , "waybar" , "Waybar client" )
620730 create_client_parser (subparsers , "stdout" , "Stdout client" )
621731 create_client_parser (subparsers , "hyprlock" , "Hyprlock client" )
622732
623733 subparsers .add_parser ("status" , help = "Check manager status" )
734+ subparsers .add_parser ("reload" , help = "Reload cava manager (restart cava process)" )
624735
625736 args = parser .parse_args ()
626737
@@ -630,7 +741,7 @@ def main():
630741 print ("Cava manager is already running" )
631742 sys .exit (0 )
632743
633- server .start (args .bars , args .range )
744+ server .start (args .bars , args .range , args . channels , args . reverse )
634745
635746 elif args .command in ["waybar" , "stdout" , "hyprlock" ]:
636747 hyde_config = HydeConfig ()
@@ -656,6 +767,9 @@ def main():
656767 print ("Cava manager is not running" )
657768 sys .exit (1 )
658769
770+ elif args .command == "reload" :
771+ CavaReloadClient ().reload ()
772+
659773 else :
660774 parser .print_help ()
661775
0 commit comments