app: add config reload on F5

This commit is contained in:
Username
2026-02-24 14:18:19 +01:00
parent e9726da401
commit 0bc41d1a46

View File

@@ -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