quickshell-rice/quickshell/services/python/theme_sync.py

1362 lines
45 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
import os, sys, argparse, re
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_dir = os.path.expanduser('~/.config/alacritty')
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')
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}"
"""
try:
with open(colors_file, 'w', encoding='utf-8') as f:
f.write(colors_block)
f.flush()
os.fsync(f.fileno())
except Exception:
pass
if os.path.exists(toml_file):
try:
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')
except Exception:
pass
else:
try:
with open(toml_file, 'w', encoding='utf-8') as f:
f.write('[general]\nimport = ["colors.toml"]\nlive_config_reload = true\n')
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/equibop/themes'),
os.path.expanduser('~/.config/equicord/themes'),
os.path.expanduser('~/.config/Vencord/themes'),
os.path.expanduser('~/.config/vesktop/themes')
]:
if os.path.exists(os.path.dirname(client_dir)):
os.makedirs(client_dir, exist_ok=True)
css_file = os.path.join(client_dir, 'quickshell-theme.css')
with open(css_file, 'w') as f:
f.write(css_content)
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
# Graceful SIGTERM restart with profile lock release buffer
res = subprocess.run(["pgrep", "-f", "zen-browser"], capture_output=True, text=True)
if res.returncode == 0:
subprocess.run(["pkill", "-15", "-f", "zen-browser"])
import time
for _ in range(30):
check = subprocess.run(["pgrep", "-f", "zen-browser"], capture_output=True, text=True)
if check.returncode != 0:
time.sleep(0.6) # Safety buffer for lock release
break
time.sleep(0.1)
subprocess.Popen(["zen-browser"], start_new_session=True)
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 = [
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')
]
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') as f:
json.dump(v_json, f, indent=2)
# 3. Write active variant JSON with runtime parameters
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') as f:
json.dump(active_json, f, indent=2)
except Exception:
pass
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):
import re
fastfetch_dir = os.path.expanduser('~/.config/fastfetch')
os.makedirs(fastfetch_dir, exist_ok=True)
conf_path = os.path.join(fastfetch_dir, 'config.jsonc')
if os.path.exists(conf_path):
try:
with open(conf_path, 'r', encoding='utf-8') as f:
content = f.read()
modified = False
if '"keys"' in content:
content, count1 = re.subn(r'"keys"\s*:\s*"[^"]*"', f'"keys": "{accent}"', content)
if count1 > 0: modified = True
if '"title"' in content:
content, count2 = re.subn(r'"title"\s*:\s*"[^"]*"', f'"title": "{sub_accent}"', content)
if count2 > 0: modified = True
if not modified and '"display"' in content:
content = re.sub(r'"display"\s*:\s*\{', f'"display": {{\n "color": {{\n "keys": "{accent}",\n "title": "{sub_accent}"\n }},', content, count=1)
modified = True
if modified:
with open(conf_path, 'w', encoding='utf-8') as f:
f.write(content)
return
except Exception:
pass
fastfetch_jsonc = f"""{{
"$schema": "https://github.com/fastfetch-cli/fastfetch/raw/dev/doc/json_schema.json",
"logo": {{
"source": "~/.config/fastfetch/hamster_cookie.txt",
"padding": {{
"right": 4
}}
}},
"display": {{
"color": {{
"keys": "{accent}",
"title": "{sub_accent}"
}}
}},
"modules": [
"title",
"separator",
"os",
"host",
"kernel",
"uptime",
"packages",
"shell",
"display",
"wm",
"terminal",
"cpu",
"gpu",
"memory",
"break",
"colors"
]
}}
"""
try:
with open(conf_path, 'w', encoding='utf-8') as f:
f.write(fastfetch_jsonc)
except Exception:
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()