#!/usr/bin/env python3 import os, sys, argparse, re def get_app_config_dirs(app_names): """ Dynamically discover all config and data directories for any native or Flatpak application. Checks ~/.config/, ~/.local/share/, ~/.var/app/* (Flatpak sandboxes), and system paths. """ if isinstance(app_names, str): app_names = [app_names] dirs = set() home = os.path.expanduser('~') for name in app_names: c1 = os.path.join(home, '.config', name) c2 = os.path.join(home, '.local', 'share', name) if os.path.exists(c1): dirs.add(c1) if os.path.exists(c2): dirs.add(c2) var_app = os.path.join(home, '.var', 'app') if os.path.exists(var_app): try: for entry in os.scandir(var_app): if not entry.is_dir(): continue sub_locations = [ os.path.join(entry.path, 'config'), os.path.join(entry.path, 'data'), os.path.join(entry.path, '.config'), os.path.join(entry.path, '.local', 'share') ] for sub_loc in sub_locations: if not os.path.exists(sub_loc): continue for name in app_names: target = os.path.join(sub_loc, name) if os.path.exists(target): dirs.add(target) try: for item in os.scandir(sub_loc): if any(n.lower() in item.name.lower() for n in app_names): dirs.add(item.path) except Exception: pass except Exception: pass return list(dirs) def restart_app_if_running(name, search_patterns, launch_commands): """ Gracefully restarts a native or Flatpak application if it is currently running. """ import subprocess, time is_running = False for pat in search_patterns: res = subprocess.run(["pgrep", "-i", "-f", pat], capture_output=True, text=True) if res.returncode == 0: is_running = True break if not is_running: return # Gracefully terminate running instances for pat in search_patterns: subprocess.run(["pkill", "-15", "-i", "-f", pat], capture_output=True) # Wait for process exit & lock release buffer for _ in range(30): still_running = False for pat in search_patterns: check = subprocess.run(["pgrep", "-i", "-f", pat], capture_output=True, text=True) if check.returncode == 0: still_running = True break if not still_running: time.sleep(0.5) break time.sleep(0.1) # Launch using first available working launch command (Flatpak -> Desktop launcher -> Native binary) for cmd in launch_commands: try: if cmd.startswith("flatpak run"): app_id = cmd.split()[-1] check_fp = subprocess.run(["flatpak", "info", app_id], capture_output=True, text=True) if check_fp.returncode == 0: subprocess.Popen(cmd, shell=True, start_new_session=True) return elif cmd.startswith("gtk-launch"): app_id = cmd.split()[-1] res_gtk = subprocess.run(["gtk-launch", app_id], capture_output=True) if res_gtk.returncode == 0: return else: bin_name = cmd.split()[0] res_bin = subprocess.run(f"command -v {bin_name}", capture_output=True, shell=True) if res_bin.returncode == 0: subprocess.Popen(cmd, shell=True, start_new_session=True) return except Exception: pass # Generic shell fallback launch if launch_commands: raw_cmd = " || ".join(launch_commands) subprocess.Popen(f"sh -c '{raw_cmd} &'", shell=True, start_new_session=True) def sync_gtk(bg, surface, current_line, fg, accent, is_dark): css_content = f"""/* Auto-generated by Quickshell Theme Engine */ @define-color window_bg_color {bg}; @define-color window_fg_color {fg}; @define-color view_bg_color {surface}; @define-color view_fg_color {fg}; @define-color headerbar_bg_color {bg}; @define-color headerbar_fg_color {fg}; @define-color card_bg_color {surface}; @define-color card_fg_color {fg}; @define-color dialog_bg_color {bg}; @define-color dialog_fg_color {fg}; @define-color popover_bg_color {surface}; @define-color popover_fg_color {fg}; @define-color accent_color {accent}; @define-color accent_bg_color {accent}; @define-color accent_fg_color {'#ffffff' if is_dark else '#000000'}; """ for dir_path in [os.path.expanduser('~/.config/gtk-3.0'), os.path.expanduser('~/.config/gtk-4.0')]: os.makedirs(dir_path, exist_ok=True) css_file = os.path.join(dir_path, 'gtk.css') with open(css_file, 'w') as f: f.write(css_content) def sync_alacritty(bg, surface, current_line, fg, accent, sub_accent): import glob, os, re alacritty_dirs = get_app_config_dirs(['alacritty', 'Alacritty']) for fb in [ os.path.expanduser('~/.config/alacritty'), os.path.expanduser('~/.var/app/io.github.alacritty.Alacritty/config/alacritty'), os.path.expanduser('~/.var/app/org.alacritty.Alacritty/config/alacritty') ]: if fb not in alacritty_dirs: alacritty_dirs.append(fb) colors_block = f"""[colors.primary] background = "{bg}" foreground = "{fg}" [colors.cursor] text = "{bg}" cursor = "{accent}" [colors.selection] text = "{bg}" background = "{accent}" [colors.normal] black = "{bg}" red = "#ff9580" green = "#8aff80" yellow = "#ffff80" blue = "{accent}" magenta = "{sub_accent}" cyan = "#80ffea" white = "{fg}" [colors.bright] black = "{current_line}" red = "#ff9580" green = "#8aff80" yellow = "#ffff80" blue = "{accent}" magenta = "{sub_accent}" cyan = "#80ffea" white = "{fg}" """ for alacritty_dir in alacritty_dirs: try: os.makedirs(alacritty_dir, exist_ok=True) colors_file = os.path.join(alacritty_dir, 'colors.toml') toml_file = os.path.join(alacritty_dir, 'alacritty.toml') with open(colors_file, 'w', encoding='utf-8') as f: f.write(colors_block) f.flush() os.fsync(f.fileno()) if os.path.exists(toml_file): with open(toml_file, 'r', encoding='utf-8') as f: content = f.read() lines = content.splitlines() cleaned_lines = [] in_colors = False for line in lines: stripped = line.strip() if stripped.startswith('[colors') or stripped.startswith('[[colors'): in_colors = True continue elif in_colors and stripped.startswith('[') and not stripped.startswith('[colors') and not stripped.startswith('[[colors'): in_colors = False if not in_colors: cleaned_lines.append(line) content = '\n'.join(cleaned_lines) if 'colors.toml' not in content: import_line = 'import = ["colors.toml"]\n' if '[general]' in content: content = content.replace('[general]', '[general]\n' + import_line) else: content = '[general]\n' + import_line + '\n' + content with open(toml_file, 'w', encoding='utf-8') as f: f.write(content.strip() + '\n') else: with open(toml_file, 'w', encoding='utf-8') as f: f.write('[general]\nimport = ["colors.toml"]\nlive_config_reload = true\n') except Exception: pass except Exception: pass try: os.utime(colors_file, None) if os.path.exists(toml_file): os.utime(toml_file, None) except Exception: pass # Send live OSC 10/11 color update sequences to all active pseudo-terminals osc_sequence = f"\033]10;{fg}\007\033]11;{bg}\007".encode('utf-8') for pts in glob.glob('/dev/pts/*'): try: if os.path.isabs(pts) and os.access(pts, os.W_OK): fd = os.open(pts, os.O_WRONLY | os.O_NOCTTY) os.write(fd, osc_sequence) os.close(fd) except Exception: pass def sync_discord(bg, surface, current_line, fg, accent, sub_accent, is_dark): css_content = f"""/* Auto-generated by Quickshell Theme Engine (Catppuccin Discord Architecture) */ .visual-refresh.theme-dark, .visual-refresh .theme-dark, .theme-dark, .theme-light, :root, html, body {{ --brand-500: {accent} !important; --brand-530: {accent} !important; --brand-560: {accent} !important; --brand-600: {accent} !important; --brand-630: {accent} !important; --brand-700: {accent} !important; --brand-experiment: {accent} !important; --accent-color: {accent} !important; --__header-bar-background: {surface} !important; --text-normal: {fg} !important; --text-default: {fg} !important; --text-muted: {fg} !important; --text-link: {accent} !important; --text-brand: {accent} !important; --text-strong: {fg} !important; --text-subtle: {fg} !important; --text-primary: {fg} !important; --text-secondary: {fg} !important; --header-primary: {fg} !important; --header-secondary: {fg} !important; --app-frame-background: {bg} !important; --background-secondary-alt: {surface} !important; --background-accent: {current_line} !important; --background-surface-highest: {current_line} !important; --background-surface-higher: {surface} !important; --background-surface-high: {surface} !important; --background-base-lowest: {bg} !important; --background-base-lower: {surface} !important; --background-base-low: {surface} !important; --bg-surface-raised: {surface} !important; --background-gradient-highest: {surface} !important; --home-background: {bg} !important; --chat-background: {bg} !important; --chat-background-default: {bg} !important; --chat-border: {bg} !important; --chat-text-muted: {fg} !important; --border-muted: {current_line} !important; --border-strong: {surface} !important; --border-normal: {bg} !important; --border-subtle: {bg} !important; --custom-channel-members-bg: {surface} !important; --card-background-default: {surface} !important; --modal-background: {surface} !important; --modal-footer-background: {bg} !important; --input-background-default: {bg} !important; --input-text-default: {fg} !important; --input-placeholder-text-default: {fg} !important; --background-code: {surface} !important; --channels-default: {fg} !important; --channel-icon: {fg} !important; --channeltextarea-background: {surface} !important; --icon-muted: {fg} !important; --icon-default: {fg} !important; --icon-strong: {fg} !important; --icon-subtle: {fg} !important; --icon-primary: {fg} !important; --icon-secondary: {fg} !important; --interactive-normal: {fg} !important; --interactive-hover: {accent} !important; --interactive-active: {accent} !important; --interactive-muted: {fg} !important; --interactive-icon-default: {fg} !important; --interactive-icon-hover: {accent} !important; --interactive-icon-active: {accent} !important; --interactive-text-default: {fg} !important; --interactive-text-hover: {accent} !important; --interactive-text-active: {accent} !important; --user-profile-overlay-background: {surface} !important; }} """ quick_css = os.path.expanduser('~/.config/equibop/settings/quickCss.css') if os.path.exists(os.path.dirname(quick_css)): with open(quick_css, 'w') as f: f.write(css_content) for client_dir in [ os.path.expanduser('~/.config/vesktop/themes'), os.path.expanduser('~/.config/Vencord/themes'), os.path.expanduser('~/.config/equibop/themes'), os.path.expanduser('~/.config/equicord/themes'), os.path.expanduser('~/.config/discord/themes'), os.path.expanduser('~/.var/app/dev.vencord.Vesktop/config/vesktop/themes'), os.path.expanduser('~/.var/app/dev.vencord.Vesktop/config/Vencord/themes'), os.path.expanduser('~/.var/app/com.discordapp.Discord/config/discord/themes'), os.path.expanduser('~/.var/app/com.discordapp.Discord/config/Vencord/themes') ]: try: os.makedirs(client_dir, exist_ok=True) css_file = os.path.join(client_dir, 'quickshell-theme.css') with open(css_file, 'w', encoding='utf-8') as f: f.write(css_content) except Exception: pass def sync_vscode(bg, surface, current_line, fg, accent, sub_accent, is_dark): import json colors_dict = { "editor.background": bg, "editor.foreground": fg, "sideBar.background": surface, "sideBar.foreground": fg, "activityBar.background": bg, "activityBar.foreground": accent, "statusBar.background": bg, "statusBar.foreground": fg, "titleBar.activeBackground": bg, "titleBar.activeForeground": fg, "tab.activeBackground": surface, "tab.activeForeground": fg, "tab.inactiveBackground": bg, "tab.inactiveForeground": current_line, "panel.background": surface, "terminal.background": bg, "terminal.foreground": fg } token_colors = { "comments": "#7970a9" if is_dark else "#8c8c8c", "strings": "#8aff80" if is_dark else "#22863a", "keywords": accent, "functions": sub_accent, "variables": fg, "numbers": "#ffca80" if is_dark else "#b08800", "types": sub_accent, "textMateRules": [ { "scope": ["keyword", "storage.type", "storage.modifier"], "settings": {"foreground": accent} }, { "scope": ["entity.name.function", "support.function"], "settings": {"foreground": sub_accent} }, { "scope": ["string", "string.quoted"], "settings": {"foreground": "#8aff80" if is_dark else "#22863a"} }, { "scope": ["comment"], "settings": {"foreground": "#7970a9" if is_dark else "#8c8c8c", "fontStyle": "italic"} }, { "scope": ["constant.numeric", "constant.language"], "settings": {"foreground": "#ffca80" if is_dark else "#b08800"} }, { "scope": ["variable", "variable.other"], "settings": {"foreground": fg} } ] } for base_dir in [os.path.expanduser('~/.config/Code/User'), os.path.expanduser('~/.config/Code - OSS/User'), os.path.expanduser('~/.config/VSCodium/User')]: if os.path.exists(os.path.dirname(base_dir)): os.makedirs(base_dir, exist_ok=True) settings_path = os.path.join(base_dir, 'settings.json') data = {} if os.path.exists(settings_path): try: with open(settings_path, 'r') as f: raw = f.read() cleaned = re.sub(r'//.*', '', raw) data = json.loads(cleaned) if cleaned.strip() else {} except Exception: data = {} data["workbench.colorCustomizations"] = colors_dict data["editor.tokenColorCustomizations"] = token_colors data["workbench.colorTheme"] = "Default Dark Modern" if is_dark else "Default Light Modern" try: with open(settings_path, 'w') as f: json.dump(data, f, indent=4) except Exception: pass def sync_zen(bg, surface, current_line, fg, accent, sub_accent, is_dark): import subprocess chrome_css = f"""/* Auto-generated by Quickshell Theme Engine */ :root {{ --zen-main-browser-background: {bg} !important; --zen-workspace-background: {bg} !important; --zen-colors-bg: {bg} !important; --zen-colors-bg-alt: {bg} !important; --zen-colors-tertiary: {surface} !important; --zen-colors-primary: {accent} !important; --zen-colors-secondary: {sub_accent} !important; --zen-primary-color: {accent} !important; --zen-theme-accent: {accent} !important; --zen-colors-input-bg: {surface} !important; --zen-colors-text: {fg} !important; --zen-colors-border: {bg} !important; --zen-borders-color: {bg} !important; --zen-border-color: {bg} !important; --zen-sidebar-border: {bg} !important; --zen-sidebar-bg: {bg} !important; --zen-sidebar-background: {bg} !important; --zen-sidebar-top-buttons-bg: {bg} !important; --zen-element-separation: 0px !important; --zen-main-browser-margin: 0px !important; --zen-main-browser-padding: 0px !important; --zen-user-border-radius: 0px !important; --lwt-accent-color: {bg} !important; --lwt-text-color: {fg} !important; --lwt-sidebar-background: {bg} !important; --lwt-sidebar-text-color: {fg} !important; --toolbar-bgcolor: {bg} !important; --toolbar-color: {fg} !important; --toolbar-field-background-color: {surface} !important; --toolbar-field-color: {fg} !important; --toolbar-field-focus-background-color: {surface} !important; --toolbarbutton-hover-background: {surface} !important; --arrowpanel-background: {surface} !important; --arrowpanel-color: {fg} !important; --tab-selected-bgcolor: {current_line} !important; --tab-selected-bg: {current_line} !important; --urlbar-box-background: {surface} !important; --urlbar-background: {surface} !important; --chrome-content-separator-color: {bg} !important; --toolbox-border-bottom-color: {bg} !important; }} *, *::before, *::after {{ border-color: {bg} !important; box-shadow: none !important; outline: none !important; }} #main-window, #navigator-toolbox, #nav-bar, #PersonalToolbar, #urlbar-background, #sidebar-box, #sidebar-header, #sidebar-splitter, .sidebar-splitter, #TabsToolbar, .browserStack, #appcontent, #tabbrowser-tabpanels, #zen-main-app-wrapper, #browser, .browserContainer, browser[type="content"], deck, tabbox, vbox, hbox, #zen-sidebar-top-buttons, #zen-sidebar-bottom-buttons, #zen-sidebar-icons-wrapper, #zen-current-workspace-indicator, .sidebar-panel, #zen-sidebar-box, #zen-appcontent-navbar-container {{ background: {bg} !important; background-color: {bg} !important; color: {fg} !important; border: none !important; border-color: {bg} !important; box-shadow: none !important; outline: none !important; }} /* Active / Selected Tab Distinct Styling in Zen Browser */ .tabbrowser-tab[selected], .tabbrowser-tab[selected] .tab-background, .tab-background[selected], #tabbrowser-tabs .tabbrowser-tab[selected], tab[selected="true"] {{ background: {surface} !important; background-color: {surface} !important; color: {fg} !important; border-left: none !important; border-radius: 10px !important; }} /* Zen Browser Curved Active Line Accent Color Sync */ .tab-line[selected], .tab-line[selected="true"], .tabbrowser-tab[selected] .tab-line, .tabbrowser-tab[selected]::before, .tabbrowser-tab[selected] .tab-context-line {{ background: {accent} !important; background-color: {accent} !important; color: {accent} !important; }} /* Ensure Tab Inner Content, Label & Text Have Seamless Transparent Background */ .tab-content, .tab-label-container, .tab-label, .tab-text, .tab-icon-stack, .tab-stack {{ background: transparent !important; background-color: transparent !important; }} /* Hovered & Default Tab Background Rounded Corners */ .tabbrowser-tab, .tabbrowser-tab .tab-background, .tab-background {{ border-radius: 10px !important; }} /* Hovered Tab Styling in Zen Browser */ .tabbrowser-tab:hover:not([selected]), .tabbrowser-tab:hover:not([selected]) .tab-background {{ background: {current_line} !important; background-color: {current_line} !important; border-radius: 10px !important; }} #sidebar-splitter, .sidebar-splitter {{ width: 0px !important; min-width: 0px !important; background: {bg} !important; border: none !important; }} /* Sleek & Elegant Floating URL Bar & Address Dropdown */ #urlbar {{ border: none !important; box-shadow: none !important; }} #urlbar-background {{ background: {surface} !important; background-color: {surface} !important; border-radius: 12px !important; border: 1px solid {current_line} !important; }} #urlbar[breakout][breakout-extend] > #urlbar-background {{ background: {surface} !important; background-color: {surface} !important; border-radius: 14px !important; border: 1px solid {accent} !important; box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45) !important; }} #urlbar-input-container {{ background: transparent !important; border: none !important; padding-left: 8px !important; padding-right: 8px !important; }} /* URL Bar Dropdown Results Container */ .urlbarView, .urlbarView-body-outer, .urlbarView-body-inner, #urlbar-results {{ background: transparent !important; background-color: transparent !important; border: none !important; padding: 6px !important; }} /* Clean & Modern URL Result Rows */ .urlbarView-row, .urlbarView-row-inner {{ border-radius: 8px !important; margin-top: 2px !important; margin-bottom: 2px !important; padding: 6px 10px !important; border: none !important; background: transparent !important; }} .urlbarView-row[selected], .urlbarView-row:hover, .urlbarView-row[selected] .urlbarView-row-inner, .urlbarView-row:hover .urlbarView-row-inner {{ background: {current_line} !important; background-color: {current_line} !important; }} .urlbarView-row[selected] .urlbarView-title, .urlbarView-row:hover .urlbarView-title {{ color: {accent} !important; }} /* URL Title and Link Typography Styling */ .urlbarView-title {{ color: {fg} !important; font-weight: 500 !important; }} .urlbarView-secondary, .urlbarView-url {{ color: {sub_accent} !important; opacity: 0.85 !important; }} .urlbarView-action {{ color: {accent} !important; background: Qt.rgba(Theme.accent.r, Theme.accent.g, Theme.accent.b, 0.15) !important; border-radius: 6px !important; }} """ content_css = f"""/* Auto-generated Newtab & Content Theme */ @-moz-document url("about:home"), url("about:newtab"), url("about:blank"), url-prefix("about:") {{ body, #root, .container, .main, html {{ background: {bg} !important; background-color: {bg} !important; color: {fg} !important; }} }} """ pref_lines = [ 'user_pref("toolkit.legacyUserProfileCustomizations.stylesheets", true);\n', 'user_pref("svg.context-properties.content.enabled", true);\n', 'user_pref("browser.sessionstore.resume_from_crash", false);\n', 'user_pref("toolkit.startup.max_resumed_crashes", -1);\n' ] zen_bases = [ os.path.expanduser('~/.config/zen'), os.path.expanduser('~/.zen'), os.path.expanduser('~/.var/app/app.zen_browser.zen/.zen'), os.path.expanduser('~/.var/app/io.github.zen_browser.zen/.zen'), os.path.expanduser('~/.var/app/org.zen_browser.zen/.zen'), os.path.expanduser('~/.var/app/app.zen_browser.zen/config/zen'), os.path.expanduser('~/.var/app/io.github.zen_browser.zen/config/zen') ] for zen_base in zen_bases: if os.path.exists(zen_base): for profile in os.listdir(zen_base): profile_dir = os.path.join(zen_base, profile) if os.path.isdir(profile_dir) and ('.' in profile or 'default' in profile.lower()): # Ensure user.js enables stylesheets and disables crash dialogs user_js = os.path.join(profile_dir, 'user.js') js_content = "" if os.path.exists(user_js): with open(user_js, 'r') as f: js_content = f.read() for line in pref_lines: pref_key = line.split('"')[1] if pref_key not in js_content: with open(user_js, 'a') as f: f.write("\n" + line) # Write userChrome.css and userContent.css chrome_dir = os.path.join(profile_dir, 'chrome') os.makedirs(chrome_dir, exist_ok=True) try: with open(os.path.join(chrome_dir, 'userChrome.css'), 'w') as f: f.write(chrome_css) with open(os.path.join(chrome_dir, 'userContent.css'), 'w') as f: f.write(content_css) except Exception: pass restart_app_if_running( "zen", ["zen-browser", "zen-bin", "zen_browser", "app.zen_browser.zen", "io.github.zen_browser.zen"], [ "flatpak run app.zen_browser.zen", "flatpak run io.github.zen_browser.zen", "gtk-launch app.zen_browser.zen", "gtk-launch io.github.zen_browser.zen", "gtk-launch zen-browser", "zen-browser", "zen-bin", "zen" ] ) def hex_to_rgb(hex_str): try: h = hex_str.lstrip('#') if len(h) == 3: h = ''.join([c*2 for c in h]) r = int(h[0:2], 16) g = int(h[2:4], 16) b = int(h[4:6], 16) return f"rgb({r}, {g}, {b})" except Exception: return hex_str def slugify(name): import re s = re.sub(r'[^a-z0-9]+', '-', name.lower()).strip('-') return f"quickshell-{s}" if s else "quickshell" PREDEFINED_VARIANTS = [ {"name": "Zoey Pink", "isDark": False, "accent": "#ec4899", "bg": "#fff0f5", "surface": "#fbcfe8", "currentLine": "#f472b6", "fg": "#4a044e"}, {"name": "Zoey Night", "isDark": True, "accent": "#f472b6", "bg": "#251c2e", "surface": "#33263e", "currentLine": "#483659", "fg": "#f5e6f8"}, {"name": "Emo Zoey", "isDark": True, "accent": "#ff2a85", "bg": "#120914", "surface": "#1c0d20", "currentLine": "#32143a", "fg": "#f4d7f7"}, {"name": "Pro", "isDark": True, "accent": "#9580ff", "bg": "#22212c", "surface": "#2b2938", "currentLine": "#454158", "fg": "#f8f8f2"}, {"name": "Blade", "isDark": True, "accent": "#80ffea", "bg": "#212c2a", "surface": "#293835", "currentLine": "#415854", "fg": "#f8f8f2"}, {"name": "Buff", "isDark": True, "accent": "#ffca80", "bg": "#2a212c", "surface": "#352938", "currentLine": "#544158", "fg": "#f8f8f2"}, {"name": "Cyan", "isDark": True, "accent": "#80ffea", "bg": "#0b0d0f", "surface": "#181b1f", "currentLine": "#414d58", "fg": "#f8f8f2"}, {"name": "Lincoln", "isDark": True, "accent": "#ffff80", "bg": "#2c2a21", "surface": "#38352a", "currentLine": "#585441", "fg": "#f8f8f2"}, {"name": "Morpheus", "isDark": True, "accent": "#ff9580", "bg": "#2c2122", "surface": "#382a2b", "currentLine": "#584145", "fg": "#f8f8f2"}, {"name": "Alucard", "isDark": False, "accent": "#6272a4", "bg": "#f8f8f2", "surface": "#e6e6e6", "currentLine": "#d0d0d0", "fg": "#282a36"}, {"name": "Gruvbox Dark", "isDark": True, "accent": "#fe8019", "bg": "#282828", "surface": "#3c3836", "currentLine": "#504945", "fg": "#ebdbb2"}, {"name": "Gruvbox Material", "isDark": True, "accent": "#ea698c", "bg": "#1d2021", "surface": "#282828", "currentLine": "#3c3836", "fg": "#ddc7a1"}, {"name": "Gruvbox Light", "isDark": False, "accent": "#af3a03", "bg": "#fbf1c7", "surface": "#ebdbb2", "currentLine": "#d5c4a1", "fg": "#3c3836"}, {"name": "Rosé Pine", "isDark": True, "accent": "#ebbcba", "bg": "#191724", "surface": "#1f1d2e", "currentLine": "#26233a", "fg": "#e0def4"}, {"name": "Rosé Pine Moon", "isDark": True, "accent": "#ea9a97", "bg": "#232136", "surface": "#2a273f", "currentLine": "#393552", "fg": "#e0def4"}, {"name": "Rosé Pine Dawn", "isDark": False, "accent": "#d7827e", "bg": "#faf4ed", "surface": "#f2e9e1", "currentLine": "#e4d7d0", "fg": "#575279"}, {"name": "Catppuccin Mocha", "isDark": True, "accent": "#cba6f7", "bg": "#1e1e2e", "surface": "#313244", "currentLine": "#45475a", "fg": "#cdd6f4"}, {"name": "Catppuccin Macchiato", "isDark": True, "accent": "#f5bde6", "bg": "#24273a", "surface": "#363a4f", "currentLine": "#494d64", "fg": "#cad3f5"}, {"name": "Catppuccin Latte", "isDark": False, "accent": "#8839ef", "bg": "#eff1f5", "surface": "#e6e9ef", "currentLine": "#ccd0da", "fg": "#4c4f69"}, {"name": "Everforest Dark", "isDark": True, "accent": "#a7c080", "bg": "#2d353b", "surface": "#343f44", "currentLine": "#3d484d", "fg": "#d3c6aa"}, {"name": "Everforest Light", "isDark": False, "accent": "#8da101", "bg": "#fdf6e3", "surface": "#f4e0c5", "currentLine": "#e5d5c5", "fg": "#5c6a72"}, {"name": "Tokyo Night", "isDark": True, "accent": "#7aa2f7", "bg": "#1a1b26", "surface": "#24283b", "currentLine": "#414868", "fg": "#c0caf5"}, {"name": "Tokyo Night Storm", "isDark": True, "accent": "#7aa2f7", "bg": "#24283b", "surface": "#1f2335", "currentLine": "#414868", "fg": "#c0caf5"}, {"name": "Tokyo Night Day", "isDark": False, "accent": "#2e7de9", "bg": "#e1e2e7", "surface": "#d5d6db", "currentLine": "#c4c8da", "fg": "#3760bf"}, {"name": "Nord Dark", "isDark": True, "accent": "#88c0d0", "bg": "#2e3440", "surface": "#3b4252", "currentLine": "#434c5e", "fg": "#eceff4"}, {"name": "Nord Light", "isDark": False, "accent": "#5e81ac", "bg": "#eceff4", "surface": "#e5e9f0", "currentLine": "#d8dee9", "fg": "#2e3440"}, {"name": "Solarized Dark", "isDark": True, "accent": "#268bd2", "bg": "#002b36", "surface": "#073642", "currentLine": "#586e75", "fg": "#839496"}, {"name": "Solarized Light", "isDark": False, "accent": "#268bd2", "bg": "#fdf6e3", "surface": "#eee8d5", "currentLine": "#93a1a1", "fg": "#657b83"}, {"name": "One Dark Pro", "isDark": True, "accent": "#61afef", "bg": "#282c34", "surface": "#21252b", "currentLine": "#3e4451", "fg": "#abb2bf"}, {"name": "One Light", "isDark": False, "accent": "#4078f2", "bg": "#fafafa", "surface": "#f0f0f0", "currentLine": "#e5e5e6", "fg": "#383a42"}, {"name": "Monokai Pro", "isDark": True, "accent": "#ffd866", "bg": "#2d2a2e", "surface": "#3a3a3a", "currentLine": "#4a4a4a", "fg": "#fcfcfa"}, {"name": "Cyberpunk Neon", "isDark": True, "accent": "#ff007f", "bg": "#120e24", "surface": "#22194d", "currentLine": "#3a2a80", "fg": "#00ff9f"} ] def make_feishin_json(bg, surface, current_line, fg, accent, is_dark, name="Quickshell", slug="quickshell"): mode = "dark" if is_dark else "light" extends_theme = "defaultDark" if is_dark else "defaultLight" return { "id": slug, "name": name, "version": "1.0.0", "author": "Quickshell", "style": mode, "mode": mode, "extends": extends_theme, "colors": { "primary": accent, "background": bg, "background-alternate": surface, "surface": surface, "surface-foreground": fg, "foreground": fg, "foreground-muted": current_line } } def sync_feishin(bg, surface, current_line, fg, accent, sub_accent, is_dark, variant_name="Pro"): import json, glob active_slug = slugify(variant_name) feishin_dirs = get_app_config_dirs(['feishin', 'Feishin']) for fb in [ os.path.expanduser('~/.config/feishin'), os.path.expanduser('~/.var/app/io.github.jeffvli.feishin/config/feishin'), os.path.expanduser('~/.var/app/org.jeffvli.feishin/config/feishin'), os.path.expanduser('~/.var/app/feishin/config/feishin') ]: if fb not in feishin_dirs: feishin_dirs.append(fb) for base_dir in feishin_dirs: try: themes_dir = os.path.join(base_dir, 'Themes') os.makedirs(themes_dir, exist_ok=True) # 1. Clean up any previous .backup files and restore as .json for backup_file in glob.glob(os.path.join(themes_dir, '*.backup')): try: clean_json = backup_file[:-7] if not os.path.exists(clean_json): os.rename(backup_file, clean_json) else: os.remove(backup_file) except Exception: pass # 2. Pre-generate JSON files for all predefined variants so all themes are available for v in PREDEFINED_VARIANTS: v_slug = slugify(v["name"]) v_json = make_feishin_json(v["bg"], v["surface"], v["currentLine"], v["fg"], v["accent"], v["isDark"], v["name"], v_slug) v_path = os.path.join(themes_dir, f"{v_slug}.json") with open(v_path, 'w', encoding='utf-8') as f: json.dump(v_json, f, indent=2) # 3. Write active variant JSON with runtime parameters + quickshell.json active_json = make_feishin_json(bg, surface, current_line, fg, accent, is_dark, variant_name, active_slug) active_path = os.path.join(themes_dir, f"{active_slug}.json") with open(active_path, 'w', encoding='utf-8') as f: json.dump(active_json, f, indent=2) with open(os.path.join(themes_dir, "quickshell.json"), 'w', encoding='utf-8') as f: json.dump(active_json, f, indent=2) # 4. Auto-update preferences.json in Feishin config directory if present pref_file = os.path.join(base_dir, 'preferences.json') if os.path.exists(pref_file): try: with open(pref_file, 'r', encoding='utf-8') as f: pref_data = json.load(f) pref_data["theme"] = active_slug with open(pref_file, 'w', encoding='utf-8') as f: json.dump(pref_data, f, indent=2) except Exception: pass except Exception: pass restart_app_if_running( "feishin", ["feishin", "io.github.jeffvli.feishin", "org.jeffvli.feishin"], [ "flatpak run io.github.jeffvli.feishin", "flatpak run org.jeffvli.feishin", "gtk-launch io.github.jeffvli.feishin", "gtk-launch feishin", "feishin" ] ) def sync_starship(bg, surface, current_line, fg, accent, sub_accent, is_dark): starship_dir = os.path.expanduser('~/.config') os.makedirs(starship_dir, exist_ok=True) starship_file = os.path.join(starship_dir, 'starship.toml') red_color = "#ff5555" if is_dark else "#b91c1c" green_color = "#50fa7b" if is_dark else "#15803d" yellow_color = "#f1fa8c" if is_dark else "#b45309" blue_color = accent cyan_color = "#8be9fd" if is_dark else "#0284c7" starship_config = f"""# Starship Minimal & Efficient Theme with Nerd Fonts (Auto-synced with Quickshell) format = \"\"\" $os\\ $directory\\ $git_branch\\ $git_status\\ $package\\ $c\\ $cpp\\ $cmake\\ $golang\\ $java\\ $nodejs\\ $python\\ $rust\\ $cmd_duration\\ $status\\ $line_break\\ $character\"\"\" palette = "quickshell" [os] disabled = false style = "accent" format = "[$symbol]($style) " [os.symbols] Arch = "" EndeavourOS = "" NixOS = "" Fedora = "" Ubuntu = "" Debian = "" Manjaro = "" Pop = "" Linux = "" [directory] style = "bold accent" format = "[$path]($style)[$read_only]($read_only_style) " truncation_length = 3 truncation_symbol = "…/" home_symbol = "~" read_only = " 🔒" read_only_style = "red" [git_branch] symbol = " " style = "bold sub_accent" format = "[$symbol$branch]($style) " [git_status] style = "red" format = "([$all_status$ahead_behind]($style) )" conflicted = "=" ahead = "⇡${{count}}" behind = "⇣${{count}}" diverged = "⇕⇡${{ahead_count}}⇣${{behind_count}}" untracked = "?${{count}}" stashed = "$${{count}}" modified = "!${{count}}" staged = "+${{count}}" renamed = "»${{count}}" deleted = "-${{count}}" [cmd_duration] min_time = 2000 style = "yellow" format = "[⏱ $duration]($style) " [status] disabled = false style = "bold red" symbol = "✖ " format = "[$symbol$status]($style) " [character] success_symbol = "[❯](accent)" error_symbol = "[❯](red)" vimcmd_symbol = "[❮](sub_accent)" [package] disabled = true [nodejs] symbol = " " style = "green" format = "[$symbol($version )]($style)" [python] symbol = " " style = "yellow" format = "[$symbol($version )]($style)" [rust] symbol = " " style = "red" format = "[$symbol($version )]($style)" [golang] symbol = " " style = "cyan" format = "[$symbol($version )]($style)" [c] symbol = " " style = "blue" format = "[$symbol($version )]($style)" [palettes.quickshell] accent = "{accent}" sub_accent = "{sub_accent}" bg = "{bg}" surface = "{surface}" fg = "{fg}" current_line = "{current_line}" red = "{red_color}" green = "{green_color}" yellow = "{yellow_color}" blue = "{blue_color}" cyan = "{cyan_color}" """ try: with open(starship_file, 'w', encoding='utf-8') as f: f.write(starship_config) except Exception as e: print(f"[Starship Sync Error] {e}", flush=True) def sync_vim(bg, surface, current_line, fg, accent, sub_accent, is_dark): mode = "dark" if is_dark else "light" comment_color = "#7970a9" if is_dark else "#8c8c8c" green_color = "#8aff80" if is_dark else "#16a34a" orange_color = "#ffca80" if is_dark else "#ea580c" red_color = "#ff9580" if is_dark else "#dc2626" vim_theme_content = f"""" Auto-generated by Quickshell Theme Engine highlight clear if exists("syntax_on") syntax reset endif let g:colors_name = "quickshell_theme" set background={mode} hi Normal guifg={fg} guibg={bg} hi Terminal guifg={fg} guibg={bg} hi CursorLine guibg={surface} hi CursorColumn guibg={surface} hi LineNr guifg={current_line} guibg={bg} hi CursorLineNr guifg={accent} guibg={surface} gui=bold hi Visual guibg={current_line} hi StatusLine guifg={fg} guibg={surface} gui=bold hi StatusLineNC guifg={current_line} guibg={bg} hi VertSplit guifg={current_line} guibg={bg} hi Pmenu guifg={fg} guibg={surface} hi PmenuSel guifg={bg} guibg={accent} hi Search guifg={bg} guibg={accent} hi IncSearch guifg={bg} guibg={sub_accent} hi Comment guifg={comment_color} gui=italic hi Constant guifg={sub_accent} hi String guifg={green_color} hi Character guifg={green_color} hi Number guifg={orange_color} hi Boolean guifg={sub_accent} hi Float guifg={orange_color} hi Identifier guifg={fg} hi Function guifg={accent} gui=bold hi Statement guifg={accent} hi Conditional guifg={accent} hi Repeat guifg={accent} hi Label guifg={accent} hi Operator guifg={sub_accent} hi Keyword guifg={accent} gui=bold hi Exception guifg={accent} hi PreProc guifg={sub_accent} hi Include guifg={accent} hi Define guifg={accent} hi Macro guifg={sub_accent} hi PreCondit guifg={accent} hi Type guifg={sub_accent} hi StorageClass guifg={accent} hi Structure guifg={accent} hi Typedef guifg={accent} hi Special guifg={sub_accent} hi SpecialChar guifg={sub_accent} hi Tag guifg={accent} hi Delimiter guifg={fg} hi SpecialComment guifg={comment_color} gui=bold hi Debug guifg={sub_accent} hi Underlined guifg={accent} gui=underline hi Error guifg={fg} guibg={red_color} hi Todo guifg={bg} guibg={accent} gui=bold """ colors_dir = os.path.expanduser('~/.vim/colors') os.makedirs(colors_dir, exist_ok=True) with open(os.path.join(colors_dir, 'quickshell_theme.vim'), 'w') as f: f.write(vim_theme_content) vimrc_path = os.path.expanduser('~/.vimrc') vimrc_content = f"""syntax on set termguicolors set background={mode} colorscheme quickshell_theme """ with open(vimrc_path, 'w') as f: f.write(vimrc_content) def sync_btop(bg, surface, current_line, fg, accent, sub_accent): btop_dir = os.path.expanduser('~/.config/btop') themes_dir = os.path.join(btop_dir, 'themes') os.makedirs(themes_dir, exist_ok=True) theme_content = f"""# Auto-generated by Quickshell Theme Engine theme[main_bg]="{bg}" theme[main_fg]="{fg}" theme[title]="{fg}" theme[hi_fg]="{accent}" theme[selected_bg]="{surface}" theme[selected_fg]="{accent}" theme[inactive_fg]="{current_line}" theme[graph_text]="{fg}" theme[meter_bg]="{surface}" theme[proc_misc]="{sub_accent}" theme[cpu_box]="{accent}" theme[mem_box]="{sub_accent}" theme[net_box]="{accent}" theme[proc_box]="{sub_accent}" theme[div_line]="{current_line}" theme[temp_start]="{accent}" theme[temp_mid]="{sub_accent}" theme[temp_end]="#ff9580" theme[cpu_start]="{accent}" theme[cpu_mid]="{sub_accent}" theme[cpu_end]="#ff9580" theme[free_start]="{accent}" theme[free_mid]="{sub_accent}" theme[free_end]="#8aff80" theme[cached_start]="{accent}" theme[cached_mid]="{sub_accent}" theme[cached_end]="#ffff80" theme[available_start]="{accent}" theme[available_mid]="{sub_accent}" theme[available_end]="#80ffea" theme[used_start]="{accent}" theme[used_mid]="{sub_accent}" theme[used_end]="#ff9580" theme[download_start]="{accent}" theme[download_mid]="{sub_accent}" theme[download_end]="#80ffea" theme[upload_start]="{accent}" theme[upload_mid]="{sub_accent}" theme[upload_end]="#ff80bf" """ try: with open(os.path.join(themes_dir, 'quickshell.theme'), 'w') as f: f.write(theme_content) except Exception: pass conf_file = os.path.join(btop_dir, 'btop.conf') if os.path.exists(conf_file): try: with open(conf_file, 'r') as f: conf = f.read() if 'color_theme =' in conf: lines = conf.splitlines() new_lines = [] for l in lines: if l.strip().startswith('color_theme ='): new_lines.append('color_theme = "quickshell"') else: new_lines.append(l) conf = '\n'.join(new_lines) + '\n' else: conf = 'color_theme = "quickshell"\n' + conf with open(conf_file, 'w') as f: f.write(conf) except Exception: pass else: try: with open(conf_file, 'w') as f: f.write('color_theme = "quickshell"\n') except Exception: pass def sync_fastfetch(bg, surface, current_line, fg, accent, sub_accent): # Preserves user's exact master fastfetch configuration from ~/.config/fastfetch/ pass def sync_ghostty(bg, surface, current_line, fg, accent, sub_accent): ghostty_dir = os.path.expanduser('~/.config/ghostty') os.makedirs(ghostty_dir, exist_ok=True) conf_path = os.path.join(ghostty_dir, 'config') config_content = f"""# Auto-generated by Quickshell Theme Engine background = {bg} foreground = {fg} selection-background = {current_line} selection-foreground = {fg} cursor-color = {accent} palette = 0={bg} palette = 1=#ff9580 palette = 2=#8aff80 palette = 3=#ffff80 palette = 4={accent} palette = 5={sub_accent} palette = 6=#80ffea palette = 7={fg} palette = 8={current_line} palette = 9=#ff9580 palette = 10=#8aff80 palette = 11=#ffff80 palette = 12={accent} palette = 13={sub_accent} palette = 14=#80ffea palette = 15=#ffffff """ try: with open(conf_path, 'w') as f: f.write(config_content) except Exception: pass def sync_micro(bg, surface, current_line, fg, accent, sub_accent): micro_dir = os.path.expanduser('~/.config/micro') colors_dir = os.path.join(micro_dir, 'colorschemes') os.makedirs(colors_dir, exist_ok=True) colorscheme = f"""color-link default "{fg},{bg}" color-link comment "#7970a9" color-link identifier "{sub_accent}" color-link constant "{sub_accent}" color-link statement "{accent}" color-link symbol "{sub_accent}" color-link preproc "{sub_accent}" color-link type "{accent}" color-link special "{sub_accent}" color-link ignore "default" color-link error "bold #ff9580" color-link todo "bold #ffff80" color-link indent-char "{current_line}" color-link line-number "{current_line}" color-link current-line-number "{accent}" color-link statusline "{fg},{surface}" color-link tabbar "{fg},{bg}" color-link cursor-line "{surface}" color-link color-column "{surface}" color-link divider "{current_line}" """ try: with open(os.path.join(colors_dir, 'quickshell.micro'), 'w') as f: f.write(colorscheme) except Exception: pass settings_file = os.path.join(micro_dir, 'settings.json') if os.path.exists(settings_file): try: import json with open(settings_file, 'r') as f: data = json.load(f) data["colorscheme"] = "quickshell" with open(settings_file, 'w') as f: json.dump(data, f, indent=4) except Exception: pass else: try: import json with open(settings_file, 'w') as f: json.dump({"colorscheme": "quickshell"}, f, indent=4) except Exception: pass def sync_konsole(bg, surface, current_line, fg, accent, sub_accent): konsole_dir = os.path.expanduser('~/.local/share/konsole') os.makedirs(konsole_dir, exist_ok=True) colorscheme = f"""[Background] Color={bg} [BackgroundFaint] Color={surface} [BackgroundIntense] Color={surface} [Color0] Color={bg} [Color0Faint] Color={bg} [Color0Intense] Color={current_line} [Color1] Color=#ff9580 [Color1Faint] Color=#ff9580 [Color1Intense] Color=#ff9580 [Color2] Color=#8aff80 [Color2Faint] Color=#8aff80 [Color2Intense] Color=#8aff80 [Color3] Color=#ffff80 [Color3Faint] Color=#ffff80 [Color3Intense] Color=#ffff80 [Color4] Color={accent} [Color4Faint] Color={accent} [Color4Intense] Color={accent} [Color5] Color={sub_accent} [Color5Faint] Color={sub_accent} [Color5Intense] Color={sub_accent} [Color6] Color=#80ffea [Color6Faint] Color=#80ffea [Color6Intense] Color=#80ffea [Color7] Color={fg} [Color7Faint] Color={fg} [Color7Intense] Color=#ffffff [Foreground] Color={fg} [ForegroundFaint] Color={fg} [ForegroundIntense] Color=#ffffff [General] Description=Quickshell Dynamic Theme Opacity=1 Wallpaper= """ try: with open(os.path.join(konsole_dir, 'Quickshell.colorscheme'), 'w') as f: f.write(colorscheme) except Exception: pass def sync_xresources(bg, surface, current_line, fg, accent, sub_accent): xres_path = os.path.expanduser('~/.Xresources') xres_content = f"""! Auto-generated by Quickshell Theme Engine *.background: {bg} *.foreground: {fg} *.cursorColor: {accent} *.color0: {bg} *.color1: #ff9580 *.color2: #8aff80 *.color3: #ffff80 *.color4: {accent} *.color5: {sub_accent} *.color6: #80ffea *.color7: {fg} *.color8: {current_line} *.color9: #ff9580 *.color10: #8aff80 *.color11: #ffff80 *.color12: {accent} *.color13: {sub_accent} *.color14: #80ffea *.color15: #ffffff """ try: with open(xres_path, 'w') as f: f.write(xres_content) subprocess.run(["xrdb", "-merge", xres_path], check=False, stderr=subprocess.DEVNULL) except Exception: pass def main(): parser = argparse.ArgumentParser() parser.add_argument('--bg', required=True) parser.add_argument('--surface', required=True) parser.add_argument('--currentLine', required=True) parser.add_argument('--fg', required=True) parser.add_argument('--accent', required=True) parser.add_argument('--subAccent', required=True) parser.add_argument('--isDark', required=True) parser.add_argument('--variantName', default='Pro') args = parser.parse_args() is_dark = args.isDark.lower() == 'true' sync_gtk(args.bg, args.surface, args.currentLine, args.fg, args.accent, is_dark) sync_alacritty(args.bg, args.surface, args.currentLine, args.fg, args.accent, args.subAccent) sync_discord(args.bg, args.surface, args.currentLine, args.fg, args.accent, args.subAccent, is_dark) sync_vscode(args.bg, args.surface, args.currentLine, args.fg, args.accent, args.subAccent, is_dark) sync_zen(args.bg, args.surface, args.currentLine, args.fg, args.accent, args.subAccent, is_dark) sync_feishin(args.bg, args.surface, args.currentLine, args.fg, args.accent, args.subAccent, is_dark, args.variantName) sync_starship(args.bg, args.surface, args.currentLine, args.fg, args.accent, args.subAccent, is_dark) sync_vim(args.bg, args.surface, args.currentLine, args.fg, args.accent, args.subAccent, is_dark) sync_btop(args.bg, args.surface, args.currentLine, args.fg, args.accent, args.subAccent) sync_fastfetch(args.bg, args.surface, args.currentLine, args.fg, args.accent, args.subAccent) sync_ghostty(args.bg, args.surface, args.currentLine, args.fg, args.accent, args.subAccent) sync_micro(args.bg, args.surface, args.currentLine, args.fg, args.accent, args.subAccent) sync_konsole(args.bg, args.surface, args.currentLine, args.fg, args.accent, args.subAccent) sync_xresources(args.bg, args.surface, args.currentLine, args.fg, args.accent, args.subAccent) if __name__ == '__main__': main()