refactor(battery): consolidate button styles, refactor service, remove ddcutil

- Merge game-mode/charge-limit CSS into shared selectors
- Rewrite Battery service with IntEnum states and charge-limit support
- Add ChargeLimitButton with active-state styling
- Drop ddcutil from dependencies (unused)
This commit is contained in:
sankalp committed 2026-07-21 09:21:29 +05:45
1 parent a2ac3d0f55
commit faaa99bd24
5 files changed
+617 -123

No files matched your search

+1 -1
View File
@@ -49,7 +49,7 @@ The interactive installer handles dependencies, builds C-extensions, and configu
1. **Install dependencies:**
```bash
paru -S uv fabric-cli-git uwsm cliphist slurp grim swappy wl-clipboard wtype libnotify playerctl matugen-bin hypridle hyprsunset hyprpicker hyprshot gtk-session-lock awww otf-apple-sf-pro webp-pixbuf-loader cinnamon-desktop libmediaart acpi brightnessctl power-profiles-daemon ddcutil at-spi2-core networkmanager network-manager-applet blueman pipewire libpulse gcc make pkgconf meson ninja wayland-protocols gobject-introspection gtk-layer-shell librsvg libqalculate appmenu-gtk-module libdbusmenu-gtk3 libdbusmenu-qt5 pciutils wf-recorder ffmpeg --needed
paru -S uv fabric-cli-git uwsm cliphist slurp grim swappy wl-clipboard wtype libnotify playerctl matugen-bin hypridle hyprsunset hyprpicker hyprshot gtk-session-lock awww otf-apple-sf-pro webp-pixbuf-loader cinnamon-desktop libmediaart acpi brightnessctl power-profiles-daemon at-spi2-core networkmanager network-manager-applet blueman pipewire libpulse gcc make pkgconf meson ninja wayland-protocols gobject-introspection gtk-layer-shell librsvg libqalculate appmenu-gtk-module libdbusmenu-gtk3 libdbusmenu-qt5 pciutils wf-recorder ffmpeg --needed
```
2. **Clone and sync:**
-1
View File
@@ -66,7 +66,6 @@ PACKAGES=(
acpi
brightnessctl
power-profiles-daemon
ddcutil
at-spi2-core
networkmanager
network-manager-applet
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#fff" stroke="#aaaaaa" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-zap"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>

After

Width:  |  Height:  |  Size: 277 B

+595 -77
View File
@@ -1,24 +1,172 @@
from enum import IntEnum
from fabric.core.service import Property, Service, Signal
from fabric.utils import Gio, GLib, logger
BATTERY_BUS_NAME = "org.freedesktop.UPower"
BATTERY_BUS_PATH = "/org/freedesktop/UPower/devices/DisplayDevice"
BATTERY_INTERFACE = "org.freedesktop.UPower.Device"
UPOWER_BUS = "org.freedesktop.UPower"
UPOWER_PATH = "/org/freedesktop/UPower"
DEVICE_IFACE = "org.freedesktop.UPower.Device"
DEVICE_DISPLAY_PATH = "/org/freedesktop/UPower/devices/DisplayDevice"
POWER_PROFILE_BUS_NAME = "net.hadess.PowerProfiles"
POWER_PROFILE_BUS_PATH = "/net/hadess/PowerProfiles"
POWER_PROFILE_INTERFACE = "net.hadess.PowerProfiles"
POWER_PROFILE_BUS = "org.freedesktop.UPower.PowerProfiles"
POWER_PROFILE_PATH = "/org/freedesktop/UPower/PowerProfiles"
DEVICE_STATE = {
0: "UNKNOWN",
1: "CHARGING",
2: "DISCHARGING",
3: "EMPTY",
4: "FULLY_CHARGED",
5: "PENDING_CHARGE",
6: "PENDING_DISCHARGE",
class DeviceState(IntEnum):
UNKNOWN = 0
CHARGING = 1
DISCHARGING = 2
EMPTY = 3
FULLY_CHARGED = 4
PENDING_CHARGE = 5
PENDING_DISCHARGE = 6
class UpDeviceKind(IntEnum):
UNKNOWN = 0
LINE_POWER = 1
BATTERY = 2
UPS = 3
MONITOR = 4
MOUSE = 5
KEYBOARD = 6
PDA = 7
PHONE = 8
MEDIA_PLAYER = 9
TABLET = 10
COMPUTER = 11
GAMING_INPUT = 12
PEN = 13
TOUCHPAD = 14
HEADSET = 17
SPEAKERS = 18
HEADPHONES = 19
def is_peripheral(self) -> bool:
return self in (
self.MOUSE,
self.KEYBOARD,
self.GAMING_INPUT,
self.HEADSET,
self.HEADPHONES,
self.TOUCHPAD,
)
def is_power_source(self) -> bool:
return self == self.BATTERY
class PeripheralDeviceKind(IntEnum):
KEYBOARD = 0
MOUSE = 1
HEADPHONES = 2
GAMEPAD = 3
def __str__(self) -> str:
return _PERIPHERAL_NAMES[self]
_PERIPHERAL_NAMES = {
PeripheralDeviceKind.KEYBOARD: "Keyboard",
PeripheralDeviceKind.MOUSE: "Mouse",
PeripheralDeviceKind.HEADPHONES: "Headphones",
PeripheralDeviceKind.GAMEPAD: "Gamepad",
}
_DEVICE_KIND_TO_PERIPHERAL = {
UpDeviceKind.MOUSE: PeripheralDeviceKind.MOUSE,
UpDeviceKind.TOUCHPAD: PeripheralDeviceKind.MOUSE,
UpDeviceKind.KEYBOARD: PeripheralDeviceKind.KEYBOARD,
UpDeviceKind.HEADPHONES: PeripheralDeviceKind.HEADPHONES,
UpDeviceKind.HEADSET: PeripheralDeviceKind.HEADPHONES,
UpDeviceKind.GAMING_INPUT: PeripheralDeviceKind.GAMEPAD,
}
class BatteryData:
__slots__ = ("capacity", "is_discharging", "status")
def __init__(self, capacity: int, status: "BatteryStatus", is_discharging: bool):
self.capacity = capacity
self.status = status
self.is_discharging = is_discharging
def get_icon_name(self) -> str:
if self.status == BatteryStatus.CHARGING:
return "battery-level-04-charging-symbolic"
if self.status == BatteryStatus.FULL:
return "battery-full-charged-symbolic"
return _level_icon(self.capacity)
class BatteryStatus(IntEnum):
UNKNOWN = 0
CHARGING = 1
DISCHARGING = 2
NOT_CHARGING = 3
FULL = 4
def __str__(self) -> str:
return _STATUS_NAMES[self]
_STATUS_NAMES = {
BatteryStatus.UNKNOWN: "UNKNOWN",
BatteryStatus.CHARGING: "CHARGING",
BatteryStatus.DISCHARGING: "DISCHARGING",
BatteryStatus.NOT_CHARGING: "NOT_CHARGING",
BatteryStatus.FULL: "FULLY_CHARGED",
}
def _level_icon(capacity: int) -> str:
if capacity < 20:
return "battery-level-00-symbolic"
if capacity < 40:
return "battery-level-02-symbolic"
if capacity < 60:
return "battery-level-04-symbolic"
if capacity < 80:
return "battery-level-06-symbolic"
return "battery-level-08-symbolic"
def _state_from_raw(state: int) -> BatteryStatus:
return {
DeviceState.CHARGING: BatteryStatus.CHARGING,
DeviceState.DISCHARGING: BatteryStatus.DISCHARGING,
DeviceState.FULLY_CHARGED: BatteryStatus.FULL,
DeviceState.PENDING_CHARGE: BatteryStatus.NOT_CHARGING,
DeviceState.PENDING_DISCHARGE: BatteryStatus.NOT_CHARGING,
}.get(state, BatteryStatus.UNKNOWN)
def _device_kind_from_u32(value: int) -> UpDeviceKind:
try:
return UpDeviceKind(value)
except ValueError:
return UpDeviceKind.UNKNOWN
class Peripheral:
__slots__ = ("data", "kind", "name", "path")
def __init__(
self, name: str, kind: PeripheralDeviceKind, data: BatteryData, path: str
):
self.name = name
self.kind = kind
self.data = data
self.path = path
class ChargeLimit:
__slots__ = ("device_path", "enabled")
def __init__(self, enabled: bool, device_path: str):
self.enabled = enabled
self.device_path = device_path
class Battery(Service):
"""A service for interacting with the battery's DBus and Power Profiles.
@@ -42,157 +190,489 @@ class Battery(Service):
@Property(bool, "readable", default_value=False)
def available(self) -> bool:
return self.do_get_cached_property("IsPresent") or False
if self._system_battery is not None:
return True
return self._do_get_cached_property("IsPresent") or False
@Property(str, "readable", default_value="")
def vendor(self) -> str:
return self.do_get_cached_property("Vendor") or ""
return self._do_get_cached_property("Vendor") or ""
@Property(int, "readable", default_value=0)
def percent(self) -> int:
return self.do_get_cached_property("Percentage") or 0
if self._system_battery is not None:
return self._system_battery.capacity
return self._do_get_cached_property("Percentage") or 0
@Property(bool, "readable", default_value=False)
def charging(self) -> bool:
val = self.do_get_cached_property("State")
if val is None:
return False
return val == DEVICE_STATE.get("CHARGING", 1)
if self._system_battery is not None:
return self._system_battery.status == BatteryStatus.CHARGING
return self._raw_state() == DeviceState.CHARGING
@Property(bool, "readable", default_value=False)
def discharging(self) -> bool:
val = self.do_get_cached_property("State")
if val is None:
return False
return val == DEVICE_STATE.get("DISCHARGING", 2)
if self._system_battery is not None:
return self._system_battery.status == BatteryStatus.DISCHARGING
return self._raw_state() == DeviceState.DISCHARGING
@Property(bool, "readable", default_value=False)
def charged(self) -> bool:
val = self.do_get_cached_property("State")
if val is None:
return False
return val == DEVICE_STATE.get("FULLY_CHARGED", 4)
if self._system_battery is not None:
return self._system_battery.status == BatteryStatus.FULL
return self._raw_state() == DeviceState.FULLY_CHARGED
@Property(str, "readable", default_value="")
def icon_name(self) -> str:
return self.do_get_cached_property("IconName") or ""
if self._system_battery is not None:
return self._system_battery.get_icon_name()
return self._do_get_cached_property("IconName") or ""
@Property(int, "readable", default_value=0)
def time_remaining(self) -> int:
return self.do_get_cached_property("TimeToEmpty") or 0
return self._do_get_cached_property("TimeToEmpty") or 0
@Property(int, "readable", default_value=0)
def time_to_full(self) -> int:
return self.do_get_cached_property("TimeToFull") or 0
return self._do_get_cached_property("TimeToFull") or 0
@Property(float, "readable", default_value=0.0)
def temperature(self) -> float:
return self.do_get_cached_property("Temperature") or 0.0
return self._do_get_cached_property("Temperature") or 0.0
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._bus: Gio.DBusConnection | None = None
self._proxy = None
self._power_profile_proxy = None
# _prop_cache stores ONLY properties received via PropertiesChanged.
# For fresh reads we always fall through to the GDBus proxy cache.
self._display_proxy: Gio.DBusProxy | None = None
self._power_profile_proxy: Gio.DBusProxy | None = None
self._prop_cache: dict[str, object] = {}
self.do_register()
self._device_proxies: dict[str, Gio.DBusProxy] = {}
self._system_battery: BatteryData | None = None
self._system_battery_paths: list[str] = []
self._peripherals: list[Peripheral] = []
self._charge_limit: ChargeLimit | None = None
self._power_profile: str = ""
self._toggling_charge_limit: bool = False
self._do_register()
def do_register(self) -> None:
def _raw_state(self) -> DeviceState:
val = self._do_get_cached_property("State")
if val is None:
return DeviceState.UNKNOWN
try:
return DeviceState(val)
except ValueError:
return DeviceState.UNKNOWN
def _do_register(self) -> None:
self._bus = Gio.bus_get_sync(Gio.BusType.SYSTEM)
self._proxy = Gio.DBusProxy.new_sync(
self._display_proxy = Gio.DBusProxy.new_sync(
self._bus,
Gio.DBusProxyFlags.NONE,
None,
BATTERY_BUS_NAME,
BATTERY_BUS_PATH,
BATTERY_INTERFACE,
UPOWER_BUS,
DEVICE_DISPLAY_PATH,
DEVICE_IFACE,
None,
)
logger.info("[Battery] Proxy initialized")
logger.info("[Battery] DisplayDevice proxy initialized")
self._bus.signal_subscribe(
BATTERY_BUS_NAME,
UPOWER_BUS,
"org.freedesktop.DBus.Properties",
"PropertiesChanged",
BATTERY_BUS_PATH,
DEVICE_DISPLAY_PATH,
None,
Gio.DBusSignalFlags.NONE,
self.do_handle_property_change,
self._do_handle_display_device_change,
)
self._do_enumerate_devices()
self._do_setup_upower_signals()
self._do_setup_power_profile()
def _do_enumerate_devices(self) -> None:
try:
result = self._bus.call_sync(
UPOWER_BUS,
UPOWER_PATH,
UPOWER_BUS,
"EnumerateDevices",
None,
None,
Gio.DBusCallFlags.NONE,
-1,
None,
)
device_paths = result.unpack()[0]
except Exception as e:
logger.warning(f"[Battery] Failed to enumerate devices: {e}")
return
for path in device_paths:
self._do_add_device(path)
self._do_recalculate_system_battery()
def _do_add_device(self, path: str) -> None:
try:
proxy = Gio.DBusProxy.new_sync(
self._bus,
Gio.DBusProxyFlags.NONE,
None,
UPOWER_BUS,
path,
DEVICE_IFACE,
None,
)
self._device_proxies[path] = proxy
self._bus.signal_subscribe(
UPOWER_BUS,
"org.freedesktop.DBus.Properties",
"PropertiesChanged",
path,
None,
Gio.DBusSignalFlags.NONE,
self._do_handle_device_change,
)
except Exception as e:
logger.warning(f"[Battery] Failed to create proxy for {path}: {e}")
def _do_setup_upower_signals(self) -> None:
try:
self._bus.signal_subscribe(
UPOWER_BUS,
UPOWER_BUS,
"DeviceAdded",
UPOWER_PATH,
None,
Gio.DBusSignalFlags.NONE,
self._do_handle_device_added,
)
self._bus.signal_subscribe(
UPOWER_BUS,
UPOWER_BUS,
"DeviceRemoved",
UPOWER_PATH,
None,
Gio.DBusSignalFlags.NONE,
self._do_handle_device_removed,
)
logger.info("[Battery] UPower device signals subscribed")
except Exception as e:
logger.warning(f"[Battery] Failed to setup UPower signals: {e}")
def _do_setup_power_profile(self) -> None:
try:
self._power_profile_proxy = Gio.DBusProxy.new_sync(
self._bus,
Gio.DBusProxyFlags.NONE,
None,
POWER_PROFILE_BUS_NAME,
POWER_PROFILE_BUS_PATH,
POWER_PROFILE_INTERFACE,
POWER_PROFILE_BUS,
POWER_PROFILE_PATH,
POWER_PROFILE_BUS,
None,
)
result = self._power_profile_proxy.get_cached_property("ActiveProfile")
if result is not None:
self._power_profile = result.unpack()
self._bus.signal_subscribe(
POWER_PROFILE_BUS_NAME,
POWER_PROFILE_BUS,
"org.freedesktop.DBus.Properties",
"PropertiesChanged",
POWER_PROFILE_BUS_PATH,
POWER_PROFILE_PATH,
None,
Gio.DBusSignalFlags.NONE,
self.do_handle_power_profile_change,
self._do_handle_power_profile_change,
)
logger.info("[Battery] Power Profiles Proxy initialized")
logger.info("[Battery] Power Profiles proxy initialized")
except Exception as e:
logger.warning(f"[Battery] Power Profiles daemon not available: {e}")
def do_handle_property_change(
def _do_handle_display_device_change(
self,
_connection: Gio.DBusConnection,
_sender_name: str,
_object_path: str,
_interface_name: str,
_signal_name: str,
_sender: str,
_path: str,
_interface: str,
_signal: str,
parameters: GLib.Variant,
):
if self._toggling_charge_limit:
return
try:
_iface, changed_props, _invalidated = parameters.unpack()
for prop_name, variant in changed_props.items():
self._prop_cache[prop_name] = variant.unpack()
except Exception as e:
logger.warning(f"[Battery] Failed to parse PropertiesChanged: {e}")
logger.warning(
f"[Battery] Failed to parse DisplayDevice PropertiesChanged: {e}"
)
self.emit("changed")
def do_handle_power_profile_change(self, *_):
def _do_handle_device_change(self, *_args):
if self._toggling_charge_limit:
return
self._do_recalculate_system_battery()
self._do_recalculate_peripherals()
self.emit("changed")
def _do_handle_device_added(self, *_args):
try:
result = self._bus.call_sync(
UPOWER_BUS,
UPOWER_PATH,
UPOWER_BUS,
"EnumerateDevices",
None,
None,
Gio.DBusCallFlags.NONE,
-1,
None,
)
for path in result.unpack()[0]:
if path not in self._device_proxies:
self._do_add_device(path)
self._do_recalculate_system_battery()
self._do_recalculate_peripherals()
self.emit("changed")
except Exception as e:
logger.warning(f"[Battery] Failed to handle DeviceAdded: {e}")
def _do_handle_device_removed(self, *_args):
try:
result = self._bus.call_sync(
UPOWER_BUS,
UPOWER_PATH,
UPOWER_BUS,
"EnumerateDevices",
None,
None,
Gio.DBusCallFlags.NONE,
-1,
None,
)
current_paths = set(result.unpack()[0])
removed = [p for p in self._device_proxies if p not in current_paths]
for path in removed:
del self._device_proxies[path]
if removed:
self._do_recalculate_system_battery()
self._do_recalculate_peripherals()
self.emit("changed")
except Exception as e:
logger.warning(f"[Battery] Failed to handle DeviceRemoved: {e}")
def _do_recalculate_system_battery(self) -> None:
charging = False
discharging = False
pending_charge = False
pending_discharge = False
fully_charged_count = 0
total_devices = 0
paths = []
for path, proxy in self._device_proxies.items():
kind_raw = proxy.get_cached_property("Type")
power_supply = proxy.get_cached_property("PowerSupply")
if kind_raw is None or power_supply is None:
continue
kind = _device_kind_from_u32(kind_raw.unpack())
if not kind.is_power_source() or not power_supply.unpack():
continue
energy_full_variant = proxy.get_cached_property("EnergyFull")
if energy_full_variant is not None and energy_full_variant.unpack() == 0.0:
continue
state_variant = proxy.get_cached_property("State")
if state_variant is None:
continue
try:
state = DeviceState(state_variant.unpack())
except ValueError:
continue
total_devices += 1
paths.append(path)
if state == DeviceState.CHARGING:
charging = True
elif state == DeviceState.DISCHARGING:
discharging = True
elif state == DeviceState.FULLY_CHARGED:
fully_charged_count += 1
elif state == DeviceState.PENDING_CHARGE:
pending_charge = True
elif state == DeviceState.PENDING_DISCHARGE:
pending_discharge = True
self._system_battery_paths = paths
if total_devices == 0:
self._system_battery = None
self._charge_limit = None
return
if fully_charged_count == total_devices:
merged_state = DeviceState.FULLY_CHARGED
elif charging:
merged_state = DeviceState.CHARGING
elif discharging:
merged_state = DeviceState.DISCHARGING
elif pending_charge:
merged_state = DeviceState.PENDING_CHARGE
elif pending_discharge:
merged_state = DeviceState.PENDING_DISCHARGE
else:
merged_state = DeviceState.UNKNOWN
if len(paths) == 1:
proxy = self._device_proxies[paths[0]]
pct_variant = proxy.get_cached_property("Percentage")
if pct_variant is None:
self._system_battery = None
return
capacity = int(pct_variant.unpack())
else:
energy = 0.0
energy_full = 0.0
for path in paths:
proxy = self._device_proxies[path]
e = proxy.get_cached_property("Energy")
ef = proxy.get_cached_property("EnergyFull")
if e is not None:
energy += e.unpack()
if ef is not None:
energy_full += ef.unpack()
if energy_full == 0.0:
self._system_battery = None
return
capacity = int(energy / energy_full * 100.0)
self._system_battery = BatteryData(
capacity=capacity,
status=_state_from_raw(merged_state),
is_discharging=merged_state == DeviceState.DISCHARGING,
)
self._do_recalculate_charge_limit()
def _do_recalculate_charge_limit(self) -> None:
self._charge_limit = None
for path in self._system_battery_paths:
proxy = self._device_proxies.get(path)
if proxy is None:
continue
try:
introspection = proxy.call_sync(
"org.freedesktop.DBus.Introspectable.Introspect",
None,
Gio.DBusCallFlags.NONE,
-1,
None,
)
xml = introspection.unpack()[0]
if "EnableChargeThreshold" not in xml:
continue
supported_variant = proxy.get_cached_property(
"ChargeThresholdSupported"
)
if supported_variant is None or not supported_variant.unpack():
continue
enabled_variant = proxy.get_cached_property("ChargeThresholdEnabled")
if enabled_variant is None:
continue
self._charge_limit = ChargeLimit(
enabled=enabled_variant.unpack(),
device_path=path,
)
return
except Exception as e:
logger.warning(f"[Battery] Failed to check charge limit on {path}: {e}")
def _do_recalculate_peripherals(self) -> None:
peripherals = []
for path, proxy in self._device_proxies.items():
kind_raw = proxy.get_cached_property("Type")
power_supply = proxy.get_cached_property("PowerSupply")
if kind_raw is None or power_supply is None:
continue
kind = _device_kind_from_u32(kind_raw.unpack())
if not kind.is_peripheral() or power_supply.unpack():
continue
peripheral_kind = _DEVICE_KIND_TO_PERIPHERAL.get(kind)
if peripheral_kind is None:
continue
model_variant = proxy.get_cached_property("Model")
name = (
model_variant.unpack()
if model_variant is not None
else str(peripheral_kind)
)
state_variant = proxy.get_cached_property("State")
state = (
DeviceState(state_variant.unpack())
if state_variant is not None
else DeviceState.UNKNOWN
)
pct_variant = proxy.get_cached_property("Percentage")
capacity = int(pct_variant.unpack()) if pct_variant is not None else 0
peripherals.append(
Peripheral(
name=name,
kind=peripheral_kind,
data=BatteryData(
capacity=capacity,
status=_state_from_raw(state),
is_discharging=state == DeviceState.DISCHARGING,
),
path=path,
)
)
self._peripherals = peripherals
def _do_handle_power_profile_change(self, *_args):
if self._power_profile_proxy is not None:
result = self._power_profile_proxy.get_cached_property("ActiveProfile")
if result is not None:
self._power_profile = result.unpack()
self.emit("power_profile_changed")
def do_get_cached_property(self, property_name):
"""Read from the GDBus proxy cache, which DBus keeps up-to-date."""
result = self._proxy.get_cached_property(property_name)
def _do_get_cached_property(self, property_name: str):
result = self._display_proxy.get_cached_property(property_name)
if result is not None:
return result.unpack()
# Fallback: value not in proxy cache yet (e.g. very early in startup)
return self._prop_cache.get(property_name)
def get_power_profile(self) -> str | None:
"""Get the current active power profile."""
if not self._power_profile_proxy:
return None
result = self._power_profile_proxy.get_cached_property("ActiveProfile")
return result.unpack() if result is not None else None
def set_power_profile(self, profile: str) -> bool:
"""Set the active power profile."""
if not self._power_profile_proxy:
return False
try:
parameters = GLib.Variant(
"(ssv)",
(POWER_PROFILE_INTERFACE, "ActiveProfile", GLib.Variant("s", profile)),
(POWER_PROFILE_BUS, "ActiveProfile", GLib.Variant("s", profile)),
)
self._bus.call_sync(
POWER_PROFILE_BUS_NAME,
POWER_PROFILE_BUS_PATH,
POWER_PROFILE_BUS,
POWER_PROFILE_PATH,
"org.freedesktop.DBus.Properties",
"Set",
parameters,
@@ -207,15 +687,53 @@ class Battery(Service):
return False
def get_available_power_profiles(self) -> list | None:
"""Get list of available power profiles."""
if not self._power_profile_proxy:
return None
result = self._power_profile_proxy.get_cached_property("Profiles")
if result is not None:
profiles_data = result.unpack()
profiles = []
for profile_dict in profiles_data:
if "Profile" in profile_dict:
profiles.append(profile_dict["Profile"])
return profiles
return [p["Profile"] for p in profiles_data if "Profile" in p]
return None
@property
def charge_limit(self) -> ChargeLimit | None:
return self._charge_limit
@property
def peripherals(self) -> list[Peripheral]:
return self._peripherals
@property
def system_battery(self) -> BatteryData | None:
return self._system_battery
def toggle_charge_limit(self) -> bool:
if self._charge_limit is None:
return False
try:
proxy = self._device_proxies.get(self._charge_limit.device_path)
if proxy is None:
return False
target = not self._charge_limit.enabled
self._toggling_charge_limit = True
try:
proxy.call_sync(
"EnableChargeThreshold",
GLib.Variant("(b)", (target,)),
Gio.DBusCallFlags.NONE,
-1,
None,
)
finally:
GLib.idle_add(self._clear_toggling_flag)
self._charge_limit = ChargeLimit(
enabled=target, device_path=self._charge_limit.device_path
)
return True
except Exception as e:
logger.error(f"[Battery] Failed to toggle charge limit: {e}")
return False
def _clear_toggling_flag(self) -> bool:
self._toggling_charge_limit = False
return False
+20 -44
View File
@@ -35,7 +35,9 @@
color: @glass-text;
}
#energy-mode-button-clickable {
#energy-mode-button-clickable,
#charge-limit-button-clickable,
#game-mode-button-clickable{
background: transparent;
border: none;
border-radius: apply(radius-sm);
@@ -43,15 +45,21 @@
margin: 1px 0;
}
#energy-mode-button-clickable:hover {
#energy-mode-button-clickable:hover,
#charge-limit-button-clickable:hover,
#game-mode-button-clickable:hover {
background: @glass-hover;
}
#energy-mode-button-clickable:hover #energy-mode-icon {
#energy-mode-button-clickable:hover #energy-mode-icon,
#game-mode-button-clickable:hover #game-mode-icon,
#charge-limit-button-clickable:hover #charge-limit-icon {
background-color: @glass-active;
}
#energy-mode-icon {
#energy-mode-icon,
#game-mode-icon,
#charge-limit-icon{
border-radius: apply(radius-full);
padding: 4px;
margin-top: -2px;
@@ -67,11 +75,13 @@
margin-bottom: -4px;
color: @glass-text-secondary;
}
.battery-section-title {
font-size: apply(font-base);
color: @glass-text-secondary;
margin: 2px 0 0 8px;
}
.battery-power-mode {
font-size: apply(font-md);
}
@@ -82,66 +92,32 @@
min-height: 30px;
border-radius: apply(radius-sm);
}
.battery-settings-button {
margin-top: -5px;
min-height: 30px;
border-radius: apply(radius-sm);
}
.battery-settings-button label {
font-weight: 500;
color: @glass-text;
}
#energy-mode-button:first-child {
margin-top: 0;
}
#energy-mode-button {
margin-top: -8px;
margin-bottom: 4px;
color: @glass-text-secondary;
}
.gamemode-button {
font-size: apply(font-sm);
margin-left: -6px;
color: @glass-text-secondary;
}
#game-mode-button {
margin: 2px 0;
}
#game-mode-icon {
margin-left: -6px;
}
#game-mode-button-clickable {
background: transparent;
border: none;
border-radius: apply(radius-sm);
padding: 6px 12px;
margin: 1px 0;
}
#game-mode-button-clickable:hover {
background: @glass-hover;
}
#game-mode-button-clickable:hover #game-mode-icon {
background-color: @glass-active;
}
#game-mode-icon {
border-radius: apply(radius-full);
padding: 4px;
margin-right: 8px;
background-color: @glass-hover;
color: @glass-text-secondary;
}
#energy-mode-icon.connected {
#energy-mode-icon.connected,
#charge-limit-icon.connected {
background-color: @glass-active;
color: @glass-text;
}
#game-mode-icon.connected {
background-color: @glass-active;
color: @glass-text;
}