Skip to content

Commit 72e593b

Browse files
committed
Merge branch 'vol' into dev
2 parents bbb9913 + b361eaf commit 72e593b

File tree

2 files changed

+467
-1
lines changed

2 files changed

+467
-1
lines changed

modules/notch.py

Lines changed: 349 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from fabric.widgets.label import Label
77
from fabric.widgets.revealer import Revealer
88
from fabric.widgets.stack import Stack
9+
from fabric.audio.service import Audio
910
from gi.repository import Gdk, GLib, Gtk, Pango
1011

1112
import config.data as data
@@ -139,6 +140,11 @@ def __init__(self, monitor_id: int = 0, **kwargs):
139140
monitor=monitor_id,
140141
)
141142

143+
# Audio display variables
144+
self.VOLUME_DISPLAY_DURATION = 2000
145+
self._current_display_timeout_id = None
146+
self._suppress_first_audio_display = True
147+
142148
self._typed_chars_buffer = ""
143149
self._launcher_transitioning = False
144150
self._launcher_transition_timeout = None
@@ -171,6 +177,78 @@ def __init__(self, monitor_id: int = 0, **kwargs):
171177
self.tmux = TmuxManager(notch=self)
172178
self.cliphist = ClipHistory(notch=self)
173179

180+
# Audio service initialization
181+
self.audio = Audio()
182+
183+
# Volume display widgets
184+
self.volume_icon = Image(
185+
name="volume-display-icon",
186+
icon_name="audio-volume-high-symbolic",
187+
icon_size=16
188+
)
189+
self.volume_icon.set_valign(Gtk.Align.CENTER)
190+
191+
self.volume_label = Label(
192+
name="volume-display-label",
193+
label="..."
194+
)
195+
self.volume_label.set_valign(Gtk.Align.CENTER)
196+
197+
self.volume_bar = Gtk.ProgressBar(
198+
name="volume-display-bar"
199+
)
200+
self.volume_bar.set_fraction(1.0)
201+
self.volume_bar.set_show_text(False)
202+
self.volume_bar.set_hexpand(False)
203+
self.volume_bar.set_valign(Gtk.Align.CENTER)
204+
205+
self.volume_box = Box(
206+
name="volume-display-box",
207+
orientation="h",
208+
spacing=8,
209+
h_align="center",
210+
v_align="center",
211+
children=[
212+
self.volume_icon,
213+
self.volume_bar,
214+
self.volume_label
215+
]
216+
)
217+
218+
# Microphone display widgets
219+
self.mic_icon = Image(
220+
name="mic-display-icon",
221+
icon_name="microphone-sensitivity-high-symbolic",
222+
icon_size=16
223+
)
224+
self.mic_icon.set_valign(Gtk.Align.CENTER)
225+
226+
self.mic_label = Label(
227+
name="mic-display-label",
228+
label="..."
229+
)
230+
self.mic_label.set_valign(Gtk.Align.CENTER)
231+
232+
self.mic_bar = Gtk.ProgressBar(
233+
name="mic-display-bar"
234+
)
235+
self.mic_bar.set_fraction(1.0)
236+
self.mic_bar.set_show_text(False)
237+
self.mic_bar.set_valign(Gtk.Align.CENTER)
238+
239+
self.mic_box = Box(
240+
name="mic-display-box",
241+
orientation="h",
242+
spacing=8,
243+
h_align="center",
244+
v_align="center",
245+
children=[
246+
self.mic_icon,
247+
self.mic_bar,
248+
self.mic_label
249+
]
250+
)
251+
174252
self.window_label = Label(
175253
name="notch-window-label",
176254
h_expand=True,
@@ -240,6 +318,9 @@ def __init__(self, monitor_id: int = 0, **kwargs):
240318
self.user_label,
241319
self.active_window_box,
242320
self.player_small,
321+
# Add audio display widgets to compact stack
322+
self.volume_box,
323+
self.mic_box,
243324
],
244325
)
245326
self.compact_stack.set_visible_child(self.active_window_box)
@@ -406,6 +487,9 @@ def __init__(self, monitor_id: int = 0, **kwargs):
406487
self.add(self.notch_wrap)
407488
self.show_all()
408489

490+
# Connect audio signals after a short delay
491+
GLib.timeout_add(100, self._connect_audio_signals)
492+
409493
self.add_keybinding("Escape", lambda *_: self.close_notch())
410494
self.add_keybinding("Ctrl Tab", lambda *_: self.dashboard.go_to_next_child())
411495
self.add_keybinding(
@@ -435,6 +519,270 @@ def __init__(self, monitor_id: int = 0, **kwargs):
435519

436520
self.connect("key-press-event", self.on_key_press)
437521

522+
# Audio-related methods
523+
def _connect_audio_signals(self, retry_count=0):
524+
max_retries = 5
525+
526+
try:
527+
if self.audio:
528+
self.audio.connect("notify::speaker", self._on_speaker_changed)
529+
self.audio.connect("notify::microphone", self._on_microphone_changed)
530+
531+
if self.audio.speaker:
532+
self.audio.speaker.connect("changed", self._on_speaker_changed_signal)
533+
GLib.idle_add(self._update_volume_widgets_silently)
534+
535+
if self.audio.microphone:
536+
self.audio.microphone.connect("changed", self._on_microphone_changed_signal)
537+
GLib.idle_add(self._update_mic_widgets_silently)
538+
539+
GLib.timeout_add(500, self._enable_audio_display)
540+
return False
541+
542+
except Exception as e:
543+
print(f"Audio connection error (attempt {retry_count + 1}): {e}")
544+
545+
if retry_count < max_retries - 1:
546+
GLib.timeout_add(1000, lambda: self._connect_audio_signals(retry_count + 1))
547+
548+
return False
549+
550+
def _on_speaker_changed(self, audio_service, speaker):
551+
if self.audio.speaker:
552+
try:
553+
self.audio.speaker.disconnect_by_func(self._on_speaker_changed_signal)
554+
except:
555+
pass
556+
self.audio.speaker.connect("changed", self._on_speaker_changed_signal)
557+
self._update_volume_widgets_silently()
558+
559+
def _on_microphone_changed(self, audio_service, microphone):
560+
if self.audio.microphone:
561+
try:
562+
self.audio.microphone.disconnect_by_func(self._on_microphone_changed_signal)
563+
except:
564+
pass
565+
self.audio.microphone.connect("changed", self._on_microphone_changed_signal)
566+
self._update_mic_widgets_silently()
567+
568+
def _on_speaker_changed_signal(self, speaker, *args):
569+
self._handle_speaker_change()
570+
571+
def _on_microphone_changed_signal(self, microphone, *args):
572+
self._handle_microphone_change()
573+
574+
def _handle_speaker_change(self):
575+
if not self.audio or not self.audio.speaker:
576+
return
577+
578+
if self._suppress_first_audio_display:
579+
self._update_volume_widgets_silently()
580+
return
581+
582+
speaker = self.audio.speaker
583+
volume = speaker.volume
584+
is_muted = speaker.muted
585+
586+
volume_int = int(round(volume))
587+
volume_percentage = volume_int / 100.0
588+
self.volume_bar.set_fraction(volume_percentage)
589+
590+
self._update_volume_appearance(volume_int, is_muted)
591+
592+
if is_muted:
593+
self.volume_icon.set_from_icon_name("audio-volume-muted-symbolic", 16)
594+
self.volume_label.set_text("Muted")
595+
elif volume_int == 0:
596+
self.volume_icon.set_from_icon_name("audio-volume-muted-symbolic", 16)
597+
self.volume_label.set_text("Muted")
598+
else:
599+
if volume_int <= 33:
600+
icon_name = "audio-volume-low-symbolic"
601+
elif volume_int <= 66:
602+
icon_name = "audio-volume-medium-symbolic"
603+
else:
604+
icon_name = "audio-volume-high-symbolic"
605+
606+
self.volume_icon.set_from_icon_name(icon_name, 16)
607+
self.volume_label.set_text(f"{volume_int}%")
608+
609+
if not self._is_notch_open:
610+
self.show_volume_display()
611+
612+
def _handle_microphone_change(self):
613+
if not self.audio or not self.audio.microphone:
614+
return
615+
616+
if self._suppress_first_audio_display:
617+
self._update_mic_widgets_silently()
618+
return
619+
620+
microphone = self.audio.microphone
621+
volume = microphone.volume
622+
is_muted = microphone.muted
623+
624+
volume_int = int(round(volume))
625+
volume_percentage = volume_int / 100.0
626+
self.mic_bar.set_fraction(volume_percentage)
627+
628+
self._update_mic_appearance(volume_int, is_muted)
629+
630+
if is_muted:
631+
self.mic_icon.set_from_icon_name("microphone-disabled-symbolic", 16)
632+
self.mic_label.set_text("Muted")
633+
else:
634+
self.mic_icon.set_from_icon_name("microphone-sensitivity-high-symbolic", 16)
635+
self.mic_label.set_text(f"{volume_int}%")
636+
637+
if not self._is_notch_open:
638+
self.show_mic_display()
639+
640+
def _enable_audio_display(self):
641+
self._suppress_first_audio_display = False
642+
return False
643+
644+
def _update_volume_appearance(self, volume_int, is_muted):
645+
volume_box_style = self.volume_box.get_style_context()
646+
volume_icon_style = self.volume_icon.get_style_context()
647+
volume_bar_style = self.volume_bar.get_style_context()
648+
649+
for cls in ["volume-muted", "volume-low", "volume-medium", "volume-high"]:
650+
volume_box_style.remove_class(cls)
651+
volume_icon_style.remove_class(cls)
652+
volume_bar_style.remove_class(cls)
653+
654+
if is_muted or volume_int == 0:
655+
volume_box_style.add_class("volume-muted")
656+
volume_icon_style.add_class("volume-muted")
657+
volume_bar_style.add_class("volume-muted")
658+
elif volume_int <= 33:
659+
volume_box_style.add_class("volume-low")
660+
volume_icon_style.add_class("volume-low")
661+
volume_bar_style.add_class("volume-low")
662+
elif volume_int <= 66:
663+
volume_box_style.add_class("volume-medium")
664+
volume_icon_style.add_class("volume-medium")
665+
volume_bar_style.add_class("volume-medium")
666+
else:
667+
volume_box_style.add_class("volume-high")
668+
volume_icon_style.add_class("volume-high")
669+
volume_bar_style.add_class("volume-high")
670+
671+
def _update_mic_appearance(self, volume_int, is_muted):
672+
mic_box_style = self.mic_box.get_style_context()
673+
mic_icon_style = self.mic_icon.get_style_context()
674+
mic_bar_style = self.mic_bar.get_style_context()
675+
676+
for cls in ["mic-muted", "mic-low", "mic-medium", "mic-high"]:
677+
mic_box_style.remove_class(cls)
678+
mic_icon_style.remove_class(cls)
679+
mic_bar_style.remove_class(cls)
680+
681+
if is_muted:
682+
mic_box_style.add_class("mic-muted")
683+
mic_icon_style.add_class("mic-muted")
684+
mic_bar_style.add_class("mic-muted")
685+
elif volume_int <= 33:
686+
mic_box_style.add_class("mic-low")
687+
mic_icon_style.add_class("mic-low")
688+
mic_bar_style.add_class("mic-low")
689+
elif volume_int <= 66:
690+
mic_box_style.add_class("mic-medium")
691+
mic_icon_style.add_class("mic-medium")
692+
mic_bar_style.add_class("mic-medium")
693+
else:
694+
mic_box_style.add_class("mic-high")
695+
mic_icon_style.add_class("mic-high")
696+
mic_bar_style.add_class("mic-high")
697+
698+
def _update_volume_widgets_silently(self):
699+
if not self.audio or not self.audio.speaker:
700+
return
701+
702+
speaker = self.audio.speaker
703+
volume = speaker.volume
704+
is_muted = speaker.muted
705+
706+
volume_int = int(round(volume))
707+
volume_percentage = volume_int / 100.0
708+
self.volume_bar.set_fraction(volume_percentage)
709+
710+
self._update_volume_appearance(volume_int, is_muted)
711+
712+
if is_muted:
713+
self.volume_icon.set_from_icon_name("audio-volume-muted-symbolic", 16)
714+
self.volume_label.set_text("Muted")
715+
elif volume_int == 0:
716+
self.volume_icon.set_from_icon_name("audio-volume-muted-symbolic", 16)
717+
self.volume_label.set_text("Muted")
718+
else:
719+
if volume_int <= 33:
720+
icon_name = "audio-volume-low-symbolic"
721+
elif volume_int <= 66:
722+
icon_name = "audio-volume-medium-symbolic"
723+
else:
724+
icon_name = "audio-volume-high-symbolic"
725+
726+
self.volume_icon.set_from_icon_name(icon_name, 16)
727+
self.volume_label.set_text(f"{volume_int}%")
728+
729+
def _update_mic_widgets_silently(self):
730+
if not self.audio or not self.audio.microphone:
731+
return
732+
733+
microphone = self.audio.microphone
734+
volume = microphone.volume
735+
is_muted = microphone.muted
736+
737+
volume_int = int(round(volume))
738+
volume_percentage = volume_int / 100.0
739+
self.mic_bar.set_fraction(volume_percentage)
740+
741+
self._update_mic_appearance(volume_int, is_muted)
742+
743+
if is_muted:
744+
self.mic_icon.set_from_icon_name("microphone-disabled-symbolic", 16)
745+
self.mic_label.set_text(" Muted")
746+
else:
747+
self.mic_icon.set_from_icon_name("microphone-sensitivity-high-symbolic", 16)
748+
self.mic_label.set_text(f" {volume_int}%")
749+
750+
def show_volume_display(self):
751+
if self._is_notch_open:
752+
return
753+
754+
if self._current_display_timeout_id:
755+
GLib.source_remove(self._current_display_timeout_id)
756+
757+
self.compact_stack.set_visible_child(self.volume_box)
758+
self._current_display_timeout_id = GLib.timeout_add(
759+
self.VOLUME_DISPLAY_DURATION,
760+
self.return_to_normal_view
761+
)
762+
763+
def show_mic_display(self):
764+
if self._is_notch_open:
765+
return
766+
767+
if self._current_display_timeout_id:
768+
GLib.source_remove(self._current_display_timeout_id)
769+
770+
self.compact_stack.set_visible_child(self.mic_box)
771+
self._current_display_timeout_id = GLib.timeout_add(
772+
self.VOLUME_DISPLAY_DURATION,
773+
self.return_to_normal_view
774+
)
775+
776+
def return_to_normal_view(self):
777+
self._current_display_timeout_id = None
778+
779+
if not self._is_notch_open:
780+
current_child = self.compact_stack.get_visible_child()
781+
if current_child in [self.volume_box, self.mic_box]:
782+
self.compact_stack.set_visible_child(self.active_window_box)
783+
784+
return False
785+
438786
def on_button_enter(self, widget, event):
439787
self.is_hovered = True
440788
window = widget.get_window()
@@ -1096,4 +1444,4 @@ def on_key_press(self, widget, event):
10961444
self.open_launcher_with_text(keychar)
10971445
return True
10981446

1099-
return False
1447+
return False

0 commit comments

Comments
 (0)