Created
January 20, 2026 15:04
-
-
Save DamianPala/3270827fb8a1bf1fb2cb03445f93e3dd to your computer and use it in GitHub Desktop.
Ghostty Terminal Theme Picker
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| # Pick a Ghostty theme using fzf, strip "(resources)"-style suffixes, | |
| # write it to the config, then tell the user to reload Ghostty. | |
| # | |
| # Requirements: | |
| # - fzf: sudo apt install fzf | |
| # - ghostty in PATH (unless using --user-only) | |
| # - perl (for in-place config edits) | |
| CFG="${XDG_CONFIG_HOME:-$HOME/.config}/ghostty/config" | |
| mkdir -p "$(dirname "$CFG")" | |
| touch "$CFG" | |
| # If you want a curated list, add theme names below and run: | |
| # ./ghostty_themes.sh --user-only | |
| USER_THEMES=( | |
| "Andromeda" | |
| "Arcoiris" | |
| "Ayu Mirage" | |
| "Builtin Pastel Dark" | |
| "Carbonfox" | |
| "Catppuccin Mocha" | |
| "Dracula" | |
| "Dracula+" | |
| "GitHub Dark Default" | |
| "GitLab Dark Grey" | |
| "Monokai Pro" | |
| "Monokai Pro Dusk" | |
| "Monokai Pro Spectrum" | |
| "Seti" | |
| "TokyoNight Night" | |
| "Wombat" | |
| ) | |
| USER_ONLY=0 | |
| AUTO_RELOAD=1 | |
| SUPPRESS_RELOAD_TOAST=1 | |
| usage() { | |
| cat <<'USAGE' | |
| Usage: ghostty_themes.sh [--user-only] [--no-auto-reload] | |
| --user-only Use only themes listed in USER_THEMES inside this script. | |
| --no-auto-reload Do not auto-reload Ghostty after writing the config. | |
| USAGE | |
| } | |
| die() { | |
| echo "$*" >&2 | |
| exit 1 | |
| } | |
| require_cmd() { | |
| command -v "$1" >/dev/null 2>&1 || die "Missing dependency: $1" | |
| } | |
| escape_for_double_quotes() { | |
| local s="$1" | |
| s="${s//\\/\\\\}" | |
| s="${s//\"/\\\"}" | |
| printf '%s' "$s" | |
| } | |
| while [ $# -gt 0 ]; do | |
| case "$1" in | |
| --user-only) | |
| USER_ONLY=1 | |
| ;; | |
| --no-auto-reload) | |
| AUTO_RELOAD=0 | |
| ;; | |
| -h|--help) | |
| usage | |
| exit 0 | |
| ;; | |
| --) | |
| shift | |
| break | |
| ;; | |
| *) | |
| die "Unknown option: $1" | |
| ;; | |
| esac | |
| shift | |
| done | |
| [ $# -eq 0 ] || die "Unexpected arguments: $*" | |
| require_cmd fzf | |
| require_cmd perl | |
| if [ "$USER_ONLY" -eq 0 ]; then | |
| require_cmd ghostty | |
| fi | |
| THEMES=() | |
| ensure_no_reload_toast() { | |
| local current val new_val t has_no=0 | |
| current="$(sed -nE 's/^[[:space:]]*app-notifications[[:space:]]*=[[:space:]]*(.*)$/\1/p' "$CFG" | head -n1)" | |
| if [ -z "$current" ]; then | |
| printf '\napp-notifications = no-config-reload\n' >> "$CFG" | |
| return 0 | |
| fi | |
| val="$(printf '%s' "$current" | tr -d '[:space:]')" | |
| val="${val#\"}"; val="${val%\"}" | |
| val="${val#\'}"; val="${val%\'}" | |
| if [ "$val" = "false" ]; then | |
| return 0 | |
| fi | |
| if [ "$val" = "true" ]; then | |
| new_val="no-config-reload" | |
| else | |
| IFS=',' read -r -a parts <<< "$val" | |
| new_val="" | |
| for t in "${parts[@]}"; do | |
| [ -n "$t" ] || continue | |
| if [ "$t" = "no-config-reload" ]; then | |
| has_no=1 | |
| new_val="${new_val:+$new_val,}$t" | |
| continue | |
| fi | |
| if [ "$t" = "config-reload" ]; then | |
| continue | |
| fi | |
| new_val="${new_val:+$new_val,}$t" | |
| done | |
| if [ "$has_no" -eq 0 ]; then | |
| new_val="${new_val:+$new_val,}no-config-reload" | |
| fi | |
| fi | |
| APP_NOTIFICATIONS_LINE="app-notifications = $new_val" \ | |
| perl -0777 -i -pe 's/^[[:space:]]*app-notifications[[:space:]]*=.*$/$ENV{APP_NOTIFICATIONS_LINE}/m' "$CFG" | |
| } | |
| if [ "$USER_ONLY" -eq 1 ]; then | |
| if [ "${#USER_THEMES[@]}" -eq 0 ]; then | |
| echo "USER_THEMES is empty. Add themes in the script or run without --user-only." >&2 | |
| exit 1 | |
| fi | |
| for t in "${USER_THEMES[@]}"; do | |
| [ -n "$t" ] || continue | |
| THEMES+=("$t") | |
| done | |
| else | |
| # Filter to dark themes by checking background luminance in each theme file. | |
| # Single awk process to avoid per-theme process overhead. | |
| mapfile -t THEMES < <( | |
| ghostty +list-themes --plain --path | awk ' | |
| function trim(s) { sub(/^[ \t]+/, "", s); sub(/[ \t]+$/, "", s); return s } | |
| function is_dark(hex, r, g, b, l) { | |
| if (hex == "") return 1 | |
| hex = trim(hex) | |
| if (substr(hex,1,1) == "#") hex = substr(hex,2) | |
| if (hex !~ /^[0-9A-Fa-f]{6}$/) return 1 | |
| r = strtonum("0x" substr(hex,1,2)) | |
| g = strtonum("0x" substr(hex,3,2)) | |
| b = strtonum("0x" substr(hex,5,2)) | |
| l = 0.2126*r + 0.7152*g + 0.0722*b | |
| return (l < 128) | |
| } | |
| { | |
| line = $0 | |
| idx = index(line, " /") | |
| if (idx == 0) next | |
| name = substr(line, 1, idx - 1) | |
| path = substr(line, idx + 2) | |
| if (substr(path,1,1) != "/") path = "/" path | |
| sub(/[ \t]*\([^)]*\)[ \t]*$/, "", name) | |
| bg = "" | |
| while ((getline l < path) > 0) { | |
| if (l ~ /^[ \t]*background[ \t]*=/) { | |
| split(l, parts, "=") | |
| bg = parts[2] | |
| gsub(/[ \t]/, "", bg) | |
| break | |
| } | |
| } | |
| close(path) | |
| if (is_dark(bg)) print name | |
| }' | |
| ) | |
| fi | |
| if [ "${#THEMES[@]}" -eq 0 ]; then | |
| echo "No themes available after filtering." >&2 | |
| exit 1 | |
| fi | |
| LAST_POS=0 | |
| USE_TAC=1 | |
| while true; do | |
| # Pick a theme (plain output is best for piping) | |
| FZF_OPTS=(--prompt='Ghostty theme> ' --height=60% --border --tac) | |
| if [ "$LAST_POS" -gt 0 ]; then | |
| FZF_OPTS+=(--bind "load:pos($LAST_POS)") | |
| else | |
| # With --tac, "last" moves to the top-most displayed item. | |
| FZF_OPTS+=(--bind "load:last") | |
| fi | |
| RAW_THEME="$(printf '%s\n' "${THEMES[@]}" | fzf "${FZF_OPTS[@]}")" | |
| [ -n "$RAW_THEME" ] || exit 0 | |
| # Strip any trailing " (something)" suffix (e.g. "Andromeda (resources)" -> "Andromeda") | |
| THEME="$(printf '%s' "$RAW_THEME" | sed -E 's/[[:space:]]*\([^)]*\)[[:space:]]*$//')" | |
| LAST_POS=0 | |
| total="${#THEMES[@]}" | |
| for i in "${!THEMES[@]}"; do | |
| if [ "${THEMES[$i]}" = "$RAW_THEME" ] || [ "${THEMES[$i]}" = "$THEME" ]; then | |
| if [ "$USE_TAC" -eq 1 ]; then | |
| LAST_POS=$((total - i)) | |
| else | |
| LAST_POS=$((i + 1)) | |
| fi | |
| break | |
| fi | |
| done | |
| # Write to config: replace existing theme line or append a new one. | |
| # Always quote the theme name because many themes contain spaces. | |
| THEME_LINE="theme = \"$(escape_for_double_quotes "$THEME")\"" | |
| if grep -qE '^[[:space:]]*theme[[:space:]]*=' "$CFG"; then | |
| # Replace the first matching theme line | |
| THEME_LINE="$THEME_LINE" perl -0777 -i -pe 's/^[[:space:]]*theme[[:space:]]*=.*$/$ENV{THEME_LINE}/m' "$CFG" | |
| else | |
| printf '\n%s\n' "$THEME_LINE" >> "$CFG" | |
| fi | |
| echo "Theme set to: $THEME" | |
| if [ "$AUTO_RELOAD" -eq 1 ]; then | |
| if [ "$SUPPRESS_RELOAD_TOAST" -eq 1 ]; then | |
| ensure_no_reload_toast | |
| fi | |
| # Try to auto-reload Ghostty by sending the default keybind. | |
| # This requires the Ghostty window to be focused. | |
| RELOADED=0 | |
| if [ -n "${WAYLAND_DISPLAY-}" ]; then | |
| if command -v wtype >/dev/null 2>&1; then | |
| wtype -M ctrl -M shift r -m shift -m ctrl | |
| RELOADED=1 | |
| elif command -v ydotool >/dev/null 2>&1; then | |
| ydotool key 29:1 42:1 19:1 19:0 42:0 29:0 | |
| RELOADED=1 | |
| fi | |
| elif [ -n "${DISPLAY-}" ]; then | |
| if command -v xdotool >/dev/null 2>&1; then | |
| xdotool key --clearmodifiers ctrl+shift+r | |
| RELOADED=1 | |
| fi | |
| fi | |
| if [ "$RELOADED" -eq 1 ]; then | |
| echo "Reloaded Ghostty config." | |
| else | |
| echo 'Now reload Ghostty (default shortcut): Ctrl+Shift+R' | |
| echo 'Tip: install xdotool (X11) or wtype/ydotool (Wayland) to auto-reload.' | |
| fi | |
| else | |
| echo 'Now reload Ghostty (default shortcut): Ctrl+Shift+R' | |
| echo 'Tip: re-run with --auto-reload if you want the script to trigger reloads.' | |
| fi | |
| done |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment