diff --git a/sims/modules/bar.py b/sims/modules/bar.py index cfb4dc5..e161fde 100644 --- a/sims/modules/bar.py +++ b/sims/modules/bar.py @@ -11,6 +11,7 @@ from sims.modules.notmuch import NotmuchWidget from sims.modules.buddy import Buddy from sims.modules.org import OrgDropdown, OrgWidget from sims.modules.screenrec import ScreenrecWidget +from sims.modules.microphone import MicrophoneWidget from fabric.widgets.wayland import WaylandWindow as Window from fabric.system_tray.widgets import SystemTray from sims.widgets.fenster import FensterWorkspaces, FensterActiveWindow @@ -110,11 +111,13 @@ class StatusBar(Window): if org_service is not None: self.org = OrgWidget(service=org_service, dropdown=org_dropdown) + self.microphone = MicrophoneWidget() + self.status_container = Box( name="widgets-container", spacing=4, orientation="h", - children=self.progress_bars_overlay, + children=[self.microphone, self.progress_bars_overlay], ) end_container_children = [] diff --git a/sims/modules/microphone.py b/sims/modules/microphone.py new file mode 100644 index 0000000..5df9208 --- /dev/null +++ b/sims/modules/microphone.py @@ -0,0 +1,68 @@ +from fabric.audio.service import Audio +from fabric.widgets.box import Box +from fabric.widgets.button import Button +from fabric.widgets.label import Label + +MIC_ICON = "󰍬" +MIC_MUTED_ICON = "󰍭" + + +class MicrophoneWidget(Box): + """Bar widget showing microphone state; click to toggle mute. + + Mutes *all* input sources (not just the default one) so virtual + sources like EasyEffects and their underlying hardware mics are + silenced together. + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.audio = Audio() + + self.icon_label = Label( + name="microphone-icon", + label=MIC_ICON, + ) + self.button = Button( + name="microphone-button", + child=self.icon_label, + on_clicked=self.on_clicked, + style="background: transparent; border: none; margin: 0; box-shadow: none;", + ) + self.add(self.button) + + # `changed` fires for any stream change (incl. mute state) and + # `microphone-changed` when the default source switches. + self.audio.connect("changed", self.update_icon) + self.audio.connect("microphone-changed", self.update_icon) + self.update_icon() + + def on_clicked(self, *_): + mics = self.audio.microphones or [] + if not mics and self.audio.microphone: + mics = [self.audio.microphone] + if not mics: + return + # if anything is live -> mute everything; else unmute everything + mute = self._any_unmuted(mics) + for mic in mics: + mic.muted = mute + self.update_icon() + + def _any_unmuted(self, mics) -> bool: + return any(not mic.muted for mic in mics) + + def update_icon(self, *_): + mics = self.audio.microphones or [] + if not mics and self.audio.microphone: + mics = [self.audio.microphone] + if not mics: + self.set_visible(False) + return + self.set_visible(True) + if self._any_unmuted(mics): + self.icon_label.set_label(MIC_ICON) + self.icon_label.remove_style_class("muted") + else: + self.icon_label.set_label(MIC_MUTED_ICON) + self.icon_label.add_style_class("muted") diff --git a/sims/styles/bar.css b/sims/styles/bar.css index b0d19cf..d7601b4 100644 --- a/sims/styles/bar.css +++ b/sims/styles/bar.css @@ -53,6 +53,20 @@ font-weight: bold; } +#microphone-icon { + color: var(--foreground); + font-size: 20px; +} + +#microphone-button { + min-width: 28px; + padding: 0 6px; +} + +#microphone-icon.muted { + color: var(--red); +} + #cpu-progress-bar, #ram-progress-bar, #volume-progress-bar {