These are the dotfiles I use on macOS, Linux, and FreeBSD. make links the configuration of each tool into place. Cendre, the shared color theme, gives the terminal tools the same look. Configuration that belongs to one OS, or that root owns, stays in its own directory.
You need Git, Make, and the applications that you want to configure. The Makefile links configuration. It does not install the applications.
git clone git@github.com:wunki/dotfiles.git
cd dotfiles
makeOn a clean Ubuntu 26.04 desktop, clone this repository and run the workstation bootstrap:
sudo apt update
sudo apt install --yes git
git clone https://github.com/wunki/dotfiles.git ~/Code/wunki/dotfiles
cd ~/Code/wunki/dotfiles
bin/setup-ubuntuIf you want to see the planned work first, run bin/setup-ubuntu --dry-run. The script installs the core terminal workstation, Neovim, an upstream tmux build under ~/dev/tools/tmux, the tools that mise manages, the Fish plugins, and the separate wunki/dot-nvim configuration. You can run the script again at any time. It does not change the login shell.
Fish comes from its official standalone release. gh comes from the official APT repository of GitHub. The bootstrap does not install fonts, terminal emulators, Zed, Sublime Text, Helix, Docker, PostgreSQL, or machine services.
make applies every user-level target. If you want one tool only, run its target:
make fish
make ghostty
make gtk
make helix
make tmux
make piThe Linux system targets need sudo. make never runs them:
make linux # keyd and udev
make auto-suspend # desktop suspend timer| Area | Configuration |
|---|---|
| Shells | Fish and Zsh, split into shared and OS-specific files |
| Desktop and terminal | GTK 4, GNOME Shell, Ghostty, and tmux |
| Editors | Helix, Zed, and Sublime Text |
| CLI tools | Bat, btop, Delta, eza, fzf, Hunk, Lazygit, and mise |
| AI tools | Shared agent instructions and skills, plus Claude Code, Codex, and Pi integration |
| Linux system | keyd remaps, Apple Studio Display access, and automatic suspend |
| Scripts | Tool installers, Neovim URL handling, Wake-on-LAN, and desktop suspend helpers |
Cendre is the shared dark palette for Bat, btop, Delta, eza, fzf, Ghostty, GTK 4, GNOME Shell, Helix, Hunk, Lazygit, Pi, and tmux. The earlier tmux themes stay under tmux/themes/, so you can switch back later.
Most Cendre files come from the generated extras of the theme. This repository does not change them. The shell and Makefile wiring selects the correct file for each tool.
# Shells
make fish
make zsh
# Terminal and editors
make ghostty
make gtk
make helix
make zed
make sublime
make tmux
# CLI tools
make bat
make btop
make delta
make eza
make fzf
make lazygit
make mise
make hunk
# AI tools
make agents
make claude
make codex
make pi
# Scripts in ~/.local/bin
make binIf ~/.config/btop is a real directory and not a symlink, make btop backs it up before it replaces it. make delta enables Delta with Cendre for normal Git diffs. It also adds git hdiff and git hshow, which open a review in Hunk. If the legacy global diff.external=difft override is present, the target removes it. LazyGit keeps its own Difftastic command.
make gtk links the GTK 4 overrides into ~/.config/gtk-4.0/gtk.css. On Linux, it also links the GNOME Shell theme into ~/.themes/cendre. It selects Cendre through the User Themes extension and keeps the rest of the enabled extensions. The Ubuntu bootstrap installs that extension. After the first installation of the extension, log out once before you expect the shell theme to appear. Restart GTK 4 applications to update their window chrome.
Install the tools that you use before you apply the shell configuration.
# macOS
brew install eza bat fzf zoxide tree autossh mise gitu
# Debian/Ubuntu
sudo apt install eza bat fzf zoxide tree autossh git gh tmux
# Fedora
sudo dnf install eza bat fzf zoxide tree autossh git gh tmux
# Arch
sudo pacman -S eza bat fzf zoxide tree autossh github-cli tmuxInstall Fisher, the Fish plugin manager. Then install the plugins listed in fish/fish_plugins:
curl -sL https://raw.githubusercontent.com/jorgebucaran/fisher/main/functions/fisher.fish | source
fisher updateRun tide configure and select the Lean layout with 16 colors. The terminal theme then controls the palette.
Install the external tools and clone the plugins:
brew install mise gitu
mkdir -p ~/.zsh
git clone https://github.com/zsh-users/zsh-autosuggestions.git ~/.zsh/zsh-autosuggestions
git clone https://github.com/zdharma-continuum/fast-syntax-highlighting.git ~/.zsh/fast-syntax-highlighting
git clone https://github.com/hlissner/zsh-autopair.git ~/.zsh/zsh-autopair
git clone https://github.com/sindresorhus/pure.git ~/.zsh/pureGlobal npm CLIs use the npm backend of mise, not the deprecated default package files of Node. The CLIs then keep working when Node upgrades.
mise use -g \
'npm:hunkdiff@latest' \
'npm:@earendil-works/pi-coding-agent@latest'
mise install
mise reshimThe shared instructions and skills live in agents/. The petar-writing skill describes the voice used on petar.dev. It removes generic AI phrasing and does not invent personal context.
agents/
AGENTS.md shared working instructions
skills/ shared Agent Skills
~/.agents linked by make agents
~/.claude/skills linked by make claude
~/.codex/AGENTS.md linked by make codex
~/.pi/agent assembled by make pi
Pi keeps its configuration, models, extensions, and themes under pi/agent/. Claude and Codex receive only the shared pieces that they support.
The repository includes skills for requirements gathering, structural search, code simplification, technical and personal writing, PR descriptions, developer logs, tutoring, Elixir dependency updates, atomic commits, fetching GitHub files, and rendering reMarkable PDFs. The directory names under agents/skills/ are the source of truth.
CleanShot X copies a local macOS path together with the screenshot. That path does not exist on an SSH host. cc-clip sends the clipboard image through an SSH reverse tunnel instead. The tmux binding then writes the image to a real remote path and pastes that path into the active pane.
Install the local pieces on the Mac:
brew install pngpaste
curl -fsSL https://raw.githubusercontent.com/ShunmeiCho/cc-clip/main/scripts/install.sh | sh
cc-clip service installAdd the tunnel to the named host in ~/.ssh/config:
Host desktop
RemoteForward 18339 127.0.0.1:18339
ControlMaster no
ControlPath noneDeploy only the remote clipboard transport. The --claude target installs the xclip shim that Claude Code and Pi use. It does not make the tmux path specific to Claude. --no-notify skips the agent notification integrations. cc-clip still creates a session ID that it uses for image-transfer notifications. If you want all notifications off, remove that file after every connect or redeploy:
cc-clip connect desktop --claude --no-notify
ssh desktop 'rm -f ~/.cache/cc-clip/session.id ~/.cache/cc-clip/notify.nonce'Apply the tmux configuration on the remote:
ssh desktop
cd ~/Code/wunki/dotfiles
make tmux
tmux source-file ~/.tmux.confCopy an image in CleanShot X. Then press Ctrl-h Shift-I inside the remote tmux session. The binding is active when tmux has SSH_CONNECTION. It runs cc-clip paste --out-dir /tmp/screenshots and pastes the remote path into the current pane. Codex and other CLIs that accept image paths can read the file without Xvfb or DISPLAY. Pi can also use its normal Ctrl+V path through the installed xclip shim.
The Mac daemon listens only on 127.0.0.1:18339. SSH exposes it on the loopback interface of the remote. The token in ~/.cache/cc-clip/session.token authenticates each request. cc-clip does not select the server. The SSH Host block creates the tunnel, and the cc-clip paste process writes the file on the remote host that runs it.
Keep an SSH connection open while you use the binding. A tmux session can survive a disconnect, but the reverse tunnel cannot. The first live SSH connection owns port 18339. A second connection can report remote port forwarding failed for listen port 18339 and still use the tunnel of the first connection.
Neovim itself is configured in a separate repository. This repository contains the macOS and tmux integration that opens a stack trace from the browser in the correct Neovim process.
nvs starts Neovim with a socket at /tmp/nvim-$SESSION_NAME. A registered nvim:// handler sends the file to that socket, selects the matching tmux pane, and focuses Ghostty. If the socket is not available, the handler opens a new Ghostty window instead.
Apply the scripts first:
make binThe URL handler app lives at ~/Applications/Nvim URL Handler.app and must register the nvim URL scheme. Its AppleScript source is bin/NvimURLHandler.applescript. The Makefile does not create the app and does not register the URL.
Add the editor URL to the .mise.toml of the project:
[env]
_.source = "~/.local/bin/mise-tmux-session"
PLUG_EDITOR = "nvim://file/__FILE__:__LINE__?tmux-session={{ env.TMUX_SESSION_NAME }}"Start Neovim with nvs inside tmux.
This section records the keyboard repeat, mouse movement, and scrolling values that I use on the Ubuntu workstation.
GNOME stores the keyboard repeat values in dconf, its settings database, and not in a dotfile. You must apply these values on each machine.
| Key | Meaning |
|---|---|
repeat-interval |
Milliseconds between repeated characters. A lower value is faster. Use 1 or higher, because 0 can break the Wayland login. |
delay |
Delay before the repeat starts. If normal taps produce double characters, raise it. |
My current values give about 55 repeated characters per second, and normal taps stay reliable:
gsettings set org.gnome.desktop.peripherals.keyboard repeat-interval 18
gsettings set org.gnome.desktop.peripherals.keyboard delay 200The changes apply at once. The GNOME defaults are repeat-interval 30 and delay 500.
GNOME shows notification banners at the top center and has no setting to move them. The Just Perfection extension adds one and stores it in dconf. Install the extension on each machine, then apply the setting.
gsettings --schemadir ~/.local/share/gnome-shell/extensions/just-perfection-desktop@just-perfection/schemas \
set org.gnome.shell.extensions.just-perfection notification-banner-position 2The values are 0 top left, 1 top center (default), 2 top right, 3 bottom left, 4 bottom center, and 5 bottom right. The change applies at once.
Let the Linux HID++ driver manage the wheel resolution of the MX Master 4. If you force Scroll Wheel Resolution on in Solaar, the movement is smooth but much too fast. Solaar and the kernel then both try to control the same device setting.
Install Solaar, the Logitech device manager for Linux. Then refresh the permissions of the Bolt receiver that is already connected. If you can disconnect the mouse safely, you can also unplug and replug the receiver after the installation. The result is the same.
sudo apt install solaar
sudo udevadm control --reload-rules
sudo udevadm trigger --action=add --subsystem-match=hidraw
udevadm settleOpen Solaar. Click the icon at the right edge of Scroll Wheel Resolution until its mode reads Ignore this setting. This is the default for new devices. After you change the mode, turn the mouse off and on, so that the kernel can reset the wheel.
Make sure that Solaar will not touch the setting:
rg _sensitive ~/.config/solaar/config.yamlThe result must include hires-smooth-resolution: ignore. The nearby hires-smooth-resolution: true or false line records the last value that Solaar saw. It does not override the device while the sensitivity mode is ignore.
Firefox already enables smooth scrolling and mass-spring-damper physics. Keep mousewheel.default.delta_multiplier_y at its default value of 100. A higher value makes each wheel movement travel farther. If scrolling becomes too fast again, make sure that the Solaar mode is still ignore, turn the mouse off and on, and reset that Firefox preference from about:config.
The current baseline keeps pointer movement responsive on the 5K display and does not change the wheel behavior:
| Setting | Value | Reason |
|---|---|---|
| Connection | Logi Bolt receiver | Exposes the mouse to Solaar and avoids Bluetooth-specific configuration differences. |
| Sensitivity | 1200 DPI | A 20% increase over the 1000 DPI default of Logitech. Set Sensitivity (DPI) in Solaar. |
| GNOME acceleration | Default profile, speed 0.573 |
Keeps adaptive acceleration and fine control. Raise the DPI before you push the desktop speed slider further. |
| Natural scrolling | On | Matches the macOS content direction. |
| Wheel resolution | Ignore in Solaar | Leaves smooth scrolling to the Linux HID++ driver. |
| Wheel mode | Ratcheted, torque 75, switch speed 12 |
Keeps clear steps and keeps the physical free-spin toggle. These values are about feel, not tracking accuracy. |
| Wheel diversion | Off | Sends standard wheel events directly to applications. |
| Haptic feedback | 60 |
Current preference. It does not affect pointer or wheel tracking. |
Logitech supports 200 to 8000 DPI in steps of 50 DPI. When you tune further, change the DPI in steps of 50 to 100 points. Change one layer at a time, so that you can judge the result.
Most targets link files into the home directory of the current user. The Linux system configuration lives under linux/ and has its own installation targets, because it writes to /etc or /usr/local.
linux/
keyd/default.conf -> /etc/keyd/default.conf
systemd/auto-suspend-monitor/auto-suspend-monitor -> /usr/local/sbin/auto-suspend-monitor
systemd/auto-suspend-monitor/auto-suspend-monitor.* -> /etc/systemd/system/
udev/50-apple-studio-display.rules -> /etc/udev/rules.d/50-apple-studio-display.rules
make linux # keyd and udev
make keyd # keyboard remaps and Studio Display brightness keys
make udev # stable Studio Display device and user access
make auto-suspend # automatic desktop suspendmake auto-suspend installs a systemd timer that checks for activity once per minute. If there is no active session for 30 minutes, it suspends the desktop.
Active SSH, Tailscale SSH, Zed remote, console, and non-idle graphical sessions reset the countdown. Sessions stuck in closing do not. Sleep and idle inhibitors prevent the suspend.
Before the suspend, the monitor writes ~/.cache/dotfiles-desktop-sleep/slept-at. sleep-desktop writes the same timestamp when you request the suspend by hand. wake-desktop reads and removes it after the machine returns, and reports how long the machine slept.
When the machine runs Windows with WSL2, sleep-wsl calls the Windows suspend API from an SSH session inside WSL. wake-wsl sends Wake-on-LAN through the Pi, waits for WSL SSH, and reports how long the machine slept. Windows must expose S3 sleep, and its Ethernet adapter must be armed for wake.
The installer needs shellcheck, Python 3, and runuser. It validates the monitor, installs root-owned copies, reloads systemd, and enables the timer.
systemctl status auto-suspend-monitor.timer
journalctl -t auto-suspend-monitorThe Studio Display has no /sys/class/backlight device and does not support DDC/CI. Brightness is available through the USB HID protocol of Apple with asdcontrol, installed at /usr/local/bin/asdcontrol.
bin/asd-brightnesssupportsup,down, andget, with a step of 6%.linux/udev/50-apple-studio-display.rulesexposes/dev/apple-studio-displaywith access for theusersgroup. It targets USB interface 07, the brightness HID of the display.linux/keyd/default.confmaps theF15andF14events of the keyboard toasd-brightness. This works outside the desktop environment and does not needsudoat runtime.
Install both pieces with make keyd udev or make linux. To change the step size, change STEP in bin/asd-brightness.
Make sure that you can write to the destination, then run the target again. Some targets back up an existing real directory instead of replacing it without notice.
Install Fisher, run fisher update, and restart Fish.
Make sure that the configuration directory of the tool points into this repository. For Bat, run bat cache --build after you add or update the theme.
Run status buildinfo and compare its target with the cases in fish/config.fish.
MIT. See LICENSE.