From 8b4d8296813d0bb183dacd68d9a7ae004cdc18d2 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sun, 22 Jun 2025 18:47:25 +0545 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A7=20chore(workspaces):=20update=20Wo?= =?UTF-8?q?rkspaces=20and=20WorkspaceButton=20instances,=20remove=20Hyprla?= =?UTF-8?q?nd=20import?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/dock/components/__init__.py | 10 +- modules/dock/components/battery.py | 15 +- modules/dock/components/controls.py | 75 +++++---- modules/dock/components/indicators.py | 27 +-- modules/dock/components/metrics.py | 154 ++++++++++------- modules/dock/components/music_player.py | 213 +++++++----------------- modules/dock/components/tray.py | 170 ------------------- modules/dock/components/workspaces.py | 22 +-- 8 files changed, 219 insertions(+), 467 deletions(-) delete mode 100644 modules/dock/components/tray.py diff --git a/modules/dock/components/__init__.py b/modules/dock/components/__init__.py index 3b8f27f..9e03f6d 100644 --- a/modules/dock/components/__init__.py +++ b/modules/dock/components/__init__.py @@ -111,7 +111,15 @@ class DockComponents(Box): for component_name, widget in components.items(): if component_name in self.component_visibility: - widget.set_visible(self.component_visibility[component_name]) + # Special handling for applications component - let it manage its own visibility + if component_name == "applications": + # Only apply config visibility if the component has content + if hasattr(widget, '_update_visibility_based_on_content'): + widget._update_visibility_based_on_content() + else: + widget.set_visible(self.component_visibility[component_name]) + else: + widget.set_visible(self.component_visibility[component_name]) def toggle_component_visibility(self, component_name): components = { diff --git a/modules/dock/components/battery.py b/modules/dock/components/battery.py index ffcf929..dbfd132 100644 --- a/modules/dock/components/battery.py +++ b/modules/dock/components/battery.py @@ -1,12 +1,11 @@ -from gi.repository import GLib, Gdk - -from fabric.widgets.label import Label -from fabric.widgets.box import Box -from fabric.widgets.circularprogressbar import CircularProgressBar -from fabric.widgets.button import Button -from services.battery import Battery as BatteryService -import utils.icons as icons import config.data as data +import utils.icons as icons +from fabric.widgets.box import Box +from fabric.widgets.button import Button +from fabric.widgets.circularprogressbar import CircularProgressBar +from fabric.widgets.label import Label +from gi.repository import Gdk, GLib +from services.battery import Battery as BatteryService class Battery(Box): diff --git a/modules/dock/components/controls.py b/modules/dock/components/controls.py index 37bd484..1d2e257 100644 --- a/modules/dock/components/controls.py +++ b/modules/dock/components/controls.py @@ -1,3 +1,5 @@ +import config.data as data +import utils.icons as icons from fabric.audio.service import Audio from fabric.widgets.box import Box from fabric.widgets.button import Button @@ -5,11 +7,7 @@ from fabric.widgets.circularprogressbar import CircularProgressBar from fabric.widgets.eventbox import EventBox from fabric.widgets.label import Label from fabric.widgets.overlay import Overlay -from fabric.widgets.scale import Scale from gi.repository import Gdk, GLib - -import config.data as data -import utils.icons as icons from services.brightness import Brightness as BrightnessService @@ -22,17 +20,19 @@ class Brightness(Box): return self.progress_bar = CircularProgressBar( - name="button-brightness", size=28, line_width=2, - start_angle=150, end_angle=390, + name="button-brightness", + size=28, + line_width=2, + start_angle=150, + end_angle=390, + ) + self.brightness_label = Label( + name="brightness-label", markup=icons.brightness_high ) - self.brightness_label = Label(name="brightness-label", markup=icons.brightness_high) self.brightness_button = Button(child=self.brightness_label) self.event_box = EventBox( events=["scroll", "smooth-scroll"], - child=Overlay( - child=self.progress_bar, - overlays=self.brightness_button - ), + child=Overlay(child=self.progress_bar, overlays=self.brightness_button), ) self.event_box.connect("scroll-event", self.on_scroll) self.add(self.event_box) @@ -49,22 +49,20 @@ class Brightness(Box): def on_scroll(self, _, event): if self.brightness.max_screen == -1: return - + current_brightness = self.brightness.screen_brightness max_brightness = self.brightness.max_screen step = max(1, int(max_brightness * 0.01)) # 1% of max brightness - + if event.direction == Gdk.ScrollDirection.SMOOTH: - if hasattr(event, 'delta_y') and abs(event.delta_y) > 0: + if hasattr(event, "delta_y") and abs(event.delta_y) > 0: new_brightness = current_brightness - int(event.delta_y * step) self.brightness.screen_brightness = new_brightness - # Remove delta_x handling elif event.direction == Gdk.ScrollDirection.UP: self.brightness.screen_brightness = current_brightness + step elif event.direction == Gdk.ScrollDirection.DOWN: self.brightness.screen_brightness = current_brightness - step - def on_progress_value_changed(self, widget, pspec): if self._updating_from_brightness: return @@ -72,10 +70,15 @@ class Brightness(Box): new_brightness = int(new_norm * self.brightness.max_screen) self._pending_value = new_brightness if self._update_source_id is None: - self._update_source_id = GLib.timeout_add(50, self._update_brightness_callback) + self._update_source_id = GLib.timeout_add( + 50, self._update_brightness_callback + ) def _update_brightness_callback(self): - if self._pending_value is not None and self._pending_value != self.brightness.screen_brightness: + if ( + self._pending_value is not None + and self._pending_value != self.brightness.screen_brightness + ): self.brightness.screen_brightness = self._pending_value self._pending_value = None return True @@ -105,13 +108,17 @@ class Brightness(Box): GLib.source_remove(self._update_source_id) super().destroy() + class Volume(Box): def __init__(self, **kwargs): super().__init__(name="button-bar-vol", **kwargs) self.audio = Audio() self.progress_bar = CircularProgressBar( - name="button-volume", size=28, line_width=2, - start_angle=150, end_angle=390, + name="button-volume", + size=28, + line_width=2, + start_angle=150, + end_angle=390, ) self.vol_label = Label(name="vol-label", markup=icons.vol_high) self.vol_button = Button(on_clicked=self.toggle_mute, child=self.vol_label) @@ -148,12 +155,12 @@ class Volume(Box): def on_scroll(self, _, event): if not self.audio.speaker: return - + current_volume = self.audio.speaker.volume step = 1 - + if event.direction == Gdk.ScrollDirection.SMOOTH: - if hasattr(event, 'delta_y') and abs(event.delta_y) > 0: + if hasattr(event, "delta_y") and abs(event.delta_y) > 0: new_volume = current_volume - int(event.delta_y * step) self.audio.speaker.volume = new_volume # Remove delta_x handling @@ -165,7 +172,7 @@ class Volume(Box): def on_speaker_changed(self, *_): if not self.audio.speaker: return - + vol_high_icon = icons.vol_high vol_medium_icon = icons.vol_medium vol_mute_icon = icons.vol_off @@ -178,7 +185,7 @@ class Volume(Box): vol_off_icon = icons.bluetooth_disconnected self.progress_bar.value = self.audio.speaker.volume / 100 - + if self.audio.speaker.muted: self.vol_label.set_markup(vol_mute_icon) self.progress_bar.add_style_class("muted") @@ -196,13 +203,17 @@ class Volume(Box): else: self.vol_label.set_markup(vol_off_icon) + class MicroPhone(Box): def __init__(self, **kwargs): super().__init__(name="button-bar-mic", **kwargs) self.audio = Audio() self.progress_bar = CircularProgressBar( - name="button-mic", size=28, line_width=2, - start_angle=150, end_angle=390, + name="button-mic", + size=28, + line_width=2, + start_angle=150, + end_angle=390, ) self.mic_label = Label(name="mic-label", markup=icons.mic) self.mic_button = Button(on_clicked=self.toggle_mute, child=self.mic_label) @@ -240,9 +251,8 @@ class MicroPhone(Box): if not self.audio.microphone: return if event.direction == Gdk.ScrollDirection.SMOOTH: - if hasattr(event, 'delta_y') and abs(event.delta_y) > 0: + if hasattr(event, "delta_y") and abs(event.delta_y) > 0: self.audio.microphone.volume -= event.delta_y - # Remove delta_x handling elif event.direction == Gdk.ScrollDirection.UP: self.audio.microphone.volume += 1 elif event.direction == Gdk.ScrollDirection.DOWN: @@ -251,9 +261,9 @@ class MicroPhone(Box): def on_microphone_changed(self, *_): if not self.audio.microphone: return - + self.progress_bar.value = self.audio.microphone.volume / 100 - + if self.audio.microphone.muted: self.mic_label.set_markup(icons.mic_mute) self.progress_bar.add_style_class("muted") @@ -271,11 +281,12 @@ class MicroPhone(Box): else: self.mic_label.set_markup(icons.mic_mute) + class Controls(Box): def __init__(self, **kwargs): brightness = BrightnessService.get_initial() children = [] - if (brightness.screen_brightness != -1): + if brightness.screen_brightness != -1: children.append(Brightness()) children.extend([Volume(), MicroPhone()]) super().__init__( diff --git a/modules/dock/components/indicators.py b/modules/dock/components/indicators.py index 61af85e..14858b8 100644 --- a/modules/dock/components/indicators.py +++ b/modules/dock/components/indicators.py @@ -1,22 +1,19 @@ -from fabric.widgets.box import Box -from fabric.widgets.button import Button -from fabric.widgets.label import Label -from fabric.widgets.overlay import Overlay -from fabric.widgets.circularprogressbar import CircularProgressBar - import config.data as data import utils.icons as icons -from services.network import NetworkClient from fabric.bluetooth import BluetoothClient +from fabric.widgets.box import Box +from fabric.widgets.button import Button +from fabric.widgets.circularprogressbar import CircularProgressBar +from fabric.widgets.label import Label +from fabric.widgets.overlay import Overlay +from services.network import NetworkClient class WifiIndicator(Button): def __init__(self, **kwargs): super().__init__(name="button-bar-network", **kwargs) - # Initialize NetworkClient self.network = NetworkClient() - # Create progress bar and wifi label self.progress_bar = CircularProgressBar( name="button-network", size=28, @@ -26,13 +23,10 @@ class WifiIndicator(Button): ) self.wifi_label = Label(name="network-label", markup=icons.loader) - # Create overlay with progress bar and label self.add(Overlay(child=self.progress_bar, overlays=self.wifi_label)) - # Connect signals self.network.connect("ready", self.on_network_ready) - # If already ready, set up signals immediately if self.network.is_ready: self.setup_signals() @@ -40,27 +34,23 @@ class WifiIndicator(Button): self.setup_signals() def setup_signals(self): - # Connect to NetworkClient signals self.network.connect("changed", self.on_network_changed) self.network.connect("wifi-device-added", self.on_wifi_device_added) self.network.connect("wifi-device-removed", self.on_wifi_device_removed) self.network.connect("ethernet-device-added", self.on_ethernet_device_added) self.network.connect("ethernet-device-removed", self.on_ethernet_device_removed) - # Connect to WiFi device if available if self.network.wifi_device: self.network.wifi_device.connect("changed", self.on_wifi_changed) self.network.wifi_device.connect("ap-added", self.on_wifi_changed) self.network.wifi_device.connect("ap-removed", self.on_wifi_changed) - # Connect to Ethernet device if available if self.network.ethernet_device: self.network.ethernet_device.connect("changed", self.on_ethernet_changed) self.update_state() def update_state(self): - # Check ethernet first if ( self.network.ethernet_device and self.network.ethernet_device.state == "activated" @@ -71,7 +61,6 @@ class WifiIndicator(Button): self.set_tooltip_text(tooltip) return - # Then check wifi if not self.network.wifi_device: self.wifi_label.set_markup(icons.cloud_off) self.progress_bar.value = 0 @@ -132,19 +121,15 @@ class BluetoothIndicator(Button): def __init__(self, **kwargs): super().__init__(name="button-bar-bluetooth", **kwargs) - # Initialize BluetoothClient self.bluetooth = BluetoothClient() - # Create bluetooth label self.bt_label = Label(name="bluetooth-label", markup=icons.loader) self.add(self.bt_label) - # Connect signals self.bluetooth.connect("changed", self.on_bluetooth_changed) self.bluetooth.connect("device-added", self.on_device_added) self.bluetooth.connect("device-removed", self.on_device_removed) - # Initial state update self.update_state() def update_state(self): diff --git a/modules/dock/components/metrics.py b/modules/dock/components/metrics.py index 0cf6b5d..04abce0 100644 --- a/modules/dock/components/metrics.py +++ b/modules/dock/components/metrics.py @@ -1,8 +1,9 @@ import json -import logging import subprocess +import config.data as data import psutil +import utils.icons as icons from fabric.widgets.box import Box from fabric.widgets.button import Button from fabric.widgets.circularprogressbar import CircularProgressBar @@ -10,16 +11,8 @@ from fabric.widgets.label import Label from fabric.widgets.revealer import Revealer from gi.repository import GLib -import config.data as data -import utils.icons as icons - -logger = logging.getLogger(__name__) class MetricsProvider: - """ - Class responsible for obtaining centralized CPU, memory, disk usage, and battery metrics. - It updates periodically so that all widgets querying it display the same values. - """ def __init__(self): self.gpu = [] self.cpu = 0.0 @@ -47,13 +40,11 @@ class MetricsProvider: return True def _start_gpu_update_async(self): - """Starts a new GLib thread to run nvtop in the background.""" self._gpu_update_running = True GLib.Thread.new("nvtop-thread", lambda _: self._run_nvtop_in_thread(), None) def _run_nvtop_in_thread(self): - """Runs nvtop via subprocess in a separate GLib thread.""" output = None error_message = None try: @@ -61,34 +52,29 @@ class MetricsProvider: output = result except FileNotFoundError: error_message = "nvtop command not found." - logger.warning(error_message) except subprocess.CalledProcessError as e: - error_message = f"nvtop failed with exit code {e.returncode}: {e.stderr.strip()}" - logger.error(error_message) + error_message = ( + f"nvtop failed with exit code {e.returncode}: {e.stderr.strip()}" + ) except subprocess.TimeoutExpired: error_message = "nvtop command timed out." - logger.error(error_message) except Exception as e: error_message = f"Unexpected error running nvtop: {e}" - logger.error(error_message) GLib.idle_add(self._process_gpu_output, output, error_message) self._gpu_update_running = False def _process_gpu_output(self, output, error_message): - """Process nvtop JSON output on the main loop.""" try: if error_message: - logger.error(f"GPU update failed: {error_message}") self.gpu = [] elif output: try: info = json.loads(output) if not info or not isinstance(info, list): - logger.warning("Invalid nvtop data format") self.gpu = [] return False - + gpu_utils = [] for gpu in info: if not isinstance(gpu, dict): @@ -102,19 +88,15 @@ class MetricsProvider: gpu_utils.append(util_value) except (ValueError, TypeError): continue - + self.gpu = gpu_utils if gpu_utils else [] - except (KeyError, ValueError, TypeError, AttributeError) as e: - logger.error(f"Failed parsing nvtop JSON: {e}") + except (KeyError, ValueError, TypeError, AttributeError): self.gpu = [] else: - logger.warning("nvtop returned no output.") self.gpu = [] - except json.JSONDecodeError as e: - logger.error(f"JSON decode error: {e}") + except json.JSONDecodeError: self.gpu = [] - except Exception as e: - logger.error(f"Error processing nvtop output: {e}") + except Exception: self.gpu = [] return False @@ -130,45 +112,42 @@ class MetricsProvider: result = subprocess.check_output(["nvtop", "-s"], text=True, timeout=5) if not result.strip(): return [] - + info = json.loads(result) if not info or not isinstance(info, list): return [] - + valid_gpus = [] for gpu in info: if not isinstance(gpu, dict): continue - + gpu_data = { - 'device_name': gpu.get('device_name', 'GPU'), - 'gpu_util': gpu.get('gpu_util', '0%'), - 'mem_util': gpu.get('mem_util', '0%'), - 'temperature': gpu.get('temperature', '0 C'), - 'power': gpu.get('power', '0 W') + "device_name": gpu.get("device_name", "GPU"), + "gpu_util": gpu.get("gpu_util", "0%"), + "mem_util": gpu.get("mem_util", "0%"), + "temperature": gpu.get("temperature", "0 C"), + "power": gpu.get("power", "0 W"), } valid_gpus.append(gpu_data) - + return valid_gpus - + except FileNotFoundError: - logger.warning("nvtop not found; GPU info unavailable.") return [] - except subprocess.CalledProcessError as e: - logger.error(f"nvtop init sync failed: {e}") + except subprocess.CalledProcessError: return [] except subprocess.TimeoutExpired: - logger.error("nvtop init call timed out.") return [] - except json.JSONDecodeError as e: - logger.error(f"Init JSON parse error: {e}") + except json.JSONDecodeError: return [] - except Exception as e: - logger.error(f"Unexpected error during GPU init: {e}") + except Exception: return [] + shared_provider = MetricsProvider() + class SingularMetric: def __init__(self, id, name, icon): self.name_markup = name @@ -205,6 +184,7 @@ class SingularMetric: def markup(self): return f"{self.icon_markup} {self.name_markup}: {self.level.get_label()}" + class Metrics(Button): def __init__(self, **kwargs): super().__init__(name="metrics", **kwargs) @@ -216,17 +196,53 @@ class Metrics(Button): all_visible=True, ) - visible = getattr(data, "METRICS_VISIBLE", {'cpu': True, 'ram': True, 'swap': True, 'disk': True, 'gpu': True}) - disks = [SingularMetric("disk", f"DISK ({path})" if len(data.METRICS_DISKS) != 1 else "DISK", icons.disk) - for path in data.METRICS_DISKS] if visible.get('disk', True) else [] + visible = getattr( + data, + "METRICS_VISIBLE", + {"cpu": True, "ram": True, "swap": True, "disk": True, "gpu": True}, + ) + disks = ( + [ + SingularMetric( + "disk", + f"DISK ({path})" if len(data.METRICS_DISKS) != 1 else "DISK", + icons.disk, + ) + for path in data.METRICS_DISKS + ] + if visible.get("disk", True) + else [] + ) gpu_info = shared_provider.get_gpu_info() - gpus = [SingularMetric(f"gpu", f"GPU ({v['device_name']})" if len(gpu_info) != 1 else "GPU", icons.gpu) - for v in gpu_info] if visible.get('gpu', True) else [] + gpus = ( + [ + SingularMetric( + "gpu", + f"GPU ({v['device_name']})" if len(gpu_info) != 1 else "GPU", + icons.gpu, + ) + for v in gpu_info + ] + if visible.get("gpu", True) + else [] + ) - self.cpu = SingularMetric("cpu", "CPU", icons.cpu) if visible.get('cpu', True) else None - self.ram = SingularMetric("ram", "RAM", icons.memory) if visible.get('ram', True) else None - self.swap = SingularMetric("swap", "SWAP", icons.swap) if visible.get('swap', True) else None + self.cpu = ( + SingularMetric("cpu", "CPU", icons.cpu) + if visible.get("cpu", True) + else None + ) + self.ram = ( + SingularMetric("ram", "RAM", icons.memory) + if visible.get("ram", True) + else None + ) + self.swap = ( + SingularMetric("swap", "SWAP", icons.swap) + if visible.get("swap", True) + else None + ) self.disk = disks self.gpu = gpus @@ -259,9 +275,12 @@ class Metrics(Button): def on_click(self, widget): if not data.VERTICAL: self.labels_visible = not self.labels_visible - if self.cpu: self.cpu.revealer.set_reveal_child(self.labels_visible) - if self.ram: self.ram.revealer.set_reveal_child(self.labels_visible) - if self.swap: self.swap.revealer.set_reveal_child(self.labels_visible) + if self.cpu: + self.cpu.revealer.set_reveal_child(self.labels_visible) + if self.ram: + self.ram.revealer.set_reveal_child(self.labels_visible) + if self.swap: + self.swap.revealer.set_reveal_child(self.labels_visible) for disk in self.disk: disk.revealer.set_reveal_child(self.labels_visible) for gpu in self.gpu: @@ -289,11 +308,20 @@ class Metrics(Button): gpu.level.set_label(self._format_percentage(int(gpus[i]))) tooltip_metrics = [] - if self.disk: tooltip_metrics.extend(self.disk) - if self.ram: tooltip_metrics.append(self.ram) - if self.swap: tooltip_metrics.append(self.swap) - if self.cpu: tooltip_metrics.append(self.cpu) - if self.gpu: tooltip_metrics.extend(self.gpu) - self.set_tooltip_markup((" - " if not data.VERTICAL else "\n").join([v.markup() for v in tooltip_metrics])) + if self.disk: + tooltip_metrics.extend(self.disk) + if self.ram: + tooltip_metrics.append(self.ram) + if self.swap: + tooltip_metrics.append(self.swap) + if self.cpu: + tooltip_metrics.append(self.cpu) + if self.gpu: + tooltip_metrics.extend(self.gpu) + self.set_tooltip_markup( + (" - " if not data.VERTICAL else "\n").join( + [v.markup() for v in tooltip_metrics] + ) + ) return True diff --git a/modules/dock/components/music_player.py b/modules/dock/components/music_player.py index 27107be..121bd12 100644 --- a/modules/dock/components/music_player.py +++ b/modules/dock/components/music_player.py @@ -1,39 +1,34 @@ +import os +import tempfile +import urllib.parse + import config.data as data +import requests import utils.icons as icons from fabric.widgets.box import Box from fabric.widgets.button import Button -from fabric.widgets.label import Label -from fabric.widgets.eventbox import EventBox from fabric.widgets.centerbox import CenterBox +from fabric.widgets.eventbox import EventBox +from fabric.widgets.label import Label from fabric.widgets.stack import Stack -from gi.repository import GLib, Gdk, GdkPixbuf +from gi.repository import Gdk, GdkPixbuf, GLib from services.mpris import MediaManager -from utils.circle_image import CircleImage from utils.audio_visualizer import SpectrumRender -import urllib.parse -import os -import requests -import tempfile +from utils.circle_image import CircleImage class MusicPlayer(EventBox): def __init__(self, **kwargs): - super().__init__( - name="music-player", - events=["button-press"], - **kwargs - ) + super().__init__(name="music-player", events=["button-press"], **kwargs) orientation = "v" if data.VERTICAL else "h" - # Create main container box self.main_container = Box( name="music-player-container", spacing=4, orientation=orientation, ) - # Initialize media manager self._media_manager = MediaManager() self._media_manager.connect("player-appeared", self._on_player_changed) self._media_manager.connect("player-vanished", self._on_player_changed) @@ -41,84 +36,62 @@ class MusicPlayer(EventBox): self._current_player = None self._manually_selected_player = None self._update_timeout_id = None - self._current_player_signals = [] # Track signal connections for current player + self._current_player_signals = [] - # Marquee animation variables self._marquee_timeout_id = None self._marquee_position = 0 self._original_title = "" self._title_needs_marquee = False - # Thumbnail spinning animation variables self._spin_timeout_id = None self._spin_angle = 0 - # Visualizer state self._show_visualizer = False - - # Create music player view with stack for visualizer/text switching self._create_music_player_view() - - # Set the main container as child self.add(self.main_container) - - # Connect click handler for the entire music player self.connect("button-press-event", self._on_music_player_clicked) - - # Hide initially - will show when media players become available self.set_visible(False) - # Delay initial update to allow MediaManager to initialize properly GLib.timeout_add(100, self._delayed_initial_update) def _create_music_player_view(self): - """Create the music player view with stack for visualizer/text switching like PlayerSmall""" orientation = "v" if data.VERTICAL else "h" - # Create CircleImage for album art thumbnails (clickable to toggle visualizer) self.album_thumbnail = CircleImage( name="music-album-thumbnail", - size=34, # Appropriate size for dock + size=34, v_align="center", - visible=True, # Always visible, will show default or album art + visible=True, ) - # Make album thumbnail clickable to toggle visualizer self.album_thumbnail.add_events(Gdk.EventMask.BUTTON_PRESS_MASK) self.album_thumbnail.connect("button-press-event", self._on_thumbnail_clicked) - # Create track info display self.track_label = Label( name="music-track", label="No Media", - ellipsize=0, # No ellipsize for marquee + ellipsize=0, h_align="center", ) - # Fixed width to prevent box size changes self.track_label.set_size_request(100, -1) self.track_label.set_hexpand(False) self.track_label.set_vexpand(False) - # Create spectrum visualizer lazily to save memory self.cavalcade = None self.cavalcade_box = None - # Create stack for crossfade between visualizer and text self.center_stack = Stack( name="music-player-stack", transition_type="crossfade", transition_duration=100, v_align="center", v_expand=False, - children=[self.track_label] # Start with just track label + children=[self.track_label], ) - # Start with track label visible self.center_stack.set_visible_child(self.track_label) - # Create control buttons self._create_controls() - # Create main music player layout using CenterBox like PlayerSmall self.music_player_box = CenterBox( name="music-player-centerbox", orientation=orientation, @@ -131,13 +104,10 @@ class MusicPlayer(EventBox): end_children=self.controls_box, ) - # Add to main container self.main_container.add(self.music_player_box) def _load_album_art(self, art_url): - """Load album art from URL and set it to the CircleImage""" - # Cache the last loaded URL to avoid reloading the same image - if hasattr(self, '_last_art_url') and self._last_art_url == art_url: + if hasattr(self, "_last_art_url") and self._last_art_url == art_url: return if not art_url: @@ -146,9 +116,8 @@ class MusicPlayer(EventBox): return try: - if art_url.startswith('file://'): - # Handle local file URLs - file_path = urllib.parse.unquote(art_url[7:]) # Remove 'file://' prefix + if art_url.startswith("file://"): + file_path = urllib.parse.unquote(art_url[7:]) if os.path.exists(file_path): self.album_thumbnail.set_image_from_file(file_path) self._show_album_art() @@ -156,22 +125,20 @@ class MusicPlayer(EventBox): else: self._load_fallback_image() self._last_art_url = None - elif art_url.startswith(('http://', 'https://')): - # Handle remote URLs by downloading them first + elif art_url.startswith(("http://", "https://")): response = requests.get(art_url) if response.status_code == 200: - # Create a temporary file - with tempfile.NamedTemporaryFile(delete=False, suffix='.jpg') as temp_file: + with tempfile.NamedTemporaryFile( + delete=False, suffix=".jpg" + ) as temp_file: temp_file.write(response.content) temp_path = temp_file.name - # Load the image from the temporary file pixbuf = GdkPixbuf.Pixbuf.new_from_file(temp_path) self.album_thumbnail.set_image_from_pixbuf(pixbuf) self._show_album_art() self._last_art_url = art_url - # Clean up the temporary file os.unlink(temp_path) else: self._load_fallback_image() @@ -184,7 +151,6 @@ class MusicPlayer(EventBox): self._last_art_url = None def _load_fallback_image(self): - """Load fallback image from current wallpaper when no album art is available""" fallback = os.path.expanduser("~/.current.wall") try: @@ -197,67 +163,54 @@ class MusicPlayer(EventBox): self._hide_album_art() def _show_album_art(self): - """Show album art thumbnail""" self.album_thumbnail.set_visible(True) - # Start spinning animation when showing album art self._start_spin_animation() def _hide_album_art(self): - """Hide album art thumbnail""" self.album_thumbnail.set_visible(False) - # Stop spinning animation when hiding album art - self._stop_spin_animation() def _start_spin_animation(self): - """Start the thumbnail spinning animation""" if not self._spin_timeout_id: - self._spin_timeout_id = GLib.timeout_add(100, self._animate_spin) # 100ms to save CPU + self._spin_timeout_id = GLib.timeout_add( + 100, self._animate_spin + ) # 100ms to save CPU def _stop_spin_animation(self): - """Stop the thumbnail spinning animation""" if self._spin_timeout_id: GLib.source_remove(self._spin_timeout_id) self._spin_timeout_id = None - # Reset angle to 0 self._spin_angle = 0 self.album_thumbnail.angle = 0 def _animate_spin(self): - """Animate the thumbnail spinning""" if not self._current_player or not self.album_thumbnail.get_visible(): return False - # Only spin when music is playing if self._current_player.status == "playing": - self._spin_angle = (self._spin_angle + 5) % 360 # Rotate 2 degrees per frame + self._spin_angle = (self._spin_angle + 5) % 360 self.album_thumbnail.angle = self._spin_angle - return True # Continue animation + return True def _update_player_indicators(self): - """Update the player indicators to show all available players""" - # Get all available players available_players = self._media_manager.players - # Only show indicators if there are multiple players to save memory if len(available_players) <= 1: - # Clear indicators if only one or no players for child in self.player_indicators_box.get_children(): self.player_indicators_box.remove(child) return - # Clear existing indicators for child in self.player_indicators_box.get_children(): self.player_indicators_box.remove(child) - current_player_name = self._current_player.player_name if self._current_player else "None" + current_player_name = ( + self._current_player.player_name if self._current_player else "None" + ) - # Create indicators for each player (only when multiple players exist) for player in available_players: player_name = player.player_name or "Unknown" player_icon_markup = self._get_player_icon(player_name) - # Create clickable button with indicator label indicator_button = Button( name="music-player-indicator-button", child=Label( @@ -267,8 +220,7 @@ class MusicPlayer(EventBox): can_focus=False, ) - # Set tooltip with player info - is_current_player = (player.player_name == current_player_name) + is_current_player = player.player_name == current_player_name tooltip_text = f"Player: {player_name}" if is_current_player: tooltip_text += " (Active)" @@ -276,29 +228,26 @@ class MusicPlayer(EventBox): tooltip_text += " - Click to switch" indicator_button.set_tooltip_text(tooltip_text) - # Add visual distinction for active player (you can style this with CSS) if is_current_player: indicator_button.get_child().set_name("music-player-indicator-active") else: indicator_button.get_child().set_name("music-player-indicator") - # Connect click handler with player reference - indicator_button.connect("clicked", self._on_player_indicator_clicked, player) + indicator_button.connect( + "clicked", self._on_player_indicator_clicked, player + ) self.player_indicators_box.add(indicator_button) - # Force refresh the indicators container self.player_indicators_box.show_all() def _create_controls(self): - """Create the media control buttons""" self.controls_box = Box( name="music-controls", orientation="h" if not data.VERTICAL else "v", spacing=2, ) - # Previous button self.prev_button = Button( name="music-prev", child=Label(markup=icons.prev), @@ -307,7 +256,6 @@ class MusicPlayer(EventBox): self.prev_button.connect("clicked", self._on_previous_clicked) self.prev_button.set_tooltip_text("Previous Track") - # Play/Pause button self.play_pause_button = Button( name="music-play-pause", child=Label(name="play-pause-icon", markup=icons.play), @@ -316,7 +264,6 @@ class MusicPlayer(EventBox): self.play_pause_button.connect("clicked", self._on_play_pause_clicked) self.play_pause_button.set_tooltip_text("Play/Pause") - # Next button self.next_button = Button( name="music-next", child=Label(markup=icons.next), @@ -325,34 +272,32 @@ class MusicPlayer(EventBox): self.next_button.connect("clicked", self._on_next_clicked) self.next_button.set_tooltip_text("Next Track") - # Player indicators container self.player_indicators_box = Box( name="music-player-indicators", orientation="h" if not data.VERTICAL else "v", spacing=2, ) - # Add buttons and indicators to controls self.controls_box.add(self.prev_button) self.controls_box.add(self.play_pause_button) self.controls_box.add(self.next_button) self.controls_box.add(self.player_indicators_box) def _delayed_initial_update(self): - """Delayed initial update to allow MediaManager to initialize""" self._update_display() - return False # Don't repeat this timeout + return False def _connect_player_signals(self, player): - """Connect to player signals for immediate updates""" if player: - # Connect to metadata changes for immediate track/thumbnail updates - metadata_signal = player.connect("metadata_changed", self._on_metadata_changed) - status_signal = player.connect("playback_status_changed", self._on_playback_status_changed) + metadata_signal = player.connect( + "metadata_changed", self._on_metadata_changed + ) + status_signal = player.connect( + "playback_status_changed", self._on_playback_status_changed + ) self._current_player_signals = [metadata_signal, status_signal] def _disconnect_player_signals(self): - """Disconnect from current player signals""" if self._current_player and self._current_player_signals: for signal_id in self._current_player_signals: try: @@ -362,50 +307,41 @@ class MusicPlayer(EventBox): self._current_player_signals = [] def _on_metadata_changed(self, *_args): - """Handle immediate metadata changes (track, artist, album art)""" GLib.idle_add(self._update_display) def _on_playback_status_changed(self, *_args): - """Handle immediate playback status changes""" GLib.idle_add(self._update_display) def _on_player_changed(self, *_args): - """Handle player appearance/disappearance""" GLib.idle_add(self._update_display) def _update_display(self): - """Update the display based on current player state""" - - # Store previous player to detect changes previous_player = self._current_player - # Determine current player (manual selection takes priority) if self._manually_selected_player: - # Check if manually selected player is still available manually_selected_available = next( - (p for p in self._media_manager.players - if p.player_name == self._manually_selected_player.player_name), - None + ( + p + for p in self._media_manager.players + if p.player_name == self._manually_selected_player.player_name + ), + None, ) if manually_selected_available: self._current_player = manually_selected_available self._manually_selected_player = manually_selected_available else: - # Manual player no longer available, clear selection self._manually_selected_player = None self._current_player = self._media_manager.current_player else: self._current_player = self._media_manager.current_player - # Connect to new player signals if player changed if self._current_player != previous_player: self._disconnect_player_signals() if self._current_player: self._connect_player_signals(self._current_player) - if not self._current_player: - # No players available, hide the component self.set_visible(False) if self._update_timeout_id: GLib.source_remove(self._update_timeout_id) @@ -413,42 +349,33 @@ class MusicPlayer(EventBox): if self._marquee_timeout_id: GLib.source_remove(self._marquee_timeout_id) self._marquee_timeout_id = None - # Stop spinning animation when no player self._stop_spin_animation() return False self.set_visible(True) - # Try to load album art, use fallback if not available album_art_url = self._current_player.album_image_url if album_art_url: self._load_album_art(album_art_url) else: - # No album art available, use fallback image self._load_fallback_image() - # Update stack display (text vs visualizer) self._update_stack_display() - - # Update player indicators to show all available players self._update_player_indicators() - # Update play/pause button play_pause_icon = self.play_pause_button.get_child() if self._current_player.status == "playing": play_pause_icon.set_markup(icons.pause) else: play_pause_icon.set_markup(icons.play) - # Update track info with marquee animation track_title = self._current_player.track_title or "Unknown Track" max_display_chars = 12 self._original_title = track_title self._title_needs_marquee = len(track_title) > max_display_chars - # Reset marquee if title changed - if self._original_title != getattr(self, '_last_original_title', ''): + if self._original_title != getattr(self, "_last_original_title", ""): self._last_original_title = self._original_title self._marquee_position = 0 self._start_marquee_animation() @@ -456,12 +383,10 @@ class MusicPlayer(EventBox): self._update_marquee_text() self._update_tooltips() - # Update button sensitivity self.prev_button.set_sensitive(self._current_player.can_go_previous) self.next_button.set_sensitive(self._current_player.can_go_next) self.play_pause_button.set_sensitive(self._current_player.can_pause) - # Remove all polling - rely entirely on signals for updates if self._update_timeout_id: GLib.source_remove(self._update_timeout_id) self._update_timeout_id = None @@ -469,8 +394,6 @@ class MusicPlayer(EventBox): return True def _start_marquee_animation(self): - """Start or restart the marquee animation""" - # Stop existing marquee animation if self._marquee_timeout_id: GLib.source_remove(self._marquee_timeout_id) self._marquee_timeout_id = None @@ -479,13 +402,12 @@ class MusicPlayer(EventBox): self._marquee_timeout_id = GLib.timeout_add(250, self._animate_marquee) def _animate_marquee(self): - """Animate the marquee scrolling""" if not self._current_player: return False self._marquee_position += 1 title_length = len(self._original_title) - loop_length = title_length + 3 # Add spacing between loops + loop_length = title_length + 3 if self._marquee_position >= loop_length: self._marquee_position = 0 @@ -494,7 +416,6 @@ class MusicPlayer(EventBox): return True def _update_marquee_text(self): - """Update the displayed text based on marquee position""" if not self._current_player: return @@ -509,7 +430,7 @@ class MusicPlayer(EventBox): display_title = extended_text[start_pos:end_pos] else: part1 = extended_text[start_pos:] - part2 = self._original_title[:end_pos - len(extended_text)] + part2 = self._original_title[: end_pos - len(extended_text)] display_title = (part1 + part2)[:max_display_chars] else: display_title = self._original_title[:max_display_chars] @@ -517,7 +438,6 @@ class MusicPlayer(EventBox): self.track_label.set_label(display_title) def _get_player_icon(self, player_name): - """Get the appropriate icon for a player based on its name""" if not player_name: return icons.disc @@ -527,102 +447,82 @@ class MusicPlayer(EventBox): return icons.firefox elif "spotify" in player_name_lower: return icons.spotify - elif any(browser in player_name_lower for browser in [ - "chromium", "chrome", "edge", "brave", "opera", "vivaldi" - ]): + elif any( + browser in player_name_lower + for browser in ["chromium", "chrome", "edge", "brave", "opera", "vivaldi"] + ): return icons.chromium else: return icons.disc def _update_tooltips(self): - """Update tooltips with full track information""" if not self._current_player: return - # Get full track information for tooltip full_track_title = self._current_player.track_title or "Unknown Track" track_artist = self._current_player.track_artist or "Unknown Artist" player_name = self._current_player.player_name or "Media Player" - # Create tooltip text with full information tooltip_text = f"{full_track_title}\nby {track_artist}\n\nPlayer: {player_name}" self.track_label.set_tooltip_text(tooltip_text) def _on_play_pause_clicked(self, _button): - """Handle play/pause button click""" if self._current_player: self._current_player.play_pause() - # No need for manual update - signal will trigger it def _on_previous_clicked(self, _button): - """Handle previous button click""" if self._current_player: self._current_player.previous() - # No need for manual update - signal will trigger it def _on_next_clicked(self, _button): - """Handle next button click""" if self._current_player: self._current_player.next() - # No need for manual update - signal will trigger it def _on_player_indicator_clicked(self, _button, player): - """Handle player indicator click to switch players""" if player: self._manually_selected_player = player self._update_display() def _on_thumbnail_clicked(self, _widget, _event): - """Handle thumbnail click to toggle visualizer""" if self._current_player: self._show_visualizer = not self._show_visualizer self._update_stack_display() - return True # Stop event propagation + return True def _on_music_player_clicked(self, _widget, _event): - """Handle music player click to toggle between text and visualizer view""" if self._current_player: self._show_visualizer = not self._show_visualizer self._update_stack_display() return True def _create_visualizer_if_needed(self): - """Create visualizer lazily to save memory""" if not self.cavalcade: self.cavalcade = SpectrumRender() self.cavalcade_box = self.cavalcade.get_spectrum_box() self.center_stack.add(self.cavalcade_box) def _destroy_visualizer(self): - """Destroy visualizer to free memory when not needed""" if self.cavalcade_box: self.center_stack.remove(self.cavalcade_box) self.cavalcade_box = None - if self.cavalcade and hasattr(self.cavalcade, 'cava'): + if self.cavalcade and hasattr(self.cavalcade, "cava"): self.cavalcade.cava.close() self.cavalcade = None def _update_stack_display(self): - """Update stack to show either text or visualizer""" if self._show_visualizer and self._current_player: - # Create visualizer only when needed self._create_visualizer_if_needed() if self.cavalcade_box: self.cavalcade_box.set_visible(True) self.cavalcade_box.show_all() self.center_stack.set_visible_child(self.cavalcade_box) else: - # Show text label in stack self.center_stack.set_visible_child(self.track_label) - # Destroy visualizer to save memory when not in use self._destroy_visualizer() def __del__(self): - """Cleanup when component is destroyed""" - # Disconnect player signals self._disconnect_player_signals() - # Clean up all timeouts if self._update_timeout_id: GLib.source_remove(self._update_timeout_id) if self._marquee_timeout_id: @@ -630,5 +530,4 @@ class MusicPlayer(EventBox): if self._spin_timeout_id: GLib.source_remove(self._spin_timeout_id) - # Clean up visualizer self._destroy_visualizer() diff --git a/modules/dock/components/tray.py b/modules/dock/components/tray.py deleted file mode 100644 index aac9dcd..0000000 --- a/modules/dock/components/tray.py +++ /dev/null @@ -1,170 +0,0 @@ -import config.data as data -from gi.repository import Gdk, GdkPixbuf, GLib, Gray, Gtk -from fabric.widgets.box import Box -import logging -import gi - -gi.require_version("Gray", "0.1") - - -logger = logging.getLogger(__name__) - - -class SystemTray(Box): - def __init__( - self, pixel_size: int = 20, refresh_interval: int = 1, **kwargs - ) -> None: - orientation = ( - Gtk.Orientation.HORIZONTAL - if not data.VERTICAL - else Gtk.Orientation.VERTICAL - ) - super().__init__(name="tray", orientation=orientation, spacing=8, **kwargs) - self.enabled = True - super().set_visible(False) - self.pixel_size = pixel_size - self.refresh_interval = refresh_interval - - self.buttons_by_id = {} - self.items_by_id = {} - - self.watcher = Gray.Watcher() - self.watcher.connect("item-added", self.on_watcher_item_added) - - GLib.timeout_add_seconds(self.refresh_interval, self._refresh_all_items) - - def set_visible(self, visible: bool): - self.enabled = visible - self._update_visibility() - - def _update_visibility(self): - has = len(self.get_children()) > 0 - super().set_visible(self.enabled and has) - - def _get_item_pixbuf(self, item: Gray.Item) -> GdkPixbuf.Pixbuf: - try: - pm = Gray.get_pixmap_for_pixmaps(item.get_icon_pixmaps(), self.pixel_size) - if pm: - return pm.as_pixbuf(self.pixel_size, GdkPixbuf.InterpType.HYPER) - - name = item.get_icon_name() - theme = Gtk.IconTheme.new() - path = item.get_icon_theme_path() - if path: - theme.prepend_search_path(path) - return theme.load_icon( - name, self.pixel_size, Gtk.IconLookupFlags.FORCE_SIZE - ) - except GLib.Error as e: - logger.debug(f"Icon load error {e}") - return Gtk.IconTheme.get_default().load_icon( - "image-missing", self.pixel_size, Gtk.IconLookupFlags.FORCE_SIZE - ) - - def _refresh_item_ui(self, identifier: str, item: Gray.Item, button: Gtk.Button): - pixbuf = self._get_item_pixbuf(item) - img = button.get_image() - if isinstance(img, Gtk.Image): - img.set_from_pixbuf(pixbuf) - else: - new = Gtk.Image.new_from_pixbuf(pixbuf) - button.set_image(new) - new.show() - tip = None - if hasattr(item, "get_tooltip_text"): - tip = item.get_tooltip_text() - elif hasattr(item, "get_title"): - tip = item.get_title() - if tip: - button.set_tooltip_text(tip) - else: - button.set_has_tooltip(False) - - def _refresh_all_items(self) -> bool: - for ident, item in self.items_by_id.items(): - btn = self.buttons_by_id.get(ident) - if btn: - self._refresh_item_ui(ident, item, btn) - return True - - def on_watcher_item_added(self, _, identifier: str): - item = self.watcher.get_item_for_identifier(identifier) - if not item: - return - - if identifier in self.buttons_by_id: - self.buttons_by_id[identifier].destroy() - del self.buttons_by_id[identifier] - del self.items_by_id[identifier] - - btn = self.do_bake_item_button(item) - self.buttons_by_id[identifier] = btn - self.items_by_id[identifier] = item - - item.connect( - "notify::icon-pixmaps", - lambda itm, pspec: self._refresh_item_ui(identifier, itm, btn), - ) - item.connect( - "notify::icon-name", - lambda itm, pspec: self._refresh_item_ui(identifier, itm, btn), - ) - - try: - item.connect( - "updated", lambda itm: self._refresh_item_ui(identifier, itm, btn) - ) - except TypeError: - pass - - item.connect( - "removed", lambda itm: self.on_item_instance_removed(identifier, itm) - ) - - self.add(btn) - btn.show_all() - self._update_visibility() - - def do_bake_item_button(self, item: Gray.Item) -> Gtk.Button: - btn = Gtk.Button() - btn.connect("button-press-event", lambda b, e: self.on_button_click(b, item, e)) - img = Gtk.Image.new_from_pixbuf(self._get_item_pixbuf(item)) - btn.set_image(img) - tip = ( - item.get_tooltip_text() - if hasattr(item, "get_tooltip_text") - else getattr(item, "get_title", lambda: None)() - ) - if tip: - btn.set_tooltip_text(tip) - return btn - - def on_item_instance_removed(self, identifier: str, removed_item: Gray.Item): - if self.items_by_id.get(identifier) is removed_item: - btn = self.buttons_by_id.pop(identifier, None) - self.items_by_id.pop(identifier, None) - if btn: - btn.destroy() - self._update_visibility() - - def on_button_click( - self, button: Gtk.Button, item: Gray.Item, event: Gdk.EventButton - ): - if event.button == Gdk.BUTTON_PRIMARY: - try: - item.activate(int(event.x_root), int(event.y_root)) - except Exception as e: - logger.error(f"Activate error: {e}") - elif event.button == Gdk.BUTTON_SECONDARY: - menu = getattr(item, "get_menu", lambda: None)() - if isinstance(menu, Gtk.Menu): - menu.popup_at_widget( - button, Gdk.Gravity.SOUTH_WEST, Gdk.Gravity.NORTH_WEST, event - ) - else: - cm = getattr(item, "context_menu", None) - if cm: - try: - cm(int(event.x_root), int(event.y_root)) - except Exception as e: - logger.error(f"ContextMenu error: {e}") diff --git a/modules/dock/components/workspaces.py b/modules/dock/components/workspaces.py index 3637a38..b973669 100644 --- a/modules/dock/components/workspaces.py +++ b/modules/dock/components/workspaces.py @@ -1,21 +1,19 @@ import json import subprocess -from fabric.hyprland.service import Hyprland -from fabric.hyprland.widgets import Workspaces, WorkspaceButton -from fabric.widgets.button import Button -import config.data as data -from fabric.widgets.box import Box +import config.data as data +from fabric.hyprland.service import Hyprland +from fabric.hyprland.widgets import WorkspaceButton, Workspaces +from fabric.widgets.box import Box +from fabric.widgets.button import Button CHINESE_NUMERALS = ["一", "二", "三", "四", "五", "六", "七", "八", "九", "〇"] class Workspace(Button): def __init__(self): - # Determine if invert style should be applied - should_invert = ( - data.DOCK_THEME in ["Dense", "Edge"] or - (data.DOCK_THEME == "Pills" and data.DOCK_POSITION in ["Left", "Right"]) + should_invert = data.DOCK_THEME in ["Dense", "Edge"] or ( + data.DOCK_THEME == "Pills" and data.DOCK_POSITION in ["Left", "Right"] ) super().__init__( @@ -41,7 +39,6 @@ class Workspace(Button): return json.loads(active.decode("utf-8"))["name"] def _get_active_workspaces(self): - """Get list of workspaces that have windows""" clients_data = self.connection.send_command("j/clients").reply clients = json.loads(clients_data.decode("utf-8")) @@ -56,11 +53,9 @@ class Workspace(Button): ) def _on_click(self, _widget): - """Handle button click to cycle through workspaces""" self._cycle_workspace() def _cycle_workspace(self): - """Core logic to cycle through workspaces""" active_workspaces = self._get_active_workspaces() if not active_workspaces: @@ -87,7 +82,6 @@ class Workspace(Button): self.set_label(str(self.activeWorkspace)) -# Create workspace components using Workspaces widget workspaces = Workspaces( name="workspaces", invert_scroll=True, @@ -132,10 +126,8 @@ workspaces_num = Workspaces( ) -# Create the workspace button instance workspaceactive = Workspace() -# Export the workspace widget based on configuration workspace = Box( name="workspaces-container", children=[