Skip to content

Commit 6be1ed5

Browse files
committed
feat(cava): add reload functionality and improve process management
- Implemented `_reload_cava` method to restart the cava process. - Added callback registration for reload actions. - Enhanced error handling during process termination and audio data reading. - Created local directory for cava configuration files.
1 parent 0f28dcc commit 6be1ed5

3 files changed

Lines changed: 89 additions & 14 deletions

File tree

docs/widgets/(Widget)-Cava.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
| `gradient_color_3` | string | "#cba6f7" | Third gradient color in hex format |
2727
| `hide_empty` | boolean | false | Hide widget when no audio is playing (requires `sleep_timer` to be enabled) |
2828
| `container_padding` | object | {top: 0, left: 0, bottom: 0, right: 0} | Padding of the widget container |
29+
| `callbacks` | dict | `{'on_left': 'do_nothing', 'on_middle': 'do_nothing', 'on_right': 'reload_cava'}` | Callbacks for mouse events on the widget. |
2930

3031
## Example Configuration
3132

@@ -77,6 +78,7 @@
7778
- **gradient_color_3**: Third gradient color in hex format.
7879
- **hide_empty**: Hide widget when no audio is playing (requires `sleep_timer` to be enabled).
7980
- **container_padding**: Explicitly set padding inside widget container.
81+
- **callbacks**: A dictionary specifying the callbacks for mouse events. The keys are `on_left`, `on_middle`, and `on_right`, and the values are the names of the callback functions.
8082

8183
More information on this option is documented in the [example config file](https://github.com/karlstav/cava/blob/master/example_files/config)
8284

src/core/validation/widgets/yasb/cava.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@
2222
'gradient_color_3': '#cba6f7',
2323
'hide_empty': False,
2424
'container_padding': {'top': 0, 'left': 0, 'bottom': 0, 'right': 0},
25+
'callbacks': {
26+
'on_left': 'do_nothing',
27+
'on_middle': 'do_nothing',
28+
'on_right': 'reload_cava'
29+
}
2530
}
2631

2732
VALIDATION_SCHEMA = {
@@ -152,5 +157,23 @@
152157
}
153158
},
154159
'default': DEFAULTS['container_padding']
160+
},
161+
'callbacks': {
162+
'type': 'dict',
163+
'schema': {
164+
'on_left': {
165+
'type': 'string',
166+
'default': DEFAULTS['callbacks']['on_left'],
167+
},
168+
'on_middle': {
169+
'type': 'string',
170+
'default': DEFAULTS['callbacks']['on_middle'],
171+
},
172+
'on_right': {
173+
'type': 'string',
174+
'default': DEFAULTS['callbacks']['on_right'],
175+
}
176+
},
177+
'default': DEFAULTS['callbacks']
155178
}
156179
}

src/core/widgets/yasb/cava.py

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import os
2+
from pathlib import Path
23
import struct
34
import subprocess
4-
import tempfile
55
import textwrap
66
import logging
77
import shutil
@@ -69,6 +69,7 @@ def __init__(
6969
gradient_color_3: str,
7070
hide_empty: bool,
7171
container_padding: dict[str, int],
72+
callbacks: dict[str, str],
7273
):
7374
super().__init__(class_name="cava-widget")
7475
# Widget configuration
@@ -125,6 +126,12 @@ def __init__(
125126
self._bar_frame = CavaBar(self)
126127
self._widget_container_layout.addWidget(self._bar_frame)
127128

129+
self.register_callback("reload_cava", self._reload_cava)
130+
131+
self.callback_left = callbacks['on_left']
132+
self.callback_right = callbacks['on_right']
133+
self.callback_middle = callbacks['on_middle']
134+
128135
# Connect signal and start audio processing
129136
self.samplesUpdated.connect(self.on_samples_updated)
130137
self.destroyed.connect(self.stop_cava)
@@ -141,13 +148,35 @@ def __init__(
141148
QApplication.instance().aboutToQuit.connect(self.stop_cava)
142149
atexit.register(self.stop_cava)
143150

151+
152+
def _reload_cava(self):
153+
"""Stop current cava process and start a new one"""
154+
try:
155+
self.stop_cava()
156+
157+
self.samples = [0] * self._bars_number
158+
159+
QTimer.singleShot(500, self.start_cava)
160+
161+
if self._hide_empty and self._sleep_timer > 0:
162+
if hasattr(self, '_hide_timer'):
163+
self._hide_timer.stop()
164+
self._hide_cava_widget = True
165+
self.show()
166+
except Exception as e:
167+
logging.error(f"Error reloading cava: {e}")
168+
144169
def stop_cava(self) -> None:
145170
self._stop_cava = True
146171
if hasattr(self, "_cava_process") and self._cava_process.poll() is None:
147-
self._cava_process.terminate()
172+
try:
173+
self._cava_process.terminate()
174+
self._cava_process.wait(timeout=2)
175+
except subprocess.TimeoutExpired:
176+
self._cava_process.kill()
148177
if hasattr(self, "thread_cava") and self.thread_cava.is_alive():
149178
if threading.current_thread() != self.thread_cava:
150-
self.thread_cava.join()
179+
self.thread_cava.join(timeout=2)
151180

152181
def initialize_colors(self) -> None:
153182
self.foreground_color = QColor(self._foreground)
@@ -180,6 +209,9 @@ def hide_bar_frame(self) -> None:
180209

181210

182211
def start_cava(self) -> None:
212+
# Reset stop flag to allow new process to start
213+
self._stop_cava = False
214+
183215
# Build configuration file, temp config file will be created in %temp% directory
184216
config_template = textwrap.dedent(f"""\
185217
# Cava config auto-generated by YASB
@@ -216,34 +248,52 @@ def start_cava(self) -> None:
216248
bytetype, bytesize, bytenorm = ("B", 1, 255)
217249

218250
def process_audio():
251+
252+
LOCALDATA_FOLDER = Path(os.environ["LOCALAPPDATA"]) / "Yasb"
253+
if not LOCALDATA_FOLDER.exists():
254+
LOCALDATA_FOLDER.mkdir(parents=True, exist_ok=True)
255+
256+
cava_config_path = None
219257
try:
220-
cava_config_path = os.path.join(tempfile.gettempdir(), "yasb_cava_config")
258+
cava_config_path = LOCALDATA_FOLDER / Path(f"yasb_cava_config")
221259
with open(cava_config_path, "w") as config_file:
222260
config_file.write(config_template)
223261
config_file.flush()
262+
224263
self._cava_process = subprocess.Popen(
225264
["cava", "-p", cava_config_path],
226265
stdout=subprocess.PIPE,
266+
stderr=subprocess.PIPE,
227267
creationflags=subprocess.CREATE_NO_WINDOW
228268
)
269+
229270
chunk = bytesize * self._bars_number
230271
fmt = bytetype * self._bars_number
231-
while True:
272+
273+
while not self._stop_cava:
232274
try:
233275
data = self._cava_process.stdout.read(chunk)
276+
if len(data) < chunk:
277+
break
278+
samples = [val / bytenorm for val in struct.unpack(fmt, data)]
279+
self.samplesUpdated.emit(samples)
234280
except Exception as e:
235-
return
236-
if len(data) < chunk:
281+
logging.error(f"Error reading cava data: {e}")
237282
break
238-
samples = [val / bytenorm for val in struct.unpack(fmt, data)]
239-
if self._stop_cava:
240-
break
241-
self.samplesUpdated.emit(samples)
283+
242284
except Exception as e:
243-
logging.error(f"Error processing audio in Cava: {e}")
244-
self.stop_cava()
285+
logging.error(f"Error starting cava process: {e}")
245286
finally:
246-
self.stop_cava()
287+
# Clean up config file
288+
if cava_config_path and os.path.exists(cava_config_path):
289+
try:
290+
os.unlink(cava_config_path)
291+
except:
292+
pass
247293

294+
# Wait for previous thread to finish if it exists
295+
if hasattr(self, 'thread_cava') and self.thread_cava.is_alive():
296+
self.thread_cava.join(timeout=1)
297+
248298
self.thread_cava = threading.Thread(target=process_audio, daemon=True)
249299
self.thread_cava.start()

0 commit comments

Comments
 (0)