feat(src/window):restructure dock UI and application switcher using config‑driven layout

This commit is contained in:
sankalp committed 2026-05-03 22:06:48 +05:45
1 parent be308191c5
commit 63f0b046ca
3 files changed
+647 -90

No files matched your search

+99 -58
View File
@@ -20,7 +20,7 @@ from fabric.widgets.overlay import Overlay
from fabric.widgets.revealer import Revealer
from fabric.widgets.wayland import WaylandWindow as Window
import shared.data as data
from services.config import config, on_config_change
from services.modus import modus_service
from utils.functions import (
clear_children,
@@ -44,15 +44,10 @@ class AppBar(Box):
self.pinned_items_pos = []
self._parent = parent
# Set orientation based on dock position
orientation = (
"vertical" if data.DOCK_POSITION in ["Left", "Right"] else "horizontal"
)
super().__init__(
spacing=0,
name="dock",
orientation=orientation,
orientation="horizontal",
children=[],
)
self.icon_resolver = IconResolver()
@@ -87,6 +82,39 @@ class AppBar(Box):
self._app_monitor_timer_id = GLib.timeout_add(250, update_running_apps)
GLib.idle_add(self.update_dock_apps)
def update_icon_size(self):
"""Update all icons in the dock to the current config size"""
desktop_apps = get_desktop_applications(include_hidden=False)
# Update pinned apps
for app_id, button in self.pinned_buttons.items():
if hasattr(button, "icon_image"):
if app_id == "trash" or getattr(button, "is_trash", False):
icon_size = config().get("dock_icon_size", 52)
pixbuf = self.icon_resolver.get_icon_pixbuf("user-trash", icon_size)
button.icon_image.set_from_pixbuf(pixbuf)
else:
# Find app data to get icon
app_data = next(
(
a
for a in self.pinned_apps
if self._matches_app_identifier(a, app_id)
),
app_id,
)
app = self._find_desktop_app(app_id, desktop_apps)
pixbuf = self._get_app_icon(app_data, app)
button.icon_image.set_from_pixbuf(pixbuf)
# Update running apps
for addr, button in self.client_buttons.items():
if hasattr(button, "icon_image"):
app_class = getattr(button, "app_class", "")
app = self._find_desktop_app(app_class, desktop_apps)
pixbuf = self._get_app_icon(app_class, app)
button.icon_image.set_from_pixbuf(pixbuf)
def destroy(self):
"""Clean up timers and children"""
if hasattr(self, "_app_monitor_timer_id") and self._app_monitor_timer_id:
@@ -131,10 +159,11 @@ class AppBar(Box):
icon_pixbuf = self._get_app_icon(app_data, app)
icon_image = Image(name="dock_item_icon", pixbuf=icon_pixbuf)
main_container = Box(
name="dock_item_main_container",
orientation="v",
children=[Image(name="dock_item_icon", pixbuf=icon_pixbuf)],
children=[icon_image],
)
pinned_button = Button(
@@ -153,14 +182,16 @@ class AppBar(Box):
)
pinned_button.add_style_class("shown")
pinned_button.icon_image = icon_image
self.pinned_buttons[app_identifier] = pinned_button
self.pinned_apps_container.add(pinned_button)
self.pinned_items_pos.append(pinned_button)
def _get_app_icon(self, app_data, app=None):
icon_size = config().get("dock_icon_size", 52)
if app:
return app.get_icon_pixbuf(data.DOCK_ICON_SIZE)
return app.get_icon_pixbuf(icon_size)
icon_name = ""
if isinstance(app_data, dict):
@@ -169,19 +200,19 @@ class AppBar(Box):
icon_name = app_data
return self.icon_resolver.get_icon_pixbuf(
icon_name or "application-x-executable", data.DOCK_ICON_SIZE
icon_name or "application-x-executable", icon_size
)
def _create_trash_button(self):
"""Create a trash button that opens the trash in file manager"""
trash_icon_pixbuf = self.icon_resolver.get_icon_pixbuf(
"user-trash", data.DOCK_ICON_SIZE
)
icon_size = config().get("dock_icon_size", 52)
trash_icon_pixbuf = self.icon_resolver.get_icon_pixbuf("user-trash", icon_size)
trash_icon_image = Image(name="dock_item_icon", pixbuf=trash_icon_pixbuf)
main_container = Box(
name="dock_item_main_container",
orientation="v",
children=[Image(name="dock_item_icon", pixbuf=trash_icon_pixbuf)],
children=[trash_icon_image],
)
trash_button = Button(
@@ -199,6 +230,7 @@ class AppBar(Box):
trash_button.add_style_class("shown")
trash_button.is_trash = True
trash_button.icon_image = trash_icon_image
self.pinned_buttons["trash"] = trash_button
self.pinned_apps_container.add(trash_button)
@@ -646,6 +678,7 @@ class AppBar(Box):
client_button.client_data = client
client_button.app_class = app_class
client_button.workspace_label = workspace_label
client_button.icon_image = client_image
client_button.add_style_class("shown")
self.client_buttons[instance_address] = client_button
@@ -666,7 +699,7 @@ class AppBar(Box):
return None
def _should_show_app_instance(self, client):
if not data.DOCK_HIDE_SPECIAL_WORKSPACE_APPS:
if not config().get("dock_hide_special_workspace_apps", True):
return True
workspace_id = self._get_workspace_id(client)
@@ -755,19 +788,13 @@ class AppBar(Box):
class Dock(Window):
def __init__(self):
dock_config = self._get_dock_config()
if not data.DOCK_ENABLED:
super().__init__(layer="top", title="dock", anchor=dock_config["anchor"])
self.children = Box() # Empty dock if disabled
return
super().__init__(layer="top", anchor=dock_config["anchor"])
super().__init__(layer="top", anchor="bottom center", title="modus-dock")
self.app_bar = AppBar(self)
self.revealer = Revealer(
child=Box(children=[self.app_bar], style="padding: 20px 50px 5px 50px;"),
transition_duration=200,
transition_type=dock_config["transition"],
transition_type="slide-up",
)
self.children = EventBox(
@@ -777,17 +804,51 @@ class Dock(Window):
on_leave_notify_event=lambda *_: self.on_hover_leave(),
)
self.revealer.set_reveal_child(True)
self.app_bar.add_style_class("shown")
self._update_visibility()
self.dock_height = 100
self.is_hovered = False
self.hide_ticket = 0
# Only setup occlusion monitoring if auto-hide is enabled
if data.DOCK_AUTO_HIDE:
# Subscribe to config changes for hot-reloading
on_config_change(self._on_config_change)
if config().get("dock_auto_hide", True):
self.setup_occlusion_monitoring()
def _on_config_change(self, new_config, old_config):
# Handle dock visibility
if config().has_changed("dock_enabled", old_config):
self._update_visibility()
# Handle icon size changes
if config().has_changed("dock_icon_size", old_config):
self.app_bar.update_icon_size()
# Handle auto-hide changes
if config().has_changed("dock_auto_hide", old_config):
if new_config.get("dock_auto_hide", True):
self.setup_occlusion_monitoring()
else:
if hasattr(self, "_occlusion_timer_id") and self._occlusion_timer_id:
GLib.source_remove(self._occlusion_timer_id)
self._occlusion_timer_id = None
self.revealer.set_reveal_child(True)
self.app_bar.add_style_class("shown")
# Handle special workspace apps visibility
if config().has_changed("dock_hide_special_workspace_apps", old_config):
self.app_bar.update_dock_apps()
def _update_visibility(self):
enabled = config().get("dock_enabled", True)
if enabled:
self.show()
self.revealer.set_reveal_child(True)
self.app_bar.add_style_class("shown")
else:
self.hide()
def on_hover_enter(self):
self.is_hovered = True
self.hide_ticket = random.getrandbits(32)
@@ -800,14 +861,10 @@ class Dock(Window):
def delayed_hide(ticket):
if ticket == self.hide_ticket and not self.is_hovered:
# Only hide if we aren't occluded (or if occlusion logic says so)
# For now, just trigger the basic hide if auto-hide is on
if data.DOCK_AUTO_HIDE:
# Check if actually occluded before hiding if DOCK_ALWAYS_OCCLUDED is false
occlusion_pos = self._get_dock_config()["occlusion"]
is_occluded = data.DOCK_ALWAYS_OCCLUDED or check_occlusion(
(occlusion_pos, self.dock_height)
)
if config().get("dock_auto_hide", True):
is_occluded = config().get(
"dock_always_occluded", False
) or check_occlusion(("bottom", self.dock_height))
if is_occluded:
self.revealer.set_reveal_child(False)
self.app_bar.remove_style_class("shown")
@@ -815,34 +872,18 @@ class Dock(Window):
GLib.timeout_add(500, delayed_hide, self.hide_ticket)
def _get_dock_config(self):
config_map = {
"Left": {
"anchor": "left center",
"transition": "slide-right",
"occlusion": "left",
},
"Right": {
"anchor": "right center",
"transition": "slide-left",
"occlusion": "right",
},
"Bottom": {
"anchor": "bottom center",
"transition": "slide-up",
"occlusion": "bottom",
},
}
return config_map.get(data.DOCK_POSITION, config_map["Bottom"])
def setup_occlusion_monitoring(self):
# Remove existing timer if any
if hasattr(self, "_occlusion_timer_id") and self._occlusion_timer_id:
GLib.source_remove(self._occlusion_timer_id)
self._occlusion_timer_id = None
def check_dock_occlusion():
try:
if data.DOCK_ALWAYS_OCCLUDED:
if config().get("dock_always_occluded", False):
is_occluded = True
else:
occlusion_pos = self._get_dock_config()["occlusion"]
is_occluded = check_occlusion((occlusion_pos, self.dock_height))
is_occluded = check_occlusion(("bottom", self.dock_height))
if (
is_occluded
+489
View File
@@ -0,0 +1,489 @@
from fabric.widgets.box import Box
from fabric.widgets.button import Button
from fabric.widgets.centerbox import CenterBox
from fabric.widgets.entry import Entry
from fabric.widgets.label import Label
from fabric.widgets.scrolledwindow import ScrolledWindow
from fabric.widgets.stack import Stack
from fabric.utils import Gtk, GLib, Gdk
from services.config import config, on_config_change
from utils.utils import setup_cursor_hover, svg_file
class SettingsRow(CenterBox):
def __init__(self, label: str, child=None, description: str = None, **kwargs):
label_widget = Label(label=label, name="settings-row-label", h_align="start")
start_children = [label_widget]
if description:
description_widget = Label(
label=description, name="settings-row-description", h_align="start"
)
start_children = [
Box(
orientation="v",
spacing=2,
children=[label_widget, description_widget],
)
]
super().__init__(
name="settings-row",
start_children=start_children,
end_children=[child] if child else [],
**kwargs,
)
self.label_widget = label_widget
if description:
self.description_widget = description_widget
class SettingsSwitch(Button):
def __init__(self, config_key: str, **kwargs):
self.config_key = config_key
self.active = config().get(config_key, False)
super().__init__(name="settings-switch", on_clicked=self.toggle, **kwargs)
self._update_style()
setup_cursor_hover(self, "pointer")
# Sync with external config changes
on_config_change(self._on_config_change)
def _on_config_change(self, new_config, old_config):
if config().has_changed(self.config_key, old_config):
self.active = new_config.get(self.config_key, False)
self._update_style()
def toggle(self, *args):
self.active = not self.active
config().set(self.config_key, self.active)
config().save()
self._update_style()
def _update_style(self):
if self.active:
self.add_style_class("active")
self.set_label("On")
else:
self.remove_style_class("active")
self.set_label("Off")
class SettingsEntry(Entry):
def __init__(self, config_key: str, **kwargs):
self.config_key = config_key
initial_value = str(config().get(config_key, ""))
super().__init__(
name="settings-entry",
text=initial_value,
on_activate=self.on_change,
**kwargs,
)
# Sync with external config changes
on_config_change(self._on_config_change)
def _on_config_change(self, new_config, old_config):
if config().has_changed(self.config_key, old_config):
new_val = str(new_config.get(self.config_key, ""))
if self.get_text() != new_val:
self.set_text(new_val)
def on_change(self, entry, *args):
value = entry.get_text()
# Try to convert to int if possible
try:
if value.isdigit():
value = int(value)
except Exception:
pass
config().set(self.config_key, value)
config().save()
class SettingsComboBox(Gtk.ComboBoxText):
def __init__(self, config_key: str, options: list, **kwargs):
super().__init__(**kwargs)
self.set_name("settings-combo")
self.set_halign(Gtk.Align.END)
self.config_key = config_key
self.options = options
for opt in options:
self.append_text(opt)
self._update_from_config()
self.connect("changed", self.on_change)
# Sync with external config changes
on_config_change(self._on_config_change)
def _update_from_config(self):
current_val = str(config().get(self.config_key, ""))
if current_val in self.options:
self.set_active(self.options.index(current_val))
def _on_config_change(self, new_config, old_config):
if config().has_changed(self.config_key, old_config):
self._update_from_config()
def on_change(self, combo):
value = combo.get_active_text()
if value:
config().set(self.config_key, value)
config().save()
class SettingsList(Box):
"""A list of tags that can be added or removed"""
def __init__(self, config_key: str, **kwargs):
super().__init__(name="settings-list", orientation="v", spacing=5, **kwargs)
self.config_key = config_key
self.list_box = Box(spacing=5, name="settings-list-tags")
self.entry = Entry(
name="settings-list-entry",
placeholder="Add new item...",
on_activate=self.on_add,
)
self.add(self.list_box)
self.add(self.entry)
self._update_list()
on_config_change(self._on_config_change)
def _on_config_change(self, new_config, old_config):
if config().has_changed(self.config_key, old_config):
self._update_list()
def _update_list(self):
items = config().get(self.config_key, [])
if not isinstance(items, list):
items = []
# Clear existing tags
self.list_box.children = []
for item in items:
tag = Button(
name="settings-list-tag",
child=Box(
spacing=5,
children=[
Label(label=item),
Label(label="✕", name="settings-list-tag-close"),
],
),
on_clicked=lambda *_, i=item: self.on_remove(i),
)
setup_cursor_hover(tag, "pointer")
self.list_box.pack_start(tag, False, False, 0)
self.list_box.show_all()
def on_add(self, entry):
val = entry.get_text().strip()
if not val:
return
items = config().get(self.config_key, [])
if val not in items:
items.append(val)
config().set(self.config_key, items)
config().save()
entry.set_text("")
self._update_list()
def on_remove(self, item):
items = config().get(self.config_key, [])
if item in items:
items.remove(item)
config().set(self.config_key, items)
config().save()
self._update_list()
class SettingsPage(ScrolledWindow):
def __init__(self, title: str, rows: list, **kwargs):
container = Box(
name="settings-page-container",
orientation="v",
spacing=10,
h_expand=True,
v_expand=True,
children=[
Label(label=title, name="settings-page-title", h_align="start"),
Box(orientation="v", spacing=1, children=rows, h_expand=True),
],
)
super().__init__(
name="settings-page",
h_expand=True,
v_expand=True,
propagate_width=True,
propagate_height=True,
min_content_width=600,
min_content_height=500,
**kwargs,
)
self.add(container)
self.show_all()
self._container = container
class SettingsWindow(Gtk.Window):
def __init__(self, **kwargs):
GLib.set_prgname("modus-settings")
super().__init__(title="Modus Settings", **kwargs)
self.set_name("settings-window")
self.set_wmclass("modus-settings", "Modus")
self.set_default_size(850, 600)
self.set_resizable(False)
self.set_type_hint(Gtk.Window.get_type_hint(self) | Gdk.WindowTypeHint.DIALOG)
self.set_position(Gtk.WindowPosition.CENTER)
self.set_visible(False)
# Reset global singleton on destroy
self.connect("destroy", self._on_window_destroy)
self.stack = Stack(
name="settings-stack",
transition_type="none",
h_expand=True,
v_expand=True,
)
self.pages = {
"general": self._create_general_page(),
"dock": self._create_dock_page(),
"panel": self._create_panel_page(),
"notifications": self._create_notifications_page(),
}
for name, page in self.pages.items():
self.stack.add_named(page, name)
self.sidebar_buttons = {}
self.sidebar_buttons["general"] = self._create_sidebar_button(
"General", "general", "misc/logo.svg"
)
self.sidebar_buttons["dock"] = self._create_sidebar_button(
"Dock", "dock", "misc/control.svg"
)
self.sidebar_buttons["panel"] = self._create_sidebar_button(
"Panel", "panel", "misc/control-center.svg"
)
self.sidebar_buttons["notifications"] = self._create_sidebar_button(
"Notifications", "notifications", "notifications/notification-active.svg"
)
self.set_page("general")
self.sidebar = Box(
name="settings-sidebar",
orientation="v",
spacing=5,
children=list(self.sidebar_buttons.values()),
)
self.main_box = Box(
name="settings-main-box",
orientation="h",
h_expand=True,
v_expand=True,
children=[
self.sidebar,
Box(
name="settings-content-wrapper",
children=[self.stack],
h_expand=True,
v_expand=True,
),
],
)
self.add(self.main_box)
def _create_sidebar_button(self, label, page_name, icon_name):
btn = Button(
name="settings-sidebar-button",
child=Box(
orientation="h",
spacing=10,
children=[svg_file(icon_name, size=16), Label(label=label)],
),
on_clicked=lambda *_: self.set_page(page_name),
)
setup_cursor_hover(btn, "pointer")
return btn
def set_page(self, page_name):
if page_name not in self.pages:
return
self.stack.set_visible_child(self.pages[page_name])
for name, btn in self.sidebar_buttons.items():
if name == page_name:
btn.add_style_class("active")
else:
btn.remove_style_class("active")
def _create_general_page(self):
return SettingsPage(
"General Settings",
[
SettingsRow(
"Wallpapers Directory",
SettingsEntry("wallpapers_dir"),
"Path to your wallpaper collection",
),
SettingsRow(
"Weather Location",
SettingsEntry("weather_location"),
"City name for weather updates",
),
SettingsRow(
"Keyboard Layouts",
SettingsList("keyboard_layouts"),
"Manage active keyboard input languages",
),
SettingsRow(
"Window Switcher",
SettingsSwitch("window_switcher"),
"Enable the Alt-Tab window switcher",
),
SettingsRow(
"Items Per Row",
SettingsEntry("window_switcher_items_per_row"),
"Maximum items in window switcher row",
),
SettingsRow(
"Hide Special Workspace",
SettingsSwitch("hide_special_workspace"),
"Don't show special workspace in indicators",
),
SettingsRow(
"OSD",
SettingsSwitch("osd"),
"On-screen display for volume/brightness",
),
],
)
def _create_dock_page(self):
return SettingsPage(
"Dock Settings",
[
SettingsRow(
"Enabled",
SettingsSwitch("dock_enabled"),
"Show the application dock",
),
SettingsRow(
"Auto Hide",
SettingsSwitch("dock_auto_hide"),
"Hide dock when not in use",
),
SettingsRow(
"Always Occluded",
SettingsSwitch("dock_always_occluded"),
"Keep dock behind other windows",
),
SettingsRow(
"Icon Size",
SettingsEntry("dock_icon_size"),
"Size of dock icons in pixels",
),
SettingsRow(
"Hide Special Apps",
SettingsSwitch("dock_hide_special_workspace_apps"),
"Hide apps from special workspace in dock",
),
],
)
def _create_panel_page(self):
# panel visibility settings
rows = []
panel_settings = [
("iMac Button", "imac_button"),
("Global Menu", "global_menu"),
("Systray", "systray"),
("Control Center", "control_center"),
("Search", "search"),
("Network", "network"),
("Battery", "battery"),
("Bluetooth", "bluetooth"),
("Date & Time", "date_time"),
("Workspace Indicator", "workspace_indicator"),
("Notification Center", "notification_center"),
]
for label, key in panel_settings:
rows.append(SettingsRow(label, SettingsSwitch(key)))
rows.append(
SettingsRow(
"Systray Ignore",
SettingsList("systray_ignore"),
"Icons to hide from the system tray",
)
)
return SettingsPage("Panel Settings", rows)
def _create_notifications_page(self):
return SettingsPage(
"Notification Settings",
[
SettingsRow(
"Timeout",
SettingsEntry("notification_timeout"),
"How long notifications stay on screen (e.g. 5s)",
),
SettingsRow(
"Ignored Apps",
SettingsList("notification_ignored_apps"),
"Apps that won't show notifications",
),
SettingsRow(
"Limited History",
SettingsList("notification_limited_apps_history"),
"Apps with only the latest notification shown",
),
],
)
def _on_window_destroy(self, *args):
global _settings_window
_settings_window = None
def toggle(self):
if not self.get_visible():
self.show_all()
self.present()
# Re-sync current page
current_page = "general"
for name, btn in self.sidebar_buttons.items():
if "active" in btn.get_style_context().list_classes():
current_page = name
break
self.set_page(current_page)
else:
self.hide()
_settings_window = None
def get_settings_window():
global _settings_window
if _settings_window is None:
_settings_window = SettingsWindow()
return _settings_window
+59 -32
View File
@@ -8,7 +8,7 @@ from fabric.widgets.image import Image
from fabric.widgets.label import Label
from fabric.widgets.wayland import WaylandWindow as Window
import shared.data as data
from services.config import config, on_config_change
from utils.functions import is_special_workspace
from utils.icon_resolver import IconResolver
@@ -31,9 +31,10 @@ class ApplicationSwitcher(Window):
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
# Subscribe to config changes
on_config_change(self._on_config_changed)
container = Box(
name="application-switcher-container",
@@ -70,12 +71,25 @@ class ApplicationSwitcher(Window):
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:
@@ -119,13 +133,13 @@ class ApplicationSwitcher(Window):
# 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 data.DOCK_HIDE_SPECIAL_WORKSPACE_APPS and self._is_special_workspace(
c
):
if hide_special and self._is_special_workspace(c):
continue
filtered_windows.append(c)
@@ -143,16 +157,24 @@ class ApplicationSwitcher(Window):
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)
# 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)
@@ -256,25 +278,30 @@ class ApplicationSwitcher(Window):
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)
# 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())
# 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")
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):