♻️ refactor(dock,workspace,switcher): centralize special workspace detection logic

This commit is contained in:
sankalp committed 2025-08-14 14:57:19 +05:45
1 parent feec2917f6
commit d0444752e1
6 files changed
+227 -199

No files matched your search

+107 -114
View File
@@ -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)
+4 -12
View File
@@ -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))
+2 -18
View File
@@ -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():
+24 -24
View File
@@ -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()
todo_service = TodoService()
+32 -31
View File
@@ -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; */
/* } */
+58
View File
@@ -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