Files
TerminalPolyjuice/nushell/modules/pj_prompt.nu
T
gwbeip 10d133444d feat(nushell): re-export completions for use-based startup
- switch completion module imports to export use so externs propagate

- extend managed completion command set and required file checks

- rename prompt static cache env var to PJ_LEFT_PROMPT_STATIC_PART

- load completion module via use in nu_polyjuice
2026-05-30 21:38:36 +08:00

665 lines
19 KiB
Nu
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
def _pj_detect_os_key [] {
let host_name = (((sys host | get name?) | default ($nu.os-info.name? | default "")) | into string | str downcase)
if ($host_name | str contains "darwin") {
return "Macos"
}
if ($host_name | str contains "windows") {
return "Windows"
}
let os_release = if ("/etc/os-release" | path exists) {
"/etc/os-release"
} else if ("/usr/lib/os-release" | path exists) {
"/usr/lib/os-release"
} else {
""
}
if ($os_release != "") {
let id_line = (open $os_release | lines | where {|l| $l | str starts-with "ID=" } | get 0?)
let id = if ($id_line == null) { "" } else { (($id_line | str replace "ID=" "") | str trim -c '"') }
match ($id | str downcase) {
"alpine" => "Alpine"
"alma" => "AlmaLinux"
"almalinux" => "AlmaLinux"
"amzn" => "Amazon"
"arch" => "Arch"
"artix" => "Artix"
"centos" => "CentOS"
"debian" => "Debian"
"endeavouros" => "EndeavourOS"
"fedora" => "Fedora"
"garuda" => "Garuda"
"gentoo" => "Gentoo"
"kali" => "Kali"
"linuxmint" => "Mint"
"manjaro" => "Manjaro"
"mariner" => "Mariner"
"nixos" => "NixOS"
"nobara" => "Nobara"
"opensuse" => "openSUSE"
"opensuse-leap" => "openSUSE"
"opensuse-tumbleweed" => "openSUSE"
"ol" => "OracleLinux"
"pop" => "Pop"
"raspbian" => "Raspbian"
"rhel" => "RedHatEnterprise"
"rocky" => "RockyLinux"
"solus" => "Solus"
"sles" => "SUSE"
"opensuse-slowroll" => "openSUSE"
"ubuntu" => "Ubuntu"
"void" => "Void"
_ => "Linux"
}
} else {
if ($host_name | str contains "linux") {
"Linux"
} else {
"Unknown"
}
}
}
def _pj_os_symbol_from_key [key: string] {
match $key {
"Alpaquita" => " "
"Alpine" => " "
"AlmaLinux" => " "
"Amazon" => " "
"Android" => " "
"Arch" => " "
"Artix" => " "
"CachyOS" => " "
"CentOS" => " "
"Debian" => " "
"DragonFly" => " "
"Emscripten" => " "
"EndeavourOS" => " "
"Fedora" => " "
"FreeBSD" => " "
"Garuda" => "󰛓 "
"Gentoo" => " "
"HardenedBSD" => "󰞌 "
"Illumos" => "󰈸 "
"Kali" => " "
"Linux" => " "
"Mabox" => " "
"Macos" => " "
"Manjaro" => " "
"Mariner" => " "
"MidnightBSD" => " "
"Mint" => " "
"NetBSD" => " "
"NixOS" => " "
"Nobara" => " "
"OpenBSD" => "󰈺 "
"openSUSE" => " "
"OracleLinux" => "󰌷 "
"Pop" => " "
"Raspbian" => " "
"Redhat" => " "
"RedHatEnterprise" => " "
"RockyLinux" => " "
"Redox" => "󰀘 "
"Solus" => "󰠳 "
"SUSE" => " "
"Ubuntu" => " "
"Unknown" => " "
"Void" => " "
"Windows" => "󰍲 "
_ => " "
}
}
def _pj_os_symbol [] {
let key = ($env.PJ_OS_KEY? | default (_pj_detect_os_key))
_pj_os_symbol_from_key $key
}
def _pj_palette_default [] {
{
c_reset: (ansi reset)
c_os: (ansi '#6b6b6b')
c_user: (ansi '#4ca47b')
c_root: (ansi '#c2295b')
c_host: (ansi '#4ca47b')
c_ssh: (ansi '#d65d0e')
c_dir: (ansi '#9d8fbe')
c_c: (ansi '#306898')
c_cpp: (ansi '#306898')
c_rust: (ansi '#000000')
c_zig: (ansi '#306898')
c_python: (ansi '#4685b7')
c_git: (ansi '#4ca47b')
c_alert: (ansi '#c2295b')
c_time: (ansi '#a6356f')
c_duration: (ansi '#a6356f')
c_shell: (ansi cyan_bold)
c_indicator_ok: (ansi green_bold)
c_indicator_err: (ansi red_bold)
c_right_error: (ansi red_bold)
}
}
def _pj_first_version [text: string] {
let parsed = ($text | parse -r '(?<ver>\d+(?:\.\d+)+)')
if (($parsed | length) > 0) {
($parsed | get ver.0)
} else {
""
}
}
def _pj_cmd_duration_ms [] {
let raw = ($env.CMD_DURATION_MS? | default 0)
let kind = ($raw | describe)
if ($kind == "int") {
return $raw
}
if ($kind == "duration") {
return (($raw | into int) / 1_000_000)
}
let s = ($raw | into string | str trim)
let parsed = ($s | parse -r '^(?<n>\d+)')
if (($parsed | length) > 0) {
(($parsed | get n.0) | into int)
} else {
0
}
}
def _pj_format_duration [ms: int] {
if ($ms < 1000) {
return $"($ms)ms"
}
let ms_part = ($ms mod 1000)
if ($ms < 60_000) {
let s = ($ms // 1000)
return $"($s)s ($ms_part)ms"
}
if ($ms < 3_600_000) {
let total_s = ($ms // 1000)
let s = ($total_s mod 60)
let m = ($total_s // 60)
return $"($m)m ($s)s ($ms_part)ms"
}
let total_s = ($ms // 1000)
let s = ($total_s mod 60)
let total_m = ($total_s // 60)
let m = ($total_m mod 60)
let h = ($total_m // 60)
$"($h)h ($m)m ($s)s ($ms_part)ms"
}
def _pj_is_path_within [path: string, base: string] {
if ($base == "") {
return false
}
if ($path == $base) {
return true
}
($path | str starts-with $"($base)/")
}
def _pj_git_root_normalized [] {
if (which ^git | is-empty) {
return ""
}
let top = (^git rev-parse --show-toplevel | complete)
if ($top.exit_code != 0) {
return ""
}
let raw = (($top.stdout | str trim) | str replace -a "\\" "/")
if ($raw == "/") {
"/"
} else {
($raw | str trim -r -c "/")
}
}
def _pj_truncate_pwd [pwd: string, length: int = 4] {
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 "\\" "/")
let normalized_pwd = if ($normalized_pwd_raw == "/") {
"/"
} else {
($normalized_pwd_raw | str trim -r -c "/")
}
let normalized_home = if ($normalized_home_raw == "/") {
"/"
} else {
($normalized_home_raw | str trim -r -c "/")
}
let in_home = if ($normalized_home == "") {
false
} else {
(_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)
if $in_repo {
let repo_name = ($repo_root | path basename)
let rel = if ($normalized_pwd == $repo_root) {
""
} else {
($normalized_pwd | str substring (($repo_root | str length) + 1)..)
}
let rel_parts = if ($rel == "") {
[]
} else {
($rel | split row "/" | where {|it| $it != "" })
}
let keep_rel_count = if ($length <= 1) { 0 } else { $length - 1 }
let shown_rel_parts = if (($rel_parts | length) <= $keep_rel_count) {
$rel_parts
} else {
($rel_parts | last $keep_rel_count)
}
if (($shown_rel_parts | length) == 0) {
return $"…/($repo_name)"
} else {
return $"…/($repo_name)/($shown_rel_parts | str join '/')"
}
}
let shown = if $in_home {
let rest = ($normalized_pwd | str substring ($normalized_home | str length)..)
if ($rest == "") { "~" } else { $"~($rest)" }
} else {
$normalized_pwd
}
if ($shown == "~") {
return "~"
}
let parts = if ($shown | str starts-with "~/") {
($shown | str substring 2.. | split row "/" | where {|it| $it != "" })
} else if ($shown | str starts-with "/") {
($shown | str substring 1.. | split row "/" | where {|it| $it != "" })
} else if ($shown =~ '^[A-Za-z]:(/|$)') {
let start = if ($shown =~ '^[A-Za-z]:/') { 3 } else { 2 }
($shown | str substring $start.. | split row "/" | where {|it| $it != "" })
} else {
($shown | split row "/" | where {|it| $it != "" })
}
let prefix = if ($shown | str starts-with "~/") {
"~/"
} else if ($shown | str starts-with "/") {
"/"
} else if ($shown =~ '^[A-Za-z]:(/|$)') {
let drive = ($shown | str substring 0..1)
$"($drive)/"
} else {
""
}
if (($parts | length) <= $length) {
if ($prefix == "") { ($parts | str join "/") } else { $"($prefix)($parts | str join '/')" }
} else {
let tail = ($parts | last $length | str join "/")
$"($prefix)…/($tail)"
}
}
def _pj_git_info [] {
if (which ^git | is-empty) {
return {
branch: ""
changed: 0
untracked: 0
stashed: 0
ahead: 0
behind: 0
}
}
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)
if ($status.exit_code != 0) {
return {
branch: $branch_text
changed: 0
untracked: 0
stashed: 0
ahead: 0
behind: 0
}
}
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 ab = ($lines | where {|l| $l | str starts-with "# branch.ab " } | get 0?)
let ahead = if ($ab == null) {
0
} else {
let m = (($ab | parse -r '# branch.ab \+(?<a>\d+) -(?<b>\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 \+(?<a>\d+) -(?<b>\d+)') | get 0?)
if ($m == null) { 0 } else { (($m | get b) | into int) }
}
{
branch: $branch_text
changed: $changed
untracked: $untracked
stashed: $stashed
ahead: $ahead
behind: $behind
}
}
def _pj_git_branch_module [p: record] {
let git = (_pj_git_info)
let branch = ($git.branch | default "")
mut status_parts = []
if (($git.changed | default 0) > 0) {
$status_parts = ($status_parts | append $"($p.c_alert)!($git.changed)($p.c_reset)")
}
if (($git.untracked | default 0) > 0) {
$status_parts = ($status_parts | append $"($p.c_alert)?($git.untracked)($p.c_reset)")
}
if (($git.stashed | default 0) > 0) {
$status_parts = ($status_parts | append $"($p.c_alert)\$($git.stashed)($p.c_reset)")
}
if (($git.ahead | default 0) > 0) {
$status_parts = ($status_parts | append $"($p.c_alert)($git.ahead)($p.c_reset)")
}
if (($git.behind | default 0) > 0) {
$status_parts = ($status_parts | append $"($p.c_alert)($git.behind)($p.c_reset)")
}
let git_status = if (($status_parts | length) > 0) {
$" (($status_parts | str join ' '))"
} else {
""
}
if ($branch == "") {
""
} else {
$"($p.c_git)on  ($branch)($git_status) ($p.c_reset)"
}
}
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 [] {
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 != "") {
$user
} else {
($env.USER? | default ($env.USERNAME? | default "user"))
}
let host = (sys host | get hostname? | default "")
let in_ssh = (($env.SSH_CONNECTION? | default "") != "" or ($env.SSH_TTY? | default "") != "")
let prompt_newline = if (($nu.os-info.name | str downcase) == "windows") { "\n\r" } else { "\n" }
{
os_symbol: (_pj_os_symbol)
effective_user: $effective_user
host: $host
in_ssh: $in_ssh
shell_name: "nu"
prompt_newline: $prompt_newline
}
}
def _pj_left_prompt [] {
let p = ($env.PJ_PROMPT_PALETTE? | default (_pj_palette_default))
let static = ($env.PJ_LEFT_PROMPT_STATIC_PART? | default (_pj_prompt_static_base))
let user_color = if ($static.effective_user == "root") { $p.c_root } else { $p.c_user }
let host_part = if $static.in_ssh {
$"($p.c_host)@($static.host)($p.c_reset)($p.c_ssh)  󰬚󰬚󰬏 ($p.c_reset)"
} else {
""
}
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 langs = (_pj_lang_segments)
let lang_parts = ($langs | each {|seg|
if ($seg | str starts-with "") {
$"($p.c_python)via ($seg) ($p.c_reset)"
} else if ($seg | str starts-with "󱘗") {
$"($p.c_rust)via ($seg) ($p.c_reset)"
} else if ($seg | str starts-with "") {
$"($p.c_cpp)via ($seg) ($p.c_reset)"
} else if ($seg | str starts-with "") {
$"($p.c_c)via ($seg) ($p.c_reset)"
} else if ($seg | str starts-with "") {
$"($p.c_zig)via ($seg) ($p.c_reset)"
} else {
$seg
}
})
let now = (date now | format date "%R")
let time_part = $"($p.c_time)($now) ($p.c_reset)"
let duration_text = (_pj_format_duration (_pj_cmd_duration_ms))
let duration_part = $"($p.c_duration) 󱫑 ($duration_text) ($p.c_reset)"
mut out = ""
$out = $"($out)($p.c_os)($static.os_symbol)($p.c_reset)"
$out = $"($out)($user_color)($static.effective_user)($p.c_reset)"
if ($host_part != "") { $out = $"($out)($host_part)" }
$out = $"($out)($shell_part)"
$out = $"($out)($p.c_dir) ($dir) ($p.c_reset)"
if ($git_part != "") { $out = $"($out)($git_part)" }
for m in $lang_parts {
$out = $"($out)($m)"
}
$out = $"($static.prompt_newline)($out)($time_part)($duration_part)($static.prompt_newline)"
$out
}
def _pj_prompt_indicator [] {
let p = ($env.PJ_PROMPT_PALETTE? | default (_pj_palette_default))
if (($env.LAST_EXIT_CODE? | default 0) == 0) {
$"($p.c_indicator_ok)($p.c_reset) "
} else {
$"($p.c_indicator_err)($p.c_reset) "
}
}
export-env {
load-env {
PJ_PROMPT_PALETTE: (_pj_palette_default)
PJ_OS_KEY: (_pj_detect_os_key)
PJ_LEFT_PROMPT_STATIC_PART: (_pj_prompt_static_base)
PROMPT_MULTILINE_INDICATOR: ":"
PROMPT_INDICATOR: {||
_pj_prompt_indicator
}
PROMPT_INDICATOR_VI_INSERT: {||
_pj_prompt_indicator
}
PROMPT_INDICATOR_VI_NORMAL: {||
_pj_prompt_indicator
}
PROMPT_COMMAND: {||
_pj_left_prompt
}
PROMPT_COMMAND_RIGHT: {||
let p = ($env.PJ_PROMPT_PALETTE? | default (_pj_palette_default))
if (($env.LAST_EXIT_CODE? | default 0) != 0) {
$"($p.c_right_error)Exit code: ($env.LAST_EXIT_CODE)($p.c_reset)"
} else {
""
}
}
config: ($env.config? | default {} | merge {
render_right_prompt_on_last_line: true
})
}
}