refactor(src/window): remove unused ApplicationSwitcher component

This commit is contained in:
sankalp committed 2026-07-09 22:33:14 +05:45
1 parent 84976320f0
commit 1dd99cf79d
4 files changed
+442 -329

No files matched your search

+1 -1
View File
@@ -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
-328
View File
@@ -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}")
+3
View File
@@ -0,0 +1,3 @@
from window.switcher.main import ApplicationSwitcher
__all__ = ["ApplicationSwitcher"]
+438
View File
@@ -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")