diff --git a/.editorconfig b/.editorconfig index 2e450828..0bfd42e1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -16,3 +16,8 @@ trim_trailing_whitespace = false [*.sh] indent_size = 4 + +; Scripts in ~/.local/bin are extensionless by convention, so [*.sh] misses +; them. Keep them indented like every other shell script in the repo. +[home/dot_local/bin/*] +indent_size = 4 diff --git a/docs/wsl.md b/docs/wsl.md index bfbe2611..d3a4c824 100644 --- a/docs/wsl.md +++ b/docs/wsl.md @@ -1,20 +1,25 @@ -# 🪟 WSL and 1Password +# 🪟 WSL + +A WSL distro is a Linux userland without the pieces a desktop Linux takes for +granted: no local SSH agent holding your keys, and no graphical browser. This +repo wires up both, guarded so nothing leaks into a native Linux or macOS +session. + +## SSH keys live in 1Password In WSL your SSH private keys are not on the Linux side at all — they live in -1Password on the Windows host. Rather than forwarding an *agent socket*, the +1Password on the Windows host. Rather than forwarding an _agent socket_, the [1Password WSL integration][opwsl] forwards the whole SSH request to the Windows OpenSSH client (`ssh.exe`), which talks to the 1Password SSH agent and raises the approval prompt on Windows. -This repo wires that up for you. - -## What is configured automatically +### What is configured automatically -| Concern | How | Where | -| --- | --- | --- | -| Git over SSH | `core.sshCommand = ssh.exe` | `dot_config/git/config.tmpl` | -| Interactive `ssh` / `ssh-add` | aliased to `ssh.exe` / `ssh-add.exe` | `dot_config/shell/functions/wsl-ssh.sh`, `dot_config/fish/conf.d/wsl-ssh.fish` | -| Commit signing | `gpg.ssh.program = op-ssh-sign-wsl.exe` | `dot_config/git/config.tmpl` (see [git-signing.md](git-signing.md)) | +| Concern | How | Where | +| ----------------------------- | --------------------------------------- | ------------------------------------------------------------------------------ | +| Git over SSH | `core.sshCommand = ssh.exe` | `dot_config/git/config.tmpl` | +| Interactive `ssh` / `ssh-add` | aliased to `ssh.exe` / `ssh-add.exe` | `dot_config/shell/functions/wsl-ssh.sh`, `dot_config/fish/conf.d/wsl-ssh.fish` | +| Commit signing | `gpg.ssh.program = op-ssh-sign-wsl.exe` | `dot_config/git/config.tmpl` (see [git-signing.md](git-signing.md)) | The aliases are guarded on `WSL_DISTRO_NAME` **and** on `ssh.exe` being reachable, so they never leak into a native Linux or macOS session and stay @@ -61,7 +66,58 @@ Approving a key authorizes the current WSL session only. A new session or tab prompts again — the same [authorization model][opauth] 1Password uses everywhere else. -## What does *not* work in WSL +## Opening links in a browser + +A WSL distro has no browser of its own, and no `xdg-open`. Anything that opens +one — `gh auth login`, OAuth flows, `npm docs` — looks for `xdg-open` or +`$BROWSER`, and when neither points anywhere useful it falls back to whatever +terminal browser happens to be installed. Completing an OAuth flow in `lynx` is +not a good time. + +`~/.local/bin/wsl-browser` hands the URL to Windows via `powershell.exe +Start-Process`, which opens it in the real default browser. `BROWSER` is +pointed at it, which fixes every tool at once instead of configuring them one +at a time. + +| Concern | How | Where | +| ------------- | ------------------------------- | -------------------------------------------------------------------------------------- | +| Opening a URL | `powershell.exe Start-Process` | `dot_local/bin/executable_wsl-browser` | +| `$BROWSER` | exported when running under WSL | `dot_config/shell/functions/wsl-browser.sh`, `dot_config/fish/conf.d/wsl-browser.fish` | + +Guarded on `WSL_DISTRO_NAME` **and** on `powershell.exe` being reachable, so it +stays inert on a native Linux session or when [interop][interop] is disabled. + +Two deliberate choices in that script: + +- **It only accepts `http(s)` URLs or files that exist.** `Start-Process` is + Windows' general "run this" verb, not a browser — handed `calc.exe` or a path + to an executable, it would _run_ it. Since `$BROWSER` is invoked by other + programs, anything else is refused rather than quietly launched. +- **The URL is passed through `WSLENV`, not interpolated** into the PowerShell + command string, so a URL containing quotes or semicolons is always data and + never code. + +### Why not wslu? + +[wslu][wslu] provides `wslview`, which does the same job and would be the +obvious dependency. It was [archived upstream][wsluarchive] in 2025 — last +release 4.1.3, April 2024 — and it is not packaged for Debian 13. Installing an +unmaintained `.deb` outside `apt` is worse than shipping the few lines above. +If it ever returns to the archives, `wslview` is a drop-in replacement: point +`BROWSER` at it and delete the script. + +### Check it + +```bash +echo "$BROWSER" # ~/.local/bin/wsl-browser +wsl-browser https://github.com +``` + +If a tool still opens a terminal browser it is probably remembering its own +setting — `gh`, for example, has `gh config set browser`. Clear it with +`gh config set browser ""` and let `$BROWSER` win. + +## What does _not_ work in WSL 1Password **shell plugins** (`op plugin run`) are unsupported under WSL, so the `gh` / `copilot` wrappers are not applied there. See @@ -71,3 +127,5 @@ everywhere else. [opagent]: https://www.1password.dev/ssh/get-started/ [opauth]: https://www.1password.dev/ssh/agent/security#authorization-model [interop]: https://learn.microsoft.com/windows/wsl/wsl-config#interop-settings +[wslu]: https://wslutiliti.es/wslu/ +[wsluarchive]: https://github.com/wslutilities/wslu diff --git a/home/dot_config/fish/conf.d/wsl-browser.fish b/home/dot_config/fish/conf.d/wsl-browser.fish new file mode 100644 index 00000000..f615aeec --- /dev/null +++ b/home/dot_config/fish/conf.d/wsl-browser.fish @@ -0,0 +1,17 @@ +# WSL -> Windows browser integration for fish. +# +# A WSL distro has no graphical browser and no xdg-open. Tools that "open a +# browser" — `gh auth login`, OAuth flows, `npm docs` — fall back to whatever +# terminal browser is installed (lynx, w3m), which is a miserable way to +# complete an OAuth flow. Pointing BROWSER at the wsl-browser helper in +# ~/.local/bin fixes every such tool at once. See docs/wsl.md. +# +# Guarded on WSL_DISTRO_NAME (set by WSL itself) so this never leaks into a +# native Linux or macOS session, and on powershell.exe being reachable, which +# requires WSL interop to be enabled. + +if set -q WSL_DISTRO_NAME; and command -q powershell.exe + if test -x $HOME/.local/bin/wsl-browser + set -gx BROWSER $HOME/.local/bin/wsl-browser + end +end diff --git a/home/dot_config/shell/functions/wsl-browser.sh b/home/dot_config/shell/functions/wsl-browser.sh new file mode 100644 index 00000000..e6188f49 --- /dev/null +++ b/home/dot_config/shell/functions/wsl-browser.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# WSL -> Windows browser integration for Bash and Zsh. +# +# A WSL distro has no graphical browser and no xdg-open. Tools that "open a +# browser" — `gh auth login`, OAuth flows, `npm docs` — fall back to whatever +# terminal browser is installed (lynx, w3m), which is a miserable way to +# complete an OAuth flow. Pointing BROWSER at the wsl-browser helper in +# ~/.local/bin fixes every such tool at once. See docs/wsl.md. +# +# Guarded on WSL_DISTRO_NAME (set by WSL itself) so this never leaks into a +# native Linux or macOS session, and on powershell.exe being reachable, which +# requires WSL interop to be enabled. + +if [ -n "${WSL_DISTRO_NAME:-}" ] && command -v powershell.exe >/dev/null 2>&1 && + [ -x "${HOME}/.local/bin/wsl-browser" ]; then + BROWSER="${HOME}/.local/bin/wsl-browser" + export BROWSER +fi diff --git a/home/dot_local/bin/executable_wsl-browser b/home/dot_local/bin/executable_wsl-browser new file mode 100644 index 00000000..a9b09c62 --- /dev/null +++ b/home/dot_local/bin/executable_wsl-browser @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Open a URL or file in the Windows default browser, from WSL. +# +# WSL has no browser of its own, and no xdg-open. Tools that "open a browser" +# — `gh auth login`, OAuth flows, `npm docs` — fall back to whatever terminal +# browser happens to be installed (lynx, w3m). Completing an OAuth flow in lynx +# is not a good time. Pointing $BROWSER here fixes every such tool at once. +# +# The obvious dependency for this is wslu's `wslview`, but that project was +# archived upstream in 2025 (last release 4.1.3, April 2024) and is not +# packaged for Debian 13. Rather than install an unmaintained .deb outside apt, +# we ship these few lines. See docs/wsl.md. + +set -euo pipefail + +target=${1:-} +if [ -z "${target}" ]; then + echo "usage: ${0##*/} " >&2 + exit 2 +fi + +# Windows' Start-Process is a general "run this" verb, not a browser: handed a +# program name or an .exe path it would execute it. So only pass it things that +# are unambiguously browsable — an http(s) URL, or a file that exists. Anything +# else is refused rather than quietly launched. +if [ -e "${target}" ]; then + target=$(wslpath -w "${target}") +elif [[ ${target} =~ ^https?://[^[:space:]]+$ ]]; then + : # a plain http(s) URL — fine +else + echo "${0##*/}: refusing to open '${target}' (not an http(s) URL or existing file)" >&2 + exit 1 +fi + +# The target is handed over as an environment variable rather than interpolated +# into the PowerShell command string, so a URL containing quotes or semicolons +# is always data and never code. WSLENV is what makes the variable visible to +# the Windows process. `--` stops Start-Process reading a leading `-` as a flag. +export WSLB_TARGET="${target}" +export WSLENV="${WSLENV:+${WSLENV}:}WSLB_TARGET" + +# shellcheck disable=SC2016 # $env: is PowerShell syntax, evaluated on the Windows side. +exec powershell.exe -NoProfile -NonInteractive \ + -Command 'Start-Process -- $env:WSLB_TARGET'