Repository navigation
fix: give Windows its own font list, link opener and shell quoting - #125
Merged
Merged
Conversation
Two services reached for Linux binaries on every platform. fonts.List shelled out to fc-list, which Windows has no equivalent of, so the font picker offered only the bundled default and the current selection. Windows now enumerates the Fonts registry key, machine-wide and per-user, folding a family's faces onto one entry. The value-name parsing is pure and sits in the untagged file, so it is tested everywhere; only the registry read is per-OS. OpenExternal hardcoded xdg-open even though the package already had a per-OS seam for opening files. It now goes through that seam. Windows opens a URL with rundll32 rather than the `cmd /c start` used for files: cmd strips metacharacters off its command line before start sees them, so a `&` in a query string would end the command and run what followed it.
Two more places where Windows got a POSIX assumption, both in OpenInEditor and both reachable from a filename a checked-out branch chose. The default opener ran `cmd /c start`. cmd reads & | < > as command separators and Go quotes an argument only when it holds a space, so opening a file named "a&calc.txt" with no $EDITOR set ran calc. explorer takes the path as a plain argument instead, with no shell in between. The terminal-editor line was quoted with shquote, whose own package doc says it is POSIX-only and is why the worktree setup wrapper skips Windows — this call site did not. cmd.exe does not strip single quotes, so every path with a space (C:\Users\First Last\...) reached the editor cut in half. Quoting is now the session shell's, behind the same per-OS seam as the openers. cmd cannot express a path holding " or %, or one ending in a backslash: the quote cannot be put back, % expands inside quotes, and the backslash escapes the closing quote. Those are refused, and the file opens with the default handler — a line that would run something else is worse than not using the editor.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Four Windows bugs in two services, all from the same habit: code that composes a
command line assuming Linux. The first two showed up as
WARNlines in aWindows
lich.logwhile chasing #124; looking for the third turned up thefourth.
1 — Fonts
fonts.Listshelled out tofc-list, so on Windows Settings › Font offeredonly the bundled default and whatever was already selected.
Windows now enumerates
SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fontsunder both
HKLMandHKCU— the same database DirectWrite, and so Chromium,resolves against — via
golang.org/x/sys/windows/registry(already a directdependency;
go.modis untouched). A missing per-user key is an empty list, notan error.
Registry value names are
<family>[ <face>] (<format>), and one entry may carryseveral families joined by
&. The parsing folds a family's faces onto oneentry so the picker offers
Arialrather than four Arials. The face-word listis deliberately short —
Black,LightandSemiboldare absent, becauseArial BlackandSegoe UI Lightare families in their own right. The ceilingis named in the code: a family shipping no regular face is offered under its
trimmed name, and the terminal falls back to the default when Chromium cannot
resolve it.
2 — Links
OpenExternalhardcodedxdg-openeven thoughinternal/systemalready had aper-OS seam (
openDefault, for files). It now goes through that seam, whichgains an
openURLalongside.macOS needed its own:
openDefaultisopen -t, which forces the texteditor — right for a source file, wrong for a link.
Windows uses
rundll32 url.dll,FileProtocolHandlerrather than thecmd /c startused for files.cmdparses metacharacters out of its command linebefore
startsees them, sohttps://x/?a=1&b=2would end the command at the&and runb=2.3 — A filename could run a command
That same
cmd /c startcarried the file path inopenDefault, and there themetacharacter is not ours — it is whatever the file is called.
OpenInEditorfalls back toopenDefaultwhen neither$VISUALnor$EDITORis set. Its only caller is the FilesPanel, which lists
git statusfor thecheckout — and checking out someone else's branch is what lich is for. Go's
syscall.EscapeArgquotes an argument only when it holds a space, tab or quote,so
&went through bare andcmdcut the command there:relpath.Validatedoes not close this — it rejects rooted paths and traversal,which is all it claims to do.
Windows now opens the file with
explorer, which takes the path as a plainargument with no shell in between. Not the
rundll32used for URLs above:FileProtocolHandlerURL-decodes, so a file nameda%20b.txtwould open thewrong path.
4 — Every Windows path with a space was broken anyway
Looking for #3's blast radius turned this up. The terminal-editor branch of
OpenInEditorbuilds a command line for the session's shell and quoted itwith
shquote, whose own package doc says:The setup wrapper skips Windows. This call site did not. cmd.exe does not strip
single quotes, so with
EDITOR=nvima Windows session was pasted:and nvim opened
'C:\Users\First. No attacker needed — most Windows homedirectories have a space in them.
Quoting now goes through the same per-OS seam as the openers:
shquoteonUnix, double quotes on Windows. cmd cannot express three cases, and they are
refused rather than half-escaped — a
"(nothing puts one back inside a quotedrun), a
%(expanded inside quotes too), and a trailing backslash (escapes theclosing quote for the target's own argv parser). A refused path opens with the
default handler, which after #3 is shell-free.
Shape
Pure rules — the registry value parsing, the cmd quoting — live in the untagged
files and are tested on every OS, following
windowsBrowserCandidatesininternal/chromium. Only the registry read and the launcher choice are per-OS.Test plan
gofmt -l .andgo vet ./...cleango test ./...— 536 passGOOS=linux|darwin|windows—go build+go vetcleanbiome check,vitest run(501 pass) — frontend untouched, run for the gateTestParseRegistryFamilies,TestTrimFontFacesKeepsALoneFaceWord,TestOpenExternalPassesURLAsOneArgument,TestQuoteCmdPath,TestOpenDefaultKeepsThePathOutOfAShellci:os—TestListSmokeis the real registry read and can no longer skipon Windows;
TestOpenInEditorTerminalReturnsCommandandTestOpenInEditorFallsBackWhenTheShellCannotQuoteassert the cmd.exe branchthere
pull-request link opens in the browser; a file under a path with a space
opens in
nvim; a file nameda&calc.txtopens instead of runningcalc🤖 Generated with Claude Code