Let Carapace Handle Nushell External Completions

Move Nushell external completions to Carapace, use choices and user specs to fix missing brew services completions, and keep Fish bridging out of config.
/

Continuing from the previous post. When I first configured Nushell completions, I maintained an allowlist in external_completer: use fish_completer when Fish has better completions, and otherwise use carapace_completer. The core logic looked roughly like this:

match $spans.0 {    nu | tv | bun | git | rclone => $fish_completer    _ => $carapace_completer} | do $in $spans

This configuration works. But the selection logic is written into Nushell config, so other shells cannot reuse it. Moving that logic into Carapace as the middle layer means I no longer have to maintain the same dispatch rules in every shell.

Of course, I probably will not use another shell again anyway, so this does feel a bit like tinkering for tinkering’s sake 😇

Starting with brew services 🍺

Recently I switched from Clash Verge Rev to a bare mihomo core, so I often use brew services start mihomo to start the service.

Carapace does not yet support service-name completions for the brew services subcommand. To my pleasant surprise, Fish can complete it:

Using Fish to complete brew services commands
Using Fish to complete brew services commands

Following the earlier approach, I only need to forward brew completions to fish_completer, like this:

match $spans.0 {    nu | tv | bun | git | rclone | brew => $fish_completer    _ => $carapace_completer} | do $in $spans

This fixes the immediate issue, but the completion strategy is still scattered inside Nushell. Below is the Carapace approach.

Carapace’s Selection Order 🔢

Carapace now divides completers into different groups. On macOS, when a command has multiple completion sources, the default priority is roughly:

  1. user
  2. system
  3. darwin
  4. bsd
  5. unix
  6. common
  7. bridge

This explains why simply enabling the Fish bridge, meaning setting the CARAPACE_BRIDGES=fish environment variable, does not necessarily solve the brew problem. For example, run carapace brew nushell brew se | jq to inspect completions for brew se, and you will find only the search subcommand. services is missing.

[  {    "value": "search ",    "display": "search",    "description": "Perform a substring search of cask tokens and formula names for <text>",    "style": {      "fg": "blue"    }  }]

The reason is that brew already has a built-in Carapace completer, which belongs to common. A Fish bridge discovered through CARAPACE_BRIDGES belongs to bridge. By default, common takes precedence over bridge.

You can use carapace --list brew to see the actual order:

{  "brew": [    {      "name": "brew",      "description": "The missing package manager for macOS",      "group": "common",      "package": "github.com/carapace-sh/carapace-bin/completers_release/common/brew_completer/cmd",      "url": "https://brew.sh/"    },    {      "name": "brew",      "description": "The missing package manager for macOS",      "group": "bridge",      "variant": "fish"    }  ]}

For this, you can use carapace choices to adjust the priority of completion sources for a command. In the code below, move Fish completions to the front, then run carapace brew nushell brew se | jq . again. Besides search, you will now see completion entries for services and setup-ruby:

$ carapace --choice brew/fish@bridge$ carapace brew nushell brew se | jq .[  {    "value": "search ",    "display": "search",    "description": "Perform a substring search of cask tokens and formula names for text"  },  {    "value": "services ",    "display": "services",    "description": "Integrates Homebrew formulae with macOS's launchctl manager"  },  {    "value": "setup-ruby ",    "display": "setup-ruby",    "description": "Installs and configures Homebrew's Ruby"  }]

Another Solution: Override brew with a user spec 👥

From the priority list above, user group is the highest-priority completion source. So it is natural to define brew in the user group and make it use Fish completions.

Carapace automatically loads specs under the configuration directory.

I set XDG_CONFIG_HOME = ~/.config, so I created brew.yaml in ~/.config/carapace/specs:

~/.config/carapace/specs/brew.yaml
# yaml-language-server: $schema=https://carapace.sh/schemas/command.jsonname: brewdescription: The missing package manager for macOSparsing: disabledcompletion:  positionalany: ["$carapace.bridge.Fish([brew])"]

Note that name: brew must match the file name brew.yaml.

After that, open a new shell and run carapace brew nushell brew se | jq. You will see the same output as in the previous section: the completion results include all three subcommands.

Cleaning Up Nushell Completion Config 🫧

With the work above done, Nushell no longer needs to maintain an allowlist of “which commands should use Fish.” Keep only alias expansion logic, then hand everything to Carapace:

let carapace_completer = {|spans: list<string>|    CARAPACE_LENIENT=1 carapace $spans.0 nushell ...$spans | from json}let external_completer = {|spans|    let expanded_alias = scope aliases    | where name == $spans.0    | get -o 0.expansion    let spans = if $expanded_alias != null {        $spans        | skip 1        | prepend ($expanded_alias | split row ' ' | take 1)    } else {        $spans    }    match $spans.0 {        _ => $carapace_completer    } | do $in $spans}$env.config.completions = {  case_sensitive: false  quick: true # set this to false to prevent auto-selecting completions when only one remains  partial: true # set this to false to prevent partial filling of the prompt  algorithm: "prefix" # prefix or fuzzy  external: {    enable: true    completer: $external_completer  }  use_ls_colors: true}