diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 27acab2f937..39f10872653 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -33,7 +33,11 @@ jobs: ruby-version: 3.4.6 - name: Install packages - run: sudo apt-get install --yes zsh fish tmux shfmt + run: | + 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 diff --git a/README.md b/README.md index 01220a815c5..727bc3f6fdf 100644 --- a/README.md +++ b/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.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 `**` 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 ---------- diff --git a/install b/install index 121b6004365..3eb1fd95b57 100755 --- a/install +++ b/install @@ -6,10 +6,11 @@ version=0.71.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,15 @@ if [[ $shells =~ fish ]]; then fi fi +if [[ "$shells" =~ nushell ]]; then + echo "Setting up Nushell integration ..." + mkdir -p "$nushell_autoload_dir" + echo -n " Generate fzf.nu ... " + "$fzf_base"/bin/fzf --nushell > "$nushell_autoload_dir/fzf.nu" + echo "OK" + echo +fi + if [ $update_config -eq 1 ]; then echo 'Finished. Restart your shell or reload config file.' if [[ $shells =~ bash ]]; then @@ -445,6 +461,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 diff --git a/main.go b/main.go index 40b664a81c2..c8c91a68046 100644 --- a/main.go +++ b/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 diff --git a/shell/completion-examples.nu b/shell/completion-examples.nu new file mode 100644 index 00000000000..1cd5b805c9f --- /dev/null +++ b/shell/completion-examples.nu @@ -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 **") +# - 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 # candidates to feed to fzf +# opts: list # 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} +# } +# } diff --git a/shell/completion.nu b/shell/completion.nu new file mode 100644 index 00000000000..86116414f25 --- /dev/null +++ b/shell/completion.nu @@ -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 = "!" + +# - $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_nu [prepend: string, append: string] { + let default_opts = $env.FZF_DEFAULT_OPTS? | default '' + let default_opts_file = $env.FZF_DEFAULT_OPTS_FILE? | default '' + let height_opt = $env.FZF_TMUX_HEIGHT? | default '40%' + + let file_opts = try { + open $default_opts_file | str trim + } catch { + '' + } + + # Build options string 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 + $"--height ($height_opt) --min-height 20+ --bind=ctrl-z:ignore ($prepend) ($file_opts) ($default_opts) ($append)" | str trim +} + +# Wrapper for running fzf or fzf-tmux +def __fzf_comprun_nu [ 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 # 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_nu "" ($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_nu | 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_nu`. + _fzf_comprun_nu $context_name $query ...$fzf_prefinal_opt # Pass args correctly to custom function + } else if ($env.TMUX_PANE? | 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_nu [] { + # 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+(?.+)' # 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 '^(?:\[)?(?[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+(?.+)' # 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_nu [ prefix: string # The text before the trigger + , fzf_opts_arg: list # 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_nu "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_nu [prefix: string] { + # Zsh args: base, lbuf, _fzf_compgen_path, "-m", "", " " + # Nu: prefix, empty command name (use find), ["-m"], "", " " + __fzf_generic_path_completion_nu $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_nu [ query: string # The initial query string for fzf + , data_gen_closure: closure # Closure that generates candidates + , fzf_opts_arg: list # 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_nu "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_nu [ 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_nu $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_nu + | 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_nu for the initial query + let selected_host = (_fzf_complete_nu $query $host_candidates_gen ["+m"]) # Pass host_prefix here + if not ($selected_host | is-empty) { + $completion_result = $selected_host # _fzf_complete_nu 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_nu [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_nu $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_nu $prefix {|| $candidates} $fzf_opts --post_process_closure $post) + } else { + $completion_results = (_fzf_complete_nu $prefix {|| $candidates} $fzf_opts) + } + } else { + $completion_results = (_fzf_complete_nu $prefix {|| $user_result} ["-m"]) + } + } else { + match $cmd_word { + "ssh" | "scp" | "sftp" | "telnet" => { $completion_results = (_fzf_complete_ssh_nu $prefix $line_without_trigger) } + "kill" => { $completion_results = (_fzf_complete_kill_nu $prefix) } + _ if ($cmd_word in $env.FZF_COMPLETION_DIR_COMMANDS) => { + $completion_results = (__fzf_generic_path_completion_nu $prefix [] "/") + } + _ => { + # Default to path completion if no specific command matches + $completion_results = (_fzf_path_completion_nu $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 --- + +# 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 + } +} + +# vim: set sts=2 ts=2 sw=2 tw=120 et : diff --git a/shell/key-bindings.nu b/shell/key-bindings.nu new file mode 100644 index 00000000000..4e0b9dca0da --- /dev/null +++ b/shell/key-bindings.nu @@ -0,0 +1,164 @@ +# ____ ____ +# / __/___ / __/ +# / /_/_ / / /_ +# / __/ / /_/ __/ +# /_/ /___/_/ 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 { + 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 operate on an already-materialized list and are + # effectively free. + 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 +} diff --git a/src/options.go b/src/options.go index 081621b8bee..8c098717f95 100644 --- a/src/options.go +++ b/src/options.go @@ -232,6 +232,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 @@ -578,6 +579,7 @@ type Options struct { Bash bool Zsh bool Fish bool + Nushell bool Man bool Fuzzy bool FuzzyAlgo algo.Algo @@ -725,6 +727,7 @@ func defaultOptions() *Options { Bash: false, Zsh: false, Fish: false, + Nushell: false, Man: false, Fuzzy: true, FuzzyAlgo: algo.FuzzyMatchV2, @@ -2521,6 +2524,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 @@ -2633,6 +2637,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 diff --git a/test/lib/common.rb b/test/lib/common.rb index dcb8011c6a7..65e8b7bf8a3 100644 --- a/test/lib/common.rb +++ b/test/lib/common.rb @@ -78,6 +78,38 @@ def fish "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,24 @@ 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: send a marker and wait for it + sleep 2 + send_keys 'clear', :Enter + self.until(&:empty?) + send_keys '"ready"', :Enter + self.until { |lines| lines.any_include?('ready') } + send_keys 'clear', :Enter + self.until(&:empty?) + end end def kill @@ -242,11 +286,19 @@ def any_include?(val) 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 diff --git a/test/test_shell_integration.rb b/test/test_shell_integration.rb index cc013ce0320..fcb96388033 100644 --- a/test/test_shell_integration.rb +++ b/test/test_shell_integration.rb @@ -1103,3 +1103,123 @@ def test_ctrl_r_multi 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 diff --git a/typos.toml b/typos.toml index 118438738b6..c7808ccfb65 100644 --- a/typos.toml +++ b/typos.toml @@ -6,6 +6,7 @@ enew = "enew" tabe = "tabe" Iterm = "Iterm" ser = "ser" +Slq = "Slq" [files] extend-exclude = ["README.md", "*.s"] diff --git a/uninstall b/uninstall index 7882cf5cdc7..64bed0a4aa4 100755 --- a/uninstall +++ b/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.nu" + config_dir=$(dirname "$prefix_expand") if [[ $xdg == 1 ]] && [[ $config_dir == */fzf ]] && [[ -d $config_dir ]]; then rmdir "$config_dir"