feat(src/window): switch configuration from JSON to TOML and refactor pinned apps handling

This commit is contained in:
sankalp committed 2026-07-04 06:29:01 +05:45
1 parent e5a02015b3
commit 0d11b68add
7 files changed
+333 -41

No files matched your search

+34 -6
View File
@@ -1,6 +1,8 @@
import json
import subprocess
import tomlkit
from fabric.utils import (
GLib,
Gtk,
@@ -26,14 +28,12 @@ from services.modus import modus_service
from utils.functions import (
clear_children,
is_special_workspace_id,
read_json_file,
write_json_file,
)
from utils.icon_resolver import IconResolver
from utils.occlusion import check_occlusion
# Pinned apps file
PINNED_APPS_FILE = get_relative_path("../../config/dock.json")
PINNED_APPS_FILE = get_relative_path("../../config/dock.toml")
class AppBar(Box):
@@ -57,7 +57,7 @@ class AppBar(Box):
# Initialize GTK menu
self.menu = Gtk.Menu()
self.pinned_apps = read_json_file(PINNED_APPS_FILE) or []
self.pinned_apps = self._read_pinned_apps()
self.pinned_apps_container = Box()
self.add(self.pinned_apps_container)
@@ -162,6 +162,34 @@ class AppBar(Box):
super().destroy()
def _read_pinned_apps(self) -> list:
try:
if os.path.exists(PINNED_APPS_FILE):
with open(PINNED_APPS_FILE, "r") as f:
data = tomlkit.load(f)
return list(data.get("pinned", []))
except Exception as e:
logger.error(f"[AppBar] Failed to read pinned apps: {e}")
return []
def _write_pinned_apps(self):
try:
doc = tomlkit.document()
arr = tomlkit.array()
for app in self.pinned_apps:
if isinstance(app, dict):
tbl = tomlkit.inline_table()
for k, v in app.items():
tbl[k] = v
arr.append(tbl)
else:
arr.append(app)
doc["pinned"] = arr
with open(PINNED_APPS_FILE, "w") as f:
tomlkit.dump(doc, f)
except Exception as e:
logger.error(f"[AppBar] Failed to write pinned apps: {e}")
def _populate_pinned_apps(self):
clear_children(self.pinned_apps_container)
@@ -515,7 +543,7 @@ class AppBar(Box):
except Exception:
self.pinned_apps.append(app_class)
write_json_file(self.pinned_apps, PINNED_APPS_FILE)
self._write_pinned_apps()
self._populate_pinned_apps()
return True
@@ -530,7 +558,7 @@ class AppBar(Box):
self.pinned_apps.pop(i)
if apps_to_remove:
write_json_file(self.pinned_apps, PINNED_APPS_FILE)
self._write_pinned_apps()
self._populate_pinned_apps()
return True
return False
+1 -1
View File
@@ -278,7 +278,7 @@ class EmojiPlugin(PluginBase):
subtitle = f"{keywords}" + (" • Recent" if is_recent else "")
# Use larger font size for emojis to make them visible
icon_markup = f"<span size='xx-large'>{emoji}</span>"
icon_markup = f"<span size='xx-large'>{GLib.markup_escape_text(emoji)}</span>"
return Result(
title=name,
+12 -23
View File
@@ -1,7 +1,8 @@
import colorsys
import hashlib
import json
import threading
import tomlkit
from typing import Dict, List, Optional
from fabric.utils import (
@@ -74,39 +75,32 @@ class WallpaperPlugin(PluginBase):
)
def _get_matugen_state(self) -> bool:
"""Get current matugen state from config.json."""
self.matugen_enabled = True # Default to True
"""Get current matugen state from config.toml."""
self.matugen_enabled = True
try:
with open(data.CONFIG_FILE, "r") as f:
config = json.load(f)
config = tomlkit.load(f)
self.matugen_enabled = config.get("matugen_enabled", True)
except FileNotFoundError:
# File doesn't exist, keep default True
pass
except Exception as e:
logger.error(f"Error reading config file: {e}")
# Keep default True on error
return self.matugen_enabled
def _set_matugen_state(self, enabled: bool):
"""Set matugen state and save to config.json."""
"""Set matugen state and save to config.toml."""
self.matugen_enabled = enabled
# Save the state to config.json
try:
# Read current config
config = {}
if os.path.exists(data.CONFIG_FILE):
with open(data.CONFIG_FILE, "r") as f:
config = json.load(f)
config = tomlkit.load(f)
# Update matugen state
config["matugen_enabled"] = enabled
# Write back to config file
with open(data.CONFIG_FILE, "w") as f:
json.dump(config, f, indent=4)
tomlkit.dump(config, f)
except Exception as e:
logger.error(f"Error writing matugen state to config: {e}")
@@ -335,10 +329,9 @@ class WallpaperPlugin(PluginBase):
"""Get current color scheme from config (default to tonal-spot)."""
try:
with open(data.CONFIG_FILE, "r") as f:
config = json.load(f)
config = tomlkit.load(f)
return config.get("current_scheme", "scheme-tonal-spot")
except FileNotFoundError:
# File doesn't exist, return default
return "scheme-tonal-spot"
except Exception as e:
logger.error(f"Error reading current scheme from config: {e}")
@@ -349,20 +342,16 @@ class WallpaperPlugin(PluginBase):
scheme_name = self.schemes.get(scheme, scheme)
matugen_enabled = self._get_matugen_state()
# Save the scheme to config
try:
# Read current config
config = {}
config = tomlkit.document()
if os.path.exists(data.CONFIG_FILE):
with open(data.CONFIG_FILE, "r") as f:
config = json.load(f)
config = tomlkit.load(f)
# Update current scheme
config["current_scheme"] = scheme
# Write back to config file
with open(data.CONFIG_FILE, "w") as f:
json.dump(config, f, indent=4)
tomlkit.dump(config, f)
except Exception as e:
logger.error(f"Error saving current scheme to config: {e}")
+13 -11
View File
@@ -1,26 +1,28 @@
from fabric.utils import logger
import json
import os
from typing import Any, Dict, List
import tomlkit
from fabric.utils import get_relative_path
class TriggerConfig:
def __init__(self, config_path: str = None):
if config_path is None:
config_path = get_relative_path("../../../config/launcher.json")
config_path = get_relative_path("../../../config/launcher.toml")
self.config_path = config_path
# Load configuration from JSON file
config = {"launcher_config": {}, "settings": {}}
if os.path.exists(config_path):
try:
with open(config_path, "r", encoding="utf-8") as f:
config = json.load(f)
except Exception as e:
logger.error(f"Error loading trigger config: {e}")
try:
with open(config_path, "r") as f:
data = tomlkit.load(f)
config["launcher_config"] = dict(data.get("launcher_config", {}))
config["settings"] = dict(data.get("settings", {}))
except FileNotFoundError:
pass
except Exception as e:
from fabric.utils import logger
logger.error(f"Error loading trigger config: {e}")
self.config = config
self.launcher_config = self.config.get("launcher_config", {})
+241
View File
@@ -0,0 +1,241 @@
from pathlib import Path
import tomlkit
from fabric.utils import GLib, logger
from fabric.widgets.box import Box
from fabric.widgets.button import Button
from fabric.widgets.label import Label
from gi.repository import Gio
from shared.window.applet_window import AppletWindow
from utils.functions import run_command, thread
from utils.utils import setup_cursor_hover, svg_file
MODS_CONFIG_PATH = (
Path(__file__).resolve().parent.parent.parent.parent.parent / "config" / "mods.toml"
)
RELOAD_DELAY_MS = 200
def dropdown_divider():
return Box(
children=[Box(name="dropdown-divider", h_expand=True)],
name="dropdown-divider-box",
h_align="fill",
h_expand=True,
)
class ModPopup(AppletWindow):
def __init__(
self, mod_name, options, parent_window, pointing_to, source_button, **kwargs
):
self._mod_name = mod_name
self._source_button = source_button
option_widgets = []
for opt in sorted(options, key=lambda o: o.get("order", 99)):
if opt.get("divider"):
option_widgets.append(dropdown_divider())
btn = Button(
name="dropdown-option",
child=Label(label=opt["label"], name="dropdown-option-label"),
h_align="fill",
h_expand=True,
)
cmd = opt["on-clicked"]
btn.connect("clicked", lambda *_, c=cmd: self._run_command(c))
setup_cursor_hover(btn, "pointer")
option_widgets.append(btn)
options_box = Box(
name="dropdown-options",
orientation="vertical",
spacing=0,
children=option_widgets,
)
main_box = Box(name="dropdown-menu", children=[options_box])
super().__init__(
parent=parent_window,
pointing_to=pointing_to,
name="dropdown-menu",
title="modus-dropdown",
layer="overlay",
visible=False,
**kwargs,
)
self.children = [main_box]
self.connect("notify::visible", self._on_visible_changed)
def _on_visible_changed(self, *_):
if self.get_visible():
self._source_button.add_style_class("active")
else:
self._source_button.remove_style_class("active")
def _run_command(self, cmd):
self.close_applet()
thread(run_command, cmd.split(), timeout=30)
def toggle(self, pointing_to=None):
if pointing_to:
self.set_pointing_to(pointing_to)
super().toggle()
class CustomMods(Box):
def __init__(self, parent_window, **kwargs):
self._parent = parent_window
self._monitors = []
self._reload_pending = False
self._mods_data = {}
self._mod_buttons = {}
self._mod_popups = {}
super().__init__(name="custom-mods", orientation="h", spacing=0, **kwargs)
self._load_mods()
self._setup_monitor()
def _load_mods(self):
try:
if not MODS_CONFIG_PATH.exists():
return
with open(MODS_CONFIG_PATH) as f:
data = tomlkit.load(f)
except Exception as e:
logger.error(f"[CustomMods] Failed to parse {MODS_CONFIG_PATH}: {e}")
return
mods = {}
for mod_name, val in data.get("Mods", {}).items():
icon = val.get("icon", "")
icon_size = val.get("icon-size", 18)
order = val.get("order", 99)
on_clicked = val.get("on-clicked")
options = val.get("options", [])
mods[mod_name] = {
"name": mod_name,
"icon": icon,
"icon_size": icon_size,
"order": order,
"on-clicked": on_clicked,
"options": options,
}
self._rebuild(mods)
def _on_btn_press(self, btn, event, mod):
popup = self._mod_popups.get(mod["name"])
if event.button == 3:
if popup:
popup.toggle(pointing_to=btn)
return True
if event.button == 1:
if popup and popup._is_open:
popup.toggle()
return True
cmd = mod.get("on-clicked")
if cmd:
thread(run_command, cmd.split(), timeout=30)
return True
if popup:
popup.toggle(pointing_to=btn)
return True
return False
def _rebuild(self, mods):
for child in list(self.get_children()):
self.remove(child)
for popup in self._mod_popups.values():
if popup:
popup.destroy()
self._mod_popups.clear()
self._mod_buttons.clear()
self._mods_data.clear()
sorted_mods = sorted(mods.values(), key=lambda m: m.get("order", 99))
for mod in sorted_mods:
icon_widget = svg_file(mod["icon"], size=mod["icon_size"])
btn = Button(name="global-menu", child=icon_widget)
setup_cursor_hover(btn, "pointer")
popup = None
if mod["options"]:
popup = ModPopup(
mod_name=mod["name"],
options=mod["options"],
parent_window=self._parent,
pointing_to=btn,
source_button=btn,
)
btn.connect(
"button-press-event",
lambda b, e, m=mod: self._on_btn_press(b, e, m),
)
self.add(btn)
self._mod_buttons[mod["name"]] = btn
self._mod_popups[mod["name"]] = popup
self._mods_data[mod["name"]] = mod
self.show_all()
def _setup_monitor(self):
try:
for path in [MODS_CONFIG_PATH, MODS_CONFIG_PATH.parent]:
if not path.exists():
continue
gio_file = Gio.File.new_for_path(str(path))
monitor = gio_file.monitor_file(Gio.FileMonitorFlags.NONE, None)
monitor.connect("changed", self._on_file_changed)
self._monitors.append(monitor)
except Exception as e:
logger.error(f"[CustomMods] Failed to setup monitor: {e}")
def _on_file_changed(self, monitor, gfile, other_file, event_type):
if event_type == Gio.FileMonitorEvent.DELETED and gfile.get_path() == str(
MODS_CONFIG_PATH
):
self._schedule_reload()
return
valid_events = [
Gio.FileMonitorEvent.CHANGES_DONE_HINT,
Gio.FileMonitorEvent.CHANGED,
Gio.FileMonitorEvent.CREATED,
]
if event_type in valid_events and not self._reload_pending:
path = gfile.get_path()
if path and path != str(MODS_CONFIG_PATH):
return
self._schedule_reload()
def _schedule_reload(self):
self._reload_pending = True
GLib.timeout_add(RELOAD_DELAY_MS, self._reload_config)
def _reload_config(self):
self._reload_pending = False
self._load_mods()
return False
def destroy(self):
for monitor in self._monitors:
monitor.cancel()
for popup in self._mod_popups.values():
if popup:
popup.destroy()
self._mod_popups.clear()
self._mod_buttons.clear()
self._mods_data.clear()
super().destroy()
@@ -0,0 +1,20 @@
from fabric.widgets.button import Button
from services.google_lens import GoogleLens
from utils.functions import thread
from utils.utils import setup_cursor_hover, svg_file
class GoogleLensIndicator(Button):
def __init__(self, **kwargs):
super().__init__(name="panel-button", **kwargs)
self._lens = GoogleLens.get_initial()
self.icon = svg_file("google-lens.svg", size=16)
self.add(self.icon)
setup_cursor_hover(self, "pointer")
self.connect("clicked", self.on_clicked)
def on_clicked(self, *_):
thread(self._lens.search)
+12
View File
@@ -15,6 +15,8 @@ from window.controlcenter.main import ModusControlCenter
from window.notification.notification_center import NotificationCenter
from window.panel.components.enhanced_system_tray import apply_enhanced_system_tray
from window.panel.components.globalmenu import GlobalMenu
from window.panel.components.custom_mods import CustomMods
from window.panel.components.google_lens_indicator import GoogleLensIndicator
from window.panel.components.indicators import (
BatteryIndicator,
BluetoothIndicator,
@@ -109,6 +111,8 @@ class Panel(Window):
)
setup_cursor_hover(self.datetime_btn, "pointer")
self.google_lens_indicator = GoogleLensIndicator()
self.custom_mods = CustomMods(parent_window=self)
self.workspace_indicator = WorkspaceIndicator()
self.recording_indicator = RecordingIndicator()
@@ -235,6 +239,10 @@ class Panel(Window):
# Right
right_children = []
if config_data.get("custom_mods", True):
right_children.append(self.custom_mods)
if config_data.get("google_lens", True):
right_children.append(self.google_lens_indicator)
if config_data.get("workspace_indicator", True):
right_children.append(self.workspace_indicator)
@@ -268,6 +276,8 @@ class Panel(Window):
keys = {
"imac_button",
"global_menu",
"google_lens",
"custom_mods",
"workspace_indicator",
"systray",
"battery",
@@ -328,6 +338,8 @@ class Panel(Window):
# Destroy components
for component in [
self.globalmenu,
self.google_lens_indicator,
self.custom_mods,
self.workspace_indicator,
self.recording_indicator,
self.indicators,