diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7e045d..6869319 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,12 +40,11 @@ jobs: tmp_home="$(mktemp -d)" export HOME="$tmp_home" ./bin/boo theme list >/dev/null - ./bin/boo preview all --plain >/dev/null for theme_file in themes/*.theme; do theme="${theme_file##*/}" theme="${theme%.theme}" - ./bin/boo preview "$theme" --plain >/dev/null + ./bin/boo theme "$theme" >/dev/null done - name: Install smoke test @@ -55,7 +54,6 @@ jobs: export HOME="$tmp_home" bash scripts/install.sh >/dev/null "$HOME/.local/bin/boo" theme list >/dev/null - "$HOME/.local/bin/boo" preview all --plain >/dev/null count=0 for theme_file in "$HOME/.config/boo/themes/"*.theme; do diff --git a/README.md b/README.md index 7a4fcc5..2efc8f6 100644 --- a/README.md +++ b/README.md @@ -72,12 +72,6 @@ boo opacity glass boo prompt set native ``` -Optional preview: - -```bash -boo preview all -``` - ## If Something Looks Wrong ### `boo: command not found` @@ -111,12 +105,21 @@ boo doctor ```bash boo theme list +boo theme select boo theme abyss boo crimson -boo preview all -boo preview abyss --plain ``` +Use the interactive selector for the easiest flow: + +```bash +boo theme select +``` + +- Shows all themes in a searchable list +- Live terminal-style preview panel on the right +- Press `Enter` to apply, `Esc` to cancel + ### Create your own theme ```bash @@ -187,6 +190,7 @@ Expected result: - `abyss`: deep indigo with violet-magenta accents (default) - `clay`: warm cream light mode with earthy terracotta accents +- `glacier`: icy daylight light mode with steel-blue accents - `crimson`: high-contrast red mode - `fallout`: RobCo phosphor CRT, warm amber-lime on near-black - `lunar`: desaturated monochrome noir diff --git a/bin/boo b/bin/boo index fcbf71f..bd5f8e3 100755 --- a/bin/boo +++ b/bin/boo @@ -64,7 +64,7 @@ print_help() { printf "\n" printf " ${cl}CONFIGURE${cr}\n" printf "\n" - _hcmd "theme" "[ · list · create · delete]" "apply themes & ANSI palette" + _hcmd "theme" "[ · list · select · create · delete]" "apply themes & ANSI palette" _hcmd "font" "[ · size]" "font family & size" _hcmd "opacity" "[glass · solid]" "background opacity" _hcmd "splash" "[ · none]" "startup splash art" @@ -74,7 +74,6 @@ print_help() { printf " ${cl}TOOLS${cr}\n" printf "\n" _hcmd "status" "" "current config at a glance" - _hcmd "preview" "[ · all]" "ANSI color swatches" _hcmd "reload" "[--unsafe]" "reload Ghostty config" _hcmd "upgrade" "" "update boo to latest release" _hcmd "doctor" "[fix]" "diagnose & fix issues" @@ -371,7 +370,7 @@ write_splash_name() { } list_splash_names() { - printf '%s\n' apple boo saturn minimal + printf '%s\n' apple boo saturn eclipse halo monolith minimal } print_splash_preview() { @@ -867,6 +866,222 @@ hex_luma() { printf '%d\n' $(( (r * 299 + g * 587 + b * 114) / 1000 )) } +theme_mode_for_bg() { + local bg="$1" + local bg_l + bg_l=$(hex_luma "$bg") + if (( bg_l >= 150 )); then + printf 'light\n' + else + printf 'dark\n' + fi +} + +theme_palette_strip_main() { + local hex rgb + local main_colors=( + "$_T_bg" + "$_T_fg" + "$_T_accent" + "$_T_cursor" + "$_T_selection_bg" + ) + + for hex in "${main_colors[@]}"; do + rgb="$(hex_to_rgb "$hex" 2>/dev/null || printf '128;128;128')" + printf '\033[38;2;%s;48;2;%sm███\033[0m' "$rgb" "$rgb" + done +} + +theme_selector_rows() { + local active_theme="${1:-}" + local name accent_rgb mode marker colored_name description row + local -a dark_rows=() + local -a light_rows=() + + while IFS= read -r name; do + [[ -n "$name" ]] || continue + if ! load_theme "$name"; then + continue + fi + + accent_rgb="$(hex_to_rgb "$_T_accent" 2>/dev/null || printf '204;68;255')" + mode="$(theme_mode_for_bg "$_T_bg")" + marker=" " + [[ "$name" == "$active_theme" ]] && marker="*" + colored_name="$(printf '\033[38;2;%sm%s\033[0m' "$accent_rgb" "$name")" + description="${_T_description:-no description}" + description="${description//$'\t'/ }" + + row="$(printf '%s\t%s\t%s\t%s\t%s' "$name" "$marker" "$mode" "$colored_name" "$description")" + if [[ "$mode" == "light" ]]; then + light_rows+=("$row") + else + dark_rows+=("$row") + fi + done < <(list_theme_names) + + if (( ${#dark_rows[@]} > 0 )); then + printf '__section__\t \t \t\033[1mDARK THEMES\033[0m\t\n' + printf '%s\n' "${dark_rows[@]}" + fi + if (( ${#light_rows[@]} > 0 )); then + if (( ${#dark_rows[@]} > 0 )); then + printf '__section__\t \t \t\033[2m────────────────\033[0m\t\n' + fi + printf '__section__\t \t \t\033[1mLIGHT THEMES\033[0m\t\n' + printf '%s\n' "${light_rows[@]}" + fi +} + +preview_fit_text() { + local width="$1" + local text="$2" + + (( width < 1 )) && return + if (( ${#text} > width )); then + if (( width > 3 )); then + text="${text:0:$((width - 3))}..." + else + text="${text:0:$width}" + fi + fi + printf '%-*s' "$width" "$text" +} + +print_preview_terminal_row() { + local inner="$1" + local bg_rgb="$2" + local fg_rgb="$3" + local text="$4" + local border_rgb="${5:-140;140;140}" + local row + + row="$(preview_fit_text "$inner" "$text")" + printf ' \033[38;2;%sm│\033[48;2;%sm\033[38;2;%sm%s\033[0m\033[38;2;%sm│\033[0m\n' \ + "$border_rgb" "$bg_rgb" "$fg_rgb" "$row" "$border_rgb" +} + +print_theme_selector_preview() { + local theme="${1:-}" + local accent_rgb mode + local bg_rgb fg_rgb cursor_rgb cursor_text_rgb sel_bg_rgb sel_fg_rgb + local ok_rgb warn_rgb err_rgb info_rgb + local border_rgb muted_rgb + local preview_cols inner line border + + if [[ -z "$theme" ]]; then + printf 'No theme selected.\n' + return 1 + fi + if [[ "$theme" == "__section__" ]]; then + printf '\n Select a theme row to preview it.\n\n' + return 0 + fi + + if ! load_theme "$theme"; then + printf 'Invalid theme: %s\n' "$theme" + if [[ -n "${_THEME_LOAD_ERROR:-}" ]]; then + printf '%s\n' "$_THEME_LOAD_ERROR" + fi + return 1 + fi + + accent_rgb="$(hex_to_rgb "$_T_accent" 2>/dev/null || printf '204;68;255')" + bg_rgb="$(hex_to_rgb "$_T_bg" 2>/dev/null || printf '12;12;12')" + fg_rgb="$(hex_to_rgb "$_T_fg" 2>/dev/null || printf '220;220;220')" + cursor_rgb="$(hex_to_rgb "$_T_cursor" 2>/dev/null || printf '255;255;255')" + cursor_text_rgb="$(hex_to_rgb "$_T_cursor_text" 2>/dev/null || printf '0;0;0')" + sel_bg_rgb="$(hex_to_rgb "$_T_selection_bg" 2>/dev/null || printf '70;70;70')" + sel_fg_rgb="$(hex_to_rgb "$_T_selection_fg" 2>/dev/null || printf '235;235;235')" + ok_rgb="$(hex_to_rgb "$_T_pal_10" 2>/dev/null || printf '120;210;120')" + warn_rgb="$(hex_to_rgb "$_T_pal_11" 2>/dev/null || printf '230;190;90')" + err_rgb="$(hex_to_rgb "$_T_pal_1" 2>/dev/null || printf '230;95;95')" + info_rgb="$(hex_to_rgb "$_T_pal_12" 2>/dev/null || printf '110;170;240')" + border_rgb="$(hex_to_rgb "$_T_pal_8" 2>/dev/null || printf '120;120;120')" + muted_rgb="$(hex_to_rgb "$_T_pal_7" 2>/dev/null || printf '170;170;170')" + mode="$(theme_mode_for_bg "$_T_bg")" + preview_cols="${FZF_PREVIEW_COLUMNS:-80}" + (( preview_cols < 44 )) && preview_cols=44 + inner=$(( preview_cols - 4 )) + border="$(printf '%*s' "$inner" '' | tr ' ' '─')" + + printf '\n' + printf ' \033[1m\033[38;2;%sm%s\033[0m (%s)\n' "$accent_rgb" "$theme" "$mode" + printf ' \033[38;5;244m%s\033[0m\n\n' "${_T_description:-no description}" + + printf ' \033[38;2;%sm╭%s╮\033[0m\n' "$border_rgb" "$border" + line="$(preview_fit_text "$inner" ' o o o boo terminal')" + printf ' \033[38;2;%sm│\033[48;2;%sm\033[38;2;%sm%s\033[0m\033[38;2;%sm│\033[0m\n' \ + "$border_rgb" "$sel_bg_rgb" "$sel_fg_rgb" "$line" "$border_rgb" + print_preview_terminal_row "$inner" "$bg_rgb" "$fg_rgb" " > boo theme $theme" "$border_rgb" + print_preview_terminal_row "$inner" "$bg_rgb" "$ok_rgb" " [ok] theme applied" "$border_rgb" + print_preview_terminal_row "$inner" "$bg_rgb" "$fg_rgb" " > boo doctor" "$border_rgb" + print_preview_terminal_row "$inner" "$bg_rgb" "$warn_rgb" " 7 ok - 1 warn - 0 fail" "$border_rgb" + print_preview_terminal_row "$inner" "$bg_rgb" "$fg_rgb" " > echo ready" "$border_rgb" + print_preview_terminal_row "$inner" "$bg_rgb" "$info_rgb" " ready" "$border_rgb" + line="$(preview_fit_text "$inner" ' typing...')" + printf ' \033[38;2;%sm│\033[48;2;%sm\033[38;2;%sm%s\033[48;2;%sm\033[38;2;%sm|\033[48;2;%sm\033[38;2;%sm\033[0m\033[38;2;%sm│\033[0m\n' \ + "$border_rgb" "$bg_rgb" "$fg_rgb" "${line:0:$((inner - 1))}" "$cursor_rgb" "$cursor_text_rgb" "$bg_rgb" "$fg_rgb" "$border_rgb" + printf ' \033[38;2;%sm╰%s╯\033[0m\n' "$border_rgb" "$border" + + printf '\n \033[38;2;%smmain colors\033[0m %s\n\n' "$muted_rgb" "$(theme_palette_strip_main)" +} + +cmd_theme_select() { + local _t active chosen preview_cmd selected_line rows + _t="$(read_theme_file)" + _init_ui "$_t" + + if ! command -v fzf >/dev/null 2>&1; then + printf "\n ${_cv}fzf is required for interactive theme selection${_cr}\n" + printf " ${_cd}install fzf, then run:${_cr} ${_ca}boo theme select${_cr}\n" + printf " ${_cd}fallback:${_cr} ${_ca}boo theme list${_cr}\n\n" + return 1 + fi + + active="$(read_theme_file)" + rows="$(theme_selector_rows "$active")" + if [[ -z "$rows" ]]; then + printf "\n ${_cd}No themes found in %s${_cr}\n\n" "$THEMES_DIR" + return 1 + fi + + printf -v preview_cmd '%q __theme_selector_preview {1}' "${BOO_BIN_DIR}/boo" + + while true; do + selected_line="$( + printf '%s\n' "$rows" | \ + fzf --ansi \ + --delimiter=$'\t' \ + --with-nth=2,4 \ + --height=90% \ + --layout=reverse \ + --border \ + --prompt="theme > " \ + --header=$'Enter: apply theme - Esc: cancel' \ + --preview="$preview_cmd" \ + --preview-window="right:62%:nowrap" + )" || return 0 + + chosen="${selected_line%%$'\t'*}" + [[ -n "$chosen" ]] || continue + if [[ "$chosen" == "__section__" ]]; then + continue + fi + break + done + + if [[ "$chosen" == "$active" ]]; then + printf "\n ${_cd}%s is already active${_cr}\n\n" "$chosen" + return 0 + fi + + apply_theme "$chosen" + print_theme_applied_card "$chosen" + cmd_reload > /dev/null 2>&1 || true +} + theme_text_color_for_bg() { local bg="$1" local bg_l dark_l light_l diff_dark diff_light @@ -1152,7 +1367,7 @@ apply_prompt_theme() { left_os_bg="$(blend_color "$_T_bg" "$_T_fg" 140)" left_user_bg="$(blend_color "$_T_bg" "$_T_fg" 210)" left_path_bg="$(blend_color "$_T_bg" "$_T_fg" 90)" - left_git_bg="$(blend_color "$_T_bg" "$_T_fg" 280)" + left_git_bg="${_T_omp_git_bg}" left_exec_bg="$(blend_color "$_T_bg" "$_T_fg" 360)" left_root_bg="$_T_pal_1" left_root_fg="$(theme_text_color_for_bg "$left_root_bg")" @@ -1183,8 +1398,9 @@ apply_prompt_theme() { tf_fg="$(readable_color_or_theme_text "$chip_infra_bg" "${_T_omp_tf_fg}")" sysinfo_fg="$(readable_color_or_theme_text "$chip_metric_bg" "${_T_omp_sysinfo_fg}")" time_fg="$(readable_color_or_theme_text "$chip_time_bg" "${_T_ui_label:-$_T_pal_7}")" - git_dirty_bg="$_T_pal_3" - git_diverged_bg="$_T_pal_5" + # Keep git state highlighting subtle so light themes don't get muddy/dark blocks. + git_dirty_bg="$(blend_color "$left_git_bg" "$_T_pal_3" 240)" + git_diverged_bg="$(blend_color "$left_git_bg" "$_T_pal_5" 240)" git_dirty_tpl="{{ if or (.Working.Changed) (.Staging.Changed) }}${git_dirty_bg}{{ end }}" git_diverged_tpl="{{ if and (gt .Ahead 0) (gt .Behind 0) }}${git_diverged_bg}{{ end }}" @@ -2116,26 +2332,71 @@ cmd_theme() { cmd_theme_delete "$theme" return ;; + select|choose|pick) + cmd_theme_select + return + ;; list) - local _t name accent_rgb count - local description + local _t name accent_rgb count mode palette row + local -a dark_rows=() + local -a light_rows=() + local -a invalid_rows=() _t="$(read_theme_file)" _init_ui "$_t" - printf "\n ${_cd}THEMES${_cr}\n\n" + printf "\n ${_cd}THEMES${_cr}\n" count=0 while IFS= read -r name; do [[ -n "$name" ]] || continue count=$((count + 1)) if ! load_theme "$name"; then - printf " ${_cb}%s${_cr} ${_cv}[invalid theme file]${_cr}\n" "$name" + invalid_rows+=(" ${_cb}${name}${_cr} ${_cv}[invalid theme file]${_cr}") continue fi - description="$_T_description" accent_rgb="$(hex_to_rgb "$_T_accent" 2>/dev/null || printf '204;68;255')" - printf " ${_cb}\033[38;2;%sm%-10s${_cr} ${_cv}%s${_cr}\n" "$accent_rgb" "$name" "$description" + palette="$(theme_palette_strip_main)" + mode="$(theme_mode_for_bg "$_T_bg")" + if [[ "$mode" == "light" ]]; then + printf -v row " ${_cb}\033[38;2;%sm%-10s${_cr} %s" "$accent_rgb" "$name" "$palette" + light_rows+=("$row") + else + printf -v row " ${_cb}\033[38;2;%sm%-10s${_cr} %s" "$accent_rgb" "$name" "$palette" + dark_rows+=("$row") + fi done < <(list_theme_names) if (( count == 0 )); then - printf " ${_cd}No themes found in %s${_cr}\n" "$THEMES_DIR" + printf "\n ${_cd}No themes found in %s${_cr}\n\n" "$THEMES_DIR" + return + fi + + local idx + if (( ${#dark_rows[@]} > 0 )); then + printf "\n ${_cl}DARK${_cr}\n\n" + for (( idx = 0; idx < ${#dark_rows[@]}; idx++ )); do + printf "%b\n" "${dark_rows[$idx]}" + if (( idx + 1 < ${#dark_rows[@]} )); then + printf "\n" + fi + done + fi + + if (( ${#light_rows[@]} > 0 )); then + printf "\n ${_cl}LIGHT${_cr}\n\n" + for (( idx = 0; idx < ${#light_rows[@]}; idx++ )); do + printf "%b\n" "${light_rows[$idx]}" + if (( idx + 1 < ${#light_rows[@]} )); then + printf "\n" + fi + done + fi + + if (( ${#invalid_rows[@]} > 0 )); then + printf "\n ${_cv}INVALID${_cr}\n\n" + for (( idx = 0; idx < ${#invalid_rows[@]}; idx++ )); do + printf "%b\n" "${invalid_rows[$idx]}" + if (( idx + 1 < ${#invalid_rows[@]} )); then + printf "\n" + fi + done fi printf "\n" ;; @@ -2174,7 +2435,7 @@ cmd_theme() { exit 1 else printf 'Invalid theme command: %s\n' "$action" >&2 - printf 'Usage: boo theme [list|set |]\n' >&2 + printf 'Usage: boo theme [list|select|set |]\n' >&2 exit 1 fi ;; @@ -2450,7 +2711,7 @@ cmd_splash() { printf "\n ${_ca}✓${_cr} ${_cv}splash → %s (default)${_cr}\n\n" "$DEFAULT_SPLASH" print_shell_apply_hint ;; - apple|boo|saturn|minimal) + apple|boo|saturn|eclipse|halo|monolith|minimal) write_splash_name "$action" printf "\n ${_ca}✓${_cr} ${_cv}splash → %s${_cr}\n\n" "$action" print_shell_apply_hint @@ -2820,9 +3081,9 @@ case "$subcommand" in shift || true cmd_reload "${1:-}" ;; - preview) + __theme_selector_preview) shift || true - cmd_preview "$@" + print_theme_selector_preview "${1:-}" ;; splash) shift || true diff --git a/themes/glacier.theme b/themes/glacier.theme new file mode 100644 index 0000000..ea70aac --- /dev/null +++ b/themes/glacier.theme @@ -0,0 +1,35 @@ +# builtin +description=icy daylight light mode - steel blue accents +accent=#3c79b8 +bg=#eef4fb +fg=#253242 +cursor=#3c79b8 +cursor_text=#eef4fb +selection_bg=#d7e5f5 +selection_fg=#1a2633 +ui_dim=#617489 +ui_label=#526579 +ui_value=#2f4357 +omp_chip_cloud_bg=#cfe0f2 +omp_go_fg=#2f6f54 +omp_az_fg=#3c79b8 +omp_gcp_fg=#4b88c5 +omp_tf_fg=#6b5fb0 +omp_sysinfo_fg=#5b7691 +omp_git_bg=#c4d8ee +pal_0=#253242 +pal_1=#b5525e +pal_2=#2f7a5d +pal_3=#8a6d2f +pal_4=#3e6fa5 +pal_5=#6e5fae +pal_6=#2d7f8a +pal_7=#dce8f5 +pal_8=#4a5a6b +pal_9=#c7616d +pal_10=#3f8c6d +pal_11=#9b8040 +pal_12=#4e83be +pal_13=#7c6fc0 +pal_14=#3b95a0 +pal_15=#eef4fb