From 893ec8e3cbb7e686fb562eb9743f95e185bba11a Mon Sep 17 00:00:00 2001 From: domverse Date: Fri, 17 Jul 2026 10:53:46 +0200 Subject: [PATCH] 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 --- .chezmoiroot | 1 + .gitignore | 8 + README.md | 93 ++++++ home/.chezmoiignore | 27 ++ home/dot_bashrc | 132 ++++++++ .../executable_statusline-command.sh | 143 +++++++++ home/dot_config/fish/conf.d/rustup.fish | 1 + home/dot_config/fish/config.fish.tmpl | 33 ++ .../private_atuin/private_config.toml | 289 ++++++++++++++++++ home/dot_config/starship.toml | 10 + home/dot_gitconfig | 2 + setup.sh | 189 ++++++++++++ 12 files changed, 928 insertions(+) create mode 100644 .chezmoiroot create mode 100644 .gitignore create mode 100644 README.md create mode 100644 home/.chezmoiignore create mode 100644 home/dot_bashrc create mode 100644 home/dot_claude/executable_statusline-command.sh create mode 100644 home/dot_config/fish/conf.d/rustup.fish create mode 100644 home/dot_config/fish/config.fish.tmpl create mode 100644 home/dot_config/private_atuin/private_config.toml create mode 100644 home/dot_config/starship.toml create mode 100644 home/dot_gitconfig create mode 100644 setup.sh diff --git a/.chezmoiroot b/.chezmoiroot new file mode 100644 index 0000000..5e72f75 --- /dev/null +++ b/.chezmoiroot @@ -0,0 +1 @@ +home diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4191ded --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# Secrets — never commit +.git-credentials +secrets.yml +*.key + +# Editor / OS noise +*.swp +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..f37fcd8 --- /dev/null +++ b/README.md @@ -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 **10–20 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 ` and `atuin sync`. +2. **git identity** — not set globally, only `credential.helper=store`: + ```bash + git config --global user.name '' + git config --global user.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. diff --git a/home/.chezmoiignore b/home/.chezmoiignore new file mode 100644 index 0000000..ef2d75e --- /dev/null +++ b/home/.chezmoiignore @@ -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 diff --git a/home/dot_bashrc b/home/dot_bashrc new file mode 100644 index 0000000..cc20512 --- /dev/null +++ b/home/dot_bashrc @@ -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" diff --git a/home/dot_claude/executable_statusline-command.sh b/home/dot_claude/executable_statusline-command.sh new file mode 100644 index 0000000..3ddb72a --- /dev/null +++ b/home/dot_claude/executable_statusline-command.sh @@ -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 -> "@", 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 diff --git a/home/dot_config/fish/conf.d/rustup.fish b/home/dot_config/fish/conf.d/rustup.fish new file mode 100644 index 0000000..e4cb363 --- /dev/null +++ b/home/dot_config/fish/conf.d/rustup.fish @@ -0,0 +1 @@ +source "$HOME/.cargo/env.fish" diff --git a/home/dot_config/fish/config.fish.tmpl b/home/dot_config/fish/config.fish.tmpl new file mode 100644 index 0000000..f653c97 --- /dev/null +++ b/home/dot_config/fish/config.fish.tmpl @@ -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 diff --git a/home/dot_config/private_atuin/private_config.toml b/home/dot_config/private_atuin/private_config.toml new file mode 100644 index 0000000..c40461a --- /dev/null +++ b/home/dot_config/private_atuin/private_config.toml @@ -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" ] diff --git a/home/dot_config/starship.toml b/home/dot_config/starship.toml new file mode 100644 index 0000000..3b07cd9 --- /dev/null +++ b/home/dot_config/starship.toml @@ -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" diff --git a/home/dot_gitconfig b/home/dot_gitconfig new file mode 100644 index 0000000..98a080b --- /dev/null +++ b/home/dot_gitconfig @@ -0,0 +1,2 @@ +[credential] + helper = store diff --git a/setup.sh b/setup.sh new file mode 100644 index 0000000..81f90e7 --- /dev/null +++ b/setup.sh @@ -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 " +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 ''" +echo " git config --global user.email ''" +echo " 3. Log out and back in for the fish shell change to take effect." +echo + +[ "$FAIL" -gt 0 ] && exit 1 +exit 0