Tools/carapace.ps1
|
# Companion for carapace — registers native argument completers for ~519 commands. # Writes: bundled specs to $XDG_CONFIG_HOME\carapace\specs\*.yaml, and the cached # init script under $XDG_CACHE_HOME\dotforge\ (Get-DFCachedCommandOutput). # Sets: CARAPACE_BRIDGES (adds 'inshellisense', see below); carapace's own init # prepends $XDG_CONFIG_HOME\carapace\bin to PATH. # Invoke-Expression is required by carapace's init pattern — no alternative exists. [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingInvokeExpression', '')] param() function Enable-DFCarapaceInshellisenseBridge { <# .SYNOPSIS Adds inshellisense to CARAPACE_BRIDGES when it is installed but does not own Tab. .DESCRIPTION Retains user bridges, removes duplicate entries case-insensitively, and leaves the setting alone when inshellisense is the tab-completion winner. .OUTPUTS System.Boolean. True when the bridge is enabled. #> [CmdletBinding()] param([string]$TabCompletionWinner) $executable = Get-Command is -ErrorAction Ignore if (-not $executable) { $executable = Get-Command inshellisense -ErrorAction Ignore } if (-not $executable -or $TabCompletionWinner -eq 'inshellisense') { return $false } $bridges = [System.Collections.Generic.List[string]]::new() $seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) foreach ($bridge in @($Env:CARAPACE_BRIDGES -split ',')) { $name = $bridge.Trim(); if ($name -and $seen.Add($name)) { $bridges.Add($name) } } if ($seen.Add('inshellisense')) { $bridges.Add('inshellisense') } $Env:CARAPACE_BRIDGES = $bridges -join ',' $true } # carapace's init emits Register-ArgumentCompleter calls only — it never binds Tab # and never overrides TabExpansion. Tab itself is bound once, after every tool has # registered. Tab ownership is selected by the tab-completion role; both route through # TabExpansion2, which consults these completers. Registration order is # irrelevant, so no after on PSFzf is declared. (carapace.json does declare # "after": ["fnm"] so fnm puts the Node-hosted `is` on PATH before the bridge # check below.) # # Argument completers are registered session-wide by the engine regardless of the # scope Invoke-Expression runs in, so dot-sourcing from Register-DFTool is safe. # # Known deviation: carapace's generated init prepends $XDG_CONFIG_HOME/carapace/bin # to PATH itself (the bridge-shim directory) instead of going through Add-DFToPath. # That line is emitted by carapace, not DotForge, and cannot be rerouted. # # Bridges: when inshellisense (`is`) is on PATH and did not win Tab, # 'inshellisense' is appended to CARAPACE_BRIDGES so commands carapace has no # completer for fall back to inshellisense's specs. Bridge entries the user set # are kept. Other bridges (zsh/fish/bash) are left to the user: each shells out # once per completion. $_tabWinner = (Get-DFRole 'tab-completion').Winner Enable-DFCarapaceInshellisenseBridge -TabCompletionWinner $_tabWinner | Out-Null # Deploy bundled specs (e.g. scoop, which carapace ships no completer for) into # carapace's spec directory. carapace auto-loads *.yaml from there — see # `carapace --help` ("Specs are loaded from ..."). We overwrite DotForge-shipped # specs (matched by filename) so fixes propagate; users wanting a custom scoop # spec can place it elsewhere or edit the deployed copy, which is refreshed only # when the bundled content changes. $_bundledSpecs = Join-Path $PSScriptRoot 'carapace' 'specs' if (Test-Path $_bundledSpecs) { $_specDir = Join-Path (Get-DFXdgPath Config) 'carapace' 'specs' New-DFDirectory $_specDir | Out-Null Get-ChildItem $_bundledSpecs -Filter '*.yaml' | ForEach-Object { $_dest = Join-Path $_specDir $_.Name $_new = Get-Content $_.FullName -Raw if (-not (Test-Path $_dest) -or (Get-Content $_dest -Raw) -ne $_new) { Set-Content -Path $_dest -Value $_new -Encoding UTF8 -NoNewline # byte-identical, so the compare above matches next time } } } # carapace appends a trailing space to each CompletionText (its "token complete" # convention). PSFzf's FixCompletionResult quotes any completion containing a # space, so a fuzzy-picked `docker build` would land as `docker "build "`. When # PSFzf will own Tab (Native mode + PSFzf available), trim the trailing space so # the picker inserts a clean, unquoted value — PSFzf re-adds a single trailing # space itself. The replacement targets carapace's generated constructor call # and no-ops (leaving the space intact, which MenuComplete needs for subcommand # chaining) if carapace ever changes that codegen. Catalogued in # docs/external-dependencies.md. # # The init script is a pure function of carapace's own build (verified # byte-identical across runs) -- cached keyed to the binary's own file # identity so a carapace upgrade regenerates it. See # docs/superpowers/specs/2026-09-05-startup-perf-audit.md. # carapace registers a completer per command it knows, including commands that # only have a user spec in the specs folder, so the folder's contents are part of # the cache key: adding, removing or editing a spec regenerates the init script. $_specKey = if ($_specDir -and (Test-Path $_specDir)) { (Get-ChildItem $_specDir -Filter '*.yaml' -File | Sort-Object Name | ForEach-Object { "$($_.Name):$($_.LastWriteTimeUtc.Ticks)" }) -join ',' } $_carapaceInit = Get-DFCachedCommandOutput -Name 'carapace-init' -Executable 'carapace' -ExtraKey $_specKey -Generate { carapace _carapace powershell | Out-String } if ($_tabWinner -eq 'PSFzf') { # Trimming could leave an empty CompletionText, which [CompletionResult]::new # rejects -- drop whitespace-only items before they reach the constructor. $_carapaceInit = $_carapaceInit.Replace( 'ConvertFrom-Json | ForEach-Object {', 'ConvertFrom-Json | Where-Object { ([string]$_.CompletionText).Trim() } | ForEach-Object {') $_carapaceInit = $_carapaceInit.Replace( '[CompletionResult]::new($_.CompletionText,', '[CompletionResult]::new(([string]$_.CompletionText).TrimEnd(),') } # When carapace has no completions it returns "" to suppress PowerShell's file # fallback. pwsh 7.6 turns that "" into [CompletionResult]::new(''), which throws; # PSFzf's Tab handler swallows the exception, so Tab silently does nothing (e.g. # `ls ..\<Tab>` -- carapace does not understand the backslash prefix). A bare # return lets PowerShell fall back to filesystem completion instead. No-ops if # carapace changes that codegen. Catalogued in docs/external-dependencies.md. $_carapaceInit = $_carapaceInit.Replace('return "" # prevent default file completion', 'return') Invoke-Expression $_carapaceInit function Initialize-DFRoleTabCompletion { param($Tool, $Role) Set-PSReadLineKeyHandler -Key Tab -Function MenuComplete } |