diff --git a/bar/bar.py b/bar/bar.py index cd9df43..76f764a 100644 --- a/bar/bar.py +++ b/bar/bar.py @@ -1,185 +1,56 @@ # fabric bar.py example # https://github.com/Fabric-Development/fabric/blob/rewrite/examples/bar/bar.py -import psutil +from loguru import logger + from fabric import Application -from fabric.widgets.box import Box -from fabric.widgets.label import Label -from fabric.widgets.overlay import Overlay -from fabric.widgets.eventbox import EventBox -from fabric.widgets.datetime import DateTime -from fabric.widgets.centerbox import CenterBox from fabric.system_tray.widgets import SystemTray -from fabric.widgets.circularprogressbar import CircularProgressBar from fabric.widgets.wayland import WaylandWindow as Window -from .river.widgets import RiverWorkspaces, RiverWorkspaceButton, RiverActiveWindow +from .river.widgets import ( + get_river_connection, +) from fabric.utils import ( - FormattedString, - bulk_replace, - invoke_repeater, get_relative_path, ) -from bar.modules.player import Player -from bar.modules.vinyl import VinylButton - -AUDIO_WIDGET = True - -if AUDIO_WIDGET is True: - try: - from fabric.audio.service import Audio - except Exception as e: - print(e) - AUDIO_WIDGET = False - - -class VolumeWidget(Box): - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.audio = Audio() - - self.progress_bar = CircularProgressBar( - name="volume-progress-bar", pie=True, size=24 - ) - - self.event_box = EventBox( - events="scroll", - child=Overlay( - child=self.progress_bar, - overlays=Label( - label="", - style="margin: 0px 6px 0px 0px; font-size: 12px", # to center the icon glyph - ), - ), - ) - - self.audio.connect("notify::speaker", self.on_speaker_changed) - self.event_box.connect("scroll-event", self.on_scroll) - self.add(self.event_box) - - def on_scroll(self, _, event): - match event.direction: - case 0: - self.audio.speaker.volume += 8 - case 1: - self.audio.speaker.volume -= 8 - return - - def on_speaker_changed(self, *_): - if not self.audio.speaker: - return - self.progress_bar.value = self.audio.speaker.volume / 100 - self.audio.speaker.bind( - "volume", "value", self.progress_bar, lambda _, v: v / 100 - ) - return - - -class StatusBar(Window): - def __init__(self, display: int, monitor: int = 1, with_system_tray: bool = False): - super().__init__( - name="bar", - layer="top", - anchor="left top right", - margin="0px 0px -2px 0px", - exclusivity="auto", - visible=False, - all_visible=False, - monitor=monitor, - ) - self.workspaces = RiverWorkspaces( - display, - name="workspaces", - spacing=4, - buttons_factory=lambda ws_id: RiverWorkspaceButton(id=ws_id, label=None), - ) - self.date_time = DateTime(name="date-time", formatters="%d %b - %H:%M") - self.system_tray = None - if with_system_tray: - self.system_tray = SystemTray(name="system-tray", spacing=4) - - self.active_window = RiverActiveWindow( - name="active-window", - max_length=50, - style="color: #ffffff; font-size: 14px; font-weight: bold;", - ) - - self.ram_progress_bar = CircularProgressBar( - name="ram-progress-bar", pie=True, size=24 - ) - self.cpu_progress_bar = CircularProgressBar( - name="cpu-progress-bar", pie=True, size=24 - ) - self.progress_bars_overlay = Overlay( - child=self.ram_progress_bar, - overlays=[ - self.cpu_progress_bar, - Label("", style="margin: 0px 6px 0px 0px; font-size: 12px"), - ], - ) - self.player = Player() - self.vinyl = VinylButton() - - self.status_container = Box( - name="widgets-container", - spacing=4, - orientation="h", - children=self.progress_bars_overlay, - ) - self.status_container.add(VolumeWidget()) if AUDIO_WIDGET is True else None - - end_container_children = [ - self.vinyl, - self.status_container, - self.date_time, - ] - - if self.system_tray is not None: - end_container_children = [ - self.vinyl, - self.status_container, - self.system_tray, - self.date_time, - ] - self.children = CenterBox( - name="bar-inner", - start_children=Box( - name="start-container", - spacing=6, - orientation="h", - children=[ - Label(name="nixos-label", markup=""), - self.workspaces, - ], - ), - center_children=Box( - name="center-container", - spacing=4, - orientation="h", - children=[self.active_window], - ), - end_children=Box( - name="end-container", - spacing=4, - orientation="h", - children=end_container_children, - ), - ) - - invoke_repeater(1000, self.update_progress_bars) - - self.show_all() - - def update_progress_bars(self): - self.ram_progress_bar.value = psutil.virtual_memory().percent / 100 - self.cpu_progress_bar.value = psutil.cpu_percent() / 100 - return True +from .modules.bar import StatusBar def main(): - bar = StatusBar(45) - bar_two = StatusBar(44, monitor=2, with_system_tray=True) - app = Application("bar", bar, bar_two) - app.set_stylesheet_from_file(get_relative_path("bar.css")) + tray = SystemTray(name="system-tray", spacing=4) + river = get_river_connection() + + # Dummy window just to hold the event loop + dummy = Window(visible=False) + + # Real bar windows will be added later + bar_windows = [] + + def spawn_bars(): + logger.info("[Bar] Spawning bars after river ready") + outputs = river.outputs + + if not outputs: + logger.warning("[Bar] No outputs found — skipping bar spawn") + return + + output_ids = sorted(outputs.keys()) + + for i, output_id in enumerate(output_ids): + print("i", i) + print("output_id", output_id) + bar = StatusBar(display=output_id, tray=tray if i == 0 else None, monitor=i) + bar_windows.append(bar) + + return False + + if river.ready: + print("river ready", river._ready) + spawn_bars() + else: + river.connect("notify::ready", lambda sender, pspec: spawn_bars()) + + app = Application("bar", dummy) + app.set_stylesheet_from_file(get_relative_path("bar.css")) app.run() diff --git a/bar/modules/bar.py b/bar/modules/bar.py new file mode 100644 index 0000000..be6e14b --- /dev/null +++ b/bar/modules/bar.py @@ -0,0 +1,128 @@ +import psutil +from fabric.widgets.box import Box +from fabric.widgets.label import Label +from fabric.widgets.overlay import Overlay +from fabric.widgets.datetime import DateTime +from fabric.widgets.centerbox import CenterBox +from bar.modules.player import Player +from bar.modules.vinyl import VinylButton +from fabric.widgets.wayland import WaylandWindow as Window +from fabric.system_tray.widgets import SystemTray +from ..river.widgets import ( + RiverWorkspaces, + RiverWorkspaceButton, + RiverActiveWindow, + get_river_connection, +) +from fabric.utils import ( + invoke_repeater, +) +from fabric.widgets.circularprogressbar import CircularProgressBar + + +class StatusBar(Window): + def __init__( + self, + display: int, + tray: SystemTray | None = None, + monitor: int = 1, + river_service=None, + ): + super().__init__( + name="bar", + layer="top", + anchor="left top right", + margin="0px 0px -2px 0px", + exclusivity="auto", + visible=False, + all_visible=False, + monitor=monitor, + ) + if river_service: + self.river = river_service + else: + self.river = get_river_connection() + + self.workspaces = RiverWorkspaces( + display, + name="workspaces", + spacing=4, + buttons_factory=lambda ws_id: RiverWorkspaceButton(id=ws_id, label=None), + river_service=self.river, + ) + self.date_time = DateTime(name="date-time", formatters="%d %b - %H:%M") + self.system_tray = tray + + self.active_window = RiverActiveWindow( + name="active-window", + max_length=50, + style="color: #ffffff; font-size: 14px; font-weight: bold;", + ) + + self.ram_progress_bar = CircularProgressBar( + name="ram-progress-bar", pie=True, size=24 + ) + self.cpu_progress_bar = CircularProgressBar( + name="cpu-progress-bar", pie=True, size=24 + ) + + self.progress_label = Label( + "", style="margin: 0px 6px 0px 0px; font-size: 12px" + ) + self.progress_bars_overlay = Overlay( + child=self.ram_progress_bar, + overlays=[self.cpu_progress_bar, self.progress_label], + ) + self.player = Player() + self.vinyl = VinylButton() + + self.status_container = Box( + name="widgets-container", + spacing=4, + orientation="h", + children=self.progress_bars_overlay, + ) + + end_container_children = [ + self.vinyl, + self.status_container, + ] + + if self.system_tray: + end_container_children.append(self.system_tray) + + end_container_children.append(self.date_time) + + self.children = CenterBox( + name="bar-inner", + start_children=Box( + name="start-container", + spacing=6, + orientation="h", + children=[ + Label(name="nixos-label", markup=""), + self.workspaces, + ], + ), + center_children=Box( + name="center-container", + spacing=4, + orientation="h", + children=[self.active_window], + ), + end_children=Box( + name="end-container", + spacing=4, + orientation="h", + children=end_container_children, + ), + ) + + invoke_repeater(1000, self.update_progress_bars) + + self.show_all() + + def update_progress_bars(self): + self.ram_progress_bar.value = psutil.virtual_memory().percent / 100 + self.cpu_progress_bar.value = psutil.cpu_percent() / 100 + return True diff --git a/bar/modules/volume.py b/bar/modules/volume.py new file mode 100644 index 0000000..c106bfe --- /dev/null +++ b/bar/modules/volume.py @@ -0,0 +1,48 @@ +from fabric.widgets.circularprogressbar import CircularProgressBar +from fabric.audio.service import Audio +from fabric.widgets.eventbox import EventBox +from fabric.widgets.box import Box +from fabric.widgets.overlay import Overlay +from fabric.widgets.label import Label + + +class VolumeWidget(Box): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.audio = Audio() + + self.progress_bar = CircularProgressBar( + name="volume-progress-bar", pie=True, size=24 + ) + + self.event_box = EventBox( + events="scroll", + child=Overlay( + child=self.progress_bar, + overlays=Label( + label="", + style="margin: 0px 6px 0px 0px; font-size: 12px", # to center the icon glyph + ), + ), + ) + + self.audio.connect("notify::speaker", self.on_speaker_changed) + self.event_box.connect("scroll-event", self.on_scroll) + self.add(self.event_box) + + def on_scroll(self, _, event): + match event.direction: + case 0: + self.audio.speaker.volume += 8 + case 1: + self.audio.speaker.volume -= 8 + return + + def on_speaker_changed(self, *_): + if not self.audio.speaker: + return + self.progress_bar.value = self.audio.speaker.volume / 100 + self.audio.speaker.bind( + "volume", "value", self.progress_bar, lambda _, v: v / 100 + ) + return diff --git a/bar/river/service.py b/bar/river/service.py index c6d10e4..2723851 100644 --- a/bar/river/service.py +++ b/bar/river/service.py @@ -12,6 +12,10 @@ from fabric.utils.helpers import idle_add from pywayland.client import Display from pywayland.protocol.wayland import WlOutput, WlSeat from .generated.river_status_unstable_v1 import ZriverStatusManagerV1 +from gi.repository import ( + Gio, + GLib, +) @dataclass @@ -48,7 +52,7 @@ class River(Service): return self._active_window_title @Signal - def ready(self): + def ready_signal(self): return self.notify("ready") @Signal("event", flags="detailed") @@ -65,18 +69,15 @@ class River(Service): self.seat_status = None # Start the connection in a separate thread - self.river_thread = threading.Thread( - target=self._river_connection_task, daemon=True, name="river-status-service" + self.river_thread = GLib.Thread.new( + "river-status-service", self._river_connection_task ) - self.river_thread.start() def _river_connection_task(self): """Main thread that connects to River and listens for events""" try: - # Create a new display connection - THIS IS WHERE THE ERROR OCCURS logger.info("[RiverService] Starting connection to River") - # Let's add some more diagnostic info to help troubleshoot logger.debug( f"[RiverService] XDG_RUNTIME_DIR={os.environ.get('XDG_RUNTIME_DIR', 'Not set')}" ) @@ -84,14 +85,7 @@ class River(Service): f"[RiverService] WAYLAND_DISPLAY={os.environ.get('WAYLAND_DISPLAY', 'Not set')}" ) - # Create the display connection - # with Display() as display: - # registry = display.get_registry() - # logger.debug("[RiverService] Registry obtained") - - # Discover globals - - display = Display("wayland-1") + display = Display() display.connect() logger.debug("[RiverService] Display connection created") @@ -236,11 +230,13 @@ class River(Service): logger.error(traceback.format_exc()) + return True + def _set_ready(self): """Set the service as ready (called on main thread via idle_add)""" self._ready = True logger.info("[RiverService] Service ready") - self.ready.emit() + self.ready_signal.emit() return False # Don't repeat def _emit_view_tags(self, output_id, tags): diff --git a/bar/river/widgets.py b/bar/river/widgets.py index be4f9ab..87b7de1 100644 --- a/bar/river/widgets.py +++ b/bar/river/widgets.py @@ -76,12 +76,16 @@ class RiverWorkspaceButton(Button): class RiverWorkspaces(EventBox): - def __init__(self, output_id=None, max_tags=9, **kwargs): + def __init__(self, output_id, river_service=None, max_tags=9, **kwargs): super().__init__(events="scroll") - self.service = get_river_connection() self._box = Box(**kwargs) self.children = self._box + if river_service: + self.river = river_service + else: + self.river = get_river_connection() + # Store output_id as received self.output_id = output_id @@ -94,33 +98,33 @@ class RiverWorkspaces(EventBox): self._box.add(btn) # Connect to service events - self.service.connect("event::focused_tags", self.on_focus_change_general) - self.service.connect("event::view_tags", self.on_view_change_general) - self.service.connect("event::urgent_tags", self.on_urgent_change_general) - self.service.connect("event::output_removed", self.on_output_removed) + self.river.connect("event::focused_tags", self.on_focus_change_general) + self.river.connect("event::view_tags", self.on_view_change_general) + self.river.connect("event::urgent_tags", self.on_urgent_change_general) + self.river.connect("event::output_removed", self.on_output_removed) # Initial setup when service is ready - if self.service.ready: + if self.river.ready: self.on_ready(None) else: - self.service.connect("event::ready", self.on_ready) + self.river.connect("event::ready", self.on_ready) self.connect("scroll-event", self.on_scroll) def on_ready(self, _): """Initialize widget state when service is ready""" logger.debug( - f"[RiverWorkspaces] Service ready, outputs: {list(self.service.outputs.keys())}" + f"[RiverWorkspaces] Service ready, outputs: {list(self.river.outputs.keys())}" ) # If no output_id was specified, take the first one - if self.output_id is None and self.service.outputs: - self.output_id = next(iter(self.service.outputs.keys())) + if self.output_id is None and self.river.outputs: + self.output_id = next(iter(self.river.outputs.keys())) logger.info(f"[RiverWorkspaces] Selected output {self.output_id}") # Initialize state from selected output - if self.output_id is not None and self.output_id in self.service.outputs: - output_info = self.service.outputs[self.output_id] + if self.output_id is not None and self.output_id in self.river.outputs: + output_info = self.river.outputs[self.output_id] # Initialize buttons with current state # Access fields directly on the OutputInfo dataclass @@ -196,13 +200,13 @@ class RiverWorkspaces(EventBox): logger.info(f"[RiverWorkspaces] Our output {self.output_id} was removed") # Try to find another output - if self.service.outputs: - self.output_id = next(iter(self.service.outputs.keys())) + if self.river.outputs: + self.output_id = next(iter(self.river.outputs.keys())) logger.info(f"[RiverWorkspaces] Switching to output {self.output_id}") # Update state for new output - if self.output_id in self.service.outputs: - output_info = self.service.outputs[self.output_id] + if self.output_id in self.river.outputs: + output_info = self.river.outputs[self.output_id] # Access fields directly on the OutputInfo dataclass focused_tags = output_info.tags_focused view_tags = output_info.tags_view @@ -214,41 +218,46 @@ class RiverWorkspaces(EventBox): def on_workspace_click(self, btn): """Handle workspace button click""" logger.info(f"[RiverWorkspaces] Clicked on workspace {btn.id}") - self.service.toggle_focused_tag(btn.id) + self.river.toggle_focused_tag(btn.id) def on_scroll(self, _, event): """Handle scroll events""" direction = event.direction if direction == Gdk.ScrollDirection.DOWN: logger.info("[RiverWorkspaces] Scroll down - focusing next view") - self.service.run_command("focus-view", "next") + self.river.run_command("focus-view", "next") elif direction == Gdk.ScrollDirection.UP: logger.info("[RiverWorkspaces] Scroll up - focusing previous view") - self.service.run_command("focus-view", "previous") + self.river.run_command("focus-view", "previous") class RiverActiveWindow(Label): """Widget to display the currently active window's title""" - def __init__(self, max_length=None, ellipsize="end", **kwargs): + def __init__(self, max_length=None, ellipsize="end", river_service=None, **kwargs): super().__init__(**kwargs) - self.service = get_river_connection() + + if river_service: + self.river = river_service + else: + self.river = get_river_connection() + self.max_length = max_length self.ellipsize = ellipsize # Set initial state - if self.service.ready: + if self.river.ready: self.on_ready(None) else: - self.service.connect("event::ready", self.on_ready) + self.river.connect("event::ready", self.on_ready) # Connect to active window changes - self.service.connect("event::active_window", self.on_active_window_changed) + self.river.connect("event::active_window", self.on_active_window_changed) def on_ready(self, _): """Initialize widget when service is ready""" logger.debug("[RiverActiveWindow] Service ready") - self.update_title(self.service.active_window) + self.update_title(self.river.active_window) def on_active_window_changed(self, _, event): """Update widget when active window changes"""