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