From e9726da40104d7f2b07a7563e823684d162704e5 Mon Sep 17 00:00:00 2001 From: Username Date: Tue, 24 Feb 2026 14:17:02 +0100 Subject: [PATCH] app: add volume key bindings and status display --- src/tuimble/app.py | 54 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/src/tuimble/app.py b/src/tuimble/app.py index 6bc67f3..b1e5ff8 100644 --- a/src/tuimble/app.py +++ b/src/tuimble/app.py @@ -20,6 +20,16 @@ from tuimble.ptt import KittyPtt, TogglePtt, detect_backend log = logging.getLogger(__name__) +VOLUME_STEPS = (0.0, 0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 2.0) + + +def _next_volume(current: float) -> float: + """Cycle through VOLUME_STEPS, wrapping to 0.0 after max.""" + for step in VOLUME_STEPS: + if step > current + 0.01: + return step + return VOLUME_STEPS[0] + # -- custom messages (pymumble thread -> Textual) ---------------------------- @@ -63,6 +73,14 @@ class StatusBar(Static): connected = reactive(False) self_deaf = reactive(False) server_info = reactive("") + output_vol = reactive(100) + input_vol = reactive(100) + + @staticmethod + def _vol_bar(pct: int) -> str: + """Compact 4-char volume indicator using block chars.""" + filled = round(pct / 25) + return "\u2588" * filled + "\u2591" * (4 - filled) def render(self) -> str: w = self.content_size.width if self.content_size.width > 0 else 80 @@ -88,8 +106,14 @@ class StatusBar(Static): return f" {conn_sym} {deaf_sym}{ptt_sym}" if w < 60: return f" {conn_full} {deaf_full}{' ' if deaf_full else ''}{ptt_full}" + + vol = ( + f" [dim]out[/]{self._vol_bar(self.output_vol)}" + f" [dim]in[/]{self._vol_bar(self.input_vol)}" + ) info = f" [dim]{self.server_info}[/]" if self.server_info else "" - return f" {conn_full} {deaf_full}{' ' if deaf_full else ''}{ptt_full}{info}" + deaf = f"{deaf_full} " if deaf_full else "" + return f" {conn_full} {deaf}{ptt_full}{vol}{info}" class ChannelTree(Static): @@ -345,6 +369,8 @@ class TuimbleApp(App): BINDINGS = [ ("f1", "toggle_deaf", "Deafen"), + ("f2", "cycle_output_volume", "Vol Out"), + ("f3", "cycle_input_volume", "Vol In"), ("q", "quit", "Quit"), ("ctrl+c", "quit", "Quit"), ] @@ -388,6 +414,10 @@ class TuimbleApp(App): yield Footer() def on_mount(self) -> None: + status = self.query_one("#status", StatusBar) + status.output_vol = int(self._audio.output_gain * 100) + status.input_vol = int(self._audio.input_gain * 100) + chatlog = self.query_one("#chatlog", ChatLog) chatlog.write("[dim]tuimble v0.1.0[/dim]") srv = self._config.server @@ -550,6 +580,28 @@ class TuimbleApp(App): else: chatlog.write("[#9ece6a]\u2713 undeafened[/]") + # -- volume --------------------------------------------------------------- + + def action_cycle_output_volume(self) -> None: + """Cycle output volume through preset steps.""" + vol = _next_volume(self._audio.output_gain) + self._audio.output_gain = vol + pct = int(vol * 100) + status = self.query_one("#status", StatusBar) + status.output_vol = pct + chatlog = self.query_one("#chatlog", ChatLog) + chatlog.write(f"[dim]output volume {pct}%[/dim]") + + def action_cycle_input_volume(self) -> None: + """Cycle input volume through preset steps.""" + vol = _next_volume(self._audio.input_gain) + self._audio.input_gain = vol + pct = int(vol * 100) + status = self.query_one("#status", StatusBar) + status.input_vol = pct + chatlog = self.query_one("#chatlog", ChatLog) + chatlog.write(f"[dim]input volume {pct}%[/dim]") + # -- PTT ----------------------------------------------------------------- def on_key(self, event: events.Key) -> None: