Compare commits

...

12 Commits

Author SHA1 Message Date
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
10 changed files with 301 additions and 70 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
+3
View File
@@ -0,0 +1,3 @@
*
!.gitignore
!noop.nu
+1
View File
@@ -0,0 +1 @@
# Fallback completion module intentionally exports nothing.
+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 *
+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: [
{
+46 -63
View File
@@ -142,34 +142,6 @@ 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) {
@@ -576,34 +548,42 @@ def _pj_lang_segments [] {
$out
}
def _pj_shell_module [p: record] {
$"($p.c_shell) nu ($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_time_module [p: record] {
let now = (date now | format date "%R")
$"($p.c_time)($now) ($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"
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)"
{
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 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)
@@ -624,12 +604,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 +619,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 +632,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
+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)
}
}
+2
View File
@@ -6,6 +6,8 @@ $env.NU_POLYJUICE_PATH = $env.FILE_PWD
# modules
use modules/pj_aliases.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 *