Skip to content

Instantly share code, notes, and snippets.

@DamianPala
Created January 20, 2026 15:04
Show Gist options
  • Select an option

  • Save DamianPala/3270827fb8a1bf1fb2cb03445f93e3dd to your computer and use it in GitHub Desktop.

Select an option

Save DamianPala/3270827fb8a1bf1fb2cb03445f93e3dd to your computer and use it in GitHub Desktop.
Ghostty Terminal Theme Picker
#!/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