diff --git a/.github/workflows/install.yml b/.github/workflows/install.yml index d8fc3de..eaffe2c 100644 --- a/.github/workflows/install.yml +++ b/.github/workflows/install.yml @@ -54,3 +54,71 @@ jobs: run: | test -f nvim/lazy-lock.json test -d "$HOME/.local/share/nvim/lazy/lazy.nvim" + + # Personal mode on Linux was previously only parse-checked, which is why + # linux-extras.sh could ship with an unattended-gpg hang nobody would hit + # until a real install. This actually runs it: the apt repositories, the + # signing keys and the second-run path. + # + # Linux only -- the macOS half of personal mode is casks, which need a real + # session and are not worth a runner. + personal-linux: + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@v5 + + # ubuntu-latest ships the Google Cloud CLI preinstalled, so + # linux-extras.sh short-circuits on `command -v gcloud` and its own + # install path -- dearmor, sources.list, apt install -- never runs. The + # job then "verified" gcloud against the runner's copy, leaving the code + # that carried the unattended-gpg bug untested by the job meant to test it. + - name: Drop the runner's preinstalled gcloud so the installer's path runs + run: | + sudo apt-get remove -y google-cloud-cli || true + sudo rm -f /usr/share/keyrings/cloud.google.gpg \ + /etc/apt/sources.list.d/google-cloud-sdk.list + ! command -v gcloud + + - name: Run installer via bootstrap (personal mode) + run: ./bootstrap.sh personal + + - name: Verify the cask-equivalent tools actually installed + run: | + for brew_bin in /home/linuxbrew/.linuxbrew/bin/brew "$HOME/.linuxbrew/bin/brew"; do + [ -x "$brew_bin" ] && eval "$("$brew_bin" shellenv)" && break + done + # Claude Code goes to ~/.local/bin, which is not on PATH under bash + test -x "$HOME/.local/bin/claude" || { echo "Claude Code missing"; exit 1; } + gcloud --version >/dev/null || { echo "gcloud missing"; exit 1; } + op --version >/dev/null || { echo "1Password CLI missing"; exit 1; } + # Assert the keyrings the installer writes, so a future runner image + # preinstalling one of these cannot hollow this job out silently. + test -f /usr/share/keyrings/cloud.google.gpg \ + || { echo "gcloud keyring missing -- installer path did not run"; exit 1; } + test -f /usr/share/keyrings/1password-archive-keyring.gpg \ + || { echo "1Password keyring missing -- installer path did not run"; exit 1; } + echo "all personal-mode Linux tools installed by our own code path" + + # Removing the packages while LEAVING the keyrings in place is what makes + # this exercise gpg at all. A plain rerun short-circuits on + # `command -v gcloud` and never reaches the dearmor step, so the earlier + # version of this job could not have caught the unattended-gpg bug. + - name: Remove the packages, keep the keyrings + run: | + sudo apt-get remove -y google-cloud-cli 1password-cli + test -f /usr/share/keyrings/cloud.google.gpg + test -f /usr/share/keyrings/1password-archive-keyring.gpg + ! command -v gcloud + ! command -v op + + - name: Rerun over existing keyrings (dearmor must not prompt) + run: timeout 900 ./bootstrap.sh personal + + # linux-extras.sh's exit code is swallowed by `|| warning` in install.sh, + # so assert the outcome directly or a failure here passes silently. + - name: Both tools reinstalled over the existing keyrings + run: | + gcloud --version >/dev/null || { echo "gcloud missing after rerun"; exit 1; } + op --version >/dev/null || { echo "1Password CLI missing after rerun"; exit 1; } + echo "dearmor ran unattended over existing keyrings" diff --git a/Brewfile.core b/Brewfile.core index 9b523a1..ae62c93 100644 --- a/Brewfile.core +++ b/Brewfile.core @@ -29,3 +29,7 @@ brew "neovim" brew "mise" # polyglot version manager (replaces nvm/pyenv/rbenv) brew "tmux" +# --- Dependencies of bin/ scripts (linked onto PATH in both modes) --- +brew "fswatch" # super-sync: file watching +brew "rsync" # super-sync: the sync itself (macOS ships an ancient one) + diff --git a/Brewfile.personal b/Brewfile.personal index 0216dec..663c029 100644 --- a/Brewfile.personal +++ b/Brewfile.personal @@ -1,6 +1,7 @@ # Brewfile.personal - extras installed ONLY in personal mode. -# On Linux the cask lines are skipped (see install.sh); install those -# apps through their vendor instructions instead. +# On Linux the cask lines are skipped (see install.sh); linux-extras.sh then +# installs the same tools from their official Linux channels. The Claude +# desktop app is the exception - see https://code.claude.com/docs/en/desktop-linux # --- AI tooling --- cask "claude-code" # Claude Code CLI diff --git a/README.md b/README.md index e4b5736..201f0bc 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,13 @@ Corporate mode also prompts for your **work** git email, and nothing personal is ### Linux -Packages come from [Homebrew on Linux](https://docs.brew.sh/Homebrew-on-Linux) on both OSes, so there is one manifest with identical tool versions and no `batcat`/`fdfind` renames. apt is only used to bootstrap Homebrew's build dependencies. Casks (iTerm2, the GUI apps in personal mode) are macOS-only and get skipped; JetBrains Mono Nerd Font is installed from the nerd-fonts release into `~/.local/share/fonts` instead. For the stock GNOME Terminal, set that font in your profile and use [catppuccin/gnome-terminal](https://github.com/catppuccin/gnome-terminal) for the colors. The `install` CI workflow runs corporate mode on both Ubuntu and macOS on every PR. +Packages come from [Homebrew on Linux](https://docs.brew.sh/Homebrew-on-Linux) on both OSes, so there is one manifest with identical tool versions and no `batcat`/`fdfind` renames. apt is only used to bootstrap Homebrew's build dependencies. iTerm2 and the other casks are macOS-only and get skipped; JetBrains Mono Nerd Font is installed from the nerd-fonts release into `~/.local/share/fonts` instead. + +**Terminal font is set for you.** `linux-terminal.sh` points the terminal at the nerd font over gsettings, covering Ptyxis (the default from Ubuntu 25.10), GNOME Terminal and Console - the counterpart to the iTerm2 dynamic profile on macOS. A font you chose yourself is left alone; force it with `DOTFILES_TERMINAL_FONT`, which also overrides the font that gets set. Quit the terminal completely afterwards: a running one caches the font list from startup, so a new tab still shows tofu. For colors, Ptyxis ships palettes in its preferences; GNOME Terminal users can use [catppuccin/gnome-terminal](https://github.com/catppuccin/gnome-terminal). + +**Personal mode installs the cask tools too.** Since `cask` lines are stripped on Linux, `linux-extras.sh` installs Claude Code (native installer), the gcloud CLI and the 1Password CLI from their official Linux channels, so personal mode is not quietly thinner than on macOS. The Claude desktop app is the one exception - see [desktop on Linux](https://code.claude.com/docs/en/desktop-linux). + +The `install` CI workflow runs corporate mode on both Ubuntu and macOS on every PR, plus personal mode on Ubuntu - the only coverage for `linux-extras.sh`. ## What's where @@ -71,6 +77,8 @@ Brewfile.macos # macOS-only: iTerm2, nerd font cask, mas Brewfile.personal # extras for personal mode install.sh # installer (brew, symlinks, git identity, secrets, theme caches) macos.sh # opt-in macOS system defaults (--with-macos-defaults) +linux-terminal.sh # sets the nerd font in Ptyxis/GNOME Terminal/Console +linux-extras.sh # Linux installs for the personal-mode cask tools iterm2/ # dynamic profile (Dracula+ colors + nerd font, auto-loaded) zsh/ # .zshrc + antidote plugin list starship/ # prompt config (catppuccin mocha powerline) diff --git a/bin/gifenc b/bin/gifenc index 1cd459e..986b0cf 100755 --- a/bin/gifenc +++ b/bin/gifenc @@ -1,21 +1,45 @@ #!/bin/sh +# High-quality GIF from a video, via a per-clip colour palette. # From http://blog.pkh.me/p/21-high-quality-gif-with-ffmpeg.html +# +# Usage: gifenc [output.gif] FPS=15 gifenc clip.mov +set -eu + +if [ $# -lt 1 ]; then + echo "Usage: $0 [output.gif]" >&2 + exit 1 +fi in="$1" -out="$2" +out="${2:-}" + +[ -f "$in" ] || { echo "No such file: $in" >&2; exit 1; } if [ -z "$out" ]; then - out="$(echo $in | cut -f 1 -d '.' ).gif" + # Strip only the final extension. `cut -f1 -d.` cut at the FIRST dot, so + # my.clip.mov became my.gif, and any dot in a parent directory truncated + # the path outright. + out="${in%.*}.gif" fi -palette="/tmp/palette.png" +# A private directory, not `mktemp -t