diff --git a/config/profiles.py b/config/profiles.py new file mode 100644 index 0000000..d935ca3 --- /dev/null +++ b/config/profiles.py @@ -0,0 +1,93 @@ +# v3.0.2 profiles.py +from dataclasses import dataclass +from typing import Final + + +@dataclass(frozen=True, slots=True) +class ThemePalette: + name: str + background: str + background_inactive: str + border_active: str + border_inactive: str + border_dragging: str + foreground: str + selection: str + code_background: str + shadow: str + + +THEMES: Final[list[ThemePalette]] = [ + ThemePalette( + name="macos_blue", + background="rgba(15,23,42,0.26)", + background_inactive="rgba(15,23,42,0.18)", + border_active="rgba(125,211,252,0.34)", + border_inactive="rgba(255,255,255,0.10)", + border_dragging="rgba(125,211,252,0.52)", + foreground="rgba(248,250,252,0.96)", + selection="rgba(56,189,248,0.30)", + code_background="rgba(255,255,255,0.08)", + shadow="rgba(15,23,42,0.30)", + ), + ThemePalette( + name="ruby", + background="rgba(69,10,10,0.24)", + background_inactive="rgba(69,10,10,0.16)", + border_active="rgba(251,113,133,0.34)", + border_inactive="rgba(255,255,255,0.10)", + border_dragging="rgba(251,113,133,0.52)", + foreground="rgba(255,241,242,0.96)", + selection="rgba(244,63,94,0.30)", + code_background="rgba(255,255,255,0.08)", + shadow="rgba(69,10,10,0.30)", + ), + ThemePalette( + name="yellow", + background="rgba(113,63,18,0.24)", + background_inactive="rgba(113,63,18,0.16)", + border_active="rgba(250,204,21,0.36)", + border_inactive="rgba(255,255,255,0.10)", + border_dragging="rgba(250,204,21,0.56)", + foreground="rgba(254,252,232,0.96)", + selection="rgba(250,204,21,0.28)", + code_background="rgba(255,255,255,0.08)", + shadow="rgba(113,63,18,0.28)", + ), + ThemePalette( + name="jade", + background="rgba(2,44,34,0.24)", + background_inactive="rgba(2,44,34,0.16)", + border_active="rgba(52,211,153,0.34)", + border_inactive="rgba(255,255,255,0.10)", + border_dragging="rgba(52,211,153,0.52)", + foreground="rgba(236,253,245,0.96)", + selection="rgba(16,185,129,0.28)", + code_background="rgba(255,255,255,0.08)", + shadow="rgba(2,44,34,0.30)", + ), + ThemePalette( + name="light", + background="rgba(255,255,255,0.34)", + background_inactive="rgba(255,255,255,0.24)", + border_active="rgba(148,163,184,0.32)", + border_inactive="rgba(148,163,184,0.18)", + border_dragging="rgba(96,165,250,0.42)", + foreground="rgba(15,23,42,0.96)", + selection="rgba(96,165,250,0.22)", + code_background="rgba(15,23,42,0.06)", + shadow="rgba(15,23,42,0.12)", + ), + ThemePalette( + name="dark", + background="rgba(10,10,10,0.34)", + background_inactive="rgba(10,10,10,0.24)", + border_active="rgba(255,255,255,0.16)", + border_inactive="rgba(255,255,255,0.08)", + border_dragging="rgba(255,255,255,0.24)", + foreground="rgba(250,250,250,0.96)", + selection="rgba(255,255,255,0.14)", + code_background="rgba(255,255,255,0.06)", + shadow="rgba(0,0,0,0.42)", + ), +] \ No newline at end of file diff --git a/config/settings.py b/config/settings.py new file mode 100644 index 0000000..1c8bb39 --- /dev/null +++ b/config/settings.py @@ -0,0 +1,357 @@ +# v3.1.1 settings.py +import json +from dataclasses import dataclass +from pathlib import Path +from typing import Final + + +APP_DIR: Final[Path] = ( + Path(__file__).resolve().parent.parent +) + +SETTINGS_FILE: Final[Path] = ( + APP_DIR / "settings.json" +) + + +DEFAULT_SETTINGS: Final[dict] = { + "active_profile": "default", + "profiles": { + "default": { + "theme": "macos_blue", + "window": { + "width": 260, + "height": 300, + "min_width": 220, + "min_height": 140, + "layout_margin": 4, + "layout_spacing": 4, + "drag_handle_height": 28, + "resize_grip_size": 16, + "corner_radius": 16, + "startup_stabilize_ms": 80, + }, + "appearance": { + "enable_mica": True, + "enable_acrylic": True, + "enable_glass_blur": True, + "enable_animations": True, + "enable_shadow": True, + "opacity_active": 1.0, + "opacity_inactive": 0.96, + "opacity_dragging": 0.92, + "focus_animation_ms": 180, + }, + "editor": { + "font_family": [ + "Inter", + "SF Pro Display", + "Helvetica", + "Arial", + "system-ui", + ], + "font_size": 13, + "preview_delay_ms": 700, + "save_debounce_ms": 500, + "max_note_size": 2_000_000, + "enable_markdown_preview": True, + "enable_clickable_links": True, + "enable_antialiasing": True, + "enable_spellcheck": False, + "padding": 8, + }, + "behavior": { + "enable_tray": True, + "enable_persistence": True, + "enable_window_memory": True, + "enable_auto_restore": True, + "high_contrast_inactive": False, + }, + }, + "light": { + "theme": "light", + "window": { + "width": 260, + "height": 300, + "min_width": 220, + "min_height": 140, + "layout_margin": 4, + "layout_spacing": 4, + "drag_handle_height": 28, + "resize_grip_size": 16, + "corner_radius": 16, + "startup_stabilize_ms": 80, + }, + "appearance": { + "enable_mica": False, + "enable_acrylic": False, + "enable_glass_blur": False, + "enable_animations": True, + "enable_shadow": False, + "opacity_active": 1.0, + "opacity_inactive": 0.98, + "opacity_dragging": 0.96, + "focus_animation_ms": 120, + }, + "editor": { + "font_family": [ + "Inter", + "Segoe UI", + "Arial", + "system-ui", + ], + "font_size": 13, + "preview_delay_ms": 500, + "save_debounce_ms": 400, + "max_note_size": 2_000_000, + "enable_markdown_preview": True, + "enable_clickable_links": True, + "enable_antialiasing": True, + "enable_spellcheck": False, + "padding": 8, + }, + "behavior": { + "enable_tray": True, + "enable_persistence": True, + "enable_window_memory": True, + "enable_auto_restore": True, + "high_contrast_inactive": False, + }, + }, + "accessibility": { + "theme": "dark", + "window": { + "width": 320, + "height": 380, + "min_width": 260, + "min_height": 180, + "layout_margin": 6, + "layout_spacing": 6, + "drag_handle_height": 32, + "resize_grip_size": 18, + "corner_radius": 18, + "startup_stabilize_ms": 80, + }, + "appearance": { + "enable_mica": False, + "enable_acrylic": False, + "enable_glass_blur": False, + "enable_animations": False, + "enable_shadow": False, + "opacity_active": 1.0, + "opacity_inactive": 1.0, + "opacity_dragging": 1.0, + "focus_animation_ms": 0, + }, + "editor": { + "font_family": [ + "Inter", + "Segoe UI", + "Arial", + "system-ui", + ], + "font_size": 15, + "preview_delay_ms": 300, + "save_debounce_ms": 300, + "max_note_size": 2_000_000, + "enable_markdown_preview": True, + "enable_clickable_links": True, + "enable_antialiasing": True, + "enable_spellcheck": False, + "padding": 10, + }, + "behavior": { + "enable_tray": True, + "enable_persistence": True, + "enable_window_memory": True, + "enable_auto_restore": True, + "high_contrast_inactive": True, + }, + }, + }, +} + + +@dataclass(slots=True) +class SettingsManager: + settings: dict + + @classmethod + def load(cls) -> "SettingsManager": + if not SETTINGS_FILE.exists(): + SETTINGS_FILE.write_text( + json.dumps( + DEFAULT_SETTINGS, + indent=2, + ), + encoding="utf-8", + ) + + return cls( + DEFAULT_SETTINGS + ) + + try: + raw = SETTINGS_FILE.read_text( + encoding="utf-8", + ) + + data = json.loads(raw) + + merged = cls.merge_defaults( + DEFAULT_SETTINGS, + data, + ) + + return cls(merged) + + except Exception: + return cls( + DEFAULT_SETTINGS + ) + + @staticmethod + def merge_defaults( + defaults: dict, + current: dict, + ) -> dict: + result = {} + + for key, value in defaults.items(): + result[key] = value + + for key, value in current.items(): + if ( + key in result + and isinstance( + result[key], + dict, + ) + and isinstance( + value, + dict, + ) + ): + result[key] = ( + SettingsManager.merge_defaults( + result[key], + value, + ) + ) + + else: + result[key] = value + + return result + + def save(self) -> None: + SETTINGS_FILE.write_text( + json.dumps( + self.settings, + indent=2, + ), + encoding="utf-8", + ) + + @property + def active_profile_name( + self, + ) -> str: + return self.settings.get( + "active_profile", + "default", + ) + + @property + def profiles(self) -> dict: + return self.settings.get( + "profiles", + {}, + ) + + @property + def profile(self) -> dict: + active = ( + self.active_profile_name + ) + + profiles = self.profiles + + if active not in profiles: + return profiles[ + "default" + ] + + return profiles[active] + + def set_active_profile( + self, + profile_name: str, + ) -> None: + if ( + profile_name + not in self.profiles + ): + return + + self.settings[ + "active_profile" + ] = profile_name + + self.save() + + def reload(self) -> None: + loaded = ( + SettingsManager.load() + ) + + self.settings = ( + loaded.settings + ) + + def reset(self) -> None: + self.settings = ( + DEFAULT_SETTINGS + ) + + self.save() + + def create_profile( + self, + name: str, + data: dict, + ) -> None: + self.settings[ + "profiles" + ][name] = data + + self.save() + + def delete_profile( + self, + name: str, + ) -> None: + if name == "default": + return + + if ( + name + not in self.settings[ + "profiles" + ] + ): + return + + del self.settings[ + "profiles" + ][name] + + if ( + self.active_profile_name + == name + ): + self.settings[ + "active_profile" + ] = "default" + + self.save() \ No newline at end of file