✨ feat(config): add settings GUI tabs for appearance, keybindings, system, and about

This commit is contained in:
sankalp committed 2025-07-21 08:18:27 +05:45
1 parent d5d2721820
commit 88e42162cd
5 files changed
+1266 -1085

No files matched your search

+40
View File
@@ -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"<b>{APP_NAME_CAP}</b>",
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='<a href="https://github.com/S4NKALP/Modus">https://github.com/S4NKALP/Modus</a>'
)
repo_box.add(repo_label)
repo_box.add(repo_link)
vbox.add(repo_box)
vbox.add(Box(v_expand=True))
return vbox
+689
View File
@@ -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="<b>Wallpapers</b>", 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="<b>Profile Icon</b>", 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="<b>Layout Options</b>", 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="<b>Modules</b>", 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))
+40 -1085
View File
File diff suppressed because it is too large. Load diff
+115
View File
@@ -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="<b>Action</b>", h_align="start", style="margin-bottom: 5px;"
)
modifier_label = Label(
markup="<b>Modifier</b>", h_align="start", style="margin-bottom: 5px;"
)
separator_label = Label(
label="+", h_align="center", style="margin-bottom: 5px;"
)
key_label = Label(
markup="<b>Key</b>", 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, ""))
+382
View File
@@ -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="<b>Terminal Settings</b>", 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="<small>Examples: 'kitty -e', 'alacritty -e', 'foot -e'</small>",
h_align="start",
)
system_grid.attach(hint_label, 0, 2, 2, 1)
# Hyprland integration
hypr_header = Label(markup="<b>Hyprland Integration</b>", 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="<small>Existing configs will be backed up</small>",
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="<b>Notification Settings</b>", 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='<small>Apps with limited notification history (format: "App1", "App2")</small>',
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='<small>Apps whose notifications are ignored in history (format: "App1", "App2")</small>',
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="<b>System Metrics Options</b>", 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)