Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/bin
.DS_Store
home/.chezmoidata/brewfile.yml
*.rayconfig
25 changes: 17 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Shell, Git, editor, and XDG configuration also work on Linux where supported.
| Editors | Zed, VS Code, Vim, and Neovim |
| Shortcuts | Raycast Caps Lock Hyper translated by Ghostty into tmux commands |
| Packages | Homebrew bundle with personal and work profiles |
| Secrets | Owner SSH key encrypted with age; Raycast export encrypted by Raycast |
| Secrets | Owner SSH key encrypted with age; machine-specific data stays local |
| macOS | Dock, Finder, keyboard, Safari, and general defaults |

## Install
Expand All @@ -27,9 +27,13 @@ cd ~/.local/share/chezmoi
./init.sh --macos
```

Use `WORK=true ./init.sh --macos` for the work package profile. Run
`./init.sh --help` for Git identity and signing-key options. Omit `--macos` to
apply the dotfiles without changing macOS defaults.
Use `WORK=true ./init.sh --macos` for the work package profile. An existing
`~/.gitconfig` is left unchanged and any detected global identity is reused. If
the file does not exist, bootstrap prompts for a missing Git name or email
before creating the default config. Put machine-specific overrides or signing
settings in `~/.gitconfig.local`. On new machines, the root config includes
repository defaults from the managed `~/.config/git/dotfiles.gitconfig`. Omit
`--macos` to apply the dotfiles without changing macOS defaults.

### SSH and age

Expand All @@ -54,12 +58,15 @@ load the key on first use. Shell startup does not call `ssh-add`.

| Component | Once per machine |
| --- | --- |
| Raycast | Import `~/.config/raycast/raycast.rayconfig` using Raycast's import action |
| Raycast | Configure Caps Lock as Hyper (`Control+Option+Command`, no Shift) |
| Raycast scripts | Add `~/.config/raycast/scripts/` as a Script Command directory |
| Neovim | Open once to finish Lazy, Mason, and tree-sitter setup |

The Raycast export passphrase is separate from age. tmux plugins install through
TPM; Continuum and Resurrect handle session restoration.
Raycast exports are intentionally not managed because they can contain private
local data and did not reliably restore settings. To add a Ghostty launcher,
open Raycast's Applications extension, add a Command for Ghostty, and assign the
hotkey there. tmux plugins install through TPM; Continuum and Resurrect handle
session restoration.

## Terminal shortcuts

Expand All @@ -69,6 +76,8 @@ symbols.

| Shortcut | Action |
| --- | --- |
| `Command+K` | Clear the active application screen through tmux |
| `Shift+Enter` | Insert a newline in Claude Code and shell prompts |
| `Caps+C` | New window |
| `Caps+I` | Side-by-side pane |
| `Caps+-` | Stacked pane |
Expand Down Expand Up @@ -99,7 +108,7 @@ Machine-specific overrides use unmanaged `*.local` files such as
| --- | --- |
| `home/dot_config/ghostty/` | Ghostty and Hyper mappings |
| `home/dot_config/tmux/` | tmux, status scripts, and plugins |
| `home/dot_config/raycast/` | Raycast export and Script Commands |
| `home/dot_config/raycast/` | Raycast Script Commands |
| `home/dot_config/zed/` | Zed settings and keybindings |
| `home/dot_config/Code/User/` | VS Code XDG settings |
| `home/dot_config/nvim/` | Neovim configuration |
Expand Down
2 changes: 0 additions & 2 deletions home/.chezmoi.toml.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,12 @@ recipient = "age1x8xj89ps3n8j2cy6guv2n9l788q0ud39yq0pw72lxehgdl2fdv8qqa0lfw"

{{ $name := promptStringOnce . "name" "name" }}
{{ $email := promptStringOnce . "email" "email" }}
{{ $signingKey := promptStringOnce . "signingKey" "signingKey" }}
{{/* WORK=true selects the work machine setup; anything else (incl. PERSONAL=true) is personal. */}}
{{ $work := eq (env "WORK" | lower) "true" }}

[data]
name = {{ $name | quote }}
email = {{ $email | quote }}
signingKey = {{ $signingKey | quote }}
profile = {{ ternary "work" "personal" $work | quote }}
work = {{ $work }}
personal = {{ not $work }}
Expand Down
7 changes: 7 additions & 0 deletions home/create_dot_gitconfig.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[user]
name = {{ .name | quote }}
email = {{ .email | quote }}
useConfigOnly = true

[include]
path = {{ joinPath .chezmoi.homeDir ".config/git/dotfiles.gitconfig" | quote }}
Comment on lines +6 to +7

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Migrate previously managed Git roots to the new include

On machines that applied the parent revision, ~/.gitconfig already exists with the old defaults and includes only ~/.gitconfig.local; the create_ template therefore leaves it untouched, so it never includes the new managed file. Consequently, the updated confgit alias edits a file that Git does not read and those existing installations still receive no future default updates. The fresh evidence relative to the earlier comment is that the new split places its include only in this create-only root; migrate roots matching the previously managed configuration or document/apply an explicit upgrade step.

Useful? React with 👍 / 👎.

2 changes: 1 addition & 1 deletion home/dot_aliases
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ alias confaliasl='${EDITOR:-nvim} ~/.aliases.local'
alias confexports='chezmoi edit --apply ~/.exports'
alias confexportsl='${EDITOR:-nvim} ~/.exports.local'
alias conffunctionsl='${EDITOR:-nvim} ~/.functions.local'
alias confgit='chezmoi edit --apply ~/.gitconfig'
alias confgit='chezmoi edit --apply ~/.config/git/dotfiles.gitconfig'
alias confgitl='${EDITOR:-nvim} ~/.gitconfig.local'
alias confzcompl='chezmoi edit --apply ~/.zsh_completions'
alias confzcompll='${EDITOR:-nvim} ~/.zsh_completions.local'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,8 @@ scrollbar = system
# Visual bell without sound
bell-features = no-system,no-audio,attention,border

# Clear the active tmux pane's history, then redraw its shell prompt.
keybind = super+k=text:\x01:clear-history\r\x0c

# Zsh's default Ctrl+V widget quotes the following LF into the edit buffer.
# This works through tmux without enabling its extended-key protocol.
keybind = shift+enter=text:\x16\n
# Send the standard clear-screen key through tmux to the active application.
keybind = super+k=text:\x0c
Comment on lines +48 to +49

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bind Shift-Enter for the Zsh extended-key sequence

When Shift-Enter is used at an ordinary Zsh prompt rather than inside Claude Code, the new tmux extended-key setup forwards a modified-key escape sequence, but home/dot_zshrc has no binding for that sequence. Because this block removes Ghostty's prior Ctrl-V plus LF fallback, the shortcut documented as inserting a newline in shell prompts instead becomes an unbound ZLE sequence; add an appropriate ZLE binding while retaining native handling for Claude Code.

Useful? React with 👍 / 👎.


# Shell
shell-integration = detect
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,3 @@
[user]
name = {{ .name | quote }}
email = {{ .email | quote }}
useConfigOnly = true
{{- if .signingKey }}
signingkey = {{ .signingKey | quote }}
{{- end }}

{{- if .signingKey }}
[commit]
gpgsign = true
{{- end }}

[alias]
# Helpers
aliases = config --get-regexp "^alias\\."
Expand Down
Binary file removed home/dot_config/raycast/raycast.rayconfig
Binary file not shown.
4 changes: 3 additions & 1 deletion home/dot_config/tmux/tmux.conf
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ bind C-a send-prefix
# Terminal
set -g default-terminal "tmux-256color"
set -su terminal-features
set -s terminal-features[3] "xterm-ghostty:RGB"
set -s terminal-features[3] "xterm-ghostty:RGB:extkeys"
set -s extended-keys on
set -g focus-events on

# Mouse / clipboard
set -g mouse on
Expand Down
43 changes: 17 additions & 26 deletions init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,19 @@ set -euo pipefail

git_name="${GIT_NAME:-}"
git_email="${GIT_EMAIL:-}"
git_signing_key="${GIT_SIGNING_KEY:-}"
run_macos=false

usage() {
cat <<'EOF'
Usage: ./init.sh [options]

Options:
--name="Your Name" Git user.name for this machine
--email="you@example" Git user.email for this machine
--signing-key="KEY" Git user.signingkey (SSH key / GPG key id)
--macos Run macOS post-setup after apply
-h, --help Show this help

Environment:
GIT_NAME
GIT_EMAIL
GIT_SIGNING_KEY

Notes:
Values for name/email/signing-key are stored by chezmoi in
~/.config/chezmoi/chezmoi.toml the first time they're provided.

Omit the flags on subsequent runs to reuse stored values.
Chezmoi will prompt interactively if a value is still missing.
An existing ~/.gitconfig is preserved and its global identity is reused.
If no identity is configured, bootstrap prompts for a Git name and email.
EOF
exit 0
}
Expand All @@ -46,15 +34,6 @@ error() {

while [[ $# -gt 0 ]]; do
case "$1" in
--name=*)
git_name="${1#*=}"
;;
--email=*)
git_email="${1#*=}"
;;
--signing-key=*)
git_signing_key="${1#*=}"
;;
--macos)
run_macos=true
;;
Expand Down Expand Up @@ -90,11 +69,23 @@ apply_dotfiles() {
exit 1
fi

if [[ -z "${git_name}" ]]; then
git_name="$(git config --global --get user.name 2>/dev/null || true)"
fi
if [[ -z "${git_email}" ]]; then
git_email="$(git config --global --get user.email 2>/dev/null || true)"
fi

local args=(init --apply "${PWD}")

[[ -n "${git_name}" ]] && args+=(--promptString "name=${git_name}")
[[ -n "${git_email}" ]] && args+=(--promptString "email=${git_email}")
[[ -n "${git_signing_key}" ]] && args+=(--promptString "signingKey=${git_signing_key}")
if [[ -e "${HOME}/.gitconfig" ]]; then
# The create-only template will preserve this file; blank values avoid
# asking irrelevant questions when identity comes from conditional includes.
args+=(--promptString "name=${git_name}" --promptString "email=${git_email}")
else
[[ -n "${git_name}" ]] && args+=(--promptString "name=${git_name}")
[[ -n "${git_email}" ]] && args+=(--promptString "email=${git_email}")
fi

info "Applying dotfiles from local repo..."
chezmoi "${args[@]}"
Expand Down
25 changes: 20 additions & 5 deletions test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ chezmoi execute-template \
--init \
--promptString 'name=CI' \
--promptString 'email=ci@example.invalid' \
--promptString 'signingKey=' \
< home/.chezmoi.toml.tmpl \
> "${rendered_config}"

Expand All @@ -64,7 +63,6 @@ chezmoi --config "${rendered_config}" data >/dev/null

grep -q '^name = "CI"$' "${rendered_config}" || fail "generated config missing CI name"
grep -q '^email = "ci@example.invalid"$' "${rendered_config}" || fail "generated config missing CI email"
grep -q '^signingKey = ""$' "${rendered_config}" || fail "empty signingKey should render as empty string"
grep -q '^personal = true$' "${rendered_config}" || fail "default profile should be personal"
grep -q '^work = false$' "${rendered_config}" || fail "work should default to false"

Expand All @@ -77,14 +75,32 @@ WORK=true chezmoi execute-template \
--init \
--promptString 'name=CI' \
--promptString 'email=ci@example.invalid' \
--promptString 'signingKey=' \
< home/.chezmoi.toml.tmpl \
> "${rendered_work_config}"

grep -q '^profile = "work"$' "${rendered_work_config}" || fail "WORK=true should select the work profile"
grep -q '^work = true$' "${rendered_work_config}" || fail "WORK=true should set work = true"
grep -q '^personal = false$' "${rendered_work_config}" || fail "WORK=true should set personal = false"

section "Checking safe adoption defaults"

[[ -f home/create_dot_gitconfig.tmpl ]] || fail "Git defaults must be create-only"
[[ ! -e home/dot_gitconfig.tmpl ]] || fail "Git config must not overwrite an existing setup"
Comment thread
froppa marked this conversation as resolved.
grep -Fq 'name = {{ .name | quote }}' home/create_dot_gitconfig.tmpl || fail "new machines must receive a Git name"
grep -Fq 'email = {{ .email | quote }}' home/create_dot_gitconfig.tmpl || fail "new machines must receive a Git email"
grep -Fq 'joinPath .chezmoi.homeDir ".config/git/dotfiles.gitconfig"' home/create_dot_gitconfig.tmpl || fail "root Git config must include managed defaults"
[[ -f home/dot_config/git/dotfiles.gitconfig.tmpl ]] || fail "managed Git defaults are missing"
grep -Fq "alias confgit='chezmoi edit --apply ~/.config/git/dotfiles.gitconfig'" home/dot_aliases || fail "confgit must edit managed defaults"
[[ ! -e home/dot_config/raycast/raycast.rayconfig ]] || fail "Raycast exports must not be managed"
[[ -f home/dot_config/ghostty/config.ghostty ]] || fail "Ghostty config must use its current filename"
grep -Fq 'keybind = super+k=text:\x0c' home/dot_config/ghostty/config.ghostty || fail "missing tmux-aware Command-K binding"
if grep -Fq 'keybind = shift+enter=' home/dot_config/ghostty/config.ghostty; then
fail "Shift-Enter must use Ghostty and Claude Code's native handling"
fi
grep -Fq 'xterm-ghostty:RGB:extkeys' home/dot_config/tmux/tmux.conf || fail "Ghostty must advertise extended keys to tmux"
grep -Fq 'set -s extended-keys on' home/dot_config/tmux/tmux.conf || fail "tmux must pass modified keys to Claude Code"
grep -Fq 'set -g focus-events on' home/dot_config/tmux/tmux.conf || fail "tmux must pass focus events to Claude Code"

section "Testing chezmoi dry-run apply"

# Encrypted entries need the age identity, which only exists on real machines.
Expand All @@ -96,7 +112,6 @@ chezmoi init \
--keep-going \
--exclude encrypted \
--promptString 'name=CI' \
--promptString 'email=ci@example.invalid' \
--promptString 'signingKey='
--promptString 'email=ci@example.invalid'

section "All tests passed"