diff --git a/config/settings/about_tab.py b/config/settings/about_tab.py
new file mode 100644
index 0000000..b475efc
--- /dev/null
+++ b/config/settings/about_tab.py
@@ -0,0 +1,40 @@
+from fabric.widgets.box import Box
+from fabric.widgets.label import Label
+
+from config.data import APP_NAME_CAP
+
+
+class AboutTab:
+ """About information tab for settings"""
+
+ def __init__(self):
+ pass
+
+ def create_about_tab(self):
+ """Create the About tab content"""
+ vbox = Box(orientation="v", spacing=18, style="margin: 30px;")
+ vbox.add(
+ Label(
+ markup=f"{APP_NAME_CAP}",
+ h_align="start",
+ style="font-size: 1.5em; margin-bottom: 8px;",
+ )
+ )
+ vbox.add(
+ Label(
+ label="A hackable shell for Hyprland, powered by Fabric.",
+ h_align="start",
+ style="margin-bottom: 12px;",
+ )
+ )
+ repo_box = Box(orientation="h", spacing=6, h_align="start")
+ repo_label = Label(label="GitHub:", h_align="start")
+ repo_link = Label(
+ markup='https://github.com/S4NKALP/Modus'
+ )
+ repo_box.add(repo_label)
+ repo_box.add(repo_link)
+ vbox.add(repo_box)
+
+ vbox.add(Box(v_expand=True))
+ return vbox
diff --git a/config/settings/appearance_tab.py b/config/settings/appearance_tab.py
new file mode 100644
index 0000000..9873ce1
--- /dev/null
+++ b/config/settings/appearance_tab.py
@@ -0,0 +1,689 @@
+import os
+import gi
+from fabric.widgets.box import Box
+from fabric.widgets.button import Button
+from fabric.widgets.image import Image as FabricImage
+from fabric.widgets.label import Label
+from fabric.widgets.scrolledwindow import ScrolledWindow
+from gi.repository import Gdk, GdkPixbuf, Gtk
+
+from config.data import NOTIF_POS_DEFAULT, NOTIF_POS_KEY
+from config.settings.utils import bind_vars
+
+gi.require_version("Gtk", "3.0")
+
+
+class AppearanceTab:
+ """Appearance and layout management tab for settings"""
+
+ def __init__(self, themes, parent_window=None):
+ self.themes = themes
+ self.parent_window = parent_window
+ self.selected_face_icon = None
+
+ # Widget references
+ self.wall_dir_chooser = None
+ self.face_image = None
+ self.face_status_label = None
+ self.ws_mode_combo = None
+ self.ws_chinese_switch = None
+ self.position_combo = None
+ self.dock_theme_combo = None
+ self.icon_size_scale = None
+ self.application_switcher_scale = None
+ self.dock_switch = None
+ self.dock_auto_hide_switch = None
+ self.dock_hover_switch = None
+ self.dock_hide_special_switch = None
+ self.dock_hide_special_apps_switch = None
+ self.notification_pos_combo = None
+ self.component_switches = {}
+ self.corners_switch = None
+
+ def create_appearance_tab(self):
+ """Create the Appearance tab content"""
+ scrolled_window = ScrolledWindow(
+ h_scrollbar_policy="never",
+ v_scrollbar_policy="automatic",
+ h_expand=False,
+ v_expand=False,
+ propagate_width=False,
+ propagate_height=False,
+ )
+ # Set fixed size to match tab stack dimensions
+ scrolled_window.set_size_request(580, 580)
+
+ vbox = Box(orientation="v", spacing=15, style="margin: 15px;")
+ scrolled_window.add(vbox)
+
+ # Create wallpaper and profile icon section
+ self._create_wallpaper_profile_section(vbox)
+
+ # Add separator
+ separator1 = Box(
+ style="min-height: 1px; background-color: alpha(@fg_color, 0.2); margin: 5px 0px;",
+ h_expand=True,
+ )
+ vbox.add(separator1)
+
+ # Create layout options section
+ self._create_layout_options_section(vbox)
+
+ # Add separator
+ separator2 = Box(
+ style="min-height: 1px; background-color: alpha(@fg_color, 0.2); margin: 5px 0px;",
+ h_expand=True,
+ )
+ vbox.add(separator2)
+
+ # Create modules section
+ self._create_modules_section(vbox)
+
+ return scrolled_window
+
+ def _create_wallpaper_profile_section(self, vbox):
+ """Create wallpaper and profile icon section"""
+ top_grid = Gtk.Grid()
+ top_grid.set_column_spacing(20)
+ top_grid.set_row_spacing(5)
+ top_grid.set_margin_bottom(10)
+ vbox.add(top_grid)
+
+ # Wallpaper section
+ wall_header = Label(markup="Wallpapers", h_align="start")
+ top_grid.attach(wall_header, 0, 0, 1, 1)
+ wall_label = Label(label="Directory:", h_align="start", v_align="center")
+ top_grid.attach(wall_label, 0, 1, 1, 1)
+
+ chooser_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
+ chooser_container.set_halign(Gtk.Align.START)
+ chooser_container.set_valign(Gtk.Align.CENTER)
+ self.wall_dir_chooser = Gtk.FileChooserButton(
+ title="Select a folder", action=Gtk.FileChooserAction.SELECT_FOLDER
+ )
+ self.wall_dir_chooser.set_tooltip_text(
+ "Select the directory containing your wallpaper images"
+ )
+ self.wall_dir_chooser.set_filename(bind_vars.get("wallpapers_dir", ""))
+ self.wall_dir_chooser.set_size_request(180, -1)
+ chooser_container.add(self.wall_dir_chooser)
+ top_grid.attach(chooser_container, 1, 1, 1, 1)
+
+ # Profile icon section
+ face_header = Label(markup="Profile Icon", h_align="start")
+ top_grid.attach(face_header, 2, 0, 2, 1)
+ current_face = os.path.expanduser("~/.face.icon")
+ face_image_container = Box(
+ style_classes=["image-frame"], h_align="center", v_align="center"
+ )
+ self.face_image = FabricImage(size=64)
+ try:
+ if os.path.exists(current_face):
+ pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(current_face, 64, 64)
+ self.face_image.set_from_pixbuf(pixbuf)
+ else:
+ self.face_image.set_from_icon_name("user-info", Gtk.IconSize.DIALOG)
+ except Exception as e:
+ print(f"Error loading face icon: {e}")
+ self.face_image.set_from_icon_name("image-missing", Gtk.IconSize.DIALOG)
+ face_image_container.add(self.face_image)
+ top_grid.attach(face_image_container, 2, 1, 1, 1)
+
+ browse_btn_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
+ browse_btn_container.set_halign(Gtk.Align.START)
+ browse_btn_container.set_valign(Gtk.Align.CENTER)
+ face_btn = Button(
+ label="Browse...",
+ tooltip_text="Select a square image for your profile icon",
+ on_clicked=self.on_select_face_icon,
+ )
+ browse_btn_container.add(face_btn)
+ top_grid.attach(browse_btn_container, 3, 1, 1, 1)
+ self.face_status_label = Label(label="", h_align="start")
+ top_grid.attach(self.face_status_label, 2, 2, 2, 1)
+
+ def _create_layout_options_section(self, vbox):
+ """Create layout options section"""
+ layout_header = Label(markup="Layout Options", h_align="start")
+ vbox.add(layout_header)
+ layout_grid = Gtk.Grid()
+ layout_grid.set_column_spacing(20)
+ layout_grid.set_row_spacing(10)
+ layout_grid.set_margin_start(10)
+ layout_grid.set_margin_top(5)
+ vbox.add(layout_grid)
+
+ # Workspace mode
+ ws_mode_label = Label(label="Workspace Mode", h_align="start", v_align="center")
+ layout_grid.attach(ws_mode_label, 0, 0, 1, 1)
+
+ ws_mode_combo_container = Gtk.Box(
+ orientation=Gtk.Orientation.HORIZONTAL,
+ halign=Gtk.Align.START,
+ valign=Gtk.Align.CENTER,
+ )
+ self.ws_mode_combo = Gtk.ComboBoxText()
+ self.ws_mode_combo.set_tooltip_text("Select how workspaces are displayed")
+ ws_modes = ["Dots", "Numbers", "Single Button"]
+ for mode in ws_modes:
+ self.ws_mode_combo.append_text(mode)
+
+ # Set current mode based on config
+ current_dots = bind_vars.get("workspace_dots", True)
+ current_nums = bind_vars.get("workspace_nums", False)
+ if current_dots:
+ current_mode = "Dots"
+ elif current_nums:
+ current_mode = "Numbers"
+ else:
+ current_mode = "Single Button"
+
+ try:
+ self.ws_mode_combo.set_active(ws_modes.index(current_mode))
+ except ValueError:
+ self.ws_mode_combo.set_active(0)
+
+ self.ws_mode_combo.connect("changed", self.on_ws_mode_changed)
+ ws_mode_combo_container.add(self.ws_mode_combo)
+ layout_grid.attach(ws_mode_combo_container, 1, 0, 1, 1)
+
+ # Chinese numerals option
+ ws_chinese_label = Label(
+ label="Use Chinese Numerals", h_align="start", v_align="center"
+ )
+ layout_grid.attach(ws_chinese_label, 2, 0, 1, 1)
+ ws_chinese_switch_container = Gtk.Box(
+ orientation=Gtk.Orientation.HORIZONTAL,
+ halign=Gtk.Align.START,
+ valign=Gtk.Align.CENTER,
+ )
+ self.ws_chinese_switch = Gtk.Switch(
+ active=bind_vars.get("workspace_use_chinese_numerals", False),
+ sensitive=current_nums, # Only enabled when numbers mode is selected
+ )
+ self.ws_chinese_switch.set_tooltip_text(
+ "Use Chinese numerals (一, 二, 三...) instead of Arabic numbers"
+ )
+ ws_chinese_switch_container.add(self.ws_chinese_switch)
+ layout_grid.attach(ws_chinese_switch_container, 3, 0, 1, 1)
+
+ # Dock position
+ position_label = Label(label="Dock Position", h_align="start", v_align="center")
+ layout_grid.attach(position_label, 0, 1, 1, 1)
+ position_combo_container = Gtk.Box(
+ orientation=Gtk.Orientation.HORIZONTAL,
+ halign=Gtk.Align.START,
+ valign=Gtk.Align.CENTER,
+ )
+ self.position_combo = Gtk.ComboBoxText()
+ self.position_combo.set_tooltip_text("Select the position of the dock")
+ positions = ["Bottom", "Top", "Left", "Right"]
+ for pos in positions:
+ self.position_combo.append_text(pos)
+ current_position = bind_vars.get("dock_position", "Bottom")
+ try:
+ self.position_combo.set_active(positions.index(current_position))
+ except ValueError:
+ self.position_combo.set_active(0)
+ self.position_combo.connect("changed", self.on_position_changed)
+ position_combo_container.add(self.position_combo)
+ layout_grid.attach(position_combo_container, 1, 1, 1, 1)
+
+ # Dock theme
+ dock_theme_label = Label(label="Dock Theme", h_align="start", v_align="center")
+ layout_grid.attach(dock_theme_label, 2, 1, 1, 1)
+ dock_theme_combo_container = Gtk.Box(
+ orientation=Gtk.Orientation.HORIZONTAL,
+ halign=Gtk.Align.START,
+ valign=Gtk.Align.CENTER,
+ )
+ self.dock_theme_combo = Gtk.ComboBoxText()
+ self.dock_theme_combo.set_tooltip_text("Select the visual theme for the dock")
+ for theme in self.themes:
+ self.dock_theme_combo.append_text(theme)
+ current_dock_theme = bind_vars.get("dock_theme", "Pills")
+ try:
+ self.dock_theme_combo.set_active(self.themes.index(current_dock_theme))
+ except ValueError:
+ self.dock_theme_combo.set_active(0)
+ dock_theme_combo_container.add(self.dock_theme_combo)
+ layout_grid.attach(dock_theme_combo_container, 3, 1, 1, 1)
+
+ # Show Dock switch
+ dock_label = Label(label="Show Dock", h_align="start", v_align="center")
+ layout_grid.attach(dock_label, 0, 2, 1, 1)
+ dock_switch_container = Gtk.Box(
+ orientation=Gtk.Orientation.HORIZONTAL,
+ halign=Gtk.Align.START,
+ valign=Gtk.Align.CENTER,
+ )
+ self.dock_switch = Gtk.Switch(active=bind_vars.get("dock_enabled", True))
+ self.dock_switch.connect("notify::active", self.on_dock_enabled_changed)
+ dock_switch_container.add(self.dock_switch)
+ layout_grid.attach(dock_switch_container, 1, 2, 1, 1)
+
+ # Show Dock Only on Hover
+ dock_hover_label = Label(
+ label="Show Dock Only on Hover", h_align="start", v_align="center"
+ )
+ layout_grid.attach(dock_hover_label, 2, 2, 1, 1)
+ dock_hover_switch_container = Gtk.Box(
+ orientation=Gtk.Orientation.HORIZONTAL,
+ halign=Gtk.Align.START,
+ valign=Gtk.Align.CENTER,
+ )
+ self.dock_hover_switch = Gtk.Switch(
+ active=bind_vars.get("dock_always_occluded", False),
+ sensitive=self.dock_switch.get_active(),
+ )
+ dock_hover_switch_container.add(self.dock_hover_switch)
+ layout_grid.attach(dock_hover_switch_container, 3, 2, 1, 1)
+
+ # Auto-hide Dock
+ dock_auto_hide_label = Label(
+ label="Auto-hide Dock", h_align="start", v_align="center"
+ )
+ layout_grid.attach(dock_auto_hide_label, 0, 3, 1, 1)
+ dock_auto_hide_switch_container = Gtk.Box(
+ orientation=Gtk.Orientation.HORIZONTAL,
+ halign=Gtk.Align.START,
+ valign=Gtk.Align.CENTER,
+ )
+ self.dock_auto_hide_switch = Gtk.Switch(
+ active=bind_vars.get("dock_auto_hide", False),
+ sensitive=self.dock_switch.get_active(),
+ )
+ dock_auto_hide_switch_container.add(self.dock_auto_hide_switch)
+ layout_grid.attach(dock_auto_hide_switch_container, 1, 3, 1, 1)
+
+ # Hide Special Workspace
+ dock_hide_special_label = Label(
+ label="Hide Special Workspace", h_align="start", v_align="center"
+ )
+ layout_grid.attach(dock_hide_special_label, 2, 3, 1, 1)
+ dock_hide_special_switch_container = Gtk.Box(
+ orientation=Gtk.Orientation.HORIZONTAL,
+ halign=Gtk.Align.START,
+ valign=Gtk.Align.CENTER,
+ )
+ self.dock_hide_special_switch = Gtk.Switch(
+ active=bind_vars.get("dock_hide_special_workspace", True),
+ sensitive=self.dock_switch.get_active(),
+ )
+ self.dock_hide_special_switch.set_tooltip_text(
+ "Hide the dock when on special workspaces (scratchpad)"
+ )
+ dock_hide_special_switch_container.add(self.dock_hide_special_switch)
+ layout_grid.attach(dock_hide_special_switch_container, 3, 3, 1, 1)
+
+ # Icon size scale
+ icon_size_label = Label(
+ label="Taskbar Icon Size", h_align="start", v_align="center"
+ )
+ layout_grid.attach(icon_size_label, 0, 4, 1, 1)
+
+ icon_size_scale_container = Gtk.Box(
+ orientation=Gtk.Orientation.HORIZONTAL,
+ halign=Gtk.Align.START,
+ valign=Gtk.Align.CENTER,
+ hexpand=True,
+ )
+
+ self.icon_size_scale = Gtk.Scale.new_with_range(
+ Gtk.Orientation.HORIZONTAL, 20, 30, 2
+ )
+ self.icon_size_scale.set_value(bind_vars.get("dock_icon_size", 20))
+ self.icon_size_scale.set_size_request(150, -1)
+ self.icon_size_scale.set_draw_value(True)
+ self.icon_size_scale.set_value_pos(Gtk.PositionType.RIGHT)
+ self.icon_size_scale.set_tooltip_text("Adjust the size of dock icons")
+
+ icon_size_scale_container.add(self.icon_size_scale)
+ layout_grid.attach(icon_size_scale_container, 1, 4, 1, 1)
+
+ # Application switcher items scale
+ application_switcher_label = Label(
+ label="Application Switcher Items", h_align="start", v_align="center"
+ )
+ layout_grid.attach(application_switcher_label, 2, 4, 1, 1)
+
+ application_switcher_scale_container = Gtk.Box(
+ orientation=Gtk.Orientation.HORIZONTAL,
+ halign=Gtk.Align.START,
+ valign=Gtk.Align.CENTER,
+ hexpand=True,
+ )
+
+ self.application_switcher_scale = Gtk.Scale.new_with_range(
+ Gtk.Orientation.HORIZONTAL, 5, 20, 1
+ )
+ self.application_switcher_scale.set_value(
+ bind_vars.get("window_switcher_items_per_row", 13)
+ )
+ self.application_switcher_scale.set_size_request(150, -1)
+ self.application_switcher_scale.set_draw_value(True)
+ self.application_switcher_scale.set_value_pos(Gtk.PositionType.RIGHT)
+ self.application_switcher_scale.set_tooltip_text(
+ "Adjust the number of items per row in the application switcher"
+ )
+
+ application_switcher_scale_container.add(self.application_switcher_scale)
+ layout_grid.attach(application_switcher_scale_container, 3, 4, 1, 1)
+
+ # Hide Special Workspace Apps
+ dock_hide_special_apps_label = Label(
+ label="Hide Special Workspace Apps", h_align="start", v_align="center"
+ )
+ layout_grid.attach(dock_hide_special_apps_label, 2, 5, 1, 1)
+ dock_hide_special_apps_switch_container = Gtk.Box(
+ orientation=Gtk.Orientation.HORIZONTAL,
+ halign=Gtk.Align.START,
+ valign=Gtk.Align.CENTER,
+ )
+ self.dock_hide_special_apps_switch = Gtk.Switch(
+ active=bind_vars.get("dock_hide_special_workspace_apps", True),
+ sensitive=self.dock_switch.get_active(),
+ )
+ self.dock_hide_special_apps_switch.set_tooltip_text(
+ "Hide applications that are in special workspaces (scratchpad) from the dock"
+ )
+ dock_hide_special_apps_switch_container.add(self.dock_hide_special_apps_switch)
+ layout_grid.attach(dock_hide_special_apps_switch_container, 3, 5, 1, 1)
+
+ # Notification position
+ notification_pos_label = Label(
+ label="Notification Position", h_align="start", v_align="center"
+ )
+ layout_grid.attach(notification_pos_label, 0, 8, 1, 1)
+
+ notification_pos_combo_container = Box(
+ orientation=Gtk.Orientation.HORIZONTAL,
+ halign=Gtk.Align.START,
+ valign=Gtk.Align.CENTER,
+ )
+
+ self.notification_pos_combo = Gtk.ComboBoxText()
+ self.notification_pos_combo.set_tooltip_text(
+ "Select where notifications appear on the screen."
+ )
+
+ notification_positions_list = ["Top", "Bottom"]
+ for pos in notification_positions_list:
+ self.notification_pos_combo.append_text(pos)
+
+ current_notif_pos = bind_vars.get(NOTIF_POS_KEY, NOTIF_POS_DEFAULT)
+ try:
+ self.notification_pos_combo.set_active(
+ notification_positions_list.index(current_notif_pos)
+ )
+ except ValueError:
+ self.notification_pos_combo.set_active(0)
+
+ self.notification_pos_combo.connect(
+ "changed", self.on_notification_position_changed
+ )
+
+ notification_pos_combo_container.add(self.notification_pos_combo)
+ layout_grid.attach(notification_pos_combo_container, 1, 8, 3, 1)
+
+ def _create_modules_section(self, vbox):
+ """Create modules section"""
+ components_header = Label(markup="Modules", h_align="start")
+ vbox.add(components_header)
+ components_grid = Gtk.Grid()
+ components_grid.set_column_spacing(15)
+ components_grid.set_row_spacing(8)
+ components_grid.set_margin_start(10)
+ components_grid.set_margin_top(5)
+ vbox.add(components_grid)
+
+ self.component_switches = {}
+ component_display_names = {
+ "workspace": "Workspaces",
+ "metrics": "System Metrics",
+ "date_time": "Date & Time",
+ "battery": "Battery Indicator",
+ "controls": "Control Panel",
+ "indicators": "Indicators",
+ "music_player": "Music Player",
+ "applications": "Taskbar",
+ "language": "Language Indicator",
+ }
+
+ self.corners_switch = Gtk.Switch(active=bind_vars.get("corners_visible", True))
+ num_components = len(component_display_names) + 1
+ rows_per_column = (num_components + 1) // 2
+
+ corners_label = Label(
+ label="Rounded Corners", h_align="start", v_align="center"
+ )
+ components_grid.attach(corners_label, 0, 0, 1, 1)
+ switch_container_corners = Gtk.Box(
+ orientation=Gtk.Orientation.HORIZONTAL,
+ halign=Gtk.Align.START,
+ valign=Gtk.Align.CENTER,
+ )
+ switch_container_corners.add(self.corners_switch)
+ components_grid.attach(switch_container_corners, 1, 0, 1, 1)
+
+ item_idx = 0
+ for name, display in component_display_names.items():
+ if item_idx < (rows_per_column - 1):
+ row = item_idx + 1
+ col = 0
+ else:
+ row = item_idx - (rows_per_column - 1)
+ col = 2
+
+ component_label = Label(label=display, h_align="start", v_align="center")
+ components_grid.attach(component_label, col, row, 1, 1)
+
+ switch_container = Gtk.Box(
+ orientation=Gtk.Orientation.HORIZONTAL,
+ halign=Gtk.Align.START,
+ valign=Gtk.Align.CENTER,
+ )
+ component_switch = Gtk.Switch(
+ active=bind_vars.get(f"dock_{name}_visible", True)
+ )
+ switch_container.add(component_switch)
+ components_grid.attach(switch_container, col + 1, row, 1, 1)
+ self.component_switches[name] = component_switch
+ item_idx += 1
+
+ # Event handlers
+ def on_ws_mode_changed(self, combo):
+ """Handle workspace mode change"""
+ mode = combo.get_active_text()
+ is_numbers_mode = mode == "Numbers"
+ self.ws_chinese_switch.set_sensitive(is_numbers_mode)
+ if not is_numbers_mode:
+ self.ws_chinese_switch.set_active(False)
+
+ def on_position_changed(self, _combo):
+ """Handle dock position change"""
+ # Note: centered_switch is not used in appearance tab,
+ # this method is kept for compatibility
+ pass
+
+ def on_dock_enabled_changed(self, switch, _gparam):
+ """Handle dock enabled/disabled change"""
+ is_active = switch.get_active()
+ self.dock_hover_switch.set_sensitive(is_active)
+ self.dock_auto_hide_switch.set_sensitive(is_active)
+ self.dock_hide_special_switch.set_sensitive(is_active)
+ self.dock_hide_special_apps_switch.set_sensitive(is_active)
+ if not is_active:
+ self.dock_hover_switch.set_active(False)
+ self.dock_auto_hide_switch.set_active(False)
+ self.dock_hide_special_switch.set_active(False)
+ self.dock_hide_special_apps_switch.set_active(False)
+
+ def on_notification_position_changed(self, combo):
+ """Handle notification position change"""
+ selected_text = combo.get_active_text()
+ if selected_text:
+ bind_vars[NOTIF_POS_KEY] = selected_text
+ print(
+ f"Notification position updated in bind_vars: {
+ bind_vars[NOTIF_POS_KEY]
+ }"
+ )
+
+ def on_select_face_icon(self, _widget):
+ """Handle face icon selection"""
+ dialog = Gtk.FileChooserDialog(
+ title="Select Face Icon",
+ transient_for=self.parent_window,
+ action=Gtk.FileChooserAction.OPEN,
+ )
+ dialog.add_buttons(
+ Gtk.STOCK_CANCEL,
+ Gtk.ResponseType.CANCEL,
+ Gtk.STOCK_OPEN,
+ Gtk.ResponseType.OK,
+ )
+ image_filter = Gtk.FileFilter()
+ image_filter.set_name("Image files")
+ for mime in ["image/png", "image/jpeg"]:
+ image_filter.add_mime_type(mime)
+ for pattern in ["*.png", "*.jpg", "*.jpeg"]:
+ image_filter.add_pattern(pattern)
+ dialog.add_filter(image_filter)
+ if dialog.run() == Gtk.ResponseType.OK:
+ self.selected_face_icon = dialog.get_filename()
+ self.face_status_label.label = (
+ f"Selected: {os.path.basename(self.selected_face_icon)}"
+ )
+ try:
+ pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
+ self.selected_face_icon, 64, 64
+ )
+ self.face_image.set_from_pixbuf(pixbuf)
+ except Exception as e:
+ print(f"Error loading selected face icon preview: {e}")
+ self.face_image.set_from_icon_name("image-missing", Gtk.IconSize.DIALOG)
+ dialog.destroy()
+
+ def get_appearance_values(self):
+ """Get current appearance values from widgets"""
+ values = {}
+
+ values["wallpapers_dir"] = self.wall_dir_chooser.get_filename()
+ values["dock_position"] = self.position_combo.get_active_text()
+ values["vertical"] = values["dock_position"] in ["Left", "Right"]
+ values["dock_enabled"] = self.dock_switch.get_active()
+ values["dock_auto_hide"] = self.dock_auto_hide_switch.get_active()
+ values["dock_always_occluded"] = self.dock_hover_switch.get_active()
+ values["dock_hide_special_workspace"] = self.dock_hide_special_switch.get_active()
+ values["dock_hide_special_workspace_apps"] = self.dock_hide_special_apps_switch.get_active()
+ values["dock_icon_size"] = int(self.icon_size_scale.get_value())
+ values["window_switcher_items_per_row"] = int(self.application_switcher_scale.get_value())
+ values["corners_visible"] = self.corners_switch.get_active()
+ values["dock_theme"] = self.dock_theme_combo.get_active_text()
+
+ selected_notif_pos_text = self.notification_pos_combo.get_active_text()
+ if selected_notif_pos_text:
+ values[NOTIF_POS_KEY] = selected_notif_pos_text
+ else:
+ values[NOTIF_POS_KEY] = NOTIF_POS_DEFAULT
+
+ for component_name, switch in self.component_switches.items():
+ values[f"dock_{component_name}_visible"] = switch.get_active()
+
+ # Save workspace settings
+ ws_mode = self.ws_mode_combo.get_active_text()
+ values["workspace_dots"] = ws_mode == "Dots"
+ values["workspace_nums"] = ws_mode == "Numbers"
+ values["workspace_use_chinese_numerals"] = self.ws_chinese_switch.get_active()
+
+ return values
+
+ def get_selected_face_icon(self):
+ """Get the selected face icon path"""
+ return self.selected_face_icon
+
+ def clear_selected_face_icon(self):
+ """Clear the selected face icon"""
+ self.selected_face_icon = None
+ self.face_status_label.label = ""
+
+ def reset_to_defaults(self, defaults):
+ """Reset appearance settings to default values"""
+ self.wall_dir_chooser.set_filename(defaults.get("wallpapers_dir", ""))
+
+ positions = ["Bottom", "Top", "Left", "Right"]
+ default_position = defaults.get("dock_position", "Bottom")
+ try:
+ self.position_combo.set_active(positions.index(default_position))
+ except ValueError:
+ self.position_combo.set_active(0)
+
+ self.dock_switch.set_active(defaults.get("dock_enabled", True))
+ self.dock_auto_hide_switch.set_active(defaults.get("dock_auto_hide", False))
+ self.dock_hover_switch.set_active(defaults.get("dock_always_occluded", False))
+ self.dock_hide_special_switch.set_active(defaults.get("dock_hide_special_workspace", True))
+ self.dock_hide_special_apps_switch.set_active(defaults.get("dock_hide_special_workspace_apps", True))
+
+ # Update sensitivity based on dock enabled state
+ dock_enabled = self.dock_switch.get_active()
+ self.dock_hover_switch.set_sensitive(dock_enabled)
+ self.dock_auto_hide_switch.set_sensitive(dock_enabled)
+ self.dock_hide_special_switch.set_sensitive(dock_enabled)
+ self.dock_hide_special_apps_switch.set_sensitive(dock_enabled)
+
+ default_dock_theme_val = defaults.get("dock_theme", "Pills")
+ try:
+ self.dock_theme_combo.set_active(self.themes.index(default_dock_theme_val))
+ except ValueError:
+ self.dock_theme_combo.set_active(0)
+
+ default_notif_pos_val = defaults.get(NOTIF_POS_KEY, NOTIF_POS_DEFAULT)
+ notification_positions_list = ["Top", "Bottom"]
+ try:
+ self.notification_pos_combo.set_active(
+ notification_positions_list.index(default_notif_pos_val)
+ )
+ except ValueError:
+ self.notification_pos_combo.set_active(0)
+
+ for name, switch in self.component_switches.items():
+ switch.set_active(defaults.get(f"dock_{name}_visible", True))
+
+ # Reset workspace settings
+ default_dots = defaults.get("workspace_dots", True)
+ default_nums = defaults.get("workspace_nums", False)
+ if default_dots:
+ default_mode = "Dots"
+ elif default_nums:
+ default_mode = "Numbers"
+ else:
+ default_mode = "Single Button"
+
+ ws_modes = ["Dots", "Numbers", "Single Button"]
+ try:
+ self.ws_mode_combo.set_active(ws_modes.index(default_mode))
+ except ValueError:
+ self.ws_mode_combo.set_active(0)
+
+ self.ws_chinese_switch.set_active(defaults.get("workspace_use_chinese_numerals", False))
+ self.ws_chinese_switch.set_sensitive(default_nums)
+ self.corners_switch.set_active(defaults.get("corners_visible", True))
+
+ # Reset face icon
+ self.selected_face_icon = None
+ self.face_status_label.label = ""
+ current_face = os.path.expanduser("~/.face.icon")
+ try:
+ if os.path.exists(current_face):
+ pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(current_face, 64, 64)
+ self.face_image.set_from_pixbuf(pixbuf)
+ else:
+ self.face_image.set_from_icon_name("user-info", Gtk.IconSize.DIALOG)
+ except Exception as e:
+ print(f"Error loading face icon: {e}")
+ self.face_image.set_from_icon_name("image-missing", Gtk.IconSize.DIALOG)
+
+ # Reset scales
+ self.icon_size_scale.set_value(defaults.get("dock_icon_size", 20))
+ self.application_switcher_scale.set_value(defaults.get("window_switcher_items_per_row", 13))
diff --git a/config/settings/gui.py b/config/settings/gui.py
index f3ebb19..9cc4b29 100644
--- a/config/settings/gui.py
+++ b/config/settings/gui.py
@@ -16,10 +16,14 @@ from fabric.widgets.window import Window
from gi.repository import Gdk, GdkPixbuf, GLib, Gtk
from PIL import Image
-from config.data import APP_NAME, APP_NAME_CAP, NOTIF_POS_DEFAULT, NOTIF_POS_KEY
-from config.settings.utils import backup_and_replace, bind_vars, start_config
+from config.data import APP_NAME, APP_NAME_CAP
+from config.settings.utils import backup_and_replace, start_config
from config.settings.wifi_tab import WiFiTab
from config.settings.bluetooth_tab import BluetoothTab
+from config.settings.key_bindings_tab import KeyBindingsTab
+from config.settings.appearance_tab import AppearanceTab
+from config.settings.system_tab import SystemTab
+from config.settings.about_tab import AboutTab
gi.require_version("Gtk", "3.0")
@@ -51,9 +55,13 @@ class HyprConfGUI(Window):
self.show_lock_checkbox = show_lock_checkbox
self.show_idle_checkbox = show_idle_checkbox
- # Initialize WiFi and Bluetooth tabs
+ # Initialize all tabs
self.wifi_tab = WiFiTab()
self.bluetooth_tab = BluetoothTab()
+ self.key_bindings_tab = KeyBindingsTab()
+ self.appearance_tab = AppearanceTab(self.themes, self)
+ self.system_tab = SystemTab(show_lock_checkbox, show_idle_checkbox, self.enforce_window_size)
+ self.about_tab = AboutTab()
# Pass window size enforcement method to tabs
self.wifi_tab.set_window_size_enforcer(self.enforce_window_size)
@@ -80,10 +88,10 @@ class HyprConfGUI(Window):
self.wifi_tab_content = self.wifi_tab.create_wifi_tab()
self.bluetooth_tab_content = self.bluetooth_tab.create_bluetooth_tab()
- self.key_bindings_tab_content = self.create_key_bindings_tab()
- self.appearance_tab_content = self.create_appearance_tab()
- self.system_tab_content = self.create_system_tab()
- self.about_tab_content = self.create_about_tab()
+ self.key_bindings_tab_content = self.key_bindings_tab.create_key_bindings_tab()
+ self.appearance_tab_content = self.appearance_tab.create_appearance_tab()
+ self.system_tab_content = self.system_tab.create_system_tab()
+ self.about_tab_content = self.about_tab.create_about_tab()
self.tab_stack.add_titled(
self.wifi_tab_content, "Wifi", "Wifi"
@@ -137,967 +145,38 @@ class HyprConfGUI(Window):
"""Public method to enforce window size - can be called by tabs"""
self._force_window_size(self)
- def create_key_bindings_tab(self):
- scrolled_window = ScrolledWindow(
- h_scrollbar_policy="never",
- v_scrollbar_policy="automatic",
- h_expand=False,
- v_expand=False,
- propagate_width=False,
- propagate_height=False,
- )
- # Set fixed size to match tab stack dimensions
- scrolled_window.set_size_request(580, 580)
- main_vbox = Box(orientation="v", spacing=10, style="margin: 15px;")
- scrolled_window.add(main_vbox)
- keybind_grid = Gtk.Grid()
- keybind_grid.set_column_spacing(10)
- keybind_grid.set_row_spacing(8)
- keybind_grid.set_margin_start(5)
- keybind_grid.set_margin_end(5)
- keybind_grid.set_margin_top(5)
- keybind_grid.set_margin_bottom(5)
+ # Removed create_appearance_tab - now using AppearanceTab class
- action_label = Label(
- markup="Action", h_align="start", style="margin-bottom: 5px;"
- )
- modifier_label = Label(
- markup="Modifier", h_align="start", style="margin-bottom: 5px;"
- )
- separator_label = Label(
- label="+", h_align="center", style="margin-bottom: 5px;"
- )
- key_label = Label(
- markup="Key", h_align="start", style="margin-bottom: 5px;"
- )
+ # on_notification_position_changed moved to AppearanceTab class
- keybind_grid.attach(action_label, 0, 0, 1, 1)
- keybind_grid.attach(modifier_label, 1, 0, 1, 1)
- keybind_grid.attach(separator_label, 2, 0, 1, 1)
- keybind_grid.attach(key_label, 3, 0, 1, 1)
+ # Removed create_system_tab - now using SystemTab class
+ # Removed create_about_tab - now using AboutTab class
- self.entries = []
- bindings = [
- (f"Reload {APP_NAME_CAP}", "prefix_restart", "suffix_restart"),
- ("Message", "prefix_msg", "suffix_msg"),
- (
- "Application Switcher",
- "prefix_application_switcher",
- "suffix_application_switcher",
- ),
- ("Launcher", "prefix_launcher", "suffix_launcher"),
- ("App Launcher", "prefix_app_launcher", "suffix_app_launcher"),
- ("Clipboard History", "prefix_cliphist", "suffix_cliphist"),
- ("Wallpapers", "prefix_wallpapers", "suffix_wallpapers"),
- ("Random Wallpaper", "prefix_randwall", "suffix_randwall"),
- ("Emoji Picker", "prefix_emoji", "suffix_emoji"),
- ("Kanban", "prefix_kanban", "suffix_kanban"),
- ("Power Menu", "prefix_power", "suffix_power"),
- ("Toggle Caffeine", "prefix_caffeine", "suffix_caffeine"),
- ("Settings", "prefix_settings", "suffix_settings"),
- (
- "Restart with inspector",
- "prefix_restart_inspector",
- "suffix_restart_inspector",
- ),
- ]
- for i, (label_text, prefix_key, suffix_key) in enumerate(bindings):
- row = i + 1
- binding_label = Label(label=label_text, h_align="start")
- keybind_grid.attach(binding_label, 0, row, 1, 1)
- prefix_entry = Entry(text=bind_vars.get(prefix_key, ""))
- keybind_grid.attach(prefix_entry, 1, row, 1, 1)
- plus_label = Label(label="+", h_align="center")
- keybind_grid.attach(plus_label, 2, row, 1, 1)
- suffix_entry = Entry(text=bind_vars.get(suffix_key, ""))
- keybind_grid.attach(suffix_entry, 3, row, 1, 1)
- self.entries.append((prefix_key, suffix_key, prefix_entry, suffix_entry))
+ # Event handlers moved to respective tab classes
- main_vbox.add(keybind_grid)
- return scrolled_window
-
- def create_appearance_tab(self):
- scrolled_window = ScrolledWindow(
- h_scrollbar_policy="never",
- v_scrollbar_policy="automatic",
- h_expand=False,
- v_expand=False,
- propagate_width=False,
- propagate_height=False,
- )
- # Set fixed size to match tab stack dimensions
- scrolled_window.set_size_request(580, 580)
-
- vbox = Box(orientation="v", spacing=15, style="margin: 15px;")
- scrolled_window.add(vbox)
-
- top_grid = Gtk.Grid()
- top_grid.set_column_spacing(20)
- top_grid.set_row_spacing(5)
- top_grid.set_margin_bottom(10)
- vbox.add(top_grid)
-
- wall_header = Label(markup="Wallpapers", h_align="start")
- top_grid.attach(wall_header, 0, 0, 1, 1)
- wall_label = Label(label="Directory:", h_align="start", v_align="center")
- top_grid.attach(wall_label, 0, 1, 1, 1)
-
- chooser_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
- chooser_container.set_halign(Gtk.Align.START)
- chooser_container.set_valign(Gtk.Align.CENTER)
- self.wall_dir_chooser = Gtk.FileChooserButton(
- title="Select a folder", action=Gtk.FileChooserAction.SELECT_FOLDER
- )
- self.wall_dir_chooser.set_tooltip_text(
- "Select the directory containing your wallpaper images"
- )
- self.wall_dir_chooser.set_filename(bind_vars.get("wallpapers_dir", ""))
- self.wall_dir_chooser.set_size_request(180, -1)
- chooser_container.add(self.wall_dir_chooser)
- top_grid.attach(chooser_container, 1, 1, 1, 1)
-
- face_header = Label(markup="Profile Icon", h_align="start")
- top_grid.attach(face_header, 2, 0, 2, 1)
- current_face = os.path.expanduser("~/.face.icon")
- face_image_container = Box(
- style_classes=["image-frame"], h_align="center", v_align="center"
- )
- self.face_image = FabricImage(size=64)
- try:
- if os.path.exists(current_face):
- pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(current_face, 64, 64)
- self.face_image.set_from_pixbuf(pixbuf)
- else:
- self.face_image.set_from_icon_name("user-info", Gtk.IconSize.DIALOG)
- except Exception as e:
- print(f"Error loading face icon: {e}")
- self.face_image.set_from_icon_name("image-missing", Gtk.IconSize.DIALOG)
- face_image_container.add(self.face_image)
- top_grid.attach(face_image_container, 2, 1, 1, 1)
-
- browse_btn_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
- browse_btn_container.set_halign(Gtk.Align.START)
- browse_btn_container.set_valign(Gtk.Align.CENTER)
- face_btn = Button(
- label="Browse...",
- tooltip_text="Select a square image for your profile icon",
- on_clicked=self.on_select_face_icon,
- )
- browse_btn_container.add(face_btn)
- top_grid.attach(browse_btn_container, 3, 1, 1, 1)
- self.face_status_label = Label(label="", h_align="start")
- top_grid.attach(self.face_status_label, 2, 2, 2, 1)
-
- separator1 = Box(
- style="min-height: 1px; background-color: alpha(@fg_color, 0.2); margin: 5px 0px;",
- h_expand=True,
- )
- vbox.add(separator1)
-
- layout_header = Label(markup="Layout Options", h_align="start")
- vbox.add(layout_header)
- layout_grid = Gtk.Grid()
- layout_grid.set_column_spacing(20)
- layout_grid.set_row_spacing(10)
- layout_grid.set_margin_start(10)
- layout_grid.set_margin_top(5)
- vbox.add(layout_grid)
-
- # Workspace mode
- ws_mode_label = Label(label="Workspace Mode", h_align="start", v_align="center")
- layout_grid.attach(ws_mode_label, 0, 0, 1, 1)
-
- ws_mode_combo_container = Gtk.Box(
- orientation=Gtk.Orientation.HORIZONTAL,
- halign=Gtk.Align.START,
- valign=Gtk.Align.CENTER,
- )
- self.ws_mode_combo = Gtk.ComboBoxText()
- self.ws_mode_combo.set_tooltip_text("Select how workspaces are displayed")
- ws_modes = ["Dots", "Numbers", "Single Button"]
- for mode in ws_modes:
- self.ws_mode_combo.append_text(mode)
-
- # Set current mode based on config
- current_dots = bind_vars.get("workspace_dots", True)
- current_nums = bind_vars.get("workspace_nums", False)
- if current_dots:
- current_mode = "Dots"
- elif current_nums:
- current_mode = "Numbers"
- else:
- current_mode = "Single Button"
-
- try:
- self.ws_mode_combo.set_active(ws_modes.index(current_mode))
- except ValueError:
- self.ws_mode_combo.set_active(0)
-
- self.ws_mode_combo.connect("changed", self.on_ws_mode_changed)
- ws_mode_combo_container.add(self.ws_mode_combo)
- layout_grid.attach(ws_mode_combo_container, 1, 0, 1, 1)
-
- # Chinese numerals option
- ws_chinese_label = Label(
- label="Use Chinese Numerals", h_align="start", v_align="center"
- )
- layout_grid.attach(ws_chinese_label, 2, 0, 1, 1)
- ws_chinese_switch_container = Gtk.Box(
- orientation=Gtk.Orientation.HORIZONTAL,
- halign=Gtk.Align.START,
- valign=Gtk.Align.CENTER,
- )
- self.ws_chinese_switch = Gtk.Switch(
- active=bind_vars.get("workspace_use_chinese_numerals", False),
- sensitive=current_nums, # Only enabled when numbers mode is selected
- )
- self.ws_chinese_switch.set_tooltip_text(
- "Use Chinese numerals (一, 二, 三...) instead of Arabic numbers"
- )
- ws_chinese_switch_container.add(self.ws_chinese_switch)
- layout_grid.attach(ws_chinese_switch_container, 3, 0, 1, 1)
-
- position_label = Label(label="Dock Position", h_align="start", v_align="center")
- layout_grid.attach(position_label, 0, 1, 1, 1)
- position_combo_container = Gtk.Box(
- orientation=Gtk.Orientation.HORIZONTAL,
- halign=Gtk.Align.START,
- valign=Gtk.Align.CENTER,
- )
- self.position_combo = Gtk.ComboBoxText()
- self.position_combo.set_tooltip_text("Select the position of the dock")
- positions = ["Bottom", "Top", "Left", "Right"]
- for pos in positions:
- self.position_combo.append_text(pos)
- current_position = bind_vars.get("dock_position", "Bottom")
- try:
- self.position_combo.set_active(positions.index(current_position))
- except ValueError:
- self.position_combo.set_active(0)
- self.position_combo.connect("changed", self.on_position_changed)
- position_combo_container.add(self.position_combo)
- layout_grid.attach(position_combo_container, 1, 1, 1, 1)
-
- dock_theme_label = Label(label="Dock Theme", h_align="start", v_align="center")
- layout_grid.attach(dock_theme_label, 2, 1, 1, 1)
- dock_theme_combo_container = Gtk.Box(
- orientation=Gtk.Orientation.HORIZONTAL,
- halign=Gtk.Align.START,
- valign=Gtk.Align.CENTER,
- )
- self.dock_theme_combo = Gtk.ComboBoxText()
- self.dock_theme_combo.set_tooltip_text("Select the visual theme for the dock")
- for theme in self.themes:
- self.dock_theme_combo.append_text(theme)
- current_dock_theme = bind_vars.get("dock_theme", "Pills")
- try:
- self.dock_theme_combo.set_active(self.themes.index(current_dock_theme))
- except ValueError:
- self.dock_theme_combo.set_active(0)
- dock_theme_combo_container.add(self.dock_theme_combo)
- layout_grid.attach(dock_theme_combo_container, 3, 1, 1, 1)
-
- # Add icon size scale
- icon_size_label = Label(
- label="Taskbar Icon Size", h_align="start", v_align="center"
- )
- layout_grid.attach(icon_size_label, 0, 4, 1, 1)
-
- icon_size_scale_container = Gtk.Box(
- orientation=Gtk.Orientation.HORIZONTAL,
- halign=Gtk.Align.START,
- valign=Gtk.Align.CENTER,
- hexpand=True,
- )
-
- self.icon_size_scale = Gtk.Scale.new_with_range(
- Gtk.Orientation.HORIZONTAL, 20, 30, 2
- )
- self.icon_size_scale.set_value(bind_vars.get("dock_icon_size", 20))
- self.icon_size_scale.set_size_request(150, -1)
- self.icon_size_scale.set_draw_value(True)
- self.icon_size_scale.set_value_pos(Gtk.PositionType.RIGHT)
- self.icon_size_scale.set_tooltip_text("Adjust the size of dock icons")
-
- icon_size_scale_container.add(self.icon_size_scale)
- layout_grid.attach(icon_size_scale_container, 1, 4, 1, 1)
-
- # Add application switcher items scale
- application_switcher_label = Label(
- label="Application Switcher Items", h_align="start", v_align="center"
- )
- layout_grid.attach(application_switcher_label, 2, 4, 1, 1)
-
- application_switcher_scale_container = Gtk.Box(
- orientation=Gtk.Orientation.HORIZONTAL,
- halign=Gtk.Align.START,
- valign=Gtk.Align.CENTER,
- hexpand=True,
- )
-
- self.application_switcher_scale = Gtk.Scale.new_with_range(
- Gtk.Orientation.HORIZONTAL, 5, 20, 1
- )
- self.application_switcher_scale.set_value(
- bind_vars.get("window_switcher_items_per_row", 13)
- )
- self.application_switcher_scale.set_size_request(150, -1)
- self.application_switcher_scale.set_draw_value(True)
- self.application_switcher_scale.set_value_pos(Gtk.PositionType.RIGHT)
- self.application_switcher_scale.set_tooltip_text(
- "Adjust the number of items per row in the application switcher"
- )
-
- application_switcher_scale_container.add(self.application_switcher_scale)
- layout_grid.attach(application_switcher_scale_container, 3, 4, 1, 1)
-
- dock_label = Label(label="Show Dock", h_align="start", v_align="center")
- layout_grid.attach(dock_label, 0, 2, 1, 1)
- dock_switch_container = Gtk.Box(
- orientation=Gtk.Orientation.HORIZONTAL,
- halign=Gtk.Align.START,
- valign=Gtk.Align.CENTER,
- )
- self.dock_switch = Gtk.Switch(active=bind_vars.get("dock_enabled", True))
- self.dock_switch.connect("notify::active", self.on_dock_enabled_changed)
- dock_switch_container.add(self.dock_switch)
- layout_grid.attach(dock_switch_container, 1, 2, 1, 1)
-
- dock_auto_hide_label = Label(
- label="Auto-hide Dock", h_align="start", v_align="center"
- )
- layout_grid.attach(dock_auto_hide_label, 0, 3, 1, 1)
- dock_auto_hide_switch_container = Gtk.Box(
- orientation=Gtk.Orientation.HORIZONTAL,
- halign=Gtk.Align.START,
- valign=Gtk.Align.CENTER,
- )
- self.dock_auto_hide_switch = Gtk.Switch(
- active=bind_vars.get("dock_auto_hide", False),
- sensitive=self.dock_switch.get_active(),
- )
- dock_auto_hide_switch_container.add(self.dock_auto_hide_switch)
- layout_grid.attach(dock_auto_hide_switch_container, 1, 3, 1, 1)
-
- dock_hover_label = Label(
- label="Show Dock Only on Hover", h_align="start", v_align="center"
- )
- layout_grid.attach(dock_hover_label, 2, 2, 1, 1)
- dock_hover_switch_container = Gtk.Box(
- orientation=Gtk.Orientation.HORIZONTAL,
- halign=Gtk.Align.START,
- valign=Gtk.Align.CENTER,
- )
- self.dock_hover_switch = Gtk.Switch(
- active=bind_vars.get("dock_always_occluded", False),
- sensitive=self.dock_switch.get_active(),
- )
- dock_hover_switch_container.add(self.dock_hover_switch)
- layout_grid.attach(dock_hover_switch_container, 3, 2, 1, 1)
-
- # Add dock hide special workspace option
- dock_hide_special_label = Label(
- label="Hide Special Workspace", h_align="start", v_align="center"
- )
- layout_grid.attach(dock_hide_special_label, 2, 3, 1, 1)
- dock_hide_special_switch_container = Gtk.Box(
- orientation=Gtk.Orientation.HORIZONTAL,
- halign=Gtk.Align.START,
- valign=Gtk.Align.CENTER,
- )
- self.dock_hide_special_switch = Gtk.Switch(
- active=bind_vars.get("dock_hide_special_workspace", True),
- sensitive=self.dock_switch.get_active(),
- )
- self.dock_hide_special_switch.set_tooltip_text(
- "Hide the dock when on special workspaces (scratchpad)"
- )
- dock_hide_special_switch_container.add(self.dock_hide_special_switch)
- layout_grid.attach(dock_hide_special_switch_container, 3, 3, 1, 1)
-
- # Add dock hide special workspace apps option
- dock_hide_special_apps_label = Label(
- label="Hide Special Workspace Apps", h_align="start", v_align="center"
- )
- layout_grid.attach(dock_hide_special_apps_label, 2, 5, 1, 1)
- dock_hide_special_apps_switch_container = Gtk.Box(
- orientation=Gtk.Orientation.HORIZONTAL,
- halign=Gtk.Align.START,
- valign=Gtk.Align.CENTER,
- )
- self.dock_hide_special_apps_switch = Gtk.Switch(
- active=bind_vars.get("dock_hide_special_workspace_apps", True),
- sensitive=self.dock_switch.get_active(),
- )
- self.dock_hide_special_apps_switch.set_tooltip_text(
- "Hide applications that are in special workspaces (scratchpad) from the dock"
- )
- dock_hide_special_apps_switch_container.add(self.dock_hide_special_apps_switch)
- layout_grid.attach(dock_hide_special_apps_switch_container, 3, 5, 1, 1)
-
- notification_pos_label = Label(
- label="Notification Position", h_align="start", v_align="center"
- )
- layout_grid.attach(notification_pos_label, 0, 8, 1, 1)
-
- notification_pos_combo_container = Box(
- orientation=Gtk.Orientation.HORIZONTAL,
- halign=Gtk.Align.START,
- valign=Gtk.Align.CENTER,
- )
-
- self.notification_pos_combo = Gtk.ComboBoxText()
- self.notification_pos_combo.set_tooltip_text(
- "Select where notifications appear on the screen."
- )
-
- notification_positions_list = ["Top", "Bottom"]
- for pos in notification_positions_list:
- self.notification_pos_combo.append_text(pos)
-
- current_notif_pos = bind_vars.get(NOTIF_POS_KEY, NOTIF_POS_DEFAULT)
- try:
- self.notification_pos_combo.set_active(
- notification_positions_list.index(current_notif_pos)
- )
- except ValueError:
- self.notification_pos_combo.set_active(0)
-
- self.notification_pos_combo.connect(
- "changed", self.on_notification_position_changed
- )
-
- notification_pos_combo_container.add(self.notification_pos_combo)
- layout_grid.attach(notification_pos_combo_container, 1, 8, 3, 1)
-
- separator2 = Box(
- style="min-height: 1px; background-color: alpha(@fg_color, 0.2); margin: 5px 0px;",
- h_expand=True,
- )
- vbox.add(separator2)
-
- components_header = Label(markup="Modules", h_align="start")
- vbox.add(components_header)
- components_grid = Gtk.Grid()
- components_grid.set_column_spacing(15)
- components_grid.set_row_spacing(8)
- components_grid.set_margin_start(10)
- components_grid.set_margin_top(5)
- vbox.add(components_grid)
-
- self.component_switches = {}
- component_display_names = {
- "workspace": "Workspaces",
- "metrics": "System Metrics",
- "date_time": "Date & Time",
- "battery": "Battery Indicator",
- "controls": "Control Panel",
- "indicators": "Indicators",
- "music_player": "Music Player",
- "applications": "Taskbar",
- "language": "Language Indicator",
- }
-
- self.corners_switch = Gtk.Switch(active=bind_vars.get("corners_visible", True))
- num_components = len(component_display_names) + 1
- rows_per_column = (num_components + 1) // 2
-
- corners_label = Label(
- label="Rounded Corners", h_align="start", v_align="center"
- )
- components_grid.attach(corners_label, 0, 0, 1, 1)
- switch_container_corners = Gtk.Box(
- orientation=Gtk.Orientation.HORIZONTAL,
- halign=Gtk.Align.START,
- valign=Gtk.Align.CENTER,
- )
- switch_container_corners.add(self.corners_switch)
- components_grid.attach(switch_container_corners, 1, 0, 1, 1)
-
- current_row = 0
- current_col = 0
- item_idx = 0
- for i, (name, display) in enumerate(component_display_names.items()):
- if item_idx < (rows_per_column - 1):
- row = item_idx + 1
- col = 0
- else:
- row = item_idx - (rows_per_column - 1)
- col = 2
-
- component_label = Label(label=display, h_align="start", v_align="center")
- components_grid.attach(component_label, col, row, 1, 1)
-
- switch_container = Gtk.Box(
- orientation=Gtk.Orientation.HORIZONTAL,
- halign=Gtk.Align.START,
- valign=Gtk.Align.CENTER,
- )
- component_switch = Gtk.Switch(
- active=bind_vars.get(f"dock_{name}_visible", True)
- )
- switch_container.add(component_switch)
- components_grid.attach(switch_container, col + 1, row, 1, 1)
- self.component_switches[name] = component_switch
- item_idx += 1
-
- return scrolled_window
-
- def on_notification_position_changed(self, combo: Gtk.ComboBoxText):
- selected_text = combo.get_active_text()
- if selected_text:
- bind_vars[NOTIF_POS_KEY] = selected_text
- print(
- f"Notification position updated in bind_vars: {
- bind_vars[NOTIF_POS_KEY]
- }"
- )
-
- def create_system_tab(self):
- scrolled_window = ScrolledWindow(
- h_scrollbar_policy="never",
- v_scrollbar_policy="automatic",
- h_expand=False,
- v_expand=False,
- propagate_width=False,
- propagate_height=False,
- )
- # Set fixed size to match tab stack dimensions
- scrolled_window.set_size_request(580, 580)
-
- vbox = Box(orientation="v", spacing=15, style="margin: 15px;")
- scrolled_window.add(vbox)
-
- system_grid = Gtk.Grid()
- system_grid.set_column_spacing(20)
- system_grid.set_row_spacing(10)
- system_grid.set_margin_bottom(15)
- vbox.add(system_grid)
-
- terminal_header = Label(markup="Terminal Settings", h_align="start")
- system_grid.attach(terminal_header, 0, 0, 2, 1)
- terminal_label = Label(label="Command:", h_align="start", v_align="center")
- system_grid.attach(terminal_label, 0, 1, 1, 1)
- self.terminal_entry = Entry(
- text=bind_vars.get("terminal_command", "kitty -e"),
- tooltip_text="Command used to launch terminal apps (e.g., 'kitty -e')",
- h_expand=True,
- )
- system_grid.attach(self.terminal_entry, 1, 1, 1, 1)
- hint_label = Label(
- markup="Examples: 'kitty -e', 'alacritty -e', 'foot -e'",
- h_align="start",
- )
- system_grid.attach(hint_label, 0, 2, 2, 1)
-
- hypr_header = Label(markup="Hyprland Integration", h_align="start")
- system_grid.attach(hypr_header, 2, 0, 2, 1)
- row = 1
- self.lock_switch = None
- if self.show_lock_checkbox:
- lock_label = Label(
- label="Replace Hyprlock config", h_align="start", v_align="center"
- )
- system_grid.attach(lock_label, 2, row, 1, 1)
- lock_switch_container = Gtk.Box(
- orientation=Gtk.Orientation.HORIZONTAL,
- halign=Gtk.Align.START,
- valign=Gtk.Align.CENTER,
- )
- self.lock_switch = Gtk.Switch(
- tooltip_text="Replace Hyprlock configuration with Modus's custom config"
- )
- lock_switch_container.add(self.lock_switch)
- system_grid.attach(lock_switch_container, 3, row, 1, 1)
- row += 1
- self.idle_switch = None
- if self.show_idle_checkbox:
- idle_label = Label(
- label="Replace Hypridle config", h_align="start", v_align="center"
- )
- system_grid.attach(idle_label, 2, row, 1, 1)
- idle_switch_container = Gtk.Box(
- orientation=Gtk.Orientation.HORIZONTAL,
- halign=Gtk.Align.START,
- valign=Gtk.Align.CENTER,
- )
- self.idle_switch = Gtk.Switch(
- tooltip_text="Replace Hypridle configuration with Modus's custom config"
- )
- idle_switch_container.add(self.idle_switch)
- system_grid.attach(idle_switch_container, 3, row, 1, 1)
- row += 1
- if self.show_lock_checkbox or self.show_idle_checkbox:
- note_label = Label(
- markup="Existing configs will be backed up",
- h_align="start",
- )
- system_grid.attach(note_label, 2, row, 2, 1)
-
- # Notifications app lists section
- notifications_header = Label(
- markup="Notification Settings", h_align="start"
- )
- vbox.add(notifications_header)
-
- notif_grid = Gtk.Grid()
- notif_grid.set_column_spacing(20)
- notif_grid.set_row_spacing(10)
- notif_grid.set_margin_start(10)
- notif_grid.set_margin_top(5)
- notif_grid.set_margin_bottom(15)
- vbox.add(notif_grid)
-
- # Limited Apps History
- limited_apps_label = Label(
- label="Limited Apps History:", h_align="start", v_align="center"
- )
- notif_grid.attach(limited_apps_label, 0, 0, 1, 1)
-
- limited_apps_list = bind_vars.get("limited_apps_history", ["Spotify"])
- limited_apps_text = ", ".join(f'"{app}"' for app in limited_apps_list)
- self.limited_apps_entry = Entry(
- text=limited_apps_text,
- tooltip_text='Enter app names separated by commas, e.g: "Spotify", "Discord"',
- h_expand=True,
- )
- notif_grid.attach(self.limited_apps_entry, 1, 0, 1, 1)
-
- limited_apps_hint = Label(
- markup='Apps with limited notification history (format: "App1", "App2")',
- h_align="start",
- )
- notif_grid.attach(limited_apps_hint, 0, 1, 2, 1)
-
- # History Ignored Apps
- ignored_apps_label = Label(
- label="History Ignored Apps:", h_align="start", v_align="center"
- )
- notif_grid.attach(ignored_apps_label, 0, 2, 1, 1)
-
- ignored_apps_list = bind_vars.get("history_ignored_apps", ["Modus"])
- ignored_apps_text = ", ".join(f'"{app}"' for app in ignored_apps_list)
- self.ignored_apps_entry = Entry(
- text=ignored_apps_text,
- tooltip_text='Enter app names separated by commas, e.g: "Modus", "Screenshot"',
- h_expand=True,
- )
- notif_grid.attach(self.ignored_apps_entry, 1, 2, 1, 1)
-
- ignored_apps_hint = Label(
- markup='Apps whose notifications are ignored in history (format: "App1", "App2")',
- h_align="start",
- )
- notif_grid.attach(ignored_apps_hint, 0, 3, 2, 1)
-
-
- metrics_header = Label(markup="System Metrics Options", h_align="start")
- vbox.add(metrics_header)
- metrics_grid = Gtk.Grid(
- column_spacing=15, row_spacing=8, margin_start=10, margin_top=5
- )
- vbox.add(metrics_grid)
-
- self.metrics_switches = {}
- metric_names = {
- "cpu": "CPU",
- "ram": "RAM",
- "swap": "Swap",
- "disk": "Disk",
- "gpu": "GPU",
- }
-
- metrics_grid.attach(Label(label="Show in Metrics", h_align="start"), 0, 0, 1, 1)
- for i, (key, label_text) in enumerate(metric_names.items()):
- switch = Gtk.Switch(
- active=bind_vars.get("metrics_visible", {}).get(key, True)
- )
- self.metrics_switches[key] = switch
- metrics_grid.attach(
- Label(label=label_text, h_align="start"), 0, i + 1, 1, 1
- )
- metrics_grid.attach(switch, 1, i + 1, 1, 1)
-
- def enforce_minimum_metrics(switch_dict):
- enabled_switches = [s for s in switch_dict.values() if s.get_active()]
- can_disable = len(enabled_switches) > 1
- for s in switch_dict.values():
- s.set_sensitive(True if can_disable or not s.get_active() else False)
-
- def on_metric_toggle(switch, gparam, switch_dict):
- enforce_minimum_metrics(switch_dict)
-
- for k_s, s_s in self.metrics_switches.items():
- s_s.connect("notify::active", on_metric_toggle, self.metrics_switches)
- enforce_minimum_metrics(self.metrics_switches)
-
- disks_label = Label(
- label="Disk directories for Metrics", h_align="start", v_align="center"
- )
- vbox.add(disks_label)
-
- # Create a scrolled container for disk entries to prevent overflow
- disk_entries_scrolled = ScrolledWindow(
- h_scrollbar_policy="never",
- v_scrollbar_policy="automatic",
- h_expand=False,
- v_expand=False,
- propagate_width=False,
- propagate_height=False,
- )
- # Set fixed size for disk entries container
- disk_entries_scrolled.set_size_request(550, 120)
-
- self.disk_entries = Box(orientation="v", spacing=8, h_align="start")
- # Set size constraints for the disk entries box
- self.disk_entries.set_size_request(530, -1)
- disk_entries_scrolled.add(self.disk_entries)
-
- self._create_disk_edit_entry_func = lambda path: self._add_disk_entry_widget(
- path
- )
-
- for p in bind_vars.get("metrics_disks", ["/"]):
- self._create_disk_edit_entry_func(p)
- vbox.add(disk_entries_scrolled)
-
- add_container = Gtk.Box(
- orientation=Gtk.Orientation.HORIZONTAL,
- halign=Gtk.Align.START,
- valign=Gtk.Align.CENTER,
- )
- add_btn = Button(
- label="Add new disk",
- on_clicked=lambda _: self._create_disk_edit_entry_func("/"),
- )
- add_container.add(add_btn)
- vbox.add(add_container)
-
- return scrolled_window
-
- def _add_disk_entry_widget(self, path):
- """Helper to add a disk entry row to the disk_entries Box."""
- bar = Box(orientation="h", spacing=10, h_align="start")
- # Set fixed height for disk entry rows
- bar.set_size_request(-1, 30)
- entry = Entry(text=path, h_expand=True)
- bar.add(entry)
- x_btn = Button(label="X")
- x_btn.connect(
- "clicked",
- lambda _, current_bar_to_remove=bar: self._remove_disk_entry(current_bar_to_remove),
- )
- bar.add(x_btn)
- self.disk_entries.add(bar)
- self.disk_entries.show_all()
- # Enforce window size after adding content
- GLib.idle_add(self.enforce_window_size)
-
- def _remove_disk_entry(self, bar_to_remove):
- """Helper to remove a disk entry and enforce window size"""
- self.disk_entries.remove(bar_to_remove)
- # Enforce window size after removing content
- GLib.idle_add(self.enforce_window_size)
-
- def create_about_tab(self):
- vbox = Box(orientation="v", spacing=18, style="margin: 30px;")
- vbox.add(
- Label(
- markup=f"{APP_NAME_CAP}",
- h_align="start",
- style="font-size: 1.5em; margin-bottom: 8px;",
- )
- )
- vbox.add(
- Label(
- label="A hackable shell for Hyprland, powered by Fabric.",
- h_align="start",
- style="margin-bottom: 12px;",
- )
- )
- repo_box = Box(orientation="h", spacing=6, h_align="start")
- repo_label = Label(label="GitHub:", h_align="start")
- repo_link = Label(
- markup='https://github.com/S4NKALP/Modus'
- )
- repo_box.add(repo_label)
- repo_box.add(repo_link)
- vbox.add(repo_box)
-
- vbox.add(Box(v_expand=True))
- return vbox
-
- def on_ws_mode_changed(self, combo):
- mode = combo.get_active_text()
- is_numbers_mode = mode == "Numbers"
- self.ws_chinese_switch.set_sensitive(is_numbers_mode)
- if not is_numbers_mode:
- self.ws_chinese_switch.set_active(False)
-
- def on_ws_num_changed(self, switch, gparam):
- is_active = switch.get_active()
- self.ws_chinese_switch.set_sensitive(is_active)
- if not is_active:
- self.ws_chinese_switch.set_active(False)
-
- def on_position_changed(self, combo):
- position = combo.get_active_text()
- is_vertical = position in ["Left", "Right"]
- self.centered_switch.set_sensitive(is_vertical)
- if not is_vertical:
- self.centered_switch.set_active(False)
-
- def on_dock_enabled_changed(self, switch, gparam):
- is_active = switch.get_active()
- self.dock_hover_switch.set_sensitive(is_active)
- self.dock_auto_hide_switch.set_sensitive(is_active)
- self.dock_hide_special_switch.set_sensitive(is_active)
- self.dock_hide_special_apps_switch.set_sensitive(is_active)
- if not is_active:
- self.dock_hover_switch.set_active(False)
- self.dock_auto_hide_switch.set_active(False)
- self.dock_hide_special_switch.set_active(False)
- self.dock_hide_special_apps_switch.set_active(False)
-
- def on_select_face_icon(self, widget):
- dialog = Gtk.FileChooserDialog(
- title="Select Face Icon",
- transient_for=self.get_toplevel(),
- action=Gtk.FileChooserAction.OPEN,
- )
- dialog.add_buttons(
- Gtk.STOCK_CANCEL,
- Gtk.ResponseType.CANCEL,
- Gtk.STOCK_OPEN,
- Gtk.ResponseType.OK,
- )
- image_filter = Gtk.FileFilter()
- image_filter.set_name("Image files")
- for mime in ["image/png", "image/jpeg"]:
- image_filter.add_mime_type(mime)
- for pattern in ["*.png", "*.jpg", "*.jpeg"]:
- image_filter.add_pattern(pattern)
- dialog.add_filter(image_filter)
- if dialog.run() == Gtk.ResponseType.OK:
- self.selected_face_icon = dialog.get_filename()
- self.face_status_label.label = (
- f"Selected: {os.path.basename(self.selected_face_icon)}"
- )
- try:
- pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
- self.selected_face_icon, 64, 64
- )
- self.face_image.set_from_pixbuf(pixbuf)
- except Exception as e:
- print(f"Error loading selected face icon preview: {e}")
- self.face_image.set_from_icon_name("image-missing", Gtk.IconSize.DIALOG)
- dialog.destroy()
-
- def on_accept(self, widget):
+ def on_accept(self, _widget):
current_bind_vars_snapshot = {}
- for prefix_key, suffix_key, prefix_entry, suffix_entry in self.entries:
- current_bind_vars_snapshot[prefix_key] = prefix_entry.get_text()
- current_bind_vars_snapshot[suffix_key] = suffix_entry.get_text()
- current_bind_vars_snapshot["wallpapers_dir"] = (
- self.wall_dir_chooser.get_filename()
- )
+ # Get values from key bindings tab
+ current_bind_vars_snapshot.update(self.key_bindings_tab.get_key_binding_values())
- current_bind_vars_snapshot["dock_position"] = (
- self.position_combo.get_active_text()
- )
- current_bind_vars_snapshot["vertical"] = current_bind_vars_snapshot[
- "dock_position"
- ] in ["Left", "Right"]
+ # Get values from appearance tab
+ current_bind_vars_snapshot.update(self.appearance_tab.get_appearance_values())
- current_bind_vars_snapshot["dock_enabled"] = self.dock_switch.get_active()
- current_bind_vars_snapshot["dock_auto_hide"] = (
- self.dock_auto_hide_switch.get_active()
- )
- current_bind_vars_snapshot["dock_always_occluded"] = (
- self.dock_hover_switch.get_active()
- )
- current_bind_vars_snapshot["dock_hide_special_workspace"] = (
- self.dock_hide_special_switch.get_active()
- )
- current_bind_vars_snapshot["dock_hide_special_workspace_apps"] = (
- self.dock_hide_special_apps_switch.get_active()
- )
- current_bind_vars_snapshot["dock_icon_size"] = int(
- self.icon_size_scale.get_value()
- )
- current_bind_vars_snapshot["window_switcher_items_per_row"] = int(
- self.application_switcher_scale.get_value()
- )
- current_bind_vars_snapshot["terminal_command"] = self.terminal_entry.get_text()
- current_bind_vars_snapshot["corners_visible"] = self.corners_switch.get_active()
- current_bind_vars_snapshot["dock_theme"] = (
- self.dock_theme_combo.get_active_text()
- )
+ # Get values from system tab
+ current_bind_vars_snapshot.update(self.system_tab.get_system_values())
- selected_notif_pos_text = self.notification_pos_combo.get_active_text()
- if selected_notif_pos_text:
- current_bind_vars_snapshot[NOTIF_POS_KEY] = selected_notif_pos_text
- else:
- current_bind_vars_snapshot[NOTIF_POS_KEY] = NOTIF_POS_DEFAULT
+ # Get face icon and hyprland switches
+ selected_icon_path = self.appearance_tab.get_selected_face_icon()
+ hyprland_switches = self.system_tab.get_hyprland_switches()
+ replace_lock = hyprland_switches["replace_lock"]
+ replace_idle = hyprland_switches["replace_idle"]
- for component_name, switch in self.component_switches.items():
- current_bind_vars_snapshot[f"dock_{component_name}_visible"] = (
- switch.get_active()
- )
-
- # Save workspace settings
- ws_mode = self.ws_mode_combo.get_active_text()
- current_bind_vars_snapshot["workspace_dots"] = ws_mode == "Dots"
- current_bind_vars_snapshot["workspace_nums"] = ws_mode == "Numbers"
- current_bind_vars_snapshot["workspace_use_chinese_numerals"] = (
- self.ws_chinese_switch.get_active()
- )
-
- current_bind_vars_snapshot["metrics_visible"] = {
- k: s.get_active() for k, s in self.metrics_switches.items()
- }
- current_bind_vars_snapshot["metrics_disks"] = [
- child.get_children()[0].get_text()
- for child in self.disk_entries.get_children()
- if isinstance(child, Gtk.Box)
- and child.get_children()
- and isinstance(child.get_children()[0], Entry)
- ]
-
- # Parse notification app lists
- def parse_app_list(text):
- """Parse comma-separated app names with quotes"""
- if not text.strip():
- return []
- apps = []
- for app in text.split(","):
- app = app.strip()
- if app.startswith('"') and app.endswith('"'):
- app = app[1:-1]
- elif app.startswith("'") and app.endswith("'"):
- app = app[1:-1]
- if app:
- apps.append(app)
- return apps
-
- current_bind_vars_snapshot["limited_apps_history"] = parse_app_list(
- self.limited_apps_entry.get_text()
- )
- current_bind_vars_snapshot["history_ignored_apps"] = parse_app_list(
- self.ignored_apps_entry.get_text()
- )
-
- selected_icon_path = self.selected_face_icon
- replace_lock = self.lock_switch and self.lock_switch.get_active()
- replace_idle = self.idle_switch and self.idle_switch.get_active()
-
- if self.selected_face_icon:
- self.selected_face_icon = None
- self.face_status_label.label = ""
+ if selected_icon_path:
+ self.appearance_tab.clear_selected_face_icon()
def _apply_and_reload_task_thread():
nonlocal current_bind_vars_snapshot
@@ -1244,7 +323,7 @@ class HyprConfGUI(Window):
self.face_image.set_from_icon_name("image-missing", Gtk.IconSize.DIALOG)
return GLib.SOURCE_REMOVE
- def on_reset(self, widget):
+ def on_reset(self, _widget):
dialog = Gtk.MessageDialog(
transient_for=self.get_toplevel(),
flags=0,
@@ -1270,139 +349,15 @@ class HyprConfGUI(Window):
utils.bind_vars.clear()
utils.bind_vars.update(DEFAULTS.copy())
- for prefix_key, suffix_key, prefix_entry, suffix_entry in self.entries:
- prefix_entry.set_text(utils.bind_vars[prefix_key])
- suffix_entry.set_text(utils.bind_vars[suffix_key])
-
- self.wall_dir_chooser.set_filename(utils.bind_vars["wallpapers_dir"])
-
- positions = ["Bottom", "Top", "Left", "Right"]
- default_position = DEFAULTS.get("dock_position", "Bottom")
- try:
- self.position_combo.set_active(positions.index(default_position))
- except ValueError:
- self.position_combo.set_active(0)
-
- self.dock_switch.set_active(utils.bind_vars.get("dock_enabled", True))
- self.dock_auto_hide_switch.set_active(
- utils.bind_vars.get("dock_auto_hide", True)
- )
- self.dock_hover_switch.set_active(
- utils.bind_vars.get("dock_always_occluded", False)
- )
- self.dock_hide_special_switch.set_active(
- utils.bind_vars.get("dock_hide_special_workspace", True)
- )
- self.dock_hide_special_apps_switch.set_active(
- utils.bind_vars.get("dock_hide_special_workspace_apps", True)
- )
- self.dock_hover_switch.set_sensitive(self.dock_switch.get_active())
- self.dock_auto_hide_switch.set_sensitive(self.dock_switch.get_active())
- self.dock_hide_special_switch.set_sensitive(self.dock_switch.get_active())
- self.dock_hide_special_apps_switch.set_sensitive(self.dock_switch.get_active())
- self.terminal_entry.set_text(utils.bind_vars["terminal_command"])
-
- default_dock_theme_val = DEFAULTS.get("dock_theme", "Pills")
- try:
- self.dock_theme_combo.set_active(
- self.themes.index(default_dock_theme_val)
- )
- except ValueError:
- self.dock_theme_combo.set_active(0)
-
- default_notif_pos_val = DEFAULTS.get(NOTIF_POS_KEY, NOTIF_POS_DEFAULT)
- notification_positions_list = ["Top", "Bottom"]
- try:
- self.notification_pos_combo.set_active(
- notification_positions_list.index(default_notif_pos_val)
- )
- except ValueError:
- self.notification_pos_combo.set_active(0)
-
- for name, switch in self.component_switches.items():
- switch.set_active(utils.bind_vars.get(f"dock_{name}_visible", True))
-
- # Reset workspace settings
- default_dots = DEFAULTS.get("workspace_dots", True)
- default_nums = DEFAULTS.get("workspace_nums", False)
- if default_dots:
- default_mode = "Dots"
- elif default_nums:
- default_mode = "Numbers"
- else:
- default_mode = "Single Button"
-
- ws_modes = ["Dots", "Numbers", "Single Button"]
- try:
- self.ws_mode_combo.set_active(ws_modes.index(default_mode))
- except ValueError:
- self.ws_mode_combo.set_active(0)
-
- self.ws_chinese_switch.set_active(
- utils.bind_vars.get("workspace_use_chinese_numerals", False)
- )
- self.ws_chinese_switch.set_sensitive(default_nums)
- self.corners_switch.set_active(utils.bind_vars.get("corners_visible", True))
-
- metrics_vis_defaults = DEFAULTS.get("metrics_visible", {})
- for k, s_widget in self.metrics_switches.items():
- s_widget.set_active(metrics_vis_defaults.get(k, True))
-
- def enforce_minimum_metrics(switch_dict):
- enabled_switches = [s for s in switch_dict.values() if s.get_active()]
- can_disable = len(enabled_switches) > 1
- for s in switch_dict.values():
- s.set_sensitive(
- True if can_disable or not s.get_active() else False
- )
-
- enforce_minimum_metrics(self.metrics_switches)
-
- for child in list(self.disk_entries.get_children()):
- self.disk_entries.remove(child)
-
- for p in DEFAULTS.get("metrics_disks", ["/"]):
- self._add_disk_edit_entry_func(p)
-
- # Reset notification app lists
- limited_apps_list = DEFAULTS.get("limited_apps_history", ["Spotify"])
- limited_apps_text = ", ".join(f'"{app}"' for app in limited_apps_list)
- self.limited_apps_entry.set_text(limited_apps_text)
-
- ignored_apps_list = DEFAULTS.get("history_ignored_apps", ["Hyprshot"])
- ignored_apps_text = ", ".join(f'"{app}"' for app in ignored_apps_list)
- self.ignored_apps_entry.set_text(ignored_apps_text)
+ # Reset all tabs to defaults
+ self.key_bindings_tab.reset_to_defaults(utils.bind_vars)
+ self.appearance_tab.reset_to_defaults(utils.bind_vars)
+ self.system_tab.reset_to_defaults(utils.bind_vars)
self._update_panel_position_sensitivity()
-
- self.selected_face_icon = None
- self.face_status_label.label = ""
- current_face = os.path.expanduser("~/.face.icon")
- try:
- pixbuf = (
- GdkPixbuf.Pixbuf.new_from_file_at_size(current_face, 64, 64)
- if os.path.exists(current_face)
- else None
- )
- if pixbuf:
- self.face_image.set_from_pixbuf(pixbuf)
- else:
- self.face_image.set_from_icon_name("user-info", Gtk.IconSize.DIALOG)
- except Exception:
- self.face_image.set_from_icon_name("image-missing", Gtk.IconSize.DIALOG)
-
- if self.lock_switch:
- self.lock_switch.set_active(False)
- if self.idle_switch:
- self.idle_switch.set_active(False)
- # Add reset for icon size scale
- self.icon_size_scale.set_value(utils.bind_vars.get("dock_icon_size", 20))
- self.application_switcher_scale.set_value(
- utils.bind_vars.get("window_switcher_items_per_row", 13)
- )
print("Settings reset to defaults.")
- def on_close(self, widget):
+ def on_close(self, _widget):
if self.application:
self.application.quit()
else:
diff --git a/config/settings/key_bindings_tab.py b/config/settings/key_bindings_tab.py
new file mode 100644
index 0000000..e5d392d
--- /dev/null
+++ b/config/settings/key_bindings_tab.py
@@ -0,0 +1,115 @@
+import gi
+from fabric.widgets.box import Box
+from fabric.widgets.entry import Entry
+from fabric.widgets.label import Label
+from fabric.widgets.scrolledwindow import ScrolledWindow
+from gi.repository import Gtk
+
+from config.data import APP_NAME_CAP
+from config.settings.utils import bind_vars
+
+gi.require_version("Gtk", "3.0")
+
+
+class KeyBindingsTab:
+ """Key bindings management tab for settings"""
+
+ def __init__(self):
+ self.entries = []
+
+ def create_key_bindings_tab(self):
+ """Create the Key Bindings tab content"""
+ scrolled_window = ScrolledWindow(
+ h_scrollbar_policy="never",
+ v_scrollbar_policy="automatic",
+ h_expand=False,
+ v_expand=False,
+ propagate_width=False,
+ propagate_height=False,
+ )
+ # Set fixed size to match tab stack dimensions
+ scrolled_window.set_size_request(580, 580)
+
+ main_vbox = Box(orientation="v", spacing=10, style="margin: 15px;")
+ scrolled_window.add(main_vbox)
+
+ keybind_grid = Gtk.Grid()
+ keybind_grid.set_column_spacing(10)
+ keybind_grid.set_row_spacing(8)
+ keybind_grid.set_margin_start(5)
+ keybind_grid.set_margin_end(5)
+ keybind_grid.set_margin_top(5)
+ keybind_grid.set_margin_bottom(5)
+
+ action_label = Label(
+ markup="Action", h_align="start", style="margin-bottom: 5px;"
+ )
+ modifier_label = Label(
+ markup="Modifier", h_align="start", style="margin-bottom: 5px;"
+ )
+ separator_label = Label(
+ label="+", h_align="center", style="margin-bottom: 5px;"
+ )
+ key_label = Label(
+ markup="Key", h_align="start", style="margin-bottom: 5px;"
+ )
+
+ keybind_grid.attach(action_label, 0, 0, 1, 1)
+ keybind_grid.attach(modifier_label, 1, 0, 1, 1)
+ keybind_grid.attach(separator_label, 2, 0, 1, 1)
+ keybind_grid.attach(key_label, 3, 0, 1, 1)
+
+ self.entries = []
+ bindings = [
+ (f"Reload {APP_NAME_CAP}", "prefix_restart", "suffix_restart"),
+ ("Message", "prefix_msg", "suffix_msg"),
+ (
+ "Application Switcher",
+ "prefix_application_switcher",
+ "suffix_application_switcher",
+ ),
+ ("Launcher", "prefix_launcher", "suffix_launcher"),
+ ("App Launcher", "prefix_app_launcher", "suffix_app_launcher"),
+ ("Clipboard History", "prefix_cliphist", "suffix_cliphist"),
+ ("Wallpapers", "prefix_wallpapers", "suffix_wallpapers"),
+ ("Random Wallpaper", "prefix_randwall", "suffix_randwall"),
+ ("Emoji Picker", "prefix_emoji", "suffix_emoji"),
+ ("Kanban", "prefix_kanban", "suffix_kanban"),
+ ("Power Menu", "prefix_power", "suffix_power"),
+ ("Toggle Caffeine", "prefix_caffeine", "suffix_caffeine"),
+ ("Settings", "prefix_settings", "suffix_settings"),
+ (
+ "Restart with inspector",
+ "prefix_restart_inspector",
+ "suffix_restart_inspector",
+ ),
+ ]
+
+ for i, (label_text, prefix_key, suffix_key) in enumerate(bindings):
+ row = i + 1
+ binding_label = Label(label=label_text, h_align="start")
+ keybind_grid.attach(binding_label, 0, row, 1, 1)
+ prefix_entry = Entry(text=bind_vars.get(prefix_key, ""))
+ keybind_grid.attach(prefix_entry, 1, row, 1, 1)
+ plus_label = Label(label="+", h_align="center")
+ keybind_grid.attach(plus_label, 2, row, 1, 1)
+ suffix_entry = Entry(text=bind_vars.get(suffix_key, ""))
+ keybind_grid.attach(suffix_entry, 3, row, 1, 1)
+ self.entries.append((prefix_key, suffix_key, prefix_entry, suffix_entry))
+
+ main_vbox.add(keybind_grid)
+ return scrolled_window
+
+ def get_key_binding_values(self):
+ """Get current key binding values from entries"""
+ values = {}
+ for prefix_key, suffix_key, prefix_entry, suffix_entry in self.entries:
+ values[prefix_key] = prefix_entry.get_text()
+ values[suffix_key] = suffix_entry.get_text()
+ return values
+
+ def reset_to_defaults(self, defaults):
+ """Reset key bindings to default values"""
+ for prefix_key, suffix_key, prefix_entry, suffix_entry in self.entries:
+ prefix_entry.set_text(defaults.get(prefix_key, ""))
+ suffix_entry.set_text(defaults.get(suffix_key, ""))
diff --git a/config/settings/system_tab.py b/config/settings/system_tab.py
new file mode 100644
index 0000000..f3be6b4
--- /dev/null
+++ b/config/settings/system_tab.py
@@ -0,0 +1,382 @@
+import gi
+from fabric.widgets.box import Box
+from fabric.widgets.button import Button
+from fabric.widgets.entry import Entry
+from fabric.widgets.label import Label
+from fabric.widgets.scrolledwindow import ScrolledWindow
+from gi.repository import GLib, Gtk
+
+from config.settings.utils import bind_vars
+
+gi.require_version("Gtk", "3.0")
+
+
+class SystemTab:
+ """System settings and metrics management tab for settings"""
+
+ def __init__(self, show_lock_checkbox=False, show_idle_checkbox=False, window_size_enforcer=None):
+ self.show_lock_checkbox = show_lock_checkbox
+ self.show_idle_checkbox = show_idle_checkbox
+ self.window_size_enforcer = window_size_enforcer
+
+ # Widget references
+ self.terminal_entry = None
+ self.lock_switch = None
+ self.idle_switch = None
+ self.limited_apps_entry = None
+ self.ignored_apps_entry = None
+ self.metrics_switches = {}
+ self.disk_entries = None
+ self._create_disk_edit_entry_func = None
+
+ def create_system_tab(self):
+ """Create the System tab content"""
+ scrolled_window = ScrolledWindow(
+ h_scrollbar_policy="never",
+ v_scrollbar_policy="automatic",
+ h_expand=False,
+ v_expand=False,
+ propagate_width=False,
+ propagate_height=False,
+ )
+ # Set fixed size to match tab stack dimensions
+ scrolled_window.set_size_request(580, 580)
+
+ vbox = Box(orientation="v", spacing=15, style="margin: 15px;")
+ scrolled_window.add(vbox)
+
+ # Create terminal and hyprland settings section
+ self._create_terminal_hyprland_section(vbox)
+
+ # Create notification settings section
+ self._create_notification_settings_section(vbox)
+
+ # Create system metrics section
+ self._create_system_metrics_section(vbox)
+
+ return scrolled_window
+
+ def _create_terminal_hyprland_section(self, vbox):
+ """Create terminal and Hyprland integration section"""
+ system_grid = Gtk.Grid()
+ system_grid.set_column_spacing(20)
+ system_grid.set_row_spacing(10)
+ system_grid.set_margin_bottom(15)
+ vbox.add(system_grid)
+
+ # Terminal settings
+ terminal_header = Label(markup="Terminal Settings", h_align="start")
+ system_grid.attach(terminal_header, 0, 0, 2, 1)
+ terminal_label = Label(label="Command:", h_align="start", v_align="center")
+ system_grid.attach(terminal_label, 0, 1, 1, 1)
+ self.terminal_entry = Entry(
+ text=bind_vars.get("terminal_command", "kitty -e"),
+ tooltip_text="Command used to launch terminal apps (e.g., 'kitty -e')",
+ h_expand=True,
+ )
+ system_grid.attach(self.terminal_entry, 1, 1, 1, 1)
+ hint_label = Label(
+ markup="Examples: 'kitty -e', 'alacritty -e', 'foot -e'",
+ h_align="start",
+ )
+ system_grid.attach(hint_label, 0, 2, 2, 1)
+
+ # Hyprland integration
+ hypr_header = Label(markup="Hyprland Integration", h_align="start")
+ system_grid.attach(hypr_header, 2, 0, 2, 1)
+ row = 1
+ self.lock_switch = None
+ if self.show_lock_checkbox:
+ lock_label = Label(
+ label="Replace Hyprlock config", h_align="start", v_align="center"
+ )
+ system_grid.attach(lock_label, 2, row, 1, 1)
+ lock_switch_container = Gtk.Box(
+ orientation=Gtk.Orientation.HORIZONTAL,
+ halign=Gtk.Align.START,
+ valign=Gtk.Align.CENTER,
+ )
+ self.lock_switch = Gtk.Switch(
+ tooltip_text="Replace Hyprlock configuration with Modus's custom config"
+ )
+ lock_switch_container.add(self.lock_switch)
+ system_grid.attach(lock_switch_container, 3, row, 1, 1)
+ row += 1
+ self.idle_switch = None
+ if self.show_idle_checkbox:
+ idle_label = Label(
+ label="Replace Hypridle config", h_align="start", v_align="center"
+ )
+ system_grid.attach(idle_label, 2, row, 1, 1)
+ idle_switch_container = Gtk.Box(
+ orientation=Gtk.Orientation.HORIZONTAL,
+ halign=Gtk.Align.START,
+ valign=Gtk.Align.CENTER,
+ )
+ self.idle_switch = Gtk.Switch(
+ tooltip_text="Replace Hypridle configuration with Modus's custom config"
+ )
+ idle_switch_container.add(self.idle_switch)
+ system_grid.attach(idle_switch_container, 3, row, 1, 1)
+ row += 1
+ if self.show_lock_checkbox or self.show_idle_checkbox:
+ note_label = Label(
+ markup="Existing configs will be backed up",
+ h_align="start",
+ )
+ system_grid.attach(note_label, 2, row, 2, 1)
+
+ def _create_notification_settings_section(self, vbox):
+ """Create notification settings section"""
+ notifications_header = Label(
+ markup="Notification Settings", h_align="start"
+ )
+ vbox.add(notifications_header)
+
+ notif_grid = Gtk.Grid()
+ notif_grid.set_column_spacing(20)
+ notif_grid.set_row_spacing(10)
+ notif_grid.set_margin_start(10)
+ notif_grid.set_margin_top(5)
+ notif_grid.set_margin_bottom(15)
+ vbox.add(notif_grid)
+
+ # Limited Apps History
+ limited_apps_label = Label(
+ label="Limited Apps History:", h_align="start", v_align="center"
+ )
+ notif_grid.attach(limited_apps_label, 0, 0, 1, 1)
+
+ limited_apps_list = bind_vars.get("limited_apps_history", ["Spotify"])
+ limited_apps_text = ", ".join(f'"{app}"' for app in limited_apps_list)
+ self.limited_apps_entry = Entry(
+ text=limited_apps_text,
+ tooltip_text='Enter app names separated by commas, e.g: "Spotify", "Discord"',
+ h_expand=True,
+ )
+ notif_grid.attach(self.limited_apps_entry, 1, 0, 1, 1)
+
+ limited_apps_hint = Label(
+ markup='Apps with limited notification history (format: "App1", "App2")',
+ h_align="start",
+ )
+ notif_grid.attach(limited_apps_hint, 0, 1, 2, 1)
+
+ # History Ignored Apps
+ ignored_apps_label = Label(
+ label="History Ignored Apps:", h_align="start", v_align="center"
+ )
+ notif_grid.attach(ignored_apps_label, 0, 2, 1, 1)
+
+ ignored_apps_list = bind_vars.get("history_ignored_apps", ["Modus"])
+ ignored_apps_text = ", ".join(f'"{app}"' for app in ignored_apps_list)
+ self.ignored_apps_entry = Entry(
+ text=ignored_apps_text,
+ tooltip_text='Enter app names separated by commas, e.g: "Modus", "Screenshot"',
+ h_expand=True,
+ )
+ notif_grid.attach(self.ignored_apps_entry, 1, 2, 1, 1)
+
+ ignored_apps_hint = Label(
+ markup='Apps whose notifications are ignored in history (format: "App1", "App2")',
+ h_align="start",
+ )
+ notif_grid.attach(ignored_apps_hint, 0, 3, 2, 1)
+
+ def _create_system_metrics_section(self, vbox):
+ """Create system metrics section"""
+ metrics_header = Label(markup="System Metrics Options", h_align="start")
+ vbox.add(metrics_header)
+ metrics_grid = Gtk.Grid(
+ column_spacing=15, row_spacing=8, margin_start=10, margin_top=5
+ )
+ vbox.add(metrics_grid)
+
+ self.metrics_switches = {}
+ metric_names = {
+ "cpu": "CPU",
+ "ram": "RAM",
+ "swap": "Swap",
+ "disk": "Disk",
+ "gpu": "GPU",
+ }
+
+ metrics_grid.attach(Label(label="Show in Metrics", h_align="start"), 0, 0, 1, 1)
+ for i, (key, label_text) in enumerate(metric_names.items()):
+ switch = Gtk.Switch(
+ active=bind_vars.get("metrics_visible", {}).get(key, True)
+ )
+ self.metrics_switches[key] = switch
+ metrics_grid.attach(
+ Label(label=label_text, h_align="start"), 0, i + 1, 1, 1
+ )
+ metrics_grid.attach(switch, 1, i + 1, 1, 1)
+
+ def enforce_minimum_metrics(switch_dict):
+ enabled_switches = [s for s in switch_dict.values() if s.get_active()]
+ can_disable = len(enabled_switches) > 1
+ for s in switch_dict.values():
+ s.set_sensitive(True if can_disable or not s.get_active() else False)
+
+ def on_metric_toggle(_switch, _gparam, switch_dict):
+ enforce_minimum_metrics(switch_dict)
+
+ for s_s in self.metrics_switches.values():
+ s_s.connect("notify::active", on_metric_toggle, self.metrics_switches)
+ enforce_minimum_metrics(self.metrics_switches)
+
+ # Disk directories section
+ disks_label = Label(
+ label="Disk directories for Metrics", h_align="start", v_align="center"
+ )
+ vbox.add(disks_label)
+
+ # Create a scrolled container for disk entries to prevent overflow
+ disk_entries_scrolled = ScrolledWindow(
+ h_scrollbar_policy="never",
+ v_scrollbar_policy="automatic",
+ h_expand=False,
+ v_expand=False,
+ propagate_width=False,
+ propagate_height=False,
+ )
+ # Set fixed size for disk entries container
+ disk_entries_scrolled.set_size_request(550, 120)
+
+ self.disk_entries = Box(orientation="v", spacing=8, h_align="start")
+ # Set size constraints for the disk entries box
+ self.disk_entries.set_size_request(530, -1)
+ disk_entries_scrolled.add(self.disk_entries)
+
+ self._create_disk_edit_entry_func = lambda path: self._add_disk_entry_widget(path)
+
+ for p in bind_vars.get("metrics_disks", ["/"]):
+ self._create_disk_edit_entry_func(p)
+ vbox.add(disk_entries_scrolled)
+
+ add_container = Gtk.Box(
+ orientation=Gtk.Orientation.HORIZONTAL,
+ halign=Gtk.Align.START,
+ valign=Gtk.Align.CENTER,
+ )
+ add_btn = Button(
+ label="Add new disk",
+ on_clicked=lambda _: self._create_disk_edit_entry_func("/"),
+ )
+ add_container.add(add_btn)
+ vbox.add(add_container)
+
+ def _add_disk_entry_widget(self, path):
+ """Helper to add a disk entry row to the disk_entries Box."""
+ bar = Box(orientation="h", spacing=10, h_align="start")
+ # Set fixed height for disk entry rows
+ bar.set_size_request(-1, 30)
+ entry = Entry(text=path, h_expand=True)
+ bar.add(entry)
+ x_btn = Button(label="X")
+ x_btn.connect(
+ "clicked",
+ lambda _, current_bar_to_remove=bar: self._remove_disk_entry(current_bar_to_remove),
+ )
+ bar.add(x_btn)
+ self.disk_entries.add(bar)
+ self.disk_entries.show_all()
+ # Enforce window size after adding content
+ if self.window_size_enforcer:
+ GLib.idle_add(self.window_size_enforcer)
+
+ def _remove_disk_entry(self, bar_to_remove):
+ """Helper to remove a disk entry and enforce window size"""
+ self.disk_entries.remove(bar_to_remove)
+ # Enforce window size after removing content
+ if self.window_size_enforcer:
+ GLib.idle_add(self.window_size_enforcer)
+
+ def get_system_values(self):
+ """Get current system values from widgets"""
+ values = {}
+
+ values["terminal_command"] = self.terminal_entry.get_text()
+
+ values["metrics_visible"] = {
+ k: s.get_active() for k, s in self.metrics_switches.items()
+ }
+ values["metrics_disks"] = [
+ child.get_children()[0].get_text()
+ for child in self.disk_entries.get_children()
+ if isinstance(child, Gtk.Box)
+ and child.get_children()
+ and isinstance(child.get_children()[0], Entry)
+ ]
+
+ # Parse notification app lists
+ def parse_app_list(text):
+ """Parse comma-separated app names with quotes"""
+ if not text.strip():
+ return []
+ apps = []
+ for app in text.split(","):
+ app = app.strip()
+ if app.startswith('"') and app.endswith('"'):
+ app = app[1:-1]
+ elif app.startswith("'") and app.endswith("'"):
+ app = app[1:-1]
+ if app:
+ apps.append(app)
+ return apps
+
+ values["limited_apps_history"] = parse_app_list(
+ self.limited_apps_entry.get_text()
+ )
+ values["history_ignored_apps"] = parse_app_list(
+ self.ignored_apps_entry.get_text()
+ )
+
+ return values
+
+ def get_hyprland_switches(self):
+ """Get the state of Hyprland integration switches"""
+ return {
+ "replace_lock": self.lock_switch and self.lock_switch.get_active(),
+ "replace_idle": self.idle_switch and self.idle_switch.get_active()
+ }
+
+ def reset_to_defaults(self, defaults):
+ """Reset system settings to default values"""
+ self.terminal_entry.set_text(defaults.get("terminal_command", "kitty -e"))
+
+ metrics_vis_defaults = defaults.get("metrics_visible", {})
+ for k, s_widget in self.metrics_switches.items():
+ s_widget.set_active(metrics_vis_defaults.get(k, True))
+
+ def enforce_minimum_metrics(switch_dict):
+ enabled_switches = [s for s in switch_dict.values() if s.get_active()]
+ can_disable = len(enabled_switches) > 1
+ for s in switch_dict.values():
+ s.set_sensitive(
+ True if can_disable or not s.get_active() else False
+ )
+
+ enforce_minimum_metrics(self.metrics_switches)
+
+ # Reset disk entries
+ for child in list(self.disk_entries.get_children()):
+ self.disk_entries.remove(child)
+
+ for p in defaults.get("metrics_disks", ["/"]):
+ self._create_disk_edit_entry_func(p)
+
+ # Reset notification app lists
+ limited_apps_list = defaults.get("limited_apps_history", ["Spotify"])
+ limited_apps_text = ", ".join(f'"{app}"' for app in limited_apps_list)
+ self.limited_apps_entry.set_text(limited_apps_text)
+
+ ignored_apps_list = defaults.get("history_ignored_apps", ["Modus"])
+ ignored_apps_text = ", ".join(f'"{app}"' for app in ignored_apps_list)
+ self.ignored_apps_entry.set_text(ignored_apps_text)
+
+ # Reset Hyprland switches
+ if self.lock_switch:
+ self.lock_switch.set_active(False)
+ if self.idle_switch:
+ self.idle_switch.set_active(False)