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}