Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
4a771aa
Detect Omarchy and reorganize shell config around drop-in directories
CS-5 Aug 28, 2026
309582c
Manage the Hyprland config, Omarchy shell, custom theme and agents wi…
CS-5 Aug 28, 2026
5434640
Build and install the keyboard-backlight daemon, its unit, and the to…
CS-5 Aug 28, 2026
5ed9ac0
Declare Omarchy packages, installers and shell plugins in a manifest
CS-5 Aug 28, 2026
228074b
Add a dotfiles sync widget, menu and status backend
CS-5 Aug 28, 2026
44d6d11
Route update and install.sh through Omarchy's own entrypoints
CS-5 Aug 28, 2026
6884746
Note Omarchy in the README install instructions
CS-5 Aug 28, 2026
b390e3f
Clarify what the carson.dotfiles loaded flag distinguishes
CS-5 Aug 28, 2026
3110031
Install Claude Code through mise instead of its own installer
CS-5 Aug 28, 2026
ca0c67f
Manage only the Hyprland files that are actually customized
CS-5 Aug 28, 2026
c4deccd
Run omarchy update last so a reboot cannot strand the dotfiles steps
CS-5 Aug 28, 2026
35924ca
Fix dotfiles-sync's missing gum, and stop clean from fighting Omarchy
CS-5 Aug 28, 2026
5382da5
Add a test suite and CI, and normalize shell formatting
CS-5 Aug 28, 2026
856e515
Give PowerShell the same drop-in layout, and Windows an update command
CS-5 Aug 28, 2026
7afe1bb
Install fish plugins from chezmoi so macOS gets them too
CS-5 Aug 28, 2026
f7576f1
Fix the CI idempotency check to ignore always-run scripts
CS-5 Aug 28, 2026
97cdbfb
Trim cloud-init to what it alone needs, and add an .editorconfig
CS-5 Aug 28, 2026
3aca68d
Show the changed files and their line counts in the widget tooltip
CS-5 Aug 28, 2026
c0903dc
Sync theme definitions but never the active theme
CS-5 Aug 29, 2026
a244359
Cleanups and pruning
CS-5 Aug 31, 2026
f4cd1df
Maybe fix vscode package
CS-5 Aug 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Matches what the repo already does, and what ./test.sh enforces for shell
# (shfmt -i 4, default case indentation).
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space

[*.{sh,bash}]
indent_size = 4

# chezmoi source names carry attribute prefixes rather than extensions, so the
# shell scripts installed to ~/.local/bin match on the prefix instead.
[executable_*]
indent_size = 4

[*.{ps1,psm1}]
indent_size = 4

[*.{json,jsonc,toml,yaml,yml,md}]
indent_size = 2

[*.fish]
indent_size = 4

[*.lua]
indent_size = 2

[*.qml]
indent_size = 2

# Go templates wrap whatever they render; leave their indentation alone.
[*.tmpl]
trim_trailing_whitespace = false

# Vendored upstream, kept byte-identical for easy diffing against new releases.
[root/private_dot_config/fish/functions/fundle.fish]
trim_trailing_whitespace = false
insert_final_newline = false
3 changes: 2 additions & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"timonwong.shellcheck",
"foxundermoon.shell-format",
"mads-hartmann.bash-ide-vscode",
"bmalehorn.vscode-fish"
"bmalehorn.vscode-fish",
"theqtcompany.qt-qml"
]
}
4 changes: 3 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,7 @@
"editor.codeActionsOnSave": {
"source.fixAll.markdownlint": "explicit"
}
}
},
"qt-core.showWelcomePageOnActivation": false,
"qt-qml.qmlls.enabled": false
}
109 changes: 83 additions & 26 deletions AGENTS.md

Large diffs are not rendered by default.

25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,24 @@ My personal dotfiles, managed with [chezmoi](https://www.chezmoi.io/).

## Install

### Linux (Debian/Ubuntu/Arch) — dev containers, WSL, cloud VMs
### Linux (Debian/Ubuntu/Arch/Omarchy) — dev containers, WSL, cloud VMs, desktops

Clone and run the bootstrap script. It installs mise, chezmoi, fish, and Claude
Code, then applies the dotfiles:
Clone and run the bootstrap script. It installs mise, chezmoi and fish, then
applies the dotfiles — which is what installs the CLI tools, Claude Code
included:

```sh
git clone https://github.com/CS-5/dotfiles.git ~/.local/share/chezmoi
~/.local/share/chezmoi/install.sh [--work-email <address>]
```

On [Omarchy](https://omarchy.org) the same command also brings up the desktop:
Hyprland config, the Omarchy shell layout, the custom theme, declared packages
and shell plugins, and the ThinkPad keyboard-backlight daemon. It defers to
Omarchy wherever Omarchy already owns something — packages go through `omarchy
pkg add`, chezmoi comes from the Arch repos, and Claude Code is left to
Omarchy's own mise wrapper. Expect one sudo prompt, then log out and back in.

The work identity comes from a single file, `~/work.email`, containing just your
work email address; its domain selects the identity (e.g. `…@kirbtech.com`). No
file means a personal machine. `--work-email` writes that file before applying;
Expand All @@ -40,6 +48,17 @@ sh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply CS-5
iex "&{$(irm 'https://get.chezmoi.io/ps1')} init --apply CS-5"
```

Everything the dotfiles can do for themselves happens during that apply — mise
tools, fish plugins, completions, Claude Code and its plugins. Two things a
bootstrap script does on Linux have no equivalent here:

- **A per-host signing key.** Without one, commits sign through the SSH agent
using the committed public key, which works. To get a key on disk instead,
run `scripts/generate-signing-key.sh` **before** `chezmoi init` — the key path
is read when the config template renders, so generating it afterwards has no
effect until the next `chezmoi init`.
- **Making fish the login shell** (`chsh -s $(which fish)` on macOS).

## Commit signing

Commits are signed with an SSH key. The signing **public** key is baked into the
Expand Down
7 changes: 3 additions & 4 deletions cloud-init.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,13 @@ users:
package_update: true
package_upgrade: true

# Only what cloud-init itself needs to fetch the repo. Everything else
# (build-essential, fish, neovim, wget/unzip/gnupg, mise, chezmoi and the
# dotfiles' own tool list) is install.sh's job.
packages:
- build-essential
- ca-certificates
- curl
- git
- wget
- unzip
- gnupg

runcmd:
- su - carson -c "git clone https://github.com/CS-5/dotfiles.git ~/.local/share/chezmoi"
Expand Down
58 changes: 32 additions & 26 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,6 @@
set -eufo pipefail

# Discover dev container workspace if present (/workspaces/<repo-name> convention).
# Uses find rather than a glob because `set -f` disables pathname expansion.
#
# Codespaces names the folder outright in CODESPACE_VSCODE_FOLDER, so prefer it.
# The find fallback has to skip dotdirs and sort: Codespaces also mounts
# /workspaces/.codespaces, and find emits filesystem order, so an unsorted
# `head -n1` picks .codespaces over the repo — and `mise trust` then trusts a
# directory with no config in it.
WORKSPACE_DIR="${CODESPACE_VSCODE_FOLDER:-}"
if [[ -n "$WORKSPACE_DIR" && ! -d "$WORKSPACE_DIR" ]]; then
WORKSPACE_DIR=""
Expand All @@ -21,13 +14,18 @@ if [[ -z "$WORKSPACE_DIR" && -d "/workspaces" ]]; then
WORKSPACE_DIR="$(find /workspaces -maxdepth 1 -mindepth 1 -type d ! -name '.*' 2>/dev/null | sort | head -n1)"
fi

# Dev containers sign commits via the host's forwarded SSH agent; real hosts
# (WSL, VMs, bare metal) own a signing key on disk.
# Is Dev Container?
IS_DC=false
if [[ -n "${REMOTE_CONTAINERS_IPC:-}" || "${USER:-}" == "vscode" || "${CODESPACES:-}" == "true" || -n "$WORKSPACE_DIR" ]]; then
IS_DC=true
fi

# Is Omarchy?
IS_OMARCHY=false
if [[ -r /etc/os-release ]] && grep -q '^ID=omarchy$' /etc/os-release; then
IS_OMARCHY=true
fi

# Work identity is detected from ~/work.email at chezmoi render time. Pass
# --work-email to write that file here (non-interactive provisioning); leave it
# unset to keep any existing ~/work.email (no file => personal identity).
Expand Down Expand Up @@ -68,16 +66,17 @@ export PATH="$HOME/.local/bin:$PATH"

#### Bootstrap Dependencies ####
show_progress "Installing bootstrap dependencies"
# Same package names in Debian/Ubuntu and Arch repos.

# Same package names in Debian/Ubuntu and Arch repos
BOOTSTRAP_PKGS=(curl git wget unzip gnupg fish neovim)
if command -v apt-get >/dev/null 2>&1; then

if [[ "$IS_OMARCHY" == "true" ]]; then
omarchy pkg add "${BOOTSTRAP_PKGS[@]}" base-devel
elif command -v apt-get >/dev/null 2>&1; then
sudo apt-get update
sudo apt-get install -y "${BOOTSTRAP_PKGS[@]}"
sudo apt-get install -y "${BOOTSTRAP_PKGS[@]}" build-essential
elif command -v pacman >/dev/null 2>&1; then
# -Syu, not -Sy: Arch doesn't support partial upgrades, so installing
# against a synced-but-not-upgraded system can break shared libs.
# --needed skips packages already present.
sudo pacman -Syu --needed --noconfirm "${BOOTSTRAP_PKGS[@]}"
sudo pacman -Syu --needed --noconfirm "${BOOTSTRAP_PKGS[@]}" base-devel
else
log_error "No supported package manager found (apt-get or pacman)."
log_error "Install these manually, then re-run: ${BOOTSTRAP_PKGS[*]}"
Expand All @@ -86,30 +85,42 @@ fi
log_success "Bootstrap dependencies installed"

#### Mise ####
# Leave an existing mise alone. Dev container images commonly install a pinned
# mise and assert the pin at build time; reinstalling from mise.run would replace
# it with latest and silently defeat that. Same shape as the guards in
# .chezmoiscripts/*mise*.

# Leave an existing mise alone. Omarchy ships with Mise and Dev Container images may also
if command -v mise >/dev/null 2>&1; then
log_info "mise $(mise --version | cut -d' ' -f1) already installed, leaving it as-is"
else
show_progress "Installing mise"
curl -fsSL https://mise.run | sh
log_success "mise installed"
fi

# If there's a workspace dir (Dev Container)
if [[ -n "$WORKSPACE_DIR" ]]; then
mise trust --cd="$WORKSPACE_DIR" --quiet
fi

#### Signing Key ####

# Must run before chezmoi applies so the key is detected at render time.
if [[ "$IS_DC" != "true" ]]; then
show_progress "Ensuring commit signing key"
"$SCRIPT_DIR/generate-signing-key.sh"
log_success "Signing key ready"
fi

#### Chezmoi ####

# On Omarchy, take chezmoi from the repos rather than the curl installer, so it
# is upgraded by `omarchy update` along with everything else.
if [[ "$IS_OMARCHY" == "true" ]] && ! command -v chezmoi >/dev/null 2>&1; then
show_progress "Installing chezmoi from the Arch repos"
omarchy pkg add chezmoi
log_success "chezmoi installed"
fi

#### Chezmoi Setup ####

show_progress "Installing chezmoi and dotfiles"
if [[ -n "$WORK_EMAIL" ]]; then
"$SCRIPT_DIR/install-dotfiles.sh" --work-email "$WORK_EMAIL"
Expand All @@ -119,17 +130,12 @@ fi
log_success "Dotfiles installed and applied"

#### Shell ####

show_progress "Setting up shell"
fish -c "fundle install"
if [[ "${SHELL:-}" != *"fish"* ]]; then
log_info "Changing default shell to fish"
sudo chsh -s "$(which fish)" "${USER:-$(id -un)}"
fi
log_success "Shell setup complete"

#### Claude Code ####
show_progress "Installing Claude Code"
curl -fsSL https://claude.ai/install.sh | bash
log_success "Claude Code installed"

log_success "Setup complete"
4 changes: 2 additions & 2 deletions render.sh
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ fi
# and .chezmoidata are unavailable. Without --init both load, and the rendered
# config's [data] is still supplied via --config.
chezmoi execute-template --init --source="$SCRIPT_DIR/root" \
< "$SCRIPT_DIR/root/.chezmoi.toml.tmpl" > "$TEMP_DIR/chezmoi.toml"
<"$SCRIPT_DIR/root/.chezmoi.toml.tmpl" >"$TEMP_DIR/chezmoi.toml"

# --override-data (chezmoi >= 2.66) applies last, on top of the rendered
# config's [data], so --data can force values like isMac or isDc directly.
Expand All @@ -142,4 +142,4 @@ if [[ -n "$OVERRIDE_DATA" ]]; then
fi

chezmoi execute-template --config="$TEMP_DIR/chezmoi.toml" --source="$SCRIPT_DIR/root" \
${OVERRIDE_ARGS[@]+"${OVERRIDE_ARGS[@]}"} < "$SCRIPT_DIR/$TEMPLATE_FILE"
${OVERRIDE_ARGS[@]+"${OVERRIDE_ARGS[@]}"} <"$SCRIPT_DIR/$TEMPLATE_FILE"
28 changes: 12 additions & 16 deletions root/.chezmoi.toml.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,24 @@ sourceDir = {{ env "DOTFILES_SOURCE_DIR" | default "~/.local/share/chezmoi" | qu
{{ $probablyDc := or (env "REMOTE_CONTAINERS_IPC" | not | not) (eq (env "USER") "vscode") (stat "/workspaces" | not | not) -}}
{{ $isCodespaces := env "CODESPACES" | not | not -}}
{{ $isDc := or $probablyDc $isCodespaces -}}
{{ $isOmarchy := and (eq .chezmoi.os "linux") (eq (get .chezmoi.osRelease "id" | default "") "omarchy") -}}
{{ $isThinkPad := false -}}

{{ $binDir := ".local/bin" -}}

{{/* Work identity is driven by ~/work.email: a file whose sole contents
are one work email address. When the file is absent, the
DOTFILES_WORK_EMAIL env var is consulted as a fallback (Codespaces
user secrets surface as env vars). The email's domain selects the
identity; no email or an unrecognized domain means personal. Mirrors
the per-host signing-key file below. Add new jobs to $identities. */}}
{{ $dmiFamilyPath := "/sys/class/dmi/id/product_family" -}}
{{ if stat $dmiFamilyPath -}}
{{ $isThinkPad = include $dmiFamilyPath | trim | hasPrefix "ThinkPad" -}}
{{ end -}}

{{ $identities := dict
"kirbtech.com" "kirbtech"
"journalytic.com" "journalytic" -}}
{{ $identity := "personal" -}}

{{ $workEmail := "" -}}
{{/* Defaults to ~/work.email; DOTFILES_WORK_EMAIL_FILE relocates it (render.sh). */}}
{{ $workEmailPath := joinPath .chezmoi.homeDir "work.email" -}}

{{ if env "DOTFILES_WORK_EMAIL_FILE" -}}{{ $workEmailPath = env "DOTFILES_WORK_EMAIL_FILE" -}}{{ end -}}
{{ $email := "" -}}
{{ if stat $workEmailPath -}}
Expand All @@ -34,24 +37,15 @@ sourceDir = {{ env "DOTFILES_SOURCE_DIR" | default "~/.local/share/chezmoi" | qu
{{ end -}}
{{ $isWork := ne $identity "personal" -}}

{{/* Signing public key: committed canonical default, overridden by a
per-host file, overridden by an explicit env var. The key is public,
so committing it is safe; the matching private key lives in the SSH
agent wherever commits are made. */}}
{{ $signingKey := "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKEgjvCUvsO0qdi0D+H0ksk2HbfGhlQU7fQE4F817N+G" -}}
{{ $signingKeyPath := joinPath .chezmoi.homeDir ".ssh/git_signing.pub" -}}
{{ if stat $signingKeyPath -}}{{ $signingKey = include $signingKeyPath | trim -}}{{ end -}}
{{ if env "DOTFILES_SIGNING_KEY" -}}{{ $signingKey = env "DOTFILES_SIGNING_KEY" -}}{{ end -}}

{{/* Private signing key on disk (hosts/VMs/WSL). When present, git signs
directly from the file with no agent; otherwise it falls back to the
canonical public key via a forwarded agent (dev containers). */}}
{{ $signingKeyFilePath := joinPath .chezmoi.homeDir ".ssh/git_signing" -}}
{{ $signingKeyFile := "" -}}
{{ if stat $signingKeyFilePath -}}{{ $signingKeyFile = $signingKeyFilePath -}}{{ end -}}

{{/* Sign commits everywhere except Codespaces, which signs commits with
GitHub's own web-flow key and cannot reach the forwarded signing key. */}}
{{ $gitSign := and (not $isCodespaces) (ne $signingKey "") -}}

[data]
Expand All @@ -64,6 +58,8 @@ sourceDir = {{ env "DOTFILES_SOURCE_DIR" | default "~/.local/share/chezmoi" | qu
isWindows = {{ eq .chezmoi.os "windows" }}
isWsl = {{ env "WSL_DISTRO_NAME" | not | not }}
isDc = {{ $isDc }}
isOmarchy = {{ $isOmarchy }}
isThinkPad = {{ $isThinkPad }}

# Identity
identity = {{ $identity | quote }}
Expand Down
Loading