From d3eeeda8b62f24a87b3da495a29b4a2e2973c9f8 Mon Sep 17 00:00:00 2001 From: Nicola Spieser Date: Wed, 25 Feb 2026 04:24:29 +0100 Subject: [PATCH 01/10] fix: instant prompt "bad substitution" when LC_TIME contains dots/hyphens (#18) The LC_TIME save/restore stash expressions in instant_prompt_time and instant_prompt_date used the ${${var::=val}+} pattern to silently assign variables. The + operator interprets the assigned value as a parameter name, which causes "bad substitution" when the value contains non-identifier characters (e.g., en_US.UTF-8 with dots and hyphens). Replace +} with ##*} for the locale save/restore steps. The ##* glob strips the entire value, achieving the same silent-discard effect without interpreting it as a parameter name. Also bumps __p9k_instant_prompt_version to 48 to invalidate stale cached instant prompt files. Reported-by: alejandroqh Helped-by: romkatv --- internal/p10k.zsh | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/internal/p10k.zsh b/internal/p10k.zsh index 78bb42af..547f45f9 100644 --- a/internal/p10k.zsh +++ b/internal/p10k.zsh @@ -3801,7 +3801,11 @@ instant_prompt_time() { # The stash is evaluated at display time via ${(%)...}, so we wrap it to temporarily # override the locale. See issue #2871. _p9k_escape $_POWERLEVEL9K_TIME_FORMAT - local stash='${${__p9k_instant_prompt_time::=${${${__p9k_instant_prompt_lc_time_save::=$LC_TIME}+}${${LC_TIME::=C}+}${(%)${__p9k_instant_prompt_time_format::='$_p9k__ret'}}${${LC_TIME::=$__p9k_instant_prompt_lc_time_save}+}}}+}' + # Save LC_TIME, set to C for consistent formatting, format, then restore. + # Use ##* instead of + to discard the value silently: +} interprets the value + # as a parameter name, which causes "bad substitution" when it contains + # non-identifier chars like '.' or '-' (e.g., en_US.UTF-8). Fixes #18. + local stash='${${__p9k_instant_prompt_time::=${${__p9k_instant_prompt_lc_time_save::=$LC_TIME}##*}${${LC_TIME::=C}##*}${(%)${__p9k_instant_prompt_time_format::='$_p9k__ret'}}${${LC_TIME::=$__p9k_instant_prompt_lc_time_save}##*}}+}' _p9k_escape $_POWERLEVEL9K_TIME_FORMAT _p9k_prompt_segment prompt_time "$_p9k_color2" "$_p9k_color1" "TIME_ICON" 1 '' $stash$_p9k__ret } @@ -3846,7 +3850,8 @@ instant_prompt_date() { # Force LC_TIME=C during stash expansion to avoid localized day/month names in instant prompt. # Mirrors the fix in instant_prompt_time for issue #2871. _p9k_escape $_POWERLEVEL9K_DATE_FORMAT - local stash='${${__p9k_instant_prompt_date::=${${${__p9k_instant_prompt_lc_time_save_date::=$LC_TIME}+}${${LC_TIME::=C}+}${(%)${__p9k_instant_prompt_date_format::='$_p9k__ret'}}${${LC_TIME::=$__p9k_instant_prompt_lc_time_save_date}+}}}+}' + # Use ##* instead of + to discard locale values silently (see instant_prompt_time, #18). + local stash='${${__p9k_instant_prompt_date::=${${__p9k_instant_prompt_lc_time_save_date::=$LC_TIME}##*}${${LC_TIME::=C}##*}${(%)${__p9k_instant_prompt_date_format::='$_p9k__ret'}}${${LC_TIME::=$__p9k_instant_prompt_lc_time_save_date}##*}}+}' _p9k_escape $_POWERLEVEL9K_DATE_FORMAT _p9k_prompt_segment prompt_date "$_p9k_color2" "$_p9k_color1" "DATE_ICON" 1 '' $stash$_p9k__ret } @@ -6557,7 +6562,7 @@ _p9k_set_instant_prompt() { [[ -n $RPROMPT ]] || unset RPROMPT } -typeset -gri __p9k_instant_prompt_version=47 +typeset -gri __p9k_instant_prompt_version=48 _p9k_dump_instant_prompt() { local user=${(%):-%n} From ad423a36f16575e9dcdafdeca819237f8834d951 Mon Sep 17 00:00:00 2001 From: Nicola Spieser Date: Wed, 25 Feb 2026 04:24:46 +0100 Subject: [PATCH 02/10] docs: update CHANGELOG and version for v1.24.15 --- CHANGELOG.md | 6 ++++++ internal/p10k.zsh | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3a73140..d00a4546 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this community fork of Powerlevel10k. +## [v1.24.15] - 2026-02-25 + +### Fixed +- **instant prompt**: Fix `(anon):197: bad substitution` when `LC_TIME` contains non-identifier characters like dots or hyphens (e.g., `en_US.UTF-8`). The LC_TIME save/restore in `instant_prompt_time` and `instant_prompt_date` stash expressions used `${${var::=val}+}` which interprets `val` as a parameter name; replaced with `${${var::=val}##*}` (#18) +- Bump `__p9k_instant_prompt_version` to 48 to invalidate stale cached instant prompt files + ## [v1.24.14] - 2026-02-24 ### Fixed diff --git a/internal/p10k.zsh b/internal/p10k.zsh index 547f45f9..bf73eebd 100644 --- a/internal/p10k.zsh +++ b/internal/p10k.zsh @@ -10014,7 +10014,7 @@ if [[ $__p9k_dump_file != $__p9k_instant_prompt_dump_file && -n $__p9k_instant_p zf_rm -f -- $__p9k_instant_prompt_dump_file{,.zwc} 2>/dev/null fi -typeset -g P9K_VERSION=1.20.15 +typeset -g P9K_VERSION=1.24.15 if [[ ${VSCODE_SHELL_INTEGRATION-} == <1-> && ${+__p9k_force_term_shell_integration} == 0 ]]; then typeset -gri __p9k_force_term_shell_integration=1 From 9db78019d0b68080597377ef78743f2854a08705 Mon Sep 17 00:00:00 2001 From: Nicola Spieser Date: Wed, 25 Feb 2026 08:20:24 +0100 Subject: [PATCH 03/10] test: add regression test for ##* stash pattern (bad substitution fix #18) Verifies that the ##* pattern used in instant_prompt_time/date locale save/restore does not trigger 'bad substitution' errors with LC_TIME values containing dots, hyphens, or other non-identifier characters. --- test/test_stash_bad_substitution.zsh | 64 ++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 test/test_stash_bad_substitution.zsh diff --git a/test/test_stash_bad_substitution.zsh b/test/test_stash_bad_substitution.zsh new file mode 100644 index 00000000..e3101bd8 --- /dev/null +++ b/test/test_stash_bad_substitution.zsh @@ -0,0 +1,64 @@ +#!/usr/bin/env zsh +# Test: ##* stash pattern does not cause "bad substitution" with special chars +# Regression test for issue #18: LC_TIME values containing dots, hyphens, +# or other non-identifier characters must not trigger "bad substitution". + +emulate -L zsh +setopt extended_glob + +local -i pass=0 fail=0 + +function assert_ok() { + local desc=$1 + shift + local err + err=$( eval "$@" 2>&1 >/dev/null ) + if [[ -z $err ]]; then + print -P " %F{green}PASS%f: $desc" + (( pass++ )) + else + print -P " %F{red}FAIL%f: $desc (stderr: $err)" + (( fail++ )) + fi +} + +print -P "%F{yellow}Testing stash pattern with problematic LC_TIME values%f" + +# The fixed pattern uses ##* to silently discard the assigned value. +# The old pattern used +} which interprets the value as a parameter name. + +# Test various LC_TIME values that contain non-identifier characters +local -a problem_values=( + 'en_US.UTF-8' + 'de_DE.UTF-8' + 'ja_JP.EUC-JP' + 'zh_CN.GB18030' + 'POSIX' + 'C' + '' + 'en_GB.ISO-8859-1' +) + +for val in $problem_values; do + # Test the FIXED pattern (##*): should never error + assert_ok "##* pattern with LC_TIME='$val'" \ + 'local __save; local LC_TIME="'$val'"; : ${${__save::=$LC_TIME}##*}${${LC_TIME::=C}##*}${${LC_TIME::=$__save}##*}' +done + +print +# Test that the old broken pattern (+}) DOES fail on dotted values +# This confirms the regression test is meaningful +local old_err +old_err=$( eval 'local __save; local LC_TIME="en_US.UTF-8"; : ${${__save::=$LC_TIME}+}' 2>&1 >/dev/null ) +if [[ -n $old_err ]]; then + print -P " %F{green}PASS%f: old +} pattern correctly fails on dotted values (confirms fix is needed)" + (( pass++ )) +else + # On some zsh versions +} might not error; that's OK, just note it + print -P " %F{yellow}INFO%f: old +} pattern did not error on this zsh version (${ZSH_VERSION})" +fi + +print +print -P "Results: %F{green}$pass passed%f, %F{red}$fail failed%f" +(( fail )) && exit 1 +exit 0 From 5407639646ac6de8fef1feee62202cf25e274e67 Mon Sep 17 00:00:00 2001 From: Lucas Larson Date: Thu, 5 Mar 2026 15:02:33 -0500 Subject: [PATCH 04/10] fix: reference to maintainer References to @redbasecap-buiss: Working: https://github.com/quantumnic/powerlevel10k/blob/9db78019d0/CHANGELOG.md?plain=1#L180-L185 Broken and replaced to fix #20: https://github.com/quantumnic/powerlevel10k/blob/9db78019d0/MAINTAINERS.md?plain=1#L5 Signed-off-by: Lucas Larson --- MAINTAINERS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 6fcf1ee6..a4af1acb 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -2,7 +2,7 @@ ## Current Maintainers -- **Nicola Spieser** ([@redbasecap-buiss](https://github.com/redbasecap-buiss)) +- **Nicola Spieser** ([@quantumnic](https://github.com/quantumnic)) ## Original Author From 7ff836131ab462073053dde941c3188ce0e4243c Mon Sep 17 00:00:00 2001 From: Kevin Ji <1146876+kevinji@users.noreply.github.com> Date: Tue, 10 Mar 2026 18:09:44 -0700 Subject: [PATCH 05/10] fix(gcloud): prevent gcloud CLI from hanging (#2935) If the `gcloud` command is interactive (e.g. asks to install a component, or needs an update), the current code will hang forever, and is uninterruptible as `INT` is being swallowed. Fix this with ` Date: Sat, 14 Mar 2026 07:06:58 +0100 Subject: [PATCH 06/10] Squashed 'gitstatus/' changes from 44504a24..075baf6e 075baf6e Fix gitstatus build failure with Apple Clang 17 (Xcode 16+) 6bcf109c Compiling on mips64 (#476) git-subtree-dir: gitstatus git-subtree-split: 075baf6ecb19f58b09c9562f33c20b842e870961 --- gitstatus/build | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gitstatus/build b/gitstatus/build index ea96a25a..66b4e59f 100755 --- a/gitstatus/build +++ b/gitstatus/build @@ -272,9 +272,9 @@ case "$gitstatus_kernel" in gitstatus_cxxflags="$gitstatus_cxxflags -I"$brew_prefix"/opt/libiconv/include" fi libgit2_cmake_flags="$libgit2_cmake_flags -DUSE_ICONV=ON" - gitstatus_ldlibs="$gitstatus_ldlibs -liconv" + gitstatus_ldlibs="$gitstatus_ldlibs -liconv -lz" gitstatus_ldflags="$gitstatus_ldflags -L${workdir}/lib" - libgit2_cmake_flags="$libgit2_cmake_flags -DENABLE_REPRODUCIBLE_BUILDS=OFF" + libgit2_cmake_flags="$libgit2_cmake_flags -DENABLE_REPRODUCIBLE_BUILDS=OFF -DUSE_BUNDLED_ZLIB=OFF" ;; msys*|mingw*) gitstatus_ldflags="$gitstatus_ldflags ${static_pie:--static}" @@ -551,6 +551,7 @@ if [ -z "$gitstatus_cpu" ]; then arm64|aarch64) gitstatus_cpu=armv8-a;; ppc64|ppc64le) gitstatus_cpu=powerpc64le;; riscv64) gitstatus_cpu=rv64imafdc;; + mips64) gitstatus_cpu=mips64;; loongarch64) gitstatus_cpu=loongarch64;; x86_64|amd64) gitstatus_cpu=x86-64;; x86) gitstatus_cpu=i586;; From 0625dd30c39515b18886ae1f10b2b75611f815b6 Mon Sep 17 00:00:00 2001 From: Nicola Spieser Date: Sun, 15 Mar 2026 02:06:05 +0100 Subject: [PATCH 07/10] docs: nicer README header with badges + centered layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Centered title with ⚡ emoji - MIT License, Zsh 5.1+, Oh My Zsh badges - Cleaner description text - Credit to original author romkatv --- README.md | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 2eda8f35..c22f056e 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,20 @@ -# Powerlevel10k (Community Fork) +
-> **Community-maintained fork of [Powerlevel10k](https://github.com/romkatv/powerlevel10k)** -> by [Roman Perepelitsa (romkatv)](https://github.com/romkatv). -> -> The original project has been archived with limited support. This fork continues -> development with bug fixes and community contributions. All credit for the original -> work goes to romkatv — thank you for creating one of the best Zsh themes ever made. ❤️ +# ⚡ Powerlevel10k + +**The fastest & most customizable Zsh theme — community maintained** + +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) +[![Zsh](https://img.shields.io/badge/zsh-5.1%2B-green.svg)](https://www.zsh.org) +[![Oh My Zsh](https://img.shields.io/badge/Oh%20My%20Zsh-compatible-orange.svg)](https://ohmyz.sh) + +*Community-maintained fork of [romkatv/powerlevel10k](https://github.com/romkatv/powerlevel10k) — keeping it alive with bug fixes & improvements. All original credit goes to [Roman Perepelitsa](https://github.com/romkatv) ❤️* + +
+ +--- -Powerlevel10k is a theme for Zsh. It emphasizes [speed](#uncompromising-performance), -[flexibility](#extremely-customizable) and [out-of-the-box experience](#configuration-wizard). +Powerlevel10k is a Zsh theme that puts **speed**, **flexibility** and **out-of-the-box experience** first. It starts your shell instantly, looks stunning, and works exactly the way you want it to. ![Powerlevel10k]( https://raw.githubusercontent.com/quantumnic/powerlevel10k-media/master/prompt-styles-high-contrast.png) From a35ab2eab1714fbfab0cfd310a86189e9b4b6709 Mon Sep 17 00:00:00 2001 From: Alejandro Quintanar <40313108+alejandroqh@users.noreply.github.com> Date: Tue, 28 Apr 2026 22:17:24 +0200 Subject: [PATCH 08/10] Keep migration working when Zinit is absent The migration script runs under nounset, but the Zinit lookup read ZINIT[PLUGINS_DIR] before ZINIT existed. Declare the associative array before the fallback expansion so non-Zinit users reach the no-installation or config migration paths instead of exiting early. Constraint: migrate.zsh uses set -u and supports systems without Zinit Rejected: Remove nounset | broader safety behavior change outside this bug Confidence: high Scope-risk: narrow Tested: Reproduced master failure with empty HOME: PLUGINS_DIR parameter not set Tested: Verified PR head exits 0 with empty HOME and no Zinit Tested: zsh test/run_all.zsh --- migrate.zsh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/migrate.zsh b/migrate.zsh index a13c3212..473c0e3f 100755 --- a/migrate.zsh +++ b/migrate.zsh @@ -75,6 +75,9 @@ for d in "${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/themes/powerlevel10k" \ done # Zinit / Zplugin +# Declare ZINIT as associative array if not already set, so the +# ${ZINIT[PLUGINS_DIR]:-...} fallback works under nounset (set -u). +(( ${+ZINIT} )) || typeset -A ZINIT for d in ~/.zinit/plugins/romkatv---powerlevel10k \ ~/.local/share/zinit/plugins/romkatv---powerlevel10k \ "${ZINIT[PLUGINS_DIR]:-/dev/null}/romkatv---powerlevel10k" \ From 7722689b385a68db9c896fe5df6d0eb5a1b5e3ec Mon Sep 17 00:00:00 2001 From: Test User Date: Tue, 28 Apr 2026 23:03:31 +0200 Subject: [PATCH 09/10] Make fork adoption safer to verify Users moving from romkatv/powerlevel10k need a reversible path that can be inspected before it changes their shell setup. The implementation adds check and dry-run migration modes, local diagnostics for p10k and gitstatus, and a lightweight ai_workspace segment without adding dependencies. Constraint: Preserve user prompt configuration and avoid new dependencies Rejected: Reset migrated repos with git reset --hard | destructive for local changes Rejected: Depend on a new migration parser | unnecessary for known plugin and config formats Confidence: high Scope-risk: moderate Directive: Keep migration modes side-effect free unless MODE=apply Tested: zsh -f test/run_all.zsh; repo-wide zsh -n; make zwc; p10k doctor smoke; gitstatus pure fallback; native gitstatus build Not-tested: Native gitstatus build on non-darwin/arm64 platforms --- internal/icons.zsh | 7 + internal/p10k.zsh | 158 +++++++++++++++++++ migrate.zsh | 241 ++++++++++++++++++++++++----- test/test_ai_workspace_segment.zsh | 72 +++++++++ test/test_migrate_cli.zsh | 89 +++++++++++ test/test_p10k_doctor.zsh | 57 +++++++ 6 files changed, 583 insertions(+), 41 deletions(-) create mode 100644 test/test_ai_workspace_segment.zsh create mode 100644 test/test_migrate_cli.zsh create mode 100644 test/test_p10k_doctor.zsh diff --git a/internal/icons.zsh b/internal/icons.zsh index 03445204..9a4b5e6d 100644 --- a/internal/icons.zsh +++ b/internal/icons.zsh @@ -88,6 +88,7 @@ function _p9k_init_icons() { SWAP_ICON '\uE87D'$s #  RAM_ICON '\uE1E2 ' #  SERVER_ICON '\uE895'$s #  + AI_WORKSPACE_ICON 'AI' VCS_UNTRACKED_ICON '\uE16C'$s #  VCS_UNSTAGED_ICON '\uE17C'$s #  VCS_STAGED_ICON '\uE168'$s #  @@ -247,6 +248,7 @@ function _p9k_init_icons() { SWAP_ICON '\uF0E4'$s #  RAM_ICON '\uF0E4'$s #  SERVER_ICON '\uF233'$s #  + AI_WORKSPACE_ICON 'AI' VCS_UNTRACKED_ICON '\uF059'$s #  VCS_UNSTAGED_ICON '\uF06A'$s #  VCS_STAGED_ICON '\uF055'$s #  @@ -407,6 +409,7 @@ function _p9k_init_icons() { SWAP_ICON "${CODEPOINT_OF_AWESOME_DASHBOARD:+\\u$CODEPOINT_OF_AWESOME_DASHBOARD$s}" RAM_ICON "${CODEPOINT_OF_AWESOME_DASHBOARD:+\\u$CODEPOINT_OF_AWESOME_DASHBOARD$s}" SERVER_ICON "${CODEPOINT_OF_AWESOME_SERVER:+\\u$CODEPOINT_OF_AWESOME_SERVER$s}" + AI_WORKSPACE_ICON 'AI' VCS_UNTRACKED_ICON "${CODEPOINT_OF_AWESOME_QUESTION_CIRCLE:+\\u$CODEPOINT_OF_AWESOME_QUESTION_CIRCLE$s}" VCS_UNSTAGED_ICON "${CODEPOINT_OF_AWESOME_EXCLAMATION_CIRCLE:+\\u$CODEPOINT_OF_AWESOME_EXCLAMATION_CIRCLE$s}" VCS_STAGED_ICON "${CODEPOINT_OF_AWESOME_PLUS_CIRCLE:+\\u$CODEPOINT_OF_AWESOME_PLUS_CIRCLE$s}" @@ -561,6 +564,7 @@ function _p9k_init_icons() { SWAP_ICON '\uF464'$s #  RAM_ICON '\uF0E4'$s #  SERVER_ICON '\uF0AE'$s #  + AI_WORKSPACE_ICON 'AI' VCS_UNTRACKED_ICON '\uF059'$s #  VCS_UNSTAGED_ICON '\uF06A'$s #  VCS_STAGED_ICON '\uF055'$s #  @@ -722,6 +726,7 @@ function _p9k_init_icons() { SWAP_ICON '\uF464'$s #  RAM_ICON '\uF0E4'$s #  SERVER_ICON '\uF0AE'$s #  + AI_WORKSPACE_ICON 'AI' VCS_UNTRACKED_ICON '\uF059'$s #  VCS_UNSTAGED_ICON '\uF06A'$s #  VCS_STAGED_ICON '\uF055'$s #  @@ -875,6 +880,7 @@ function _p9k_init_icons() { SWAP_ICON 'swap' RAM_ICON 'ram' SERVER_ICON '' + AI_WORKSPACE_ICON 'AI' VCS_UNTRACKED_ICON '?' VCS_UNSTAGED_ICON '!' VCS_STAGED_ICON '+' @@ -1030,6 +1036,7 @@ function _p9k_init_icons() { SWAP_ICON 'SWP' RAM_ICON 'RAM' SERVER_ICON '' + AI_WORKSPACE_ICON 'AI' VCS_UNTRACKED_ICON '?' VCS_UNSTAGED_ICON '\u25CF' # ● VCS_STAGED_ICON '\u271A' # ✚ diff --git a/internal/p10k.zsh b/internal/p10k.zsh index 7b31e72c..94bcda01 100644 --- a/internal/p10k.zsh +++ b/internal/p10k.zsh @@ -1683,6 +1683,36 @@ prompt_host() { instant_prompt_host() { prompt_host; } +################################################################ +# AI workspace: show when the shell is running under an AI coding tool. +function _p9k_ai_workspace_detect() { + if [[ -n $_POWERLEVEL9K_AI_WORKSPACE_CONTEXT ]]; then + typeset -g P9K_AI_WORKSPACE=$_POWERLEVEL9K_AI_WORKSPACE_CONTEXT + elif [[ -n ${CODEX_SANDBOX:-}${CODEX_SESSION_ID:-}${OPENAI_CODEX:-} ]]; then + typeset -g P9K_AI_WORKSPACE=codex + elif [[ -n ${CLAUDECODE:-}${CLAUDE_CODE:-}${CLAUDECODE_ENTRYPOINT:-} ]]; then + typeset -g P9K_AI_WORKSPACE=claude + elif [[ -n ${GEMINI_CLI:-}${GEMINI_CLI_VERSION:-} ]]; then + typeset -g P9K_AI_WORKSPACE=gemini + elif [[ -n ${CURSOR_TRACE_ID:-}${CURSOR_AGENT:-} ]]; then + typeset -g P9K_AI_WORKSPACE=cursor + elif [[ -n ${WINDSURF:-}${WINDSURF_SHELL:-} ]]; then + typeset -g P9K_AI_WORKSPACE=windsurf + elif [[ -n ${AIDER_MODEL:-}${AIDER_EDIT_FORMAT:-} ]]; then + typeset -g P9K_AI_WORKSPACE=aider + elif [[ -n ${VSCODE_INJECTION:-} ]]; then + typeset -g P9K_AI_WORKSPACE=agent + else + unset P9K_AI_WORKSPACE + return 1 + fi +} + +prompt_ai_workspace() { + _p9k_ai_workspace_detect || return + _p9k_prompt_segment "$0" "magenta" "$_p9k_color1" AI_WORKSPACE_ICON 1 '' "$_POWERLEVEL9K_AI_WORKSPACE_TEMPLATE" +} + ################################################################ # Toolbox: https://github.com/containers/toolbox function prompt_toolbox() { @@ -8018,6 +8048,8 @@ _p9k_init_params() { _p9k_declare -e POWERLEVEL9K_CONTEXT_TEMPLATE "%n@%m" _p9k_declare -e POWERLEVEL9K_USER_TEMPLATE "%n" _p9k_declare -e POWERLEVEL9K_HOST_TEMPLATE "%m" + _p9k_declare -s POWERLEVEL9K_AI_WORKSPACE_CONTEXT "" + _p9k_declare -e POWERLEVEL9K_AI_WORKSPACE_TEMPLATE '${P9K_AI_WORKSPACE}' _p9k_declare -F POWERLEVEL9K_COMMAND_EXECUTION_TIME_THRESHOLD 3 _p9k_declare -i POWERLEVEL9K_COMMAND_EXECUTION_TIME_PRECISION 2 # Other options: "d h m s". @@ -9660,6 +9692,8 @@ typeset -gr __p9k_p10k_usage="Usage: %2Fp10k%f %Bcommand%b [options] Commands: %Bconfigure%b run interactive configuration wizard + %Bdoctor%b print environment diagnostics + %Bgitstatus%b print gitstatus diagnostics %Breload%b reload configuration %Bsegment%b print a user-defined prompt segment %Bdisplay%b show, hide or toggle prompt parts @@ -9746,6 +9780,14 @@ typeset -gr __p9k_p10k_reload_usage="Usage: %2Fp10k%f %Breload%b Reload configuration." +typeset -gr __p9k_p10k_doctor_usage="Usage: %2Fp10k%f %Bdoctor%b [gitstatus] + +Print environment diagnostics. With %Bgitstatus%b, print only gitstatus diagnostics." + +typeset -gr __p9k_p10k_gitstatus_usage="Usage: %2Fp10k%f %Bgitstatus%b %Bdoctor%b + +Print diagnostics for gitstatus and the VCS backend." + typeset -gr __p9k_p10k_finalize_usage="Usage: %2Fp10k%f %Bfinalize%b Perform the final stage of initialization. Must be called at the very end of zshrc." @@ -9814,6 +9856,105 @@ Example: Print the current state of all prompt parts: # 2 -- reset-prompt blocked and needed typeset -gi __p9k_reset_state +function _p9k_doctor_line() { + local state=$1 label=$2 text=$3 color + case $state in + ok) color=2;; + warn) color=3;; + fail) color=1;; + *) color=6;; + esac + print -rP -- " %F{$color}${(r:5:)state}%f %B$label%b $text" +} + +function _p9k_doctor_gitstatus() { + print -rP -- "%Bgitstatus%b" + + local gitstatus_dir=${_POWERLEVEL9K_GITSTATUS_DIR:-${__p9k_root_dir}/gitstatus} + if [[ -d $gitstatus_dir ]]; then + _p9k_doctor_line ok "directory" "${gitstatus_dir//\%/%%}" + else + _p9k_doctor_line fail "directory" "not found: ${gitstatus_dir//\%/%%}" + fi + + if [[ -r $gitstatus_dir/gitstatus.plugin.zsh ]]; then + _p9k_doctor_line ok "plugin" "gitstatus.plugin.zsh is readable" + else + _p9k_doctor_line fail "plugin" "gitstatus.plugin.zsh is not readable" + fi + + if [[ -r $gitstatus_dir/gitstatus-git-backend.zsh ]]; then + _p9k_doctor_line ok "fallback" "pure-zsh git backend is available" + else + _p9k_doctor_line warn "fallback" "pure-zsh git backend is missing" + fi + + if (( _POWERLEVEL9K_DISABLE_GITSTATUS )); then + _p9k_doctor_line warn "daemon" "disabled by POWERLEVEL9K_DISABLE_GITSTATUS" + elif (( ! $+commands[git] )); then + _p9k_doctor_line warn "daemon" "git command is not available" + elif (( $+GITSTATUS_DAEMON_PID_POWERLEVEL9K )); then + _p9k_doctor_line ok "daemon" "running with pid $GITSTATUS_DAEMON_PID_POWERLEVEL9K" + elif (( $+functions[gitstatus_start_p9k_] )); then + _p9k_doctor_line warn "daemon" "plugin is loaded but daemon is not running" + else + _p9k_doctor_line warn "daemon" "not loaded; VCS segment may be inactive or not initialized yet" + fi + + if [[ -n ${GITSTATUS_DAEMON:-} ]]; then + if [[ $GITSTATUS_DAEMON == /* ]]; then + [[ -x $GITSTATUS_DAEMON ]] && + _p9k_doctor_line ok "GITSTATUS_DAEMON" "${GITSTATUS_DAEMON//\%/%%}" || + _p9k_doctor_line warn "GITSTATUS_DAEMON" "not executable: ${GITSTATUS_DAEMON//\%/%%}" + else + _p9k_doctor_line warn "GITSTATUS_DAEMON" "must be an absolute path: ${GITSTATUS_DAEMON//\%/%%}" + fi + fi +} + +function _p9k_doctor() { + print -rP -- "%BPowerlevel10k Doctor%b" + + if [[ $ZSH_VERSION == (5.<1->*|<6->.*) ]]; then + _p9k_doctor_line ok "zsh" "$ZSH_VERSION" + else + _p9k_doctor_line fail "zsh" "$ZSH_VERSION is below the required 5.1" + fi + + if [[ -r $__p9k_root_dir/internal/p10k.zsh ]]; then + _p9k_doctor_line ok "installation" "${__p9k_root_dir//\%/%%}" + else + _p9k_doctor_line fail "installation" "cannot read ${__p9k_root_dir//\%/%%}/internal/p10k.zsh" + fi + + local cfg=${POWERLEVEL9K_CONFIG_FILE:-${ZDOTDIR:-$HOME}/.p10k.zsh} + if [[ -r $cfg ]]; then + _p9k_doctor_line ok "config" "${cfg//\%/%%}" + elif [[ -e $cfg ]]; then + _p9k_doctor_line warn "config" "exists but is not readable: ${cfg//\%/%%}" + else + _p9k_doctor_line warn "config" "not found: ${cfg//\%/%%}" + fi + + case ${langinfo[CODESET]} in + utf-8|UTF-8|utf8|UTF8) _p9k_doctor_line ok "locale" "$langinfo[CODESET]";; + *) _p9k_doctor_line warn "locale" "${langinfo[CODESET]:-unknown}; UTF-8 is recommended";; + esac + + if (( terminfo[colors] >= 8 )); then + _p9k_doctor_line ok "terminal colors" "$terminfo[colors]" + else + _p9k_doctor_line warn "terminal colors" "${terminfo[colors]:-0}; prompt styling may be limited" + fi + + (( $+commands[git] )) && + _p9k_doctor_line ok "git" "$commands[git]" || + _p9k_doctor_line warn "git" "not found; VCS prompt support will be limited" + + print "" + _p9k_doctor_gitstatus +} + function p10k() { [[ $# != 1 || $1 != finalize ]] || { p10k-instant-prompt-finalize; return 0 } @@ -9950,6 +10091,23 @@ function p10k() { local -a reply p9k_configure "$@" || return ;; + doctor) + case ${2:-} in + "") (( ARGC == 1 )) || { print -rP -- $__p9k_p10k_doctor_usage >&2; return 1; } + _p9k_doctor;; + gitstatus) (( ARGC == 2 )) || { print -rP -- $__p9k_p10k_doctor_usage >&2; return 1; } + _p9k_doctor_gitstatus;; + *) print -rP -- $__p9k_p10k_doctor_usage >&2; return 1;; + esac + ;; + gitstatus) + if [[ ${2:-} == doctor ]] && (( ARGC == 2 )); then + _p9k_doctor_gitstatus + else + print -rP -- $__p9k_p10k_gitstatus_usage >&2 + return 1 + fi + ;; reload) if (( ARGC > 1 )); then print -rP -- $__p9k_p10k_reload_usage >&2 diff --git a/migrate.zsh b/migrate.zsh index 473c0e3f..a98674d2 100755 --- a/migrate.zsh +++ b/migrate.zsh @@ -3,43 +3,112 @@ # migrate.zsh — Migrate from romkatv/powerlevel10k to quantumnic/powerlevel10k # # Usage: -# zsh migrate.zsh +# zsh migrate.zsh [--dry-run|--check|--doctor] # # or: curl -fsSL https://raw.githubusercontent.com/quantumnic/powerlevel10k/master/migrate.zsh | zsh # # What it does: # 1. Detects your current p10k installation method # 2. Updates the git remote (or plugin reference) from romkatv → quantumnic # 3. Preserves your ~/.p10k.zsh configuration (no changes) -# 4. Pulls latest changes +# 4. Pulls latest changes when possible # # Your prompt configuration (~/.p10k.zsh) is NEVER modified. set -euo pipefail -print -P "%F{cyan}%B── Powerlevel10k Migration ──%b%f" -print -P "Migrating from %F{red}romkatv%f/powerlevel10k → %F{green}quantumnic%f/powerlevel10k" -print "" - OLD_OWNER="romkatv" NEW_OWNER="quantumnic" MIGRATED=0 +MODE=apply + +typeset -a UPDATED_REMOTES UPDATED_CONFIGS BACKUPS FOUND_REMOTES FOUND_CONFIGS CURRENT_INSTALLS +typeset -a MANUAL_ACTIONS WARNINGS + +usage() { + print -r -- "Usage: zsh migrate.zsh [--dry-run|--check|--doctor|--help]" + print -r -- "" + print -r -- "Options:" + print -r -- " --dry-run show planned changes without writing files or running git updates" + print -r -- " --check inspect installation/config state without making changes" + print -r -- " --doctor alias for --check" + print -r -- " --help print this help" +} + +while (( $# )); do + case $1 in + --dry-run|-n) MODE=dry-run;; + --check) MODE=check;; + --doctor) MODE=check;; + --help|-h) usage; exit 0;; + *) + print -r -- "migrate.zsh: unknown option: $1" >&2 + usage >&2 + exit 2 + ;; + esac + shift +done + +case $MODE in + check) print -P "%F{cyan}%B── Powerlevel10k Migration Check ──%b%f";; + dry-run) print -P "%F{cyan}%B── Powerlevel10k Migration Dry Run ──%b%f";; + *) print -P "%F{cyan}%B── Powerlevel10k Migration ──%b%f";; +esac +print -P "Migrating from %F{red}romkatv%f/powerlevel10k → %F{green}quantumnic%f/powerlevel10k" +print "" + +timestamp() { + if [[ -n ${P10K_MIGRATE_TIMESTAMP:-} ]]; then + print -r -- "$P10K_MIGRATE_TIMESTAMP" + else + date +%Y%m%d-%H%M%S 2>/dev/null || print -r -- "$$" + fi +} + +human_path() { + local p=$1 + if [[ -n ${HOME:-} && $p == $HOME(|/*) ]]; then + p="~${p#$HOME}" + fi + print -r -- "$p" +} + +mode_prefix() { + case $MODE in + check) print -r -- "Found";; + dry-run) print -r -- "Would update";; + *) print -r -- "Updating";; + esac +} # --- Helper: update git remote in a directory --- update_remote() { - local dir=$1 + local label=$1 dir=$2 if [[ -d "$dir/.git" ]]; then local url url=$(git -C "$dir" remote get-url origin 2>/dev/null || true) if [[ "$url" == *"$OLD_OWNER/powerlevel10k"* ]]; then local new_url=${url/$OLD_OWNER/$NEW_OWNER} - print -P " %F{yellow}→%f Updating remote in %F{blue}$dir%f" + FOUND_REMOTES+=("$label:$(human_path "$dir")") + print -P " %F{yellow}→%f $(mode_prefix) remote in %F{blue}$(human_path "$dir")%f ($label)" print -P " old: $url" print -P " new: $new_url" - git -C "$dir" remote set-url origin "$new_url" - git -C "$dir" fetch origin --depth=1 2>/dev/null || git -C "$dir" fetch origin 2>/dev/null - git -C "$dir" pull --ff-only 2>/dev/null || git -C "$dir" reset --hard origin/master 2>/dev/null || true + [[ $MODE != apply ]] && return 0 + if ! git -C "$dir" remote set-url origin "$new_url"; then + WARNINGS+=("failed to update remote in $(human_path "$dir")") + return 1 + fi + if ! git -C "$dir" fetch origin --depth=1 2>/dev/null && + ! git -C "$dir" fetch origin 2>/dev/null; then + WARNINGS+=("remote updated but fetch failed in $(human_path "$dir")") + elif ! git -C "$dir" pull --ff-only 2>/dev/null; then + WARNINGS+=("remote updated but fast-forward pull failed in $(human_path "$dir")") + fi + UPDATED_REMOTES+=("$label:$(human_path "$dir")") MIGRATED=1 return 0 + elif [[ "$url" == *"$NEW_OWNER/powerlevel10k"* ]]; then + CURRENT_INSTALLS+=("$label:$(human_path "$dir")") fi fi return 1 @@ -49,61 +118,151 @@ update_remote() { update_config_file() { local file=$1 if [[ -f "$file" ]] && grep -q "$OLD_OWNER/powerlevel10k" "$file" 2>/dev/null; then - print -P " %F{yellow}→%f Updating references in %F{blue}$file%f" - # Create backup - cp "$file" "${file}.p10k-migrate-backup" + FOUND_CONFIGS+=("$(human_path "$file")") + print -P " %F{yellow}→%f $(mode_prefix) references in %F{blue}$(human_path "$file")%f" + [[ $MODE != apply ]] && return 0 + local backup="${file}.p10k-migrate-$(timestamp)-backup" + cp -p "$file" "$backup" 2>/dev/null || cp "$file" "$backup" if [[ "$OSTYPE" == darwin* ]]; then sed -i '' "s|$OLD_OWNER/powerlevel10k|$NEW_OWNER/powerlevel10k|g" "$file" else sed -i "s|$OLD_OWNER/powerlevel10k|$NEW_OWNER/powerlevel10k|g" "$file" fi + UPDATED_CONFIGS+=("$(human_path "$file")") + BACKUPS+=("$(human_path "$backup")") MIGRATED=1 - print -P " (backup: ${file}.p10k-migrate-backup)" + print -P " backup: %F{blue}$(human_path "$backup")%f" + print -P " restore: %F{yellow}cp $(human_path "$backup") $(human_path "$file")%f" + fi +} + +detect_homebrew() { + (( $+commands[brew] )) || return 0 + local taps + taps=$(brew tap 2>/dev/null || true) + if [[ $taps == *"$OLD_OWNER/powerlevel10k"* ]]; then + MANUAL_ACTIONS+=("Homebrew tap $OLD_OWNER/powerlevel10k is installed; run: brew untap $OLD_OWNER/powerlevel10k") fi } # --- 1. Check common installation directories --- print -P "%F{cyan}Checking installation directories...%f" -# Manual install -update_remote ~/powerlevel10k 2>/dev/null || true +if (( ! ${+ZINIT} )); then + typeset -A ZINIT +elif [[ ${(t)ZINIT} != *association* ]]; then + unset ZINIT + typeset -A ZINIT +fi -# Oh My Zsh -for d in "${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/themes/powerlevel10k" \ - "$HOME/.oh-my-zsh/custom/themes/powerlevel10k"; do - update_remote "$d" 2>/dev/null || true -done +local_zdotdir=${ZDOTDIR:-$HOME} +local_xdg_config=${XDG_CONFIG_HOME:-$HOME/.config} +local_xdg_data=${XDG_DATA_HOME:-$HOME/.local/share} -# Zinit / Zplugin -# Declare ZINIT as associative array if not already set, so the -# ${ZINIT[PLUGINS_DIR]:-...} fallback works under nounset (set -u). -(( ${+ZINIT} )) || typeset -A ZINIT -for d in ~/.zinit/plugins/romkatv---powerlevel10k \ - ~/.local/share/zinit/plugins/romkatv---powerlevel10k \ - "${ZINIT[PLUGINS_DIR]:-/dev/null}/romkatv---powerlevel10k" \ - ~/.zplugin/plugins/romkatv---powerlevel10k; do - update_remote "$d" 2>/dev/null || true -done +install_dirs=( + "manual" "$HOME/powerlevel10k" + "manual-xdg" "$local_xdg_data/powerlevel10k" + "oh-my-zsh" "${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/themes/powerlevel10k" + "oh-my-zsh" "$HOME/.oh-my-zsh/custom/themes/powerlevel10k" + "zinit" "$HOME/.zinit/plugins/romkatv---powerlevel10k" + "zinit" "$local_xdg_data/zinit/plugins/romkatv---powerlevel10k" + "zinit" "${ZINIT[PLUGINS_DIR]:-/dev/null}/romkatv---powerlevel10k" + "zplugin" "$HOME/.zplugin/plugins/romkatv---powerlevel10k" + "zim" "${ZIM_HOME:-$HOME/.zim}/modules/powerlevel10k" + "zplug" "$HOME/.zplug/repos/romkatv/powerlevel10k" + "antigen" "$HOME/.antigen/bundles/romkatv/powerlevel10k" + "antidote" "$HOME/.antidote/repos/romkatv/powerlevel10k" + "antidote" "$local_xdg_data/antidote/repos/romkatv/powerlevel10k" +) -# Zim -if [[ -d "${ZIM_HOME:-$HOME/.zim}/modules/powerlevel10k" ]]; then - update_remote "${ZIM_HOME:-$HOME/.zim}/modules/powerlevel10k" 2>/dev/null || true -fi +for label d in "${install_dirs[@]}"; do + update_remote "$label" "$d" 2>/dev/null || true +done +detect_homebrew # --- 2. Update plugin manager config files --- print -P "%F{cyan}Checking config files...%f" -update_config_file ~/.zshrc -update_config_file ~/.zimrc -update_config_file ~/.zsh_plugins.txt # Antidote -update_config_file ~/.zsh_plugins.zsh # Antidote (compiled) +config_files=( + "$local_zdotdir/.zshrc" + "$local_zdotdir/.zshenv" + "$local_zdotdir/.zprofile" + "$HOME/.zshrc" + "$HOME/.zimrc" + "$HOME/.zplugrc" + "$HOME/.antigenrc" + "$HOME/.zinitrc" + "$HOME/.zpluginrc" + "$HOME/.zsh_plugins.txt" + "$HOME/.zsh_plugins.zsh" + "$local_xdg_config/sheldon/plugins.toml" + "$local_xdg_config/sheldon/plugins.lock" +) + +typeset -a seen_config_files +for f in "${config_files[@]}"; do + (( ${seen_config_files[(Ie)$f]} )) || seen_config_files+=("$f") +done +for f in "${seen_config_files[@]}"; do + update_config_file "$f" +done # --- 3. Summary --- print "" -if (( MIGRATED )); then +print -P "%F{cyan}Summary:%f" +print -P " Remote installs needing migration: %B${#FOUND_REMOTES}%b" +print -P " Config files needing migration: %B${#FOUND_CONFIGS}%b" +if (( ${#CURRENT_INSTALLS} )); then + print -P " Already on quantumnic: %B${#CURRENT_INSTALLS}%b" +fi +if (( ${#UPDATED_REMOTES} )); then + print -P " Remotes updated: %B${#UPDATED_REMOTES}%b" +fi +if (( ${#UPDATED_CONFIGS} )); then + print -P " Config files updated: %B${#UPDATED_CONFIGS}%b" +fi +if (( ${#BACKUPS} )); then + print -P " Backups created:" + local b + for b in "${BACKUPS[@]}"; do + print -P " %F{blue}$b%f" + done +fi +if (( ${#MANUAL_ACTIONS} )); then + print -P " Manual actions:" + local action + for action in "${MANUAL_ACTIONS[@]}"; do + print -P " %F{yellow}$action%f" + done +fi +if (( ${#WARNINGS} )); then + print -P " Warnings:" + local warning + for warning in "${WARNINGS[@]}"; do + print -P " %F{yellow}$warning%f" + done +fi +print "" + +if [[ $MODE == check ]]; then + if (( ${#FOUND_REMOTES} || ${#FOUND_CONFIGS} || ${#MANUAL_ACTIONS} )); then + print -P "%F{yellow}Migration is available. Run without --check to apply, or with --dry-run to preview.%f" + else + print -P "%F{green}No romkatv/powerlevel10k references found.%f" + fi +elif [[ $MODE == dry-run ]]; then + if (( ${#FOUND_REMOTES} || ${#FOUND_CONFIGS} || ${#MANUAL_ACTIONS} )); then + print -P "%F{yellow}Dry run complete. No files, remotes, or caches were changed.%f" + else + print -P "%F{green}No romkatv/powerlevel10k references found.%f" + fi +elif (( MIGRATED )); then print -P "%F{green}%B✓ Migration complete!%b%f" print -P " Your %F{blue}~/.p10k.zsh%f configuration was preserved (no changes needed)." print -P " Run %F{yellow}exec zsh%f to reload your shell." +elif (( ${#MANUAL_ACTIONS} )); then + print -P "%F{yellow}Manual migration steps remain.%f" + print -P " Complete the manual actions above, then run %F{yellow}exec zsh%f to reload your shell." else print -P "%F{yellow}No romkatv/powerlevel10k installation found.%f" print -P " If you installed via Homebrew or a system package, update manually:" diff --git a/test/test_ai_workspace_segment.zsh b/test/test_ai_workspace_segment.zsh new file mode 100644 index 00000000..680fd5c4 --- /dev/null +++ b/test/test_ai_workspace_segment.zsh @@ -0,0 +1,72 @@ +#!/usr/bin/env zsh +# Tests for the ai_workspace prompt segment. + +emulate -L zsh + +local root="${0:A:h}/.." +local -i pass=0 fail=0 + +function assert_contains() { + local desc=$1 haystack=$2 needle=$3 + if [[ $haystack == *$needle* ]]; then + print -P " %F{green}PASS%f: $desc" + (( pass++ )) + else + print -P " %F{red}FAIL%f: $desc" + print " Expected to contain: $needle" + (( fail++ )) + fi +} + +function assert_equals() { + local desc=$1 actual=$2 expected=$3 + if [[ $actual == $expected ]]; then + print -P " %F{green}PASS%f: $desc" + (( pass++ )) + else + print -P " %F{red}FAIL%f: $desc" + print " Expected: $expected" + print " Got: $actual" + (( fail++ )) + fi +} + +local p10k_source icons_source +p10k_source=$(<"$root/internal/p10k.zsh") || exit 1 +icons_source=$(<"$root/internal/icons.zsh") || exit 1 + +assert_contains "ai_workspace prompt function exists" "$p10k_source" "prompt_ai_workspace" +assert_contains "Codex environment is detected" "$p10k_source" "CODEX_SANDBOX" +assert_contains "Claude environment is detected" "$p10k_source" "CLAUDECODE" +assert_contains "Gemini environment is detected" "$p10k_source" "GEMINI_CLI" +assert_contains "template parameter is declared" "$p10k_source" "POWERLEVEL9K_AI_WORKSPACE_TEMPLATE" +assert_contains "AI icon is available" "$icons_source" "AI_WORKSPACE_ICON" + +local home out +home=$(mktemp -d "${TMPDIR:-/tmp}/p10k-ai-segment-test.XXXXXXXXXX") || exit 1 +trap 'rm -rf -- "$home"' EXIT + +out=$(HOME="$home" ZDOTDIR="$home" POWERLEVEL9K_DISABLE_INSTANT_PROMPT=true CODEX_SANDBOX=seatbelt zsh -f -c " + source ${(q)root}/powerlevel10k.zsh-theme + _p9k_ai_workspace_detect + print -r -- \$P9K_AI_WORKSPACE +" 2>/dev/null) +assert_equals "Codex detection sets workspace label" "$out" "codex" + +out=$(HOME="$home" ZDOTDIR="$home" POWERLEVEL9K_DISABLE_INSTANT_PROMPT=true POWERLEVEL9K_AI_WORKSPACE_CONTEXT=manual zsh -f -c " + source ${(q)root}/powerlevel10k.zsh-theme + _p9k_init_vars + _p9k_init_params + _p9k_ai_workspace_detect + print -r -- \$P9K_AI_WORKSPACE +" 2>/dev/null) +out=${out##*$'\n'} +assert_equals "explicit workspace context wins" "$out" "manual" + +print +if (( fail )); then + print -P "%F{red}$fail test(s) failed%f, $pass passed" + exit 1 +else + print -P "%F{green}All $pass tests passed%f" +fi diff --git a/test/test_migrate_cli.zsh b/test/test_migrate_cli.zsh new file mode 100644 index 00000000..c3f8ed35 --- /dev/null +++ b/test/test_migrate_cli.zsh @@ -0,0 +1,89 @@ +#!/usr/bin/env zsh +# Tests for migrate.zsh dry-run, check mode and timestamped backups. + +emulate -L zsh +set -u + +local script="${0:A:h}/../migrate.zsh" +local -i pass=0 fail=0 + +function assert_contains() { + local desc=$1 haystack=$2 needle=$3 + if [[ $haystack == *$needle* ]]; then + print -P " %F{green}PASS%f: $desc" + (( pass++ )) + else + print -P " %F{red}FAIL%f: $desc" + print " Expected to contain: $needle" + print " Got: ${haystack[1,300]}" + (( fail++ )) + fi +} + +function assert_equals() { + local desc=$1 actual=$2 expected=$3 + if [[ $actual == $expected ]]; then + print -P " %F{green}PASS%f: $desc" + (( pass++ )) + else + print -P " %F{red}FAIL%f: $desc" + print " Expected: $expected" + print " Got: $actual" + (( fail++ )) + fi +} + +function assert_file_exists() { + local desc=$1 file=$2 + if [[ -f $file ]]; then + print -P " %F{green}PASS%f: $desc" + (( pass++ )) + else + print -P " %F{red}FAIL%f: $desc" + print " Missing file: $file" + (( fail++ )) + fi +} + +function read_file() { + local content + IFS= read -r content <$1 + print -r -- "$content" +} + +local tmp +tmp=$(mktemp -d "${TMPDIR:-/tmp}/p10k-migrate-test.XXXXXXXXXX") || exit 1 +trap 'rm -rf -- "$tmp"' EXIT + +mkdir -p "$tmp/check-home" "$tmp/dry-home/.config/sheldon" "$tmp/apply-home" + +local out +out=$(HOME="$tmp/check-home" ZDOTDIR="$tmp/check-home" zsh -f "$script" --check 2>&1) +assert_contains "check mode prints check header" "$out" "Migration Check" +assert_contains "check mode handles missing Zinit under nounset" "$out" "No romkatv/powerlevel10k references found" +out=$(HOME="$tmp/check-home" ZDOTDIR="$tmp/check-home" ZINIT=scalar zsh -f "$script" --check 2>&1) +assert_contains "check mode handles scalar Zinit under nounset" "$out" "No romkatv/powerlevel10k references found" + +print -r -- 'source romkatv/powerlevel10k' >"$tmp/dry-home/.zshrc" +print -r -- 'github = "romkatv/powerlevel10k"' >"$tmp/dry-home/.config/sheldon/plugins.toml" +out=$(HOME="$tmp/dry-home" ZDOTDIR="$tmp/dry-home" XDG_CONFIG_HOME="$tmp/dry-home/.config" zsh -f "$script" --dry-run 2>&1) +assert_contains "dry-run reports zshrc update" "$out" "Would update references in" +assert_contains "dry-run covers Sheldon config" "$out" "plugins.toml" +assert_contains "dry-run reports no writes" "$out" "No files, remotes, or caches were changed" +assert_equals "dry-run leaves zshrc unchanged" "$(read_file "$tmp/dry-home/.zshrc")" 'source romkatv/powerlevel10k' + +print -r -- 'zinit light romkatv/powerlevel10k' >"$tmp/apply-home/.zshrc" +out=$(P10K_MIGRATE_TIMESTAMP=20260428-123456 HOME="$tmp/apply-home" ZDOTDIR="$tmp/apply-home" zsh -f "$script" 2>&1) +assert_contains "apply reports config update count" "$out" "Config files updated:" +assert_contains "apply prints restore command" "$out" "restore:" +assert_equals "apply rewrites config reference" "$(read_file "$tmp/apply-home/.zshrc")" 'zinit light quantumnic/powerlevel10k' +assert_file_exists "apply creates timestamped backup" "$tmp/apply-home/.zshrc.p10k-migrate-20260428-123456-backup" +assert_equals "backup preserves original config" "$(read_file "$tmp/apply-home/.zshrc.p10k-migrate-20260428-123456-backup")" 'zinit light romkatv/powerlevel10k' + +print +if (( fail )); then + print -P "%F{red}$fail test(s) failed%f, $pass passed" + exit 1 +else + print -P "%F{green}All $pass tests passed%f" +fi diff --git a/test/test_p10k_doctor.zsh b/test/test_p10k_doctor.zsh new file mode 100644 index 00000000..8bc5c26d --- /dev/null +++ b/test/test_p10k_doctor.zsh @@ -0,0 +1,57 @@ +#!/usr/bin/env zsh +# Tests for p10k doctor and gitstatus diagnostics. + +emulate -L zsh + +local root="${0:A:h}/.." +local -i pass=0 fail=0 + +function assert_contains() { + local desc=$1 haystack=$2 needle=$3 + if [[ $haystack == *$needle* ]]; then + print -P " %F{green}PASS%f: $desc" + (( pass++ )) + else + print -P " %F{red}FAIL%f: $desc" + print " Expected to contain: $needle" + print " Got: ${haystack[1,300]}" + (( fail++ )) + fi +} + +local home +home=$(mktemp -d "${TMPDIR:-/tmp}/p10k-doctor-test.XXXXXXXXXX") || exit 1 +trap 'rm -rf -- "$home"' EXIT + +local out +out=$(HOME="$home" ZDOTDIR="$home" POWERLEVEL9K_DISABLE_INSTANT_PROMPT=true zsh -f -c " + source ${(q)root}/powerlevel10k.zsh-theme + p10k help doctor + p10k help gitstatus + p10k doctor + p10k doctor gitstatus + p10k gitstatus doctor +" 2>&1) + +local gitstatus_alias_out +gitstatus_alias_out=$(HOME="$home" ZDOTDIR="$home" POWERLEVEL9K_DISABLE_INSTANT_PROMPT=true zsh -f -c " + source ${(q)root}/powerlevel10k.zsh-theme + p10k gitstatus doctor +" 2>&1) + +assert_contains "doctor help is available" "$out" "Print environment diagnostics" +assert_contains "gitstatus diagnostics help is available" "$out" "Print diagnostics for gitstatus" +assert_contains "doctor prints heading" "$out" "Powerlevel10k Doctor" +assert_contains "doctor checks zsh" "$out" "zsh" +assert_contains "doctor checks installation" "$out" "installation" +assert_contains "doctor prints gitstatus section" "$out" "gitstatus" +assert_contains "gitstatus doctor checks plugin" "$out" "gitstatus.plugin.zsh" +assert_contains "gitstatus command alias checks plugin" "$gitstatus_alias_out" "gitstatus.plugin.zsh" + +print +if (( fail )); then + print -P "%F{red}$fail test(s) failed%f, $pass passed" + exit 1 +else + print -P "%F{green}All $pass tests passed%f" +fi From b52ae59ae2e1dee4bade919409f14f5da0838e73 Mon Sep 17 00:00:00 2001 From: Test User Date: Tue, 28 Apr 2026 23:08:34 +0200 Subject: [PATCH 10/10] Prepare verified v1.24.16 release The previous feature commit is ready to publish, so this updates the project-visible version and changelog to describe the release contents. Constraint: Release tag should match P9K_VERSION Rejected: Reuse v1.24.15 | latest release already uses that tag and code version Confidence: high Scope-risk: narrow Directive: Keep CHANGELOG entries aligned with release tags Tested: zsh -f test/run_all.zsh; repo-wide zsh -n; gitstatus/test-pure-fallback.zsh; make zwc; compiled-theme p10k doctor smoke with P9K_VERSION=1.24.16; native gitstatus build Not-tested: Native gitstatus build on non-darwin/arm64 platforms --- CHANGELOG.md | 11 +++++++++++ internal/p10k.zsh | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d00a4546..e48975b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to this community fork of Powerlevel10k. +## [v1.24.16] - 2026-04-28 + +### Added +- **migration**: Add `--check`/`--doctor` and `--dry-run` modes with timestamped backups, broader plugin manager/config detection, Homebrew tap detection, and clearer summary output. +- **diagnostics**: Add `p10k doctor`, `p10k doctor gitstatus`, and `p10k gitstatus doctor` for local environment and gitstatus/VCS backend checks. +- **ai_workspace**: Add an opt-in prompt segment for shells running under AI coding tools, with automatic detection for Codex, Claude, Gemini, Cursor, Windsurf, Aider, and VS Code agent environments. +- **tests**: Add migration CLI, p10k doctor, and ai_workspace segment tests. + +### Fixed +- **migration**: Handle missing or scalar `ZINIT` safely under `set -u` while checking Zinit plugin directories. + ## [v1.24.15] - 2026-02-25 ### Fixed diff --git a/internal/p10k.zsh b/internal/p10k.zsh index 94bcda01..48bccdac 100644 --- a/internal/p10k.zsh +++ b/internal/p10k.zsh @@ -10174,7 +10174,7 @@ if [[ $__p9k_dump_file != $__p9k_instant_prompt_dump_file && -n $__p9k_instant_p zf_rm -f -- $__p9k_instant_prompt_dump_file{,.zwc} 2>/dev/null fi -typeset -g P9K_VERSION=1.24.15 +typeset -g P9K_VERSION=1.24.16 if [[ ${VSCODE_SHELL_INTEGRATION-} == <1-> && ${+__p9k_force_term_shell_integration} == 0 ]]; then typeset -gri __p9k_force_term_shell_integration=1