- Remove transient NEED_AUTH/DISCONNECTED error paths from wifi connect - Guard double-callback in connect_wifi_bssid/connect_wifi_with_password - Shell-quote user paths in wallpaper, screenshot, screenrecorder services - Fix OSD hide-timer use-after-free and applet_window stale source ID - Fix brightness poll timer leak on read failure - Fix spotlight search stale idle source removal - Lock dbusmenu _cache mutation under _registry_lock - Fix entry.revision written outside _registry_lock scope - Add fabric-cli exec wrapper for lock screen (lock_screen.lock()) - Replace hyprlock with lock screen in power menu and dropdown - Add debug mode toggle in settings (controls log verbosity) - Add diagnostic logging to globalmenu click chain
533 lines
17 KiB
Python
533 lines
17 KiB
Python
from fabric.utils import Gdk, GLib, Gtk, logger
|
|
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 services.config import config, on_config_change
|
|
from shared.widgets.smooth_switch import SmoothSwitch
|
|
from utils.gtk_utils import setup_cursor_hover, svg_file
|
|
|
|
|
|
class SettingsRow(CenterBox):
|
|
def __init__(
|
|
self, label: str, child=None, description: str | None = 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(SmoothSwitch):
|
|
def __init__(self, config_key: str, **kwargs):
|
|
self.config_key = config_key
|
|
initial_active = config().get(config_key, False)
|
|
|
|
super().__init__(
|
|
name="settings-switch",
|
|
active=initial_active,
|
|
on_user_toggle=self.toggle,
|
|
**kwargs,
|
|
)
|
|
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.set_active(new_config.get(self.config_key, False))
|
|
|
|
def toggle(self, state: bool):
|
|
config().set(self.config_key, state)
|
|
config().save()
|
|
|
|
|
|
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 as e:
|
|
logger.error(f"An error occurred: {e}")
|
|
|
|
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=False,
|
|
min_content_width=600,
|
|
**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_title("modus-settings")
|
|
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(
|
|
"Debug Mode",
|
|
SettingsSwitch("debug"),
|
|
"Enable verbose logging for development",
|
|
),
|
|
SettingsRow(
|
|
"Weather Location",
|
|
SettingsEntry("weather_location"),
|
|
"City name for weather updates",
|
|
),
|
|
SettingsRow(
|
|
"Keyboard Layouts",
|
|
SettingsList("keyboard_layouts"),
|
|
"Manage active keyboard input languages",
|
|
),
|
|
SettingsRow(
|
|
"Hide Special Workspace",
|
|
SettingsSwitch("hide_special_workspace"),
|
|
"Don't show special workspace in indicators",
|
|
),
|
|
SettingsRow(
|
|
"Night Light Temperature",
|
|
SettingsEntry("night_light_temperature"),
|
|
"Color temperature in Kelvin (e.g. 4500)",
|
|
),
|
|
SettingsRow(
|
|
"Live Preview",
|
|
SettingsSwitch("switcher_live_preview"),
|
|
"Show live window previews in switcher",
|
|
),
|
|
SettingsRow(
|
|
"Preview Delay (ms)",
|
|
SettingsEntry("switcher_live_preview_delay_ms"),
|
|
"Delay before live preview appears",
|
|
),
|
|
],
|
|
)
|
|
|
|
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):
|
|
rows = [
|
|
SettingsRow(
|
|
"Global Menu",
|
|
SettingsSwitch("global_menu"),
|
|
"Show application menu in the panel",
|
|
),
|
|
SettingsRow(
|
|
"iMac Button",
|
|
SettingsSwitch("imac_button"),
|
|
),
|
|
SettingsRow(
|
|
"Systray",
|
|
SettingsSwitch("systray"),
|
|
),
|
|
SettingsRow(
|
|
"Control Center",
|
|
SettingsSwitch("control_center"),
|
|
),
|
|
SettingsRow(
|
|
"Search",
|
|
SettingsSwitch("search"),
|
|
),
|
|
SettingsRow(
|
|
"Network",
|
|
SettingsSwitch("network"),
|
|
),
|
|
SettingsRow(
|
|
"Battery",
|
|
SettingsSwitch("battery"),
|
|
),
|
|
SettingsRow(
|
|
"Bluetooth",
|
|
SettingsSwitch("bluetooth"),
|
|
),
|
|
SettingsRow(
|
|
"Date & Time",
|
|
SettingsSwitch("date_time"),
|
|
),
|
|
SettingsRow(
|
|
"Workspace Indicator",
|
|
SettingsSwitch("workspace_indicator"),
|
|
),
|
|
SettingsRow(
|
|
"Notification Center",
|
|
SettingsSwitch("notification_center"),
|
|
),
|
|
SettingsRow(
|
|
"Custom Mods",
|
|
SettingsSwitch("custom_mods"),
|
|
"Enable custom Hyprland mods in panel",
|
|
),
|
|
SettingsRow(
|
|
"Window Switcher",
|
|
SettingsSwitch("window_switcher"),
|
|
"Enable Alt+Tab window switcher",
|
|
),
|
|
SettingsRow(
|
|
"OSD",
|
|
SettingsSwitch("osd"),
|
|
"Enable on-screen display for volume/brightness",
|
|
),
|
|
SettingsRow(
|
|
"Wallpapers Directory",
|
|
SettingsEntry("wallpapers_dir"),
|
|
"Path to wallpapers folder",
|
|
),
|
|
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
|