From d0444752e1f856ef1ca65836073d3f2724d632f3 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Thu, 14 Aug 2025 14:57:19 +0545 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(dock,workspace,sw?= =?UTF-8?q?itcher):=20centralize=20special=20workspace=20detection=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/dock.py | 221 +++++++++++++------------- modules/panel/components/workspace.py | 16 +- modules/switcher.py | 20 +-- services/todo.py | 48 +++--- styles/dock.css | 63 ++++---- utils/functions.py | 58 +++++++ 6 files changed, 227 insertions(+), 199 deletions(-) diff --git a/modules/dock.py b/modules/dock.py index 1f2d326..0f32a93 100644 --- a/modules/dock.py +++ b/modules/dock.py @@ -16,7 +16,7 @@ from loguru import logger import config.data as data from services.modus import modus_service -from utils.functions import read_json_file, write_json_file +from utils.functions import read_json_file, write_json_file, is_special_workspace_id from utils.icon_resolver import IconResolver from utils.occlusion import check_occlusion from widgets.wayland import WaylandWindow as Window @@ -40,7 +40,7 @@ class AppBar(Box): ) super().__init__( - spacing=10, + spacing=0, name="dock", orientation=orientation, children=[], @@ -74,7 +74,7 @@ class AppBar(Box): logger.error(f"[AppBar] Error updating apps: {e}") return True - GLib.timeout_add(1000, update_running_apps) + GLib.timeout_add(100, update_running_apps) GLib.idle_add(self.update_dock_apps) def _populate_pinned_apps(self): @@ -486,42 +486,42 @@ class AppBar(Box): return None def update_dock_apps(self): - clients = self.get_clients() - focused_window = self.get_focused_window() - focused_address = focused_window.get("address", "") if focused_window else "" + try: + clients = self.get_clients() + focused_window = self.get_focused_window() + focused_address = focused_window.get("address", "") if focused_window else "" - current_instance_ids = set() + current_instance_ids = set() - for client in clients: - if client.get("hidden", False): - continue + for client in clients: + if client.get("hidden", False) or not self._should_show_app_instance(client): + continue - # Skip apps in special workspaces if configured to hide them - if not self._should_show_app_instance(client): - continue + instance_address = client.get("address", "") + app_class = client.get("class", "") or client.get("title", "") + if not instance_address or not app_class: + continue - instance_address = client.get("address", "") - if not instance_address: - continue + current_instance_ids.add(instance_address) - current_instance_ids.add(instance_address) - app_class = client.get("class", "") or client.get("title", "unknown-app") + if instance_address not in self.client_buttons: + self.create_instance_button(instance_address, client, app_class) + else: + self.update_instance_button(instance_address, client, app_class) - if instance_address not in self.client_buttons: - self.create_instance_button(instance_address, client, app_class) - else: - self.update_instance_button(instance_address, client, app_class) + button = self.client_buttons[instance_address] + if instance_address == focused_address: + button.add_style_class("activated") + else: + button.remove_style_class("activated") - button = self.client_buttons[instance_address] - if instance_address == focused_address: - button.add_style_class("activated") - else: - button.remove_style_class("activated") + self._update_pinned_apps_state(clients) + self._update_separator_visibility() - self._update_pinned_apps_state(clients) - self._update_separator_visibility() - - self._cleanup_removed_instances(current_instance_ids) + self._cleanup_removed_instances(current_instance_ids) + + except Exception as e: + logger.error(f"[AppBar] Error in update_dock_apps: {e}") def _update_pinned_apps_state(self, clients): running_app_classes = { @@ -549,91 +549,93 @@ class AppBar(Box): if instance_id not in current_instance_ids ] - for instance_id in buttons_to_remove: - button = self.client_buttons.pop(instance_id) - try: - if button in self.running_items_pos: - self.running_items_pos.remove(button) - button.remove_style_class("shown") - except Exception: - pass - - def remove_later(): + # Clean up removed and orphaned buttons + for instance_id in buttons_to_remove + [k for k, v in self.client_buttons.items() + if not hasattr(v, 'instance_address') or not v.get_parent()]: + if instance_id in self.client_buttons: + button = self.client_buttons.pop(instance_id) try: - self.running_apps_container.remove(button) - except Exception: - pass - return False - - GLib.timeout_add(250, remove_later) + if button in self.running_items_pos: + self.running_items_pos.remove(button) + button.remove_style_class("shown") + button.remove_style_class("activated") + if button.get_parent(): + button.get_parent().remove(button) + button.destroy() + except Exception as e: + logger.warning(f"[AppBar] Error during cleanup: {e}") def create_instance_button(self, instance_address, client, app_class): - client_image = Image(name="dock_item_icon") - try: - desktop_apps = get_desktop_applications(include_hidden=False) - desktop_app = self._find_desktop_app_by_id(app_class, desktop_apps) + client_image = Image(name="dock_item_icon") - if desktop_app: - pixbuf = desktop_app.get_icon_pixbuf(data.DOCK_ICON_SIZE) - else: - pixbuf = self.icon_resolver.get_icon_pixbuf( - app_class, data.DOCK_ICON_SIZE + try: + desktop_apps = get_desktop_applications(include_hidden=False) + desktop_app = self._find_desktop_app_by_id(app_class, desktop_apps) + + if desktop_app: + pixbuf = desktop_app.get_icon_pixbuf(data.DOCK_ICON_SIZE) + else: + pixbuf = self.icon_resolver.get_icon_pixbuf( + app_class, data.DOCK_ICON_SIZE + ) + + client_image.set_from_pixbuf(pixbuf) + except Exception as e: + logger.warning(f"[AppBar] Could not load icon for {app_class}: {e}") + + workspace_id = self._get_workspace_id(client) + workspace_label = None + if workspace_id is not None: + workspace_label = Label( + label=str(workspace_id), + name="workspace-indicator", + h_align="end", + v_align="end", ) - client_image.set_from_pixbuf(pixbuf) - except Exception as e: - logger.warning(f"[AppBar] Could not load icon for {app_class}: {e}") + image_overlay = Overlay(name="dock-image-overlay", child=client_image) + if workspace_label: + image_overlay.add_overlay(workspace_label) - workspace_id = self._get_workspace_id(client) - workspace_label = None - if workspace_id is not None: - workspace_label = Label( - label=str(workspace_id), - name="workspace-indicator", - h_align="end", - v_align="end", + indicator = Box(name="dock_item_indicator", h_align="center") + main_container = Box( + name="dock_item_main_container", + orientation="v", + children=[image_overlay, indicator], ) - image_overlay = Overlay(name="dock-image-overlay", child=client_image) - if workspace_label: - image_overlay.add_overlay(workspace_label) + tooltip_text = client.get("title", app_class) + if tooltip_text != app_class: + tooltip_text = f"{app_class}: {tooltip_text}" - indicator = Box(name="dock_item_indicator", h_align="center") - main_container = Box( - name="dock_item_main_container", - orientation="v", - children=[image_overlay, indicator], - ) + client_button = Button( + name="dock_item", + child=main_container, + tooltip_text=tooltip_text, + on_button_press_event=lambda widget, event: self.handle_instance_click( + widget, event + ), + on_enter_notify_event=lambda *_: self._handle_item_hovered( + client_button, False + ), + on_leave_notify_event=lambda *_: self._handle_item_unhovered( + client_button, False + ), + ) - tooltip_text = client.get("title", app_class) - if tooltip_text != app_class: - tooltip_text = f"{app_class}: {tooltip_text}" + client_button.instance_address = instance_address + client_button.client_data = client + client_button.app_class = app_class + client_button.workspace_label = workspace_label + client_button.add_style_class("shown") - client_button = Button( - name="dock_item", - child=main_container, - tooltip_text=tooltip_text, - on_button_press_event=lambda widget, event: self.handle_instance_click( - widget, event - ), - on_enter_notify_event=lambda *_: self._handle_item_hovered( - client_button, False - ), - on_leave_notify_event=lambda *_: self._handle_item_unhovered( - client_button, False - ), - ) - - client_button.instance_address = instance_address - client_button.client_data = client - client_button.app_class = app_class - client_button.workspace_label = workspace_label - client_button.add_style_class("shown") - - self.client_buttons[instance_address] = client_button - self.running_apps_container.add(client_button) - self.running_items_pos.append(client_button) + self.client_buttons[instance_address] = client_button + self.running_apps_container.add(client_button) + self.running_items_pos.append(client_button) + + except Exception as e: + logger.error(f"[AppBar] Error creating instance button for {app_class}: {e}") def _get_workspace_id(self, client): workspace_data = client.get("workspace", {}) @@ -644,16 +646,7 @@ class AppBar(Box): return None def _is_special_workspace_id(self, ws_id): - try: - # Convert to int if it's a string - workspace_id = int(ws_id) - # Special workspaces have negative IDs - return workspace_id < 0 - except (ValueError, TypeError): - # If it's a string, check if it starts with "special:" - if isinstance(ws_id, str) and ws_id.startswith("special:"): - return True - return False + return is_special_workspace_id(ws_id) def _should_show_app_instance(self, client): if not data.DOCK_HIDE_SPECIAL_WORKSPACE_APPS: @@ -844,4 +837,4 @@ class Dock(Window): return True - GLib.timeout_add(250, check_dock_occlusion) + GLib.timeout_add(100, check_dock_occlusion) diff --git a/modules/panel/components/workspace.py b/modules/panel/components/workspace.py index f22cfdb..c05dab0 100644 --- a/modules/panel/components/workspace.py +++ b/modules/panel/components/workspace.py @@ -1,6 +1,7 @@ from fabric.hyprland.widgets import HyprlandWorkspaces, WorkspaceButton from fabric.widgets.box import Box import config.data as data +from utils.functions import is_special_workspace_id # TODO: Support Multi Monitor @@ -31,22 +32,13 @@ class WorkspaceIndicator(Box): self.show_all() def _is_special_workspace_id(self, ws_id): - """Check if a workspace ID represents a special workspace""" - try: - # Convert to int if it's a string - workspace_id = int(ws_id) - # Special workspaces have negative IDs - return workspace_id < 0 - except (ValueError, TypeError): - # If it's a string, check if it starts with "special:" - if isinstance(ws_id, str) and ws_id.startswith("special:"): - return True - return False + return is_special_workspace_id(ws_id) def _create_filtered_button(self, ws_id): - """Create workspace button only if it should be shown""" if data.HIDE_SPECIAL_WORKSPACE and self._is_special_workspace_id(ws_id): # Return None or an empty widget to hide special workspaces return None return WorkspaceButton(id=ws_id, label=str(ws_id)) + + diff --git a/modules/switcher.py b/modules/switcher.py index 723fdb1..564c54a 100644 --- a/modules/switcher.py +++ b/modules/switcher.py @@ -11,6 +11,7 @@ from fabric.widgets.image import Image from fabric.widgets.label import Label from utils.icon_resolver import IconResolver from utils.occlusion import get_screen_dimensions +from utils.functions import is_special_workspace from widgets.wayland import WaylandWindow as Window gi.require_version("Glace", "0.1") @@ -193,24 +194,7 @@ class ApplicationSwitcher(Window): image_widget.set_from_pixbuf(icon_img) def _is_special_workspace(self, client): - """Check if a client is in a special workspace""" - if "workspace" not in client: - return False - - workspace = client["workspace"] - if "name" in workspace: - workspace_name = str(workspace["name"]) - # Special workspaces typically start with "special:" or have negative IDs - if workspace_name.startswith("special:"): - return True - - if "id" in workspace: - workspace_id = workspace["id"] - # Special workspaces have negative IDs - if workspace_id < 0: - return True - - return False + return is_special_workspace(client) def update_windows(self) -> None: for child in self.view.get_children(): diff --git a/services/todo.py b/services/todo.py index e8a9992..876505e 100644 --- a/services/todo.py +++ b/services/todo.py @@ -1,12 +1,11 @@ # Standard library imports import json -import os import uuid from datetime import datetime from pathlib import Path -# Fabric imports -from fabric.core.service import Property, Service, Signal +# Fabric imports +from fabric.core.service import Property, Service # Local imports import config.data as data @@ -14,23 +13,23 @@ import config.data as data class TodoService(Service): """Service for managing persistent todo list with JSON storage""" - + def __init__(self): super().__init__() self._todos = [] self._file_path = self._get_todos_file_path() self._load_todos() self._callbacks = [] - + def add_callback(self, callback): """Add a callback function to be notified of changes""" self._callbacks.append(callback) - + def remove_callback(self, callback): """Remove a callback function""" if callback in self._callbacks: self._callbacks.remove(callback) - + def _notify_callbacks(self, event_type, data=None): """Notify all registered callbacks of changes""" for callback in self._callbacks: @@ -38,13 +37,13 @@ class TodoService(Service): callback(event_type, data) except Exception as e: print(f"Error in todo callback: {e}") - + def _get_todos_file_path(self): """Returns the path to the todos JSON file""" cache_dir = Path(data.CACHE_DIR) / "todos" cache_dir.mkdir(parents=True, exist_ok=True) return cache_dir / "todos.json" - + def _load_todos(self): """Load todos from JSON file""" try: @@ -56,7 +55,7 @@ class TodoService(Service): except Exception as e: print(f"Error loading todos: {e}") self._todos = [] - + def _save_todos(self): """Save todos to JSON file""" try: @@ -64,12 +63,12 @@ class TodoService(Service): json.dump(self._todos, f, indent=2, ensure_ascii=False) except Exception as e: print(f"Error saving todos: {e}") - + @Property(list, "readable") def todos(self): """Get all todos""" return self._todos.copy() - + def add_todo(self, text: str, priority: str = "medium") -> dict: """Add a new todo item""" todo = { @@ -78,14 +77,14 @@ class TodoService(Service): "completed": False, "priority": priority, # low, medium, high "created_at": datetime.now().isoformat(), - "updated_at": datetime.now().isoformat() + "updated_at": datetime.now().isoformat(), } self._todos.append(todo) self._save_todos() self._notify_callbacks("todo-added", todo) self._notify_callbacks("todos-changed") return todo - + def delete_todo(self, todo_id: str) -> bool: """Delete a todo item by ID""" for i, todo in enumerate(self._todos): @@ -96,7 +95,7 @@ class TodoService(Service): self._notify_callbacks("todos-changed") return True return False - + def toggle_todo(self, todo_id: str) -> bool: """Toggle completion status of a todo item""" for todo in self._todos: @@ -108,7 +107,7 @@ class TodoService(Service): self._notify_callbacks("todos-changed") return True return False - + def edit_todo(self, todo_id: str, new_text: str) -> bool: """Edit the text of a todo item""" for todo in self._todos: @@ -120,12 +119,12 @@ class TodoService(Service): self._notify_callbacks("todos-changed") return True return False - + def set_priority(self, todo_id: str, priority: str) -> bool: """Set the priority of a todo item""" if priority not in ["low", "medium", "high"]: return False - + for todo in self._todos: if todo["id"] == todo_id: todo["priority"] = priority @@ -135,14 +134,14 @@ class TodoService(Service): self._notify_callbacks("todos-changed") return True return False - + def get_todo(self, todo_id: str) -> dict | None: """Get a specific todo by ID""" for todo in self._todos: if todo["id"] == todo_id: return todo.copy() return None - + def clear_completed(self): """Remove all completed todos""" initial_count = len(self._todos) @@ -150,20 +149,21 @@ class TodoService(Service): if len(self._todos) < initial_count: self._save_todos() self._notify_callbacks("todos-changed") - + def get_stats(self) -> dict: """Get todo statistics""" total = len(self._todos) completed = sum(1 for todo in self._todos if todo["completed"]) pending = total - completed - + return { "total": total, "completed": completed, "pending": pending, - "completion_rate": (completed / total * 100) if total > 0 else 0 + "completion_rate": (completed / total * 100) if total > 0 else 0, } # Global service instance -todo_service = TodoService() \ No newline at end of file +todo_service = TodoService() + diff --git a/styles/dock.css b/styles/dock.css index e253ebe..9bd8efa 100644 --- a/styles/dock.css +++ b/styles/dock.css @@ -22,44 +22,44 @@ #dock_item.shown.semi_hovered #dock_item_main_container { margin-top: -7px; } +/**/ +/* #dock_item.shown.semi_hovered #dock_item_indicator { */ +/* margin-top: 11px; */ +/* } */ -#dock_item.shown.semi_hovered #dock_item_indicator { - margin-top: 11px; -} - -#dock_item.shown.activated:hover #dock_item_main_container { - margin-top: -12px; -} - -#dock_item.shown.activated:hover #dock_item_indicator { - margin-top: 16px; -} - -#dock_item.shown.activated #dock_item_main_container { - margin-top: -4px; -} +/* #dock_item.shown.activated:hover #dock_item_main_container { */ +/* margin-top: -12px; */ +/* } */ +/**/ +/* #dock_item.shown.activated:hover #dock_item_indicator { */ +/* margin-top: 16px; */ +/* } */ +/**/ +/* #dock_item.shown.activated #dock_item_main_container { */ +/* margin-top: -4px; */ +/* } */ #dock_item.shown.activated #dock_item_icon { opacity: 1; /* background-color: #01458e; */ } -#dock_item.shown.activated #dock_item_indicator { - min-width: 4px; - margin-top: 8px; - background-color: #aac7ff; -} +/* #dock_item.shown.activated #dock_item_indicator { */ +/* min-width: 4px; */ +/* margin-top: 8px; */ +/* background-color: #aac7ff; */ +/* } */ #dock_item_icon { border-radius: 13px; opacity: 0.7; } -#dock_item_indicator { - transition: all 0.15s ease-out; - min-height: 4px; - border-radius: 4px; -} +/* #dock_item_indicator { */ +/* transition: all 0.15s ease-out; */ +/* min-height: 4px; */ +/* border-radius: 4px; */ +/* } */ #dock_item { transition: margin 0.2s ease-out; @@ -78,6 +78,7 @@ transition: all 0.1s ease-out; min-width: 1.5px; min-height: 50px; + margin: 0px 4px; border-radius: 2px; background-color: #2b5ea7; } @@ -88,9 +89,9 @@ margin: 0px; } -#dock_item.shown:not(.activated) { - min-width: 0px; - min-height: 0px; - margin-top: 0px; - background-color: transparent; -} +/* #dock_item.shown:not(.activated) { */ +/* min-width: 0px; */ +/* min-height: 0px; */ +/* margin-top: 0px; */ +/* background-color: transparent; */ +/* } */ diff --git a/utils/functions.py b/utils/functions.py index 7fed89b..fdae428 100644 --- a/utils/functions.py +++ b/utils/functions.py @@ -95,3 +95,61 @@ def get_wifi_connecting_icon() -> str: project_root = os.path.dirname(current_dir) return os.path.join(project_root, "config", "assets", "icons", "wifi", "wifi-connecting.svg") + + +def is_special_workspace_id(ws_id) -> bool: + """ + Check if a workspace ID represents a special workspace. + + Args: + ws_id: Workspace ID (can be int, string, or other types) + + Returns: + True if the workspace is special, False otherwise + """ + try: + # Convert to int if it's a string + workspace_id = int(ws_id) + # Special workspaces have negative IDs + return workspace_id < 0 + except (ValueError, TypeError): + # If it's a string, check if it starts with "special:" + if isinstance(ws_id, str) and ws_id.startswith("special:"): + return True + return False + + +def is_special_workspace(client: dict) -> bool: + """ + Check if a client is in a special workspace. + + Args: + client: Client data dictionary from Hyprland + + Returns: + True if the client is in a special workspace, False otherwise + """ + if "workspace" not in client: + return False + + workspace = client["workspace"] + + # Check workspace name first + if "name" in workspace: + workspace_name = str(workspace["name"]) + # Special workspaces typically start with "special:" or have negative IDs + if workspace_name.startswith("special:"): + return True + + # Check workspace ID + if "id" in workspace: + workspace_id = workspace["id"] + # Special workspaces have negative IDs + if workspace_id < 0: + return True + + return False + + + +