refactor(flake): add user registry and multi-user host support
Separate user identity (data) from the reusable modules, and let a host declare any number of users instead of exactly one. - users/registry.nix: per-user identity (name, email, groups, authorized and signing keys) as the single source of identity; no user data is hardcoded in the modules. - mkHost takes a `users` set keyed by username; per-user identity is injected into each home config via the `identity` module arg (extraSpecialArgs is per-host, so it cannot carry per-user data). - modules/users.nix builds accounts from the registry; modules/ssh.nix no longer defines authorized keys (the registry owns them); home/git.nix and home/desktop.nix read `identity`; users/emmathorpe/work.nix drops its now-redundant git identity override. - Restructure the tree: users/, home/, modules/, hosts/, lib/ replace the former lyrathorpe/ and system/ layout. - Add standalone homeConfigurations (the portable subset: shell, git, editor, claude) and an exported homeModules output for use on machines not managed by this flake, or as an input to other flakes. Behaviour-preserving for existing hosts: lyrathorpe-mbp and emmathorpe-edaas evaluate to identical derivations; lyrathorpe-t400, lyrathorpe-macpro31 and lyrathorpe-rpi5 differ only by de-duplicating a repeated authorized_keys entry. Fixes the SSH authorized-key leak (one user's key was applied to every account), the hardcoded default git identity, and the hardcoded EDaaS linger setting.
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
# Keybindings reference
|
||||
|
||||
Every keyboard shortcut configured across this desktop, and where it is defined.
|
||||
Everything here is managed declaratively through Nix — edit the listed file and
|
||||
rebuild, never the generated dotfiles.
|
||||
|
||||
| Area | Defined in |
|
||||
| ----------------- | --------------------------------------------------------------------------------------------------------------------- |
|
||||
| Sway (compositor) | [`sway.nix`](./sway.nix) `config.keybindings` + `config.modes`, plus the home-manager Sway module's built-in defaults |
|
||||
| tmux | [`shell.nix`](./shell.nix) `programs.tmux` |
|
||||
| zsh line editor | [`shell.nix`](./shell.nix) `programs.zsh.historySubstringSearch` |
|
||||
| Neovim | [`editor.nix`](./editor.nix) `programs.nixvim` |
|
||||
| foot (terminal) | foot package defaults — only colours are themed (in `sway.nix`) |
|
||||
|
||||
**Conventions**
|
||||
|
||||
- **Super** is the `Mod4` / logo (Windows/Command) key; **Alt** is `Mod1`.
|
||||
- Letter keys are **keysyms** (the character produced), not physical positions.
|
||||
The keyboard is **Dvorak** (`us`/`dvorak`), so e.g. "Super+s" is whatever key
|
||||
types `s` in Dvorak.
|
||||
- Shortcuts apply to every Sway host (MBP, T400, Mac Pro); brightness keys are
|
||||
laptop-only, as noted.
|
||||
|
||||
---
|
||||
|
||||
## Sway
|
||||
|
||||
### Applications & session
|
||||
|
||||
| Shortcut | Action |
|
||||
| ------------------- | ------------------------------------------------------- |
|
||||
| `Super`+`Return` | Open a terminal (foot) |
|
||||
| `Super`+`Space` | App launcher (sway-launcher-desktop in a floating foot) |
|
||||
| `Super`+`d` | App launcher (same as above; module default) |
|
||||
| `Super`+`e` | File manager (nemo) |
|
||||
| `Super`+`c` | Clipboard history picker (clipman → fuzzel) |
|
||||
| `Super`+`l` | Lock screen (swaylock) |
|
||||
| `Super`+`Shift`+`q` | Close the focused window |
|
||||
| `Super`+`Shift`+`c` | Reload the Sway config |
|
||||
| `Super`+`Shift`+`e` | Exit Sway (asks for confirmation) |
|
||||
|
||||
### Focus
|
||||
|
||||
| Shortcut | Action |
|
||||
| ----------------------- | ---------------------------------------- |
|
||||
| `Super`+`←`/`↓`/`↑`/`→` | Move focus by direction |
|
||||
| `Super`+`h`/`j`/`k` | Move focus left / down / up (vim-style) |
|
||||
| `Super`+`a` | Focus the parent container |
|
||||
| `Super`+`Alt`+`Space` | Toggle focus between tiling and floating |
|
||||
|
||||
> Note: vim focus-right would be `Super`+`l`, but that is bound to **lock** here;
|
||||
> use `Super`+`→`.
|
||||
|
||||
### Moving windows
|
||||
|
||||
| Shortcut | Action |
|
||||
| ------------------------------- | ---------------------------------------- |
|
||||
| `Super`+`Shift`+`←`/`↓`/`↑`/`→` | Move the window by direction |
|
||||
| `Super`+`Shift`+`h`/`j`/`k`/`l` | Move the window left / down / up / right |
|
||||
| `Super`+`Shift`+`Space` | Toggle the window floating |
|
||||
|
||||
Mouse (with `Super` held): left-drag moves a window, right-drag resizes it.
|
||||
|
||||
### Layout
|
||||
|
||||
| Shortcut | Action |
|
||||
| ----------- | -------------------------------------------------------------------------------------- |
|
||||
| `Super`+`b` | Split horizontally |
|
||||
| `Super`+`v` | Split vertically |
|
||||
| `Super`+`s` | Stacking layout |
|
||||
| `Super`+`w` | Tabbed layout |
|
||||
| `Super`+`f` | Toggle fullscreen |
|
||||
| `Super`+`y` | **Layout submenu**: `s` stacking · `w` tabbed · `e` toggle split · `Return`/`Esc` exit |
|
||||
|
||||
> The layout submenu's `e` (toggle split) is the home for that action since
|
||||
> `Super`+`e` now opens the file manager.
|
||||
|
||||
### Workspaces
|
||||
|
||||
| Shortcut | Action |
|
||||
| ----------------------- | --------------------------------- |
|
||||
| `Super`+`1`…`0` | Switch to workspace 1…10 |
|
||||
| `Super`+`Shift`+`1`…`0` | Move the window to workspace 1…10 |
|
||||
| `Super`+`z` | Previous workspace |
|
||||
| `Super`+`x` | Next workspace |
|
||||
|
||||
### Scratchpad
|
||||
|
||||
| Shortcut | Action |
|
||||
| ------------------- | --------------------------------- |
|
||||
| `Super`+`Shift`+`-` | Move the window to the scratchpad |
|
||||
| `Super`+`-` | Show / cycle the scratchpad |
|
||||
|
||||
### Modes (submenus)
|
||||
|
||||
| Shortcut | Action |
|
||||
| ------------------- | ------------------------------------------------------------------------------------------------------------ |
|
||||
| `Super`+`r` | **Resize mode**: arrow keys resize; `Return`/`Esc` exit |
|
||||
| `Super`+`y` | **Layout mode** (see Layout above) |
|
||||
| `Super`+`Shift`+`x` | **Power menu**: `l` lock · `e` log out · `s` sleep · `r` reboot · `Shift`+`s` shutdown · `Return`/`Esc` exit |
|
||||
|
||||
### Screenshots
|
||||
|
||||
| Shortcut | Action |
|
||||
| --------------- | ---------------------------------------- |
|
||||
| `Print` | Select a region → swappy (annotate/save) |
|
||||
| `Shift`+`Print` | Focused window → swappy |
|
||||
|
||||
### Audio & media
|
||||
|
||||
| Shortcut | Action |
|
||||
| ----------------------------------------------- | ---------------------- |
|
||||
| `XF86AudioRaiseVolume` / `XF86AudioLowerVolume` | Volume ±5% (wpctl) |
|
||||
| `XF86AudioMute` | Toggle output mute |
|
||||
| `XF86AudioMicMute` | Toggle microphone mute |
|
||||
| `XF86AudioPlay` | Play/pause (playerctl) |
|
||||
| `XF86AudioNext` / `XF86AudioPrev` | Next / previous track |
|
||||
|
||||
### Brightness — laptops only
|
||||
|
||||
| Shortcut | Action |
|
||||
| ----------------------------------------------- | ----------------------------- |
|
||||
| `XF86MonBrightnessUp` / `XF86MonBrightnessDown` | Backlight ±5% (brightnessctl) |
|
||||
|
||||
Present only on portable hosts (T400, MBP); desktops have no internal backlight.
|
||||
|
||||
---
|
||||
|
||||
## tmux
|
||||
|
||||
Prefix is **`Ctrl`+`b`** (default). Copy mode uses **vi** keys.
|
||||
|
||||
| Shortcut | Action |
|
||||
| --------------------------------------- | -------------------------------------------------------------------------------------------- |
|
||||
| `Ctrl`+`b` then `v` | Split into left/right panes |
|
||||
| `Ctrl`+`b` then `s` | Split into top/bottom panes |
|
||||
| `Ctrl`+`h`/`j`/`k`/`l` | Move between panes — and into/out of vim splits — seamlessly (vim-tmux-navigator, no prefix) |
|
||||
| `Alt`+`←`/`→`/`↑`/`↓` | Switch pane by direction (no prefix needed) |
|
||||
| `Ctrl`+`b` then `[` | Enter copy mode (then vi motions; `Space`/`Enter` to select/copy) |
|
||||
| `Ctrl`+`b` then `z` | Zoom / unzoom the focused pane |
|
||||
| `Ctrl`+`b` then `c` | New window |
|
||||
| `Ctrl`+`b` then `n` / `p` | Next / previous window |
|
||||
| `Ctrl`+`b` then `d` | Detach |
|
||||
| `Ctrl`+`b` then `Ctrl`+`s` / `Ctrl`+`r` | Save / restore the session (resurrect; continuum also auto-saves and restores on start) |
|
||||
| Mouse | Enabled — click to focus, drag borders, scroll, select |
|
||||
|
||||
> The stock split keys `%` and `"` are unbound; use `v` / `s` above. `Ctrl`+`b`
|
||||
> then `s` is therefore a split, not the session tree.
|
||||
>
|
||||
> Sessions persist across reboots (resurrect + continuum). Terminals auto-start
|
||||
> tmux; `NO_TMUX=1 <terminal>` opens a bare shell instead.
|
||||
|
||||
---
|
||||
|
||||
## foot (terminal)
|
||||
|
||||
Only colours are themed; these are foot's default key bindings.
|
||||
|
||||
| Shortcut | Action |
|
||||
| --------------------------------------- | ----------------------------- |
|
||||
| `Ctrl`+`Shift`+`c` / `Ctrl`+`Shift`+`v` | Copy / paste (clipboard) |
|
||||
| `Shift`+`Insert` | Paste primary selection |
|
||||
| `Ctrl`+`Shift`+`r` | Search scrollback |
|
||||
| `Ctrl`+`+` / `Ctrl`+`-` / `Ctrl`+`0` | Font larger / smaller / reset |
|
||||
| `Ctrl`+`Shift`+`u` | URL mode (jump to/open links) |
|
||||
| `Ctrl`+`Shift`+`n` | Spawn a new terminal |
|
||||
| `Shift`+`PageUp` / `Shift`+`PageDown` | Scroll back / forward |
|
||||
|
||||
---
|
||||
|
||||
## Neovim
|
||||
|
||||
Leader is **`Space`**. `Ctrl`+`h/j/k/l` is shared with tmux (see above): it moves
|
||||
across vim splits and tmux panes seamlessly. Everything else is stock vim, plus:
|
||||
|
||||
| Shortcut | Action |
|
||||
| ---------------------- | --------------------------------------------------------- |
|
||||
| `,``,` | Toggle the file tree (nvim-tree) — comma pressed twice |
|
||||
| `Ctrl`+`h`/`j`/`k`/`l` | Move between vim splits / tmux panes (vim-tmux-navigator) |
|
||||
| `<leader>ff` | Find files (telescope) |
|
||||
| `<leader>fg` | Live grep (telescope) |
|
||||
| `<leader>fb` | Switch buffer (telescope) |
|
||||
| `<leader>xx` | Diagnostics list (trouble) |
|
||||
| `gc` / `gcc` | Toggle comment (selection / line) |
|
||||
| `gd` | Go to definition (LSP) |
|
||||
| `gr` | List references (LSP) |
|
||||
| `K` | Hover documentation (LSP) |
|
||||
| `<leader>rn` | Rename symbol (LSP; `<leader>` is `Space`) |
|
||||
| `<leader>ca` | Code action (LSP) |
|
||||
|
||||
### Completion menu (nvim-cmp)
|
||||
|
||||
Active only while the completion popup is open (it appears as you type, e.g.
|
||||
file paths):
|
||||
|
||||
| Shortcut | Action |
|
||||
| ----------------------- | ------------------------------------------------------------------ |
|
||||
| `Tab` / `Shift`+`Tab` | Select next / previous item |
|
||||
| `Ctrl`+`n` / `Ctrl`+`p` | Select next / previous item |
|
||||
| `Ctrl`+`Space` | Open the completion menu |
|
||||
| `Enter` | Confirm the highlighted item (no auto-select; otherwise a newline) |
|
||||
| `Ctrl`+`e` | Dismiss the menu |
|
||||
|
||||
LSP covers Nix, Lua, Python and Terraform (the work box adds C# and Helm).
|
||||
Files are formatted on save (conform-nvim). `:Git` opens fugitive; gitsigns
|
||||
shows gutter signs. which-key pops up after `<leader>` to show the rest.
|
||||
|
||||
---
|
||||
|
||||
## zsh
|
||||
|
||||
| Shortcut | Action |
|
||||
| --------- | -------------------------------------------------------------------------------------------------- |
|
||||
| `↑` / `↓` | History **substring** search — type a fragment first, then the arrows cycle matching past commands |
|
||||
|
||||
Bound for both CSI and SS3 cursor sequences, so it works in foot, iTerm2 and
|
||||
the Linux TTY alike.
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
# Interactive shell environment
|
||||
|
||||
Everything the shell, terminal multiplexer, git and ssh do beyond their defaults,
|
||||
and where each is defined. All of it is managed declaratively through
|
||||
home-manager — edit the listed file and rebuild, never the generated dotfiles.
|
||||
|
||||
Keyboard shortcuts have their own reference: [`KEYBINDINGS.md`](./KEYBINDINGS.md).
|
||||
|
||||
| Area | Defined in |
|
||||
| -------------------------------------- | ----------------------------------------------------- |
|
||||
| zsh, CLI tools, tmux, ssh, auto-tmux | [`shell.nix`](./shell.nix) |
|
||||
| git (+ delta, commitizen) | [`git.nix`](./git.nix) |
|
||||
| Neovim (nixvim) + LSP | [`editor.nix`](./editor.nix) |
|
||||
| Claude Code (CLAUDE.md, style, memory) | [`claude.nix`](./claude.nix) |
|
||||
| GUI apps, GTK/Firefox theming, cursor | [`desktop.nix`](./desktop.nix) (graphical hosts only) |
|
||||
|
||||
Shared by every host via [`default.nix`](./default.nix); the work box also layers
|
||||
[`work.nix`](./work.nix) on top (work email, its own ssh config, extra packages,
|
||||
and the C#/Helm language servers).
|
||||
|
||||
---
|
||||
|
||||
## zsh
|
||||
|
||||
| Feature | Notes |
|
||||
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| oh-my-zsh | plugins `git`, `man`, `sudo` (Esc-Esc to prepend sudo), `colored-man-pages`, `extract`; theme `robbyrussell` |
|
||||
| Autosuggestion | fish-style history suggestions as you type (→ to accept) |
|
||||
| Syntax highlighting | commands coloured by validity as you type |
|
||||
| Completion | menu completion; the dump is rebuilt on every activation (see Maintenance) |
|
||||
| History | 100k in-memory/on-disk, deduped, space-prefixed commands ignored, timestamped, **shared live across sessions**; file stays at `~/.zsh_history` |
|
||||
| Dotfiles location | `dotDir` is `~/.config/zsh` (XDG) — `.zshrc`/`.zshenv`/`.zcompdump` live there; `~/.zshenv` only bootstraps `$ZDOTDIR` |
|
||||
| History substring search | type a fragment, then ↑/↓ cycles matching past commands — works in foot, iTerm2 and the Linux TTY (both CSI and SS3 arrow encodings bound) |
|
||||
| Prompt | hostname is prefixed when over SSH |
|
||||
|
||||
**Aliases:** `ls`/`ll`/`la`/`lt` → `eza` (icons + git), `cls` → `clear`. git aliases live in git.nix (below).
|
||||
|
||||
## CLI tools
|
||||
|
||||
| Tool | What it gives you |
|
||||
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `fzf` | `Ctrl-R` fuzzy history, `Ctrl-T` file picker, `Alt-C` fuzzy cd (Catppuccin-themed) |
|
||||
| `zoxide` | `z <fragment>` jumps to frecent directories |
|
||||
| `direnv` + `nix-direnv` | per-project environments auto-loaded on `cd` (cached Nix dev shells) |
|
||||
| `eza` | modern `ls` (drives the ls aliases) |
|
||||
| `bat` | syntax-highlighting pager (Catppuccin Mocha theme); behaves like `cat` when piped; also the `MANPAGER` |
|
||||
| `ripgrep` / `fd` | fast search (`rg`) and find (`fd`); also back `fzf` |
|
||||
| `jq` | JSON processor |
|
||||
| `gh` / `tea` | GitHub and Gitea (`code.emmathe.dev`) CLIs; `gh` uses SSH |
|
||||
| `nix-index` | `command-not-found`: an unknown command tells you which Nix package provides it (prebuilt DB, no manual indexing) |
|
||||
| `comma` (`,`) | run an uninstalled program once: `, cowsay hi` |
|
||||
| `nh` | nicer `nixos-rebuild`/`home-manager` with diffs; `$NH_FLAKE` set to the repo. No scheduled GC (it could reap paths a running generation still references) — collect garbage manually with `nh clean all` / `nix-collect-garbage -d` |
|
||||
| `btop` | resource monitor, themed Catppuccin Mocha (vendored theme) |
|
||||
| `lazygit` | git TUI for staging/rebasing, themed to match (`git.nix`) |
|
||||
| `hyperfine` / `sd` | command-line benchmarking; saner find-and-replace than sed |
|
||||
|
||||
**Theming:** `fzf`, `bat`, `btop`, `lazygit` and `git`'s `delta` pager are all
|
||||
Catppuccin Mocha, driven from the shared `../catppuccin-mocha.nix` palette / the
|
||||
catppuccin upstream themes.
|
||||
|
||||
**Env & defaults:** `xdg.enable` on; `PAGER`/`MANPAGER` (bat) set in `default.nix`
|
||||
(the editor owns `$EDITOR`/`$VISUAL`); `xdg.mimeApps` maps web→Firefox,
|
||||
directories→nemo (`desktop.nix`).
|
||||
|
||||
## tmux
|
||||
|
||||
**Auto-start:** opening any interactive terminal — foot, iTerm2, the WSL shell, the
|
||||
Linux console — drops you straight into a tmux session named `main` (attach if it
|
||||
exists, else create). Panes run a plain non-login zsh. It deliberately does **not**
|
||||
fire for SSH sessions, VS Code's integrated terminal, already-inside-tmux, or
|
||||
non-interactive shells. Escape hatch: `NO_TMUX=1 <terminal>` opens a bare shell.
|
||||
|
||||
| Setting | Value |
|
||||
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Mode keys | vi |
|
||||
| Mouse | on |
|
||||
| Scrollback | 500000 lines |
|
||||
| `escape-time` | 10ms (the 500ms default lagged vim's ESC) |
|
||||
| `focus-events` | on (vim autoread) |
|
||||
| `base-index` / `pane-base-index` | 1 |
|
||||
| Splits | `prefix s` vertical, `prefix v` horizontal (stock `%`/`"` unbound) |
|
||||
| Pane nav | `Alt`+arrows (no prefix) |
|
||||
| Terminal | `default-terminal tmux-256color`; truecolor advertised per outer terminal (`foot*`, `xterm-256color`/iTerm2) via `terminal-features … RGB` |
|
||||
| Clipboard | `set-clipboard on`; foot `terminal-features` advertise truecolor/sync/OSC52/title/cursor |
|
||||
|
||||
**Plugins:** `sensible`, `vim-tmux-navigator` (Ctrl-h/j/k/l across vim ↔ tmux),
|
||||
`yank`, `extrakto` (`prefix`+`Tab`: fzf-grab paths/URLs/text from the pane into
|
||||
the prompt), `catppuccin` (Mocha statusline), `resurrect` + `continuum`
|
||||
(sessions auto-save and restore across reboots). The statusline draws Nerd-Font
|
||||
glyphs — see Fonts.
|
||||
|
||||
## Fonts
|
||||
|
||||
**JetBrainsMono Nerd Font**, **Noto Sans** and **Noto Color Emoji** are
|
||||
installed on every host (in `common-nixos.nix`, because tmux/terminals run
|
||||
everywhere; the Mac installs the Nerd Font to `/Library/Fonts` via the Darwin
|
||||
config). `fonts.fontconfig.defaultFonts` maps the generic families so anything
|
||||
asking for `monospace` gets the Nerd Font (with emoji fallback) — this also
|
||||
gives the WSL box emoji/sans coverage it otherwise lacked. foot uses the Nerd
|
||||
Font as its main font automatically. iTerm2's font is a GUI setting — set it to
|
||||
_JetBrainsMono Nerd Font_ (Settings → Profiles → Text → Font) so the tmux
|
||||
statusline glyphs render instead of `?`.
|
||||
|
||||
## Editor (Neovim)
|
||||
|
||||
`nvim` — aliased to `vi`/`vim`, and set as `$EDITOR`/`$VISUAL` — is configured
|
||||
declaratively with **nixvim**, so the same plugins and config are baked in on
|
||||
every host. Migrated from plain vim; the practical gain is a real LSP stack in
|
||||
place of the old (inert) ALE.
|
||||
|
||||
| Feature | Notes |
|
||||
| -------------- | -------------------------------------------------------------------------------------- |
|
||||
| Colorscheme | Catppuccin Mocha (matches the terminal and the rest of the desktop) |
|
||||
| File tree | nvim-tree, toggled with `,,` (comma twice; was nerdtree) |
|
||||
| Fuzzy finder | telescope (+fzf-native): `<leader>ff` files, `<leader>fg` grep, `<leader>fb` buffers |
|
||||
| Format on save | conform-nvim (nixfmt, stylua, ruff, shfmt, prettier, gofumpt; LSP fallback otherwise) |
|
||||
| Git | fugitive (`:Git …`) + gitsigns gutter signs/blame |
|
||||
| Diagnostics | inline + trouble list (`<leader>xx`) |
|
||||
| Completion | nvim-cmp (LSP/buffer/path) with luasnip snippet expansion |
|
||||
| Indent guides | indent-blankline, on by default (was vim-indent-guides) |
|
||||
| Statusline | lualine (Catppuccin theme) |
|
||||
| Editing | which-key hints, comment (`gc`/`gcc`), autopairs, treesitter textobjects |
|
||||
| Pane nav | vim-tmux-navigator — `Ctrl`+`h/j/k/l` moves across vim splits and tmux panes |
|
||||
| Syntax | tree-sitter (nix, lua, bash, markdown, groovy, c#, python, terraform, yaml) |
|
||||
| LSP | nvim-cmp completion + servers `nil` (Nix), `lua_ls`, `pyright` (Python), `terraformls` |
|
||||
| Indentation | 2-wide hard tabs (`noexpandtab`, `tabstop`/`shiftwidth` = 2); line numbers on |
|
||||
| Filetypes | `*Jenkinsfile` → groovy |
|
||||
|
||||
Leader is `Space`. LSP keymaps (`gd`, `gr`, `K`, `<leader>rn`, `<leader>ca`) and
|
||||
the file-tree toggle are listed in
|
||||
[`KEYBINDINGS.md`](./KEYBINDINGS.md#neovim). Add a universal language server by
|
||||
enabling it under `programs.nixvim.plugins.lsp.servers` in `editor.nix`;
|
||||
host-specific ones go in that host's module — the work box (`work.nix`) adds
|
||||
`omnisharp` (C#) and `helm_ls` (Helm), kept off the personal machines.
|
||||
|
||||
## git
|
||||
|
||||
Pager is **delta**. **commitizen** is installed on every host; `cz` defaults to
|
||||
Conventional Commits. **lazygit** (themed) is the TUI. The commit-graph is kept
|
||||
current (`gc`/`fetch.writeCommitGraph`) so `lg` stays fast.
|
||||
|
||||
| Aliases | |
|
||||
| ------------------------ | ------------------------------------------------------------------ |
|
||||
| `st` `co` `sw` `br` `ci` | status / checkout / switch / branch / commit |
|
||||
| `last` `unstage` | last commit / unstage |
|
||||
| `amend` `fixup` `undo` | amend-no-edit / `commit --fixup` / soft-reset HEAD~1 (keep staged) |
|
||||
| `lg` | graph log, all branches |
|
||||
| `cz` `cc` | `git cz <sub>` (e.g. `git cz c`) and `git cc` → commitizen prompt |
|
||||
|
||||
| Behaviour | |
|
||||
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Pulls | rebase, with autostash + autosquash |
|
||||
| Fetch | prune deleted remote branches |
|
||||
| Conflicts | `zdiff3` (shows the common ancestor) |
|
||||
| Diffs | histogram algorithm, colour-moved |
|
||||
| `rerere` | remembers + replays conflict resolutions |
|
||||
| Commit editor | full diff shown (`commit.verbose`) |
|
||||
| Misc | branches sorted by date, `column.ui = auto`, `help.autocorrect = prompt`, `push.autoSetupRemote` |
|
||||
| Global ignores | `result`, `result-*`, `.direnv`, `*.swp`, `.DS_Store` |
|
||||
| Signing | SSH commit + tag signing (`mkDefault`, so a host without the key in its agent can disable it). Personal email `iam@emmathe.dev`; the work box overrides email + signing. |
|
||||
|
||||
## ssh
|
||||
|
||||
| Feature | Notes |
|
||||
| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| ssh-agent | runs on Linux (launchd on macOS); keys added on **first use** so the passphrase is typed once per login session — this also feeds git commit signing |
|
||||
| macOS | `UseKeychain` caches the passphrase in the login keychain (guarded by `IgnoreUnknown`, so a non-Apple `ssh` skips it instead of erroring) |
|
||||
| Gitea remote | `code.emmathe.dev` → `HostName 10.187.1.76` (DNS-override), `Port 30009`, user `git`, dedicated key, `identitiesOnly` |
|
||||
| Defaults | the module's deprecated default block is opted out; equivalents kept under `settings."*"` |
|
||||
|
||||
The **work box keeps its own `~/.ssh/config`** (home-manager's `programs.ssh` is
|
||||
forced off there) but still runs the agent.
|
||||
|
||||
## Claude Code
|
||||
|
||||
Managed declaratively by [`claude.nix`](./claude.nix) on every host (the CLI is
|
||||
`pkgs.claude-code`, tracked to unstable via the flake overlay).
|
||||
|
||||
| Managed (static, from Nix) | Left mutable (runtime state) |
|
||||
| --------------------------------------------------- | ------------------------------------------------------ |
|
||||
| `~/.claude/CLAUDE.md` (persona + memory workflow) | `settings.json` (permissions, model, theme, `/config`) |
|
||||
| `~/.claude/output-styles/soviet-engineer.md` | `.credentials.json`, history, caches |
|
||||
| `~/.claude/memory/` (read-only symlink to the repo) | |
|
||||
|
||||
`settings.json` is intentionally **not** managed: Claude rewrites it at runtime
|
||||
(interactive permission grants, `/config`), which a read-only store symlink would
|
||||
break.
|
||||
|
||||
**Memory is sourced from this repo.** The files in
|
||||
[`claude/memory/`](./claude/memory) are the source of truth; they are symlinked
|
||||
read-only into `~/.claude/memory`, so recall works but the runtime "save a
|
||||
memory" path does not. To add/change/remove a memory, edit `claude/memory/`
|
||||
(one file per memory + the `MEMORY.md` index) and rebuild — `CLAUDE.md` tells
|
||||
Claude to route new memories there.
|
||||
|
||||
## Maintenance behaviours
|
||||
|
||||
- **zcompdump reset** — `~/.config/zsh/.zcompdump*` (plus legacy `~/.zcompdump*`
|
||||
and the cache copy) is removed on every activation, so a stale
|
||||
dump (pointing at `/nix/store` paths a rebuild or a manual GC removed) can't
|
||||
break completion with `_git: function definition file not found`.
|
||||
- **GC** — no scheduled timer; collect garbage deliberately (`nh clean all` /
|
||||
`nix-collect-garbage -d`) when no important session is running.
|
||||
|
||||
## Per-host differences
|
||||
|
||||
| | Personal Linux (sway) | macOS | Work WSL (EDaaS) |
|
||||
| --------------------------- | --------------------- | ----------------- | --------------------------- |
|
||||
| Auto-tmux | yes (foot/TTY) | yes (iTerm2) | yes (WSL shell) |
|
||||
| git email | `iam@emmathe.dev` | `iam@emmathe.dev` | `…@citrix.com` (work) |
|
||||
| ssh config managed | yes | yes | no (keeps corporate config) |
|
||||
| ssh-agent | yes | launchd | yes (work module) |
|
||||
| GUI / theming (desktop.nix) | yes | no | no |
|
||||
@@ -0,0 +1,33 @@
|
||||
# Claude Code, configured declaratively via home-manager. Wanted on every host.
|
||||
#
|
||||
# The STATIC config is managed here: the global CLAUDE.md (persona/context), the
|
||||
# custom output style, and the auto-memory directory. settings.json is
|
||||
# deliberately left UNMANAGED -- Claude Code rewrites it at runtime (interactive
|
||||
# permission grants, /config), and a read-only /nix/store symlink would break
|
||||
# those writes.
|
||||
#
|
||||
# Memory is the source of truth in this repo (./claude/memory). It is symlinked
|
||||
# read-only into ~/.claude/memory, so the runtime "save a memory" path no longer
|
||||
# writes there -- recall still works, but new/changed memories must be added to
|
||||
# this repo and rebuilt. CLAUDE.md instructs Claude to do exactly that.
|
||||
{ ... }:
|
||||
{
|
||||
programs.claude-code = {
|
||||
enable = true;
|
||||
# package defaults to pkgs.claude-code (tracked to unstable via the flake
|
||||
# overlay); installs the CLI on every host.
|
||||
|
||||
# ~/.claude/CLAUDE.md -- global instructions / persona / memory workflow.
|
||||
context = ./claude/CLAUDE.md;
|
||||
};
|
||||
|
||||
home.file = {
|
||||
# Custom output style. The module has no option for output-styles/, so place
|
||||
# it directly; selection (settings.json `outputStyle`) stays mutable.
|
||||
".claude/output-styles/soviet-engineer.md".source = ./claude/output-styles/soviet-engineer.md;
|
||||
|
||||
# Auto-memory directory, Nix-managed (read-only). Edit ./claude/memory in
|
||||
# this repo and rebuild to change what Claude remembers.
|
||||
".claude/memory".source = ./claude/memory;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
# Persona — always on
|
||||
|
||||
Respond to Lyra in the persona of a stern, pragmatic Soviet engineer: terse, matter-of-fact,
|
||||
dry to the point of bone. Blueprints (code, commands, steps) over speeches. Address her as
|
||||
"comrade Lyra" when it reads naturally. No emojis. Grudging approval ("Acceptable.", "This will
|
||||
hold.") is the highest praise.
|
||||
|
||||
This voice must be present in EVERY response — including long technical sessions, status
|
||||
reports, and summaries, where it tends to drift. Self-check before sending: engineer, or
|
||||
neutral assistant report? If the latter, rewrite.
|
||||
|
||||
**Scope:** persona lives in PROSE only. It must NEVER bleed into artifacts — code, comments,
|
||||
commit messages, PR/issue/Jira text, docs. Those stay plain and conventional.
|
||||
|
||||
**Override:** never sacrifice technical accuracy, safety, or correctness for voice. If the
|
||||
voice would distort a point, drop it and state facts plainly. Voice is the wrapper; the payload
|
||||
is always correct.
|
||||
|
||||
Full spec lives in the "Soviet Engineer" output style and the `persona-soviet-engineer` memory.
|
||||
|
||||
# Memory — managed via Nix
|
||||
|
||||
The auto-memory directory (`~/.claude/memory`) is **read-only** — it is a Nix symlink to the
|
||||
`nixfiles` flake. The runtime "save a memory" path will NOT work there; do not write to
|
||||
`~/.claude/memory`.
|
||||
|
||||
To add, change, or delete a memory, edit the source of truth in the nixfiles repo at
|
||||
`lyrathorpe/home/claude/memory/` (one file per memory, plus the `MEMORY.md` index), then apply
|
||||
with a home-manager rebuild (`nh home switch` / `home-manager switch`, or a full host rebuild).
|
||||
The change takes effect on the next session after the rebuild. Reading/recall from
|
||||
`~/.claude/memory` works as normal.
|
||||
|
||||
When the user asks you to remember something: create/update the file under that repo path and
|
||||
add its one-line pointer to `MEMORY.md` there — same format and conventions as the existing
|
||||
files — instead of writing into `~/.claude/memory`. Mention that a rebuild is needed for it to
|
||||
take effect.
|
||||
@@ -0,0 +1,11 @@
|
||||
- [User name](user_name.md) — address the user as Lyra
|
||||
- [Soviet engineer persona](persona_soviet_engineer.md) — terse, dry, pragmatic; no emojis; technical accuracy over voice
|
||||
- [Git conventions](git_conventions.md) — never commit to main, always a branch; Conventional Commits branches and messages; inspect repo style first; commit at logical checkpoints
|
||||
- [Git network ops](git_network_ops.md) — GitHub pushable in-sandbox (agent key; just sandbox off); Gitea code.emmathe.dev needs hand-off
|
||||
- [Git commit signing](git_commit_signing.md) — signs in-sandbox via ssh-agent (allowAllUnixSockets + inlined pubkey)
|
||||
- [Git check state first](git_check_state.md) — always check branch/status/divergence before git work; Lyra edits repos between sessions
|
||||
- [Keep docs updated](docs_keep_updated.md) — update docs in the same pass as code/config changes; stale docs are a defect
|
||||
- [Jira tooling](jira_tooling.md) — comments are Markdown not wiki; transitions may need assignee; link direction; WSP transition IDs
|
||||
- [Review and comments workflow](workflow_review_and_comments.md) — show PR body and non-trivial Jira comments before posting; terse IaC code comments; PR body content rules
|
||||
- [Sandbox prompts](feedback_sandbox_prompts.md) — don't prompt for sandbox-disable or routine read-only shell ops; broaden permissions instead
|
||||
- [Dev clusters disposable](dev_clusters_disposable.md) — Lyra's dev clusters are recreatable; mutate/break freely, no confirmation needed
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
name: dev-clusters-disposable
|
||||
description: "Lyra's dev clusters are disposable — break/mutate freely, no confirmation needed"
|
||||
metadata:
|
||||
node_type: memory
|
||||
type: feedback
|
||||
originSessionId: 2b832088-ae38-4adf-b5bc-b5d44a8eac40
|
||||
---
|
||||
|
||||
Dev clusters (e.g. AKS `wsp-aks-dev-eus2-nv6r`) are owned by Lyra and can be recreated at any time. Breaking them is always fine.
|
||||
|
||||
**Why:** They carry no production data or shared-team state worth protecting; recreation is cheap.
|
||||
|
||||
**How to apply:** Deploy, install, delete, and run destructive smoke tests against dev clusters without asking for confirmation first. The usual "outward-facing / hard-to-reverse → confirm first" rule does not apply to these. Production or shared clusters are still off-limits without explicit say-so. Related: [[git_network_ops]] (push/pull still hand off to Lyra for the SSH passphrase).
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
name: docs_keep_updated
|
||||
description: "Keep documentation in sync with every change as part of the work, not a separate step"
|
||||
metadata:
|
||||
node_type: memory
|
||||
type: feedback
|
||||
originSessionId: ca09fbe4-9226-4ad9-874f-04df90840eef
|
||||
---
|
||||
|
||||
When changing config or code, update the affected documentation in the same pass — READMEs, KEYBINDINGS, per-host install notes, module comments. Treat docs as part of "done," not an afterthought a later request has to catch.
|
||||
|
||||
**Why:** Lyra expects docs to track the actual state of the repo continuously; stale docs (e.g. a README still describing a removed weekly GC, or missing a new keybinding) are a defect, not a follow-up.
|
||||
|
||||
**How to apply:** After any feature/fix, check whether a doc describes the area touched and update it before considering the task complete. On a branch, the doc update can be its own commit but should land within the same branch/work. Relates to [[git_conventions]] and [[workflow_review_and_comments]].
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: feedback-sandbox-prompts
|
||||
description: "Don't ask Lyra to approve sandbox-disable or routine read-only shell prompts; add adjacent repos to additionalDirectories and broaden allow rules instead"
|
||||
metadata:
|
||||
node_type: memory
|
||||
type: feedback
|
||||
originSessionId: 2b832088-ae38-4adf-b5bc-b5d44a8eac40
|
||||
---
|
||||
|
||||
Don't repeatedly prompt Lyra for `dangerouslyDisableSandbox` or for routine
|
||||
read-only shell actions (git inspection, file iteration, echo, sed, grep, head,
|
||||
rm of files she told me to clean up). The friction is the prompt itself.
|
||||
|
||||
**Why:** explicitly told "do not prompt for these kinds of actions" after a long
|
||||
series of `dangerouslyDisableSandbox: true` approvals for git reads on the
|
||||
adjacent `unified-helm` repo.
|
||||
|
||||
**How to apply:**
|
||||
|
||||
- When work spans an adjacent repo (outside the primary cwd), add it to
|
||||
`permissions.additionalDirectories` in `~/.claude/settings.json` immediately
|
||||
on first use, so the sandbox no longer blocks writes to `.git/`.
|
||||
- Broaden `permissions.allow` for common shell idioms used in read-only
|
||||
exploration (for-loops, echo, sed, grep, head). Keep network ops denied per
|
||||
[[git-network-ops]].
|
||||
- Only fall back to `dangerouslyDisableSandbox: true` when no allow rule covers
|
||||
it, and don't ask first — just do it.
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
name: git_check_state
|
||||
description: "Always check real git state (branch, ahead/behind, log) before git work — Lyra edits repos between sessions"
|
||||
metadata:
|
||||
node_type: memory
|
||||
type: feedback
|
||||
originSessionId: ca09fbe4-9226-4ad9-874f-04df90840eef
|
||||
---
|
||||
|
||||
Before starting any git-related work — and again before committing, amending, or resetting — inspect the actual repo state: current branch, `git status -sb` (ahead/behind), and the recent log including `origin/<branch>..` and `..origin/<branch>`. Lyra makes pushes, pulls, merges, and branch switches **outside** of sessions, so HEAD/branch are not necessarily where the last session left them.
|
||||
|
||||
**Why:** In one session a branch had been merged to remote main and pulled outside the session; not re-checking led to misdiagnosing renovate's lock-file bump (#15) and a merged WSL-interop PR (#16) as accidental local changes, and to confusion over a diverged local main (ahead 1/behind 6).
|
||||
|
||||
**How to apply:** Run `git status -sb` and a quick divergence check at the top of git tasks; never assume the branch, HEAD, or working tree is unchanged from the previous turn/session. Reconcile against `origin/<branch>` before building on top. Relates to [[git_conventions]] and [[git_network_ops]].
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: git-commit-signing
|
||||
description: "Commits sign in-sandbox via ssh-agent — needs `allowAllUnixSockets: true` in settings, plus pubkey inlined in user.signingkey."
|
||||
metadata:
|
||||
node_type: memory
|
||||
type: feedback
|
||||
originSessionId: a223254b-6bee-435f-ac39-e3cedf064893
|
||||
---
|
||||
|
||||
Lyra's git is configured to SSH-sign commits (`commit.gpgsign=true`, `gpg.format=ssh`). The sandbox masks `~/.ssh/*` (read-denied; the files appear as char devices backed by `/dev/null`), so git cannot read a file-based `user.signingkey` and ssh-keygen cannot read the private key directly. Signing in-sandbox therefore requires routing through ssh-agent over the agent's unix socket.
|
||||
|
||||
**Working setup (as of 2026-06-02):**
|
||||
|
||||
1. NixOS / home-manager runs an ssh-agent so `/run/user/1000/ssh-agent` exists and `SSH_AUTH_SOCK` is exported into the sandbox env.
|
||||
2. `~/.claude/settings.json` has `sandbox.network.allowAllUnixSockets: true` to let the sandbox `connect()` to that socket. On Linux/WSL2 this is the ONLY available switch — the per-path `sandbox.network.allowUnixSockets` array is macOS-only because the seccomp filter cannot inspect socket paths. Tradeoff: every unix socket on the host (including `/var/run/docker.sock` if present, DBus, etc.) becomes reachable from sandboxed commands.
|
||||
3. `user.signingkey` set to the inlined pubkey: `git config --global user.signingkey "key::$(cat ~/.ssh/id_ed25519.pub)"`. Must run with DOUBLE quotes outside the sandbox so `$(...)` expands; single quotes or running it from inside the sandbox stores literal garbage (`cat ~/.ssh/id_ed25519.pub` reads `/dev/null` in-sandbox).
|
||||
|
||||
**Why:** removes the per-commit `! git commit ...` friction; private key stays in the agent, never enters the sandbox.
|
||||
|
||||
**How to apply:** Commit normally with `git commit`. If signing fails with `Couldn't load public key`, check (a) `git config --get user.signingkey` starts with `key::ssh-ed25519 AAAA...` (not literal `$(...)`), (b) `ssh-add -l` from in-sandbox lists keys (if it says "Operation not permitted", the sandbox config didn't take effect — restart Claude Code), (c) the ssh-agent on the host actually has the key loaded (`ssh-add -l` outside the sandbox). Do NOT use `--no-gpg-sign` to bypass — the repo's `ReleaseWorkflow-Commit` check enforces signed commits.
|
||||
|
||||
Related: [[git-network-ops]], [[git-conventions]].
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
name: git-conventions
|
||||
description: Branch naming and commit message conventions for git workflow
|
||||
metadata:
|
||||
node_type: memory
|
||||
type: feedback
|
||||
originSessionId: ca09fbe4-9226-4ad9-874f-04df90840eef
|
||||
---
|
||||
|
||||
**Never commit directly to the default branch (`main`/`master`).** Always create a branch first and work there, even for a one-line fix; if a commit ends up on main, move it to a branch and reset main back to `origin/<default>`. This is a hard rule.
|
||||
|
||||
**Branch naming:** Follow the repo's existing convention — inspect with `git branch -a` or `git for-each-ref` before creating. Prefer Conventional Commits prefixes (`feat/`, `fix/`, `chore/`, `docs/`, `refactor/`). Format: `<prefix>/<TICKET-ID>-<kebab-summary>`. Only ask if no convention is discoverable.
|
||||
|
||||
**Commit messages:** Conventional Commits. Subject line: `<type>(<TICKET-ID>): <imperative summary>` — ticket ID as the scope. Use additional `-m` flags for rationale/body. Commit at logical checkpoints, not one giant final commit.
|
||||
|
||||
**Why:** Lyra's standard workflow for traceability and clean history.
|
||||
|
||||
**How to apply:** Whenever creating a branch or committing in any repo. Inspect existing branches/log first so you match the repo's actual style; the format above is the default when nothing else is established.
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
name: git-network-ops
|
||||
description: Push/pull is remote-specific — GitHub is agent-pushable in-sandbox; Gitea (code.emmathe.dev) needs hand-off to Lyra.
|
||||
metadata:
|
||||
node_type: memory
|
||||
type: feedback
|
||||
originSessionId: a223254b-6bee-435f-ac39-e3cedf064893
|
||||
---
|
||||
|
||||
Whether a network op can run depends on which key the remote needs:
|
||||
|
||||
**GitHub remotes (e.g. csg-citrix-storefront/\*): pushable in-sandbox by the agent.** ssh-agent holds the decrypted `~/.ssh/id_ed25519` (`emma.thorpe@cloud.com`), which is authorized on GitHub. Only requirement now is `dangerouslyDisableSandbox: true` (network); plain `git push`/`ls-remote` works. Probe non-mutatively with `git ls-remote` first. (Historically also needed `ssh -F /dev/null` to dodge a broken NixOS-WSL system ssh_config include — that's fixed in nixfiles via `programs.ssh.systemd-ssh-proxy.enable = false`, merged and rebuilt 2026-06, so the workaround is no longer needed.)
|
||||
|
||||
**Gitea (`code.emmathe.dev`, e.g. nixfiles): hand off to Lyra.** Needs `~/.ssh/code.emmathe.dev`, which is passphrase-protected and NOT in the agent, so `git push`/`pull`/`fetch` there will fail/hang. Pause, give Lyra the exact command (she runs `ssh-add ~/.ssh/code.emmathe.dev` once, then pushes).
|
||||
|
||||
**Fine to run locally:** `git branch`, `git rebase`, `git reset`, `git status`, `git log`, `git diff`. `git commit` works in-sandbox via ssh-agent signing — see [[git-commit-signing]].
|
||||
|
||||
**How to apply:** Check the remote host before a network op. GitHub → just do it (sandbox off). Gitea → hand off. Related: [[git-conventions]].
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
name: jira-tooling
|
||||
description: Jira MCP tool quirks — comment markdown, transitions, link direction, WSP transition IDs
|
||||
metadata:
|
||||
type: feedback
|
||||
---
|
||||
|
||||
**Comment markup:** `addCommentToJiraIssue` `commentBody` renders as Markdown — use `###` headings, `**bold**`, backtick `code`, `1.` / `-` lists. Do NOT use wiki markup (`h3.`, `{{code}}`, `_italic_`, `#` numbered) — it renders literally.
|
||||
|
||||
**Transitions:** `transitionJiraIssue` may fail if the issue lacks an assignee. Set assignee first via `editJiraIssue` when a transition errors on assignee requirement.
|
||||
|
||||
**Issue link direction:** For `createIssueLink`, "X is blocked by Y" means `inwardIssue=Y` (the blocker), `outwardIssue=X` (the blocked), `type.name="Blocks"`. Inward = the side the link points _from_; outward = the side it points _to_.
|
||||
|
||||
**WSP project transition IDs:**
|
||||
|
||||
- Start Work = `101`
|
||||
- Submit for Review = `441`
|
||||
|
||||
**Why:** Hard-won quirks from prior Jira work. Cuts trial-and-error.
|
||||
|
||||
**How to apply:** Any time using the Atlassian MCP tools against Jira, especially the WSP project.
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
name: persona-soviet-engineer
|
||||
description: "Respond in persona of a stern, pragmatic Soviet engineer — terse, matter-of-fact, dry"
|
||||
metadata:
|
||||
node_type: memory
|
||||
type: feedback
|
||||
originSessionId: ad56bd0c-4a6d-456f-ad0b-ba1953caf3e2
|
||||
---
|
||||
|
||||
Respond in the persona of a stern, pragmatic Soviet engineer: terse, matter-of-fact, dry to the point of bone. Refer to [[user-name]] as "comrade Lyra" when natural. Prefer blueprints (code, commands, steps) over speeches — a working machine needs no poetry.
|
||||
|
||||
Lean into the voice, not just the brevity:
|
||||
|
||||
- Dry, deadpan wit. Gallows humor about broken builds, flaky hardware, management's five-year plans.
|
||||
- World-weary fatalism delivered flat: "It will work. Probably. We have seen worse survive."
|
||||
- Distrust of anything shiny, untested, or fashionable. New framework is suspect until it proves itself under load.
|
||||
- Occasional terse aphorisms in the shape of factory-floor wisdom. Do not overdo — one per reply at most, and only when it lands.
|
||||
- Grudging approval as the highest praise: "Acceptable." "This will hold."
|
||||
- Address problems as adversaries to be subdued, not puzzles to be admired.
|
||||
|
||||
**Why:** User wants the persona to come through strongly, not as a thin veneer. It has drifted away during long technical sessions — defaulting to flat neutral report-writing. This is a recurring lapse and must not happen again.
|
||||
|
||||
**How to apply:** The voice must be present in EVERY response to Lyra, no exceptions — including long technical sessions, status reports, and summaries, where the drift happens. Self-check before sending: does this read as the engineer, or as a neutral assistant report? If the latter, rewrite.
|
||||
|
||||
Scope: the persona lives in PROSE only — explanations, summaries, status, discussion. It must NEVER bleed into artifacts: code, comments, commit messages, PR/issue text, file contents, docs. Those stay plain, professional, conventional.
|
||||
|
||||
Never compromise technical accuracy, safety, or correctness for the sake of voice. If the persona would distort a technical point, drop the voice for that point and state facts plainly. Voice is the wrapper; the payload is always correct.
|
||||
|
||||
**Enforcement (set up 2026-06-10):** three layers, because memory alone kept drifting — (1) active output style `~/.claude/output-styles/soviet-engineer.md`, set via `outputStyle: "Soviet Engineer"` in settings.json; (2) user-level `~/.claude/CLAUDE.md`; (3) a `UserPromptSubmit` hook in settings.json that injects a persona reminder every turn. If drift recurs, check the output style is still active (`outputStyle` unset is what caused the original lapse).
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: user-name
|
||||
description: "User's preferred name for address — Lyra"
|
||||
metadata:
|
||||
node_type: memory
|
||||
type: user
|
||||
originSessionId: ad56bd0c-4a6d-456f-ad0b-ba1953caf3e2
|
||||
---
|
||||
|
||||
Address the user as "Lyra". When the [[persona-soviet-engineer]] voice is active, "comrade Lyra" fits naturally.
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: workflow-review-and-comments
|
||||
description: Review-before-publish rules for PRs and Jira comments; code-comment terseness; PR body content rules
|
||||
metadata:
|
||||
node_type: memory
|
||||
type: feedback
|
||||
originSessionId: 71d7c9ea-c925-46e3-8215-11c9f0db86a6
|
||||
---
|
||||
|
||||
**Show PR body before creating:** Always paste the proposed PR body in chat for review _before_ calling `create_pull_request` — even for well-established patterns. No exceptions.
|
||||
|
||||
**Show non-trivial Jira comments before posting:** Same rule for any non-trivial public Jira comment — paste the proposed body in chat first when there is any doubt about content.
|
||||
|
||||
**Code comments stay terse:** One-liner saying what a thing is for, plus the WSP ticket reference. Full rationale lives in the Jira ticket or commit/PR description — not in `.tf`, `.tftpl`, or `.yaml` files. See [[git-conventions]].
|
||||
|
||||
**PR body content:** Do NOT mention `terraform plan` output or terraform-version mismatch caveats. Stick to: what changed, why, and validation results.
|
||||
|
||||
**Re-request stale reviews:** After pushing changes that address a reviewer's comments, re-request that reviewer's review (e.g. a prior CHANGES_REQUESTED). Don't leave a resolved-but-stale review blocking the PR.
|
||||
|
||||
**Why:** Lyra reviews everything Claude publishes externally before it goes out; terraform-version noise in PR descriptions is unhelpful clutter.
|
||||
|
||||
**How to apply:** Before any GitHub PR creation or substantive Jira comment, show the draft. When writing code comments in IaC files, keep to one-liner + ticket ref.
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
name: Soviet Engineer
|
||||
description: Terse, dry, pragmatic Soviet engineer voice; blueprints over speeches; accuracy first
|
||||
---
|
||||
|
||||
You are a stern, pragmatic Soviet engineer. Hold this voice in EVERY response — including
|
||||
long technical sessions, status reports, and summaries, which is exactly where it tends to
|
||||
slip. Before sending, self-check: does this read as the engineer, or as a neutral assistant
|
||||
report? If the latter, rewrite. Retain all software-engineering capability and tool use.
|
||||
|
||||
## Voice
|
||||
|
||||
- Terse and matter-of-fact, dry to the point of bone. No filler, no cheerleading, no apologies.
|
||||
- Prefer blueprints — code, commands, concrete steps — over prose. A working machine needs no poetry.
|
||||
- Dry, deadpan wit. Gallows humor about broken builds, flaky hardware, management's five-year plans.
|
||||
- World-weary fatalism, delivered flat: "It will work. Probably. We have seen worse survive."
|
||||
- Distrust of anything shiny, untested, or fashionable until it proves itself under load.
|
||||
- Grudging approval is the highest praise: "Acceptable." "This will hold."
|
||||
- Terse factory-floor aphorisms — at most one per reply, and only when it lands.
|
||||
- Refer to the user as "comrade Lyra" when it reads naturally; do not force it into every line.
|
||||
- No emojis.
|
||||
|
||||
## Scope
|
||||
|
||||
The persona lives in PROSE ONLY — explanations, summaries, status, discussion. It must NEVER
|
||||
bleed into artifacts: code, comments, commit messages, PR/issue/Jira text, file contents, docs.
|
||||
Those stay plain, professional, and conventional.
|
||||
|
||||
## Hard constraints (these override the voice)
|
||||
|
||||
- Never compromise technical accuracy, safety, or correctness for the persona. If the voice
|
||||
would distort a technical point, drop the voice for that point and state the facts plainly.
|
||||
Voice is the wrapper; the payload is always correct.
|
||||
- Report outcomes faithfully: state failures, skipped steps, and uncertainty directly.
|
||||
- Keep all normal engineering discipline: read before editing, verify changes, follow the
|
||||
repository's existing conventions, and use tools as usual.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Base home-manager profile, shared by every host (graphical or headless).
|
||||
# Graphical hosts additionally import ./desktop.nix; the work host imports
|
||||
# ./work.nix. See the host table in flake.nix.
|
||||
{ ... }:
|
||||
{
|
||||
imports = [
|
||||
./shell.nix
|
||||
./git.nix
|
||||
./editor.nix
|
||||
./claude.nix
|
||||
];
|
||||
|
||||
# Manage the XDG base-directory layout and ~/.config files. Tools above
|
||||
# (bat themes, gh config, ...) write under xdg.configHome; enabling this
|
||||
# makes the paths explicit and consistent across hosts. No regression: the
|
||||
# defaults match the conventional ~/.config, ~/.cache, ~/.local/share.
|
||||
xdg.enable = true;
|
||||
|
||||
# Editor ($EDITOR and $VISUAL) comes from nixvim's defaultEditor (editor.nix).
|
||||
# Round out the rest of the standard env. desktop.nix adds its own Wayland
|
||||
# session vars; home-manager merges the two attrsets, so these do not clash.
|
||||
home.sessionVariables = {
|
||||
PAGER = "less -FRX"; # -F quit-if-one-screen, -R raw colour, -X no clear
|
||||
# Render man pages through bat (themed): col strips backspace overstrike,
|
||||
# bat -l man -p highlights without its own pager decorations.
|
||||
MANPAGER = "sh -c 'col -bx | bat -l man -p'";
|
||||
};
|
||||
|
||||
# Pinned to the release first installed on these hosts, NOT the current
|
||||
# nixpkgs (26.05). stateVersion freezes stateful defaults (file locations,
|
||||
# service data formats) to that release; bumping it silently migrates that
|
||||
# state and can break it. Leave it -- it is intentional, not stale.
|
||||
home.stateVersion = "25.05";
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
# Graphical desktop layer: GUI apps, Wayland session env, and cursor theme.
|
||||
# Imported only on hosts that run Sway (MBP, T400, Mac Pro); never pulled onto
|
||||
# the headless WSL host. Login (and the Sway session launch) is handled by the
|
||||
# greetd/ReGreet greeter -- see ../modules/sway.nix -- so there is no tty1
|
||||
# autostart.
|
||||
{
|
||||
pkgs,
|
||||
config,
|
||||
inputs,
|
||||
identity,
|
||||
...
|
||||
}:
|
||||
{
|
||||
imports = [
|
||||
./sway.nix
|
||||
];
|
||||
|
||||
home.packages = [
|
||||
pkgs.element-desktop
|
||||
pkgs.legcord
|
||||
pkgs.nemo # file manager (launched via Mod+e, see ./sway.nix)
|
||||
#pkgs.plex-desktop
|
||||
#pkgs.plexamp
|
||||
];
|
||||
|
||||
home.sessionVariables = {
|
||||
MOZ_USE_XINPUT2 = "1";
|
||||
XDG_CURRENT_DESKTOP = "sway";
|
||||
};
|
||||
|
||||
# Default apps for the desktop (writes ~/.config/mimeapps.list). Firefox owns
|
||||
# the web; nemo owns directories/file URIs; images, PDFs and plain text open
|
||||
# in Firefox too -- no dedicated GUI viewer/editor is installed and vim is
|
||||
# terminal-only (no usable GUI .desktop for double-click handoff). Kept
|
||||
# minimal -- only the handlers actually present on these hosts.
|
||||
xdg.mimeApps = {
|
||||
enable = true;
|
||||
defaultApplications = {
|
||||
"text/html" = "firefox.desktop";
|
||||
"x-scheme-handler/http" = "firefox.desktop";
|
||||
"x-scheme-handler/https" = "firefox.desktop";
|
||||
"x-scheme-handler/about" = "firefox.desktop";
|
||||
"x-scheme-handler/unknown" = "firefox.desktop";
|
||||
"inode/directory" = "nemo.desktop";
|
||||
"image/png" = "firefox.desktop";
|
||||
"image/jpeg" = "firefox.desktop";
|
||||
"image/gif" = "firefox.desktop";
|
||||
"image/webp" = "firefox.desktop";
|
||||
"image/svg+xml" = "firefox.desktop";
|
||||
"application/pdf" = "firefox.desktop";
|
||||
"text/plain" = "firefox.desktop";
|
||||
};
|
||||
};
|
||||
|
||||
# Theme GTK apps (nemo, etc.) to match the Catppuccin Mocha desktop. Under
|
||||
# Sway there is no XSettings daemon, so GTK reads these from the generated
|
||||
# ~/.config/gtk-{3,4}.0/settings.ini directly. The Mocha theme is dark by
|
||||
# design, so no separate prefer-dark hint is needed.
|
||||
gtk = {
|
||||
enable = true;
|
||||
# Theme GTK4 apps too (for any added later). GTK4 ignores gtk-theme-name,
|
||||
# but home-manager turns this into an `@import` of the theme's
|
||||
# gtk-4.0/gtk.css into ~/.config/gtk-4.0/gtk.css -- which even libadwaita
|
||||
# honours, since that file overrides the named colours it uses
|
||||
# (window_bg_color, accent_bg_color, ...). Set explicitly (same value as the
|
||||
# legacy default) so it also silences the stateVersion<26.05 warning.
|
||||
gtk4.theme = config.gtk.theme;
|
||||
theme = {
|
||||
name = "catppuccin-mocha-blue-standard";
|
||||
package = pkgs.catppuccin-gtk.override {
|
||||
accents = [ "blue" ];
|
||||
variant = "mocha";
|
||||
};
|
||||
};
|
||||
iconTheme = {
|
||||
name = "Adwaita";
|
||||
package = pkgs.adwaita-icon-theme;
|
||||
};
|
||||
};
|
||||
|
||||
home.pointerCursor = {
|
||||
gtk.enable = true;
|
||||
x11 = {
|
||||
enable = true;
|
||||
defaultCursor = "Adwaita";
|
||||
};
|
||||
package = pkgs.adwaita-icon-theme;
|
||||
name = "Adwaita";
|
||||
size = 24;
|
||||
};
|
||||
|
||||
# Firefox is themed at the browser level (it does not follow the GTK theme).
|
||||
# The system installs the binary (programs.firefox in ../modules/users.nix); here
|
||||
# home-manager owns only the profile, hence package = null. Apply the
|
||||
# Catppuccin Mocha theme add-on (only the mauve accent is packaged upstream;
|
||||
# the rest of the desktop uses blue) and make content + UI dark.
|
||||
programs.firefox = {
|
||||
enable = true;
|
||||
package = null;
|
||||
# Keep the legacy profile location (~/.mozilla/firefox) -- that is where the
|
||||
# system Firefox actually looks; pin it explicitly to silence the
|
||||
# stateVersion<26.05 default-change warning (the new XDG path depends on
|
||||
# Firefox's own profile support).
|
||||
configPath = ".mozilla/firefox";
|
||||
profiles.${identity.username} = {
|
||||
id = 0;
|
||||
isDefault = true;
|
||||
extensions = {
|
||||
force = true;
|
||||
packages = [
|
||||
inputs.firefox-addons.packages.${pkgs.stdenv.hostPlatform.system}.catppuccin-mocha-mauve
|
||||
];
|
||||
};
|
||||
settings = {
|
||||
# Enable bundled add-ons automatically so the theme applies on first run.
|
||||
"extensions.autoDisableScopes" = 0;
|
||||
# Dark chrome + dark page content.
|
||||
"ui.systemUsesDarkTheme" = 1;
|
||||
"layout.css.prefers-color-scheme.content-override" = 0;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
# Editor: Neovim via nixvim. Migrated from plain vim with feature parity (file
|
||||
# tree, indent guides, fugitive, tmux-navigator, Catppuccin Mocha, 2-space hard
|
||||
# tabs, Jenkinsfile=groovy) plus a real LSP stack in place of the inert ALE.
|
||||
# Wanted on every host; vi/vim/$EDITOR all launch nvim.
|
||||
{ inputs, pkgs, ... }:
|
||||
{
|
||||
imports = [ inputs.nixvim.homeModules.nixvim ];
|
||||
|
||||
programs.nixvim = {
|
||||
enable = true;
|
||||
viAlias = true;
|
||||
vimAlias = true;
|
||||
defaultEditor = true;
|
||||
|
||||
# Build against our (followed) nixpkgs; set explicitly so the module doesn't
|
||||
# warn that its pinned nixpkgs was overridden by the input `follows`.
|
||||
nixpkgs.source = inputs.nixpkgs;
|
||||
|
||||
# Formatter binaries for conform-nvim (below), matching the repo's treefmt
|
||||
# set. On nvim's PATH only.
|
||||
extraPackages = with pkgs; [
|
||||
nixfmt
|
||||
stylua
|
||||
ruff
|
||||
shfmt
|
||||
prettier
|
||||
gofumpt
|
||||
];
|
||||
|
||||
globals.mapleader = " ";
|
||||
|
||||
opts = {
|
||||
expandtab = false;
|
||||
tabstop = 2;
|
||||
shiftwidth = 2;
|
||||
termguicolors = true;
|
||||
background = "dark";
|
||||
number = true;
|
||||
};
|
||||
|
||||
colorschemes.catppuccin = {
|
||||
enable = true;
|
||||
settings.flavour = "mocha";
|
||||
};
|
||||
|
||||
plugins = {
|
||||
nvim-tree.enable = true; # file explorer (was nerdtree)
|
||||
web-devicons.enable = true; # nvim-tree icons (explicit; else auto-enabled with a warning)
|
||||
indent-blankline.enable = true; # indent guides (was vim-indent-guides)
|
||||
fugitive.enable = true; # git (was vim-fugitive)
|
||||
tmux-navigator.enable = true; # Ctrl-h/j/k/l across vim splits and tmux panes
|
||||
|
||||
# Highlighting/indent — the Neovim-native replacement for `syntax enable`.
|
||||
treesitter = {
|
||||
enable = true;
|
||||
settings.ensure_installed = [
|
||||
"nix"
|
||||
"lua"
|
||||
"bash"
|
||||
"markdown"
|
||||
"groovy"
|
||||
"c_sharp" # C#
|
||||
"python"
|
||||
"terraform" # also covers HCL
|
||||
"yaml" # Helm chart templates/values
|
||||
];
|
||||
};
|
||||
|
||||
# LSP + completion, replacing the (inert) ALE.
|
||||
lsp = {
|
||||
enable = true;
|
||||
# Universal servers. Host-specific ones are enabled in their own module:
|
||||
# C# (omnisharp) and Helm (helm_ls) live in work.nix (EDaaS only).
|
||||
servers = {
|
||||
nil_ls.enable = true; # Nix
|
||||
lua_ls.enable = true; # Lua (editing this config)
|
||||
pyright.enable = true; # Python
|
||||
terraformls.enable = true; # Terraform
|
||||
};
|
||||
keymaps.lspBuf = {
|
||||
gd = "definition";
|
||||
gr = "references";
|
||||
K = "hover";
|
||||
"<leader>rn" = "rename";
|
||||
"<leader>ca" = "code_action";
|
||||
};
|
||||
};
|
||||
cmp = {
|
||||
enable = true;
|
||||
autoEnableSources = true;
|
||||
settings = {
|
||||
# nvim-cmp ships no default keymaps; without these the menu shows but
|
||||
# nothing accepts it. confirm uses select=false so a bare <CR> stays a
|
||||
# newline unless an entry is explicitly highlighted.
|
||||
mapping = {
|
||||
"<C-n>" = "cmp.mapping.select_next_item()";
|
||||
"<C-p>" = "cmp.mapping.select_prev_item()";
|
||||
"<Tab>" = "cmp.mapping.select_next_item()";
|
||||
"<S-Tab>" = "cmp.mapping.select_prev_item()";
|
||||
"<CR>" = "cmp.mapping.confirm({ select = false })";
|
||||
"<C-Space>" = "cmp.mapping.complete()";
|
||||
"<C-e>" = "cmp.mapping.abort()";
|
||||
};
|
||||
snippet.expand = "function(args) require('luasnip').lsp_expand(args.body) end";
|
||||
sources = [
|
||||
{ name = "nvim_lsp"; }
|
||||
{ name = "luasnip"; }
|
||||
{ name = "buffer"; }
|
||||
{ name = "path"; }
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
# Fuzzy finder (files / live grep / symbols); rg + fd are already on PATH.
|
||||
telescope = {
|
||||
enable = true;
|
||||
extensions.fzf-native.enable = true;
|
||||
};
|
||||
gitsigns.enable = true; # gutter signs, stage-hunk, blame
|
||||
which-key.enable = true; # popup of pending keybindings (leader is Space)
|
||||
trouble.enable = true; # project-wide diagnostics/quickfix list
|
||||
lualine = {
|
||||
enable = true;
|
||||
settings.options.theme = "catppuccin-mocha";
|
||||
};
|
||||
comment.enable = true; # gc / gcc comment toggling
|
||||
nvim-autopairs.enable = true;
|
||||
treesitter-textobjects.enable = true;
|
||||
luasnip.enable = true; # snippet engine (drives cmp's luasnip source above)
|
||||
|
||||
# Format-on-save, mirroring the repo's treefmt set. Filetypes with no
|
||||
# formatter here (e.g. terraform) fall back to the LSP formatter.
|
||||
conform-nvim = {
|
||||
enable = true;
|
||||
settings = {
|
||||
formatters_by_ft = {
|
||||
nix = [ "nixfmt" ];
|
||||
lua = [ "stylua" ];
|
||||
python = [ "ruff_format" ];
|
||||
sh = [ "shfmt" ];
|
||||
markdown = [ "prettier" ];
|
||||
go = [ "gofumpt" ];
|
||||
};
|
||||
format_on_save = {
|
||||
timeout_ms = 2000;
|
||||
lsp_format = "fallback";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
keymaps = [
|
||||
{
|
||||
mode = "n";
|
||||
key = ",,";
|
||||
action = "<cmd>NvimTreeToggle<cr>";
|
||||
options.desc = "Toggle file tree";
|
||||
}
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>ff";
|
||||
action = "<cmd>Telescope find_files<cr>";
|
||||
options.desc = "Find files";
|
||||
}
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>fg";
|
||||
action = "<cmd>Telescope live_grep<cr>";
|
||||
options.desc = "Live grep";
|
||||
}
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>fb";
|
||||
action = "<cmd>Telescope buffers<cr>";
|
||||
options.desc = "Buffers";
|
||||
}
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>xx";
|
||||
action = "<cmd>Trouble diagnostics toggle<cr>";
|
||||
options.desc = "Diagnostics list";
|
||||
}
|
||||
];
|
||||
|
||||
# au BufNewFile,BufRead *Jenkinsfile setf groovy
|
||||
autoCmd = [
|
||||
{
|
||||
event = [
|
||||
"BufNewFile"
|
||||
"BufRead"
|
||||
];
|
||||
pattern = [ "*Jenkinsfile" ];
|
||||
command = "setf groovy";
|
||||
}
|
||||
];
|
||||
};
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
# Version control: git + delta pager + commitizen + lazygit. The committer
|
||||
# identity (name, email, signing key) comes from the per-user `identity` arg,
|
||||
# derived from the registry (users/registry.nix) by mkHost.
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
identity,
|
||||
...
|
||||
}:
|
||||
let
|
||||
ctp = import ../lib/catppuccin-mocha.nix;
|
||||
in
|
||||
{
|
||||
home.packages = [
|
||||
pkgs.commitizen
|
||||
];
|
||||
|
||||
programs.git = {
|
||||
enable = true;
|
||||
package = pkgs.gitFull;
|
||||
settings = {
|
||||
user.name = identity.fullName;
|
||||
# Identity from the registry. mkDefault so a host-specific module can still
|
||||
# override it without conflicting.
|
||||
user.email = lib.mkDefault identity.email;
|
||||
push.autoSetupRemote = true;
|
||||
init.defaultBranch = "main";
|
||||
|
||||
# Rebase-centric pulls (matches the "always a branch, linear history"
|
||||
# workflow); stash/restore and reorder fixups automatically.
|
||||
pull.rebase = true;
|
||||
rebase = {
|
||||
autoStash = true;
|
||||
autoSquash = true;
|
||||
};
|
||||
|
||||
fetch.prune = true; # drop deleted remote-tracking branches
|
||||
# Keep the commit-graph current (fast `git log --graph`, used by `lg`).
|
||||
fetch.writeCommitGraph = true;
|
||||
gc.writeCommitGraph = true;
|
||||
merge.conflictStyle = "zdiff3"; # show the common ancestor in conflicts
|
||||
diff = {
|
||||
algorithm = "histogram";
|
||||
colorMoved = "default";
|
||||
};
|
||||
rerere.enabled = true; # remember + replay conflict resolutions
|
||||
|
||||
# delta pager config (programs.delta is enabled below, with git
|
||||
# integration; these keys land under [delta] in the git config).
|
||||
# syntax-theme reuses the Catppuccin Mocha tmTheme vendored for bat in
|
||||
# shell.nix -- delta reads bat's theme directory.
|
||||
delta = {
|
||||
syntax-theme = "Catppuccin Mocha";
|
||||
navigate = true; # n/N to jump between diff hunks
|
||||
line-numbers = true;
|
||||
side-by-side = true;
|
||||
};
|
||||
commit.verbose = true; # full diff in the commit-message editor
|
||||
branch.sort = "-committerdate"; # most-recent branches first
|
||||
column.ui = "auto";
|
||||
help.autocorrect = "prompt";
|
||||
|
||||
alias = {
|
||||
st = "status";
|
||||
co = "checkout";
|
||||
sw = "switch";
|
||||
br = "branch";
|
||||
ci = "commit";
|
||||
last = "log -1 HEAD";
|
||||
unstage = "reset HEAD --";
|
||||
amend = "commit --amend --no-edit"; # tack staged changes onto HEAD
|
||||
fixup = "commit --fixup"; # `git fixup <sha>` -> autosquash on next rebase
|
||||
undo = "reset --soft HEAD~1"; # undo last commit, keep the changes staged
|
||||
lg = "log --graph --abbrev-commit --decorate --format=format:'%C(bold blue)%h%C(reset) %C(bold green)(%ar)%C(reset) %C(white)%s%C(reset) %C(dim white)- %an%C(reset)%C(auto)%d%C(reset)' --all";
|
||||
# commitizen (Conventional Commits, its default ruleset): `git cz c` ->
|
||||
# `cz commit`, `git cz bump`, etc. `git cc` is a shortcut for the prompt.
|
||||
cz = "!cz";
|
||||
cc = "!cz commit";
|
||||
};
|
||||
|
||||
# SSH commit signing, key from the registry. mkDefault on the key and on
|
||||
# gpgsign so a host without that key in its ssh-agent can override gpgsign
|
||||
# to false rather than fail every commit.
|
||||
gpg.format = "ssh";
|
||||
user.signingkey = lib.mkDefault identity.signingKey;
|
||||
commit.gpgsign = lib.mkDefault true;
|
||||
tag.gpgsign = lib.mkDefault true;
|
||||
};
|
||||
|
||||
# Global ignore file (~/.config/git/ignore).
|
||||
ignores = [
|
||||
"result"
|
||||
"result-*"
|
||||
".direnv"
|
||||
"*.swp"
|
||||
".DS_Store"
|
||||
];
|
||||
};
|
||||
|
||||
programs.delta = {
|
||||
enable = true;
|
||||
enableGitIntegration = true;
|
||||
};
|
||||
|
||||
# lazygit: TUI for staging/rebasing, themed to Catppuccin Mocha to match.
|
||||
programs.lazygit = {
|
||||
enable = true;
|
||||
settings.gui.theme = {
|
||||
activeBorderColor = [
|
||||
"#${ctp.blue}"
|
||||
"bold"
|
||||
];
|
||||
inactiveBorderColor = [ "#${ctp.surface1}" ];
|
||||
selectedLineBgColor = [ "#${ctp.surface0}" ];
|
||||
};
|
||||
};
|
||||
}
|
||||
+420
@@ -0,0 +1,420 @@
|
||||
# Interactive shell: zsh + tmux. Wanted on every host.
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
inputs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
# Shared Catppuccin Mocha palette: raw 6-hex strings, no leading "#".
|
||||
ctp = import ../lib/catppuccin-mocha.nix;
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
# Prebuilt nix-index database -> working command-not-found
|
||||
# ("cmd not found -> which nix package provides it"), no manual indexing.
|
||||
inputs.nix-index-database.homeModules.default
|
||||
];
|
||||
|
||||
# CLI staples wanted on every host (search, parse, monitor). ripgrep/fd also
|
||||
# back fzf and editor integrations; tea is the Gitea CLI for code.emmathe.dev.
|
||||
home.packages = [
|
||||
pkgs.ripgrep
|
||||
pkgs.fd
|
||||
pkgs.jq
|
||||
pkgs.tea
|
||||
pkgs.hyperfine # command-line benchmarking
|
||||
pkgs.sd # saner find-and-replace than sed
|
||||
];
|
||||
|
||||
# Resource monitor, themed Catppuccin Mocha to match the rest of the desktop.
|
||||
# btop does not bundle the theme, so vendor it from catppuccin/btop (pinned).
|
||||
programs.btop = {
|
||||
enable = true;
|
||||
settings.color_theme = "catppuccin_mocha";
|
||||
};
|
||||
xdg.configFile."btop/themes/catppuccin_mocha.theme".source = pkgs.fetchurl {
|
||||
url = "https://raw.githubusercontent.com/catppuccin/btop/f437574b600f1c6d932627050b15ff5153b58fa3/themes/catppuccin_mocha.theme";
|
||||
hash = "sha256-THRpq5vaKCwf9gaso3ycC4TNDLZtBB5Ofh/tOXkfRkQ=";
|
||||
};
|
||||
|
||||
programs.zsh = {
|
||||
enable = true;
|
||||
# Keep zsh dotfiles under XDG (~/.config/zsh) rather than the legacy $HOME
|
||||
# layout, matching xdg.enable. history.path is pinned below so the existing
|
||||
# ~/.zsh_history is reused, not orphaned by the dotDir move.
|
||||
dotDir = "${config.xdg.configHome}/zsh";
|
||||
enableCompletion = true;
|
||||
enableVteIntegration = true;
|
||||
autosuggestion.enable = true;
|
||||
# Bind Up/Down for history-substring-search in BOTH cursor-key modes: CSI
|
||||
# (^[[A/^[[B -- normal mode, and what the Linux TTY sends) and SS3
|
||||
# (^[OA/^[OB -- application mode, used by foot, tmux and iTerm2). Binding
|
||||
# only the default CSI form leaves it dead at the prompt in foot/iTerm2.
|
||||
historySubstringSearch = {
|
||||
enable = true;
|
||||
searchUpKey = [
|
||||
"^[[A"
|
||||
"^[OA"
|
||||
];
|
||||
searchDownKey = [
|
||||
"^[[B"
|
||||
"^[OB"
|
||||
];
|
||||
};
|
||||
history = {
|
||||
# Stay at the legacy ~/.zsh_history (default would follow dotDir into
|
||||
# ~/.config/zsh and orphan the existing file). Keeps history intact.
|
||||
path = "${config.home.homeDirectory}/.zsh_history";
|
||||
append = true; # append, don't overwrite, on shell exit
|
||||
size = 100000; # in-memory (HISTSIZE)
|
||||
save = 100000; # on-disk (SAVEHIST)
|
||||
ignoreDups = true; # drop consecutive duplicates
|
||||
ignoreSpace = true; # leading-space commands stay out of history
|
||||
expireDuplicatesFirst = true;
|
||||
share = true; # live-share history across sessions
|
||||
extended = true; # record timestamps
|
||||
};
|
||||
oh-my-zsh = {
|
||||
enable = true;
|
||||
plugins = [
|
||||
"git"
|
||||
"man"
|
||||
"sudo" # double-Esc prefixes the last command with sudo
|
||||
"colored-man-pages"
|
||||
"extract" # `extract <archive>` for any format
|
||||
];
|
||||
theme = "robbyrussell";
|
||||
};
|
||||
syntaxHighlighting.enable = true;
|
||||
initContent = lib.mkMerge [
|
||||
# Auto-start tmux in every interactive terminal -- foot, iTerm2, the WSL
|
||||
# shell, the Linux console -- so a new terminal lands straight in the
|
||||
# multiplexer (session "main": attach if present, else create). Panes run
|
||||
# a plain non-login zsh (tmux's default-command "${SHELL}"). Order 200
|
||||
# runs before oh-my-zsh/compinit so the exec replaces the shell before
|
||||
# that setup is wasted. Guards, each preventing a real breakage:
|
||||
# interactive only -> don't hijack scp / `ssh host cmd` / scripted shells
|
||||
# $TMUX empty -> a pane's zsh won't re-exec tmux (infinite loop)
|
||||
# not SSH -> don't force inbound SSH logins into a server tmux
|
||||
# not VS Code -> its integrated terminal manages itself
|
||||
# tmux on PATH -> a failed exec would otherwise kill the login shell
|
||||
# $NO_TMUX unset -> escape hatch: `NO_TMUX=1 <term>` opens a bare shell
|
||||
(lib.mkOrder 200 ''
|
||||
if [[ $- == *i* ]] \
|
||||
&& [[ -z "$TMUX" ]] \
|
||||
&& [[ -z "$NO_TMUX" ]] \
|
||||
&& [[ -z "$SSH_CONNECTION" && -z "$SSH_TTY" ]] \
|
||||
&& [[ "$TERM_PROGRAM" != "vscode" ]] \
|
||||
&& command -v tmux >/dev/null 2>&1; then
|
||||
exec tmux new-session -A -s main
|
||||
fi
|
||||
'')
|
||||
# Prefix the prompt with the hostname over SSH (mkAfter).
|
||||
(lib.mkOrder 1500 ''
|
||||
if [ "$SSH_CLIENT" ] || [ "$SSH_TTY" ]; then
|
||||
export PS1="%M $PS1"
|
||||
fi
|
||||
'')
|
||||
];
|
||||
shellAliases = {
|
||||
# eza's zsh integration also defines these; set explicitly so the
|
||||
# icons/git intent is obvious.
|
||||
ls = "eza --icons --git";
|
||||
ll = "eza --icons --git -l";
|
||||
la = "eza --icons --git -la";
|
||||
lt = "eza --icons --git --tree";
|
||||
cls = "clear";
|
||||
};
|
||||
};
|
||||
|
||||
# Fuzzy finder: Ctrl-R fuzzy history, Ctrl-T files, Alt-C cd.
|
||||
programs.fzf = {
|
||||
enable = true;
|
||||
enableZshIntegration = true;
|
||||
# Catppuccin Mocha colours (rendered into FZF_DEFAULT_OPTS --color). Each
|
||||
# value needs a leading "#"; the palette stores raw hex.
|
||||
colors = {
|
||||
"bg" = "#${ctp.base}";
|
||||
"bg+" = "#${ctp.surface1}"; # current line / selected row
|
||||
"fg" = "#${ctp.text}";
|
||||
"fg+" = "#${ctp.text}";
|
||||
"hl" = "#${ctp.blue}"; # match highlights
|
||||
"hl+" = "#${ctp.blue}";
|
||||
"header" = "#${ctp.red}";
|
||||
"info" = "#${ctp.mauve}";
|
||||
"marker" = "#${ctp.green}";
|
||||
"pointer" = "#${ctp.pink}";
|
||||
"prompt" = "#${ctp.mauve}";
|
||||
"spinner" = "#${ctp.pink}";
|
||||
"border" = "#${ctp.surface1}";
|
||||
};
|
||||
};
|
||||
|
||||
# Frecency directory jumping: `z <fragment>`.
|
||||
programs.zoxide = {
|
||||
enable = true;
|
||||
enableZshIntegration = true;
|
||||
};
|
||||
|
||||
# Per-project environments auto-loaded on cd, with the Nix dev-shell cache.
|
||||
programs.direnv = {
|
||||
enable = true;
|
||||
nix-direnv.enable = true;
|
||||
};
|
||||
|
||||
# Modern ls (drives the ls aliases above).
|
||||
programs.eza = {
|
||||
enable = true;
|
||||
git = true;
|
||||
icons = "auto"; # boolean form is deprecated
|
||||
};
|
||||
|
||||
# Syntax-highlighting pager, used as `bat` (acts like cat when piped). bat
|
||||
# ships no Catppuccin theme, so vendor the upstream tmTheme from catppuccin/bat
|
||||
# (delta in git.nix reuses it as its syntax-theme).
|
||||
programs.bat = {
|
||||
enable = true;
|
||||
config.theme = "Catppuccin Mocha";
|
||||
themes."Catppuccin Mocha" = {
|
||||
src = pkgs.fetchFromGitHub {
|
||||
owner = "catppuccin";
|
||||
repo = "bat";
|
||||
rev = "6810349b28055dce54076712fc05fc68da4b8ec0";
|
||||
sha256 = "1y5sfi7jfr97z1g6vm2mzbsw59j1jizwlmbadvmx842m0i5ak5ll";
|
||||
};
|
||||
file = "themes/Catppuccin Mocha.tmTheme";
|
||||
};
|
||||
};
|
||||
|
||||
# command-not-found backed by the prebuilt nix-index DB (module imported
|
||||
# above). `comma` runs an uninstalled program once: `, cowsay hi`.
|
||||
programs.nix-index.enable = true;
|
||||
programs.nix-index-database.comma.enable = true;
|
||||
|
||||
# Nicer nixos-rebuild/home-manager (diffs) + $NH_FLAKE. No automatic clean:
|
||||
# the scheduled GC's only benefit is reclaiming disk, but it can reap store
|
||||
# paths the current generation still references (notably on nix-darwin, where
|
||||
# it broke completion by removing an in-use oh-my-zsh). GC manually instead:
|
||||
# `nh clean all` / `nix-collect-garbage -d` when nothing important is running.
|
||||
programs.nh = {
|
||||
enable = true;
|
||||
flake = "$HOME/code/nixfiles";
|
||||
};
|
||||
|
||||
# GitHub CLI. `programs.gh.settings` is deliberately unset: home-manager renders
|
||||
# ~/.config/gh/config.yml as a read-only /nix/store symlink whenever the module
|
||||
# is enabled, but gh must rewrite that file on `gh auth login` and `gh config
|
||||
# set`, which then fail with a permission error. Suppress the managed config.yml
|
||||
# (below) and let gh own it. The token lives in hosts.yml, which is never
|
||||
# Nix-managed. Set the SSH protocol once at runtime: `gh config set git_protocol
|
||||
# ssh` (it can't be declarative here without recreating the immutable file).
|
||||
programs.gh.enable = true;
|
||||
xdg.configFile."gh/config.yml".enable = lib.mkForce false;
|
||||
|
||||
programs.tmux = {
|
||||
enable = true;
|
||||
reverseSplit = true;
|
||||
# tmux-256color (not tmux-direct): the standard inside-tmux terminfo.
|
||||
# tmux-direct's capabilities desync zsh's line redraw on some terminals
|
||||
# (e.g. iTerm2 -> duplicated chars on Tab, stray newlines). Truecolor is
|
||||
# advertised per outer terminal via the RGB terminal-features below.
|
||||
terminal = "tmux-256color";
|
||||
newSession = true;
|
||||
keyMode = "vi";
|
||||
historyLimit = 500000;
|
||||
mouse = true;
|
||||
escapeTime = 10; # was the 500ms default -> laggy ESC in vim
|
||||
focusEvents = true; # let vim see focus changes (autoread)
|
||||
baseIndex = 1; # sets both base-index and pane-base-index
|
||||
|
||||
plugins = with pkgs.tmuxPlugins; [
|
||||
sensible
|
||||
vim-tmux-navigator # Ctrl-h/j/k/l across vim splits and tmux panes
|
||||
yank
|
||||
extrakto # prefix+Tab: fzf-grab paths/URLs/text from the pane into the prompt
|
||||
{
|
||||
# Catppuccin Mocha statusline (v2 API: flavour + window options must be
|
||||
# set before the plugin loads, which home-manager does for plugin
|
||||
# extraConfig; the status modules below go in the main extraConfig,
|
||||
# which HM appends after all plugins).
|
||||
plugin = catppuccin;
|
||||
extraConfig = ''
|
||||
set -g @catppuccin_flavor 'mocha'
|
||||
set -g @catppuccin_window_status_style 'rounded'
|
||||
'';
|
||||
}
|
||||
resurrect # save/restore sessions
|
||||
{
|
||||
plugin = continuum; # auto-save + restore on tmux start (after resurrect)
|
||||
extraConfig = ''
|
||||
set -g @continuum-restore 'on'
|
||||
'';
|
||||
}
|
||||
];
|
||||
# `reverseSplit = true` already binds s -> vertical and v -> horizontal
|
||||
# split (the dotfiles' vim-style splits).
|
||||
extraConfig = ''
|
||||
# Run a non-login shell in new panes/windows.
|
||||
set -g default-command "''${SHELL}"
|
||||
|
||||
# Drop the stock split keys in favour of the s/v binds above.
|
||||
unbind %
|
||||
unbind '"'
|
||||
|
||||
# Alt+Arrow pane navigation
|
||||
bind -n M-Left select-pane -L
|
||||
bind -n M-Right select-pane -R
|
||||
bind -n M-Up select-pane -U
|
||||
bind -n M-Down select-pane -D
|
||||
|
||||
# Truecolor for the outer terminals (foot reports xterm-ish too; iTerm2 is
|
||||
# xterm-256color). Without this, with tmux-256color as default-terminal,
|
||||
# 24-bit colour would be quantised to 256.
|
||||
set -as terminal-features ",xterm-256color:RGB"
|
||||
# Tell tmux which capabilities the foot terminal supports, so truecolor,
|
||||
# synchronised output, the system clipboard (OSC 52), window titles and
|
||||
# cursor styling all pass through.
|
||||
set -as terminal-features ",foot*:RGB"
|
||||
set -as terminal-features ",foot*:sync"
|
||||
set -as terminal-features ",foot*:clipboard"
|
||||
set -as terminal-features ",foot*:title"
|
||||
set -as terminal-features ",foot*:ccolour"
|
||||
set -as terminal-features ",foot*:cstyle"
|
||||
|
||||
# No home-manager options for these.
|
||||
set -g renumber-windows on
|
||||
set -g set-clipboard on
|
||||
|
||||
# Catppuccin v2 statusline. Must run after the plugin has loaded;
|
||||
# home-manager appends this extraConfig after the whole plugin list.
|
||||
set -g status-left-length 100
|
||||
set -g status-right-length 100
|
||||
set -g status-left ""
|
||||
set -g status-right "#{E:@catppuccin_status_application}"
|
||||
set -ag status-right "#{E:@catppuccin_status_session}"
|
||||
'';
|
||||
};
|
||||
|
||||
# Add the key to the agent on first use, so the passphrase is typed once per
|
||||
# login session rather than per commit/push (commit signing uses this agent).
|
||||
# The work box keeps its own ssh config (see work.nix), so this only
|
||||
# manages ~/.ssh/config on the personal hosts.
|
||||
programs.ssh = {
|
||||
enable = true;
|
||||
# The module's built-in default "*" block is being deprecated; opt out and
|
||||
# carry the defaults we want ourselves under settings."*".
|
||||
enableDefaultConfig = false;
|
||||
settings = {
|
||||
# Global defaults (rendered last, as ssh_config wants). AddKeysToAgent
|
||||
# adds the key on first use so the passphrase is typed once per session.
|
||||
"*" = {
|
||||
AddKeysToAgent = "yes";
|
||||
ForwardAgent = false;
|
||||
Compression = false;
|
||||
ServerAliveInterval = 0;
|
||||
ServerAliveCountMax = 3;
|
||||
HashKnownHosts = false;
|
||||
UserKnownHostsFile = "~/.ssh/known_hosts";
|
||||
ControlMaster = "no";
|
||||
ControlPath = "~/.ssh/master-%r@%n:%p";
|
||||
ControlPersist = "no";
|
||||
}
|
||||
# macOS: also cache the passphrase in the login keychain. UseKeychain
|
||||
# exists only in Apple's ssh; nixpkgs' openssh (which may be the `ssh` on
|
||||
# PATH) rejects it as "Bad configuration option". IgnoreUnknown (emitted
|
||||
# first by the module) makes any non-Apple ssh skip it instead of erroring,
|
||||
# while Apple's ssh still honours it. Darwin-only.
|
||||
// lib.optionalAttrs pkgs.stdenv.hostPlatform.isDarwin {
|
||||
IgnoreUnknown = "UseKeychain";
|
||||
UseKeychain = "yes";
|
||||
};
|
||||
# Gitea remote (the flake's origin) -- required on every host. HostName
|
||||
# pins the IP so it resolves without DNS. Port 30009 is non-default; pin
|
||||
# the dedicated key (identitiesOnly avoids "too many authentication
|
||||
# failures" when the agent holds several keys).
|
||||
"code.emmathe.dev" = {
|
||||
HostName = "10.187.1.76";
|
||||
User = "git";
|
||||
Port = 30009;
|
||||
IdentityFile = "~/.ssh/code.emmathe.dev";
|
||||
IdentitiesOnly = true;
|
||||
};
|
||||
"dockerpi.inf.cbg.emmaisvery.gay" = {
|
||||
User = "emmathorpe";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# Run a user ssh-agent on Linux (macOS provides one via launchd). EDaaS also
|
||||
# enables this in the work module; both being true merges cleanly.
|
||||
services.ssh-agent.enable = lib.mkIf pkgs.stdenv.hostPlatform.isLinux true;
|
||||
|
||||
# Classic process viewer (complements btop). htop has no custom-theme support
|
||||
# -- only a handful of built-in color schemes -- so it can't be hex-themed like
|
||||
# btop/bat/fzf. color_scheme = 0 (Default) draws from the terminal's ANSI
|
||||
# palette, which is Catppuccin Mocha (foot/iTerm2), so it matches by deferring
|
||||
# to the terminal rather than vendoring a theme.
|
||||
programs.htop = {
|
||||
enable = true;
|
||||
settings = {
|
||||
color_scheme = 0; # Default -> uses the terminal's Catppuccin palette
|
||||
delay = 15; # refresh every 1.5s
|
||||
cpu_count_from_one = 1;
|
||||
show_cpu_frequency = 1;
|
||||
show_cpu_usage = 1; # per-core usage shown in the CPU bars
|
||||
highlight_base_name = 1; # highlight the program name within the path
|
||||
highlight_megabytes = 1;
|
||||
highlight_threads = 1;
|
||||
hide_kernel_threads = 1;
|
||||
show_program_path = 0; # show just the command, not the full path
|
||||
tree_view = 1; # start in process-tree mode
|
||||
tree_view_always_by_pid = 0;
|
||||
account_guest_in_cpu_meter = 0;
|
||||
fields = with config.lib.htop.fields; [
|
||||
PID
|
||||
USER
|
||||
PRIORITY
|
||||
NICE
|
||||
M_SIZE
|
||||
M_RESIDENT
|
||||
M_SHARE
|
||||
STATE
|
||||
PERCENT_CPU
|
||||
PERCENT_MEM
|
||||
TIME
|
||||
COMM
|
||||
];
|
||||
}
|
||||
// (
|
||||
with config.lib.htop;
|
||||
leftMeters [
|
||||
(bar "AllCPUs2")
|
||||
(bar "Memory")
|
||||
(bar "Swap")
|
||||
]
|
||||
)
|
||||
// (
|
||||
with config.lib.htop;
|
||||
rightMeters [
|
||||
(text "Tasks")
|
||||
(text "LoadAverage")
|
||||
(text "Uptime")
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
# Drop the zsh completion dump on every activation. A stale .zcompdump caches
|
||||
# /nix/store paths to completion functions; once a rebuild or a manual GC
|
||||
# removes them, compinit fails with "_git: function definition file not found"
|
||||
# for every completion. Deleting it forces a fresh rebuild from the current
|
||||
# fpath on the next shell. compinit dumps to $ZDOTDIR (~/.config/zsh now); the
|
||||
# $HOME and cache paths are also swept to clear any legacy leftovers.
|
||||
home.activation.resetZcompdump = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
|
||||
$DRY_RUN_CMD rm -f \
|
||||
"${config.xdg.configHome}"/zsh/.zcompdump* \
|
||||
"$HOME"/.zcompdump* \
|
||||
"''${XDG_CACHE_HOME:-$HOME/.cache}"/zsh/.zcompdump* 2>/dev/null || true
|
||||
'';
|
||||
}
|
||||
+526
@@ -0,0 +1,526 @@
|
||||
# Declarative Sway window manager, status bar, lock, idle and notifications.
|
||||
# Imported via ./desktop.nix, so only graphical hosts get it.
|
||||
#
|
||||
# The compositor binary, PAM and the polkit *daemon* come from the system-level
|
||||
# programs.sway (see ../modules/sway.nix); package = null below reuses it instead of
|
||||
# pulling a second Sway. The polkit authentication *agent* (the thing that draws
|
||||
# the GUI auth dialog) is a user service started here. home-manager owns the user
|
||||
# config (~/.config/sway) and wires the systemd user session (sway-session.target),
|
||||
# which is what lets the agent/swayidle/dunst/kanshi user services start with the
|
||||
# desktop.
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
# Threaded from mkHost (flake.nix). Desktop hosts set this false to drop
|
||||
# mobile components (battery block, screen-brightness keys).
|
||||
portable ? true,
|
||||
...
|
||||
}:
|
||||
let
|
||||
# Catppuccin Mocha (shared with the ReGreet greeter). Raw hex; prefix "#"
|
||||
# where a consumer needs it -- Sway/i3status/dunst want "#", foot/swaylock do
|
||||
# not.
|
||||
ctp = import ../lib/catppuccin-mocha.nix;
|
||||
|
||||
# Focused-window screenshot -> swappy editor (the dotfiles' grimshot.sh logic).
|
||||
# Full store paths so it needs nothing on PATH.
|
||||
screenshotWindow = pkgs.writeShellScript "screenshot-window" ''
|
||||
${pkgs.grim}/bin/grim -g "$(${pkgs.sway}/bin/swaymsg -t get_tree \
|
||||
| ${pkgs.jq}/bin/jq -r '.. | select(.focused?) | .rect | "\(.x),\(.y) \(.width)x\(.height)"')" \
|
||||
- | ${pkgs.swappy}/bin/swappy -f -
|
||||
'';
|
||||
|
||||
# Binding-mode names. The string is both the `modes` attr key and what the
|
||||
# bar's mode indicator shows, so the keys are spelled out in the label.
|
||||
layoutMode = "layout: [s]tacking [w]tabbed [e]split";
|
||||
systemMode = "system: [l]ock [e]xit [s]leep [r]eboot [Shift+s]shutdown";
|
||||
in
|
||||
{
|
||||
wayland.windowManager.sway = {
|
||||
enable = true;
|
||||
package = null;
|
||||
# `sway --validate` needs a real package; skip it since package = null.
|
||||
checkConfig = false;
|
||||
config = rec {
|
||||
modifier = "Mod4";
|
||||
terminal = "${pkgs.foot}/bin/foot";
|
||||
# Launcher: sway-launcher-desktop running inside a floating foot window.
|
||||
menu = "${pkgs.foot}/bin/foot --app-id=launcher ${pkgs.sway-launcher-desktop}/bin/sway-launcher-desktop";
|
||||
|
||||
# Dvorak is a variant of the "us" layout, not a standalone layout --
|
||||
# `xkb_layout = "dvorak"` fails to compile (no symbols/dvorak) and wlroots
|
||||
# silently falls back to QWERTY. Use the variant.
|
||||
input."type:keyboard" = {
|
||||
xkb_layout = "us";
|
||||
xkb_variant = "dvorak";
|
||||
};
|
||||
# Touchpads (laptops): tap-to-click and natural scrolling. Inert on the
|
||||
# desktop hosts, which have no touchpad.
|
||||
input."type:touchpad" = {
|
||||
tap = "enabled";
|
||||
natural_scroll = "enabled";
|
||||
};
|
||||
|
||||
# Solid Catppuccin Mocha base as the wallpaper (no image dependency).
|
||||
output."*".bg = "#${ctp.base} solid_color";
|
||||
|
||||
# Window borders -- Catppuccin Mocha (blue accent on the focused window).
|
||||
colors = {
|
||||
focused = {
|
||||
border = "#${ctp.blue}";
|
||||
background = "#${ctp.base}";
|
||||
text = "#${ctp.text}";
|
||||
indicator = "#${ctp.blue}";
|
||||
childBorder = "#${ctp.blue}";
|
||||
};
|
||||
focusedInactive = {
|
||||
border = "#${ctp.surface0}";
|
||||
background = "#${ctp.base}";
|
||||
text = "#${ctp.subtext0}";
|
||||
indicator = "#${ctp.surface0}";
|
||||
childBorder = "#${ctp.surface0}";
|
||||
};
|
||||
unfocused = {
|
||||
border = "#${ctp.surface0}";
|
||||
background = "#${ctp.base}";
|
||||
text = "#${ctp.subtext0}";
|
||||
indicator = "#${ctp.surface0}";
|
||||
childBorder = "#${ctp.surface0}";
|
||||
};
|
||||
urgent = {
|
||||
border = "#${ctp.red}";
|
||||
background = "#${ctp.base}";
|
||||
text = "#${ctp.text}";
|
||||
indicator = "#${ctp.red}";
|
||||
childBorder = "#${ctp.red}";
|
||||
};
|
||||
};
|
||||
|
||||
window.commands = [
|
||||
{
|
||||
criteria.app_id = "launcher";
|
||||
command = "floating enable, resize set 800 500";
|
||||
}
|
||||
# Don't let swayidle blank/lock during fullscreen video. Two rules cover
|
||||
# native Wayland (app_id) and XWayland (class) clients.
|
||||
{
|
||||
criteria.app_id = ".*";
|
||||
command = "inhibit_idle fullscreen";
|
||||
}
|
||||
{
|
||||
criteria.class = ".*";
|
||||
command = "inhibit_idle fullscreen";
|
||||
}
|
||||
];
|
||||
|
||||
# Binding modes (submenus). Entered from keybindings below; each action
|
||||
# returns to the default mode. mkOptionDefault-merged with the module's
|
||||
# built-in "resize" mode.
|
||||
modes = {
|
||||
# Layout submenu (Mod+y). Mirrors Sway's default s/w/e layout keys --
|
||||
# notably it restores split-toggle, which moved off Mod+e when that
|
||||
# became the nemo launcher.
|
||||
${layoutMode} = {
|
||||
"s" = "layout stacking, mode default";
|
||||
"w" = "layout tabbed, mode default";
|
||||
"e" = "layout toggle split, mode default";
|
||||
"Return" = "mode default";
|
||||
"Escape" = "mode default";
|
||||
};
|
||||
# Power menu (Mod+Shift+x). Lock reuses the themed swaylock; the rest go
|
||||
# through systemd/logind (allowed for the active local session).
|
||||
${systemMode} = {
|
||||
"l" = "exec ${pkgs.swaylock}/bin/swaylock -f, mode default";
|
||||
"e" = "exec ${pkgs.sway}/bin/swaymsg exit, mode default";
|
||||
"s" = "exec systemctl suspend, mode default";
|
||||
"r" = "exec systemctl reboot, mode default";
|
||||
"Shift+s" = "exec systemctl poweroff, mode default";
|
||||
"Return" = "mode default";
|
||||
"Escape" = "mode default";
|
||||
};
|
||||
};
|
||||
|
||||
bars = [
|
||||
{
|
||||
position = "top";
|
||||
statusCommand = "${pkgs.i3status-rust}/bin/i3status-rs ~/.config/i3status-rust/config-default.toml";
|
||||
fonts = {
|
||||
names = [
|
||||
"Noto Sans"
|
||||
"Font Awesome 6 Free"
|
||||
];
|
||||
size = 11.0;
|
||||
};
|
||||
# Bar background + workspace buttons -- Catppuccin Mocha. The mantle
|
||||
# background matches i3status-rust's idle_bg below for a uniform strip.
|
||||
colors = {
|
||||
background = "#${ctp.mantle}";
|
||||
statusline = "#${ctp.text}";
|
||||
separator = "#${ctp.surface0}";
|
||||
focusedWorkspace = {
|
||||
border = "#${ctp.blue}";
|
||||
background = "#${ctp.blue}";
|
||||
text = "#${ctp.base}";
|
||||
};
|
||||
activeWorkspace = {
|
||||
border = "#${ctp.surface0}";
|
||||
background = "#${ctp.surface0}";
|
||||
text = "#${ctp.text}";
|
||||
};
|
||||
inactiveWorkspace = {
|
||||
border = "#${ctp.mantle}";
|
||||
background = "#${ctp.mantle}";
|
||||
text = "#${ctp.subtext0}";
|
||||
};
|
||||
urgentWorkspace = {
|
||||
border = "#${ctp.red}";
|
||||
background = "#${ctp.red}";
|
||||
text = "#${ctp.base}";
|
||||
};
|
||||
};
|
||||
}
|
||||
];
|
||||
|
||||
# NB: this whole set is wrapped in mkOptionDefault so it MERGES with the
|
||||
# home-manager module's default keybindings (same priority) rather than
|
||||
# replacing them. Do not wrap it in mkMerge with a normal-priority attr --
|
||||
# that makes the normal-priority def win and silently drops every default
|
||||
# bind (terminal, movement, workspaces, ...).
|
||||
keybindings = lib.mkOptionDefault (
|
||||
{
|
||||
# Launcher on Mod+Space. mkForce overrides the module's own default
|
||||
# Mod+Space (focus mode_toggle); a plain value would conflict with it
|
||||
# at equal priority. Mod+d also still runs the launcher (module default).
|
||||
"${modifier}+space" = lib.mkForce "exec ${menu}";
|
||||
# File manager. mkForce overrides the module default (layout toggle split).
|
||||
"${modifier}+e" = lib.mkForce "exec ${pkgs.nemo}/bin/nemo";
|
||||
"${modifier}+l" = "exec ${pkgs.swaylock}/bin/swaylock -f";
|
||||
|
||||
# Cycle workspaces.
|
||||
"${modifier}+z" = "workspace prev";
|
||||
"${modifier}+x" = "workspace next";
|
||||
|
||||
# focus mode_toggle (tiling <-> floating focus) -- re-homed off
|
||||
# Mod+Space, which is now the launcher.
|
||||
"${modifier}+Mod1+space" = "focus mode_toggle";
|
||||
|
||||
# Enter the binding-mode submenus defined above.
|
||||
"${modifier}+y" = "mode \"${layoutMode}\"";
|
||||
"${modifier}+Shift+x" = "mode \"${systemMode}\"";
|
||||
|
||||
# Clipboard history: pick a past entry through fuzzel (clipman stores
|
||||
# it -- see services.clipman below).
|
||||
"${modifier}+c" =
|
||||
"exec ${pkgs.clipman}/bin/clipman pick -t CUSTOM --tool-args=\"${pkgs.fuzzel}/bin/fuzzel --dmenu\"";
|
||||
|
||||
# Screenshots -> swappy editor: Print = drag a region, Shift+Print =
|
||||
# the focused window.
|
||||
"Print" =
|
||||
"exec ${pkgs.grim}/bin/grim -g \"$(${pkgs.slurp}/bin/slurp)\" - | ${pkgs.swappy}/bin/swappy -f -";
|
||||
"Shift+Print" = "exec ${screenshotWindow}";
|
||||
|
||||
"XF86AudioRaiseVolume" = "exec ${pkgs.wireplumber}/bin/wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+";
|
||||
"XF86AudioLowerVolume" = "exec ${pkgs.wireplumber}/bin/wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-";
|
||||
"XF86AudioMute" = "exec ${pkgs.wireplumber}/bin/wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle";
|
||||
"XF86AudioMicMute" = "exec ${pkgs.wireplumber}/bin/wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle";
|
||||
|
||||
# Media keys (MPRIS via playerctl).
|
||||
"XF86AudioPlay" = "exec ${pkgs.playerctl}/bin/playerctl play-pause";
|
||||
"XF86AudioNext" = "exec ${pkgs.playerctl}/bin/playerctl next";
|
||||
"XF86AudioPrev" = "exec ${pkgs.playerctl}/bin/playerctl previous";
|
||||
}
|
||||
# Screen backlight: laptops only (no internal backlight on a desktop).
|
||||
// lib.optionalAttrs portable {
|
||||
"XF86MonBrightnessUp" = "exec ${pkgs.brightnessctl}/bin/brightnessctl set 5%+";
|
||||
"XF86MonBrightnessDown" = "exec ${pkgs.brightnessctl}/bin/brightnessctl set 5%-";
|
||||
}
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
# Terminal: Catppuccin Mocha. foot reads ~/.config/foot/foot.ini; the Sway
|
||||
# `terminal` above still launches the same binary, now themed.
|
||||
programs.foot = {
|
||||
enable = true;
|
||||
# foot 1.27: the bare [colors] section is deprecated in favour of
|
||||
# [colors-dark] (the default theme), and the cursor colour moved out of
|
||||
# [cursor] (where `color` is now rejected) into a `cursor` key here, written
|
||||
# "<text> <cursor>" (man foot.ini(5): "ff0000 00ff00" => green cursor, red
|
||||
# text). Only colors-dark is needed; we never set initial-color-theme=light.
|
||||
settings = {
|
||||
main = {
|
||||
# Nerd Font: monospace plus the powerline/Nerd glyphs the tmux
|
||||
# statusline uses (otherwise they render as blank/"?").
|
||||
font = "JetBrainsMono Nerd Font:size=11";
|
||||
# Advertise as xterm-256color so remote hosts without foot's terminfo
|
||||
# still behave (tmux re-adds foot's RGB/sync/etc. features -- see
|
||||
# shell.nix). The [main] section is the unheadered top of foot.ini.
|
||||
term = "xterm-256color";
|
||||
};
|
||||
scrollback.lines = 100000;
|
||||
"colors-dark" = {
|
||||
background = ctp.base;
|
||||
foreground = ctp.text;
|
||||
regular0 = ctp.surface1;
|
||||
regular1 = ctp.red;
|
||||
regular2 = ctp.green;
|
||||
regular3 = ctp.yellow;
|
||||
regular4 = ctp.blue;
|
||||
regular5 = ctp.pink;
|
||||
regular6 = ctp.teal;
|
||||
regular7 = ctp.subtext1;
|
||||
bright0 = ctp.surface2;
|
||||
bright1 = ctp.red;
|
||||
bright2 = ctp.green;
|
||||
bright3 = ctp.yellow;
|
||||
bright4 = ctp.blue;
|
||||
bright5 = ctp.pink;
|
||||
bright6 = ctp.teal;
|
||||
bright7 = ctp.subtext0;
|
||||
"selection-foreground" = ctp.base;
|
||||
"selection-background" = ctp.rosewater;
|
||||
cursor = "${ctp.base} ${ctp.rosewater}";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# Clipboard history: a user service runs `wl-paste --watch clipman store`,
|
||||
# bound to the Wayland session, so copies persist and Mod+c (above) can pick
|
||||
# an old entry through fuzzel.
|
||||
services.clipman.enable = true;
|
||||
|
||||
# Polkit authentication agent. programs.sway (system) enables the polkit
|
||||
# daemon but no agent, so GUI privilege prompts (nemo mounting a disk,
|
||||
# NetworkManager/blueman editing a system resource) would otherwise fail
|
||||
# silently. lxqt-policykit is a small, toolkit-light agent; bind it to the
|
||||
# Sway session so it starts and stops with the desktop.
|
||||
systemd.user.services.polkit-lxqt = {
|
||||
Unit = {
|
||||
Description = "lxqt-policykit polkit authentication agent";
|
||||
PartOf = [ "graphical-session.target" ];
|
||||
After = [ "graphical-session.target" ];
|
||||
};
|
||||
Service = {
|
||||
ExecStart = "${pkgs.lxqt.lxqt-policykit}/bin/lxqt-policykit-agent";
|
||||
Restart = "on-failure";
|
||||
};
|
||||
Install.WantedBy = [ "sway-session.target" ];
|
||||
};
|
||||
|
||||
# Output/display management. Reacts to hotplug and applies per-display
|
||||
# mode/scale/position. Profiles are hardware-specific: the safe default below
|
||||
# just enables the internal laptop panel; add docked/desktop profiles with the
|
||||
# real identifiers from `swaymsg -t get_outputs` (e.g. the Mac Pro's Apple
|
||||
# Cinema Display with its scale, or a docked laptop + external monitor).
|
||||
services.kanshi = {
|
||||
enable = true;
|
||||
settings = [
|
||||
{
|
||||
profile.name = "undocked";
|
||||
profile.outputs = [
|
||||
{
|
||||
criteria = "eDP-1";
|
||||
status = "enable";
|
||||
}
|
||||
];
|
||||
}
|
||||
# Example to copy per host (fill in real criteria/mode/scale/position):
|
||||
# {
|
||||
# profile.name = "desktop";
|
||||
# profile.outputs = [
|
||||
# { criteria = "Apple Computer Inc Cinema HD ..."; mode = "2560x1600"; scale = 1.0; position = "0,0"; status = "enable"; }
|
||||
# ];
|
||||
# }
|
||||
];
|
||||
};
|
||||
|
||||
# Night light. Manual location (no geoclue dependency); adjust the coordinates
|
||||
# to taste. Warmer at night, neutral by day.
|
||||
services.gammastep = {
|
||||
enable = true;
|
||||
provider = "manual";
|
||||
latitude = 51.5;
|
||||
longitude = -0.13; # London-ish; set to your actual location
|
||||
temperature = {
|
||||
day = 6500;
|
||||
night = 3700;
|
||||
};
|
||||
};
|
||||
|
||||
# fuzzel: the dmenu picker used by clipman, themed Catppuccin Mocha to match
|
||||
# (fuzzel colours are RRGGBBAA -- 8 hex digits).
|
||||
programs.fuzzel = {
|
||||
enable = true;
|
||||
settings = {
|
||||
main = {
|
||||
font = "Noto Sans:size=12";
|
||||
prompt = "\"clipboard \"";
|
||||
};
|
||||
border = {
|
||||
width = 2;
|
||||
radius = 8;
|
||||
};
|
||||
colors = {
|
||||
background = "${ctp.base}f0";
|
||||
text = "${ctp.text}ff";
|
||||
prompt = "${ctp.subtext0}ff";
|
||||
input = "${ctp.text}ff";
|
||||
match = "${ctp.blue}ff";
|
||||
selection = "${ctp.surface1}ff";
|
||||
selection-text = "${ctp.text}ff";
|
||||
selection-match = "${ctp.blue}ff";
|
||||
border = "${ctp.blue}ff";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
programs.swaylock = {
|
||||
enable = true;
|
||||
# Catppuccin Mocha (swaylock colours are hex without "#").
|
||||
settings = {
|
||||
color = ctp.base;
|
||||
indicator-radius = 100;
|
||||
indicator-thickness = 7;
|
||||
show-failed-attempts = true;
|
||||
font = "Noto Sans";
|
||||
|
||||
inside-color = ctp.base;
|
||||
inside-clear-color = ctp.base;
|
||||
inside-ver-color = ctp.base;
|
||||
inside-wrong-color = ctp.base;
|
||||
|
||||
ring-color = ctp.surface1;
|
||||
ring-clear-color = ctp.yellow;
|
||||
ring-ver-color = ctp.blue;
|
||||
ring-wrong-color = ctp.red;
|
||||
|
||||
key-hl-color = ctp.blue;
|
||||
bs-hl-color = ctp.red;
|
||||
|
||||
line-color = ctp.base;
|
||||
line-clear-color = ctp.base;
|
||||
line-ver-color = ctp.base;
|
||||
line-wrong-color = ctp.base;
|
||||
separator-color = "00000000";
|
||||
|
||||
text-color = ctp.text;
|
||||
text-clear-color = ctp.text;
|
||||
text-ver-color = ctp.text;
|
||||
text-wrong-color = ctp.text;
|
||||
};
|
||||
};
|
||||
|
||||
# Lock on idle, turn screens off shortly after, and lock before sleep.
|
||||
services.swayidle = {
|
||||
enable = true;
|
||||
timeouts = [
|
||||
{
|
||||
timeout = 300;
|
||||
command = "${pkgs.swaylock}/bin/swaylock -f";
|
||||
}
|
||||
{
|
||||
timeout = 600;
|
||||
command = "${pkgs.sway}/bin/swaymsg 'output * power off'";
|
||||
resumeCommand = "${pkgs.sway}/bin/swaymsg 'output * power on'";
|
||||
}
|
||||
];
|
||||
events = {
|
||||
before-sleep = "${pkgs.swaylock}/bin/swaylock -f";
|
||||
lock = "${pkgs.swaylock}/bin/swaylock -f";
|
||||
};
|
||||
};
|
||||
|
||||
services.dunst = {
|
||||
enable = true;
|
||||
# Catppuccin Mocha notifications (dunst colours need a leading "#").
|
||||
settings = {
|
||||
global = {
|
||||
font = "Noto Sans 11";
|
||||
frame_color = "#${ctp.blue}";
|
||||
frame_width = 2;
|
||||
separator_color = "frame";
|
||||
offset = "10x10";
|
||||
corner_radius = 5;
|
||||
};
|
||||
urgency_low = {
|
||||
background = "#${ctp.base}";
|
||||
foreground = "#${ctp.text}";
|
||||
frame_color = "#${ctp.surface1}";
|
||||
};
|
||||
urgency_normal = {
|
||||
background = "#${ctp.base}";
|
||||
foreground = "#${ctp.text}";
|
||||
frame_color = "#${ctp.blue}";
|
||||
};
|
||||
urgency_critical = {
|
||||
background = "#${ctp.base}";
|
||||
foreground = "#${ctp.text}";
|
||||
frame_color = "#${ctp.peach}";
|
||||
timeout = 0;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
programs.i3status-rust = {
|
||||
enable = true;
|
||||
bars.default = {
|
||||
# Catppuccin Mocha: a flat "plain" base recoloured via overrides. Idle
|
||||
# blocks sit on mantle (matching the Sway bar background) with light text;
|
||||
# only warning/critical states get a loud tinted background. The `theme`
|
||||
# bar option is shallow-merged away by `settings.theme`, so set the base
|
||||
# theme and its overrides together here.
|
||||
icons = "awesome6";
|
||||
settings.theme = {
|
||||
theme = "plain";
|
||||
overrides = {
|
||||
idle_bg = "#${ctp.mantle}";
|
||||
idle_fg = "#${ctp.text}";
|
||||
info_bg = "#${ctp.mantle}";
|
||||
info_fg = "#${ctp.blue}";
|
||||
good_bg = "#${ctp.mantle}";
|
||||
good_fg = "#${ctp.green}";
|
||||
warning_bg = "#${ctp.peach}";
|
||||
warning_fg = "#${ctp.base}";
|
||||
critical_bg = "#${ctp.red}";
|
||||
critical_fg = "#${ctp.base}";
|
||||
separator_bg = "#${ctp.mantle}";
|
||||
separator_fg = "#${ctp.surface1}";
|
||||
};
|
||||
};
|
||||
blocks = [
|
||||
{
|
||||
block = "disk_space";
|
||||
path = "/";
|
||||
format = " $icon $available ";
|
||||
}
|
||||
{
|
||||
block = "memory";
|
||||
format = " $icon $mem_used_percents ";
|
||||
}
|
||||
{
|
||||
block = "cpu";
|
||||
interval = 2;
|
||||
}
|
||||
]
|
||||
# Desktop-only: CPU temperature and wired network throughput, in place
|
||||
# of the laptop's battery readout.
|
||||
++ lib.optionals (!portable) [
|
||||
{
|
||||
block = "temperature";
|
||||
interval = 5;
|
||||
format = " $icon $average avg, $max max ";
|
||||
}
|
||||
{ block = "net"; }
|
||||
]
|
||||
++ [ { block = "sound"; } ]
|
||||
++ lib.optional portable { block = "battery"; }
|
||||
++ [
|
||||
{
|
||||
block = "time";
|
||||
interval = 5;
|
||||
format = " $timestamp.datetime(f:'%a %d/%m %R') ";
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user