1{ config, pkgs, lib, ... }:
2
3let
4 inherit (lib) mkOption mkIf types optionalString;
5
6 cfg = config.programs.tmux;
7
8 defaultKeyMode = "emacs";
9 defaultResize = 5;
10 defaultShortcut = "b";
11 defaultTerminal = "screen";
12
13 boolToStr = value: if value then "on" else "off";
14
15 tmuxConf = ''
16 set -g default-terminal "${cfg.terminal}"
17 set -g base-index ${toString cfg.baseIndex}
18 setw -g pane-base-index ${toString cfg.baseIndex}
19
20 ${optionalString cfg.newSession "new-session"}
21
22 ${optionalString cfg.reverseSplit ''
23 bind v split-window -h
24 bind s split-window -v
25 ''}
26
27 set -g status-keys ${cfg.keyMode}
28 set -g mode-keys ${cfg.keyMode}
29
30 ${optionalString (cfg.keyMode == "vi" && cfg.customPaneNavigationAndResize) ''
31 bind h select-pane -L
32 bind j select-pane -D
33 bind k select-pane -U
34 bind l select-pane -R
35
36 bind -r H resize-pane -L ${toString cfg.resizeAmount}
37 bind -r J resize-pane -D ${toString cfg.resizeAmount}
38 bind -r K resize-pane -U ${toString cfg.resizeAmount}
39 bind -r L resize-pane -R ${toString cfg.resizeAmount}
40 ''}
41
42 ${optionalString (cfg.shortcut != defaultShortcut) ''
43 # rebind main key: C-${cfg.shortcut}
44 unbind C-${defaultShortcut}
45 set -g prefix C-${cfg.shortcut}
46 bind ${cfg.shortcut} send-prefix
47 bind C-${cfg.shortcut} last-window
48 ''}
49
50 setw -g aggressive-resize ${boolToStr cfg.aggressiveResize}
51 setw -g clock-mode-style ${if cfg.clock24 then "24" else "12"}
52 set -s escape-time ${toString cfg.escapeTime}
53 set -g history-limit ${toString cfg.historyLimit}
54
55 ${cfg.extraConfigBeforePlugins}
56
57 ${lib.optionalString (cfg.plugins != []) ''
58 # Run plugins
59 ${lib.concatMapStringsSep "\n" (x: "run-shell ${x.rtp}") cfg.plugins}
60
61 ''}
62
63 ${cfg.extraConfig}
64 '';
65
66in {
67 ###### interface
68
69 options = {
70 programs.tmux = {
71
72 enable = mkOption {
73 type = types.bool;
74 default = false;
75 description = "Whenever to configure {command}`tmux` system-wide.";
76 relatedPackages = [ "tmux" ];
77 };
78
79 aggressiveResize = mkOption {
80 default = false;
81 type = types.bool;
82 description = ''
83 Resize the window to the size of the smallest session for which it is the current window.
84 '';
85 };
86
87 baseIndex = mkOption {
88 default = 0;
89 example = 1;
90 type = types.int;
91 description = "Base index for windows and panes.";
92 };
93
94 clock24 = mkOption {
95 default = false;
96 type = types.bool;
97 description = "Use 24 hour clock.";
98 };
99
100 customPaneNavigationAndResize = mkOption {
101 default = false;
102 type = types.bool;
103 description = "Override the hjkl and HJKL bindings for pane navigation and resizing in VI mode.";
104 };
105
106 escapeTime = mkOption {
107 default = 500;
108 example = 0;
109 type = types.int;
110 description = "Time in milliseconds for which tmux waits after an escape is input.";
111 };
112
113 extraConfigBeforePlugins = mkOption {
114 default = "";
115 description = ''
116 Additional contents of /etc/tmux.conf, to be run before sourcing plugins.
117 '';
118 type = types.lines;
119 };
120
121 extraConfig = mkOption {
122 default = "";
123 description = ''
124 Additional contents of /etc/tmux.conf, to be run after sourcing plugins.
125 '';
126 type = types.lines;
127 };
128
129 historyLimit = mkOption {
130 default = 2000;
131 example = 5000;
132 type = types.int;
133 description = "Maximum number of lines held in window history.";
134 };
135
136 keyMode = mkOption {
137 default = defaultKeyMode;
138 example = "vi";
139 type = types.enum [ "emacs" "vi" ];
140 description = "VI or Emacs style shortcuts.";
141 };
142
143 newSession = mkOption {
144 default = false;
145 type = types.bool;
146 description = "Automatically spawn a session if trying to attach and none are running.";
147 };
148
149 reverseSplit = mkOption {
150 default = false;
151 type = types.bool;
152 description = "Reverse the window split shortcuts.";
153 };
154
155 resizeAmount = mkOption {
156 default = defaultResize;
157 example = 10;
158 type = types.int;
159 description = "Number of lines/columns when resizing.";
160 };
161
162 shortcut = mkOption {
163 default = defaultShortcut;
164 example = "a";
165 type = types.str;
166 description = "Ctrl following by this key is used as the main shortcut.";
167 };
168
169 terminal = mkOption {
170 default = defaultTerminal;
171 example = "screen-256color";
172 type = types.str;
173 description = ''
174 Set the $TERM variable. Use tmux-direct if italics or 24bit true color
175 support is needed.
176 '';
177 };
178
179 secureSocket = mkOption {
180 default = true;
181 type = types.bool;
182 description = ''
183 Store tmux socket under /run, which is more secure than /tmp, but as a
184 downside it doesn't survive user logout.
185 '';
186 };
187
188 plugins = mkOption {
189 default = [];
190 type = types.listOf types.package;
191 description = "List of plugins to install.";
192 example = lib.literalExpression "[ pkgs.tmuxPlugins.nord ]";
193 };
194
195 withUtempter = mkOption {
196 description = ''
197 Whether to enable libutempter for tmux.
198 This is required so that tmux can write to /var/run/utmp (which can be queried with `who` to display currently connected user sessions).
199 Note, this will add a guid wrapper for the group utmp!
200 '';
201 default = true;
202 type = types.bool;
203 };
204 };
205 };
206
207 ###### implementation
208
209 config = mkIf cfg.enable {
210 environment = {
211 etc."tmux.conf".text = tmuxConf;
212
213 systemPackages = [ pkgs.tmux ] ++ cfg.plugins;
214
215 variables = {
216 TMUX_TMPDIR = lib.optional cfg.secureSocket ''''${XDG_RUNTIME_DIR:-"/run/user/$(id -u)"}'';
217 };
218 };
219 security.wrappers = mkIf cfg.withUtempter {
220 utempter = {
221 source = "${pkgs.libutempter}/lib/utempter/utempter";
222 owner = "root";
223 group = "utmp";
224 setuid = false;
225 setgid = true;
226 };
227 };
228 };
229
230 imports = [
231 (lib.mkRenamedOptionModule [ "programs" "tmux" "extraTmuxConf" ] [ "programs" "tmux" "extraConfig" ])
232 ];
233}