Files
dotfiles/rider-palette/.config/rider-palette/generate.py

407 lines
15 KiB
Python

#!/usr/bin/env python3
from __future__ import annotations
import json
from pathlib import Path
from textwrap import dedent
ROOT = Path(__file__).resolve().parent
PALETTE_FILE = ROOT / "palette.json"
def load_palette() -> dict[str, str]:
data = json.loads(PALETTE_FILE.read_text())
return data["colors"]
def hex_no_hash(value: str) -> str:
return value.lstrip("#")
def hex_to_rgb(value: str) -> tuple[int, int, int]:
value = hex_no_hash(value)
return tuple(int(value[index:index + 2], 16) for index in (0, 2, 4))
def camel_from_snake(name: str) -> str:
head, *tail = name.split("_")
return head + "".join(part.capitalize() for part in tail)
def write(path: Path, content: str) -> None:
path.write_text(content.rstrip() + "\n")
def sgr_fg(value: str, *, bold: bool = False) -> str:
red, green, blue = hex_to_rgb(value)
parts = []
if bold:
parts.append("1")
parts.append(f"38;2;{red};{green};{blue}")
return ";".join(parts)
def build_ls_colors(colors: dict[str, str]) -> str:
entries = {
"no": "0",
"fi": "0",
"di": sgr_fg(colors["keyword"], bold=True),
"ln": sgr_fg(colors["field"], bold=True),
"or": sgr_fg(colors["error"]),
"mi": "0",
"so": sgr_fg(colors["field"]),
"pi": sgr_fg(colors["string"]),
"do": sgr_fg(colors["func"], bold=True),
"bd": sgr_fg(colors["number"]),
"cd": sgr_fg(colors["number"]),
"su": sgr_fg(colors["error"], bold=True),
"sg": sgr_fg(colors["escape"], bold=True),
"ex": sgr_fg(colors["func"], bold=True),
"*.tar": sgr_fg(colors["type_alt"]),
"*.tgz": sgr_fg(colors["type_alt"]),
"*.gz": sgr_fg(colors["type_alt"]),
"*.bz2": sgr_fg(colors["type_alt"]),
"*.xz": sgr_fg(colors["type_alt"]),
"*.zip": sgr_fg(colors["type_alt"]),
"*.7z": sgr_fg(colors["type_alt"]),
"*.zst": sgr_fg(colors["type_alt"]),
"*.rar": sgr_fg(colors["type_alt"]),
"*.jpg": sgr_fg(colors["type"]),
"*.jpeg": sgr_fg(colors["type"]),
"*.png": sgr_fg(colors["type"]),
"*.gif": sgr_fg(colors["type"]),
"*.svg": sgr_fg(colors["type"]),
"*.webp": sgr_fg(colors["type"]),
"*.mp3": sgr_fg(colors["type"]),
"*.flac": sgr_fg(colors["type"]),
"*.wav": sgr_fg(colors["type"]),
"*.mp4": sgr_fg(colors["type"]),
"*.mkv": sgr_fg(colors["type"]),
"*.mov": sgr_fg(colors["type"]),
"*.pdf": sgr_fg(colors["comment"]),
"*.md": sgr_fg(colors["comment"]),
"*.txt": sgr_fg(colors["comment"]),
"*.log": sgr_fg(colors["comment"]),
"*.conf": sgr_fg(colors["string"]),
"*.json": sgr_fg(colors["string"]),
"*.yaml": sgr_fg(colors["string"]),
"*.yml": sgr_fg(colors["string"]),
"*.toml": sgr_fg(colors["string"]),
"*.ini": sgr_fg(colors["string"]),
"*.sh": sgr_fg(colors["func"]),
"*.bash": sgr_fg(colors["func"]),
"*.zsh": sgr_fg(colors["func"]),
"*.py": sgr_fg(colors["field"]),
"*.js": sgr_fg(colors["field"]),
"*.ts": sgr_fg(colors["field"]),
"*.tsx": sgr_fg(colors["field"]),
"*.jsx": sgr_fg(colors["field"]),
"*.lua": sgr_fg(colors["field"]),
"*.rs": sgr_fg(colors["field"]),
"*.go": sgr_fg(colors["field"]),
"*.c": sgr_fg(colors["field"]),
"*.h": sgr_fg(colors["field"]),
"*.cpp": sgr_fg(colors["field"]),
"*.hpp": sgr_fg(colors["field"]),
"*.cs": sgr_fg(colors["field"]),
}
return ":".join(f"{key}={value}" for key, value in entries.items())
def render_palette_sh(colors: dict[str, str]) -> str:
lines = [
"# Generated from palette.json by generate.py. Do not edit directly.",
"",
]
for name, value in colors.items():
lines.append(f'export RIDER_{name.upper()}="{value}"')
lines.append("")
lines.append("# Shared Rider palette for GNU ls and compatible tools.")
lines.append(f'export LS_COLORS="{build_ls_colors(colors)}"')
return "\n".join(lines)
def render_palette_css(colors: dict[str, str]) -> str:
lines = [
"/* Generated from palette.json by generate.py. Do not edit directly. */",
]
for name, value in colors.items():
lines.append(f"@define-color rider-{name.replace('_', '-')} {value};")
return "\n".join(lines)
def render_palette_hypr(colors: dict[str, str]) -> str:
lines = [
"# Generated from palette.json by generate.py. Do not edit directly.",
"# Usage:",
"# source = ~/.config/rider-palette/palette.hyprland.conf",
"",
]
for name, value in colors.items():
lines.append(f"${name} = rgb({hex_no_hash(value)})")
lines.append(f"${camel_from_snake(name)}Alpha = {hex_no_hash(value)}")
return "\n".join(lines)
def render_palette_rasi(colors: dict[str, str]) -> str:
lines = [
"/* Generated from palette.json by generate.py. Do not edit directly. */",
"* {",
]
for name, value in colors.items():
lines.append(f" rider-{name.replace('_', '-')}: {value};")
lines.append("}")
return "\n".join(lines)
def render_tmux_conf(colors: dict[str, str]) -> str:
return dedent(
f"""
# Generated from palette.json by generate.py. Do not edit directly.
# Source this near the end of ~/.tmux.conf.
set -g status-style "bg={colors['bg']},fg={colors['fg_bright']}"
set -g status-left-style "bg={colors['bg']},fg={colors['fg_bright']}"
set -g status-right-style "bg={colors['bg']},fg={colors['fg_bright']}"
set -g status-left-length 48
set -g status-right-length 80
set -g status-justify centre
set -g window-status-separator " "
set -g status-left "#[fg={colors['bg']},bg={colors['func']},bold] #S #[fg={colors['func']},bg={colors['bg']}]"
set -g status-right "#[fg={colors['field']},bg={colors['bg']}]#[fg={colors['bg']},bg={colors['field']}] %Y-%m-%d #[fg={colors['string']},bg={colors['field']}]#[fg={colors['bg']},bg={colors['string']}] %H:%M#[fg={colors['string']},bg={colors['bg']}]"
set -g message-style "bg={colors['cursor_line']},fg={colors['fg_bright']}"
set -g message-command-style "bg={colors['cursor_line']},fg={colors['fg_bright']}"
set -g mode-style "bg={colors['selection']},fg={colors['fg_bright']}"
set -g pane-border-style "fg={colors['border']}"
set -g pane-active-border-style "fg={colors['keyword']}"
set -g clock-mode-colour "{colors['keyword']}"
set -g window-status-style "bg={colors['bg']},fg={colors['fg_gutter']}"
set -g window-status-current-style "bg={colors['bg']},fg={colors['fg_bright']}"
set -g window-status-current-format "#[fg={colors['func']},bg={colors['bg']}]#[fg={colors['bg']},bg={colors['func']},bold] #I #[fg={colors['func']},bg={colors['border']}]#[fg={colors['fg_bright']},bg={colors['border']}] #W #[fg={colors['border']},bg={colors['bg']}]"
set -g window-status-format "#[fg={colors['cursor_line']},bg={colors['bg']}]#[fg={colors['fg_gutter']},bg={colors['cursor_line']}] #I #[fg={colors['cursor_line']},bg={colors['border']}]#[fg={colors['fg_bright']},bg={colors['border']}] #W #[fg={colors['border']},bg={colors['bg']}]"
"""
).strip()
def render_alacritty_toml(colors: dict[str, str]) -> str:
return dedent(
f"""
# Generated from palette.json by generate.py. Do not edit directly.
[colors.primary]
background = "{colors['bg']}"
foreground = "{colors['fg']}"
dim_foreground = "{colors['fg_gutter']}"
bright_foreground = "{colors['fg_bright']}"
[colors.cursor]
text = "{colors['bg']}"
cursor = "{colors['fg_bright']}"
[colors.vi_mode_cursor]
text = "{colors['bg']}"
cursor = "{colors['keyword']}"
[colors.selection]
text = "{colors['fg_bright']}"
background = "{colors['selection']}"
[colors.search.matches]
foreground = "{colors['fg_bright']}"
background = "{colors['border']}"
[colors.search.focused_match]
foreground = "{colors['bg']}"
background = "{colors['func']}"
[colors.hints.start]
foreground = "{colors['bg']}"
background = "{colors['string']}"
[colors.hints.end]
foreground = "{colors['fg_bright']}"
background = "{colors['cursor_line']}"
[colors.line_indicator]
foreground = "None"
background = "None"
[colors.footer_bar]
foreground = "{colors['fg_bright']}"
background = "{colors['bg']}"
[colors.normal]
black = "{colors['bg']}"
red = "{colors['error']}"
green = "{colors['func']}"
yellow = "{colors['string']}"
blue = "{colors['keyword']}"
magenta = "{colors['type']}"
cyan = "{colors['field']}"
white = "{colors['fg']}"
[colors.bright]
black = "{colors['border']}"
red = "{colors['error']}"
green = "{colors['comment']}"
yellow = "{colors['string']}"
blue = "{colors['keyword']}"
magenta = "{colors['type_alt']}"
cyan = "{colors['field']}"
white = "{colors['fg_bright']}"
[colors.dim]
black = "{colors['cursor_line']}"
red = "{colors['error']}"
green = "{colors['comment']}"
yellow = "{colors['string']}"
blue = "{colors['keyword']}"
magenta = "{colors['type']}"
cyan = "{colors['field']}"
white = "{colors['fg_gutter']}"
"""
).strip()
def render_bash_prompt() -> str:
return dedent(
"""
# Generated from palette.json by generate.py. Do not edit directly.
# Rider-colored bash prompt using the shared palette.
# shellcheck shell=bash
if [ -r "$HOME/.config/rider-palette/palette.sh" ]; then
. "$HOME/.config/rider-palette/palette.sh"
fi
rider_fg() {
local hex="${1#\\#}"
printf '\\[\\033[38;2;%d;%d;%dm\\]' "$((16#${hex:0:2}))" "$((16#${hex:2:2}))" "$((16#${hex:4:2}))"
}
RIDER_RESET='\\[\\033[0m\\]'
__rider_git_branch() {
command -v git >/dev/null 2>&1 || return 0
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || return 0
local branch
branch="$(git symbolic-ref --quiet --short HEAD 2>/dev/null || git rev-parse --short HEAD 2>/dev/null)" || return 0
[ -n "$branch" ] || return 0
printf ' %sgit:%s%s' "$(rider_fg "$RIDER_FUNC")" "$branch" "$RIDER_RESET"
}
__rider_set_bash_prompt() {
PS1="${debian_chroot:+($debian_chroot)}$(rider_fg "$RIDER_FG_BRIGHT")\\u@\\h${RIDER_RESET}$(rider_fg "$RIDER_BORDER"):$(rider_fg "$RIDER_KEYWORD")\\w${RIDER_RESET}$(__rider_git_branch)$(rider_fg "$RIDER_BORDER") \\\\$ ${RIDER_RESET}"
}
PROMPT_COMMAND=__rider_set_bash_prompt
"""
).strip()
def render_p10k_zsh() -> str:
return dedent(
"""
# Generated from palette.json by generate.py. Do not edit directly.
# Rider-colored Powerlevel10k overrides using the shared palette.
[[ -r "$HOME/.config/rider-palette/palette.sh" ]] && source "$HOME/.config/rider-palette/palette.sh"
typeset -g POWERLEVEL9K_BACKGROUND="$RIDER_BG"
typeset -g POWERLEVEL9K_MULTILINE_FIRST_PROMPT_PREFIX="%F{$RIDER_BORDER}╭─%f"
typeset -g POWERLEVEL9K_MULTILINE_NEWLINE_PROMPT_PREFIX="%F{$RIDER_BORDER}├─%f"
typeset -g POWERLEVEL9K_MULTILINE_LAST_PROMPT_PREFIX="%F{$RIDER_BORDER}╰─%f"
typeset -g POWERLEVEL9K_MULTILINE_FIRST_PROMPT_SUFFIX="%F{$RIDER_BORDER}─╮%f"
typeset -g POWERLEVEL9K_MULTILINE_NEWLINE_PROMPT_SUFFIX="%F{$RIDER_BORDER}─┤%f"
typeset -g POWERLEVEL9K_MULTILINE_LAST_PROMPT_SUFFIX="%F{$RIDER_BORDER}─╯%f"
typeset -g POWERLEVEL9K_LEFT_SUBSEGMENT_SEPARATOR="%F{$RIDER_BORDER}\\uE0B1%f"
typeset -g POWERLEVEL9K_RIGHT_SUBSEGMENT_SEPARATOR="%F{$RIDER_BORDER}\\uE0B3%f"
typeset -g POWERLEVEL9K_OS_ICON_FOREGROUND="$RIDER_FG_BRIGHT"
typeset -g POWERLEVEL9K_DIR_FOREGROUND="$RIDER_KEYWORD"
typeset -g POWERLEVEL9K_DIR_SHORTENED_FOREGROUND="$RIDER_TYPE"
typeset -g POWERLEVEL9K_DIR_ANCHOR_FOREGROUND="$RIDER_FUNC"
typeset -g POWERLEVEL9K_VCS_VISUAL_IDENTIFIER_COLOR="$RIDER_FUNC"
typeset -g POWERLEVEL9K_VCS_LOADING_VISUAL_IDENTIFIER_COLOR="$RIDER_BORDER"
typeset -g POWERLEVEL9K_VCS_CLEAN_FOREGROUND="$RIDER_FUNC"
typeset -g POWERLEVEL9K_VCS_UNTRACKED_FOREGROUND="$RIDER_FIELD"
typeset -g POWERLEVEL9K_VCS_MODIFIED_FOREGROUND="$RIDER_NUMBER"
typeset -g POWERLEVEL9K_STATUS_OK_FOREGROUND="$RIDER_FUNC"
typeset -g POWERLEVEL9K_STATUS_OK_PIPE_FOREGROUND="$RIDER_FUNC"
typeset -g POWERLEVEL9K_STATUS_ERROR_FOREGROUND="$RIDER_ERROR"
typeset -g POWERLEVEL9K_STATUS_ERROR_SIGNAL_FOREGROUND="$RIDER_ERROR"
typeset -g POWERLEVEL9K_STATUS_ERROR_PIPE_FOREGROUND="$RIDER_ERROR"
typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_FOREGROUND="$RIDER_STRING"
typeset -g POWERLEVEL9K_TIME_FOREGROUND="$RIDER_FIELD"
typeset -g POWERLEVEL9K_DOTNET_VERSION_FOREGROUND="$RIDER_TYPE"
typeset -g POWERLEVEL9K_CONTEXT_FOREGROUND="$RIDER_FG_BRIGHT"
"""
).strip()
def render_readme() -> str:
return dedent(
"""
# Rider Palette Reuse
`palette.json` is the single source of truth for your shared Rider palette.
Regenerate all derived files with:
```bash
python3 ~/.config/rider-palette/generate.py
```
Generated outputs:
- `palette.sh`: shell env vars plus shared `LS_COLORS` for prompts and scripts
- `palette.css`: CSS variables for Waybar and Wofi
- `palette.hyprland.conf`: Hyprlang variables for Hyprland and Hyprlock
- `palette.rasi`: Rasi variables for Rofi
- `tmux.conf`: tmux status and pane colors
- `alacritty.toml`: Alacritty color theme
- `bash-prompt.sh`: bash prompt using the shared palette
- `p10k.zsh`: Powerlevel10k overrides using the shared palette
This package is meant to be stowed from the dotfiles repo so that
`~/.config/rider-palette/*` becomes available to the rest of the system.
"""
).strip()
def main() -> None:
colors = load_palette()
outputs = {
ROOT / "palette.sh": render_palette_sh(colors),
ROOT / "palette.css": render_palette_css(colors),
ROOT / "palette.hyprland.conf": render_palette_hypr(colors),
ROOT / "palette.rasi": render_palette_rasi(colors),
ROOT / "tmux.conf": render_tmux_conf(colors),
ROOT / "alacritty.toml": render_alacritty_toml(colors),
ROOT / "bash-prompt.sh": render_bash_prompt(),
ROOT / "p10k.zsh": render_p10k_zsh(),
ROOT / "README.md": render_readme(),
}
for path, content in outputs.items():
write(path, content)
if __name__ == "__main__":
main()