fix: only start systems once

This commit is contained in:
2026-09-17 13:30:59 +02:00
parent 2fc0854a11
commit 7cf69a8c75
11 changed files with 128 additions and 21 deletions
+2 -2
View File
@@ -20,7 +20,7 @@ from sims.services.screenrec import ScreenrecService
from sims.services.smart_corners import get_smart_corners_service from sims.services.smart_corners import get_smart_corners_service
from fabric.widgets.button import Button from fabric.widgets.button import Button
from fabric.widgets.circularprogressbar import CircularProgressBar from fabric.widgets.circularprogressbar import CircularProgressBar
from sims.services.system_stats import SystemStatsService from sims.services.system_stats import get_system_stats_service
from sims.config import BATTERY, BAR_HEIGHT, WINDOW_TITLE, NOTMUCH, BUDDY, ORG from sims.config import BATTERY, BAR_HEIGHT, WINDOW_TITLE, NOTMUCH, BUDDY, ORG
@@ -175,7 +175,7 @@ class StatusBar(Window):
) )
self.children = self.inner self.children = self.inner
self.system_stats_service = SystemStatsService(update_interval=3000) self.system_stats_service = get_system_stats_service()
self.system_stats_service.connect("stats-changed", self.update_progress_bars) self.system_stats_service.connect("stats-changed", self.update_progress_bars)
self.set_size_request(-1, BAR_HEIGHT) self.set_size_request(-1, BAR_HEIGHT)
+4 -3
View File
@@ -2,7 +2,7 @@ from gi.repository import GLib
from fabric.widgets.box import Box from fabric.widgets.box import Box
from fabric.widgets.label import Label from fabric.widgets.label import Label
from fabric.widgets.image import Image from fabric.widgets.image import Image
from sims.services.battery import BatteryService from sims.services.battery import get_battery_service
class Battery(Box): class Battery(Box):
@@ -15,8 +15,9 @@ class Battery(Box):
self.bat_label = Label(name="bat-label", label="100%") self.bat_label = Label(name="bat-label", label="100%")
# Create battery service with signal-based updates # Shared battery service with signal-based updates (one poller for the
self.battery_service = BatteryService(update_interval=10000) # Check every 10 seconds # whole process, including the buddy's battery checks)
self.battery_service = get_battery_service()
self.battery_service.connect("battery-changed", self.update_battery) self.battery_service.connect("battery-changed", self.update_battery)
self.children = [self.bat_icon, self.bat_label] self.children = [self.bat_icon, self.bat_label]
+5 -2
View File
@@ -1,16 +1,19 @@
from fabric.widgets.box import Box from fabric.widgets.box import Box
from sims.widgets.buddy import BuddyWidget from sims.widgets.buddy import BuddyWidget
from sims.services.buddy import BuddyService from sims.services.buddy import get_buddy_service
class Buddy(Box): class Buddy(Box):
def __init__(self, notmuch_service=None, **kwargs): def __init__(self, notmuch_service=None, **kwargs):
super().__init__(name="buddy-container", v_align="center", **kwargs) super().__init__(name="buddy-container", v_align="center", **kwargs)
self.service = BuddyService(notmuch_service=notmuch_service) self.service = get_buddy_service(notmuch_service=notmuch_service)
self.sprite = BuddyWidget(on_pet=self._on_pet) self.sprite = BuddyWidget(on_pet=self._on_pet)
self.service.connect("mood-changed", self._on_mood_changed) self.service.connect("mood-changed", self._on_mood_changed)
self.children = [self.sprite] self.children = [self.sprite]
# The shared service only emits on *changes* and may already sit in a
# non-idle mood when this output's sprite is built: adopt it now.
self.sprite.update_mood(self.service.mood)
def _on_mood_changed(self, _service, mood: str): def _on_mood_changed(self, _service, mood: str):
self.sprite.update_mood(mood) self.sprite.update_mood(mood)
+2 -2
View File
@@ -1,7 +1,7 @@
from fabric.audio.service import Audio
from fabric.widgets.box import Box from fabric.widgets.box import Box
from fabric.widgets.button import Button from fabric.widgets.button import Button
from fabric.widgets.label import Label from fabric.widgets.label import Label
from sims.services.audio import get_audio_service
MIC_ICON = "󰍬" MIC_ICON = "󰍬"
MIC_MUTED_ICON = "󰍭" MIC_MUTED_ICON = "󰍭"
@@ -17,7 +17,7 @@ class MicrophoneWidget(Box):
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self.audio = Audio() self.audio = get_audio_service()
self.icon_label = Label( self.icon_label = Label(
name="microphone-icon", name="microphone-icon",
+17 -1
View File
@@ -155,7 +155,7 @@ class NotmuchWidget(Button):
) )
# Initialize the service # Initialize the service
self.service = NotmuchService() self.service = get_notmuch_service()
self.service.connect("counts-changed", self.update_display) self.service.connect("counts-changed", self.update_display)
logger.info("[Notmuch] Notmuch widget initialized") logger.info("[Notmuch] Notmuch widget initialized")
@@ -206,3 +206,19 @@ class NotmuchWidget(Button):
logger.info( logger.info(
f"[Notmuch] Updated display: {unread} unread, {debt} aging — classes={classes}" f"[Notmuch] Updated display: {unread} unread, {debt} aging — classes={classes}"
) )
_service: "NotmuchService | None" = None
def get_notmuch_service(update_interval: int = 60000) -> "NotmuchService":
"""Return the process-wide notmuch service, creating it on first use.
Each poll shells out to `notmuch count` twice. Every bar widget used to own
a service, so one `refresh-notmuch` (or one timer tick) opened the database
2 x bars times over; now all widgets share a single poller.
"""
global _service
if _service is None:
_service = NotmuchService(update_interval=update_interval)
return _service
+3 -3
View File
@@ -14,7 +14,7 @@ from fabric.widgets.overlay import Overlay
from fabric.widgets.stack import Stack from fabric.widgets.stack import Stack
from ..widgets.circle_image import CircleImage from ..widgets.circle_image import CircleImage
import sims.modules.icons as icons import sims.modules.icons as icons
from sims.services.mpris import MprisPlayerManager, MprisPlayer from sims.services.mpris import MprisPlayer, get_mpris_player_manager
from fabric import Fabricator from fabric import Fabricator
# from sims.modules.cavalcade import SpectrumRender # from sims.modules.cavalcade import SpectrumRender
@@ -434,7 +434,7 @@ class Player(Box):
) )
self.switcher.set_stack(self.player_stack) self.switcher.set_stack(self.player_stack)
self.switcher.set_halign(Gtk.Align.CENTER) self.switcher.set_halign(Gtk.Align.CENTER)
self.mpris_manager = MprisPlayerManager() self.mpris_manager = get_mpris_player_manager()
players = self.mpris_manager.players players = self.mpris_manager.players
if players: if players:
for p in players: for p in players:
@@ -623,7 +623,7 @@ class PlayerSmall(Box):
self.add(self.mpris_small) self.add(self.mpris_small)
self.mpris_manager = MprisPlayerManager() self.mpris_manager = get_mpris_player_manager()
self.mpris_player = None self.mpris_player = None
self._players = {} self._players = {}
self._player_handlers = {} self._player_handlers = {}
+19
View File
@@ -0,0 +1,19 @@
"""Shared audio service.
fabric's `Audio` wraps a libcvc (`Cvc.MixerControl`) client: constructing one
opens a PulseAudio/PipeWire connection, subscribes to every stream event and
enumerates sinks/sources. Widgets were creating one per output, so keep a
single process-wide instance instead.
"""
from fabric.audio.service import Audio
_service: "Audio | None" = None
def get_audio_service() -> "Audio":
"""Return the process-wide Audio service, creating it on first use."""
global _service
if _service is None:
_service = Audio()
return _service
+15
View File
@@ -70,3 +70,18 @@ class BatteryService(Service):
def charging(self): def charging(self):
"""Get current charging status""" """Get current charging status"""
return self._charging return self._charging
_service: "BatteryService | None" = None
def get_battery_service(update_interval: int = 10000) -> "BatteryService":
"""Return the process-wide battery service, creating it on first use.
Only the first caller's `update_interval` wins, so the bar widget (10s) and
the buddy (15s) share one psutil poll instead of running their own.
"""
global _service
if _service is None:
_service = BatteryService(update_interval=update_interval)
return _service
+27 -6
View File
@@ -10,9 +10,9 @@ from datetime import datetime
from fabric.core.service import Service, Signal from fabric.core.service import Service, Signal
from fabric.utils import invoke_repeater from fabric.utils import invoke_repeater
from sims.services.system_stats import SystemStatsService from sims.services.battery import get_battery_service
from sims.services.battery import BatteryService from sims.services.mpris import MprisPlayer, get_mpris_player_manager
from sims.services.mpris import MprisPlayerManager, MprisPlayer from sims.services.system_stats import get_system_stats_service
from sims.config import BATTERY, NOTMUCH from sims.config import BATTERY, NOTMUCH
@@ -43,19 +43,19 @@ class BuddyService(Service):
self._excited_until = 0.0 self._excited_until = 0.0
self._last_unread = None self._last_unread = None
self._stats = SystemStatsService(update_interval=3000) self._stats = get_system_stats_service()
self._stats.connect("stats-changed", self._on_stats) self._stats.connect("stats-changed", self._on_stats)
self._battery = None self._battery = None
if BATTERY.get("enable", False): if BATTERY.get("enable", False):
self._battery = BatteryService(update_interval=15000) self._battery = get_battery_service()
self._battery.connect("battery-changed", self._on_battery) self._battery.connect("battery-changed", self._on_battery)
self._notmuch = notmuch_service self._notmuch = notmuch_service
if self._notmuch is not None: if self._notmuch is not None:
self._notmuch.connect("counts-changed", self._on_notmuch) self._notmuch.connect("counts-changed", self._on_notmuch)
self._mpris_manager = MprisPlayerManager() self._mpris_manager = get_mpris_player_manager()
self._mpris_players: dict[str, MprisPlayer] = {} self._mpris_players: dict[str, MprisPlayer] = {}
for p in self._mpris_manager.players or []: for p in self._mpris_manager.players or []:
self._attach_player(p) self._attach_player(p)
@@ -67,6 +67,11 @@ class BuddyService(Service):
# External triggers --------------------------------------------------- # External triggers ---------------------------------------------------
@property
def mood(self) -> str:
"""Currently arbitrated mood."""
return self._mood
def pet(self): def pet(self):
"""Called when the buddy is clicked.""" """Called when the buddy is clicked."""
self._petted_until = time.monotonic() + PETTED_DURATION_S self._petted_until = time.monotonic() + PETTED_DURATION_S
@@ -146,3 +151,19 @@ class BuddyService(Service):
self._mood = mood self._mood = mood
self.mood_changed(mood) self.mood_changed(mood)
return return
_service: "BuddyService | None" = None
def get_buddy_service(notmuch_service=None) -> "BuddyService":
"""Return the process-wide buddy service, creating it on first use.
`notmuch_service` only applies to the first caller (the bar that owns the
notmuch widget). One arbitrated mood and one re-arbitration ticker are
shared by every output instead of one per bar.
"""
global _service
if _service is None:
_service = BuddyService(notmuch_service=notmuch_service)
return _service
+16
View File
@@ -287,3 +287,19 @@ class MprisPlayerManager(Service):
@Property(object, "readable") @Property(object, "readable")
def players(self): def players(self):
return self._manager.get_property("players") # type: ignore return self._manager.get_property("players") # type: ignore
_service: "MprisPlayerManager | None" = None
def get_mpris_player_manager() -> "MprisPlayerManager":
"""Return the process-wide MPRIS manager, creating it on first use.
`Playerctl.PlayerManager.new()` plus one `Playerctl.Player` per MPRIS name
is the expensive part; every widget that wraps players should share this
manager and build its own `MprisPlayer` wrappers on top of it.
"""
global _service
if _service is None:
_service = MprisPlayerManager()
return _service
+16
View File
@@ -64,3 +64,19 @@ class SystemStatsService(Service):
def memory_percent(self): def memory_percent(self):
"""Get current memory percentage""" """Get current memory percentage"""
return self._memory_percent / 100 return self._memory_percent / 100
_service: "SystemStatsService | None" = None
def get_system_stats_service(update_interval: int = 3000) -> "SystemStatsService":
"""Return the process-wide stats service, creating it on first use.
`update_interval` only applies to the first caller: one poller feeds every
bar, and psutil's non-blocking `cpu_percent` keeps a process-global baseline
that extra pollers would keep resetting.
"""
global _service
if _service is None:
_service = SystemStatsService(update_interval=update_interval)
return _service