diff --git a/modules/controlcenter/battery.py b/modules/controlcenter/battery.py index f9e67c1..92b3cb7 100644 --- a/modules/controlcenter/battery.py +++ b/modules/controlcenter/battery.py @@ -1,387 +1,377 @@ -from fabric.utils import get_relative_path from fabric.widgets.box import Box from fabric.widgets.button import Button from fabric.widgets.label import Label from fabric.widgets.centerbox import CenterBox -from fabric.widgets.svg import Svg from fabric.widgets.image import Image +from fabric.widgets.separator import Separator from services.battery import Battery import subprocess +from fabric.utils import get_relative_path import os +from gi.repository import GLib -class PowerProfileButton(Button): - def __init__(self, profile_name: str, display_name: str, battery_service: Battery, parent, **kwargs): +class EnergyModeButton(CenterBox): + def __init__( + self, + profile_name: str, + display_name: str, + icon_name: str, + battery_service: Battery, + parent, + **kwargs, + ): + super().__init__(name="energy-mode-button", **kwargs) self.profile_name = profile_name self.battery_service = battery_service self.parent = parent - # Get symbolic icon for profile - profile_icons = { - "power-saver": "power-profile-power-saver-symbolic", - "powersave": "power-profile-power-saver-symbolic", - "power_saver": "power-profile-power-saver-symbolic", - "balanced": "power-profile-balanced-symbolic", - "balance": "power-profile-balanced-symbolic", - "performance": "power-profile-performance-symbolic", - "performance-mode": "power-profile-performance-symbolic" - } - - icon_name = profile_icons.get(profile_name, "preferences-system-symbolic") - - # Create symbolic icon - from fabric.widgets.image import Image - self.profile_icon = Image( - icon_name=icon_name, - size=20, - style_classes="battery-profile-icon" + self.mode_icon = Image( + icon_name=f"battery-{icon_name}-symbolic", + size=16, + name="energy-mode-icon", + style_classes="battery-profile-icon", ) - self.profile_label = Label( + self.mode_label = Label( label=display_name, - style_classes="battery-profile-label" + style_classes="battery-power-source", + h_align="start", + h_expand=True, ) - # Create vertical layout for icon and label - content = Box( - orientation="vertical", - spacing=2, - children=[self.profile_icon, self.profile_label], - h_align="center" + start_box = Box( + orientation="horizontal", + spacing=8, + children=[self.mode_icon, self.mode_label], ) - - super().__init__( - child=content, + + self.button = Button( + child=start_box, + name="energy-mode-button-clickable", on_clicked=self.on_clicked, style_classes="battery-profile-button", - **kwargs ) - - # Update initial state + + self.start_children = [self.button] self.update_state() - + def on_clicked(self, *args): - """Handle profile selection""" success = self.battery_service.change_power_profile(self.profile_name) if success: # Update all profile buttons in parent - self.parent.update_profile_buttons() - + self.parent.update_energy_mode_buttons() + + # Reset icon state after short delay + GLib.timeout_add(300, lambda: self._reset_icon_state()) + + def _reset_icon_state(self): + return False # Remove timeout + def update_state(self): - """Update the visual state based on current active profile""" is_active = self.battery_service.power_profile == self.profile_name - if is_active: - self.add_style_class("active") + self.mode_icon.add_style_class("connected") else: - self.remove_style_class("active") + self.mode_icon.remove_style_class("connected") -class GameModeButton(Button): - def __init__(self, **kwargs): - self.script_path = get_relative_path("../../scripts/gamemode.sh") - self.gamemode_active = self.check_gamemode_status() +class GameModeButton(CenterBox): + def __init__(self, parent, **kwargs): + super().__init__(name="game-mode-button", **kwargs) + self.parent = parent - # Create icon for gamemode - self.gamemode_icon = Image( - icon_name="applications-games-symbolic" if self.gamemode_active else "applications-games-symbolic", - size=20, - style_classes="battery-gamemode-icon" + self.game_icon = Image( + icon_name="applications-games-symbolic", + size=16, + name="game-mode-icon", + style_classes="battery-gamemode-icon", ) - self.gamemode_label = Label( + self.game_label = Label( label="Game Mode", - style_classes="battery-gamemode-label" + style_classes="battery-power-source", + h_align="start", + h_expand=True, ) - # Create vertical layout for icon and label - content = Box( - orientation="vertical", - spacing=2, - children=[self.gamemode_icon, self.gamemode_label], - h_align="center" + start_box = Box( + orientation="horizontal", + spacing=8, + children=[self.game_icon, self.game_label], ) - super().__init__( - child=content, - on_clicked=self.toggle_gamemode, + self.button = Button( + child=start_box, + name="game-mode-button-clickable", + on_clicked=self.on_clicked, style_classes="battery-gamemode-button", - **kwargs ) - # Update initial state + self.start_children = [self.button] self.update_state() - def check_gamemode_status(self): - """Check if gamemode is currently active""" + def on_clicked(self, *args): try: - result = subprocess.run( - [self.script_path, "check"], - capture_output=True, - text=True, - timeout=5 - ) - return result.stdout.strip() == "t" - except Exception: - return False + script_path = get_relative_path("../../scripts/gamemode.sh") + subprocess.run([script_path], check=False) - def toggle_gamemode(self, *args): - """Toggle gamemode state""" - try: - # Make script executable if it isn't - os.chmod(self.script_path, 0o755) - - # Run the toggle script - subprocess.run([self.script_path], timeout=10) - - # Update state after toggle - self.gamemode_active = self.check_gamemode_status() - self.update_state() + GLib.timeout_add(500, lambda: self.update_state()) except Exception as e: - print(f"Failed to toggle gamemode: {e}") + print(f"Failed to toggle game mode: {e}") + + GLib.timeout_add(300, lambda: self._reset_icon_state()) + + def _reset_icon_state(self): + return False # Remove timeout def update_state(self): - """Update the visual state based on gamemode status""" - if self.gamemode_active: - self.add_style_class("active") - self.gamemode_icon.set_property("style-classes", "battery-gamemode-icon active") - else: - self.remove_style_class("active") - self.gamemode_icon.set_property("style-classes", "battery-gamemode-icon") + try: + script_path = get_relative_path("../../scripts/gamemode.sh") + result = subprocess.run( + [script_path, "check"], capture_output=True, text=True, check=False + ) + is_active = result.stdout.strip() == "t" + + if is_active: + self.game_icon.add_style_class("connected") + else: + self.game_icon.remove_style_class("connected") + except Exception as e: + print(f"Failed to check game mode status: {e}") + # Default to inactive state on error + self.game_icon.remove_style_class("connected") + + return False # Remove timeout if called from GLib.timeout_add class BatteryControl(Box): def __init__(self, parent, **kwargs): super().__init__( - spacing=0, + spacing=12, orientation="vertical", name="control-center-widgets", **kwargs, ) - self.set_size_request(300, -1) + self.set_size_request(400, -1) self.parent = parent self.battery_service = Battery() - self.profile_buttons = [] + self.energy_mode_buttons = [] - # Battery information widget in control center style - self.battery_info_widget = Box( - name="battery-info-widget", + self.battery_widget = Box( + name="battery-widget", orientation="vertical", - style_classes="menu", + style_classes="battery-status-section", h_expand=True, - children=[ - Label(label="Battery", style_classes="title", h_align="start"), - ] - ) - - # Battery status labels - self.battery_percentage = Label( - label="100%", - style_classes="battery-info-item", - h_align="start" - ) - - self.battery_state = Label( - label="Fully Charged", - style_classes="battery-info-item", - h_align="start" - ) - - self.battery_capacity = Label( - label="", - style_classes="battery-info-item", - h_align="start" - ) - - self.battery_time_to_full = Label( - label="", - style_classes="battery-info-item", - h_align="start", - visible=False - ) - - self.battery_time_to_empty = Label( - label="", - style_classes="battery-info-item", - h_align="start", - visible=False - ) - - self.battery_temperature = Label( - label="", - style_classes="battery-info-item", - h_align="start" - ) - - # Add battery info to widget - self.battery_info_widget.add(self.battery_percentage) - self.battery_info_widget.add(self.battery_state) - self.battery_info_widget.add(self.battery_capacity) - self.battery_info_widget.add(self.battery_time_to_full) - self.battery_info_widget.add(self.battery_time_to_empty) - self.battery_info_widget.add(self.battery_temperature) - - # Power profiles widget in control center style - self.profiles_widget = Box( - name="power-profiles-widget", - orientation="vertical", - style_classes="menu", - h_expand=True, - children=[ - Label(label="Power Profiles", style_classes="title", h_align="start"), - ] - ) - - # Power profiles container - self.profiles_container = Box( - orientation="horizontal", spacing=8, - style_classes="battery-profiles-container", - h_align="center" ) - self.profiles_widget.add(self.profiles_container) - - # Game mode widget in control center style - self.gamemode_widget = Box( - name="gamemode-widget", - orientation="vertical", - style_classes="menu", - h_expand=True, - children=[ - Label(label="Game Mode", style_classes="title", h_align="start"), - ] + self.battery_title = Label( + label="Battery", style_classes="battery-main-title", h_align="start" ) - # Add gamemode button - self.gamemode_button = GameModeButton() - gamemode_container = Box( - orientation="horizontal", - spacing=8, - style_classes="battery-gamemode-container", - h_align="center", - children=[self.gamemode_button] + self.battery_percentage_label = Label( + label="80%", style_classes="battery-percentage", h_align="end" ) - self.gamemode_widget.add(gamemode_container) - # Add all widgets to main container - self.add(self.battery_info_widget) - self.add(self.profiles_widget) - self.add(self.gamemode_widget) + self.battery_header = CenterBox( + start_children=self.battery_title, + end_children=self.battery_percentage_label, + name="battery-header", + ) + + self.power_source_label = Label( + label="Power Source: Power Adapter", + style_classes="battery-power-source", + h_align="start", + ) + + self.charging_time_label = Label( + label="1h 4m until fully charged", + style_classes="battery-power-source", + h_align="start", + ) + + self.energy_mode_section = Box( + orientation="vertical", spacing=8, name="energy-mode-section" + ) + + self.energy_mode_title = Label( + label="Energy Mode", style_classes="battery-section-title", h_align="start" + ) + + self.energy_modes_container = Box( + orientation="vertical", spacing=4, name="energy-modes-container" + ) + + self.game_mode_section = Box( + orientation="vertical", spacing=8, name="game-mode-section" + ) + + self.game_mode_title = Label( + label="Game Mode", style_classes="battery-section-title", h_align="start" + ) + + self.game_mode_container = Box( + orientation="vertical", spacing=4, name="game-mode-container" + ) + + self.battery_settings_button = Button( + child=Label( + label="Battery Settings...", + style_classes="battery-power-source", + h_align="start", + ), + style_classes="battery-settings-button", + on_clicked=self.open_battery_settings, + ) + + self.battery_widget.add(self.battery_header) + self.battery_widget.add(self.power_source_label) + self.battery_widget.add(self.charging_time_label) + + separator1 = Separator(orientation="h", name="separator") + self.battery_widget.add(separator1) + + self.energy_mode_section.add(self.energy_mode_title) + self.energy_mode_section.add(self.energy_modes_container) + self.battery_widget.add(self.energy_mode_section) + + separator2 = Separator(orientation="h", name="separator") + self.battery_widget.add(separator2) + + self.game_mode_section.add(self.game_mode_title) + self.game_mode_section.add(self.game_mode_container) + self.battery_widget.add(self.game_mode_section) + + separator3 = Separator(orientation="h", name="separator") + self.battery_widget.add(separator3) + + self.battery_widget.add(self.battery_settings_button) + + self.add(self.battery_widget) - # Connect to battery service signals self.battery_service.connect("changed", self.on_battery_changed) self.battery_service.connect("profile_changed", self.on_profile_changed) # Initialize display self.update_battery_info() - self.create_profile_buttons() - + self.create_energy_mode_buttons() + self.create_game_mode_button() - - def create_profile_buttons(self): - """Create buttons for available power profiles""" + def open_battery_settings(self, *args): + # TODO: Implement to open Battery Settings + pass + + def create_energy_mode_buttons(self): # Clear existing buttons - for button in self.profile_buttons: + for button in self.energy_mode_buttons: button.destroy() - self.profile_buttons.clear() - + self.energy_mode_buttons.clear() + # Get available profiles available_profiles = self.battery_service.available_profiles - + if not available_profiles: no_profiles_label = Label( - label="No power profiles available", + label="No energy modes available", style_classes="battery-no-profiles", - h_align="center" + h_align="start", ) - self.profiles_container.add(no_profiles_label) + self.energy_modes_container.add(no_profiles_label) return - - # Create button for each profile + + # Define energy mode mappings with proper icon names + energy_mode_config = { + "balanced": {"display": "Automatic", "icon": "good"}, + "power-saver": {"display": "Low Power", "icon": "low"}, + "powersave": {"display": "Low Power", "icon": "low"}, + "performance": {"display": "High Power", "icon": "full"}, + } + + # Define the desired order for energy modes + desired_order = ["balanced", "power-saver", "powersave", "performance"] + + # Create ordered list of available profiles + ordered_profiles = [] + for profile_name in desired_order: + if profile_name in available_profiles: + ordered_profiles.append(profile_name) + + # Add any remaining profiles not in the desired order for profile in available_profiles: - display_name = Battery.get_profile_display_name(profile) - button = PowerProfileButton( - profile_name=profile, - display_name=display_name, - battery_service=self.battery_service, - parent=self + if profile not in ordered_profiles: + ordered_profiles.append(profile) + + # Create button for each available profile in the specified order + for profile in ordered_profiles: + config = energy_mode_config.get( + profile, {"display": profile.title(), "icon": "good"} ) - self.profile_buttons.append(button) - self.profiles_container.add(button) - - def update_profile_buttons(self): - """Update the state of all profile buttons""" - for button in self.profile_buttons: + + button = EnergyModeButton( + profile_name=profile, + display_name=config["display"], + icon_name=config["icon"], + battery_service=self.battery_service, + parent=self, + ) + self.energy_mode_buttons.append(button) + self.energy_modes_container.add(button) + + def update_energy_mode_buttons(self): + for button in self.energy_mode_buttons: button.update_state() + def create_game_mode_button(self): + # Clear existing game mode button if any + for child in list(self.game_mode_container.get_children()): + child.destroy() + + # Create game mode button + self.game_mode_button = GameModeButton(parent=self) + self.game_mode_container.add(self.game_mode_button) + def update_battery_info(self): - """Update battery information display""" if not self.battery_service.is_present: - self.battery_percentage.set_label("No Battery") - self.battery_state.set_label("Not Present") - self.battery_capacity.set_label("") - self.battery_time_to_full.set_visible(False) - self.battery_time_to_empty.set_visible(False) - self.battery_temperature.set_label("") + self.battery_percentage_label.set_label("No Battery") + self.power_source_label.set_label("Power Source: Not Present") + self.charging_time_label.set_label("") return - # Update percentage + # Update percentage in header percentage = self.battery_service.percentage - self.battery_percentage.set_label(f"Charge: {percentage}%") + self.battery_percentage_label.set_label(f"{percentage}%") - # Update state + # Update power source and charging info state = self.battery_service.state - state_text = state.replace("_", " ").title() - self.battery_state.set_label(f"Status: {state_text}") - # Update capacity - capacity = getattr(self.battery_service, 'capacity', None) - if capacity is not None: - try: - # Try to convert to float if it's a string - capacity_value = float(capacity) - self.battery_capacity.set_label(f"Capacity: {capacity_value:.1f} Wh") - except (ValueError, TypeError): - # If conversion fails, display as string - self.battery_capacity.set_label(f"Capacity: {capacity}") + if state == "CHARGING": + self.power_source_label.set_label("Power Source: Power Adapter") + time_to_full = self.battery_service.time_to_full + if time_to_full != "N/A": + self.charging_time_label.set_label( + f"{time_to_full} until fully charged" + ) + else: + self.charging_time_label.set_label("Charging...") + elif state == "FULLY_CHARGED": + self.power_source_label.set_label("Power Source: Power Adapter") + self.charging_time_label.set_label("Fully Charged") + elif state == "DISCHARGING": + self.power_source_label.set_label("Power Source: Battery") + time_to_empty = self.battery_service.time_to_empty + if time_to_empty != "N/A": + self.charging_time_label.set_label(f"{time_to_empty} remaining") + else: + self.charging_time_label.set_label("On Battery Power") else: - self.battery_capacity.set_label("Capacity: N/A") - - # Update time to full - time_to_full = self.battery_service.time_to_full - if time_to_full != "N/A" and state == "CHARGING": - self.battery_time_to_full.set_label(f"Time to full: {time_to_full}") - self.battery_time_to_full.set_visible(True) - else: - self.battery_time_to_full.set_visible(False) - - # Update time to empty - time_to_empty = self.battery_service.time_to_empty - if time_to_empty != "N/A" and state == "DISCHARGING": - self.battery_time_to_empty.set_label(f"Time remaining: {time_to_empty}") - self.battery_time_to_empty.set_visible(True) - else: - self.battery_time_to_empty.set_visible(False) - - # Update temperature - temperature = getattr(self.battery_service, 'temperature', None) - if temperature is not None: - try: - # Try to convert to float and convert from tenths of degrees Celsius to Celsius - temp_value = float(temperature) / 10.0 - self.battery_temperature.set_label(f"Temperature: {temp_value:.1f}°C") - except (ValueError, TypeError): - # If conversion fails, display as string - self.battery_temperature.set_label(f"Temperature: {temperature}") - else: - self.battery_temperature.set_label("Temperature: N/A") + self.power_source_label.set_label("Power Source: Unknown") + self.charging_time_label.set_label("") def on_battery_changed(self, *args): - """Handle battery service changes""" self.update_battery_info() def on_profile_changed(self, service, new_profile): - """Handle power profile changes""" - self.update_profile_buttons() + self.update_energy_mode_buttons() diff --git a/styles/battery-widget.css b/styles/battery-widget.css index 9594596..48c787b 100644 --- a/styles/battery-widget.css +++ b/styles/battery-widget.css @@ -1,20 +1,15 @@ .battery-main-title { font-size: 1em; - font-weight: 600; + font-weight: 900; color: rgba(255, 255, 255, 0.95); margin: 6px 10px; } .battery-power-source { font-size: 0.9em; + font-weight: 500; color: rgba(255, 255, 255, 0.8); - margin: 4px 12px; -} - -.battery-separator { - min-height: 1px; - background: rgba(255, 255, 255, 0.1); - margin: 4px 0; + margin: 0px 12px; } .battery-section-title { @@ -29,42 +24,12 @@ padding: 4px 0; } -.battery-energy-indicator { - font-size: 0.8em; - margin-right: 4px; -} - -.battery-energy-app { - font-size: 0.9em; - color: rgba(255, 255, 255, 0.8); -} - -.battery-settings-button { - background: transparent; - border: none; - padding: 8px 12px; - margin: 4px 0; - border-radius: 0; -} - -.battery-settings-button:hover { - background: rgba(255, 255, 255, 0.1); -} - -.battery-info-item { - font-size: 0.85em; - font-weight: 500; - color: rgba(255, 255, 255, 0.8); - margin: 2px 10px; - padding: 1px 0; -} - .battery-status-section { - background: rgba(255, 255, 255, 0.06); - border-radius: 12px; - padding: 16px; - margin: 8px 0; - border: 1px solid rgba(255, 255, 255, 0.08); + /* background: rgba(255, 255, 255, 0.06); */ + /* border-radius: 12px; */ + padding: 2px; + /* margin: 8px 0; */ + /* border: 1px solid rgba(255, 255, 255, 0.08); */ } .battery-percentage { @@ -73,112 +38,74 @@ color: rgba(255, 255, 255, 0.95); } -.battery-state { - font-size: 0.9em; - color: rgba(255, 255, 255, 0.8); - margin-top: 4px; -} - -.battery-time { - font-size: 0.8em; - color: rgba(255, 255, 255, 0.6); - margin-top: 2px; -} - -.battery-capacity { - font-size: 0.8em; - color: rgba(255, 255, 255, 0.7); - margin-top: 2px; -} - -.battery-temperature { - font-size: 0.8em; - color: rgba(255, 255, 255, 0.7); - margin-top: 2px; -} - .battery-section-title { font-size: 0.95em; font-weight: 500; color: rgba(255, 255, 255, 0.9); - margin: 16px 0 8px 0; + margin: 0px 10px; } -.battery-profiles-container { - background: transparent; - padding: 2px 8px; - margin: 2px 0; -} - -.battery-profile-button { +#energy-mode-button-clickable { background: transparent; border: none; - border-radius: 6px; - padding: 4px 6px; - margin: 0 1px; + border-radius: 8px; + padding: 6px 12px; + margin: 1px 0; transition: all 0.2s ease; - min-width: 45px; } -.battery-profile-button:hover { +#energy-mode-button-clickable:hover { background: rgba(255, 255, 255, 0.1); } -.battery-profile-button.active { - background: alpha(#2369ff, 0.2); +#energy-mode-icon { + border-radius: 50%; + padding: 4px; + margin-right: 8px; + background-color: rgba(255, 255, 255, 0.1); + color: rgba(255, 255, 255, 0.7); + transition: all 0.2s ease; } -.battery-profile-icon { - color: rgba(255, 255, 255, 0.8); - margin-bottom: 2px; +#energy-mode-icon.connected { + background-color: #007aff; + color: #ffffff; } -.battery-profile-button.active .battery-profile-icon { - color: rgba(0, 122, 255, 1); +#energy-mode-button .battery-power-source { + margin: 0; + font-size: 0.9em; + color: rgba(255, 255, 255, 0.85); } -.battery-no-profiles { - font-size: 0.85em; - color: rgba(255, 255, 255, 0.6); - padding: 16px; -} - -/* Gamemode Button Styles */ -.battery-gamemode-container { - background: transparent; - padding: 2px 8px; +#game-mode-button { margin: 2px 0; } -.battery-gamemode-button { +#game-mode-button-clickable { background: transparent; border: none; - border-radius: 6px; - padding: 4px 6px; - margin: 0 1px; + border-radius: 8px; + padding: 6px 12px; + margin: 1px 0; transition: all 0.2s ease; - min-width: 45px; + /* min-height: 36px; */ } -.battery-gamemode-button:hover { +#game-mode-button-clickable:hover { background: rgba(255, 255, 255, 0.1); } -.battery-gamemode-button.active { - background: alpha(#2369ff, 0.2); - /* background: rgba(255, 69, 0, 0.2); */ +#game-mode-icon { + border-radius: 50%; + padding: 4px; + margin-right: 8px; + background-color: rgba(255, 255, 255, 0.1); + color: rgba(255, 255, 255, 0.7); + transition: all 0.2s ease; } -.battery-gamemode-icon { - color: rgba(255, 255, 255, 0.8); - margin-bottom: 2px; -} - -.battery-gamemode-button.active .battery-gamemode-icon { - color: rgba(255, 69, 0, 1); -} - -.battery-gamemode-label { - font-size: 0.8em; - color: rgba(255, 255, 255, 0.8); +#game-mode-icon.connected { + background-color: #ff4500; + color: #ffffff; }