Files
TerminalPolyjuice/nushell/modules/pj_prompt.nu
T

695 lines
21 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)
}
}
# 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 '(?<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)/")
}
# 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 { inside: false, root: "" }
}
let res = (^git rev-parse --is-inside-work-tree --show-toplevel | complete)
if ($res.exit_code != 0) {
return { inside: false, root: "" }
}
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 {
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, 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 "\\" "/")
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 = $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)
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 [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
}
# 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 $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 = ($lines | where {|l| $l | str starts-with "# stash " } | get 0? | default "")
let stashed_n = if ($stashed == "") {
0
} else {
(($stashed | parse -r '# stash (?<n>\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 ab_match = if ($ab == null) {
null
} else {
($ab | parse -r '# branch.ab \+(?<a>\d+) -(?<b>\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_n
ahead: $ahead
behind: $behind
}
}
def _pj_git_branch_module [p: record, git: record] {
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)"
}
}
# ---------------------------------------------------------------------------
# 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 <lang>" parts. See the commented block above to
# restore full language detection.
def _pj_lang_segments [] {
[]
}
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\r" }
let prompt_newline = "\n\r"
{
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 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|
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
})
}
}