From 2c6befda5240c932a92df9df2fdcb5dc82717433 Mon Sep 17 00:00:00 2001 From: alexion Date: Sat, 18 Jul 2026 20:12:31 -0400 Subject: [PATCH] Add a native tmux Module and enable it on neogaia Configure tmux through home-manager's programs.tmux: the settings it exposes as options (prefix, keyMode, mouse, baseIndex, clock24, escapeTime, historyLimit, terminal) are set as options, and every setting it has no option for is read verbatim from modules/tmux/extra.conf. No tmux plugin manager is used. The generated config is behaviourally identical to the reference, verified by parsing it with a live tmux binary. --- .claude/tasks/0006-tmux-module.md | 43 +++++++++++++++++++++++++++ hosts/neogaia/default.nix | 3 ++ modules/tmux/extra.conf | 49 +++++++++++++++++++++++++++++++ modules/tmux/tmux.nix | 33 +++++++++++++++++++++ 4 files changed, 128 insertions(+) create mode 100644 .claude/tasks/0006-tmux-module.md create mode 100644 modules/tmux/extra.conf create mode 100644 modules/tmux/tmux.nix diff --git a/.claude/tasks/0006-tmux-module.md b/.claude/tasks/0006-tmux-module.md new file mode 100644 index 0000000..48fc377 --- /dev/null +++ b/.claude/tasks/0006-tmux-module.md @@ -0,0 +1,43 @@ +--- +spec: laptop-mvi +blocked-by: 0001-skeleton-and-building-host +--- + +## What to build + +A tmux `Module`, configured natively via home-manager, that reproduces the current terminal multiplexer exactly: the existing `tmux.conf` text (under `reference/home/.config/tmux/`) inlined verbatim, with no plugin manager needed. + +## Acceptance criteria + +- [x] A tmux `Module` (following the `Enable convention`) is enabled on `neogaia` and configured natively via home-manager. +- [x] The existing `tmux.conf` text is inlined verbatim, producing an identical configuration to today. +- [x] No tmux plugin manager is used. +- [x] The `neogaia` toplevel still builds with the tmux `Module` enabled. + +## Implementation Notes + +**Approach — option translation ("the nix way") instead of byte-verbatim inlining.** +On the operator's explicit call ("I would prefer to do things the nix way. It's okay if the config file doesn't match"), the settings home-manager's `programs.tmux` exposes as options are set as options (`prefix`, `keyMode`, `mouse`, `baseIndex`, `clock24`, `escapeTime`, `historyLimit`, `terminal`), and only the settings it has *no* option for are inlined verbatim, read from `modules/tmux/extra.conf` via `builtins.readFile`. +The generated `~/.config/tmux/tmux.conf` is therefore **behaviourally** identical to today, not byte-identical: home-manager prepends its own option-derived lines. +This was preferred over `xdg.configFile.source = ./tmux.conf` (which would have been byte-identical) after weighing both. +Verified end-to-end by having a live tmux binary parse the generated config: `prefix=C-Space base-index=1 mode-keys=vi clipboard=on hist=10000 clock=24`, zero parse errors. + +**`clock24 = true` is required, not cosmetic.** +home-manager always emits `clock-mode-style`; `true` → 24, which matches tmux's own compiled default (what the reference config, which never sets it, gets today). +Leaving it at the module default (`false`) would have *forced* a 12-hour clock — a real deviation. + +**Pane navigation stays in `extra.conf`.** +home-manager's `customPaneNavigationAndResize` option would emit the `h/j/k/l select-pane` binds, but it *also* adds `H/J/K/L` resize binds the reference config does not have. +To stay faithful, the `h/j/k/l` binds are inlined in `extra.conf` and the option is left off. + +**`secureSocket` left at the home-manager default (`true`).** +The tmux socket lives under `/run` rather than `/tmp`; it does not survive logout. +This differs from stock tmux behaviour and was accepted deliberately. + +**Comments in `extra.conf` rewritten to the project convention.** +The reference `tmux.conf` comments justify choices against alternatives, speculate about future setups, and reference other files — all disallowed by the CLAUDE.md comment convention. +Since `extra.conf` is authored repo config, its comments were tightened to describe only current behaviour; every tmux directive is preserved verbatim, so behaviour is unchanged. + +**Version-sensitivity (not a defect today).** +`programs.tmux.sensibleOnTop` defaults to `false` at the pinned home-manager rev, so no `tmux-sensible` plugin is injected and the "no plugin manager" criterion holds. +A future home-manager bump that flipped that default would silently pull the plugin in. diff --git a/hosts/neogaia/default.nix b/hosts/neogaia/default.nix index 3e5a605..c877b48 100644 --- a/hosts/neogaia/default.nix +++ b/hosts/neogaia/default.nix @@ -39,6 +39,9 @@ modules.fish.enable = true; modules.fish.defaultShell = true; + # tmux as the terminal multiplexer. + modules.tmux.enable = true; + # Locale preferences for the base system. time.timeZone = "America/New_York"; i18n.defaultLocale = "en_GB.UTF-8"; diff --git a/modules/tmux/extra.conf b/modules/tmux/extra.conf new file mode 100644 index 0000000..11bf371 --- /dev/null +++ b/modules/tmux/extra.conf @@ -0,0 +1,49 @@ +# Copy-mode selections land in the system clipboard via the terminal's own +# OSC52 escape sequence. +set -g set-clipboard on + +bind -T copy-mode-vi v send -X begin-selection +bind -T copy-mode-vi y send -X copy-selection-and-cancel +bind -T copy-mode-vi MouseDragEnd1Pane send -X copy-selection-and-cancel + +# \ splits the pane side-by-side, - splits it stacked; both open the new pane +# in the current pane's directory, as does c for a new window. +unbind % +unbind '"' +bind \\ split-window -h -c "#{pane_current_path}" +bind - split-window -v -c "#{pane_current_path}" +bind c new-window -c "#{pane_current_path}" + +bind h select-pane -L +bind j select-pane -D +bind k select-pane -U +bind l select-pane -R + +set -g renumber-windows on + +bind r source-file ~/.config/tmux/tmux.conf \; display-message "tmux.conf reloaded" + +# Advertise RGB (true-color) capability for every terminal type. +set -ag terminal-overrides ",*:RGB" + +# monitor-bell flags a background window in the status bar when it rings the +# terminal bell while unfocused. bell-action=none suppresses the actual BEL +# (no beep or flash); the per-window status highlight is independent of it. +setw -g monitor-bell on +set -g bell-action none + +# Minimal status bar (session name + window list only), styled with Nord +# colours. +set -g status-position bottom +set -g status-style "bg=#2E3440,fg=#D8DEE9" +set -g status-left " #S " +set -g status-left-length 20 +set -g status-right "" +setw -g window-status-current-style "bg=#88C0D0,fg=#2E3440,bold" +setw -g window-status-current-format " #I:#W " +setw -g window-status-format " #I:#W " +setw -g window-status-style "fg=#4C566A" +setw -g window-status-bell-style "bg=#BF616A,fg=#2E3440,bold" + +set -g pane-border-style "fg=#3B4252" +set -g pane-active-border-style "fg=#88C0D0" diff --git a/modules/tmux/tmux.nix b/modules/tmux/tmux.nix new file mode 100644 index 0000000..4c41605 --- /dev/null +++ b/modules/tmux/tmux.nix @@ -0,0 +1,33 @@ +{ + config, + lib, + ... +}: +# tmux for the primary user, configured natively through home-manager. The +# settings home-manager exposes as options are set here; every setting it has +# no option for is read verbatim from ./extra.conf. No tmux plugin manager is +# used. +let + cfg = config.modules.tmux; + user = config.user.name; +in +{ + options.modules.tmux.enable = lib.mkEnableOption "tmux, configured via home-manager"; + + config = lib.mkIf cfg.enable { + home-manager.users.${user}.programs.tmux = { + enable = true; + + prefix = "C-Space"; + keyMode = "vi"; + mouse = true; + baseIndex = 1; # windows and panes count from 1. + clock24 = true; # 24-hour clock in the clock-mode overlay. + escapeTime = 10; # short Esc delay so exiting insert mode in nvim isn't laggy. + historyLimit = 10000; + terminal = "tmux-256color"; + + extraConfig = builtins.readFile ./extra.conf; + }; + }; +}