@@ -49,6 +49,7 @@ def __init__(self, mainapp):
4949 self .data_handler = mainapp .draw .update
5050 self .command = ["cava" , "-p" , self .cava_config_file ]
5151 self .state = self .NONE
52+ self .process = None
5253
5354 self .env = dict (os .environ )
5455 self .env ["LC_ALL" ] = "en_US.UTF-8" # not sure if it's necessary
@@ -64,7 +65,6 @@ def __init__(self, mainapp):
6465 self .io_watch_id = None
6566
6667 def _run_process (self ):
67- logger .debug ("Launching cava process..." )
6868 try :
6969 self .process = subprocess .Popen (
7070 self .command ,
@@ -73,13 +73,11 @@ def _run_process(self):
7373 env = self .env ,
7474 preexec_fn = set_death_signal # Ensure cava gets killed when the parent dies.
7575 )
76- logger .debug ("cava successfully launched!" )
7776 self .state = self .RUNNING
7877 except Exception :
7978 logger .exception ("Fail to launch cava" )
8079
8180 def _start_io_reader (self ):
82- logger .debug ("Activating GLib IO watch for cava stream handler" )
8381 # Open FIFO in non-blocking mode for reading
8482 self .fifo_fd = os .open (self .path , os .O_RDONLY | os .O_NONBLOCK )
8583 # Open dummy write end to prevent getting an EOF on our FIFO
@@ -89,31 +87,43 @@ def _start_io_reader(self):
8987 def _io_callback (self , source , condition ):
9088 chunk = self .byte_size * self .bars # number of bytes for given format
9189 try :
90+ if self .fifo_fd is None :
91+ return False
92+
9293 data = os .read (self .fifo_fd , chunk )
9394 except OSError as e :
94- # logger.error("Error reading FIFO: {}".format(e))
95+ if e .errno == 11 : # EAGAIN - would block, normal for non-blocking
96+ return True
97+ elif e .errno == 9 : # EBADF - bad file descriptor
98+ GLib .idle_add (self .restart )
99+ return False
100+ else :
101+ return False
102+ except Exception :
95103 return False
96104
97105 # When no data is read, do not remove the IO watch immediately.
98106 if len (data ) < chunk :
99- # Instead of closing the FIFO, we log a warning and continue.
100- # logger.warning("Incomplete data packet received (expected {} bytes, got {}). Waiting for more data...".format(chunk, len(data)))
101- # Returning True keeps the IO watch active. A real EOF will only occur when the writer closes.
102- return True
107+ if len (data ) == 0 :
108+ # No data available, continue watching
109+ return True
110+ else :
111+ return True
103112
104- fmt = self .byte_type * self .bars # format string for struct.unpack
105- sample = [i / self .byte_norm for i in struct .unpack (fmt , data )]
106- GLib .idle_add (self .data_handler , sample )
113+ try :
114+ fmt = self .byte_type * self .bars # format string for struct.unpack
115+ sample = [i / self .byte_norm for i in struct .unpack (fmt , data )]
116+ GLib .idle_add (self .data_handler , sample )
117+ except (struct .error , Exception ):
118+ return True
119+
107120 return True
108121
109122 def _on_stop (self ):
110- logger .debug ("Cava stream handler deactivated" )
111123 if self .state == self .RESTARTING :
112124 self .start ()
113125 elif self .state == self .RUNNING :
114126 self .state = self .NONE
115- logger .error ("Cava process was unexpectedly terminated." )
116- # self.restart() # May cause infinity loop, need more check
117127
118128 def start (self ):
119129 """Launch cava"""
@@ -123,27 +133,54 @@ def start(self):
123133 def restart (self ):
124134 """Restart cava process"""
125135 if self .state == self .RUNNING :
126- logger .debug ("Restarting cava process (normal mode) ..." )
127136 self .state = self .RESTARTING
128- if self .process .poll () is None :
137+ if self .process and self . process .poll () is None :
129138 self .process .kill ()
130139 elif self .state == self .NONE :
131- logger .warning ("Restarting cava process (after crash) ..." )
132140 self .start ()
133141
134142 def close (self ):
135143 """Stop cava process"""
136144 self .state = self .CLOSING
137- if self . process . poll () is None :
138- self . process . kill ()
145+
146+ # Stop IO watch first
139147 if self .io_watch_id :
140148 GLib .source_remove (self .io_watch_id )
141- if self .fifo_fd :
142- os .close (self .fifo_fd )
143- if self .fifo_dummy_fd :
144- os .close (self .fifo_dummy_fd )
149+ self .io_watch_id = None
150+
151+ # Close file descriptors safely
152+ if self .fifo_fd is not None :
153+ try :
154+ os .close (self .fifo_fd )
155+ except OSError :
156+ pass
157+ finally :
158+ self .fifo_fd = None
159+
160+ if self .fifo_dummy_fd is not None :
161+ try :
162+ os .close (self .fifo_dummy_fd )
163+ except OSError :
164+ pass
165+ finally :
166+ self .fifo_dummy_fd = None
167+
168+ # Kill process if still running
169+ if self .process and self .process .poll () is None :
170+ try :
171+ self .process .kill ()
172+ self .process .wait (timeout = 2.0 ) # Wait up to 2 seconds
173+ except subprocess .TimeoutExpired :
174+ self .process .kill ()
175+ except Exception :
176+ pass
177+
178+ # Remove FIFO file
145179 if os .path .exists (self .path ):
146- os .remove (self .path )
180+ try :
181+ os .remove (self .path )
182+ except OSError :
183+ pass
147184
148185class AttributeDict (dict ):
149186 """Dictionary with keys as attributes. Does nothing but easy reading"""
@@ -159,6 +196,8 @@ def __init__(self):
159196 self .silence_value = 0
160197 self .audio_sample = []
161198 self .color = None
199+ self ._cached_color = None
200+ self ._color_file_mtime = 0
162201
163202 self .area = Gtk .DrawingArea ()
164203 self .area .connect ("draw" , self .redraw )
@@ -181,7 +220,7 @@ def is_silence(self, value):
181220
182221 def update (self , data ):
183222 """Audio data processing"""
184- self .color_update ()
223+ self .color_update_cached ()
185224 self .audio_sample = data
186225 if not self .is_silence (self .audio_sample [0 ]):
187226 self .area .queue_draw ()
@@ -226,6 +265,34 @@ def size_update(self, *args):
226265 self .sizes .bar .width = max (int (tw / self .sizes .number ), 1 )
227266 self .sizes .bar .height = self .sizes .area .height
228267
268+ def color_update_cached (self ):
269+ """Set drawing color with caching to avoid file reads on every frame"""
270+ color_file = get_relative_path ("../styles/colors.css" )
271+ try :
272+ # Check if file has been modified
273+ current_mtime = os .path .getmtime (color_file )
274+ if current_mtime != self ._color_file_mtime or self ._cached_color is None :
275+ self ._color_file_mtime = current_mtime
276+
277+ color = "#a5c8ff" # default value
278+ with open (color_file , "r" ) as f :
279+ content = f .read ()
280+ m = re .search (r"--primary:\s*(#[0-9a-fA-F]{6})" , content )
281+ if m :
282+ color = m .group (1 )
283+
284+ red = int (color [1 :3 ], 16 ) / 255
285+ green = int (color [3 :5 ], 16 ) / 255
286+ blue = int (color [5 :7 ], 16 ) / 255
287+ self ._cached_color = Gdk .RGBA (red = red , green = green , blue = blue , alpha = 1.0 )
288+
289+ self .color = self ._cached_color
290+ except Exception :
291+ if self ._cached_color is None :
292+ # Fallback to default color
293+ self ._cached_color = Gdk .RGBA (red = 0.647 , green = 0.784 , blue = 1.0 , alpha = 1.0 )
294+ self .color = self ._cached_color
295+
229296 def color_update (self ):
230297 """Set drawing color according to current settings by reading primary color from CSS"""
231298 color = "#a5c8ff" # default value
@@ -235,8 +302,8 @@ def color_update(self):
235302 m = re .search (r"--primary:\s*(#[0-9a-fA-F]{6})" , content )
236303 if m :
237304 color = m .group (1 )
238- except Exception as e :
239- logger . error ( "Failed to read primary color: {}" . format ( e ))
305+ except Exception :
306+ pass
240307 red = int (color [1 :3 ], 16 ) / 255
241308 green = int (color [3 :5 ], 16 ) / 255
242309 blue = int (color [5 :7 ], 16 ) / 255
0 commit comments