import json from fabric.hyprland.widgets import get_hyprland_connection from fabric.utils import Gdk 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 import config.data as data 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, # Start hidden until explicitly shown **kwargs, ) self.conn = get_hyprland_connection() self.icon_resolver = IconResolver() self.windows = [] self.current_index = 0 self.tab_pressed = False self.items_per_row = data.WINDOW_SWITCHER_ITEMS_PER_ROW self.icon_size = 96 self.items_per_row = 100 # Single row focus 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() 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): """Create an icon image for a specific window""" class_name = window.get("class", "").lower() 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 ) 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(): self.view.remove(child) try: clients_data = self.conn.send_command("j/clients").reply if not clients_data: return clients = json.loads(clients_data.decode("utf-8")) # Filter out hidden windows and optionally special workspace windows filtered_windows = [] for c in clients: if c.get("hidden", False): continue # Skip clients in special workspaces if the setting is enabled if data.DOCK_HIDE_SPECIAL_WORKSPACE_APPS and self._is_special_workspace( c ): continue filtered_windows.append(c) self.windows = filtered_windows active_data = self.conn.send_command("j/activewindow").reply active_window = ( json.loads(active_data.decode("utf-8")) if active_data else None ) 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 current_row = Box( name="window-row", orientation="h", spacing=16, h_align="center", v_align="center", ) self.view.add(current_row) for i, window in enumerate(self.windows): # Create icon image for this window 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, ) current_row.add(event_box) self.view.show_all() self.update_selection() except Exception as e: print(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): for row in self.view.get_children(): for i, child in enumerate(row.get_children()): index = self.view.get_children().index(row) * self.items_per_row + i if index == self.current_index: child.add_style_class("active") # Update selection label current_window = self.windows[self.current_index] app_class = current_window.get("class", "Unknown") # capitalize if it's all lowercase if app_class.islower(): app_class = app_class.capitalize() self.selection_label.set_label(app_class) # Update workspace label 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: try: command = f"/dispatch focuswindow address:{address}" self.conn.send_command(command) except Exception as e: print(f"Failed to focus window: {e}") 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: print(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: print(f"Failed to ungrab keyboard: {e}")