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 fabric.widgets.button import Button
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
@@ -175,7 +175,7 @@ class StatusBar(Window):
)
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.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.label import Label
from fabric.widgets.image import Image
from sims.services.battery import BatteryService
from sims.services.battery import get_battery_service
class Battery(Box):
@@ -15,8 +15,9 @@ class Battery(Box):
self.bat_label = Label(name="bat-label", label="100%")
# Create battery service with signal-based updates
self.battery_service = BatteryService(update_interval=10000) # Check every 10 seconds
# Shared battery service with signal-based updates (one poller for the
# whole process, including the buddy's battery checks)
self.battery_service = get_battery_service()
self.battery_service.connect("battery-changed", self.update_battery)
self.children = [self.bat_icon, self.bat_label]
+5 -2
View File
@@ -1,16 +1,19 @@
from fabric.widgets.box import Box
from sims.widgets.buddy import BuddyWidget
from sims.services.buddy import BuddyService
from sims.services.buddy import get_buddy_service
class Buddy(Box):
def __init__(self, notmuch_service=None, **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.service.connect("mood-changed", self._on_mood_changed)
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):
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.button import Button
from fabric.widgets.label import Label
from sims.services.audio import get_audio_service
MIC_ICON = "󰍬"
MIC_MUTED_ICON = "󰍭"
@@ -17,7 +17,7 @@ class MicrophoneWidget(Box):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.audio = Audio()
self.audio = get_audio_service()
self.icon_label = Label(
name="microphone-icon",
+18 -2
View File
@@ -155,7 +155,7 @@ class NotmuchWidget(Button):
)
# Initialize the service
self.service = NotmuchService()
self.service = get_notmuch_service()
self.service.connect("counts-changed", self.update_display)
logger.info("[Notmuch] Notmuch widget initialized")
@@ -205,4 +205,20 @@ class NotmuchWidget(Button):
logger.info(
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 ..widgets.circle_image import CircleImage
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 sims.modules.cavalcade import SpectrumRender
@@ -434,7 +434,7 @@ class Player(Box):
)
self.switcher.set_stack(self.player_stack)
self.switcher.set_halign(Gtk.Align.CENTER)
self.mpris_manager = MprisPlayerManager()
self.mpris_manager = get_mpris_player_manager()
players = self.mpris_manager.players
if players:
for p in players:
@@ -623,7 +623,7 @@ class PlayerSmall(Box):
self.add(self.mpris_small)
self.mpris_manager = MprisPlayerManager()
self.mpris_manager = get_mpris_player_manager()
self.mpris_player = None
self._players = {}
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
+16 -1
View File
@@ -69,4 +69,19 @@ class BatteryService(Service):
@property
def charging(self):
"""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.utils import invoke_repeater
from sims.services.system_stats import SystemStatsService
from sims.services.battery import BatteryService
from sims.services.mpris import MprisPlayerManager, MprisPlayer
from sims.services.battery import get_battery_service
from sims.services.mpris import MprisPlayer, get_mpris_player_manager
from sims.services.system_stats import get_system_stats_service
from sims.config import BATTERY, NOTMUCH
@@ -43,19 +43,19 @@ class BuddyService(Service):
self._excited_until = 0.0
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._battery = None
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._notmuch = notmuch_service
if self._notmuch is not None:
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] = {}
for p in self._mpris_manager.players or []:
self._attach_player(p)
@@ -67,6 +67,11 @@ class BuddyService(Service):
# External triggers ---------------------------------------------------
@property
def mood(self) -> str:
"""Currently arbitrated mood."""
return self._mood
def pet(self):
"""Called when the buddy is clicked."""
self._petted_until = time.monotonic() + PETTED_DURATION_S
@@ -146,3 +151,19 @@ class BuddyService(Service):
self._mood = mood
self.mood_changed(mood)
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")
def players(self):
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):
"""Get current memory percentage"""
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