Files
Modus/src/window/switcher.py
T

346 lines
11 KiB
Python

import json
from fabric.hyprland.widgets import get_hyprland_connection
from fabric.utils import Gdk, 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.wayland import WaylandWindow as Window
from services.config import config, on_config_change
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.icon_size = 96
# Subscribe to config changes
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):
# Refresh windows if settings that affect display change
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):
"""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():
child.destroy()
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 = []
hide_special = config().get("hide_special_workspace", True)
for c in clients:
if c.get("hidden", False):
continue
# Skip clients in special workspaces if the setting is enabled
if hide_special 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
# Create vertical container for rows
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)
# 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,
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):
# Flatten all buttons from all rows to find them by index
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")
# 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:
GLib.spawn_command_line_async(
f"hyprctl dispatch 'hl.dsp.focus({{ window = \"address:{address}\" }})'"
)
except Exception as e:
logger.error(f"Failed to focus window: {e}")
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}")