app: add config reload on F5
This commit is contained in:
@@ -371,6 +371,7 @@ class TuimbleApp(App):
|
||||
("f1", "toggle_deaf", "Deafen"),
|
||||
("f2", "cycle_output_volume", "Vol Out"),
|
||||
("f3", "cycle_input_volume", "Vol In"),
|
||||
("f5", "reload_config", "Reload"),
|
||||
("q", "quit", "Quit"),
|
||||
("ctrl+c", "quit", "Quit"),
|
||||
]
|
||||
@@ -402,6 +403,7 @@ class TuimbleApp(App):
|
||||
)
|
||||
self._audio.input_gain = acfg.input_gain
|
||||
self._audio.output_gain = acfg.output_gain
|
||||
self._pending_reload: Config | None = None
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header()
|
||||
@@ -602,10 +604,169 @@ class TuimbleApp(App):
|
||||
chatlog = self.query_one("#chatlog", ChatLog)
|
||||
chatlog.write(f"[dim]input volume {pct}%[/dim]")
|
||||
|
||||
# -- config reload --------------------------------------------------------
|
||||
|
||||
def _detect_config_changes(
|
||||
self, old: Config, new: Config,
|
||||
) -> tuple[list[str], list[str]]:
|
||||
"""Compare configs, return (safe_changes, restart_changes)."""
|
||||
safe: list[str] = []
|
||||
restart: list[str] = []
|
||||
|
||||
if old.ptt.key != new.ptt.key:
|
||||
safe.append(f"ptt key: {old.ptt.key} -> {new.ptt.key}")
|
||||
if old.ptt.mode != new.ptt.mode:
|
||||
safe.append(f"ptt mode: {old.ptt.mode} -> {new.ptt.mode}")
|
||||
if old.ptt.backend != new.ptt.backend:
|
||||
safe.append(
|
||||
f"ptt backend: {old.ptt.backend} -> {new.ptt.backend}"
|
||||
)
|
||||
if old.audio.input_gain != new.audio.input_gain:
|
||||
safe.append(
|
||||
f"input gain: {old.audio.input_gain} -> "
|
||||
f"{new.audio.input_gain}"
|
||||
)
|
||||
if old.audio.output_gain != new.audio.output_gain:
|
||||
safe.append(
|
||||
f"output gain: {old.audio.output_gain} -> "
|
||||
f"{new.audio.output_gain}"
|
||||
)
|
||||
|
||||
o_srv, n_srv = old.server, new.server
|
||||
for attr in ("host", "port", "username", "password",
|
||||
"certfile", "keyfile"):
|
||||
ov, nv = getattr(o_srv, attr), getattr(n_srv, attr)
|
||||
if ov != nv:
|
||||
label = "password" if attr == "password" else attr
|
||||
restart.append(f"server.{label} changed")
|
||||
|
||||
o_aud, n_aud = old.audio, new.audio
|
||||
for attr in ("input_device", "output_device", "sample_rate"):
|
||||
ov, nv = getattr(o_aud, attr), getattr(n_aud, attr)
|
||||
if ov != nv:
|
||||
restart.append(f"audio.{attr} changed")
|
||||
|
||||
return safe, restart
|
||||
|
||||
def _apply_safe_changes(self, new: Config) -> None:
|
||||
"""Apply hot-reload-safe config changes immediately."""
|
||||
self._config.ptt = new.ptt
|
||||
self._ptt = detect_backend(
|
||||
self._on_ptt_change, new.ptt.backend
|
||||
)
|
||||
|
||||
self._audio.input_gain = new.audio.input_gain
|
||||
self._audio.output_gain = new.audio.output_gain
|
||||
status = self.query_one("#status", StatusBar)
|
||||
status.input_vol = int(new.audio.input_gain * 100)
|
||||
status.output_vol = int(new.audio.output_gain * 100)
|
||||
|
||||
def _apply_restart_changes(self, new: Config) -> None:
|
||||
"""Apply changes that require reconnect/audio restart."""
|
||||
chatlog = self.query_one("#chatlog", ChatLog)
|
||||
old = self._config
|
||||
|
||||
server_changed = (
|
||||
old.server.host != new.server.host
|
||||
or old.server.port != new.server.port
|
||||
or old.server.username != new.server.username
|
||||
or old.server.password != new.server.password
|
||||
or old.server.certfile != new.server.certfile
|
||||
or old.server.keyfile != new.server.keyfile
|
||||
)
|
||||
audio_hw_changed = (
|
||||
old.audio.input_device != new.audio.input_device
|
||||
or old.audio.output_device != new.audio.output_device
|
||||
or old.audio.sample_rate != new.audio.sample_rate
|
||||
)
|
||||
|
||||
if server_changed:
|
||||
self._audio.stop()
|
||||
self._client.set_dispatcher(None)
|
||||
self._client.disconnect()
|
||||
tree = self.query_one("#sidebar", ChannelTree)
|
||||
tree.clear_state()
|
||||
|
||||
srv = new.server
|
||||
self._client = MumbleClient(
|
||||
host=srv.host,
|
||||
port=srv.port,
|
||||
username=srv.username,
|
||||
password=srv.password,
|
||||
certfile=srv.certfile,
|
||||
keyfile=srv.keyfile,
|
||||
)
|
||||
self._config = new
|
||||
chatlog.write(
|
||||
f"[dim]reconnecting to {srv.host}:{srv.port}...[/]"
|
||||
)
|
||||
self._connect_to_server()
|
||||
elif audio_hw_changed:
|
||||
self._audio.stop()
|
||||
acfg = new.audio
|
||||
self._audio = AudioPipeline(
|
||||
sample_rate=acfg.sample_rate,
|
||||
frame_size=acfg.frame_size,
|
||||
input_device=acfg.input_device,
|
||||
output_device=acfg.output_device,
|
||||
)
|
||||
self._audio.input_gain = acfg.input_gain
|
||||
self._audio.output_gain = acfg.output_gain
|
||||
self._config = new
|
||||
if self._client.connected:
|
||||
self._start_audio()
|
||||
chatlog.write("[dim]audio pipeline restarted[/dim]")
|
||||
|
||||
def action_reload_config(self) -> None:
|
||||
"""Reload config from disk (F5)."""
|
||||
chatlog = self.query_one("#chatlog", ChatLog)
|
||||
|
||||
if self._pending_reload is not None:
|
||||
new = self._pending_reload
|
||||
self._pending_reload = None
|
||||
self._apply_restart_changes(new)
|
||||
return
|
||||
|
||||
try:
|
||||
new = load_config()
|
||||
except Exception as exc:
|
||||
self._show_error(f"config reload: {exc}")
|
||||
return
|
||||
|
||||
safe, restart = self._detect_config_changes(self._config, new)
|
||||
|
||||
if not safe and not restart:
|
||||
chatlog.write("[dim]config unchanged[/dim]")
|
||||
return
|
||||
|
||||
self._apply_safe_changes(new)
|
||||
self._config.audio.input_gain = new.audio.input_gain
|
||||
self._config.audio.output_gain = new.audio.output_gain
|
||||
|
||||
if safe:
|
||||
for change in safe:
|
||||
chatlog.write(f"[dim]\u2713 {change}[/dim]")
|
||||
|
||||
if restart:
|
||||
for change in restart:
|
||||
chatlog.write(f"[#e0af68]\u26a0 {change}[/]")
|
||||
chatlog.write(
|
||||
"[dim]press F5 again to apply, "
|
||||
"or any key to cancel[/dim]"
|
||||
)
|
||||
self._pending_reload = new
|
||||
else:
|
||||
chatlog.write("[dim]\u2713 config reloaded[/dim]")
|
||||
|
||||
# -- PTT -----------------------------------------------------------------
|
||||
|
||||
def on_key(self, event: events.Key) -> None:
|
||||
"""Handle input history navigation and PTT key events."""
|
||||
if self._pending_reload is not None and event.key != "f5":
|
||||
self._pending_reload = None
|
||||
chatlog = self.query_one("#chatlog", ChatLog)
|
||||
chatlog.write("[dim]reload cancelled[/dim]")
|
||||
|
||||
focused = self.focused
|
||||
if isinstance(focused, Input) and event.key in ("up", "down"):
|
||||
inp = focused
|
||||
|
||||
Reference in New Issue
Block a user