콘텐츠로 이동

Matugen 기반 Zenburn 테마 파이프라인 치트시트

환경: Arch Linux x86_64, Linux 6.12+ zen, Hyprland, Matugen v4.2+

Frosted Zenburn Glass 디자인 시스템을 기반으로 저대비 웜 차콜 어스 톤(Zenburn)과 일일 Bing UHD 배경화면에서 추출한 Material You 팔레트를 일관되게 주입하는 파이프라인입니다. 중앙 제어 스크립트인 theme-ctl을 단일 진실 공급원(SSOT)으로 삼아, Matugen 템플릿 엔진을 통해 45° 입체 그라디언트 엣지 라이트와 테마 토큰을 각 UI 컴포넌트(Kitty, Hyprland, Noctalia Shell)로 생성하고 데스크톱 세션 재시작 없이 런타임 IPC 신호로 즉시 동기화합니다.

flowchart TD
    A["SSOT Trigger<br/>theme-ctl zenburn / wallpaper"] --> B["Theme Controller CLI<br/>~/bin/theme-ctl"]
    B -->|Canonical Tokens 주입| C1["~/.config/kitty/theme-colors.conf"]
    B -->|Canonical Tokens 주입| C2["~/.config/hypr/theme-colors.lua"]
    B -->|M3 팔레트 생성| D["Matugen v4.2 Engine<br/>config.toml"]
    D -->|Render Template| C1
    D -->|Render Template| C2
    B -->|IPC: color-scheme-set / wallpaper-set| E3["Noctalia Desktop Shell"]
    C1 -->|IPC: kitty @ set-colors| E1["Kitty Terminal"]
    C2 -->|IPC: hyprctl reload| E2["Hyprland Compositor"]

theme-ctl 명령 실행 시 정적 Zenburn 토큰 생성 또는 Matugen 엔진을 통한 동적 팔레트 렌더링이 수행되며, 이후 각 애플리케이션의 제어 소켓(kitty @, hyprctl reload, noctalia msg)을 호출하여 화면 깜빡임 없이 즉시 반영합니다.

1. 패키지 설치

# 공식 저장소 필수 패키지 설치
sudo pacman -S --needed kitty python jq

# AUR 헬퍼를 통한 핵심 컴포넌트 설치
yay -S --needed matugen-bin hyprland noctalia-shell

2. 설정 파일

~/.config/matugen/config.toml

[config]
reload_apps = false

[templates.kitty]
input_path = "~/.config/matugen/templates/theme-colors.conf"
output_path = "~/.config/kitty/theme-colors.conf"

[templates.hyprland]
input_path = "~/.config/matugen/templates/theme-colors.lua"
output_path = "~/.config/hypr/theme-colors.lua"

~/.config/matugen/templates/theme-colors.lua

Hyprland 테마에 45° 각도의 1px 입체 그라디언트 엣지 라이트(rgba(ffffff55) 반사광 + 프라이머리 반투명 액센트)를 생성합니다.

return {
  primary = "{{colors.primary.default.hex}}",
  on_primary = "{{colors.on_primary.default.hex}}",
  surface = "{{colors.surface.default.hex}}",
  outline = "{{colors.outline.default.hex}}",
  active_border = { colors = { "rgba(ffffff55)", "rgba({{colors.primary.default.hex_stripped}}40)" }, angle = 45 },
  inactive_border = "rgba(ffffff12)",
}

~/bin/theme-ctl

~/dotfiles/bin/theme-ctl에 배치되고 ~/bin/theme-ctl로 심볼릭 링크되어 실행되는 Python 스크립트입니다.

#!/usr/bin/env python3
"""
theme-ctl: Central Theme Controller for Frosted Zenburn Glass & Material You.
Supports:
  - zenburn: Injects canonical hand-crafted Zenburn design tokens into all UI configs.
  - wallpaper [path]: Dynamically extracts Material You palette from wallpaper via Matugen.
  - status: Displays current theme state and token paths.
"""

import sys
import shutil
import subprocess
from pathlib import Path

HOME = Path.home()
CONFIG_DIR = HOME / ".config"
STATE_FILE = HOME / ".cache" / "theme-ctl-state.txt"

# Canonical Zenburn Design Tokens
ZENBURN_TOKENS = {
    "bg_base": "#3f3f3f",
    "bg_surface": "#4f4f4f",
    "fg_text": "#dcdccc",
    "fg_subtext": "#8f8f8f",
    "accent_primary": "#60b48a",
    "accent_secondary": "#709080",
    "warning_sand": "#f0dfaf",
    "info_cyan": "#8cd0d3",
    "error_brick": "#dca3a3",
}


def log(msg):
    print(f"[theme-ctl] {msg}", flush=True)


def reload_desktop_apps():
    """Trigger hot-reloads without killing the compositor session."""
    # 1. Hyprland
    if shutil.which("hyprctl"):
        try:
            subprocess.run(["hyprctl", "reload"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
        except Exception:
            pass

    # 2. Kitty
    kitty_theme = CONFIG_DIR / "kitty" / "theme-colors.conf"
    if kitty_theme.exists() and shutil.which("kitty"):
        try:
            subprocess.run(["kitty", "@", "set-colors", "--all", str(kitty_theme)],
                           stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
        except Exception:
            pass

    # 3. Noctalia
    if shutil.which("noctalia"):
        try:
            subprocess.run(["noctalia", "msg", "config-reload"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
        except Exception:
            pass
    log("UI 컴포넌트 핫 릴로드 완료.")


def apply_zenburn():
    """Write canonical Zenburn design tokens directly to application target configs."""
    log("Zenburn 고정 팔레트 적용 시작...")

    # 1. Kitty
    kitty_conf = CONFIG_DIR / "kitty" / "theme-colors.conf"
    kitty_conf.parent.mkdir(parents=True, exist_ok=True)
    kitty_content = f"""# Generated by theme-ctl (Zenburn Fixed SSOT)
foreground {ZENBURN_TOKENS["fg_text"]}
background {ZENBURN_TOKENS["bg_base"]}
selection_foreground #21322f
selection_background {ZENBURN_TOKENS["accent_primary"]}
cursor {ZENBURN_TOKENS["accent_primary"]}
cursor_text_color #21322f

active_border_color {ZENBURN_TOKENS["accent_primary"]}
inactive_border_color #3f3f3f
"""
    kitty_conf.write_text(kitty_content)

    # 2. Hyprland (45도 그라디언트 보더)
    hypr_lua = CONFIG_DIR / "hypr" / "theme-colors.lua"
    hypr_lua.parent.mkdir(parents=True, exist_ok=True)
    hypr_content = f"""-- Generated by theme-ctl (Zenburn Fixed SSOT)
return {{
  primary = "{ZENBURN_TOKENS["accent_primary"]}",
  on_primary = "#21322f",
  surface = "{ZENBURN_TOKENS["bg_base"]}",
  outline = "{ZENBURN_TOKENS["accent_secondary"]}",
  active_border = {{ colors = {{ "rgba(ffffff55)", "rgba(60b48a40)" }}, angle = 45 }},
  inactive_border = "rgba(ffffff12)",
}}
"""
    hypr_lua.write_text(hypr_content)

    # 3. Noctalia Shell
    if shutil.which("noctalia"):
        try:
            subprocess.run(["noctalia", "msg", "color-scheme-set", "community", "Everforest Alt"],
                           stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
        except Exception:
            pass

    STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
    STATE_FILE.write_text("zenburn")
    log("Zenburn 테마 렌더링 완료.")
    reload_desktop_apps()


def apply_wallpaper(image_path=None):
    """Dynamically extracts Material You palette from wallpaper via Noctalia / Matugen."""
    if not image_path:
        image_path = HOME / "Pictures" / "Wallpapers" / "bing-uhd.jpg"
    else:
        image_path = Path(image_path)

    if not image_path.exists():
        log(f"오류: 배경화면 파일을 찾을 수 없습니다 ({image_path}). Zenburn 모드로 대체합니다.")
        apply_zenburn()
        return

    # 1. Noctalia Shell 테마 동기화
    if shutil.which("noctalia"):
        try:
            subprocess.run(["noctalia", "msg", "wallpaper-set", str(image_path)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
            subprocess.run(["noctalia", "msg", "color-scheme-set", "wallpaper", "m3-content"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
            log("Noctalia 데스크톱 셸 배경화면 조화 색상 동기화 완료.")
        except Exception as e:
            log(f"Noctalia 테마 동기화 실패: {e}")

    # 2. Matugen CLI 렌더링
    if shutil.which("matugen"):
        log(f"Matugen으로 배경화면({image_path.name}) 색상 분석 및 렌더링 시작...")
        config_file = CONFIG_DIR / "matugen" / "config.toml"
        cmd = ["matugen", "image", str(image_path), "--source-color-index", "0"]
        if config_file.exists():
            cmd.extend(["-c", str(config_file)])

        res = subprocess.run(cmd, capture_output=True, text=True)
        if res.returncode == 0:
            STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
            STATE_FILE.write_text(f"wallpaper:{image_path}")
            log("Matugen 템플릿 렌더링 성공!")
            reload_desktop_apps()
            return
        else:
            log(f"Matugen 실행 오류:\n{res.stderr}")

    STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
    STATE_FILE.write_text(f"wallpaper:{image_path}")
    reload_desktop_apps()


def show_status():
    current = "unknown"
    if STATE_FILE.exists():
        current = STATE_FILE.read_text().strip()
    print("========================================")
    print(" theme-ctl status")
    print(f" Current Mode : {current}")
    print(f" Matugen CLI  : {'Available' if shutil.which('matugen') else 'Not Installed'}")
    print("========================================")


def main():
    if len(sys.argv) < 2:
        print("Usage: theme-ctl <zenburn|wallpaper [path]|status>")
        sys.exit(1)

    cmd = sys.argv[1].lower()
    if cmd in ("zenburn", "zb"):
        apply_zenburn()
    elif cmd in ("wallpaper", "wp", "auto"):
        target_img = sys.argv[2] if len(sys.argv) > 2 else None
        apply_wallpaper(target_img)
    elif cmd in ("status", "info"):
        show_status()
    else:
        print(f"Unknown command: {cmd}. Available: zenburn, wallpaper, status")
        sys.exit(1)


if __name__ == "__main__":
    main()

3. 서비스 실행 및 확인

# 실행 권한 부여
chmod +x ~/bin/theme-ctl

# 1. 고정 Zenburn 어스 톤 전체 적용
~/bin/theme-ctl zenburn

# 2. 지정 배경화면 기반 Material You 동적 색상 추출 및 적용
~/bin/theme-ctl wallpaper ~/Pictures/Wallpapers/sample.jpg

# 3. 테마 파이프라인 상태 점검
~/bin/theme-ctl status

# 4. Hyprland 활성 테두리 그라디언트 설정 반영 확인
hyprctl getoption general:col.active_border

4. 트러블슈팅

Kitty Remote Control 소켓 권한 미설정 시 색상 반영 실패

theme-ctl의 kitty @ set-colors 원격 제어가 실패하는 경우, ~/.config/kitty/kitty.conf에 allow_remote_control yes 또는 listen_on unix:${XDG_RUNTIME_DIR}/kitty.sock 옵션이 설정되어 있는지 확인하십시오. 설정이 누락되면 터미널 프로세스가 재시작되기 전까지 새로운 팔레트가 반영되지 않습니다.

배경화면 부재 시 Zenburn 자동 폴백 동작

bing-wallpaper.timer 네트워크 오류나 잘못된 파일 경로로 인해 배경화면 이미지가 존재하지 않을 경우, theme-ctl은 중단되지 않고 apply_zenburn() 고정 모드로 자동 전환하여 시스템 UI가 기본 토큰을 유지하도록 보호합니다.