feat: shell + dotfiles setup for a fresh box

Two layers with a deliberate privilege split: setup.sh does the root work
(apt, rustup, cargo tools, starship, chsh) and hands off to chezmoi, which
owns everything under $HOME.

Tools are built with `cargo install --locked` to match the versions currently
running rather than whatever apt ships, at the cost of a 10-20 min cold setup.

Excluded on purpose: the atuin sync key and ~/.git-credentials (secrets),
~/.claude/settings.json (per-box decision), fnm/opencode conf.d (out of scope),
and fish_variables (machine-local).

chezmoi tracks only the executable and private attributes and derives modes
from the umask, so setup.sh pins umask 022 and atuin uses private_ to keep
0700/0600. Without both, a 002 umask silently relaxes them to 0775/0664.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 10:53:46 +02:00
commit 893ec8e3cb
12 changed files with 928 additions and 0 deletions

1
.chezmoiroot Normal file
View File

@@ -0,0 +1 @@
home

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
# Secrets — never commit
.git-credentials
secrets.yml
*.key
# Editor / OS noise
*.swp
.DS_Store

93
README.md Normal file
View File

@@ -0,0 +1,93 @@
# dotfiles
Shell + tooling setup for a fresh Ubuntu box: **fish · starship · atuin · eza · macchina**,
plus the Claude Code statusline.
Two layers, deliberately split:
| Layer | Owns | Tool |
|---|---|---|
| Provisioning | apt packages, rust toolchain, starship, default shell | `setup.sh` (root) |
| Dotfile state | everything under `$HOME` | chezmoi (user) |
`setup.sh` does the root work and then hands off to chezmoi. Nothing that needs
`sudo` lives inside chezmoi, and nothing under `$HOME` is touched by the script.
## Fresh box
```bash
git clone https://git.domverse-berlin.eu/domverse/dotfiles.git
cd dotfiles
sudo bash setup.sh
```
Run it as your normal user via `sudo` — the script reads `SUDO_USER` to know whose
home to set up, and refuses to run as root directly.
Expect **1020 minutes**: atuin, eza, macchina and oxker are built from source with
`cargo install --locked`, which needs the rust toolchain. That's the deliberate
trade for matching the currently-running versions exactly rather than taking
whatever apt ships.
## Day-to-day
Configs are managed by chezmoi, so the file in `~/.config` is a *copy*, not a
symlink. Edit through chezmoi or your change gets overwritten on the next apply:
```bash
chezmoi edit ~/.config/fish/config.fish # edit source
chezmoi diff # what drifted on this box?
chezmoi apply # write source -> $HOME
chezmoi cd # jump to the source repo, then git push
```
The `fishconfig` alias is wired to `chezmoi edit` for this reason.
## What's tracked
```
home/
dot_bashrc ~/.bashrc (stock + atuin init)
dot_gitconfig ~/.gitconfig
dot_config/fish/config.fish.tmpl aliases, eza, init lines
dot_config/fish/conf.d/rustup.fish sources ~/.cargo/env.fish
dot_config/atuin/config.toml
dot_config/starship.toml stock-equivalent, pinned
dot_claude/executable_statusline-command.sh statusline (needs jq)
```
## What's deliberately NOT tracked
See `home/.chezmoiignore`. The load-bearing exclusions:
- **`~/.local/share/atuin/key`** — the sync encryption key. Tracked by hand in
`secrets.yml`. Restore it *before* the first `atuin sync`, or history encrypted
on other hosts will not decrypt.
- **`~/.git-credentials`** — plaintext tokens (`.gitconfig` sets `helper = store`).
- **`~/.claude/settings.json`** — pins the model and sets
`skipDangerousModePermissionPrompt`; a per-box decision, set by hand.
- **`conf.d/fnm.fish`, `conf.d/dtop.env.fish`** — node/opencode toolchains, out of
scope. Their installers write these themselves.
- **`fish_variables`** — machine-local state, fish rewrites it.
## Manual steps after setup.sh
The script prints these; repeated here because they're easy to miss:
1. **atuin** — restore key from `secrets.yml` into `~/.local/share/atuin/key`, then
`atuin login -u <user>` and `atuin sync`.
2. **git identity** — not set globally, only `credential.helper=store`:
```bash
git config --global user.name '<name>'
git config --global user.email '<email>'
```
3. **Log out and back in** for the fish shell change to take effect.
## Notes
- `config.fish` aliases `cat` to `batcat` — on Ubuntu the bat binary really is
`batcat`, so the apt `bat` package is a hard dependency of the fish config.
- rustup is installed with `--no-modify-path`: `conf.d/rustup.fish` already sources
`~/.cargo/env.fish`, and the installer would otherwise add a second PATH edit.
- `starship.toml` is currently equivalent to stock defaults. It exists so the prompt
is pinned to a known config rather than tracking version defaults.

27
home/.chezmoiignore Normal file
View File

@@ -0,0 +1,27 @@
# Never manage these — they are secrets or machine-local runtime state.
# chezmoi will neither read them into source state nor write them to the target.
# Git credential store (plaintext tokens — .gitconfig sets helper=store)
.git-credentials
# Atuin runtime state + sync encryption key.
# The key is tracked by hand in secrets.yml; restore with `atuin login`.
.local/share/atuin/**
# Claude runtime state — history, per-project memory, session data.
# Only statusline-command.sh is managed. settings.json is deliberately NOT
# tracked: it pins the model and sets skipDangerousModePermissionPrompt, which
# is a per-box decision. Set it by hand on each machine.
.claude/projects/**
.claude/todos/**
.claude/statsig/**
.claude/.caveman-active
.claude/settings.json
.claude/settings.local.json
# Fish machine-local state (generated by fish itself)
.config/fish/fish_variables
# Out of scope for this repo (installed by their own installers, if at all)
.config/fish/conf.d/fnm.fish
.config/fish/conf.d/dtop.env.fish

132
home/dot_bashrc Normal file
View File

@@ -0,0 +1,132 @@
# ~/.bashrc: executed by bash(1) for non-login shells.
# see /usr/share/doc/bash/examples/startup-files (in the package bash-doc)
# for examples
# If not running interactively, don't do anything
case $- in
*i*) ;;
*) return;;
esac
# don't put duplicate lines or lines starting with space in the history.
# See bash(1) for more options
HISTCONTROL=ignoreboth
# append to the history file, don't overwrite it
shopt -s histappend
# for setting history length see HISTSIZE and HISTFILESIZE in bash(1)
HISTSIZE=1000
HISTFILESIZE=2000
# check the window size after each command and, if necessary,
# update the values of LINES and COLUMNS.
shopt -s checkwinsize
# If set, the pattern "**" used in a pathname expansion context will
# match all files and zero or more directories and subdirectories.
#shopt -s globstar
# make less more friendly for non-text input files, see lesspipe(1)
[ -x /usr/bin/lesspipe ] && eval "$(SHELL=/bin/sh lesspipe)"
# set variable identifying the chroot you work in (used in the prompt below)
if [ -z "${debian_chroot:-}" ] && [ -r /etc/debian_chroot ]; then
debian_chroot=$(cat /etc/debian_chroot)
fi
# set a fancy prompt (non-color, unless we know we "want" color)
case "$TERM" in
xterm-color|*-256color) color_prompt=yes;;
esac
# uncomment for a colored prompt, if the terminal has the capability; turned
# off by default to not distract the user: the focus in a terminal window
# should be on the output of commands, not on the prompt
#force_color_prompt=yes
if [ -n "$force_color_prompt" ]; then
if [ -x /usr/bin/tput ] && tput setaf 1 >&/dev/null; then
# We have color support; assume it's compliant with Ecma-48
# (ISO/IEC-6429). (Lack of such support is extremely rare, and such
# a case would tend to support setf rather than setaf.)
color_prompt=yes
else
color_prompt=
fi
fi
if [ "$color_prompt" = yes ]; then
PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '
else
PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ '
fi
unset color_prompt force_color_prompt
# If this is an xterm set the title to user@host:dir
case "$TERM" in
xterm*|rxvt*)
PS1="\[\e]0;${debian_chroot:+($debian_chroot)}\u@\h: \w\a\]$PS1"
;;
*)
;;
esac
# enable color support of ls and also add handy aliases
if [ -x /usr/bin/dircolors ]; then
test -r ~/.dircolors && eval "$(dircolors -b ~/.dircolors)" || eval "$(dircolors -b)"
alias ls='ls --color=auto'
#alias dir='dir --color=auto'
#alias vdir='vdir --color=auto'
alias grep='grep --color=auto'
alias fgrep='fgrep --color=auto'
alias egrep='egrep --color=auto'
fi
# colored GCC warnings and errors
#export GCC_COLORS='error=01;31:warning=01;35:note=01;36:caret=01;32:locus=01:quote=01'
# some more ls aliases
alias ll='ls -alF'
alias la='ls -A'
alias l='ls -CF'
# Add an "alert" alias for long running commands. Use like so:
# sleep 10; alert
alias alert='notify-send --urgency=low -i "$([ $? = 0 ] && echo terminal || echo error)" "$(history|tail -n1|sed -e '\''s/^\s*[0-9]\+\s*//;s/[;&|]\s*alert$//'\'')"'
# Alias definitions.
# You may want to put all your additions into a separate file like
# ~/.bash_aliases, instead of adding them here directly.
# See /usr/share/doc/bash-doc/examples in the bash-doc package.
if [ -f ~/.bash_aliases ]; then
. ~/.bash_aliases
fi
# enable programmable completion features (you don't need to enable
# this, if it's already enabled in /etc/bash.bashrc and /etc/profile
# sources /etc/bash.bashrc).
if ! shopt -oq posix; then
if [ -f /usr/share/bash-completion/bash_completion ]; then
. /usr/share/bash-completion/bash_completion
elif [ -f /etc/bash_completion ]; then
. /etc/bash_completion
fi
fi
. "$HOME/.local/bin/env"
. "$HOME/.cargo/env"
[[ -f ~/.bash-preexec.sh ]] && source ~/.bash-preexec.sh
eval "$(atuin init bash)"
# Starship prompt (interactive only)
case $- in
*i*) eval "$(starship init bash)" ;;
esac
# bun
export BUN_INSTALL="$HOME/.bun"
export PATH="$BUN_INSTALL/bin:$PATH"

View File

@@ -0,0 +1,143 @@
#!/usr/bin/env bash
# Claude Code status line - based on PS1 from ~/.bashrc
# Format: user@host:cwd | model | ctx: X% | 5h:X%↻Y | 7d:X%↻Y [CAVEMAN]
input=$(cat)
# Normalize reset timestamp: epoch int -> "@<epoch>", ISO string -> as-is
norm_ts() {
local v=$1
[ -z "$v" ] || [ "$v" = "null" ] && return
case "$v" in
''|*[!0-9]*) printf '%s' "$v" ;; # non-numeric => ISO
*) printf '@%s' "$v" ;; # all-digits => epoch
esac
}
model=$(echo "$input" | jq -r '.model.display_name // empty')
used=$(echo "$input" | jq -r '.context_window.used_percentage // empty')
five_h_pct=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty')
five_h_reset=$(echo "$input" | jq -r '.rate_limits.five_hour.resets_at // empty')
five_h_reset=$(norm_ts "$five_h_reset")
seven_d_pct=$(echo "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty')
seven_d_reset=$(echo "$input" | jq -r '.rate_limits.seven_day.resets_at // empty')
seven_d_reset=$(norm_ts "$seven_d_reset")
# PS1-derived portion: bold green user@host, reset, colon, bold blue cwd, reset
user=$(whoami)
host=$(hostname -s)
cwd=$(echo "$input" | jq -r '.workspace.current_dir // empty')
[ -z "$cwd" ] && cwd=$(pwd)
prompt_part=$(printf '\033[01;32m%s@%s\033[00m:\033[01;34m%s\033[00m' "$user" "$host" "$cwd")
# Color by usage pct: green <50, yellow 50-75, orange 75-90, red >=90
color_for_pct() {
local pct=$1
if [ "$pct" -ge 90 ]; then printf '\033[31m'
elif [ "$pct" -ge 75 ]; then printf '\033[38;5;172m'
elif [ "$pct" -ge 50 ]; then printf '\033[38;5;227m'
else printf '\033[32m'
fi
}
# Countdown ISO timestamp -> now (GNU date)
countdown() {
local reset_ts=$1
[ -z "$reset_ts" ] && return
local reset_epoch now_epoch diff_s
reset_epoch=$(date -d "$reset_ts" +%s 2>/dev/null) || return
now_epoch=$(date +%s)
diff_s=$((reset_epoch - now_epoch))
[ "$diff_s" -le 0 ] && printf "now" && return
local days=$((diff_s / 86400))
local hours=$(( (diff_s % 86400) / 3600 ))
local mins=$(( (diff_s % 3600) / 60 ))
if [ "$days" -gt 0 ]; then printf "%dd%dh" "$days" "$hours"
elif [ "$hours" -gt 0 ]; then printf "%dh%02dm" "$hours" "$mins"
else printf "%dm" "$mins"
fi
}
# Format reset timestamp as local clock time. fmt: GNU date format string
reset_clock() {
local reset_ts=$1
local fmt=$2
[ -z "$reset_ts" ] && return
date -d "$reset_ts" "+$fmt" 2>/dev/null
}
# Window elapsed pct: (window - time_until_reset) / window * 100, clamp 0-100
elapsed_pct() {
local reset_ts=$1
local window_s=$2
[ -z "$reset_ts" ] && return
local reset_epoch now_epoch diff_s elapsed
reset_epoch=$(date -d "$reset_ts" +%s 2>/dev/null) || return
now_epoch=$(date +%s)
diff_s=$((reset_epoch - now_epoch))
elapsed=$(( (window_s - diff_s) * 100 / window_s ))
[ "$elapsed" -lt 0 ] && elapsed=0
[ "$elapsed" -gt 100 ] && elapsed=100
printf '%d' "$elapsed"
}
# Claude info portion
info_parts=""
[ -n "$model" ] && info_parts="$model"
if [ -n "$used" ]; then
used_int=$(printf '%.0f' "$used")
[ -n "$info_parts" ] && info_parts="$info_parts | " || true
info_parts="${info_parts}ctx: ${used_int}%"
fi
# Rate limit segments (colored inside dim brackets via reset+recolor trick)
rate_segment=""
DIM='\033[02m'
PINK='\033[38;5;213m' # ~#EE6FF8, used for elapsed-pace pct
if [ -n "$five_h_pct" ]; then
five_int=${five_h_pct%.*}
five_color=$(color_for_pct "$five_int")
five_cd=$(countdown "$five_h_reset")
five_el=$(elapsed_pct "$five_h_reset" 18000)
five_clk=$(reset_clock "$five_h_reset" '%-l%P')
seg="${five_color}5h"
[ -n "$five_clk" ] && seg="${seg}(${five_clk})"
seg="${seg}:${five_int}%"
[ -n "$five_el" ] && seg="${seg}/${PINK}${five_el}%${five_color}"
[ -n "$five_cd" ] && seg="${seg}${five_cd}"
rate_segment="${rate_segment} | ${seg}\033[0m${DIM}"
fi
if [ -n "$seven_d_pct" ]; then
seven_int=${seven_d_pct%.*}
seven_color=$(color_for_pct "$seven_int")
seven_cd=$(countdown "$seven_d_reset")
seven_el=$(elapsed_pct "$seven_d_reset" 604800)
seven_clk=$(reset_clock "$seven_d_reset" '%a %-l%P')
seg="${seven_color}7d"
[ -n "$seven_clk" ] && seg="${seg}(${seven_clk})"
seg="${seg}:${seven_int}%"
[ -n "$seven_el" ] && seg="${seg}/${PINK}${seven_el}%${seven_color}"
[ -n "$seven_cd" ] && seg="${seg}${seven_cd}"
rate_segment="${rate_segment} | ${seg}\033[0m${DIM}"
fi
if [ -n "$info_parts" ] || [ -n "$rate_segment" ]; then
printf '%s \033[02m[%s' "$prompt_part" "$info_parts"
[ -n "$rate_segment" ] && printf '%b' "$rate_segment"
printf ']\033[00m'
else
printf '%s' "$prompt_part"
fi
#caveman
FLAG="$HOME/.claude/.caveman-active"
[ ! -f "$FLAG" ] && exit 0
MODE=$(cat "$FLAG" 2>/dev/null)
if [ -z "$MODE" ]; then
printf '\033[38;5;172m[CAVEMAN]\033[0m'
else
SUFFIX=$(echo "$MODE" | tr '[:lower:]' '[:upper:]')
printf '\033[38;5;172m[CAVEMAN:%s]\033[0m' "$SUFFIX"
fi

View File

@@ -0,0 +1 @@
source "$HOME/.cargo/env.fish"

View File

@@ -0,0 +1,33 @@
set -g fish_greeting ""
if status is-interactive
# Commands to run in interactive sessions can go here
atuin init fish | source
macchina
starship init fish | source
end
set -gx PATH "$HOME/.cargo/bin" $PATH
#FISH Aliases
alias fishconfig='chezmoi edit ~/.config/fish/config.fish'
alias resource='source ~/.config/fish/config.fish'
alias install='sudo apt install -y'
alias remove='sudo apt remove -y'
# Ghostty Term
alias nanox="TERM=xterm-256color nano"
# EZA Aliases
# Main eza alias - beautiful tree view with all details
alias l='eza --all --long --header --icons --git --group-directories-first --time-style=long-iso --color=always --hyperlink --octal-permissions --no-quotes'
alias ll='l -T -L 2'
# Tree view with 2 levels depth (perfect for project overview)
alias lt='eza --all --long --tree --level=2 --header --icons --git --group-directories-first --time-style=long-iso --color=always --hyperlink --git-ignore'
# Full tree view (careful in large directories!)
alias ltree='eza --all --long --tree --header --icons --git --group-directories-first --time-style=long-iso --color=always --hyperlink'
# Include git-ignored files (show everything, even ignored)
alias la='eza --all --long --header --icons --git --group-directories-first --time-style=long-iso --color=always --hyperlink --no-git-ignore --git-repos'
# apt update & upgrade
alias patch='sudo apt update && sudo apt upgrade -y'
alias cat='batcat'
# opencode (only if installed — not managed by this repo)
if test -d {{ .chezmoi.homeDir }}/.opencode/bin
fish_add_path {{ .chezmoi.homeDir }}/.opencode/bin
end

View File

@@ -0,0 +1,289 @@
## where to store your database, default is your system data directory
## linux/mac: ~/.local/share/atuin/history.db
## windows: %USERPROFILE%/.local/share/atuin/history.db
# db_path = "~/.history.db"
## where to store your encryption key, default is your system data directory
## linux/mac: ~/.local/share/atuin/key
## windows: %USERPROFILE%/.local/share/atuin/key
# key_path = "~/.key"
## where to store your auth session token, default is your system data directory
## linux/mac: ~/.local/share/atuin/session
## windows: %USERPROFILE%/.local/share/atuin/session
# session_path = "~/.session"
## date format used, either "us" or "uk"
# dialect = "us"
## default timezone to use when displaying time
## either "l", "local" to use the system's current local timezone, or an offset
## from UTC in the format of "<+|->H[H][:M[M][:S[S]]]"
## for example: "+9", "-05", "+03:30", "-01:23:45", etc.
# timezone = "local"
## enable or disable automatic sync
# auto_sync = true
## enable or disable automatic update checks
# update_check = true
## address of the sync server
# sync_address = "https://api.atuin.sh"
## how often to sync history. note that this is only triggered when a command
## is ran, so sync intervals may well be longer
## set it to 0 to sync after every command
# sync_frequency = "10m"
## which search mode to use
## possible values: prefix, fulltext, fuzzy, skim
# search_mode = "fuzzy"
## which filter mode to use by default
## possible values: "global", "host", "session", "session-preload", "directory", "workspace"
## consider using search.filters to customize the enablement and order of filter modes
# filter_mode = "global"
## With workspace filtering enabled, Atuin will filter for commands executed
## in any directory within a git repository tree (default: false).
##
## To use workspace mode by default when available, set this to true and
## set filter_mode to "workspace" or leave it unspecified and
## set search.filters to include "workspace" before other filter modes.
# workspaces = false
## which filter mode to use when atuin is invoked from a shell up-key binding
## the accepted values are identical to those of "filter_mode"
## leave unspecified to use same mode set in "filter_mode"
# filter_mode_shell_up_key_binding = "global"
## which search mode to use when atuin is invoked from a shell up-key binding
## the accepted values are identical to those of "search_mode"
## leave unspecified to use same mode set in "search_mode"
# search_mode_shell_up_key_binding = "fuzzy"
## which style to use
## possible values: auto, full, compact
# style = "auto"
## the maximum number of lines the interface should take up
## set it to 0 to always go full screen
# inline_height = 0
## the maximum number of lines the interface should take up
## when atuin is invoked from a shell up-key binding
## the accepted values are identical to those of "inline_height"
# inline_height_shell_up_key_binding = 0
## Invert the UI - put the search bar at the top , Default to `false`
# invert = false
## enable or disable showing a preview of the selected command
## useful when the command is longer than the terminal width and is cut off
# show_preview = true
## what to do when the escape key is pressed when searching
## possible values: return-original, return-query
# exit_mode = "return-original"
## possible values: emacs, subl
# word_jump_mode = "emacs"
## characters that count as a part of a word
# word_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
## number of context lines to show when scrolling by pages
# scroll_context_lines = 1
## use ctrl instead of alt as the shortcut modifier key for numerical UI shortcuts
## alt-0 .. alt-9
# ctrl_n_shortcuts = false
## Show numeric shortcuts (1..9) beside list items in the TUI
## set to false to hide the moving numbers if you find them distracting
# show_numeric_shortcuts = true
## default history list format - can also be specified with the --format arg
# history_format = "{time}\t{command}\t{duration}"
## prevent commands matching any of these regexes from being written to history.
## Note that these regular expressions are unanchored, i.e. if they don't start
## with ^ or end with $, they'll match anywhere in the command.
## For details on the supported regular expression syntax, see
## https://docs.rs/regex/latest/regex/#syntax
# history_filter = [
# "^secret-cmd",
# "^innocuous-cmd .*--secret=.+",
# ]
## prevent commands run with cwd matching any of these regexes from being written
## to history. Note that these regular expressions are unanchored, i.e. if they don't
## start with ^ or end with $, they'll match anywhere in CWD.
## For details on the supported regular expression syntax, see
## https://docs.rs/regex/latest/regex/#syntax
# cwd_filter = [
# "^/very/secret/area",
# ]
## Configure the maximum height of the preview to show.
## Useful when you have long scripts in your history that you want to distinguish
## by more than the first few lines.
# max_preview_height = 4
## Configure whether or not to show the help row, which includes the current Atuin
## version (and whether an update is available), a keymap hint, and the total
## amount of commands in your history.
# show_help = true
## Configure whether or not to show tabs for search and inspect
# show_tabs = true
## Configure whether or not the tabs row may be auto-hidden, which includes the current Atuin
## tab, such as Search or Inspector, and other tabs you may wish to see. This will
## only be hidden if there are fewer than this count of lines available, and does not affect the use
## of keyboard shortcuts to switch tab. 0 to never auto-hide, default is 8 (lines).
## This is ignored except in `compact` mode.
# auto_hide_height = 8
## Defaults to true. This matches history against a set of default regex, and will not save it if we get a match. Defaults include
## 1. AWS key id
## 2. Github pat (old and new)
## 3. Slack oauth tokens (bot, user)
## 4. Slack webhooks
## 5. Stripe live/test keys
# secrets_filter = true
## Defaults to true. If enabled, upon hitting enter Atuin will immediately execute the command. Press tab to return to the shell and edit.
# This applies for new installs. Old installs will keep the old behaviour unless configured otherwise.
enter_accept = true
## Defaults to false. If enabled, when triggered after &&, || or |, Atuin will complete commands to chain rather than replace the current line.
# command_chaining = false
## Defaults to "emacs". This specifies the keymap on the startup of `atuin
## search`. If this is set to "auto", the startup keymap mode in the Atuin
## search is automatically selected based on the shell's keymap where the
## keybinding is defined. If this is set to "emacs", "vim-insert", or
## "vim-normal", the startup keymap mode in the Atuin search is forced to be
## the specified one.
# keymap_mode = "auto"
## Cursor style in each keymap mode. If specified, the cursor style is changed
## in entering the cursor shape. Available values are "default" and
## "{blink,steady}-{block,underline,bar}".
# keymap_cursor = { emacs = "blink-block", vim_insert = "blink-block", vim_normal = "steady-block" }
# network_connect_timeout = 5
# network_timeout = 5
## Timeout (in seconds) for acquiring a local database connection (sqlite)
# local_timeout = 5
## Set this to true and Atuin will minimize motion in the UI - timers will not update live, etc.
## Alternatively, set env NO_MOTION=true
# prefers_reduced_motion = false
[stats]
## Set commands where we should consider the subcommand for statistics. Eg, kubectl get vs just kubectl
# common_subcommands = [
# "apt",
# "cargo",
# "composer",
# "dnf",
# "docker",
# "dotnet",
# "git",
# "go",
# "ip",
# "jj",
# "kubectl",
# "nix",
# "nmcli",
# "npm",
# "pecl",
# "pnpm",
# "podman",
# "port",
# "systemctl",
# "tmux",
# "yarn",
# ]
## Set commands that should be totally stripped and ignored from stats
# common_prefix = ["sudo"]
## Set commands that will be completely ignored from stats
# ignored_commands = [
# "cd",
# "ls",
# "vi"
# ]
[keys]
# Defaults to true. If disabled, using the up/down key won't exit the TUI when scrolled past the first/last entry.
# scroll_exits = true
# Defaults to true. The left arrow key will exit the TUI when scrolling before the first character
# exit_past_line_start = true
# Defaults to true. The right arrow key performs the same functionality as Tab and copies the selected line to the command line to be modified.
# accept_past_line_end = true
# Defaults to false. The left arrow key performs the same functionality as Tab and copies the selected line to the command line to be modified.
# accept_past_line_start = false
# Defaults to false. The backspace key performs the same functionality as Tab and copies the selected line to the command line to be modified when at the start of the line.
# accept_with_backspace = false
[sync]
# Enable sync v2 by default
# This ensures that sync v2 is enabled for new installs only
# In a later release it will become the default across the board
records = true
[preview]
## which preview strategy to use to calculate the preview height (respects max_preview_height).
## possible values: auto, static
## auto: length of the selected command.
## static: length of the longest command stored in the history.
## fixed: use max_preview_height as fixed height.
# strategy = "auto"
[daemon]
## Enables using the daemon to sync. Requires the daemon to be running in the background. Start it with `atuin daemon`
# enabled = false
## How often the daemon should sync in seconds
# sync_frequency = 300
## The path to the unix socket used by the daemon (on unix systems)
## linux/mac: ~/.local/share/atuin/atuin.sock
## windows: Not Supported
# socket_path = "~/.local/share/atuin/atuin.sock"
## Use systemd socket activation rather than opening the given path (the path must still be correct for the client)
## linux: false
## mac/windows: Not Supported
# systemd_socket = false
## The port that should be used for TCP on non unix systems
# tcp_port = 8889
# [theme]
## Color theme to use for rendering in the terminal.
## There are some built-in themes, including the base theme ("default"),
## "autumn" and "marine". You can add your own themes to the "./themes" subdirectory of your
## Atuin config (or ATUIN_THEME_DIR, if provided) as TOML files whose keys should be one or
## more of AlertInfo, AlertWarn, AlertError, Annotation, Base, Guidance, Important, and
## the string values as lowercase entries from this list:
## https://ogeon.github.io/docs/palette/master/palette/named/index.html
## If you provide a custom theme file, it should be called "NAME.toml" and the theme below
## should be the stem, i.e. `theme = "NAME"` for your chosen NAME.
# name = "autumn"
## Whether the theme manager should output normal or extra information to help fix themes.
## Boolean, true or false. If unset, left up to the theme manager.
# debug = true
[search]
## The list of enabled filter modes, in order of priority.
## The "workspace" mode is skipped when not in a workspace or workspaces = false.
## Default filter mode can be overridden with the filter_mode setting.
# filters = [ "global", "host", "session", "session-preload", "workspace", "directory" ]

View File

@@ -0,0 +1,10 @@
# Starship prompt config.
# Currently equivalent to stock defaults — `format = "$all"` is what starship
# uses when no config exists. This file exists so the prompt is pinned rather
# than tracking whatever the installed starship version defaults to, and so
# there's an obvious place to customise.
# Reference: https://starship.rs/config/
"$schema" = 'https://starship.rs/config-schema.json'
format = "$all"

2
home/dot_gitconfig Normal file
View File

@@ -0,0 +1,2 @@
[credential]
helper = store

189
setup.sh Normal file
View File

@@ -0,0 +1,189 @@
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# dotfiles/setup.sh — provision a fresh box, then hand off to chezmoi
#
# Root work (apt, starship, chsh) happens here. Everything under $HOME is
# owned by chezmoi. Run: sudo bash setup.sh
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m'
PASS=0; WARN=0; FAIL=0
log_info() { echo -e "${BLUE}[INFO]${NC} $*"; }
log_success() { echo -e "${GREEN}[ OK ]${NC} $*"; PASS=$((PASS+1)); }
log_warning() { echo -e "${YELLOW}[WARN]${NC} $*"; WARN=$((WARN+1)); }
log_error() { echo -e "${RED}[FAIL]${NC} $*"; FAIL=$((FAIL+1)); }
log_step() { echo -e "\n${BLUE}${NC} ${1}"; }
DOTFILES_REPO="${DOTFILES_REPO:-https://git.domverse-berlin.eu/domverse/dotfiles.git}"
CARGO_TOOLS=(atuin eza macchina oxker)
APT_PKGS=(fish bat ripgrep jq git curl nano build-essential pkg-config libssl-dev)
echo -e "${BLUE}"
cat <<'EOF'
╔══════════════════════════════════════════════════════════════╗
║ Shell & dotfiles bootstrap ║
║ fish · starship · atuin · eza · chezmoi · claude ║
╚══════════════════════════════════════════════════════════════╝
EOF
echo -e "${NC}"
# ── Preflight ────────────────────────────────────────────────────────────────
log_step "Preflight"
if [ "$(id -u)" -ne 0 ]; then
log_error "Must run as root: sudo bash setup.sh"
exit 1
fi
TARGET_USER="${SUDO_USER:-}"
if [ -z "$TARGET_USER" ] || [ "$TARGET_USER" = "root" ]; then
log_error "Could not determine the non-root target user (SUDO_USER unset)."
log_error "Run via 'sudo bash setup.sh' as your normal user, not as root directly."
exit 1
fi
TARGET_HOME=$(getent passwd "$TARGET_USER" | cut -d: -f6)
log_success "Target user: $TARGET_USER (home: $TARGET_HOME)"
# Run a command as the target user with a sane login-ish env.
as_user() { sudo -u "$TARGET_USER" -H bash -lc "$*"; }
# ── APT packages ─────────────────────────────────────────────────────────────
log_step "APT packages"
MISSING=()
for p in "${APT_PKGS[@]}"; do
dpkg -s "$p" >/dev/null 2>&1 || MISSING+=("$p")
done
if [ ${#MISSING[@]} -eq 0 ]; then
log_success "All apt packages already present (${APT_PKGS[*]})"
else
log_info "Installing: ${MISSING[*]}"
apt-get update -qq
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "${MISSING[@]}"
log_success "Installed ${#MISSING[@]} apt package(s)"
fi
# config.fish aliases `cat` to batcat; on Ubuntu the binary really is batcat.
if command -v batcat >/dev/null 2>&1; then
log_success "batcat present (config.fish aliases cat -> batcat)"
else
log_warning "batcat missing — the 'cat' alias in config.fish will break"
fi
# ── rustup + cargo tools ─────────────────────────────────────────────────────
log_step "Rust toolchain (build dependency for ${CARGO_TOOLS[*]})"
if as_user 'command -v cargo' >/dev/null 2>&1; then
log_success "rustup/cargo already installed"
else
log_info "Installing rustup (non-interactive, default profile)..."
as_user "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path"
log_success "rustup installed"
fi
# `cargo install` is idempotent-ish: it rebuilds if the version differs.
# --locked keeps builds reproducible. This is the slow part (~10-20 min cold).
for tool in "${CARGO_TOOLS[@]}"; do
if as_user "test -x \$HOME/.cargo/bin/$tool"; then
log_success "$tool already built"
else
log_info "Building $tool from source (this takes a while)..."
if as_user "\$HOME/.cargo/bin/cargo install --locked $tool"; then
log_success "$tool built"
else
log_error "$tool failed to build"
fi
fi
done
# ── starship ─────────────────────────────────────────────────────────────────
log_step "Starship prompt"
if command -v starship >/dev/null 2>&1; then
log_success "starship already installed ($(starship --version | head -1))"
else
log_info "Installing starship to /usr/local/bin..."
curl -sS https://starship.rs/install.sh | sh -s -- --yes >/dev/null
log_success "starship installed"
fi
# ── chezmoi + dotfiles ───────────────────────────────────────────────────────
log_step "chezmoi"
if as_user 'command -v chezmoi' >/dev/null 2>&1; then
log_success "chezmoi already installed"
else
log_info "Installing chezmoi to ~/.local/bin..."
as_user "sh -c \"\$(curl -fsLS get.chezmoi.io)\" -- -b \$HOME/.local/bin"
log_success "chezmoi installed"
fi
CHEZMOI="\$HOME/.local/bin/chezmoi"
# chezmoi does NOT preserve exact file modes — it tracks only the executable and
# private attributes, then derives the mode from the umask. A umask of 002 (the
# default here) would write world/group-writable 0664 dotfiles. Pin 022 so files
# land as 0644, and let the private_ attribute handle the 0600 cases.
log_step "chezmoi config"
CHEZMOI_CFG="$TARGET_HOME/.config/chezmoi/chezmoi.toml"
if [ -f "$CHEZMOI_CFG" ] && grep -q 'umask' "$CHEZMOI_CFG"; then
log_success "chezmoi umask already configured"
else
as_user "mkdir -p \$HOME/.config/chezmoi"
as_user "printf 'umask = 0o022\n' >> \$HOME/.config/chezmoi/chezmoi.toml"
log_success "chezmoi umask pinned to 022"
fi
log_step "Applying dotfiles"
if as_user "test -d \$HOME/.local/share/chezmoi/.git"; then
log_info "chezmoi source already initialised — applying current state"
as_user "$CHEZMOI apply" && log_success "dotfiles applied" || log_error "chezmoi apply failed"
else
log_info "Initialising chezmoi from $DOTFILES_REPO"
if as_user "$CHEZMOI init --apply '$DOTFILES_REPO'"; then
log_success "dotfiles initialised and applied"
else
log_error "chezmoi init failed — check repo access (private repo needs credentials)"
fi
fi
# ── Default shell ────────────────────────────────────────────────────────────
log_step "Default shell"
FISH_BIN=$(command -v fish || true)
if [ -z "$FISH_BIN" ]; then
log_error "fish not found — cannot set default shell"
else
grep -qxF "$FISH_BIN" /etc/shells || echo "$FISH_BIN" >> /etc/shells
CURRENT_SHELL=$(getent passwd "$TARGET_USER" | cut -d: -f7)
if [ "$CURRENT_SHELL" = "$FISH_BIN" ]; then
log_success "fish is already the default shell for $TARGET_USER"
else
chsh -s "$FISH_BIN" "$TARGET_USER"
log_success "default shell changed: $CURRENT_SHELL -> $FISH_BIN"
fi
fi
# ── Summary ──────────────────────────────────────────────────────────────────
echo
echo "────────────────────────────────────────────────────────────────"
echo -e " ${GREEN}pass: $PASS${NC} ${YELLOW}warn: $WARN${NC} ${RED}fail: $FAIL${NC}"
echo "────────────────────────────────────────────────────────────────"
echo
log_info "Manual steps that cannot be automated:"
echo " 1. atuin sync — key is NOT in this repo (tracked in secrets.yml):"
echo " atuin login -u <user>"
echo " atuin sync"
echo " Restore the key from secrets.yml into ~/.local/share/atuin/key"
echo " BEFORE first sync, or history encrypted on other hosts won't decrypt."
echo " 2. git identity is not set globally (only credential.helper=store):"
echo " git config --global user.name '<name>'"
echo " git config --global user.email '<email>'"
echo " 3. Log out and back in for the fish shell change to take effect."
echo
[ "$FAIL" -gt 0 ] && exit 1
exit 0