diff --git a/src/tuimble/app.py b/src/tuimble/app.py index 0fbdfdc..cf76a44 100644 --- a/src/tuimble/app.py +++ b/src/tuimble/app.py @@ -115,6 +115,7 @@ class StatusBar(Static): connected = reactive(False) reconnecting = reactive(False) self_deaf = reactive(False) + self_mute = reactive(False) server_info = reactive("") output_vol = reactive(100) input_vol = reactive(100) @@ -148,11 +149,14 @@ class StatusBar(Static): deaf_sym = "[#f7768e]\u2298[/]" if self.self_deaf else "" deaf_full = "[#f7768e]\u2298[/] deaf" if self.self_deaf else "" + mute_sym = "[#e0af68]\u2715[/]" if self.self_mute else "" + mute_full = "[#e0af68]\u2715[/] mute" if self.self_mute else "" if w < 40: - return f" {conn_sym} {deaf_sym}{ptt_sym}" + return f" {conn_sym} {deaf_sym}{mute_sym}{ptt_sym}" if w < 60: - return f" {conn_full} {deaf_full}{' ' if deaf_full else ''}{ptt_full}" + flags = f"{deaf_full}{' ' if deaf_full else ''}{mute_full}{' ' if mute_full else ''}" + return f" {conn_full} {flags}{ptt_full}" vol = ( f" [dim]out[/]{self._vol_bar(self.output_vol)}" @@ -164,8 +168,8 @@ class StatusBar(Static): else: pitch_str = "" info = f" [dim]{self.server_info}[/]" if self.server_info else "" - deaf = f"{deaf_full} " if deaf_full else "" - return f" {conn_full} {deaf}{ptt_full}{vol}{pitch_str}{info}" + flags = f"{deaf_full}{' ' if deaf_full else ''}{mute_full}{' ' if mute_full else ''}" + return f" {conn_full} {flags}{ptt_full}{vol}{pitch_str}{info}" class ChannelTree(Static): @@ -450,6 +454,7 @@ class TuimbleApp(App): on_failure=self._reconnect_on_failure, on_exhausted=self._reconnect_on_exhausted, ) + self._muted: bool = False self._intentional_disconnect: bool = False def _make_client(self, srv=None) -> MumbleClient: @@ -585,10 +590,12 @@ class TuimbleApp(App): def on_server_connected(self, _msg: ServerConnected) -> None: self._intentional_disconnect = False + self._muted = False status = self.query_one("#status", StatusBar) status.reconnecting = False status.connected = True + status.self_mute = False srv = self._config.server status.server_info = f"{srv.host}:{srv.port}" @@ -651,6 +658,10 @@ class TuimbleApp(App): event.input.clear() self._history.push(text) + if text.startswith("/"): + self._dispatch_command(text) + return + if not self._client.connected: self._show_error("not connected") return @@ -659,6 +670,50 @@ class TuimbleApp(App): chatlog = self.query_one("#chatlog", ChatLog) chatlog.write(f"[#e0af68]{self._config.server.username}[/] {text}") + def _dispatch_command(self, text: str) -> None: + """Handle slash commands.""" + cmd = text.split()[0].lower() + chatlog = self.query_one("#chatlog", ChatLog) + + if cmd == "/help": + chatlog.write("[dim]/deafen toggle self-deafen[/dim]") + chatlog.write("[dim]/mute toggle self-mute[/dim]") + chatlog.write("[dim]/unmute unmute yourself[/dim]") + chatlog.write("[dim]/register register on server[/dim]") + elif cmd == "/deafen": + self.action_toggle_deaf() + elif cmd == "/mute": + if self._muted: + chatlog.write("[dim]already muted[/dim]") + return + self._muted = True + self._audio.capturing = False + self._client.set_self_mute(True) + status = self.query_one("#status", StatusBar) + status.self_mute = True + status.ptt_active = False + chatlog.write("[#e0af68]\u2715 muted[/]") + elif cmd == "/unmute": + if not self._muted: + chatlog.write("[dim]already unmuted[/dim]") + return + self._muted = False + self._client.set_self_mute(False) + status = self.query_one("#status", StatusBar) + status.self_mute = False + chatlog.write("[#9ece6a]\u2713 unmuted[/]") + elif cmd == "/register": + if not self._client.connected: + self._show_error("not connected") + return + try: + self._client.register_self() + chatlog.write("[#9ece6a]\u2713 registration requested[/]") + except Exception as exc: + self._show_error(f"register failed: {exc}") + else: + self._show_error(f"unknown command: {cmd}") + # -- audio --------------------------------------------------------------- def _on_device_change(self) -> None: @@ -978,6 +1033,8 @@ class TuimbleApp(App): self._ptt.key_down() def _on_ptt_change(self, transmitting: bool) -> None: + if self._muted: + transmitting = False self._audio.capturing = transmitting status = self.query_one("#status", StatusBar) status.ptt_active = transmitting diff --git a/tests/test_widgets.py b/tests/test_widgets.py index 9eb9e53..24dffe3 100644 --- a/tests/test_widgets.py +++ b/tests/test_widgets.py @@ -256,6 +256,39 @@ async def test_statusbar_deaf(): assert "\u2298" in rendered +@pytest.mark.asyncio +async def test_statusbar_muted(): + app = StatusBarApp() + async with app.run_test(size=(80, 5)) as _pilot: + bar = app.query_one("#status", StatusBar) + bar.self_mute = True + rendered = bar.render() + assert "\u2715" in rendered + + +@pytest.mark.asyncio +async def test_statusbar_muted_compact(): + app = StatusBarApp() + async with app.run_test(size=(30, 5)) as pilot: + bar = app.query_one("#status", StatusBar) + bar.self_mute = True + await pilot.resize_terminal(30, 5) + await pilot.pause() + rendered = bar.render() + assert "\u2715" in rendered + + +@pytest.mark.asyncio +async def test_statusbar_muted_medium(): + app = StatusBarApp() + async with app.run_test(size=(50, 5)) as _pilot: + bar = app.query_one("#status", StatusBar) + bar.self_mute = True + rendered = bar.render() + assert "\u2715" in rendered + assert "mute" in rendered + + @pytest.mark.asyncio async def test_statusbar_reconnecting(): app = StatusBarApp()