feat(src/window): switch configuration from JSON to TOML and refactor pinned apps handling
This commit is contained in:
7 files changed
+333
-41
No files matched your search
+34
-6
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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", {})
|
||||
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user