用 Carapace 接管 Nushell 外部补全

Nushell 外部补全交给 Carapace 后,可以用 choice 与 user spec 解决 brew services 漏补全,并把 Fish 桥接从 Nushell 配置中移走,适合统一维护多 shell 补全策略。

书接上回。早先配置 Nushell 补全时,我在 external_completer 里维护了一份白名单:Fish 补全较完善就走 fish_completer,否则走 carapace_completer。核心逻辑大概如下:

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

这段配置可以工作。只是选择逻辑被写进了 Nushell 配置,其他 shell 无法复用;迁移到 Carapace 这个中间层,则不需要在每个 shell 里重复维护同一套分发规则。

当然,我大概率不会再使用其他 shell,有种为了折腾而折腾的感觉😇

从 brew services 说起 🍺

最近从 Clash Verge Rev 转向 mihomo 裸核,启动服务时会频繁用到 brew services start mihomo

Carapace 暂时不支持为 brew services 子命令补全服务名;比较惊喜地发现 Fish 里可以补全:

使用 Fish 补全 brew services 命令
使用 Fish 补全 brew services 命令

按照前面的做法,只需要把 brew 的补全转发到 fish_completer 即可,形如:

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

这能修好当前问题,但补全策略还是散落在 Nushell 里。下面讲一下Carapace的做法

Carapace 的选择顺序 🔢

Carapace 现在把 completer 分成不同 group。以 macOS 为例,一个命令同时存在多个补全来源时,默认优先级大致是:

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

这解释了为什么只启用 Fish bridge(即设置了环境变量 CARAPACE_BRIDGES=fish)不一定能解决 brew 的问题;例如运行carapace brew nushell brew se | jq,尝试查看brew se的补全结果,会发现只有search子命令 —— services被漏掉了

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

原因在于brew 已经有 Carapace 内置补全,属于 common;Fish bridge 如果通过 CARAPACE_BRIDGES 发现,则属于 bridge。默认情况下,common 会压过 bridge

可以用 carapace --list brew 看实际顺序:

{  "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"    }  ]}

对此,可以使用carapace choices 调整命令补全来源的优先级,参考下面代码,将fish补全移到前面,再次运行carapace brew nushell brew se | jq .,可以发现除了search,还提供了servicessetup-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"  }]

另一种解决方案: 用 user spec 覆写 brew 👥

前面优先级中可以看到,user group 是优先级最高的补全来源。那么自然可以想到在user group中定义,让brew走fish的补全方案。

Carapace 会自动加载 配置目录 下的 specs

个人设置了XDG_CONFIG_HOME = ~/.config,所以在~/.config/carapace/specs新建 brew.yaml

~/.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])"]

注意name: brew 必须和文件名 brew.yaml 对上。

结束后,新开一个 shell,运行carapace brew nushell brew se | jq,会看到同上一节一致的输出:补全结果包含完整的三个子命令

Nushell 补全配置的清理 🫧

完成上述工作,Nushell 侧就不需要维护「哪些命令走 Fish」的白名单了。保留别名展开逻辑即可,最后统一交给 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}