@ 261102.1

260523.7 · 290b18d9fe1f63f5ed7ab5639bad034563991146

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>

reading declarations

.github/workflows/linux.yml

@@ -33,7 +33,12 @@ jobs:         ruby-version: 3.4.6      - name: Install packages-      run: sudo apt-get install --yes zsh fish tmux shfmt+      run: |+        sudo install -d -m 0755 /etc/apt/keyrings+        wget -qO- https://apt.fury.io/nushell/gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/fury-nushell.gpg+        echo "deb [signed-by=/etc/apt/keyrings/fury-nushell.gpg] https://apt.fury.io/nushell/ /" | sudo tee /etc/apt/sources.list.d/fury-nushell.list+        sudo apt-get update+        sudo apt-get install --yes zsh fish tmux shfmt nushell      - name: Install Ruby gems       run: bundle install

README.md

@@ -40,7 +40,7 @@ Highlights - **Portable** -- Distributed as a single binary for easy installation - **Fast** -- Optimized to process millions of items instantly - **Versatile** -- Fully customizable through an event-action binding mechanism-- **All-inclusive** -- Comes with integrations for Bash, Zsh, Fish, Vim, and Neovim+- **All-inclusive** -- Comes with integrations for Bash, Zsh, Fish, Nushell, Vim, and Neovim  Table of Contents -----------------@@ -81,6 +81,7 @@ Table of Contents         * [Supported commands (bash)](#supported-commands-bash)         * [Custom fuzzy completion](#custom-fuzzy-completion)     * [Fuzzy completion for fish](#fuzzy-completion-for-fish)+    * [Fuzzy completion for Nushell](#fuzzy-completion-for-nushell) * [Vim plugin](#vim-plugin) * [Advanced topics](#advanced-topics)     * [Customizing for different types of input](#customizing-for-different-types-of-input)@@ -210,10 +211,18 @@ Add the following line to your shell configuration file.   # Set up fzf key bindings   fzf --fish | source   ```+* Nushell -- Nushell does not support piping into `source`, so the install+  script generates a file in the autoload directory. If you didn't use the+  install script, you can manually set it up:+  ```nu+  # Generate the integration script+  mkdir ($nu.default-config-dir | path join "autoload")+  fzf --nushell | save -f ($nu.default-config-dir | path join "autoload" "_fzf_integration.nu")+  ```  > [!NOTE]-> `--bash`, `--zsh`, and `--fish` options are only available in fzf 0.48.0 or-> later. If you have an older version of fzf, or want finer control, you can+> `--bash`, `--zsh`, `--fish`, and `--nushell` options are only available in+> recent versions of fzf. If you have an older version of fzf, or want finer control, you can > source individual script files in the [/shell](/shell) directory. The > location of the files may vary depending on the package manager you use. > Please refer to the package documentation for more information.@@ -227,6 +236,8 @@ Add the following line to your shell configuration file. > * bash: `FZF_CTRL_R_COMMAND= FZF_ALT_C_COMMAND= eval "$(fzf --bash)"` > * zsh: `FZF_CTRL_R_COMMAND= FZF_ALT_C_COMMAND= source <(fzf --zsh)` > * fish: `fzf --fish | FZF_CTRL_R_COMMAND= FZF_ALT_C_COMMAND= source`+> * nushell: add to your `env.nu`:+>   `$env.FZF_CTRL_R_COMMAND = ""; $env.FZF_ALT_C_COMMAND = ""` > > Setting the variables after sourcing the script will have no effect. @@ -506,7 +517,7 @@ Key bindings for command-line -----------------------------  By [setting up shell integration](#setting-up-shell-integration), you can use-the following key bindings in bash, zsh, and fish.+the following key bindings in bash, zsh, fish, and Nushell.  - `CTRL-T` - Paste the selected files and directories onto the command-line     - The list is generated using `--walker file,dir,follow,hidden` option@@ -574,7 +585,7 @@ More tips can be found on [the wiki page](https://github.com/junegunn/fzf/wiki/C Fuzzy completion ---------------- -Shell integration also provides fuzzy completion for bash, zsh, and fish.+Shell integration also provides fuzzy completion for bash, zsh, fish, and Nushell.  ### Files and directories @@ -823,6 +834,37 @@ function _fzf_post_complete_foo end ``` +### Fuzzy completion for Nushell++Fuzzy completion in Nushell works via the+[external completer](https://www.nushell.sh/cookbook/external_completers.html)+mechanism. There are some differences compared to bash and zsh:++- On Nushell >= 0.103.0, the external completer is no longer called for+  built-in commands (e.g. `cd`, `ls`). Fuzzy completion with `**<TAB>` only+  works for external commands.+- Custom completers can be defined via the `$env.FZF_COMPLETERS` record in+  your `config.nu`. Each entry is a closure that receives the prefix and the+  command spans, and returns either a list of candidate strings or a record+  `{ candidates: [...], opts: [...] }` for custom fzf options:+  ```nu+  $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: ["--preview", "pacman -Si {}"] }+      }+  }+  ```+  See [shell/completion-examples.nu](shell/completion-examples.nu) for more+  examples.+- The following environment variables are supported:+  `FZF_COMPLETION_TRIGGER`, `FZF_COMPLETION_OPTS`,+  `FZF_COMPLETION_PATH_OPTS`, `FZF_COMPLETION_DIR_OPTS`,+  `FZF_COMPLETION_DIR_COMMANDS`.+ Vim plugin ---------- 

install

@@ -6,10 +6,11 @@ version=0.72.0 auto_completion= key_bindings= update_config=2-shells="bash zsh fish"+shells="bash zsh fish nushell" prefix='~/.fzf' prefix_expand=~/.fzf fish_dir=${XDG_CONFIG_HOME:-$HOME/.config}/fish+nushell_autoload_dir=${XDG_CONFIG_HOME:-$HOME/.config}/nushell/autoload  help() {   cat << EOF@@ -27,6 +28,7 @@ usage: $0 [OPTIONS]     --no-bash            Do not set up bash configuration     --no-zsh             Do not set up zsh configuration     --no-fish            Do not set up fish configuration+    --no-nushell         Do not set up nushell configuration EOF } @@ -56,6 +58,7 @@ for opt in "$@"; do     --no-bash) shells=${shells/bash/} ;;     --no-zsh) shells=${shells/zsh/} ;;     --no-fish) shells=${shells/fish/} ;;+    --no-nushell) shells=${shells/nushell/} ;;     *)       echo "unknown option: $opt"       help@@ -224,7 +227,9 @@ fi [[ $* =~ "--bin" ]] && exit 0  for s in $shells; do-  if ! command -v "$s" > /dev/null; then+  bin=$s+  [[ "$s" = nushell ]] && bin=nu+  if ! command -v "$bin" > /dev/null; then     shells=${shells/$s/}   fi done@@ -251,6 +256,7 @@ for shell in $shells; do   fzf_completion="source \"$fzf_base/shell/completion.${shell}\""   fzf_key_bindings="source \"$fzf_base/shell/key-bindings.${shell}\""   [[ $shell == fish ]] && continue+  [[ $shell == nushell ]] && continue   src=${prefix_expand}.${shell}   echo -n "Generate $src ... " @@ -368,6 +374,7 @@ fi echo for shell in $shells; do   [[ $shell == fish ]] && continue+  [[ $shell == nushell ]] && continue   [ $shell = zsh ] && dest=${ZDOTDIR:-~}/.zshrc || dest=~/.bashrc   append_line $update_config "[ -f ${prefix}.${shell} ] && source ${prefix}.${shell}" "$dest" "${prefix}.${shell}" done@@ -436,6 +443,23 @@ if [[ $shells =~ fish ]]; then   fi fi +if [[ "$shells" =~ nushell ]]; then+  if [[ $key_bindings -eq 1 || $auto_completion -eq 1 ]]; then+    echo "Setting up Nushell integration ..."+    mkdir -p "$nushell_autoload_dir"+    echo -n "  Generate _fzf_integration.nu ... "+    if [[ $key_bindings -eq 1 && $auto_completion -eq 1 ]]; then+      "$fzf_base"/bin/fzf --nushell > "$nushell_autoload_dir/_fzf_integration.nu"+    elif [[ $key_bindings -eq 1 ]]; then+      cp "$fzf_base/shell/key-bindings.nu" "$nushell_autoload_dir/_fzf_integration.nu"+    else+      cp "$fzf_base/shell/completion.nu" "$nushell_autoload_dir/_fzf_integration.nu"+    fi+    echo "OK"+    echo+  fi+fi+ if [ $update_config -eq 1 ]; then   echo 'Finished. Restart your shell or reload config file.'   if [[ $shells =~ bash ]]; then@@ -445,6 +469,7 @@ if [ $update_config -eq 1 ]; then   fi   [[ $shells =~ zsh ]] && echo "   source ${ZDOTDIR:-~}/.zshrc   # zsh"   [[ $shells =~ fish && $lno_func -ne 0 ]] && echo '   fish_user_key_bindings  # fish'+  [[ $shells =~ nushell ]] && echo '   # nushell: files are loaded automatically from autoload directory'   echo   echo 'Use uninstall script to remove fzf.'   echo

main.go

@@ -29,6 +29,12 @@ var zshCompletion []byte //go:embed shell/key-bindings.fish var fishKeyBindings []byte +//go:embed shell/key-bindings.nu+var nushellKeyBindings []byte++//go:embed shell/completion.nu+var nushellCompletion []byte+ //go:embed shell/completion.fish var fishCompletion []byte @@ -71,6 +77,11 @@ func main() { 		printScript("completion.fish", fishCompletion) 		return 	}+	if options.Nushell {+		printScript("key-bindings.nu", nushellKeyBindings)+		printScript("completion.nu", nushellCompletion)+		return+	} 	if options.Help { 		fmt.Print(fzf.Usage) 		return

man/man1/fzf.1

@@ -1373,6 +1373,12 @@ Print script to set up Fish shell integration  e.g. \fBfzf \-\-fish | source\fR +.TP+.B "\-\-nushell"+Print script to set up Nushell shell integration++e.g. \fBfzf \-\-nushell | save \-f ~/.config/nushell/autoload/_fzf_integration.nu\fR+ .SS OTHERS .TP .B "\-\-no\-mouse"

shell/completion-examples.nu

new file mode 100644@@ -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}+#     }+# }

shell/completion.nu

new file mode 100644@@ -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 :

shell/key-bindings.nu

new file mode 100644@@ -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+}

src/options.go

@@ -233,6 +233,7 @@ Usage: fzf [options]     --bash                   Print script to set up Bash shell integration     --zsh                    Print script to set up Zsh shell integration     --fish                   Print script to set up Fish shell integration+    --nushell                Print script to set up Nushell integration    HELP     --version                Display version information and exit@@ -586,6 +587,7 @@ type Options struct { 	Bash              bool 	Zsh               bool 	Fish              bool+	Nushell           bool 	Man               bool 	Fuzzy             bool 	FuzzyAlgo         algo.Algo@@ -733,6 +735,7 @@ func defaultOptions() *Options { 		Bash:         false, 		Zsh:          false, 		Fish:         false,+		Nushell:      false, 		Man:          false, 		Fuzzy:        true, 		FuzzyAlgo:    algo.FuzzyMatchV2,@@ -2553,6 +2556,7 @@ func parseOptions(index *int, opts *Options, allArgs []string) error { 		opts.Bash = false 		opts.Zsh = false 		opts.Fish = false+		opts.Nushell = false 		opts.Help = false 		opts.Version = false 		opts.Man = false@@ -2665,6 +2669,9 @@ func parseOptions(index *int, opts *Options, allArgs []string) error { 		case "--fish": 			clearExitingOpts() 			opts.Fish = true+		case "--nushell":+			clearExitingOpts()+			opts.Nushell = true 		case "-h", "--help": 			clearExitingOpts() 			opts.Help = true

test/lib/common.rb

@@ -78,6 +78,38 @@ class Shell           "rm -f ~/.local/share/fish/fzf_test_history; XDG_CONFIG_HOME=#{confdir} fish"         end     end++    def nushell+      @nushell ||=+        begin+          xdg_home = '/tmp/fzf-nushell-xdg'+          config_dir = "#{xdg_home}/nushell"+          FileUtils.rm_rf(xdg_home)+          FileUtils.mkdir_p(config_dir)++          # Write env.nu to set up PATH and unset FZF variables+          File.open("#{config_dir}/env.nu", 'w') do |f|+            f.puts "$env.PATH = ($env.PATH | split row (char esep) | prepend '#{BASE}/bin')"+            UNSETS.each do |var|+              f.puts "hide-env -i #{var}"+            end+            f.puts "$env.FZF_DEFAULT_OPTS = \"--no-scrollbar --pointer '>' --marker '>'\""+            f.puts '$env.config = ($env.config | upsert history { file_format: "plaintext", max_size: 100 })'+          end++          # Write config.nu with minimal prompt+          File.open("#{config_dir}/config.nu", 'w') do |f|+            f.puts '$env.PROMPT_COMMAND = {|| "" }'+            f.puts '$env.PROMPT_INDICATOR = ""'+            f.puts '$env.PROMPT_COMMAND_RIGHT = {|| "" }'+            f.puts '$env.config = ($env.config | upsert show_banner false)'+            f.puts "source #{BASE}/shell/key-bindings.nu"+            f.puts "source #{BASE}/shell/completion.nu"+          end++          "unset #{UNSETS.join(' ')}; env XDG_CONFIG_HOME=#{xdg_home} XDG_DATA_HOME=#{xdg_home}/../fzf-nushell-data nu --config #{config_dir}/config.nu --env-config #{config_dir}/env.nu"+        end+    end   end end @@ -85,12 +117,30 @@ class Tmux   attr_reader :win    def initialize(shell = :bash)+    @shell = shell     @win = go(%W[new-window -d -P -F #I #{Shell.send(shell)}]).first     go(%W[set-window-option -t #{@win} pane-base-index 0])-    return unless shell == :fish--    send_keys 'function fish_prompt; end; clear', :Enter-    self.until(&:empty?)+    if shell == :fish+      send_keys 'function fish_prompt; end; clear', :Enter+      self.until(&:empty?)+    elsif shell == :nushell+      # Clear history from previous tests to avoid contamination+      FileUtils.rm_f('/tmp/fzf-nushell-xdg/nushell/history.txt')+      # Wait for nushell to be ready by polling with a marker command.+      # We use 'print "fzf-ready"' and check for a line that is exactly+      # 'fzf-ready' (not the command echo which includes 'print').+      retries = 0+      begin+        send_keys 'print "fzf-ready"', :Enter+        self.until { |lines| lines.any? { |l| l.strip == 'fzf-ready' } }+      rescue Minitest::Assertion+        retries += 1+        raise if retries > 5+        retry+      end+      send_keys 'clear', :Enter+      self.until(&:empty?)+    end   end    def kill@@ -242,11 +292,19 @@ class Tmux   def prepare     tries = 0     begin-      self.until(true) do |lines|+      if @shell == :nushell         message = "Prepare[#{tries}]"-        send_keys ' ', 'C-u', :Enter, message, :Left, :Right-        sleep(0.15)-        lines[-1] == message+        send_keys 'C-u', 'C-l'+        sleep 0.2+        send_keys ' ', 'C-u', :Enter, message+        self.until { |lines| lines[-1] == message }+      else+        self.until(true) do |lines|+          message = "Prepare[#{tries}]"+          send_keys ' ', 'C-u', :Enter, message, :Left, :Right+          sleep(0.15)+          lines[-1] == message+        end       end     rescue Minitest::Assertion       (tries += 1) < 5 ? retry : raise

test/test_shell_integration.rb

@@ -1103,3 +1103,123 @@ class TestFish < TestBase     end   end end++class TestNushell < TestBase+  include TestShell++  def teardown+    @tmux&.kill+  end++  def shell+    :nushell+  end++  def set_var(name, val)+    tmux.prepare+    tmux.send_keys "$env.#{name} = '#{val}'", :Enter+    tmux.prepare+  end++  def unset_var(name)+    tmux.prepare+    tmux.send_keys "hide-env -i #{name}", :Enter+    tmux.prepare+  end++  def new_shell+    tmux.send_keys 'FZF_TMUX=1 nu', :Enter+    tmux.prepare+  end++  # Override: Nushell's builtin `echo` outputs structured data, so we need+  # `^echo` (external echo) for plain text output on the command line.+  def test_ctrl_t_unicode+    writelines(['fzf-unicode 테스트1', 'fzf-unicode 테스트2'])+    set_var('FZF_CTRL_T_COMMAND', "cat #{tempname}")++    tmux.prepare+    tmux.send_keys '^echo ', 'C-t'+    tmux.until { |lines| assert_equal 2, lines.match_count }+    tmux.send_keys 'fzf-unicode'+    tmux.until { |lines| assert_equal 2, lines.match_count }++    tmux.send_keys '1'+    tmux.until { |lines| assert_equal 1, lines.match_count }+    tmux.send_keys :Tab+    tmux.until { |lines| assert_equal 1, lines.select_count }++    tmux.send_keys :BSpace+    tmux.until { |lines| assert_equal 2, lines.match_count }++    tmux.send_keys '2'+    tmux.until { |lines| assert_equal 1, lines.match_count }+    tmux.send_keys :Tab+    tmux.until { |lines| assert_equal 2, lines.select_count }++    tmux.send_keys :Enter+    tmux.until { |lines| assert_match(/\^echo .*fzf-unicode.*1.* .*fzf-unicode.*2/, lines.join) }+    tmux.send_keys :Enter+    tmux.until { |lines| assert_equal 'fzf-unicode 테스트1 fzf-unicode 테스트2', lines[-1] }+  end++  # Override: Nushell's external completer replaces the entire token,+  # so we use assert_includes instead of assert_equal for the result.+  # ~USERNAME expansion and backslash-escaped spaces are not applicable.+  def test_file_completion+    FileUtils.mkdir_p('/tmp/fzf-test')+    (1..100).each { |i| FileUtils.touch("/tmp/fzf-test/#{i}") }+    tmux.prepare++    # Multi-selection+    tmux.send_keys "cat /tmp/fzf-test/10#{trigger}", :Tab+    tmux.until { |lines| assert_equal 2, lines.match_count }+    tmux.send_keys :Tab, :Tab+    tmux.until { |lines| assert_equal 2, lines.select_count }+    tmux.send_keys :Enter+    tmux.until(true) do |lines|+      assert_includes lines[-1].to_s, '/tmp/fzf-test/10'+      assert_includes lines[-1].to_s, '/tmp/fzf-test/100'+    end++    # Single selection+    tmux.prepare+    tmux.send_keys "cat /tmp/fzf-test/10#{trigger}", :Tab+    tmux.until { |lines| assert_equal 2, lines.match_count }+    tmux.send_keys '0'+    tmux.until { |lines| assert_equal 1, lines.match_count }+    tmux.send_keys :Enter+    tmux.until(true) do |lines|+      assert_includes lines[-1].to_s, '/tmp/fzf-test/100'+    end++    # Should include hidden files+    (1..100).each { |i| FileUtils.touch("/tmp/fzf-test/.hidden-#{i}") }+    tmux.prepare+    tmux.send_keys "cat /tmp/fzf-test/hidden#{trigger}", :Tab+    tmux.until(true) do |lines|+      assert_equal 100, lines.match_count+      assert lines.any_include?('/tmp/fzf-test/.hidden-')+    end+    tmux.send_keys :Enter+  ensure+    FileUtils.rm_rf('/tmp/fzf-test')+  end++  # Nushell does not support multiline command recall the same way+  # as bash/zsh/fish, so test_ctrl_r_multiline is omitted.++  # Override: only test with 'foo' -- single and double quotes cause+  # issues in Nushell's line editor.+  def test_ctrl_r_abort+    %w[foo].each do |query|+      tmux.prepare+      tmux.send_keys :Enter, query+      tmux.until { |lines| assert lines[-1]&.start_with?(query) }+      tmux.send_keys 'C-r'+      tmux.until { |lines| assert_equal "> #{query}", lines[-1] }+      tmux.send_keys 'C-g'+      tmux.until { |lines| assert lines[-1]&.start_with?(query) }+    end+  end+end

typos.toml

@@ -6,6 +6,7 @@ enew = "enew" tabe = "tabe" Iterm = "Iterm" ser = "ser"+Slq = "Slq"  [files] extend-exclude = ["README.md", "*.s"]

uninstall

@@ -114,6 +114,9 @@ if [ -d "${fish_dir}/functions" ]; then   fi fi +nushell_autoload_dir=${XDG_CONFIG_HOME:-$HOME/.config}/nushell/autoload+remove "${nushell_autoload_dir}/_fzf_integration.nu"+ config_dir=$(dirname "$prefix_expand") if [[ $xdg == 1 ]] && [[ $config_dir == */fzf ]] && [[ -d $config_dir ]]; then   rmdir "$config_dir"