diff --git a/nushell/modules/pj_prompt.nu b/nushell/modules/pj_prompt.nu index 46f82c9..317a3c4 100644 --- a/nushell/modules/pj_prompt.nu +++ b/nushell/modules/pj_prompt.nu @@ -142,14 +142,17 @@ def _pj_palette_default [] { } } -def _pj_first_version [text: string] { - let parsed = ($text | parse -r '(?\d+(?:\.\d+)+)') - if (($parsed | length) > 0) { - ($parsed | get ver.0) - } else { - "" - } -} +# Disabled along with the language-version probing. See the commented block +# near `_pj_lang_segments` to restore. +# +# def _pj_first_version [text: string] { +# let parsed = ($text | parse -r '(?\d+(?:\.\d+)+)') +# if (($parsed | length) > 0) { +# ($parsed | get ver.0) +# } else { +# "" +# } +# } def _pj_cmd_duration_ms [] { let raw = ($env.CMD_DURATION_MS? | default 0) @@ -206,25 +209,32 @@ def _pj_is_path_within [path: string, base: string] { ($path | str starts-with $"($base)/") } -def _pj_git_root_normalized [] { +# Single `git rev-parse` call: returns whether we are inside a work tree and the +# normalized repo root (empty string when not in a repo). +def _pj_git_repo_info [] { if (which ^git | is-empty) { - return "" + return { inside: false, root: "" } } - let top = (^git rev-parse --show-toplevel | complete) - if ($top.exit_code != 0) { - return "" + let res = (^git rev-parse --is-inside-work-tree --show-toplevel | complete) + if ($res.exit_code != 0) { + return { inside: false, root: "" } } - let raw = (($top.stdout | str trim) | str replace -a "\\" "/") - if ($raw == "/") { - "/" + let parts = ($res.stdout | lines) + let inside_raw = ($parts | get 0? | default "") + let root_raw = ($parts | get 1? | default "") + let root_norm = if ($root_raw == "") { + "" } else { - ($raw | str trim -r -c "/") + let raw = (($root_raw | str trim) | str replace -a "\\" "/") + if ($raw == "/") { "/" } else { ($raw | str trim -r -c "/") } } + + { inside: (($inside_raw | str trim) == "true"), root: $root_norm } } -def _pj_truncate_pwd [pwd: string, length: int = 4] { +def _pj_truncate_pwd [pwd: string, length: int = 4, repo: record = {inside: false, root: ""}] { let home = (($nu.home-dir? | default ($nu.home-path? | default ($env.HOME? | default ""))) | into string) let normalized_pwd_raw = (($pwd | into string) | str replace -a "\\" "/") let normalized_home_raw = (($home | into string) | str replace -a "\\" "/") @@ -247,8 +257,8 @@ def _pj_truncate_pwd [pwd: string, length: int = 4] { (_pj_is_path_within $normalized_pwd $normalized_home) } - let repo_root = (_pj_git_root_normalized) - let in_repo = (_pj_is_path_within $normalized_pwd $repo_root) + let repo_root = $repo.root + let in_repo = ($repo.inside and (_pj_is_path_within $normalized_pwd $repo_root)) if $in_repo { let repo_name = ($repo_root | path basename) @@ -319,87 +329,80 @@ def _pj_truncate_pwd [pwd: string, length: int = 4] { } } -def _pj_git_info [] { - if (which ^git | is-empty) { - return { - branch: "" - changed: 0 - untracked: 0 - stashed: 0 - ahead: 0 - behind: 0 - } +def _pj_git_info [repo: record] { + let empty = { + branch: "" + changed: 0 + untracked: 0 + stashed: 0 + ahead: 0 + behind: 0 + } + # `repo.inside` can only be true when git exists, so no separate `which` + # check is needed here. + if (not $repo.inside) { + return $empty } - let repo_check = (^git rev-parse --is-inside-work-tree | complete) - if ($repo_check.exit_code != 0 or (($repo_check.stdout | str trim) != "true")) { - return { - branch: "" - changed: 0 - untracked: 0 - stashed: 0 - ahead: 0 - behind: 0 - } - } - - let branch_result = (^git branch --show-current | complete) - let branch = ($branch_result.stdout | str trim) - let branch_text = if ($branch != "") { - $branch - } else { - let head_result = (^git rev-parse --short HEAD | complete) - if ($head_result.exit_code == 0) { - ($head_result.stdout | str trim) - } else { - "" - } - } - - let status = (^git status --porcelain=2 --branch | complete) + # One `git status` call carries: branch name, ahead/behind, stash count, + # and the changed/untracked entries. Previously this was 4 separate git + # invocations (rev-parse, branch --show-current, status, stash list). + let status = (^git status --porcelain=2 --branch --show-stash | complete) if ($status.exit_code != 0) { - return { - branch: $branch_text - changed: 0 - untracked: 0 - stashed: 0 - ahead: 0 - behind: 0 - } + return $empty } let lines = ($status.stdout | lines) let changed = ($lines | where {|l| ($l | str starts-with "1 ") or ($l | str starts-with "2 ") or ($l | str starts-with "u ") } | length) let untracked = ($lines | where {|l| $l | str starts-with "? " } | length) - let stashed = ((^git stash list | complete).stdout | lines | length) + + let stashed = ($lines | where {|l| $l | str starts-with "# stash " } | get 0? | default "") + let stashed_n = if ($stashed == "") { + 0 + } else { + (($stashed | parse -r '# stash (?\d+)' | get 0? | default {n: "0"} | get n) | into int) + } + + let head_line = ($lines | where {|l| $l | str starts-with "# branch.head " } | get 0? | default "") + let head_name = if ($head_line == "") { + "" + } else { + ($head_line | str replace "# branch.head " "" | str trim) + } + # Detached HEAD: fall back to the short oid (first 7 chars), matching the + # previous `git rev-parse --short HEAD` behaviour. + let branch_text = if ($head_name == "(detached)") { + let oid_line = ($lines | where {|l| $l | str starts-with "# branch.oid " } | get 0? | default "") + let oid = if ($oid_line == "") { + "" + } else { + ($oid_line | str replace "# branch.oid " "" | str trim) + } + ($oid | str substring 0..7) + } else { + $head_name + } + let ab = ($lines | where {|l| $l | str starts-with "# branch.ab " } | get 0?) - - let ahead = if ($ab == null) { - 0 + let ab_match = if ($ab == null) { + null } else { - let m = (($ab | parse -r '# branch.ab \+(?\d+) -(?\d+)') | get 0?) - if ($m == null) { 0 } else { (($m | get a) | into int) } - } - - let behind = if ($ab == null) { - 0 - } else { - let m = (($ab | parse -r '# branch.ab \+(?\d+) -(?\d+)') | get 0?) - if ($m == null) { 0 } else { (($m | get b) | into int) } + ($ab | parse -r '# branch.ab \+(?\d+) -(?\d+)' | get 0?) } + let ahead = if ($ab_match == null) { 0 } else { (($ab_match | get a) | into int) } + let behind = if ($ab_match == null) { 0 } else { (($ab_match | get b) | into int) } { branch: $branch_text changed: $changed untracked: $untracked - stashed: $stashed + stashed: $stashed_n ahead: $ahead behind: $behind } } -def _pj_git_branch_module [p: record] { - let git = (_pj_git_info) +def _pj_git_branch_module [p: record, git: record] { let branch = ($git.branch | default "") mut status_parts = [] @@ -432,122 +435,146 @@ def _pj_git_branch_module [p: record] { } } -def _pj_has_glob [pattern: string] { - ((glob $pattern | length) > 0) -} - -def _pj_c_version [] { - if (which ^cc | is-empty) { - return "" - } - let out = (^cc --version | complete) - _pj_first_version (($out.stdout | str trim) + " " + ($out.stderr | str trim)) -} - -def _pj_cpp_version [] { - if (which ^'c++' | is-empty) { - return "" - } - let out = (^'c++' --version | complete) - _pj_first_version (($out.stdout | str trim) + " " + ($out.stderr | str trim)) -} - -def _pj_rust_version [] { - if (which ^rustc | is-empty) { - return "" - } - let out = (^rustc --version | complete) - _pj_first_version (($out.stdout | str trim) + " " + ($out.stderr | str trim)) -} - -def _pj_zig_version [] { - if (which ^zig | is-empty) { - return "" - } - let out = (^zig version | complete) - _pj_first_version (($out.stdout | str trim) + " " + ($out.stderr | str trim)) -} - -def _pj_python_version [] { - if (which ^python | is-not-empty) { - let out = (^python --version | complete) - return (_pj_first_version (($out.stdout | str trim) + " " + ($out.stderr | str trim))) - } - - if (which ^python3 | is-not-empty) { - let out = (^python3 --version | complete) - return (_pj_first_version (($out.stdout | str trim) + " " + ($out.stderr | str trim))) - } - - "" -} - -def _pj_python_venv [] { - let venv = ($env.VIRTUAL_ENV? | default "") - if ($venv != "") { - return ($venv | path basename) - } - ($env.CONDA_DEFAULT_ENV? | default "") -} +# --------------------------------------------------------------------------- +# Programming-language detection and version probing — DISABLED. +# +# These were the dominant cost in prompt rendering (up to ~660ms/frame in +# projects with source files, mostly from spawning cc/c++/python --version). +# They are commented out for now. To re-enable, uncomment the block below and +# remove (or rename) the stub `_pj_lang_segments` that returns an empty list. +# +# def _pj_has_glob [pattern: string] { +# ((glob $pattern | length) > 0) +# } +# +# def _pj_c_version [] { +# if (which ^cc | is-empty) { +# return "" +# } +# let out = (^cc --version | complete) +# _pj_first_version (($out.stdout | str trim) + " " + ($out.stderr | str trim)) +# } +# +# def _pj_cpp_version [] { +# if (which ^'c++' | is-empty) { +# return "" +# } +# let out = (^'c++' --version | complete) +# _pj_first_version (($out.stdout | str trim) + " " + ($out.stderr | str trim)) +# } +# +# def _pj_rust_version [] { +# if (which ^rustc | is-empty) { +# return "" +# } +# let out = (^rustc --version | complete) +# _pj_first_version (($out.stdout | str trim) + " " + ($out.stderr | str trim)) +# } +# +# def _pj_zig_version [] { +# if (which ^zig | is-empty) { +# return "" +# } +# let out = (^zig version | complete) +# _pj_first_version (($out.stdout | str trim) + " " + ($out.stderr | str trim)) +# } +# +# def _pj_python_version [] { +# if (which ^python | is-not-empty) { +# let out = (^python --version | complete) +# return (_pj_first_version (($out.stdout | str trim) + " " + ($out.stderr | str trim))) +# } +# +# if (which ^python3 | is-not-empty) { +# let out = (^python3 --version | complete) +# return (_pj_first_version (($out.stdout | str trim) + " " + ($out.stderr | str trim))) +# } +# +# "" +# } +# +# def _pj_python_venv [] { +# let venv = ($env.VIRTUAL_ENV? | default "") +# if ($venv != "") { +# return ($venv | path basename) +# } +# ($env.CONDA_DEFAULT_ENV? | default "") +# } +# +# def _pj_lang_segments [] { +# # One glob sweep replaces 11 separate glob calls. The resulting set of +# # lowercase extensions is checked per-language below. glob is +# # case-insensitive on Windows, so *.CPP is matched by "cpp" too. +# let exts = (glob *.{c,h,cpp,cc,cxx,hpp,hh,hxx,rs,zig,py} +# | each {|f| $f | path parse | get extension | str downcase } +# | uniq) +# +# let has_c = ("c" in $exts or "h" in $exts) +# let has_cpp = (["cpp" "cc" "cxx" "hpp" "hh" "hxx"] | any {|e| $e in $exts }) +# let has_rust = (("Cargo.toml" | path exists) or ("rs" in $exts)) +# let has_zig = (("build.zig" | path exists) or ("zig" in $exts)) +# let has_python = (("pyproject.toml" | path exists) or ("requirements.txt" | path exists) or (".python-version" | path exists) or ("py" in $exts)) +# +# mut out = [] +# if $has_c { +# let v = (_pj_c_version) +# if ($v != "") { +# $out = ($out | append $" v($v)") +# } else { +# $out = ($out | append "") +# } +# } +# +# if $has_cpp { +# let v = (_pj_cpp_version) +# if ($v != "") { +# $out = ($out | append $" v($v)") +# } else { +# $out = ($out | append "") +# } +# } +# +# if $has_rust { +# let v = (_pj_rust_version) +# if ($v != "") { +# $out = ($out | append $"󱘗 v($v)") +# } else { +# $out = ($out | append "󱘗") +# } +# } +# +# if $has_zig { +# let v = (_pj_zig_version) +# if ($v != "") { +# $out = ($out | append $" v($v)") +# } else { +# $out = ($out | append "") +# } +# } +# +# if $has_python { +# let pyver = (_pj_python_version) +# let venv = (_pj_python_venv) +# let py_base = if ($pyver != "") { $" v($pyver)" } else { "" } +# if ($venv != "") { +# $out = ($out | append $"($py_base) \(($venv)\)") +# } else { +# $out = ($out | append $py_base) +# } +# } +# +# $out +# } +# --------------------------------------------------------------------------- +# Stub: language segments are disabled. Returns an empty list so the prompt +# simply omits the "via " parts. See the commented block above to +# restore full language detection. def _pj_lang_segments [] { - let has_c = ((_pj_has_glob "*.c") or (_pj_has_glob "*.h")) - let has_cpp = ((_pj_has_glob "*.cpp") or (_pj_has_glob "*.cc") or (_pj_has_glob "*.cxx") or (_pj_has_glob "*.hpp") or (_pj_has_glob "*.hh") or (_pj_has_glob "*.hxx")) - let has_rust = (("Cargo.toml" | path exists) or (_pj_has_glob "*.rs")) - let has_zig = (("build.zig" | path exists) or (_pj_has_glob "*.zig")) - let has_python = (("pyproject.toml" | path exists) or ("requirements.txt" | path exists) or (".python-version" | path exists) or (_pj_has_glob "*.py")) - - mut out = [] - if $has_c { - let v = (_pj_c_version) - if ($v != "") { - $out = ($out | append $" v($v)") - } else { - $out = ($out | append "") - } - } - - if $has_cpp { - let v = (_pj_cpp_version) - if ($v != "") { - $out = ($out | append $" v($v)") - } else { - $out = ($out | append "") - } - } - - if $has_rust { - let v = (_pj_rust_version) - if ($v != "") { - $out = ($out | append $"󱘗 v($v)") - } else { - $out = ($out | append "󱘗") - } - } - - if $has_zig { - let v = (_pj_zig_version) - if ($v != "") { - $out = ($out | append $" v($v)") - } else { - $out = ($out | append "") - } - } - - if $has_python { - let pyver = (_pj_python_version) - let venv = (_pj_python_venv) - let py_base = if ($pyver != "") { $" v($pyver)" } else { "" } - if ($venv != "") { - $out = ($out | append $"($py_base) \(($venv)\)") - } else { - $out = ($out | append $py_base) - } - } - - $out + [] } + def _pj_prompt_static_base [] { let user = (try { whoami } catch { "" } | into string | str trim) let effective_user = if ($user != "") { @@ -584,8 +611,10 @@ def _pj_left_prompt [] { } let shell_part = $"($p.c_shell) ($static.shell_name) ($p.c_reset)" - let dir = (_pj_truncate_pwd $env.PWD 4) - let git_part = (_pj_git_branch_module $p) + let repo = (_pj_git_repo_info) + let dir = (_pj_truncate_pwd $env.PWD 4 $repo) + let git = (_pj_git_info $repo) + let git_part = (_pj_git_branch_module $p $git) let langs = (_pj_lang_segments) let lang_parts = ($langs | each {|seg|