optimize scripts

This commit is contained in:
2025-12-07 02:52:32 +01:00
parent f23fe5506d
commit a1696ee49b
5 changed files with 181 additions and 85 deletions

View File

@@ -9,8 +9,10 @@
import os import os
import sys import sys
from pathlib import Path
import argparse import argparse
import subprocess
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
PALETTES = { PALETTES = {
"catppuccin-mocha": { "catppuccin-mocha": {
@@ -50,77 +52,121 @@ SCRIPTS = {
"wlogout": [CONFIG_DIR / ".alt" / "wlogout-default" / "apply-color", CONFIG_DIR / ".alt" / "wlogout-niri" / "apply-color"], "wlogout": [CONFIG_DIR / ".alt" / "wlogout-default" / "apply-color", CONFIG_DIR / ".alt" / "wlogout-niri" / "apply-color"],
"yazi": [CONFIG_DIR / "yazi" / "apply-color"], "yazi": [CONFIG_DIR / "yazi" / "apply-color"],
} }
# or simply `find ${CONFIG_DIR} -type f -iname 'apply-color*'` to get all available scripts, # or simply `find -L ${CONFIG_DIR} -type f -iname 'apply-color*'` to get all available scripts,
# but I need the exact application names anyway, so hardcoding does make some sense # but I do need the exact application names anyway, so hardcoding does make some sense
def hex2rgb(hex_color: str) -> tuple[int, int, int]: def hex2rgb(hex_color: str) -> tuple[int, int, int]:
"""#rrggbb to (r, g, b)"""
return tuple(int(hex_color[i:i + 2], 16) for i in (0, 2, 4)) # type: ignore return tuple(int(hex_color[i:i + 2], 16) for i in (0, 2, 4)) # type: ignore
def clamp(x, minimum, maximum) -> float:
"""Clamp x to the range [minimum, maximum]"""
return max(minimum, min(x, maximum))
def rgb2hsv(rr: int, gg: int, bb: int) -> tuple[float, float, float]:
"""(r, g, b) 0-255 to (h, s, v)"""
r, g, b = rr/255.0, gg/255.0, bb/255.0
r = clamp(r, 0.0, 1.0)
g = clamp(g, 0.0, 1.0)
b = clamp(b, 0.0, 1.0)
mx = max(r, g, b)
mn = min(r, g, b)
df = mx-mn
h = 0.0
if mx == mn:
h = 0.0
elif mx == r:
h = (60 * ((g-b)/df) + 360) % 360
elif mx == g:
h = (60 * ((b-r)/df) + 120) % 360
elif mx == b:
h = (60 * ((r-g)/df) + 240) % 360
if mx == 0:
s = 0.0
else:
s = (df/mx)*100
v = mx*100
return h, s, v
def extract_color(image_path: str) -> str: def extract_color(image_path: str) -> str:
"""Extract a dominant color from the image and return it as a #rrggbb string."""
# Only import when needed
from colorthief import ColorThief from colorthief import ColorThief
return "#{:02x}{:02x}{:02x}".format(*ColorThief(image_path).get_color(quality=10)) ct = ColorThief(image_path)
# Get first 5 dominant colors
palette = ct.get_palette(color_count=5, quality=10)
best_color = None
max_score = -1.0
for color in palette:
h, s, v = rgb2hsv(*color)
# Filter out undesirable colors
# Too dark
if v < 20:
continue
# Too light
if v > 95 and s < 5:
continue
# Saturation first, then value
score = s * 2.0 + v
if score > max_score:
max_score = score
best_color = color
# Fallback to the most dominant color
if best_color is None:
best_color = ct.get_color(quality=10)
return "#{:02x}{:02x}{:02x}".format(*best_color)
def match_color(color: str, palette: dict[str, str]) -> str: def match_color(color: str, palette: dict[str, str]) -> str:
""" Matches a given color (rrggbb hex) to the closest color in the palette.""" """Match the given #rrggbb color to the closest flavor in the palette."""
# HUE distance of the given and returned color must no<t exceed this value
HUE_THRESHOLD = 60.0 # degrees
color = color.lower().strip().removeprefix('#') color = color.lower().strip().removeprefix('#')
target_rgb = hex2rgb(color)
target_h, target_s, target_v = rgb2hsv(*target_rgb)
# weigh by CCIR 601 luminosity # Warn if not representative (nearly grayscale)
fr, fg, fb = 0.299 / 255 / 255, 0.587 / 255 / 255, 0.114 / 255 / 255 if target_s < 5:
lfr, lfg, lfb = 0.299 / 255, 0.587 / 255, 0.114 / 255 print(f"Warning: Extracted color {color} is nearly grayscale. Matching might be inaccurate.")
def color_distance(c1: str, c2: str) -> float: def get_weighted_distance(hex_val: str) -> float:
r1, g1, b1 = hex2rgb(c1) p_rgb = hex2rgb(hex_val)
r2, g2, b2 = hex2rgb(c2) p_h, p_s, p_v = rgb2hsv(*p_rgb)
diff_l = (lfr * (r1 - r2) + lfg * (g1 - g2) + lfb * (b1 - b2))
diff_r = fr * (r1 - r2) ** 2
diff_g = fg * (g1 - g2) ** 2
diff_b = fb * (b1 - b2) ** 2
return (diff_r + diff_g + diff_b) * 0.75 + diff_l ** 2
def color_distance_hue(c1: str, c2: str) -> float: # RGB distance with weighting
def rgb2hue(r, g, b) -> float: rmean = (target_rgb[0] + p_rgb[0]) / 2
r, g, b = r / 255.0, g / 255.0, b / 255.0 dr = target_rgb[0] - p_rgb[0]
mx = max(r, g, b) dg = target_rgb[1] - p_rgb[1]
mn = min(r, g, b) db = target_rgb[2] - p_rgb[2]
diff = mx - mn rgb_distance = ((2 + rmean / 256) * dr**2 + 4 * dg**2 + (2 + (255 - rmean) / 256) * db**2) ** 0.5
if diff == 0: # Hue difference (with wrapping)
return 0.0 hue_diff = abs(target_h - p_h)
if hue_diff > 180:
hue_diff = 360 - hue_diff
if mx == r: # Increase hue weight when saturation is high
hue = (g - b) / diff + (6 if g < b else 0) hue_weight = 2.0 if target_s > 20 else 0.5
elif mx == g:
hue = (b - r) / diff + 2
else:
hue = (r - g) / diff + 4
return hue * 60 return rgb_distance + (hue_diff * hue_weight * 3)
r1, g1, b1 = hex2rgb(c1)
r2, g2, b2 = hex2rgb(c2)
return abs(rgb2hue(r1, g1, b1) - rgb2hue(r2, g2, b2))
closest_color = min(palette.keys(), key=lambda k: color_distance(color, palette[k])) closest_flavor = min(palette.keys(), key=lambda k: get_weighted_distance(palette[k]))
print(f"Matched color {color} to {closest_color}") print(f"Matched color #{color} to {closest_flavor} (#{palette[closest_flavor]})")
return closest_flavor
# if the hue distance is too large, rematch
if color_distance_hue(color, palette[closest_color]) > HUE_THRESHOLD:
print(f"Color {color} is too far from {closest_color}, rematching'")
else:
return closest_color
closest_color = min(palette.keys(), key=lambda k: color_distance_hue(color, palette[k]))
print(f"Rematched color {color} to {closest_color}")
return closest_color
def pick_flavor(palette: dict[str, str]) -> str: def pick_flavor_interactive(palette: dict[str, str]) -> str:
"""Prompt the user to pick a flavor interactively."""
def is_interactive() -> bool: def is_interactive() -> bool:
return sys.stdin.isatty() and sys.stdout.isatty() return sys.stdin.isatty() and sys.stdout.isatty()
@@ -153,6 +199,27 @@ def pick_flavor(palette: dict[str, str]) -> str:
sys.exit(1) sys.exit(1)
def run_script(script_path: Path, args: list[str]):
"""Helper to run a single script safely."""
script_str = str(script_path)
if not script_path.exists():
print(f"Warning: Script not found: {script_str}")
return
if not os.access(script_path, os.X_OK):
print(f"Warning: Script not executable: {script_str}")
return
try:
cmd = [script_str] + args
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"Error running {script_path}:\n{result.stderr.strip()}")
else:
print(f"✓ {script_path}")
except Exception as e:
print(f"Exception running {script_path}: {e}")
def main(): def main():
parser = argparse.ArgumentParser(description="Change color theme for various applications.") parser = argparse.ArgumentParser(description="Change color theme for various applications.")
parser.add_argument('-i', '--image', type=str, help="Path to the image") parser.add_argument('-i', '--image', type=str, help="Path to the image")
@@ -186,7 +253,7 @@ def main():
flavor = match_color(color, palette) flavor = match_color(color, palette)
print(f"Matched color: {flavor}") print(f"Matched color: {flavor}")
else: else:
flavor = pick_flavor(palette) flavor = pick_flavor_interactive(palette)
return flavor return flavor
def parse_apps() -> tuple[set[str], set[str]]: def parse_apps() -> tuple[set[str], set[str]]:
@@ -222,15 +289,18 @@ def main():
print(f"Applying flavor '{flavor}' for {len(apps)} applications.") print(f"Applying flavor '{flavor}' for {len(apps)} applications.")
script_args = [palette_name, flavor, palette[flavor]]
tasks = []
with ThreadPoolExecutor(max_workers=8) as executor:
for app in apps: for app in apps:
for script in SCRIPTS[app]: for script in SCRIPTS[app]:
print(f"Running {script}:") tasks.append(executor.submit(run_script, script, script_args))
ret = os.system(f'"{script}" {palette_name} {flavor} {palette[flavor]}')
print(f"{script} exited with code {ret}")
print("")
os.system( subprocess.run([
f'notify-send -a "change-colortheme" "Colortheme Changed" "Palette: {palette_name};\nFlavor: {flavor};\nApplied to {len(apps)} applications."') "notify-send", "-a", "change-colortheme", "Colortheme Changed",
f"Palette: {palette_name}\nFlavor: {flavor}"
])
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -67,11 +67,10 @@ screen_height=$3
# } # }
# ``` # ```
# So in order to let the most recently used wallpapers appear first: # So in order to let the most recently used wallpapers appear first:
touch "$image" touch "$image" 2>/dev/null || true # ignore errors
# Copy image to local wallpaper directory # Copy image to local wallpaper directory
ext=${image##*.}
wallpaper_ext="png" wallpaper_ext="png"
random_name=$(tr -dc 'a-zA-Z0-9' </dev/urandom | head -c 16) random_name=$(tr -dc 'a-zA-Z0-9' </dev/urandom | head -c 16)
current_dir="$HOME/.local/share/wallpaper/current" current_dir="$HOME/.local/share/wallpaper/current"

View File

@@ -1,4 +1,4 @@
#!/bin/sh #!/usr/bin/env bash
# Description: # Description:
# Use a sequence of keybinds to open the scrollback(or screen) buffer in editor (default to vim) # Use a sequence of keybinds to open the scrollback(or screen) buffer in editor (default to vim)
@@ -18,6 +18,11 @@
# keybind = ctrl+shift+j=text:ghostty-capture\x20 # keybind = ctrl+shift+j=text:ghostty-capture\x20
# keybind = ctrl+shift+h=write_screen_file:paste # keybind = ctrl+shift+h=write_screen_file:paste
if [ -z "$1" ] && ! command -v wl-paste &> /dev/null; then
echo "Error: wl-paste not found." >&2
exit 1
fi
file=${1:-$(wl-paste --no-newline)} file=${1:-$(wl-paste --no-newline)}
[ -z "$file" ] && { [ -z "$file" ] && {
@@ -38,7 +43,11 @@ case "$file" in
;; ;;
esac esac
${EDITOR:-vim} "$file" if [[ "$EDITOR" == *"code"* ]]; then
$EDITOR --wait "$file"
else
${EDITOR:-vim} "$file"
fi
rm -f "$file" rm -f "$file"

View File

@@ -75,7 +75,7 @@ def swwwLoadImg(namespace: str, wallpaper: Path):
def swwwStartDaemon(namespace: str): def swwwStartDaemon(namespace: str):
# Check if daemon is already running # Check if daemon is already running
cmd = ["pgrep", "-f", f"swww daemon -n {namespace}"], "-u", str(getuid()) cmd = ["pgrep", "-f", f"swww-daemon -n {namespace}", "-u", str(getuid())]
try: try:
output = subprocess.check_output(cmd, text=True) output = subprocess.check_output(cmd, text=True)
pids = output.strip().splitlines() pids = output.strip().splitlines()
@@ -106,11 +106,13 @@ class AutoBlur:
_thread: threading.Thread | None = None _thread: threading.Thread | None = None
_lastWallpaper: Path | None = None _lastWallpaper: Path | None = None
_isFirst = True _isFirst = True
_applyLock: threading.Lock
def __init__(self, normalDir, blurredDir, interval=0.2): def __init__(self, normalDir, blurredDir, interval=0.2):
self._interval = interval self._interval = interval
self._normalDir = normalDir self._normalDir = normalDir
self._blurredDir = blurredDir self._blurredDir = blurredDir
self._applyLock = threading.Lock()
# Niri will send "WindowsChanged" event on connect, so no need to init here # Niri will send "WindowsChanged" event on connect, so no need to init here
# init state # init state
@@ -204,6 +206,7 @@ class AutoBlur:
sleep(self._interval) sleep(self._interval)
def _apply(self, wallpaper: Path) -> bool: def _apply(self, wallpaper: Path) -> bool:
with self._applyLock:
if wallpaper == self._lastWallpaper: if wallpaper == self._lastWallpaper:
return True return True
@@ -339,14 +342,22 @@ if __name__ == "__main__":
swwwLoadImg("background", normal) swwwLoadImg("background", normal)
# Connect to Niri socket # Connect to Niri socket
_log(f"[Main] connecting to Niri socket") _log("[Main] connecting to Niri socket")
niri_socket = getNiriSocket() niri_socket = getNiriSocket()
if not niri_socket: if not niri_socket:
_log("[Main] NIRI_SOCKET environment variable is not set.") _log("[Main] NIRI_SOCKET environment variable is not set.")
exit(1) exit(1)
while True:
try:
if not connectNiri(niri_socket, handleEvent): if not connectNiri(niri_socket, handleEvent):
exit(1) _log("[Main] Connection lost or failed.")
except Exception as e:
_log(f"[Main] Exception in connection loop: {e}")
_log("[Main] Retrying in 3 seconds...")
sleep(3)
niri_socket = getNiriSocket() or niri_socket
elif desktop == "Hyprland": elif desktop == "Hyprland":
_log("[Main] running in Hyprland") _log("[Main] running in Hyprland")
_log("[Main] starting swww daemon") _log("[Main] starting swww daemon")

View File

@@ -7,8 +7,9 @@
# HYPR_AQ_DRM_DEVICES - Colon-separated list of DRM device paths for Hyprland's aq_drm # HYPR_AQ_DRM_DEVICES - Colon-separated list of DRM device paths for Hyprland's aq_drm
# BRIGHTNESSCTL_DEVICE - Device identifier for brightnessctl # BRIGHTNESSCTL_DEVICE - Device identifier for brightnessctl
# AMD -> Nvidia -> Intel # Constants
prefer_order=(amd nvidia intel) niri_config_file="$HOME/.config/niri/config/misc.kdl"
prefer_order=(amd nvidia intel) # AMD -> Nvidia -> Intel
# Get vendor and path of each GPU # Get vendor and path of each GPU
default_dri_path="$(find /dev/dri/card* 2>/dev/null | head -n 1)" default_dri_path="$(find /dev/dri/card* 2>/dev/null | head -n 1)"
@@ -63,14 +64,20 @@ for who in "${prefer_order[@]}"; do
done done
# Update niri config # Update niri config
for file in "$HOME/.config/niri/config/misc.kdl" "$HOME/.config/niri/config.kdl.template"; do function update_niri_config() {
[[ -f "$file" ]] || continue local config_file="$1"
local device_path="$2"
if grep -qE '^\s*render-drm-device\s+"[^"]+"' "$file"; then [[ -f "$config_file" ]] || return
current="$(grep -E '^\s*render-drm-device\s+"[^"]+"' "$file" | sed -E 's/^\s*render-drm-device\s+"([^"]+)".*/\1/')"
[[ "$current" == "$primary_device" ]] && continue if grep -qE '^\s*render-drm-device\s+"[^"]+"' "$config_file"; then
sed -i -E "s|^(\s*render-drm-device\s+)\"[^\"]+\"|\1\"$primary_device\"|" "$file" local current
current="$(grep -E '^\s*render-drm-device\s+"[^"]+"' "$config_file" | sed -E 's/^\s*render-drm-device\s+"([^"]+)".*/\1/')"
[[ "$current" == "$device_path" ]] && return
sed -i -E "s|^(\s*render-drm-device\s+)\"[^\"]+\"|\1\"$device_path\"|" "$config_file"
else else
printf '\ndebug {\nrender-drm-device "%s"\n}\n' "$primary_device" >> "$file" printf '\ndebug {\nrender-drm-device "%s"\n}\n' "$device_path" >> "$config_file"
fi fi
done }
update_niri_config "$niri_config_file" "$primary_device"