app: add slash commands

This commit is contained in:
Username
2026-02-28 14:11:51 +01:00
parent 15fbf0040a
commit 4bf8adc5e2
2 changed files with 94 additions and 4 deletions

View File

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

View File

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