From 1dd99cf79de52a941784703393e42345e17a7fdb Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Thu, 9 Jul 2026 22:33:14 +0545 Subject: [PATCH] refactor(src/window): remove unused ApplicationSwitcher component --- src/window/panel/components/workspace.py | 2 +- src/window/switcher.py | 328 ----------------- src/window/switcher/__init__.py | 3 + src/window/switcher/main.py | 438 +++++++++++++++++++++++ 4 files changed, 442 insertions(+), 329 deletions(-) delete mode 100644 src/window/switcher.py create mode 100644 src/window/switcher/__init__.py create mode 100644 src/window/switcher/main.py diff --git a/src/window/panel/components/workspace.py b/src/window/panel/components/workspace.py index c63865e..69d0bce 100644 --- a/src/window/panel/components/workspace.py +++ b/src/window/panel/components/workspace.py @@ -1,5 +1,5 @@ -from fabric.utils import logger from fabric.hyprland.widgets import HyprlandWorkspaces, WorkspaceButton +from fabric.utils import logger from fabric.widgets.box import Box from services.config import get_config, on_config_change diff --git a/src/window/switcher.py b/src/window/switcher.py deleted file mode 100644 index 43d2ea1..0000000 --- a/src/window/switcher.py +++ /dev/null @@ -1,328 +0,0 @@ -from typing import Dict, Tuple - -from fabric.utils import Gdk, GdkPixbuf, logger -from fabric.widgets.box import Box -from fabric.widgets.eventbox import EventBox -from fabric.widgets.image import Image -from fabric.widgets.label import Label -from fabric.widgets.wayland import WaylandWindow as Window - -from services.config import config, on_config_change -from services.modus import get_clients, get_active_window, focus_window -from utils.functions import is_special_workspace -from utils.icon_resolver import IconResolver - - -class ApplicationSwitcher(Window): - def __init__(self, **kwargs): - super().__init__( - name="application-switcher", - title="modus-switcher", - layer="top", - anchor="center", - exclusivity="auto", - keyboard_mode="exclusive", - visible=False, - **kwargs, - ) - - self.icon_resolver = IconResolver() - self.windows = [] - self.current_index = 0 - self.tab_pressed = False - self.icon_size = 96 - self._pixbuf_cache: Dict[Tuple[str, int], GdkPixbuf.Pixbuf] = {} - - on_config_change(self._on_config_changed) - - container = Box( - name="application-switcher-container", - orientation="v", - h_align="center", - v_align="center", - expand=True, - ) - self.add(container) - - self.view = Box( - name="application-switcher-view", - orientation="h", - spacing=16, - h_align="center", - v_align="center", - ) - container.add(self.view) - - self.selection_label = Label( - name="switcher-selection-label", - label="", - h_align="center", - v_align="center", - style_classes=["switcher-selection-label"], - ) - container.add(self.selection_label) - - self.workspace_label = Label( - name="switcher-workspace-label", - label="", - h_align="center", - v_align="center", - style_classes=["switcher-workspace-label"], - ) - container.add(self.workspace_label) - - self.connect("key-press-event", self.on_key_press) - self.connect("key-release-event", self.on_key_release) - - self.show_all() - self.hide() - - @property - def items_per_row(self): - return config().get("window_switcher_items_per_row", 10) - - def _on_config_changed(self, new_config, old_config): - if config().has_changed( - "hide_special_workspace", old_config - ) or config().has_changed("window_switcher_items_per_row", old_config): - if self.get_visible(): - self.update_windows() - - def show_switcher(self) -> None: - self.update_windows() - if not self.windows: - return - - self.show() - self.grab_keyboard() - self.tab_pressed = False - - def hide_switcher(self) -> None: - self.hide() - self.ungrab_keyboard() - - def create_icon_for_window(self, window): - class_name = window.get("class", "").lower() - key = (class_name, self.icon_size) - if key not in self._pixbuf_cache: - icon_img = self.icon_resolver.get_icon_pixbuf(class_name, self.icon_size) - if not icon_img: - icon_img = self.icon_resolver.get_icon_pixbuf( - "application-x-executable-symbolic", self.icon_size - ) - if icon_img: - self._pixbuf_cache[key] = icon_img - icon_img = self._pixbuf_cache.get(key) - icon_image = Image() - if icon_img: - icon_image.set_from_pixbuf(icon_img) - return icon_image - - def _is_special_workspace(self, client): - return is_special_workspace(client) - - def update_windows(self) -> None: - for child in self.view.get_children(): - child.destroy() - - try: - clients = get_clients() - if not clients: - return - - hide_special = config().get("hide_special_workspace", True) - - filtered_windows = [] - for c in clients: - if c.get("hidden", False): - continue - if hide_special and self._is_special_workspace(c): - continue - filtered_windows.append(c) - - self.windows = filtered_windows - - active_window = get_active_window() - - self.current_index = 0 - if active_window: - for i, window in enumerate(self.windows): - if window.get("address") == active_window.get("address"): - self.current_index = i - break - - rows_box = Box(orientation="v", spacing=16) - self.view.add(rows_box) - - current_row = None - items_per_row = self.items_per_row - - for i, window in enumerate(self.windows): - if i % items_per_row == 0: - current_row = Box( - name="window-row", - orientation="h", - spacing=16, - h_align="center", - v_align="center", - ) - rows_box.add(current_row) - - icon_image = self.create_icon_for_window(window) - - button_content = Box( - name="switcher-button", - orientation="v", - h_align="center", - v_align="center", - children=[ - Box( - name="switcher-icon-box", - style_classes=["window-basic", "sleek-border"], - children=[icon_image], - h_align="center", - v_align="center", - ), - ], - ) - - event_box = EventBox( - name="window-button", - style_classes=["active"] if i == self.current_index else None, - child=button_content, - events=["button-press-event"], - ) - event_box.connect( - "button-press-event", lambda w, e, idx=i: self._on_item_clicked(idx) - ) - current_row.add(event_box) - - self.view.show_all() - self.update_selection() - except Exception as e: - logger.error(f"Failed to update windows: {e}") - - def on_key_press(self, _, event): - keyval = event.keyval - state = event.state - alt_pressed = bool(state & Gdk.ModifierType.MOD1_MASK) - - if not self.windows: - return False - - if keyval == Gdk.KEY_Escape: - self.hide_switcher() - return True - - if keyval == Gdk.KEY_Tab: - if not self.tab_pressed or alt_pressed: - self.current_index = (self.current_index + 1) % len(self.windows) - self.update_selection() - self.tab_pressed = True - return True - - if keyval == Gdk.KEY_ISO_Left_Tab or ( - keyval == Gdk.KEY_Tab and (state & Gdk.ModifierType.SHIFT_MASK) - ): - self.current_index = (self.current_index - 1) % len(self.windows) - self.update_selection() - return True - - if keyval == Gdk.KEY_Return: - self.activate_selected() - self.hide_switcher() - return True - - if keyval == Gdk.KEY_Right or keyval == Gdk.KEY_l: - self.current_index = (self.current_index + 1) % len(self.windows) - self.update_selection() - return True - - if keyval == Gdk.KEY_Left or keyval == Gdk.KEY_h: - self.current_index = (self.current_index - 1) % len(self.windows) - self.update_selection() - return True - - if keyval == Gdk.KEY_Down: - next_index = self.current_index + self.items_per_row - if next_index < len(self.windows): - self.current_index = next_index - self.update_selection() - return True - - if keyval == Gdk.KEY_Up: - next_index = self.current_index - self.items_per_row - if next_index >= 0: - self.current_index = next_index - self.update_selection() - return True - - return False - - def on_key_release(self, _, event): - keyval = event.keyval - - if keyval in (Gdk.KEY_Alt_L, Gdk.KEY_Alt_R): - self.activate_selected() - self.hide_switcher() - return True - - if keyval == Gdk.KEY_Tab: - self.tab_pressed = False - return True - - return False - - def update_selection(self): - all_buttons = [] - rows_box = self.view.get_children()[0] if self.view.get_children() else None - if rows_box: - for row in rows_box.get_children(): - all_buttons.extend(row.get_children()) - - for i, child in enumerate(all_buttons): - if i == self.current_index: - child.add_style_class("active") - current_window = self.windows[self.current_index] - app_class = current_window.get("class", "Unknown") - if app_class.islower(): - app_class = app_class.capitalize() - self.selection_label.set_label(app_class) - - workspace = current_window.get("workspace", {}) - workspace_name = workspace.get("name", "Unknown") - self.workspace_label.set_label(f"Workspace {workspace_name}") - else: - child.remove_style_class("active") - - def activate_selected(self): - if not self.windows or self.current_index >= len(self.windows): - return - - window = self.windows[self.current_index] - address = window.get("address") - if address: - focus_window(address) - - def _on_item_clicked(self, index): - self.current_index = index - self.update_selection() - self.activate_selected() - self.hide_switcher() - - def grab_keyboard(self): - try: - display = Gdk.Display.get_default() - seat = display.get_default_seat() - window = self.get_window() - seat.grab(window, Gdk.SeatCapabilities.KEYBOARD, False, None, None, None) - except Exception as e: - logger.error(f"Failed to grab keyboard: {e}") - - def ungrab_keyboard(self): - try: - display = Gdk.Display.get_default() - seat = display.get_default_seat() - seat.ungrab() - except Exception as e: - logger.error(f"Failed to ungrab keyboard: {e}") diff --git a/src/window/switcher/__init__.py b/src/window/switcher/__init__.py new file mode 100644 index 0000000..03914e7 --- /dev/null +++ b/src/window/switcher/__init__.py @@ -0,0 +1,3 @@ +from window.switcher.main import ApplicationSwitcher + +__all__ = ["ApplicationSwitcher"] diff --git a/src/window/switcher/main.py b/src/window/switcher/main.py new file mode 100644 index 0000000..cf064cb --- /dev/null +++ b/src/window/switcher/main.py @@ -0,0 +1,438 @@ +from collections import OrderedDict + +from fabric.utils import Gdk, GdkPixbuf, GLib, logger +from fabric.widgets.box import Box +from fabric.widgets.eventbox import EventBox +from fabric.widgets.image import Image +from fabric.widgets.label import Label +from fabric.widgets.overlay import Overlay +from fabric.widgets.wayland import WaylandWindow as Window + +from services.config import config, on_config_change +from services.modus import close_window, focus_window, get_active_window, get_clients +from utils.functions import is_special_workspace +from utils.icon_resolver import IconResolver + + +class _SwitcherItem: + __slots__ = ( + "button", + "item_box", + "icon_box", + "name_label", + "image", + "overlay", + "badge_box", + "badge_label", + "address", + ) + + def __init__( + self, + button, + item_box, + icon_box, + name_label, + image, + overlay, + badge_box, + badge_label, + address, + ): + self.button = button + self.item_box = item_box + self.icon_box = icon_box + self.name_label = name_label + self.image = image + self.overlay = overlay + self.badge_box = badge_box + self.badge_label = badge_label + self.address = address + + +class ApplicationSwitcher(Window): + def __init__(self, **kwargs): + super().__init__( + name="application-switcher", + title="modus-switcher", + layer="top", + anchor="center", + keyboard_mode="exclusive", + visible=False, + **kwargs, + ) + + self.icon_resolver = IconResolver() + self.windows: list[dict] = [] + self.current_index = 0 + self._prev_index = -1 + self.icon_size = 80 + self._pixbuf_cache: OrderedDict = OrderedDict() + self._items: list[_SwitcherItem] = [] + self._pending_close = None + + on_config_change(self._on_config_changed) + + self.view = Box( + name="app-switcher-view", + orientation="h", + spacing=4, + h_align="center", + v_align="center", + ) + + container = Box( + name="app-switcher-container", + orientation="v", + h_align="center", + v_align="center", + children=[self.view], + ) + self.add(container) + + self.connect("key-press-event", self.on_key_press) + self.connect("key-release-event", self.on_key_release) + + self.show_all() + self.set_visible(False) + + def _on_config_changed(self, new_config, old_config): + if config().has_changed("hide_special_workspace", old_config): + if self.get_visible(): + self._rebuild() + + def show_switcher(self) -> None: + self._rebuild() + if not self.windows: + return + + self.set_opacity(0.0) + self.show() + self.ungrab_keyboard() + self.grab_keyboard() + GLib.timeout_add(16, self._fade_in) + + def _fade_in(self): + current = self.get_opacity() + if current >= 1.0: + return False + self.set_opacity(min(1.0, current + 0.15)) + return True + + def hide_switcher(self) -> None: + self.set_opacity(0.0) + if self._pending_close is not None: + close_window(self._pending_close) + self._pending_close = None + GLib.timeout_add(150, self.hide) + self.ungrab_keyboard() + + def _get_pixbuf(self, class_name: str) -> GdkPixbuf.Pixbuf | None: + key = (class_name, self.icon_size) + if key in self._pixbuf_cache: + return self._pixbuf_cache[key] + + if len(self._pixbuf_cache) > 128: + self._pixbuf_cache.popitem(last=False) + + pixbuf = self.icon_resolver.get_icon_pixbuf(class_name, self.icon_size) + if not pixbuf: + pixbuf = self.icon_resolver.get_icon_pixbuf( + "application-x-executable-symbolic", self.icon_size + ) + if pixbuf: + self._pixbuf_cache[key] = pixbuf + return pixbuf + + def _create_item(self) -> _SwitcherItem: + image = Image() + overlay = Overlay(child=image) + + name_label = Label( + label="", + name="app-switcher-item-label", + h_align="center", + ) + + icon_box = Box( + name="app-switcher-icon-box", + children=[overlay], + h_align="center", + v_align="center", + ) + + item_box = Box( + name="app-switcher-item", + orientation="v", + h_align="center", + v_align="center", + spacing=2, + children=[icon_box, name_label], + ) + + event_box = EventBox( + name="app-switcher-button", + child=item_box, + events=["button-press-event"], + can_focus=False, + ) + event_box.connect("button-press-event", self._on_item_clicked) + event_box.connect("enter-notify-event", self._on_item_enter) + event_box.connect("leave-notify-event", self._on_item_leave) + + return _SwitcherItem( + button=event_box, + item_box=item_box, + icon_box=icon_box, + name_label=name_label, + image=image, + overlay=overlay, + badge_box=None, + badge_label=None, + address=None, + ) + + def _clear_states(self): + for item in self._items: + item.button.remove_style_class("hovered") + item.button.remove_style_class("selected") + item.name_label.set_opacity(0.0) + + def _rebuild(self) -> None: + try: + self._prev_index = -1 + self._clear_states() + + for child in list(self.view.get_children()): + if not any(item.button == child for item in self._items): + self.view.remove(child) + child.destroy() + + clients = get_clients() + if not clients: + self.windows = [] + for item in self._items: + item.button.set_visible(False) + return + + hide_special = config().get("hide_special_workspace", True) + filtered = [] + for c in clients: + if c.get("hidden", False): + continue + if hide_special and is_special_workspace(c): + continue + filtered.append(c) + + active = get_active_window() + active_addr = active.get("address") if active else None + if active_addr: + idx = next( + ( + i + for i, w in enumerate(filtered) + if w.get("address") == active_addr + ), + None, + ) + if idx is not None and idx > 0: + filtered.insert(0, filtered.pop(idx)) + + self.windows = [ + { + "address": w["address"], + "class": w.get("class") or "Unknown", + "workspace_id": w.get("workspace", {}).get("id"), + } + for w in filtered + ] + self.current_index = 0 + + while len(self._items) < len(self.windows): + item = self._create_item() + self._items.append(item) + self.view.add(item.button) + + for i, item in enumerate(self._items): + visible = i < len(self.windows) + item.button.set_visible(visible) + if not visible: + item.button.remove_style_class("hovered") + + for i, item in enumerate(self._items[: len(self.windows)]): + win = self.windows[i] + item.address = win["address"] + + pixbuf = self._get_pixbuf(win["class"].lower()) + if pixbuf: + item.image.set_from_pixbuf(pixbuf) + item.name_label.set_text(win["class"]) + + ws_id = win["workspace_id"] + if ws_id is not None: + if item.badge_box is None: + badge_label = Label( + label=str(ws_id), + name="app-switcher-workspace-badge-text", + ) + badge_box = Box( + name="app-switcher-workspace-badge", + children=[badge_label], + h_align="end", + v_align="end", + ) + item.overlay.add_overlay(badge_box) + item.badge_box = badge_box + item.badge_label = badge_label + else: + item.badge_label.set_text(str(ws_id)) + item.badge_box.set_visible(True) + else: + if item.badge_box is not None: + item.badge_box.set_visible(False) + + self._update_selection() + except Exception: + logger.exception("Failed to build switcher") + + def on_key_press(self, _, event): + try: + return self._on_key_press_impl(_, event) + except Exception: + logger.exception("on_key_press error") + return False + + def _on_key_press_impl(self, _, event): + keyval = event.keyval + state = event.state + name = Gdk.keyval_name(keyval) + + if not self.windows: + return False + + if keyval == Gdk.KEY_Escape: + self.hide_switcher() + return True + + if name in ("Tab", "ISO_Left_Tab", "KP_Tab"): + if state & Gdk.ModifierType.SHIFT_MASK or name == "ISO_Left_Tab": + self.current_index = (self.current_index - 1) % len(self.windows) + else: + self.current_index = (self.current_index + 1) % len(self.windows) + self._update_selection() + return True + + if keyval in (Gdk.KEY_Right, Gdk.KEY_l): + self.current_index = (self.current_index + 1) % len(self.windows) + self._update_selection() + return True + + if keyval in (Gdk.KEY_Left, Gdk.KEY_h): + self.current_index = (self.current_index - 1) % len(self.windows) + self._update_selection() + return True + + if keyval in (Gdk.KEY_Delete, Gdk.KEY_BackSpace): + self._close_selected() + return True + + return False + + def on_key_release(self, _, event): + keyval = event.keyval + if keyval in (Gdk.KEY_Alt_L, Gdk.KEY_Alt_R): + self._activate_selected() + self.hide_switcher() + return True + return False + + def _update_selection(self): + try: + if self._prev_index >= 0 and self._prev_index < len(self._items): + prev = self._items[self._prev_index] + prev.button.remove_style_class("selected") + prev.name_label.set_opacity(0.0) + + if self.current_index < len(self._items): + curr = self._items[self.current_index] + curr.button.add_style_class("selected") + curr.name_label.set_opacity(1.0) + + self._prev_index = self.current_index + except Exception: + logger.exception("_update_selection error") + + def _activate_selected(self): + if not self.windows or self.current_index >= len(self.windows): + return + address = self.windows[self.current_index]["address"] + if address: + focus_window(address) + + def _close_selected(self): + try: + self._close_selected_impl() + except Exception: + logger.exception("_close_selected error") + + def _close_selected_impl(self): + if not self.windows or self.current_index >= len(self.windows): + return + + idx = self.current_index + self._pending_close = self.windows[idx]["address"] + + item = self._items.pop(idx) + item.button.set_visible(False) + + self.windows.pop(idx) + + if not self.windows: + self.hide_switcher() + return + + if idx < self.current_index: + self.current_index -= 1 + elif self.current_index >= len(self.windows): + self.current_index = len(self.windows) - 1 + + self._prev_index = -1 + self._update_selection() + + def _on_item_clicked(self, event_box, _event): + for i, item in enumerate(self._items): + if item.button == event_box: + self.current_index = i + break + self._activate_selected() + self.hide_switcher() + return True + + def _on_item_enter(self, event_box, _event): + event_box.add_style_class("hovered") + return False + + def _on_item_leave(self, event_box, _event): + event_box.remove_style_class("hovered") + return False + + def grab_keyboard(self): + try: + display = Gdk.Display.get_default() + seat = display.get_default_seat() + window = self.get_window() + status = seat.grab( + window, Gdk.SeatCapabilities.KEYBOARD, False, None, None, None + ) + if status != Gdk.GrabStatus.SUCCESS: + logger.warning(f"Keyboard grab failed: {status}") + except Exception: + logger.exception("Failed to grab keyboard") + + def ungrab_keyboard(self): + try: + display = Gdk.Display.get_default() + seat = display.get_default_seat() + seat.ungrab() + except Exception: + logger.exception("Failed to ungrab keyboard")