cmd / laptop

I maintain a laptop repo that configures a macOS machine as a software development environment.

Install

export LAPTOP="$HOME/laptop"
git clone https://github.com/croaky/laptop.git $LAPTOP
cd $LAPTOP
./laptop.sh

The script links the dotfiles to the main clone, ~/laptop, also when I run it from a change worktree. It asks git for the common git directory of its checkout. For a worktree, the parent of that directory is the main clone. So the links stay after the merge deletes the worktree. A change to a dotfile takes effect after the merge and a git pull in ~/laptop.

What it sets up

The script is idempotent. I run it on macOS on Apple Silicon. It:

macOS settings

The script turns off all four hot corners. A hot corner set to Mission Control opens when a drag crosses a display corner. A dragged screenshot then cannot reach its window. The F3 key still opens Mission Control.

The script also turns off "Click wallpaper to reveal desktop". When this setting is on, a click on the wallpaper moves all windows to the screen edges. A screenshot drag that starts on the wallpaper or crosses it can do this.

The script writes each setting only when it has a different value. A change in System Settings stays until the next run of laptop.sh.

Window shortcuts

Every macOS app has Left, Right, and Fill in its "Window > Move & Resize" menu. The script gives them keys as App Shortcuts, so I use no window manager app:

defaults write -g NSUserKeyEquivalents -dict-add \
  "Left" '^~\UF702' \
  "Right" '^~\UF703' \
  "Fill" '^~\U21A9'

^ is Control and ~ is Option. defaults reads \UF702, \UF703, and \U21A9 as the left arrow, right arrow, and return keys. An app reads the shortcuts when it starts. Fill does not go back to the old size. "Window > Move & Resize > Return to Previous Size" does that.

Editor formatting

Neovim formats Go with goimports and SQL with pgfmt on save. It runs the version that the file's module pins, the same version that the repo's checks run (see go / checks). So a version bump in a repo needs no change in this repo.

For SQL, Neovim runs go tool pgfmt in the directory of the file's go.mod. A module that does not pin pgfmt gets no SQL formatting on save. For Go, a script runs go tool -modfile=tools.mod goimports in a library and go tool goimports in an app. A module that pins neither gets the goimports that this script installs.

The repo commits Neovim's lazy-lock.json, so each machine loads the same plugin commits.

No JavaScript runtime

The machine has no node, npm, or bun. tsgo, dprint, esbuild's Go API, and native language servers cover what those ran. See web / no js runtime for the native toolchain.

Hosts file

hostctl manages /etc/hosts for local development and DNS-level ad blocking. laptop.sh runs it.

Local apps

Browsers treat http://localhost and http://*.localhost as potentially trustworthy origins, so Secure Context features work without TLS.

I run apps on different subdomains and ports (site.localhost:2000, htmz.localhost:2002) to test cross-origin security:

Ad blocking

A hosts file blocks ads and trackers across every app on the laptop. hostctl enables blocking by default:

hostctl

To disable blocking while keeping local app entries:

hostctl --unblock

Core script logic:

#!/bin/bash
set -euo pipefail

custom_hosts_entries() {
  cat <<EOF
# macOS defaults
255.255.255.255 broadcasthost

# local apps
127.0.0.1 site.localhost   # :2000
127.0.0.1 htmz.localhost   # :2002
EOF
}

if [[ "${1:-}" == "--unblock" ]]; then
  cat <<EOF | sudo tee /etc/hosts >/dev/null
127.0.0.1 localhost
::1 localhost

$(custom_hosts_entries)
EOF
else
  curl -s https://winhelp2002.mvps.org/hosts.txt | tr -d '\r' | sudo tee /etc/hosts >/dev/null
  (echo && custom_hosts_entries) | sudo tee -a /etc/hosts >/dev/null
fi
sudo killall -HUP mDNSResponder

The script holds the local app entries, so no other file has a copy.

Server configuration

A Go server binds to a port, and the hosts file resolves the subdomain:

fmt.Println("Serving at http://site.localhost:2000")
http.ListenAndServe(":2000", handler)

I use the 2000 port range for Go servers and 3000 for Ruby servers.

← All articles