Compare commits

...

19 Commits

Author SHA1 Message Date
gwbeip 0cae2d0fd9 chore(nushell): remove risky and redundant aliases 2026-06-14 21:54:13 +08:00
gwbeip 8544a7ae7b perf(nushell): collapse git calls and disable language probing in prompt 2026-06-14 21:54:02 +08:00
gwbeip d7377a57c6 style(helix): format statusline arrays for readability 2026-06-14 14:37:36 +08:00
gwbeip 2f1af4da8b feat(helix): add leader+l keymap to toggle line-number mode 2026-06-14 13:26:50 +08:00
gwbeip e8d166ad8c feat(helix): add auto-format, indent-guides, cursor-shape, and file-picker config 2026-06-14 12:42:14 +08:00
gwbeip a9d3e2b329 feat(deepseek api): add printkey subcommand 2026-06-14 11:18:22 +08:00
gwbeip 727e4977f8 feat(deepseek api): add balance query and API key management
Add pj deepseek command to query account balance and
pj deepseek setkey to store API key in local cache.

Also extract Python launcher into its own module (pj_python.nu)
and add common aliases (g, c, tree, myip, ports, df, reload, edit).
2026-06-14 11:10:16 +08:00
gwbeip 1b801ef121 Merge remote-tracking branch 'refs/remotes/origin/main' 2026-06-05 10:10:36 +08:00
gwbeip db99887fb5 feat(Bash): only activate fzf keybindings when logging in interactively. 2026-06-05 10:07:00 +08:00
gwbeip 1a8880749e fix(Nushell): 2026-06-03 13:04:25 +08:00
gwbeip 768c1de824 refactor nushell zoxide bootstrap and windows path normalization 2026-05-31 16:16:19 +08:00
gwbeip 7b4c13791c Add Nushell brew and uv helpers 2026-05-31 01:04:45 +08:00
gwbeip 39f94d1c1f feat(nushell): remap fzf history hotkey to alt-r
- change fzf_history_selector keycode from char_h to char_r
2026-05-30 23:57:05 +08:00
gwbeip 804b81a0e5 feat(nushell): gate cache clone behind update flag
- only clone missing nu_scripts cache when using -u/-f

- keep default pj completion as non-cloning check mode

- align completion flag help text with new behavior
2026-05-30 23:19:41 +08:00
gwbeip 356d4ab780 feat(nushell): add auto-bootstrapped completion registry flow
- generate cache registry module from command list and load it at startup

- add pj completion sync (-s) with optional help and restart behavior

- keep missing-cache fallback via noop module and preserve cache ignore rules
2026-05-30 23:12:21 +08:00
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
gwbeip 4aaf4199bd perf(nushell): cache static prompt context
- precompute os symbol, user/host, ssh flag, shell label and newline sequence at startup

- reuse cached values in _pj_left_prompt to reduce per-render overhead
2026-05-30 20:32:42 +08:00
gwbeip 9a0ad9384a feat(nushell): add cached nu_scripts completions bootstrap
- add pj completion command to clone/update nu_scripts cache

- load fixed custom-completions for git/ssh/cargo/uv/rustup from cache

- ignore nushell/cache contents in git while keeping placeholder .gitignore

- source pj_completions at startup and align prompt indicators/newline behavior
2026-05-30 20:29:46 +08:00
gwbeip c660e0d621 refactor nushell prompt helpers and reduce call depth 2026-05-30 16:42:57 +08:00
14 changed files with 656 additions and 277 deletions
+5 -3
View File
@@ -51,6 +51,8 @@ __pj_fzf_history_widget() {
}
# Alt-f: file selector, Alt-d: directory selector, Alt-h: history selector
bind -x '"\ef":__pj_fzf_file_widget'
bind -x '"\ed":__pj_fzf_dir_widget'
bind -x '"\eh":__pj_fzf_history_widget'
if [[ $- == *i* ]]; then
bind -x '"\ef":__pj_fzf_file_widget'
bind -x '"\ed":__pj_fzf_dir_widget'
bind -x '"\eh":__pj_fzf_history_widget'
fi
+37 -2
View File
@@ -6,14 +6,49 @@ cursorline = true
cursorcolumn = true
default-yank-register = "+"
continue-comments = true
auto-format = true
trim-trailing-whitespace = true
insert-final-newline = true
scrolloff = 5
[editor.cursor-shape]
normal = "block"
insert = "bar"
select = "underline"
[editor.statusline]
left = ["mode", "spinner", "read-only-indicator", "file-modification-indicator", "version-control"]
left = [
"mode",
"spinner",
"read-only-indicator",
"file-modification-indicator",
"version-control",
]
center = ["file-name", "total-line-numbers", "position-percentage"]
right = ["diagnostics", "selections", "position", "file-encoding", "file-line-ending", "file-type"]
right = [
"diagnostics",
"selections",
"position",
"file-encoding",
"file-line-ending",
"file-type",
]
separator = "│"
mode.normal = "NORMAL"
mode.insert = "INSERT"
mode.select = "SELECT"
diagnostics = ["warning", "error"]
workspace-diagnostics = ["warning", "error"]
[editor.indent-guides]
render = true
character = "╎"
skip-level = 1
[editor.file-picker]
hidden = false
parents = false
[keys.normal.space.l]
r = ":set line-number relative"
a = ":set line-number absolute"
+3
View File
@@ -0,0 +1,3 @@
*
!.gitignore
!noop.nu
+1
View File
@@ -0,0 +1 @@
# Fallback completion module intentionally exports nothing.
+9 -11
View File
@@ -14,18 +14,16 @@ export alias ....... = cd ../../../../../..
export alias ........ = cd ../../../../../../..
export alias .......... = cd ../../../../../../../..
export def py [...rest: string] {
if (which ^py | is-not-empty) {
^py ...$rest
} else if (which ^python3 | is-not-empty) {
^python3 ...$rest
} else {
print "Error: Python is not installed or could not be found."
}
}
export alias weather = curl wttr.in
export alias moon = curl wttr.in/Moon
export alias quote = curl https://api.quotable.io/random | from json | get content
export alias dadjoke = curl -H "Accept: application/json" https://icanhazdadjoke.com/ | from json | get joke
# --- System ---
export def df [] {
if (which ^df | is-not-empty) {
^df -h
} else {
^wmic logicaldisk get size,freespace,caption
}
}
+24
View File
@@ -0,0 +1,24 @@
export def --env "pj brewthu" [] {
let os_name = ($nu.os-info.name? | default "" | into string | str downcase)
if not (($os_name | str contains "darwin") or ($os_name | str contains "macos")) {
return
}
let bottle_domain = ($env.HOMEBREW_BOTTLE_DOMAIN? | default "")
if ($bottle_domain | is-empty) {
load-env {
HOMEBREW_API_DOMAIN: "https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles/api"
HOMEBREW_BOTTLE_DOMAIN: "https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles"
HOMEBREW_BREW_GIT_REMOTE: "https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/brew.git"
HOMEBREW_CORE_GIT_REMOTE: "https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/homebrew-core.git"
HOMEBREW_PIP_INDEX_URL: "https://pypi.tuna.tsinghua.edu.cn/simple"
}
} else {
hide-env HOMEBREW_API_DOMAIN
hide-env HOMEBREW_BOTTLE_DOMAIN
hide-env HOMEBREW_BREW_GIT_REMOTE
hide-env HOMEBREW_CORE_GIT_REMOTE
hide-env HOMEBREW_PIP_INDEX_URL
}
}
+185
View File
@@ -1,3 +1,173 @@
const pj_module_dir = (path self | path dirname)
const pj_cache_dir = ($pj_module_dir | path join '..' 'cache')
const pj_completion_base_dir = ($pj_cache_dir | path join 'nu_scripts' 'custom-completions')
const pj_completion_commands = [git ssh cargo uv rustup make pytest rg tar vscode winget zig]
const pj_completion_noop_module = ($pj_cache_dir | path join 'noop.nu')
const pj_completion_registry_module = ($pj_cache_dir | path join 'pj_completions_registry.nu')
const pj_completion_bootstrap_module = if ($pj_completion_registry_module | path exists) {
$pj_completion_registry_module
} else {
$pj_completion_noop_module
}
def _pj_completion_script_path [name: string] {
($pj_completion_base_dir | path join $name $"($name)-completions.nu")
}
def _pj_required_completion_files [] {
$pj_completion_commands | each {|name|
{
command: $name
file: (_pj_completion_script_path $name)
}
}
}
def _pj_completion_registry_lines [] {
let header = [
"# Auto-generated by `pj completion sync`."
"# Edit pj_completion_commands in pj_completions.nu, then re-run sync."
"const pj_registry_dir = (path self | path dirname)"
"const pj_completion_base_dir = ($pj_registry_dir | path join 'nu_scripts' 'custom-completions')"
"const pj_completion_noop_module = ($pj_registry_dir | path join 'noop.nu')"
""
]
let lines = ($pj_completion_commands | each {|name|
[
(
"export use (if ((($pj_completion_base_dir | path join '"
+ $name
+ "' '"
+ $name
+ "-completions.nu') | path exists)) { ($pj_completion_base_dir | path join '"
+ $name
+ "' '"
+ $name
+ "-completions.nu') } else { $pj_completion_noop_module }) *"
)
""
]
} | flatten)
($header | append $lines)
}
def _pj_completion_help [] {
print "pj completion commands:"
print " pj completion Check required completion files from existing cache."
print " pj completion -u Update cache, or clone nu_scripts when cache is missing."
print " pj completion -f Force re-clone cached nu_scripts repository."
print " pj completion sync Regenerate cache registry and restart Nushell."
print " pj completion -s Same as `pj completion sync`."
print " pj completion -h Show this help message."
}
export def "pj completion sync" [
--help(-h)
] {
if $help {
_pj_completion_help
return
}
mkdir $pj_cache_dir
let content = ((_pj_completion_registry_lines) | str join "\n")
$content | save -f $pj_completion_registry_module
print $"[pj completion] synced: ($pj_completion_registry_module)"
print "[pj completion] restarting Nushell..."
exec nu
}
export def "pj completion" [
--update(-u) # Update cached repo, or clone when cache is missing.
--force(-f) # Force re-clone cached nu_scripts repository.
--sync(-s) # Regenerate static registry and restart Nushell.
--help(-h) # Show help.
] {
if $help {
_pj_completion_help
return
}
if $sync {
pj completion sync
return
}
let repo_url = "https://github.com/nushell/nu_scripts.git"
let cache_dir = if ('NU_POLYJUICE_PATH' in $env) {
($env.NU_POLYJUICE_PATH | path join 'cache')
} else {
(pwd | path join 'nushell' 'cache')
}
let repo_dir = ($cache_dir | path join 'nu_scripts')
let completion_dir = ($repo_dir | path join 'custom-completions')
if (which git | is-empty) {
error make {
msg: "`git` command is required for pj completion"
help: "Install Git first, then run `pj completion` again."
}
}
mkdir $cache_dir
if $force {
if ($repo_dir | path exists) {
rm -rf $repo_dir
}
}
if not ($repo_dir | path exists) {
if ($update or $force) {
print "[pj completion] nu_scripts cache not found, cloning..."
^git clone $repo_url $repo_dir
} else {
print "[pj completion] nu_scripts cache not found. run `pj completion -u` to clone it."
return {
cache_dir: $cache_dir
repo_dir: $repo_dir
completion_dir: $completion_dir
commands: $pj_completion_commands
missing_files: []
}
}
} else {
if not (($repo_dir | path join '.git') | path exists) {
print "[pj completion] existing cache is not a git repo, re-cloning..."
rm -rf $repo_dir
^git clone $repo_url $repo_dir
} else if $update {
print "[pj completion] cache exists, updating..."
^git -C $repo_dir pull --ff-only
}
}
if not ($completion_dir | path exists) {
error make {
msg: "nu_scripts cloned, but custom-completions directory is missing"
help: $"Check repository content under ($repo_dir)."
}
}
let required = (_pj_required_completion_files)
let missing = ($required | where {|it| not ($it.file | path exists) })
if not ($missing | is-empty) {
print "[pj completion] warning: some required completion files are missing:"
$missing | each {|it| print $" - ($it.command): ($it.file)" }
}
print "[pj completion] ok"
{
cache_dir: $cache_dir
repo_dir: $repo_dir
completion_dir: $completion_dir
commands: $pj_completion_commands
missing_files: $missing
}
}
export-env {
$env.config = {
completions: {
@@ -11,4 +181,19 @@ export-env {
}
}
}
if not ($pj_completion_registry_module | path exists) {
mkdir $pj_cache_dir
let content = ((_pj_completion_registry_lines) | str join "\n")
$content | save -f $pj_completion_registry_module
if (($env.PJ_COMPLETION_BOOTSTRAPPED? | default "0") != "1") {
load-env { PJ_COMPLETION_BOOTSTRAPPED: "1" }
print "[pj completion] generated cache registry, restarting Nushell..."
exec nu
}
}
}
# Re-export completion externs from generated static registry.
export use $pj_completion_bootstrap_module *
+74
View File
@@ -0,0 +1,74 @@
const pj_module_dir = (path self | path dirname)
const pj_cache_dir = ($pj_module_dir | path join '..' 'cache')
const key_file = ($pj_cache_dir | path join 'deepseek_apikey.toml')
# Read the stored API key from cache, printing an error and returning null on failure.
def _pj_deepseek_readkey [] {
if not ($key_file | path exists) {
print $"(char --unicode '274c') No DeepSeek API key found. Use `pj deepseek setkey <api-key>` to set it."
return null
}
let config = (open $key_file)
let api_key = ($config.apikey?.key? | default "")
if ($api_key | is-empty) {
print $"(char --unicode '274c') Invalid API key in config file. Use `pj deepseek setkey <api-key>` to set a new one."
return null
}
$api_key
}
# Save your DeepSeek API key to local cache.
export def "pj deepseek setkey" [api_key: string] {
mkdir $pj_cache_dir
let content = $"[apikey]
key = \"($api_key)\"
"
$content | save -f $key_file
print $"(char --unicode '2714') DeepSeek API key saved to ($key_file)"
}
# Print the stored DeepSeek API key.
export def "pj deepseek printkey" [] {
let api_key = (_pj_deepseek_readkey)
if ($api_key == null) { return }
print $api_key
}
# Query DeepSeek account balance information.
export def "pj deepseek" [] {
if not ($key_file | path exists) {
print $"(char --unicode '274c') No DeepSeek API key found. Use `pj deepseek setkey <api-key>` to set it."
return
}
let api_key = (_pj_deepseek_readkey)
if ($api_key == null) { return }
let response = (curl -L -X GET 'https://api.deepseek.com/user/balance' -H 'Accept: application/json' -H $"Authorization: Bearer ($api_key)" | complete)
if ($response.exit_code != 0) {
print $"(char --unicode '274c') Failed to query balance: ($response.stderr)"
return
}
let balance = ($response.stdout | from json)
let available_text = if $balance.is_available { $'(char --unicode '2705') Yes' } else { $'(char --unicode '274c') No' }
let separator = ('' | fill --character (char --unicode '2501') --width 25 --alignment l)
print ""
print " DeepSeek Account Balance"
print $" ($separator)"
print $" Available : ($available_text)"
print ""
for info in $balance.balance_infos {
print $" Currency : ($info.currency)"
print $" Total : ($info.total_balance)"
print $" Granted : ($info.granted_balance)"
print $" Topped-up : ($info.topped_up_balance)"
print ""
}
}
+1 -1
View File
@@ -2,7 +2,7 @@
const fzf_history_selector = {
name: fzf_history_selector
modifier: alt
keycode: char_h
keycode: char_r
mode: [emacs, vi_insert, vi_normal]
event: [
{
+269 -257
View File
@@ -142,42 +142,17 @@ def _pj_palette_default [] {
}
}
def _pj_palette [] {
($env.PJ_PROMPT_PALETTE? | default (_pj_palette_default))
}
def _pj_detect_user [] {
let name = (try { whoami } catch { "" } | into string | str trim)
if ($name != "") {
return $name
}
($env.USER? | default ($env.USERNAME? | default "user"))
}
def _pj_runtime_default [] {
let user = (_pj_detect_user)
let host = (sys host | get hostname? | default "")
{
user: $user
is_root: ($user == "root")
host: $host
in_ssh: (($env.SSH_CONNECTION? | default "") != "" or ($env.SSH_TTY? | default "") != "")
}
}
def _pj_runtime [] {
(_pj_runtime_default)
}
def _pj_first_version [text: string] {
let parsed = ($text | parse -r '(?<ver>\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 '(?<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)
@@ -234,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 "\\" "/")
@@ -275,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)
@@ -347,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 (?<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 ahead = if ($ab == null) {
0
let ab_match = if ($ab == null) {
null
} 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) }
($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
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 = []
@@ -460,152 +435,186 @@ 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 <lang>" 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_shell_module [p: record] {
$"($p.c_shell) nu ($p.c_reset)"
}
def _pj_time_module [p: record] {
let now = (date now | format date "%R")
$"($p.c_time)($now) ($p.c_reset)"
}
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"))
}
def _pj_duration_module [p: record] {
let ms = (_pj_cmd_duration_ms)
let duration_text = (_pj_format_duration $ms)
$"($p.c_duration) 󱫑 ($duration_text) ($p.c_reset)"
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 = (_pj_palette)
let runtime = (_pj_runtime)
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 ($runtime.is_root | default false) { $p.c_root } else { $p.c_user }
let user_color = if ($static.effective_user == "root") { $p.c_root } else { $p.c_user }
let host_part = if ($runtime.in_ssh | default false) {
$"($p.c_host)@($runtime.host)($p.c_reset)($p.c_ssh)  󰬚󰬚󰬏 ($p.c_reset)"
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 = (_pj_shell_module $p)
let dir = (_pj_truncate_pwd $env.PWD 4)
let git_part = (_pj_git_branch_module $p)
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|
@@ -624,12 +633,14 @@ def _pj_left_prompt [] {
}
})
let time_part = (_pj_time_module $p)
let duration_part = (_pj_duration_module $p)
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)(_pj_os_symbol)($p.c_reset)"
$out = $"($out)($user_color)($runtime.user)($p.c_reset)"
$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)"
@@ -637,12 +648,12 @@ def _pj_left_prompt [] {
for m in $lang_parts {
$out = $"($out)($m)"
}
$out = $"\n($out)($time_part)($duration_part)\n"
$out = $"($static.prompt_newline)($out)($time_part)($duration_part)($static.prompt_newline)"
$out
}
def _pj_prompt_indicator [] {
let p = (_pj_palette)
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 {
@@ -650,30 +661,31 @@ def _pj_prompt_indicator [] {
}
}
def _pj_right_prompt [] {
let p = (_pj_palette)
if (($env.LAST_EXIT_CODE? | default 0) != 0) {
$"($p.c_right_error)Exit code: ($env.LAST_EXIT_CODE)($p.c_reset)"
} else {
$""
}
}
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: ""
PROMPT_INDICATOR_VI_NORMAL: ""
PROMPT_INDICATOR_VI_INSERT: {||
_pj_prompt_indicator
}
PROMPT_INDICATOR_VI_NORMAL: {||
_pj_prompt_indicator
}
PROMPT_COMMAND: {||
_pj_left_prompt
}
PROMPT_COMMAND_RIGHT: {||
_pj_right_prompt
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
+10
View File
@@ -0,0 +1,10 @@
# Python launcher: prefer `py` (Windows launcher), fallback to `python3`
export def py [...rest: string] {
if (which ^py | is-not-empty) {
^py ...$rest
} else if (which ^python3 | is-not-empty) {
^python3 ...$rest
} else {
print "Error: Python is not installed or could not be found."
}
}
+16
View File
@@ -0,0 +1,16 @@
export def --env "pj uvthu" [] {
let os_name = ($nu.os-info.name? | default "" | into string | str downcase)
if not (($os_name | str contains "darwin") or ($os_name | str contains "macos")) {
return
}
let uv_index_url = ($env.UV_INDEX_URL? | default "")
if ($uv_index_url | is-empty) {
load-env {
UV_INDEX_URL: "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/"
}
} else {
hide-env UV_INDEX_URL
}
}
+18 -3
View File
@@ -41,6 +41,20 @@ export-env {
# When using zoxide with --no-cmd, alias these internal functions as desired.
#
def _pj_normalize_windows_drive [path: string] {
if (($nu.os-info.name | str downcase) != "windows") {
return $path
}
let parsed = ($path | parse --regex '^(?<drive>[a-zA-Z]):(?<rest>.*)$')
if ($parsed | is-empty) {
return $path
}
let item = ($parsed | first)
$"(($item.drive | str upcase)):($item.rest)"
}
# Jump to a directory using only keywords.
def --env --wrapped __zoxide_z [...rest: string] {
let path = match $rest {
@@ -51,12 +65,13 @@ def --env --wrapped __zoxide_z [...rest: string] {
zoxide query --exclude $env.PWD -- ...$rest | str trim -r -c "\n"
}
}
cd $path
cd (_pj_normalize_windows_drive $path)
}
# Jump to a directory using interactive search.
def --env --wrapped __zoxide_zi [...rest:string] {
cd $'(zoxide query --interactive -- ...$rest | str trim -r -c "\n")'
let path = (zoxide query --interactive -- ...$rest | str trim -r -c "\n")
cd (_pj_normalize_windows_drive $path)
}
# Jump to git repository root when currently inside a repository.
@@ -72,7 +87,7 @@ def --env __zoxide_zg [] {
let root = ($top.stdout | str trim -r -c "\n")
if ($root != "") {
cd $root
cd (_pj_normalize_windows_drive $root)
}
}
+4
View File
@@ -6,9 +6,13 @@ $env.NU_POLYJUICE_PATH = $env.FILE_PWD
# modules
use modules/pj_aliases.nu *
use modules/pj_python.nu *
use modules/pj_brew.nu *
use modules/pj_uv.nu *
use modules/pj_completions.nu *
use modules/pj_helix.nu *
use modules/pj_fzf.nu *
use modules/pj_zoxide.nu *
use modules/pj_historyhintcomplete.nu *
use modules/pj_deepseek.nu *
use modules/pj_prompt.nu *