Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions .github/workflows/install.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
4 changes: 4 additions & 0 deletions Brewfile.core
Original file line number Diff line number Diff line change
Expand Up @@ -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)

5 changes: 3 additions & 2 deletions Brewfile.personal
Original file line number Diff line number Diff line change
@@ -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
Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Expand Down
38 changes: 31 additions & 7 deletions bin/gifenc
Original file line number Diff line number Diff line change
@@ -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 <input> [output.gif] FPS=15 gifenc clip.mov
set -eu

if [ $# -lt 1 ]; then
echo "Usage: $0 <input> [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 <template>`: GNU substitutes the X's and
# keeps the .png suffix, but BSD/macOS treats the whole argument as a prefix and
# appends its own suffix, leaving a file that does not end in .png -- ffmpeg
# infers the palettegen muxer from the extension, so it would fail there.
# A fixed name inside a private directory sidesteps the difference, and also
# fixes the original /tmp/palette.png, which raced between concurrent runs and
# was a predictable-name write on multi-user machines.
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT INT TERM
palette="$tmpdir/palette.png"

fps=${FPS:-30}

filters="fps=$fps,scale=500:-1:flags=lanczos"

ffmpeg -v warning -i $in -vf "$filters,palettegen" -y $palette
ffmpeg -v warning -i $in -i $palette -lavfi "$filters [x]; [x][1:v] paletteuse" -y $out
# Everything quoted: this script exists for screen recordings, whose filenames
# reliably contain spaces ("Screen Recording 2026-08-05 at 10.15.32.mov").
ffmpeg -v warning -i "$in" -vf "$filters,palettegen" -y "$palette"
ffmpeg -v warning -i "$in" -i "$palette" -lavfi "$filters [x]; [x][1:v] paletteuse" -y "$out"

echo
ls -lh $out
ls -lh "$out"
Loading