multiple monitor support

This commit is contained in:
Makesesama 2025-05-06 13:09:40 +02:00
parent 53713ee0f5
commit f8b352d624
5 changed files with 263 additions and 211 deletions

View File

@ -1,185 +1,56 @@
# fabric bar.py example # fabric bar.py example
# https://github.com/Fabric-Development/fabric/blob/rewrite/examples/bar/bar.py # https://github.com/Fabric-Development/fabric/blob/rewrite/examples/bar/bar.py
import psutil from loguru import logger
from fabric import Application 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.system_tray.widgets import SystemTray
from fabric.widgets.circularprogressbar import CircularProgressBar
from fabric.widgets.wayland import WaylandWindow as Window 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 ( from fabric.utils import (
FormattedString,
bulk_replace,
invoke_repeater,
get_relative_path, get_relative_path,
) )
from bar.modules.player import Player from .modules.bar import StatusBar
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
def main(): def main():
bar = StatusBar(45) tray = SystemTray(name="system-tray", spacing=4)
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"))
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() app.run()

128
bar/modules/bar.py Normal file
View File

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

48
bar/modules/volume.py Normal file
View File

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

View File

@ -12,6 +12,10 @@ from fabric.utils.helpers import idle_add
from pywayland.client import Display from pywayland.client import Display
from pywayland.protocol.wayland import WlOutput, WlSeat from pywayland.protocol.wayland import WlOutput, WlSeat
from .generated.river_status_unstable_v1 import ZriverStatusManagerV1 from .generated.river_status_unstable_v1 import ZriverStatusManagerV1
from gi.repository import (
Gio,
GLib,
)
@dataclass @dataclass
@ -48,7 +52,7 @@ class River(Service):
return self._active_window_title return self._active_window_title
@Signal @Signal
def ready(self): def ready_signal(self):
return self.notify("ready") return self.notify("ready")
@Signal("event", flags="detailed") @Signal("event", flags="detailed")
@ -65,18 +69,15 @@ class River(Service):
self.seat_status = None self.seat_status = None
# Start the connection in a separate thread # Start the connection in a separate thread
self.river_thread = threading.Thread( self.river_thread = GLib.Thread.new(
target=self._river_connection_task, daemon=True, name="river-status-service" "river-status-service", self._river_connection_task
) )
self.river_thread.start()
def _river_connection_task(self): def _river_connection_task(self):
"""Main thread that connects to River and listens for events""" """Main thread that connects to River and listens for events"""
try: try:
# Create a new display connection - THIS IS WHERE THE ERROR OCCURS
logger.info("[RiverService] Starting connection to River") logger.info("[RiverService] Starting connection to River")
# Let's add some more diagnostic info to help troubleshoot
logger.debug( logger.debug(
f"[RiverService] XDG_RUNTIME_DIR={os.environ.get('XDG_RUNTIME_DIR', 'Not set')}" 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')}" f"[RiverService] WAYLAND_DISPLAY={os.environ.get('WAYLAND_DISPLAY', 'Not set')}"
) )
# Create the display connection display = Display()
# with Display() as display:
# registry = display.get_registry()
# logger.debug("[RiverService] Registry obtained")
# Discover globals
display = Display("wayland-1")
display.connect() display.connect()
logger.debug("[RiverService] Display connection created") logger.debug("[RiverService] Display connection created")
@ -236,11 +230,13 @@ class River(Service):
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
return True
def _set_ready(self): def _set_ready(self):
"""Set the service as ready (called on main thread via idle_add)""" """Set the service as ready (called on main thread via idle_add)"""
self._ready = True self._ready = True
logger.info("[RiverService] Service ready") logger.info("[RiverService] Service ready")
self.ready.emit() self.ready_signal.emit()
return False # Don't repeat return False # Don't repeat
def _emit_view_tags(self, output_id, tags): def _emit_view_tags(self, output_id, tags):

View File

@ -76,12 +76,16 @@ class RiverWorkspaceButton(Button):
class RiverWorkspaces(EventBox): 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") super().__init__(events="scroll")
self.service = get_river_connection()
self._box = Box(**kwargs) self._box = Box(**kwargs)
self.children = self._box self.children = self._box
if river_service:
self.river = river_service
else:
self.river = get_river_connection()
# Store output_id as received # Store output_id as received
self.output_id = output_id self.output_id = output_id
@ -94,33 +98,33 @@ class RiverWorkspaces(EventBox):
self._box.add(btn) self._box.add(btn)
# Connect to service events # Connect to service events
self.service.connect("event::focused_tags", self.on_focus_change_general) self.river.connect("event::focused_tags", self.on_focus_change_general)
self.service.connect("event::view_tags", self.on_view_change_general) self.river.connect("event::view_tags", self.on_view_change_general)
self.service.connect("event::urgent_tags", self.on_urgent_change_general) self.river.connect("event::urgent_tags", self.on_urgent_change_general)
self.service.connect("event::output_removed", self.on_output_removed) self.river.connect("event::output_removed", self.on_output_removed)
# Initial setup when service is ready # Initial setup when service is ready
if self.service.ready: if self.river.ready:
self.on_ready(None) self.on_ready(None)
else: else:
self.service.connect("event::ready", self.on_ready) self.river.connect("event::ready", self.on_ready)
self.connect("scroll-event", self.on_scroll) self.connect("scroll-event", self.on_scroll)
def on_ready(self, _): def on_ready(self, _):
"""Initialize widget state when service is ready""" """Initialize widget state when service is ready"""
logger.debug( 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 no output_id was specified, take the first one
if self.output_id is None and self.service.outputs: if self.output_id is None and self.river.outputs:
self.output_id = next(iter(self.service.outputs.keys())) self.output_id = next(iter(self.river.outputs.keys()))
logger.info(f"[RiverWorkspaces] Selected output {self.output_id}") logger.info(f"[RiverWorkspaces] Selected output {self.output_id}")
# Initialize state from selected output # Initialize state from selected output
if self.output_id is not None and self.output_id in self.service.outputs: if self.output_id is not None and self.output_id in self.river.outputs:
output_info = self.service.outputs[self.output_id] output_info = self.river.outputs[self.output_id]
# Initialize buttons with current state # Initialize buttons with current state
# Access fields directly on the OutputInfo dataclass # 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") logger.info(f"[RiverWorkspaces] Our output {self.output_id} was removed")
# Try to find another output # Try to find another output
if self.service.outputs: if self.river.outputs:
self.output_id = next(iter(self.service.outputs.keys())) self.output_id = next(iter(self.river.outputs.keys()))
logger.info(f"[RiverWorkspaces] Switching to output {self.output_id}") logger.info(f"[RiverWorkspaces] Switching to output {self.output_id}")
# Update state for new output # Update state for new output
if self.output_id in self.service.outputs: if self.output_id in self.river.outputs:
output_info = self.service.outputs[self.output_id] output_info = self.river.outputs[self.output_id]
# Access fields directly on the OutputInfo dataclass # Access fields directly on the OutputInfo dataclass
focused_tags = output_info.tags_focused focused_tags = output_info.tags_focused
view_tags = output_info.tags_view view_tags = output_info.tags_view
@ -214,41 +218,46 @@ class RiverWorkspaces(EventBox):
def on_workspace_click(self, btn): def on_workspace_click(self, btn):
"""Handle workspace button click""" """Handle workspace button click"""
logger.info(f"[RiverWorkspaces] Clicked on workspace {btn.id}") 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): def on_scroll(self, _, event):
"""Handle scroll events""" """Handle scroll events"""
direction = event.direction direction = event.direction
if direction == Gdk.ScrollDirection.DOWN: if direction == Gdk.ScrollDirection.DOWN:
logger.info("[RiverWorkspaces] Scroll down - focusing next view") 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: elif direction == Gdk.ScrollDirection.UP:
logger.info("[RiverWorkspaces] Scroll up - focusing previous view") 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): class RiverActiveWindow(Label):
"""Widget to display the currently active window's title""" """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) 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.max_length = max_length
self.ellipsize = ellipsize self.ellipsize = ellipsize
# Set initial state # Set initial state
if self.service.ready: if self.river.ready:
self.on_ready(None) self.on_ready(None)
else: else:
self.service.connect("event::ready", self.on_ready) self.river.connect("event::ready", self.on_ready)
# Connect to active window changes # 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, _): def on_ready(self, _):
"""Initialize widget when service is ready""" """Initialize widget when service is ready"""
logger.debug("[RiverActiveWindow] Service 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): def on_active_window_changed(self, _, event):
"""Update widget when active window changes""" """Update widget when active window changes"""