feat(src/window/panel/panel): add NumLockIndicator and ChargingIndicator, improve battery service initialization

This commit is contained in:
sankalp committed 2026-07-05 23:05:49 +05:45
1 parent c64d41b3e1
commit ec65d8ead4
4 files changed
+193 -25

No files matched your search

+2 -2
View File
@@ -1,10 +1,10 @@
from services.bluetooth import BluetoothClient
from fabric.utils import logger
from fabric.widgets.box import Box
from fabric.widgets.button import Button
from fabric.widgets.label import Label
from services.battery import Battery
from services.bluetooth import BluetoothClient
from services.network import NetworkClient
from shared.window.applet_window import AppletWindow
from shared.window.battery_widget import BatteryControl
@@ -320,7 +320,7 @@ class BatteryIndicator(Box):
super().__init__(name="battery-indicator", orientation="h", **kwargs)
self.show_window = show_window
self.battery_service = Battery()
self.battery_service = Battery.get_initial()
self.battery_icon = svg_file("battery/battery-100.svg", size=23)
+6 -2
View File
@@ -1,4 +1,8 @@
from window.panel.notch.notch import Notch
from window.panel.notch.indicators import CapsLockIndicator, KeyboardLayoutIndicator
from window.panel.notch.indicators import (
CapsLockIndicator,
KeyboardLayoutIndicator,
NumLockIndicator,
)
__all__ = ["Notch", "CapsLockIndicator", "KeyboardLayoutIndicator"]
__all__ = ["Notch", "CapsLockIndicator", "KeyboardLayoutIndicator", "NumLockIndicator"]
+153 -10
View File
@@ -3,11 +3,13 @@ from fabric.widgets.box import Box
from fabric.widgets.centerbox import CenterBox
from fabric.widgets.label import Label
from services.battery import Battery
from services.capslock import CapsLock
from services.keyboard_layout import KeyboardLayout
from services.numlock import NumLock
from utils.utils import svg_file
DISPLAY_MS = 2000 # how long each indicator stays visible
DISPLAY_MS = 2000 # how long each indicator stays visible (ms)
class KeyboardLayoutIndicator(Box):
@@ -19,7 +21,6 @@ class KeyboardLayoutIndicator(Box):
v_align="center",
**kwargs,
)
self._show_cb = show_cb
self._hide_cb = hide_cb
self._hide_timer_id = 0
@@ -75,6 +76,9 @@ class KeyboardLayoutIndicator(Box):
super().destroy()
# ── CapsLock ───────────────────────────────────────────────────────────────────
class CapsLockIndicator(Box):
def __init__(self, show_cb, hide_cb, **kwargs):
super().__init__(
@@ -84,7 +88,6 @@ class CapsLockIndicator(Box):
v_align="center",
**kwargs,
)
self._show_cb = show_cb
self._hide_cb = hide_cb
self._hide_timer_id = 0
@@ -99,7 +102,7 @@ class CapsLockIndicator(Box):
)
self._label = Label(
name="notch-indicator-label",
label="On",
label="On" if self._caps.is_on else "Off",
v_align="center",
)
@@ -112,15 +115,11 @@ class CapsLockIndicator(Box):
)
)
# Accessing is_on triggers _ensure_monitoring_started() inside the service —
# without this the 500ms LED poll never begins and state_changed never fires.
_ = self._caps.is_on
self._caps.connect("state_changed", self._on_state_changed)
def _on_state_changed(self, _service, is_on: bool):
if not is_on:
return
# Show for BOTH On and Off
self._label.set_label("On" if is_on else "Off")
self._show_cb(self)
self._schedule_hide()
@@ -143,3 +142,147 @@ class CapsLockIndicator(Box):
except Exception as e:
logger.warning(f"[NotchIndicators] caps disconnect: {e}")
super().destroy()
class NumLockIndicator(Box):
def __init__(self, show_cb, hide_cb, **kwargs):
super().__init__(
name="notch-numlock",
orientation="h",
h_expand=True,
v_align="center",
**kwargs,
)
self._show_cb = show_cb
self._hide_cb = hide_cb
self._hide_timer_id = 0
self._num = NumLock.get_initial()
self._icon = svg_file(
"misc/num-lock.svg",
size=16,
name="notch-indicator-icon",
v_align="center",
)
self._label = Label(
name="notch-indicator-label",
label="On" if self._num.is_on else "Off",
v_align="center",
)
self.add(
CenterBox(
orientation="h",
h_expand=True,
start_children=self._icon,
end_children=self._label,
)
)
# is_on access also starts the 500ms LED poll inside the service
self._num.connect("state_changed", self._on_state_changed)
def _on_state_changed(self, _service, is_on: bool):
# Show for BOTH On and Off
self._label.set_label("On" if is_on else "Off")
self._show_cb(self)
self._schedule_hide()
def _schedule_hide(self):
if self._hide_timer_id:
GLib.source_remove(self._hide_timer_id)
self._hide_timer_id = GLib.timeout_add(DISPLAY_MS, self._do_hide)
def _do_hide(self):
self._hide_timer_id = 0
self._hide_cb()
return False
def destroy(self):
if self._hide_timer_id:
GLib.source_remove(self._hide_timer_id)
self._hide_timer_id = 0
try:
self._num.disconnect_by_func(self._on_state_changed)
except Exception as e:
logger.warning(f"[NotchIndicators] numlock disconnect: {e}")
super().destroy()
class ChargingIndicator(Box):
def __init__(self, show_cb, hide_cb, **kwargs):
super().__init__(
name="notch-charging",
orientation="h",
h_expand=True,
v_align="center",
**kwargs,
)
self._show_cb = show_cb
self._hide_cb = hide_cb
self._hide_timer_id = 0
self._battery = Battery.get_initial()
self._label = Label(
name="notch-indicator-label",
label="Charging",
v_align="center",
)
self._icon = svg_file(
"notch/charing.svg",
size=18,
name="notch-indicator-icon",
v_align="center",
)
self.add(
CenterBox(
orientation="h",
h_expand=True,
start_children=self._label,
end_children=self._icon,
)
)
self._battery.connect("changed", self._on_battery_changed)
GLib.timeout_add(500, self._initial_check)
def _is_charger_connected(self) -> bool:
return self._battery.charging or self._battery.charged
def _initial_check(self):
self._last_charger_state = self._is_charger_connected()
if self._last_charger_state:
self._show_cb(self)
self._schedule_hide()
return False # one-shot
def _on_battery_changed(self, _battery):
connected = self._is_charger_connected()
if connected and not self._last_charger_state:
self._show_cb(self)
self._schedule_hide()
self._last_charger_state = connected
def _schedule_hide(self):
if self._hide_timer_id:
GLib.source_remove(self._hide_timer_id)
self._hide_timer_id = GLib.timeout_add(DISPLAY_MS, self._do_hide)
def _do_hide(self):
self._hide_timer_id = 0
self._hide_cb()
return False
def destroy(self):
if self._hide_timer_id:
GLib.source_remove(self._hide_timer_id)
self._hide_timer_id = 0
try:
self._battery.disconnect_by_func(self._on_battery_changed)
except Exception as e:
logger.warning(f"[NotchIndicators] charging disconnect: {e}")
super().destroy()
+32 -11
View File
@@ -6,7 +6,12 @@ from fabric.widgets.stack import Stack
from services.screencapture import screen_capture_service
from window.panel.notch.recording import RecordingIndicator
from window.panel.notch.player import NotchPlayer
from window.panel.notch.indicators import CapsLockIndicator, KeyboardLayoutIndicator
from window.panel.notch.indicators import (
CapsLockIndicator,
KeyboardLayoutIndicator,
NumLockIndicator,
ChargingIndicator,
)
from window.controlcenter.player import get_shared_mpris_manager
@@ -35,6 +40,14 @@ class Notch(Box):
h_expand=True,
)
self._last_scroll_index = 0
self._last_scroll_time = 0
self._last_recording_check = 0
self._is_active_recording = False
self._music_services: list[str] = []
# Must be set BEFORE indicator widgets are created
self._transient_active = False
# Transient indicator pages — shown on change, hidden after 2 s
self.kbd_indicator = KeyboardLayoutIndicator(
show_cb=self._show_transient,
@@ -44,6 +57,15 @@ class Notch(Box):
show_cb=self._show_transient,
hide_cb=self._hide_transient,
)
self.num_indicator = NumLockIndicator(
show_cb=self._show_transient,
hide_cb=self._hide_transient,
)
# Persistent indicator — now also transient (2s), deferred via idle_add
self.charging_indicator = ChargingIndicator(
show_cb=self._show_transient,
hide_cb=self._hide_transient,
)
self._mpris = get_shared_mpris_manager()
self._mpris.connect("new-player", self._on_new_player)
@@ -52,14 +74,6 @@ class Notch(Box):
screen_capture_service.connect("recording-started", self._on_recording_started)
screen_capture_service.connect("recording-stopped", self._on_recording_stopped)
self._last_scroll_index = 0
self._last_scroll_time = 0
self._last_recording_check = 0
self._is_active_recording = False
self._music_services: list[str] = []
# Track if a transient indicator is currently shown
self._transient_active = False
self.notch_stack = Stack(
name="panel-notch-stack",
v_expand=True,
@@ -71,6 +85,8 @@ class Notch(Box):
self.player_widget,
self.kbd_indicator,
self.caps_indicator,
self.num_indicator,
self.charging_indicator,
],
)
self._last_stack_page = 0
@@ -126,7 +142,7 @@ class Notch(Box):
self.notch_stack.set_visible_child(widget)
def _hide_transient(self):
"""Return to the normal notch state after the indicator timer expires."""
"""Return to normal notch state after the indicator timer expires."""
self._transient_active = False
self._apply_stack_state()
@@ -231,7 +247,10 @@ class Notch(Box):
self.notch_stack.set_visible_child(self.idle_widget)
return
pages = {1: self.recording_indicator, 2: self.player_widget}
pages = {
1: self.recording_indicator,
2: self.player_widget,
}
if len(active_pages) == 1:
target = active_pages[0]
@@ -248,4 +267,6 @@ class Notch(Box):
self.player_widget.destroy()
self.kbd_indicator.destroy()
self.caps_indicator.destroy()
self.num_indicator.destroy()
self.charging_indicator.destroy()
super().destroy()