mirror of
https://github.com/junegunn/fzf
synced 2026-06-09 10:03:17 +00:00
shell: nushell integration scripts (#4630)
Co-authored-by: imsys <911254+imsys@users.noreply.github.com> Co-authored-by: Grzegorz Zalewski (Greg) <12560152+zalewskigrzegorz@users.noreply.github.com> Co-authored-by: René Jochum <rene@jochum.dev> Co-authored-by: Junegunn Choi <junegunn.c@gmail.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
ccedd064ca
commit
290b18d9fe
@@ -0,0 +1,91 @@
|
||||
# ____ ____
|
||||
# / __/___ / __/
|
||||
# / /_/_ / / /_
|
||||
# / __/ / /_/ __/
|
||||
# /_/ /___/_/ completion-examples.nu
|
||||
#
|
||||
# Example custom completers for fzf's Nushell integration.
|
||||
#
|
||||
# To use these, add the desired entries to $env.FZF_COMPLETERS in your
|
||||
# config.nu. Each closure receives two arguments:
|
||||
# - prefix: the text before the trigger (e.g. "vim" in "vim **<TAB>")
|
||||
# - spans: the full command as a list of words (e.g. ["pacman", "-S", "vim**"])
|
||||
#
|
||||
# A closure can return either:
|
||||
# - a list of candidate strings (fzf will use default options), or
|
||||
# - a record with the following optional fields:
|
||||
# candidates: list<string> # candidates to feed to fzf
|
||||
# opts: list<string> # custom fzf options (default: ["-m"])
|
||||
# post: closure (|sel| ...) # post-processing of the selected item
|
||||
#
|
||||
# Simple example:
|
||||
# $env.FZF_COMPLETERS = {
|
||||
# git: {|prefix, spans| ["branch-main", "branch-dev", "branch-feature"]}
|
||||
# }
|
||||
|
||||
# --- pacman / paru ---
|
||||
# Completes package names for pacman and paru.
|
||||
# Uses the spans to distinguish between subcommands:
|
||||
# -S (sync), -F (files): list available packages from repos
|
||||
# -Q (query), -R (remove): list installed packages
|
||||
# Returns a record with custom fzf options for package preview.
|
||||
|
||||
$env.FZF_COMPLETERS = {}
|
||||
|
||||
$env.FZF_COMPLETERS.pacman = {|prefix, spans|
|
||||
let sub = $spans | skip 1 | first
|
||||
let candidates = (if ($sub =~ "-[SF]") {
|
||||
^pacman -Slq | lines
|
||||
} else if ($sub =~ "-[QR]") {
|
||||
^pacman -Qq | lines
|
||||
} else {
|
||||
[]
|
||||
})
|
||||
{
|
||||
candidates: $candidates
|
||||
opts: ["-m", "--preview", "pacman -Si {}", "--prompt", "Package > "]
|
||||
}
|
||||
}
|
||||
|
||||
$env.FZF_COMPLETERS.paru = {|prefix, spans|
|
||||
let sub = $spans | skip 1 | first
|
||||
let candidates = (if ($sub =~ "-[SF]") {
|
||||
^pacman -Slq | lines
|
||||
} else if ($sub =~ "-[QR]") {
|
||||
^pacman -Qq | lines
|
||||
} else {
|
||||
[]
|
||||
})
|
||||
{
|
||||
candidates: $candidates
|
||||
opts: ["-m", "--preview", "pacman -Si {}", "--prompt", "Package > "]
|
||||
}
|
||||
}
|
||||
|
||||
# --- pass (password-store) ---
|
||||
# Completes entry names from ~/.password-store.
|
||||
# Returns a simple list (no custom fzf options needed).
|
||||
|
||||
$env.FZF_COMPLETERS.pass = {|prefix, spans|
|
||||
try {
|
||||
ls ~/.password-store/**/*.gpg
|
||||
| get name
|
||||
| each {$in | str replace -r '^.*?\.password-store/(.*).gpg' '${1}'}
|
||||
} catch {
|
||||
[]
|
||||
}
|
||||
}
|
||||
|
||||
# --- Example with post-processing hook ---
|
||||
# The "post" closure transforms the selected line after fzf returns.
|
||||
# This is useful when the displayed line contains more information than
|
||||
# what you want inserted on the command line (e.g. extracting a PID from
|
||||
# a full "ps" output line).
|
||||
#
|
||||
# $env.FZF_COMPLETERS.mycommand = {|prefix, spans|
|
||||
# {
|
||||
# candidates: (^some-command | lines)
|
||||
# opts: ["+m", "--header-lines=1"]
|
||||
# post: {|selection| $selection | split row ' ' | get 0}
|
||||
# }
|
||||
# }
|
||||
@@ -0,0 +1,489 @@
|
||||
# ____ ____
|
||||
# / __/___ / __/
|
||||
# / /_/_ / / /_
|
||||
# / __/ / /_/ __/
|
||||
# /_/ /___/_/ completion.nu
|
||||
|
||||
|
||||
# An implementation of completion.nu
|
||||
# This loads FZF as a Nushell External Completer
|
||||
# https://www.nushell.sh/cookbook/external_completers.html
|
||||
|
||||
|
||||
# --- Default Environment Variables ---
|
||||
# These can be overridden in your config.nu or environment.
|
||||
# Example: $env.FZF_COMPLETION_TRIGGER = "!<TAB>"
|
||||
|
||||
# - $env.FZF_TMUX (default: 0)
|
||||
# - $env.FZF_TMUX_OPTS (default: empty)
|
||||
# - $env.FZF_TMUX_HEIGHT (default: 40%)
|
||||
# - $env.FZF_COMPLETION_TRIGGER (default: '**')
|
||||
# - $env.FZF_COMPLETION_OPTS (default: empty)
|
||||
# - $env.FZF_COMPLETION_PATH_OPTS (default: empty)
|
||||
# - $env.FZF_COMPLETION_DIR_OPTS (default: empty)
|
||||
|
||||
|
||||
|
||||
$env.FZF_COMPLETION_TRIGGER = $env.FZF_COMPLETION_TRIGGER? | default '**'
|
||||
|
||||
# Options for fzf completion in general. e.g. '--border'
|
||||
$env.FZF_COMPLETION_OPTS = $env.FZF_COMPLETION_OPTS? | default ''
|
||||
|
||||
# Options specific to path completion. e.g. '--extended'
|
||||
$env.FZF_COMPLETION_PATH_OPTS = $env.FZF_COMPLETION_PATH_OPTS? | default ''
|
||||
# Options specific to directory completion. e.g. '--extended'
|
||||
$env.FZF_COMPLETION_DIR_OPTS = $env.FZF_COMPLETION_DIR_OPTS? | default ''
|
||||
|
||||
$env.FZF_COMPLETION_DIR_COMMANDS = $env.FZF_COMPLETION_DIR_COMMANDS? | default ['cd', 'pushd', 'rmdir']
|
||||
|
||||
# --- Helper Functions ---
|
||||
|
||||
# Helper to build default fzf options list
|
||||
def __fzf_defaults [prepend: string, append: string]: nothing -> string {
|
||||
let base = $"--height ($env.FZF_TMUX_HEIGHT? | default '40%') --min-height 20+ --bind=ctrl-z:ignore ($prepend)"
|
||||
let opts_file = if ($env.FZF_DEFAULT_OPTS_FILE? | default '' | is-not-empty) {
|
||||
try { open --raw ($env.FZF_DEFAULT_OPTS_FILE) | str trim } catch { '' }
|
||||
} else {
|
||||
''
|
||||
}
|
||||
let default_opts = $env.FZF_DEFAULT_OPTS? | default ''
|
||||
$"($base) ($opts_file) ($default_opts) ($append)" | str trim
|
||||
}
|
||||
|
||||
# Wrapper for running fzf or fzf-tmux
|
||||
def __fzf_comprun [ context_name: string # e.g., "fzf-completion" , "fzf-helper" - mainly for potential debugging
|
||||
, query: string # The initial query string for fzf
|
||||
, fzf_opts_arg: list<string> # Remaining options for fzf/fzf-tmux
|
||||
] {
|
||||
let stdin_content = try {
|
||||
# Collect stdin into a single string. Adjust if structured data is expected.
|
||||
$in | into string
|
||||
} catch {
|
||||
null # Set to null if there's no stdin or an error occurs reading it
|
||||
}
|
||||
|
||||
let fzf_default_opts = (__fzf_defaults "" ($env.FZF_COMPLETION_OPTS | default ''))
|
||||
let fzf_prefinal_opt = ['--query', $query, '--reverse'] | append $fzf_opts_arg
|
||||
|
||||
# Get the configured height, defaulting to '40%'
|
||||
let height_opt = $env.FZF_TMUX_HEIGHT? | default '40%'
|
||||
|
||||
# Determine if fzf should generate its own candidates via walker
|
||||
let has_walker = ($fzf_prefinal_opt | find '--walker' | is-not-empty)
|
||||
|
||||
# Check for custom comprun function (Nu equivalent)
|
||||
if (which _fzf_comprun | is-not-empty) {
|
||||
# Note: Nushell doesn't have a direct equivalent to Zsh/Bash `type -t _fzf_comprun`.
|
||||
# This check assumes a user might define a custom command named `_fzf_comprun`.
|
||||
_fzf_comprun $context_name $query ...$fzf_prefinal_opt # Pass args correctly to custom function
|
||||
} else if ($env.TMUX_PANE? | default '' | into string | is-not-empty) and (($env.FZF_TMUX? | default 0) != 0 or ($env.FZF_TMUX_OPTS? | is-not-empty)) {
|
||||
# Running inside tmux, use fzf-tmux
|
||||
let final_fzf_opts = if ($env.FZF_TMUX_OPTS? | is-not-empty) {
|
||||
$env.FZF_TMUX_OPTS | split row ' ' | append ['--'] | append $fzf_prefinal_opt
|
||||
} else {
|
||||
# Use the default -d option with the configured height for fzf-tmux
|
||||
['-d', $height_opt, '--'] | append $fzf_prefinal_opt
|
||||
}
|
||||
|
||||
if $has_walker or ($stdin_content == null) {
|
||||
with-env { FZF_DEFAULT_OPTS: $fzf_default_opts, FZF_DEFAULT_OPTS_FILE: '' } { fzf-tmux ...$final_fzf_opts }
|
||||
} else {
|
||||
$stdin_content | with-env { FZF_DEFAULT_OPTS: $fzf_default_opts, FZF_DEFAULT_OPTS_FILE: '' } { fzf-tmux ...$final_fzf_opts }
|
||||
}
|
||||
|
||||
} else {
|
||||
# Not in tmux or not configured for fzf-tmux, use fzf directly
|
||||
let final_fzf_opts = $fzf_prefinal_opt
|
||||
|
||||
if $has_walker or ($stdin_content == null) {
|
||||
with-env { FZF_DEFAULT_OPTS: $fzf_default_opts, FZF_DEFAULT_OPTS_FILE: '' } { fzf ...$final_fzf_opts }
|
||||
} else {
|
||||
$stdin_content | with-env { FZF_DEFAULT_OPTS: $fzf_default_opts, FZF_DEFAULT_OPTS_FILE: '' } { fzf ...$final_fzf_opts }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Generate host list for ssh/telnet
|
||||
def __fzf_list_hosts [] {
|
||||
# Translate the Zsh pipeline using Nu commands and external tools
|
||||
let ssh_configs = try { open ~/.ssh/config | lines } catch { [] }
|
||||
let ssh_configs_d = try { open ~/.ssh/config.d/* | lines } catch { [] }
|
||||
let ssh_config_global = try { open /etc/ssh/ssh_config | lines } catch { [] }
|
||||
let known_hosts = try { open ~/.ssh/known_hosts | lines } catch { [] }
|
||||
let hosts_file = try { open /etc/hosts | lines } catch { [] }
|
||||
|
||||
[
|
||||
(
|
||||
# Process ssh config files
|
||||
$ssh_configs | append $ssh_configs_d | append $ssh_config_global
|
||||
| where {|it| ($it | str downcase | str starts-with 'host') or ($it | str downcase | str starts-with 'hostname') }
|
||||
| parse --regex '^\s*host(?:name)?\s+(?<hosts>.+)' # Extract hosts after keyword
|
||||
| default { hosts: null } # Handle lines that don't match regex
|
||||
| get hosts
|
||||
| where {|it| $it != null }
|
||||
| split row ' '
|
||||
| where {|it| not ($it =~ '[*?%]') } # Exclude patterns containing *, ?, or %
|
||||
)
|
||||
(
|
||||
# Process known_hosts file
|
||||
$known_hosts | parse --regex '^(?:\[)?(?<hosts>[a-z0-9.,:_-]+)' # Extract hostnames (possibly in [], possibly comma-separated) - added underscore
|
||||
| default { hosts: null }
|
||||
| get hosts
|
||||
| where {|it| $it != null }
|
||||
| each { |it| $it | split row ',' } # Split comma-separated hosts if any
|
||||
| flatten
|
||||
)
|
||||
(
|
||||
# Process /etc/hosts file
|
||||
$hosts_file | where { |it| not ($it | str starts-with '#') } # Ignore comments
|
||||
| where { |it| not ($it | str trim | is-empty) } # Ignore empty lines
|
||||
| where { |it| not ($it | str contains '0.0.0.0') } # Ignore 0.0.0.0
|
||||
| str replace --regex '#.*$' '' # Remove trailing comments
|
||||
| parse --regex '^\s*\S+\s+(?<hosts>.+)' # Extract hosts part (after IP)
|
||||
| default { hosts: null }
|
||||
| get hosts
|
||||
| where {|it| $it != null }
|
||||
| split row ' ' # Split multiple hosts on the same line
|
||||
)
|
||||
]
|
||||
| flatten # Combine all lists into a single stream
|
||||
| where {|it| not ($it | is-empty) } # Remove empty entries
|
||||
| sort | uniq # Sort and remove duplicates
|
||||
}
|
||||
|
||||
|
||||
# Base function for path/directory completion
|
||||
def __fzf_generic_path_completion [ prefix: string # The text before the trigger
|
||||
, fzf_opts_arg: list<string> # Extra options for fzf
|
||||
, suffix: string # Suffix to add to selection (e.g. , "/")
|
||||
] {
|
||||
# --- Determine walker root and initial query from the prefix ---
|
||||
|
||||
mut walker_root = "."
|
||||
mut initial_query = ""
|
||||
|
||||
if ($prefix | is-empty) {
|
||||
# Case: "**"
|
||||
$walker_root = "."
|
||||
$initial_query = ""
|
||||
} else if ($prefix | str contains (char separator)) {
|
||||
# Case: "dir/subdir/partial**" or "dir/**"
|
||||
$walker_root = $prefix | path dirname
|
||||
$initial_query = $prefix | path basename
|
||||
# Handle edge case where prefix ends with separator, e.g., "dir/"
|
||||
if ($prefix | str ends-with (char separator)) {
|
||||
# Remove trailing separator to get the intended directory
|
||||
$walker_root = $prefix | str substring 0..-2
|
||||
$initial_query = ""
|
||||
}
|
||||
# Ensure walker_root isn't empty if prefix was like "/file**"
|
||||
# or if path dirname returned empty string for some reason (e.g. prefix="file/")
|
||||
if ($walker_root | is-empty) {
|
||||
if ($prefix | str starts-with (char separator)) {
|
||||
$walker_root = (char separator)
|
||||
} else if ($prefix | str ends-with (char separator)) {
|
||||
$walker_root = $prefix | str substring 0..-2
|
||||
} else { $walker_root = "." } # Fallback if dirname weirdly fails
|
||||
}
|
||||
} else {
|
||||
# Case: "partial**" (no slashes)
|
||||
$walker_root = "."
|
||||
$initial_query = $prefix
|
||||
}
|
||||
|
||||
# --- Prepare FZF options ---
|
||||
let completion_type_opts = if $suffix == '/' {
|
||||
$env.FZF_COMPLETION_DIR_OPTS? | default '' | split row ' ' | where {not ($in | is-empty)}
|
||||
} else {
|
||||
$env.FZF_COMPLETION_PATH_OPTS? | default '' | split row ' ' | where {not ($in | is-empty)}
|
||||
}
|
||||
|
||||
let walker_type = if ($suffix == '/') {
|
||||
"dir,follow"
|
||||
} else {
|
||||
"file,dir,follow,hidden"
|
||||
}
|
||||
# Expand tilde so fzf receives a valid absolute path as walker-root
|
||||
let needs_tilde_rewrite = ($walker_root | str starts-with '~')
|
||||
let walker_root_expanded = ($walker_root | path expand)
|
||||
|
||||
# Use the 'walker_root' calculated at the beginning
|
||||
let fzf_all_opts = ["--scheme=path", "--walker", $walker_type, "--walker-root", $walker_root_expanded] | append $fzf_opts_arg
|
||||
| append $completion_type_opts
|
||||
|
||||
# Call FZF run
|
||||
let fzf_selection = ( __fzf_comprun "fzf-path-completion-walker" $initial_query $fzf_all_opts ) | str trim
|
||||
|
||||
|
||||
# --- Return Result ---
|
||||
if ($fzf_selection | is-not-empty) {
|
||||
# Restore tilde prefix if the user originally typed ~/
|
||||
let home = $nu.home-dir | path expand
|
||||
let result = if $needs_tilde_rewrite {
|
||||
$fzf_selection | lines | each {|line| $line | str replace $home '~' } | str join ' '
|
||||
} else {
|
||||
$fzf_selection | lines | str join ' '
|
||||
}
|
||||
[$result]
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
}
|
||||
|
||||
# Specific path completion wrapper
|
||||
def _fzf_path_completion [prefix: string] {
|
||||
# Zsh args: base, lbuf, _fzf_compgen_path, "-m", "", " "
|
||||
# Nu: prefix, empty command name (use find), ["-m"], "", " "
|
||||
__fzf_generic_path_completion $prefix ["-m"] ""
|
||||
}
|
||||
|
||||
# General completion helper for commands that feed a list to fzf
|
||||
# This is called by ssh, kill, and user-defined completers.
|
||||
def _fzf_complete [ query: string # The initial query string for fzf
|
||||
, data_gen_closure: closure # Closure that generates candidates
|
||||
, fzf_opts_arg: list<string> # Extra options for fzf (like -m, +m)
|
||||
, --post_process_closure: closure # Closure to process the selected item (optional)
|
||||
] {
|
||||
# Generate candidates using the provided command
|
||||
let candidates = try {
|
||||
do $data_gen_closure
|
||||
} catch {
|
||||
# Capture the actual error object provided by the catch block
|
||||
let actual_error = $in
|
||||
# Print a more informative error message including the actual error details
|
||||
print -e $"Error executing data_gen closure. Closure code: ($data_gen_closure). Actual error: ($actual_error)"
|
||||
[]
|
||||
}
|
||||
|
||||
# Run fzf and get selection
|
||||
let fzf_selection = $candidates | to text
|
||||
| __fzf_comprun "fzf-helper" $query $fzf_opts_arg
|
||||
| str trim # Trim potential trailing newline from fzf
|
||||
|
||||
# Apply post-processing if closure provided and selection is not empty
|
||||
let processed_selection = if ($fzf_selection | is-not-empty) and ($post_process_closure | is-not-empty) {
|
||||
# Call the post-processing closure with the selection
|
||||
try {
|
||||
do $post_process_closure $fzf_selection
|
||||
} catch {
|
||||
print -e $"Error executing post_process closure: ($post_process_closure)"
|
||||
$fzf_selection # Return original selection on error
|
||||
}
|
||||
} else {
|
||||
$fzf_selection
|
||||
}
|
||||
|
||||
if not ($processed_selection | is-empty) {
|
||||
[($processed_selection | lines | str join ' ')]
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
}
|
||||
|
||||
# SSH/Telnet completion
|
||||
def _fzf_complete_ssh [ prefix: string
|
||||
, input_line_before_trigger: string
|
||||
] {
|
||||
let words = ($input_line_before_trigger | split row ' ')
|
||||
let word_count = $words | length
|
||||
|
||||
# Find the index of the word being completed (which is the prefix)
|
||||
# If prefix is empty, completion happens after a space, index is word_count
|
||||
# If prefix is not empty, it's the last word, index is word_count - 1
|
||||
let completion_index = if ($prefix | is-empty) { $word_count } else { $word_count - 1 }
|
||||
|
||||
mut handled = false
|
||||
mut completion_result = [] # List of completion strings to return
|
||||
|
||||
# Check for -i, -F, -E flags immediately preceding the cursor position
|
||||
if $completion_index > 0 {
|
||||
let prev_arg = ($words | get ($completion_index - 1))
|
||||
if ($prev_arg in ['-i', '-F', '-E']) {
|
||||
$handled = true
|
||||
# Call path completion with the current prefix
|
||||
$completion_result = (_fzf_path_completion $prefix)
|
||||
}
|
||||
}
|
||||
|
||||
# If not handled by path completion, do host completion
|
||||
if not $handled {
|
||||
let user_part = if ($prefix | str contains "@") { ($prefix | split row "@" | first) + "@" } else { "" }
|
||||
# The part after '@' (or the whole prefix if no '@') is the initial query for fzf
|
||||
let query = if ($prefix | str contains "@") { $prefix | split row "@" | last } else { $prefix }
|
||||
|
||||
let host_candidates_gen = {||
|
||||
__fzf_list_hosts
|
||||
| each {|host_item| $user_part + $host_item } # Prepend user@ if present in prefix
|
||||
}
|
||||
|
||||
# Zsh options: +m -- ; Nu: pass ["+m"]
|
||||
# Pass the host part of the prefix to _fzf_complete for the initial query
|
||||
let selected_host = (_fzf_complete $query $host_candidates_gen ["+m"]) # Pass host_prefix here
|
||||
if not ($selected_host | is-empty) {
|
||||
$completion_result = $selected_host # _fzf_complete returns a list
|
||||
}
|
||||
}
|
||||
|
||||
$completion_result
|
||||
}
|
||||
|
||||
# Kill completion post-processor (extracts PID)
|
||||
def _fzf_complete_kill_post_get_pid [selected_line: string] {
|
||||
# Assuming standard ps output where PID is the second column
|
||||
$selected_line | lines | each { $in | from ssv --noheaders | get 0.column1 } | to text
|
||||
}
|
||||
|
||||
# Kill completion to get process PID
|
||||
def _fzf_complete_kill [query: string] {
|
||||
let ps_gen_closure = {|| # Define ps generator as a closure
|
||||
# Try standard ps, then busybox, then cygwin format approximation
|
||||
# Use `^ps` to ensure external command execution
|
||||
try {
|
||||
^ps -eo user,pid,ppid,start,time,command | complete | if $in.exit_code == 0 { $in.stdout | lines } else { error make {msg: "ps failed"} }
|
||||
} catch {
|
||||
try {
|
||||
^ps -eo user,pid,ppid,time,args | complete | if $in.exit_code == 0 { $in.stdout | lines } else { error make {msg: "ps failed"} }
|
||||
} catch {
|
||||
try {
|
||||
^ps --everyone --full --windows | complete | if $in.exit_code == 0 { $in.stdout | lines } else { error make {msg: "ps failed"} }
|
||||
} catch {
|
||||
print -e "Error: ps command failed."
|
||||
[] # Return empty list on failure
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Note: Complex Zsh FZF bindings for kill (click-header transformer) are omitted for simplicity.
|
||||
# Users can set custom bindings via FZF_DEFAULT_OPTS if needed.
|
||||
let kill_post_closure = {|selected_line| _fzf_complete_kill_post_get_pid $selected_line }
|
||||
|
||||
let fzf_opts = ["-m", "--header-lines=1", "--no-preview", "--wrap", "--color", "fg:dim,nth:regular"]
|
||||
|
||||
_fzf_complete $query $ps_gen_closure $fzf_opts --post_process_closure $kill_post_closure
|
||||
}
|
||||
|
||||
|
||||
# --- Main FZF External Completer ---
|
||||
|
||||
# This function is registered with Nushell's external completion system.
|
||||
# It gets called when Tab is pressed.
|
||||
let fzf_external_completer = {|spans|
|
||||
let trigger: string = $env.FZF_COMPLETION_TRIGGER? | default '**'
|
||||
|
||||
if ($trigger | is-empty) { return null } # Cannot work with empty trigger
|
||||
if (($spans | length ) == 0) { return null } # Nothing to complete
|
||||
|
||||
let last_span = $spans | last
|
||||
|
||||
if ($last_span | str ends-with $trigger) {
|
||||
# --- Trigger Found ---
|
||||
|
||||
# Skip sudo to determine the actual command
|
||||
let cmd_spans = if ($spans | first) == "sudo" { $spans | skip 1 } else { $spans }
|
||||
let cmd_word = ($cmd_spans | first | default "")
|
||||
|
||||
# Calculate the prefix (part before the trigger in the last span)
|
||||
let prefix = $last_span | str substring 0..(-1 * ($trigger | str length) - 1)
|
||||
|
||||
# Reconstruct the line content *before* the trigger for context
|
||||
# This is an approximation based on spans
|
||||
let line_without_trigger = $cmd_spans | take (($cmd_spans | length) - 1) | append $prefix | str join ' '
|
||||
|
||||
# --- Dispatch to Completer ---
|
||||
mut completion_results = [] # Will hold the list of strings from the completer
|
||||
|
||||
# Check for user-defined completer in $env.FZF_COMPLETERS first.
|
||||
# Users can define custom completers in their config.nu as a record of closures:
|
||||
# $env.FZF_COMPLETERS = { git: {|prefix, spans| ... }, docker: {|prefix, spans| ... } }
|
||||
# Each closure receives the prefix (text before the trigger) and the full
|
||||
# command spans (e.g. ["pacman", "-S", "vim**"]), and should return either:
|
||||
# - a list of candidate strings, or
|
||||
# - a record { candidates: [...], opts: [...], post: {|sel| ...} } to pass
|
||||
# custom fzf options and/or a post-processing closure.
|
||||
# See shell/completion-examples.nu for examples.
|
||||
let user_completers = ($env.FZF_COMPLETERS? | default {})
|
||||
if ($cmd_word in $user_completers) {
|
||||
let user_gen = ($user_completers | get $cmd_word)
|
||||
let user_result = (do $user_gen $prefix $cmd_spans)
|
||||
if ($user_result | describe | str starts-with 'record') {
|
||||
let candidates = ($user_result | get candidates)
|
||||
let fzf_opts = ($user_result | get opts? | default ["-m"])
|
||||
let post = ($user_result | get post? | default null)
|
||||
if ($post != null) {
|
||||
$completion_results = (_fzf_complete $prefix {|| $candidates} $fzf_opts --post_process_closure $post)
|
||||
} else {
|
||||
$completion_results = (_fzf_complete $prefix {|| $candidates} $fzf_opts)
|
||||
}
|
||||
} else {
|
||||
$completion_results = (_fzf_complete $prefix {|| $user_result} ["-m"])
|
||||
}
|
||||
} else {
|
||||
match $cmd_word {
|
||||
"ssh" | "scp" | "sftp" | "telnet" => { $completion_results = (_fzf_complete_ssh $prefix $line_without_trigger) }
|
||||
"kill" => { $completion_results = (_fzf_complete_kill $prefix) }
|
||||
_ if ($cmd_word in $env.FZF_COMPLETION_DIR_COMMANDS) => {
|
||||
$completion_results = (__fzf_generic_path_completion $prefix [] "/")
|
||||
}
|
||||
_ => {
|
||||
# Default to path completion if no specific command matches
|
||||
$completion_results = (_fzf_path_completion $prefix)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Return Results ---
|
||||
# The _fzf_... functions return a list of completion strings.
|
||||
# Nushell's completer expects the suggestions for the token being completed (prefix + trigger).
|
||||
# The results from the helper functions should be the final desired strings.
|
||||
# We don't need to manually add spaces; Nushell handles that.
|
||||
$completion_results # Return the list directly
|
||||
} else {
|
||||
# --- Trigger Not Found ---
|
||||
# Return null to let Nushell fall back to other completers (e.g., default file completion).
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
# --- WRAPPER AND REGISTRATION ---
|
||||
|
||||
# Guard against re-sourcing: wrapping the completer multiple times would
|
||||
# nest wrappers and grow the call chain on every reload.
|
||||
if ($env.__fzf_completer_registered? | default false) != true {
|
||||
|
||||
# Get the currently configured external completer, if any exists
|
||||
let previous_external_completer = $env.config? | get completions? | get external? | get completer?
|
||||
|
||||
# Define the new wrapper completer
|
||||
let fzf_wrapper_completer = {|spans|
|
||||
# 1. Try the FZF completer logic first
|
||||
let fzf_result = do $fzf_external_completer $spans
|
||||
|
||||
# 2. If FZF returned a result (a list, even an empty one), return it.
|
||||
# `null` means FZF didn't handle it because the trigger wasn't present.
|
||||
if $fzf_result != null {
|
||||
$fzf_result
|
||||
} else {
|
||||
# 3. FZF didn't handle it, so call the previous completer (if it exists).
|
||||
if $previous_external_completer != null {
|
||||
do $previous_external_completer $spans
|
||||
} else {
|
||||
# 4. No previous completer, and FZF didn't handle it. Return null.
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Register the new wrapper completer
|
||||
# This ensures external completions are enabled and sets our wrapper.
|
||||
$env.config = $env.config | upsert completions {
|
||||
external: {
|
||||
enable: true
|
||||
completer: $fzf_wrapper_completer
|
||||
}
|
||||
}
|
||||
|
||||
$env.__fzf_completer_registered = true
|
||||
}
|
||||
|
||||
# vim: set sts=2 ts=2 sw=2 tw=120 et :
|
||||
@@ -0,0 +1,166 @@
|
||||
# ____ ____
|
||||
# / __/___ / __/
|
||||
# / /_/_ / / /_
|
||||
# / __/ / /_/ __/
|
||||
# /_/ /___/_/ key-bindings.nu
|
||||
#
|
||||
# - $FZF_TMUX (default: 0)
|
||||
# - $FZF_TMUX_OPTS
|
||||
# - $FZF_TMUX_HEIGHT (default: 40%)
|
||||
# - $FZF_CTRL_T_COMMAND (set to "" to disable)
|
||||
# - $FZF_CTRL_T_OPTS
|
||||
# - $FZF_CTRL_R_COMMAND (set to "" to disable)
|
||||
# - $FZF_CTRL_R_OPTS
|
||||
# - $FZF_ALT_C_COMMAND (set to "" to disable)
|
||||
# - $FZF_ALT_C_OPTS
|
||||
|
||||
# Code provided by @igor-ramazanov
|
||||
# Source: https://github.com/junegunn/fzf/issues/4122#issuecomment-2607368316
|
||||
|
||||
# Merge default options in the same order as bash/zsh:
|
||||
# 1. --height, --min-height, --bind=ctrl-z:ignore, $prepend
|
||||
# 2. $FZF_DEFAULT_OPTS_FILE contents
|
||||
# 3. $FZF_DEFAULT_OPTS, $append
|
||||
def __fzf_defaults [prepend: string, append: string]: nothing -> string {
|
||||
let base = $"--height ($env.FZF_TMUX_HEIGHT? | default '40%') --min-height 20+ --bind=ctrl-z:ignore ($prepend)"
|
||||
let opts_file = if ($env.FZF_DEFAULT_OPTS_FILE? | default '' | is-not-empty) {
|
||||
try { open --raw ($env.FZF_DEFAULT_OPTS_FILE) | str trim } catch { '' }
|
||||
} else {
|
||||
''
|
||||
}
|
||||
let default_opts = $env.FZF_DEFAULT_OPTS? | default ''
|
||||
$"($base) ($opts_file) ($default_opts) ($append)" | str trim
|
||||
}
|
||||
|
||||
# Return the fzf command to use: fzf-tmux when inside tmux and
|
||||
# FZF_TMUX is enabled or FZF_TMUX_OPTS is set, plain fzf otherwise.
|
||||
def __fzfcmd []: nothing -> list<string> {
|
||||
let in_tmux = ($env.TMUX_PANE? | default '' | into string | is-not-empty)
|
||||
if $in_tmux {
|
||||
let fzf_tmux = ($env.FZF_TMUX? | default 0 | into string)
|
||||
let fzf_tmux_opts = ($env.FZF_TMUX_OPTS? | default '' | into string)
|
||||
if ($fzf_tmux != '0') or ($fzf_tmux_opts | is-not-empty) {
|
||||
let opts = if ($fzf_tmux_opts | is-not-empty) { $fzf_tmux_opts } else { $"-d($env.FZF_TMUX_HEIGHT? | default '40%')" }
|
||||
return ['fzf-tmux' ...(($opts | split row ' ' | where { $in != '' })) '--']
|
||||
}
|
||||
}
|
||||
['fzf']
|
||||
}
|
||||
|
||||
|
||||
export-env {
|
||||
$env.FZF_CTRL_T_OPTS = $env.FZF_CTRL_T_OPTS? | default ""
|
||||
$env.FZF_CTRL_R_OPTS = $env.FZF_CTRL_R_OPTS? | default ""
|
||||
$env.FZF_ALT_C_OPTS = $env.FZF_ALT_C_OPTS? | default ""
|
||||
}
|
||||
|
||||
# Directories
|
||||
const alt_c = {
|
||||
name: fzf_dirs
|
||||
modifier: alt
|
||||
keycode: char_c
|
||||
mode: [emacs, vi_normal, vi_insert]
|
||||
event: [
|
||||
{
|
||||
send: executehostcommand
|
||||
cmd: "
|
||||
let fzf_opts = (__fzf_defaults '--reverse --walker=dir,follow,hidden --scheme=path' $'($env.FZF_ALT_C_OPTS) +m');
|
||||
let fzfcmd = (__fzfcmd);
|
||||
let fzf_args = ($fzfcmd | skip 1);
|
||||
let alt_c_cmd = ($env.FZF_ALT_C_COMMAND? | default null);
|
||||
let result = if ($alt_c_cmd == null) or ($alt_c_cmd | is-empty) {
|
||||
with-env { FZF_DEFAULT_OPTS: $fzf_opts, FZF_DEFAULT_OPTS_FILE: '' } { ^($fzfcmd | first) ...$fzf_args }
|
||||
} else {
|
||||
let fzf_cmd_str = ($fzfcmd | str join ' ');
|
||||
let sh_cmd = [$alt_c_cmd '|' $fzf_cmd_str] | str join ' ';
|
||||
with-env { FZF_DEFAULT_OPTS: $fzf_opts, FZF_DEFAULT_OPTS_FILE: '' } { ^sh -c $sh_cmd }
|
||||
};
|
||||
if ($result | is-not-empty) { cd $result };
|
||||
"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# History
|
||||
const ctrl_r = {
|
||||
name: fzf_history
|
||||
modifier: control
|
||||
keycode: char_r
|
||||
mode: [emacs, vi_insert, vi_normal]
|
||||
event: [
|
||||
{
|
||||
send: executehostcommand
|
||||
cmd: "commandline edit --replace (
|
||||
let fzf_opts = (__fzf_defaults '' $'--scheme=history --bind=ctrl-r:toggle-sort --wrap-sign \"\t↳ \" --highlight-line ($env.FZF_CTRL_R_OPTS) +m --read0');
|
||||
let fzfcmd = (__fzfcmd);
|
||||
let fzf_args = ($fzfcmd | skip 1);
|
||||
# reverse | uniq: show most recent first, deduplicate keeping the latest.
|
||||
# Nushell's `history` loads the full history as an in-memory table
|
||||
# (bounded by $env.config.history.max_size, default 100,000), so
|
||||
# reverse and uniq run on an already-materialized list. This is O(n)
|
||||
# but acceptable for typical history sizes; unlike bash/zsh `fc -r`,
|
||||
# there is no streaming primitive that would let fzf show the latest
|
||||
# entries before the full list is consumed.
|
||||
history
|
||||
| get command
|
||||
| reverse
|
||||
| uniq
|
||||
| str join (char -i 0)
|
||||
| with-env { FZF_DEFAULT_OPTS: $fzf_opts, FZF_DEFAULT_OPTS_FILE: '' } { ^($fzfcmd | first) ...$fzf_args --query (commandline) }
|
||||
| decode utf-8
|
||||
| str trim
|
||||
)"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# Files
|
||||
const ctrl_t = {
|
||||
name: fzf_files
|
||||
modifier: control
|
||||
keycode: char_t
|
||||
mode: [emacs, vi_normal, vi_insert]
|
||||
event: [
|
||||
{
|
||||
send: executehostcommand
|
||||
cmd: "
|
||||
let fzf_opts = (__fzf_defaults '--reverse --walker=file,dir,follow,hidden --scheme=path' $'($env.FZF_CTRL_T_OPTS) -m');
|
||||
let fzfcmd = (__fzfcmd);
|
||||
let fzf_args = ($fzfcmd | skip 1);
|
||||
let ctrl_t_cmd = ($env.FZF_CTRL_T_COMMAND? | default null);
|
||||
let result = if ($ctrl_t_cmd == null) or ($ctrl_t_cmd | is-empty) {
|
||||
with-env { FZF_DEFAULT_OPTS: $fzf_opts, FZF_DEFAULT_OPTS_FILE: '' } { ^($fzfcmd | first) ...$fzf_args }
|
||||
} else {
|
||||
let fzf_cmd_str = ($fzfcmd | str join ' ');
|
||||
let sh_cmd = [$ctrl_t_cmd '|' $fzf_cmd_str] | str join ' ';
|
||||
with-env { FZF_DEFAULT_OPTS: $fzf_opts, FZF_DEFAULT_OPTS_FILE: '' } { ^sh -c $sh_cmd }
|
||||
};
|
||||
let result = ($result | str replace --all (char newline) ' ' | str trim);
|
||||
commandline edit --append $result;
|
||||
commandline set-cursor --end
|
||||
"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# Helper to check if a binding is enabled. A binding is disabled when
|
||||
# the corresponding *_COMMAND variable is explicitly set to "".
|
||||
# When not defined (null), the binding is enabled (using fzf's built-in walker).
|
||||
def __fzf_binding_enabled [var_name: string]: nothing -> bool {
|
||||
let val = ($env | get -o $var_name)
|
||||
# null = not defined = enabled; "" = explicitly disabled
|
||||
$val == null or ($val | into string | is-not-empty)
|
||||
}
|
||||
|
||||
# Update the $env.config
|
||||
export-env {
|
||||
let fzf_names = ['fzf_files', 'fzf_dirs', 'fzf_history']
|
||||
# Filter out any existing fzf bindings, then re-add the enabled ones.
|
||||
# This allows re-sourcing to update bindings (e.g. after changing
|
||||
# FZF_CTRL_T_COMMAND) without creating duplicates.
|
||||
mut bindings = ($env.config.keybindings | where { |kb| $kb.name not-in $fzf_names })
|
||||
if (__fzf_binding_enabled 'FZF_ALT_C_COMMAND') { $bindings = ($bindings | append $alt_c) }
|
||||
if (__fzf_binding_enabled 'FZF_CTRL_R_COMMAND') { $bindings = ($bindings | append $ctrl_r) }
|
||||
if (__fzf_binding_enabled 'FZF_CTRL_T_COMMAND') { $bindings = ($bindings | append $ctrl_t) }
|
||||
$env.config.keybindings = $bindings
|
||||
}
|
||||
Reference in New Issue
Block a user