66from fabric .widgets .label import Label
77from fabric .widgets .revealer import Revealer
88from fabric .widgets .stack import Stack
9+ from fabric .audio .service import Audio
910from gi .repository import Gdk , GLib , Gtk , Pango
1011
1112import 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