diff --git a/sims/modules/bar.py b/sims/modules/bar.py index e161fde..c2362da 100644 --- a/sims/modules/bar.py +++ b/sims/modules/bar.py @@ -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) diff --git a/sims/modules/battery.py b/sims/modules/battery.py index 798eb97..1777770 100644 --- a/sims/modules/battery.py +++ b/sims/modules/battery.py @@ -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] diff --git a/sims/modules/buddy.py b/sims/modules/buddy.py index e40e417..e1b99b5 100644 --- a/sims/modules/buddy.py +++ b/sims/modules/buddy.py @@ -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) diff --git a/sims/modules/microphone.py b/sims/modules/microphone.py index 5df9208..e3f9b8c 100644 --- a/sims/modules/microphone.py +++ b/sims/modules/microphone.py @@ -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", diff --git a/sims/modules/notmuch.py b/sims/modules/notmuch.py index a518388..0c036a5 100644 --- a/sims/modules/notmuch.py +++ b/sims/modules/notmuch.py @@ -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}" - ) \ No newline at end of file + ) + + +_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 \ No newline at end of file diff --git a/sims/modules/player.py b/sims/modules/player.py index 26660c6..0b9c60d 100644 --- a/sims/modules/player.py +++ b/sims/modules/player.py @@ -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 = {} diff --git a/sims/services/audio.py b/sims/services/audio.py new file mode 100644 index 0000000..000e4c2 --- /dev/null +++ b/sims/services/audio.py @@ -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 diff --git a/sims/services/battery.py b/sims/services/battery.py index 740ba54..c62893b 100644 --- a/sims/services/battery.py +++ b/sims/services/battery.py @@ -69,4 +69,19 @@ class BatteryService(Service): @property def charging(self): """Get current charging status""" - return self._charging \ No newline at end of file + 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 \ No newline at end of file diff --git a/sims/services/buddy.py b/sims/services/buddy.py index e65bf48..6128c5d 100644 --- a/sims/services/buddy.py +++ b/sims/services/buddy.py @@ -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 diff --git a/sims/services/mpris.py b/sims/services/mpris.py index 2a6af57..3ffd43a 100644 --- a/sims/services/mpris.py +++ b/sims/services/mpris.py @@ -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 diff --git a/sims/services/system_stats.py b/sims/services/system_stats.py index 368122e..97f34eb 100644 --- a/sims/services/system_stats.py +++ b/sims/services/system_stats.py @@ -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