tmux 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md ADDED
@@ -0,0 +1,1076 @@
1
+ # tmux.rb
2
+
3
+ tmux.rb is a Ruby DSL that compiles readable, structured block-based
4
+ configuration into native tmux configuration and format strings.
5
+
6
+ <!-- example: intro -->
7
+ ```ruby
8
+ # ~/.config/tmux/tmux.rb
9
+ Tmux.configure do
10
+ global.prefix = "C-s" # set-option -g prefix "C-s"
11
+ global.mouse = true # set-option -g mouse on
12
+ window.mode_keys = :vi # set-option -gw mode-keys "vi"
13
+
14
+ global.status.right = Tmux.format do
15
+ if?(Tmux.window_zoomed_flag) do
16
+ left_flag "Z", bg: :white, fg: :black, attrs: :bold
17
+ end
18
+ left_segment Tmux.pane_current_command, bg: :colour238, fg: :white
19
+ left_segment "#{Tmux.session_name}@#{Tmux.host}", bg: :blue, fg: :black
20
+ end
21
+
22
+ bind "|", %q{split-window -h -c "#{pane_current_path}"}
23
+ end
24
+ ```
25
+
26
+ ```bash
27
+ tmux.rb build ~/.config/tmux/tmux.rb --output ~/.tmux.conf
28
+ ```
29
+
30
+ <details>
31
+ <summary>Screenshot: the status line this configuration draws, in a zoomed window</summary>
32
+
33
+ ![The right side of the status line: a bold Z flag, the pane's command, and session@host](docs/images/intro.png)
34
+
35
+ </details>
36
+
37
+ Screenshots in this README are real tmux sessions rendering the example just
38
+ above them ([how they are made](docs/screenshots/README.md)). They use the
39
+ **Catppuccin Mocha** terminal color scheme and JetBrains Mono Nerd Font, with
40
+ the status bar set to the terminal's own colors. Named colors such as `:blue`,
41
+ `:white`, or `colour1` take their shade from your terminal's color scheme;
42
+ `colour16` to `colour255` and hex colors such as `"#ff8800"` look the same
43
+ everywhere. Powerline edges need a font with Powerline glyphs, such as a
44
+ [Nerd Font](https://www.nerdfonts.com).
45
+
46
+ ## Why
47
+
48
+ Hand-written tmux configuration gets hard to change once it grows: status
49
+ lines become deeply nested `#{?...,...,...}` expressions where a misplaced
50
+ comma silently breaks rendering, and every level of quoting has to be escaped
51
+ by hand. tmux.rb keeps tmux's model but lets you write it as ordinary Ruby.
52
+
53
+ - **Compile, don't interpret.** Ruby runs only while you build or validate a
54
+ configuration. The output is plain tmux configuration, and tmux evaluates
55
+ its own formats, so rendering the status line never spawns Ruby and needs
56
+ nothing installed beyond tmux.
57
+ - **Skip manual escaping.** Write text, styles, variables, and conditionals
58
+ directly, without managing nested tmux quoting and escaping.
59
+ - **Keep tmux within reach.** The DSL adds readable names and helpers but
60
+ hides nothing: any option, command, or format string can still be written
61
+ directly, and `raw` passes tmux commands through verbatim.
62
+ - **Generate deterministic output.** The same source always produces
63
+ byte-identical configuration, so the generated file is easy to review and
64
+ compare in version control.
65
+ - **Treat the live server as a boundary.** `build`, `check`, and `diff` never
66
+ touch a running tmux server; only an explicit `load` does.
67
+
68
+ ## Installation
69
+
70
+ ```bash
71
+ gem install tmux
72
+ ```
73
+
74
+ tmux.rb needs Ruby 3.2 or newer. The full DSL is tested on tmux 3.2 and newer.
75
+ Compilation requires the target tmux executable to be installed.
76
+
77
+ The gem installs the `tmux.rb` command (also available as `tmux_rb`). Ruby
78
+ code that uses the library directly loads it with `require "tmux"`.
79
+
80
+ ## Configuration files
81
+
82
+ `Tmux.configure` builds configuration commands: option setters, key bindings,
83
+ and other tmux commands. `Tmux.format` builds one reusable value, such as the
84
+ contents of `status-right`, which those commands can assign to an option.
85
+
86
+ A configuration file ends with either resulting object (`Tmux::Config` or
87
+ `Tmux::Format`). Run it with the `tmux.rb` command, or use
88
+ `Tmux.load_file("config.rb")` from Ruby. Constants and helper methods defined
89
+ at the top level stay local to that file.
90
+
91
+ **Configuration files are trusted code.** `build`, `check`, `diff`, and
92
+ `load` all execute them, with the same access as a Ruby script. Only build
93
+ configurations you would be willing to run as a script. A binding created
94
+ with `load_config` re-runs its Ruby file every time the key is pressed.
95
+
96
+ ### Blocks and helper methods
97
+
98
+ Inside `Tmux.configure` and `Tmux.format` blocks, call DSL methods directly.
99
+ Methods you `def` and instance variables you set in a block stay visible in
100
+ its nested blocks, including conditional branches. Local variables and
101
+ lambdas work throughout the blocks too.
102
+
103
+ Give a block one parameter to keep Ruby's normal scoping, with the builder
104
+ passed in:
105
+
106
+ ```ruby
107
+ def status_color = "colour37"
108
+
109
+ Tmux.configure do |c|
110
+ # self is unchanged here, so status_color is the method defined above.
111
+ c.global.status.style = "fg=#{status_color}" # set-option -g status-style "fg=colour37"
112
+ end
113
+ ```
114
+
115
+ ## CLI
116
+
117
+ ```bash
118
+ tmux.rb build config.rb # print the generated configuration
119
+ tmux.rb build config.rb --output tmux.conf # write it atomically
120
+ tmux.rb check config.rb # ask an isolated tmux server to parse it
121
+ tmux.rb diff config.rb ~/.tmux.conf # unified diff; exit 0 means identical
122
+ tmux.rb load config.rb # compile and source into a tmux server
123
+ tmux.rb load -S /tmp/outer.sock config.rb # ...into the server at a socket path
124
+ tmux.rb load -L work config.rb # ...or the server with a socket name
125
+ tmux.rb --version
126
+ ```
127
+
128
+ Every command accepts `--help`.
129
+
130
+ - **`build --output`** updates the destination atomically. A
131
+ symlinked destination (such as a `~/.tmux.conf` that points into a dotfiles
132
+ repository) is followed rather than replaced, and an existing file keeps
133
+ its permissions.
134
+ - **`check`** checks tmux syntax in an isolated server, including commands
135
+ inside `if?` branches. Option names and scopes are already checked
136
+ during compilation. It does not apply commands or validate option values.
137
+ Loading the file into an isolated server checks values too:
138
+ `tmux -L scratch -f /dev/null new-session -d 'sleep 600'`, then
139
+ `tmux -L scratch source-file tmux.conf`.
140
+ - **`load`** sources the result into the server `$TMUX` names, or into the
141
+ one given with `-S SOCKET` or `-L NAME`. Pass one of those from inside a
142
+ nested tmux, where `$TMUX` names the inner server.
143
+
144
+ Errors raised while evaluating a configuration are reported with the file and
145
+ line that caused them.
146
+
147
+ ### Target tmux version
148
+
149
+ By default, configurations target the `tmux` on your `PATH`. All built-in
150
+ option names and scopes are checked against that executable's supported
151
+ options. To build for a different version, select its executable with
152
+ `--tmux`:
153
+
154
+ ```bash
155
+ tmux.rb build config.rb --tmux /opt/tmux-3.3/bin/tmux
156
+ tmux.rb check config.rb --tmux /opt/tmux-3.3/bin/tmux
157
+ tmux.rb load -S /tmp/outer.sock config.rb --tmux /opt/tmux-3.3/bin/tmux
158
+ ```
159
+
160
+ From Ruby, use `Tmux.load_file("config.rb", tmux: "/opt/tmux-3.3/bin/tmux")`
161
+ or `Tmux.configure(tmux: "/opt/tmux-3.3/bin/tmux") { ... }`.
162
+ `load` refuses to apply a configuration if the destination server's version
163
+ does not match the selected executable. Use `tmux_from` and `tmux_below` for
164
+ settings that differ between versions; only the applicable version branch's
165
+ options are checked.
166
+
167
+ ## Format DSL
168
+
169
+ Use `Tmux.format` for status lines, window tabs, pane borders, and anywhere
170
+ else tmux expands formats. [Assign the resulting format to an option](#format-valued-options)
171
+ to use it in a configuration:
172
+
173
+ <!-- example: window-tab -->
174
+ ```ruby
175
+ # A window tab: the index in a colored block, then the name, which turns
176
+ # yellow while a flash is lit.
177
+ flash = Tmux.user_option(:@attn_flash)
178
+ phase = Tmux.user_option(:@attn_phase)
179
+
180
+ Tmux.format do
181
+ # Set the foreground and background for what follows.
182
+ style fg: :white, bg: :colour238
183
+ # Literal text with the window index interpolated; it stays dynamic.
184
+ text " #{Tmux.window_index} "
185
+ # The name's colors: black on yellow while flashing, grey otherwise.
186
+ if?(flash && phase) do
187
+ style fg: :black, bg: :yellow
188
+ end.else do
189
+ style fg: :colour250, bg: :colour236
190
+ end
191
+ # The window name, padded with a space on each side.
192
+ text " #{Tmux.window_name} "
193
+ # Return to the default style so nothing leaks into the next tab.
194
+ reset_style
195
+ end
196
+ ```
197
+
198
+ <details>
199
+ <summary>Screenshot: three windows drawn with this format, the last one mid-flash</summary>
200
+
201
+ ![Three window tabs, each a grey index block followed by the window name; the flashing "logs" tab's name is black on yellow](docs/images/window-tab.png)
202
+
203
+ </details>
204
+
205
+ The sections below cover each part of the format DSL. Most calls can be
206
+ chained; `if?` supports `.elsif?` and `.else` branches.
207
+
208
+ ### Text
209
+
210
+ `text` adds text that tmux shows as written. Expressions interpolated into the
211
+ string stay dynamic. Other characters, including `#`, `%`, `,`, and `}`,
212
+ print literally, even inside conditional branches.
213
+
214
+ ```ruby
215
+ Tmux.format do
216
+ # " 3 " for window 3: the interpolated index stays a tmux variable.
217
+ text " #{Tmux.window_index} "
218
+ # Expressions can also be passed as separate parts.
219
+ text "[", Tmux.session_name, "]"
220
+ # "#", "%", and "," print literally, even inside a conditional.
221
+ if?(Tmux.pane_active) { text "#1, 50%" }
222
+ # literal: true prints a string exactly as written, format code included.
223
+ text "\#{not_a_variable}", literal: true
224
+ end
225
+ ```
226
+
227
+ `text` rejects `#[` and `#(` in a string, since styles and shell commands
228
+ have their own methods (`style` and `command`).
229
+
230
+ ### Dates and times
231
+
232
+ `strftime` adds a date/time format that tmux expands each time it renders.
233
+ Unlike `text`, its `%` codes are kept.
234
+
235
+ ```ruby
236
+ Tmux.format do
237
+ # "Tue 14:05", refreshed with the status line.
238
+ strftime "%a %H:%M"
239
+ end
240
+ ```
241
+
242
+ ### Appending expressions, formats, and format code
243
+
244
+ `append` adds tmux format source as is: an expression, a format built
245
+ elsewhere, or a string of tmux format code. Unlike `text`, a
246
+ string given to `append` is format code, not literal text.
247
+
248
+ ```ruby
249
+ clock = Tmux.format { strftime "%H:%M" }
250
+
251
+ Tmux.format do
252
+ # An expression.
253
+ append Tmux.pane_current_path
254
+ # Another format.
255
+ append clock
256
+ # Raw tmux format code, for anything the DSL has no helper for.
257
+ append "\#{t:window_activity}"
258
+ end
259
+ ```
260
+
261
+ ### Shell commands
262
+
263
+ `command` adds a `#(...)` shell command, which tmux runs in the background and
264
+ replaces with its output. Its text is format source, so `#{...}` codes expand
265
+ before the shell sees it. Commands must have balanced parentheses.
266
+
267
+ ```ruby
268
+ Tmux.format do
269
+ # The current git branch of the active pane's directory.
270
+ command "git -C \#{pane_current_path} branch --show-current"
271
+ end
272
+ ```
273
+
274
+ ### Styles
275
+
276
+ `style` sets colors and attributes for what follows; `reset_style` returns to
277
+ the default. Given a block, `style` applies only to the block's content,
278
+ which `p:` pads with spaces on both sides, and the style from before the
279
+ block returns after it.
280
+
281
+ <!-- example: styles -->
282
+ ```ruby
283
+ Tmux.format do
284
+ # Colors and attributes as a hash.
285
+ style fg: "#000000", bg: "#ff0000", attrs: :bold
286
+ text " warning "
287
+ # Attribute names alone.
288
+ style :nobold
289
+ # A styled, padded block: the window index in white on blue.
290
+ style(fg: "#ffffff", bg: :blue, p: 1) do
291
+ append Tmux.window_index
292
+ end
293
+ # After the block, the black on red from before it returns.
294
+ text " here "
295
+ # Back to the default style.
296
+ reset_style
297
+ end
298
+ ```
299
+
300
+ <details>
301
+ <summary>Screenshot: this format in the status line</summary>
302
+
303
+ ![A bold black-on-red "warning", the window index in white on blue, then "here" back in black on red](docs/images/styles.png)
304
+
305
+ </details>
306
+
307
+ ### Conditionals
308
+
309
+ `if?` chooses content each time tmux renders. It includes its block when the
310
+ condition is true; use `!` to negate a condition.
311
+ Chain `.else` on `if?` for the false branch, or omit it to emit nothing when
312
+ false. Add `.elsif?(condition)` for more branches; the first matching branch
313
+ wins. Every branch can contain any format DSL calls, including nested
314
+ conditionals.
315
+
316
+ See [Expressions and conditions](#expressions-and-conditions) for variables,
317
+ comparisons, and combining tests with `&&`, `||`, and `!`. The same tests
318
+ work in configuration conditionals.
319
+
320
+ <!-- example: conditionals -->
321
+ ```ruby
322
+ Tmux.format do
323
+ # Show "(name)" when the window is zoomed, the plain name otherwise.
324
+ if?(Tmux.window_zoomed_flag) do
325
+ text "(#{Tmux.window_name})"
326
+ end.else do
327
+ append Tmux.window_name
328
+ end
329
+ # Show a marker unless the pane is in a mode (copy mode, for example).
330
+ if?(!Tmux.pane_in_mode) { text " *" }
331
+ # Show " sync" first, otherwise " mode" in copy mode, or " async".
332
+ if?(Tmux.pane_synchronized) do
333
+ text " sync"
334
+ end.elsif?(Tmux.pane_in_mode) do
335
+ text " mode"
336
+ end.else do
337
+ text " async"
338
+ end
339
+ end
340
+ ```
341
+
342
+ <details>
343
+ <summary>Screenshot: this format in a zoomed window</summary>
344
+
345
+ ![The status line reads "(zsh) * async"](docs/images/conditionals.png)
346
+
347
+ </details>
348
+
349
+ Outside a format block, `Tmux.if?` builds a reusable conditional format with
350
+ the same `.elsif?` and `.else` chain. Its branches use the format DSL; assign
351
+ the result to an option or `append` it inside another format:
352
+
353
+ ```ruby
354
+ label = Tmux.if?(Tmux.pane_in_mode) do
355
+ text "copy"
356
+ end.elsif?(Tmux.client_prefix) do
357
+ text "prefix"
358
+ end.else do
359
+ text "normal"
360
+ end
361
+
362
+ Tmux.configure do
363
+ global.status.left = label
364
+ global.status.right = Tmux.format do
365
+ text "["
366
+ append label
367
+ text "]"
368
+ end
369
+ end
370
+ ```
371
+
372
+ ### Value and style switches
373
+
374
+ Use a [conditional chain](#conditionals) to choose which format DSL calls
375
+ run. Use `switch` to choose a value to pass to a call, such as a color for
376
+ `fg:` or `bg:`. The returned expression can be reused across styles or formats.
377
+
378
+ Call `switch` inside `Tmux.format` or `Tmux.configure` blocks, or
379
+ `Tmux.switch` outside them. Both calls build the same kind of expression;
380
+ choosing a value does not itself add content or configuration commands.
381
+
382
+ Both choose the first matching condition when tmux renders. A switch
383
+ requires an `else:` value; a conditional chain can omit `.else` to emit
384
+ nothing when no condition matches.
385
+
386
+ `switch_style` chooses a set of style attributes for the content that follows.
387
+ It takes the same mapping as `switch`, with style hashes as the choices, and
388
+ is shorthand for a conditional chain whose branches only call `style`.
389
+
390
+ <!-- example: switch -->
391
+ ```ruby
392
+ Tmux.format do
393
+ # Orange while the prefix key is held, green in copy mode, grey otherwise.
394
+ background = switch(
395
+ Tmux.client_prefix => "#FF8800",
396
+ Tmux.pane_in_mode => "#6A9955",
397
+ else: "#A6ADC8"
398
+ )
399
+ # Use that color as a background.
400
+ style(fg: :black, bg: background, p: 1) { append Tmux.session_name }
401
+ # Back to the default style, then a space.
402
+ reset_style
403
+ text " "
404
+ # Choose an entire style: red when input is off, blue otherwise.
405
+ switch_style(Tmux.pane_input_off => {fg: :red}, else: {fg: :blue})
406
+ text "#{Tmux.pane_index}"
407
+ end
408
+ ```
409
+
410
+ `switch` values can contain tmux format code. For literal text, use `text`
411
+ inside a conditional branch instead.
412
+
413
+ <details>
414
+ <summary>Screenshot: this format with pane input disabled, with the prefix held, and in copy mode</summary>
415
+
416
+ ![The session name on a grey background, followed by a red pane index while pane input is disabled](docs/images/switch.png)
417
+
418
+ ![The session name on an orange background while the prefix is held](docs/images/switch-prefix.png)
419
+
420
+ ![The session name on a green background in copy mode](docs/images/switch-copy-mode.png)
421
+
422
+ </details>
423
+
424
+ ### Wrapping
425
+
426
+ `wrap` surrounds a block's content with a prefix and suffix. The suffix is
427
+ drawn in the style that was active before the block, even if the block
428
+ changed it, including a style chosen by `switch_style`.
429
+
430
+ <!-- example: wrap -->
431
+ ```ruby
432
+ Tmux.format do
433
+ switch_style(Tmux.pane_input_off => {fg: :red}, else: {fg: :blue})
434
+ # Yellow "session:window.pane", with brackets in the selected color.
435
+ wrap "[", "]" do
436
+ style fg: :yellow
437
+ text "#{Tmux.session_name}:#{Tmux.window_index}."
438
+ # Highlight only the pane number while the prefix is held.
439
+ if?(Tmux.client_prefix) { style fg: :white }
440
+ append Tmux.pane_index
441
+ end
442
+ # A template lambda receives a placeholder for the green command name.
443
+ wrap ->(content) { " (#{content}) " } do
444
+ style fg: :green
445
+ append Tmux.pane_current_command
446
+ end
447
+ end
448
+ ```
449
+
450
+ <details>
451
+ <summary>Screenshot: this format in the status line</summary>
452
+
453
+ ![Blue brackets around yellow "demo:0.0", then blue parentheses around green "zsh"](docs/images/wrap.png)
454
+
455
+ </details>
456
+
457
+ ### Powerline helpers
458
+
459
+ Flags, segments, and chips draw Powerline-style blocks with their edge glyphs
460
+ and color transitions. Their content can be a string (with interpolated
461
+ expressions), an expression, or an array mixing the two; a block draws custom
462
+ content instead.
463
+
464
+ - `left_segment` / `right_segment`: adjoining blocks with one pointed edge.
465
+ `left_segment` draws its edge over the previous block's background
466
+ automatically. For `right_segment`, set `over:` to the next block's
467
+ background to join the blocks without gaps.
468
+ - `left_flag` / `right_flag`: a self-contained badge, pointed at one end and
469
+ notched at the other. `over:` sets the background behind the point;
470
+ `notch_over:` can set a different background for the notch. If omitted,
471
+ the notch uses the same background as the point.
472
+ - `chip`: a floating block with an edge on each side. When both edges are thin,
473
+ `bg:` colors the outline and text, and `over:` sets the shared background
474
+ behind the edges and content (default `:default`). `fg:` sets the text color
475
+ for chips with solid edges.
476
+ - `p:` sets the padding spaces on each side (default 1); `attrs:` styles the
477
+ content.
478
+ - `left:` and `right:` choose a chip's edge shapes: `:triangle` (the default),
479
+ `:thin`, `:round`, `:round_thin`, `:rslant` (`/`), or `:lslant` (`\`).
480
+ The keyword supplies the side, so `left: :round, right: :round` makes a
481
+ rounded chip. Using the same slant on both sides makes a parallelogram;
482
+ the two slants can also be mixed.
483
+ - `glyph:` accepts the same shapes for flags and segments, using the helper's
484
+ `left_` or `right_` direction. Exact names from `Tmux::Glyph` and literal
485
+ glyph strings are also accepted when you want a specific character.
486
+
487
+ `left_` helpers point left, for the right side of the status line; `right_`
488
+ helpers point right, for the left side:
489
+
490
+ <!-- example: status-right -->
491
+ ```ruby
492
+ # status-right: blocks chained from right to left.
493
+ Tmux.format do
494
+ # Path (with the home directory shortened to ~), then command; each edge
495
+ # is drawn over the previous block's color.
496
+ left_segment Tmux.pane_current_path.substitute("^#{Dir.home}", "~"), bg: :colour236, fg: :white
497
+ left_segment Tmux.pane_current_command, bg: :colour238, fg: :white
498
+ end
499
+ ```
500
+
501
+ <details>
502
+ <summary>Screenshot: this format as status-right</summary>
503
+
504
+ ![At the right edge: adjoining path and command segments](docs/images/status-right.png)
505
+
506
+ </details>
507
+
508
+ <!-- example: status-left -->
509
+ ```ruby
510
+ # status-left: blocks chained from left to right.
511
+ Tmux.format do
512
+ # Each edge sits over the next block's background for a continuous chain.
513
+ right_segment Tmux.session_name, bg: :blue, fg: :black, over: :colour238
514
+ # Content as a keyword, with its padding supplied by the string.
515
+ right_segment content: " #{Tmux.window_index} ", bg: :colour238, fg: :white, p: 0, over: :colour236
516
+ # The final edge meets the default background; only the text is italic.
517
+ right_flag(bg: :colour236, fg: :white, over: :default, attrs: :italics, close: false) do
518
+ text " #{Tmux.host_short} "
519
+ end
520
+ end
521
+ ```
522
+
523
+ <details>
524
+ <summary>Screenshot: this format as status-left</summary>
525
+
526
+ ![A continuous chain of blue session, grey window index, and dark grey italic host segments](docs/images/status-left.png)
527
+
528
+ </details>
529
+
530
+ <!-- example: chips -->
531
+ ```ruby
532
+ Tmux.format do
533
+ # A chip with the default triangle edges...
534
+ chip "default", bg: :blue, fg: :black
535
+ text " "
536
+ # ...rounded edges...
537
+ chip "round", bg: :magenta, fg: :black, left: :round, right: :round
538
+ text " "
539
+ # ...thin edges and cyan text, all on the default background...
540
+ chip "thin", bg: :cyan, fg: :black, left: :thin, right: :thin
541
+ text " "
542
+ # ...and slants leaning right or left, with the side inferred for each edge.
543
+ chip "rslant", bg: :yellow, fg: :black, left: :rslant, right: :rslant
544
+ text " "
545
+ chip "lslant", bg: :green, fg: :black, left: :lslant, right: :lslant
546
+ end
547
+ ```
548
+
549
+ <details>
550
+ <summary>Screenshot: these chips in the status line</summary>
551
+
552
+ ![Five chips labeled default, round, thin, rslant, and lslant, each using its named edge style](docs/images/chips.png)
553
+
554
+ </details>
555
+
556
+ ### Zones
557
+
558
+ `Tmux.pane_border` builds a format from left, centre, and right zones, in
559
+ that order whatever order they are defined in. Each zone is an ordinary
560
+ format block.
561
+
562
+ <!-- example: zones -->
563
+ ```ruby
564
+ Tmux.configure do
565
+ global.status = false
566
+ # Heavy pane borders with a label along the top, the active one in blue.
567
+ window.pane.border.status = :top
568
+ window.pane.border.lines = :heavy
569
+ window.pane.border.style = {fg: "#45475a"}
570
+ window.pane.active_border.style = {fg: "#89b4fa"}
571
+
572
+ active = Tmux.pane_active
573
+ accent = switch(active => "#89b4fa", else: "#45475a")
574
+ window.pane.border.format = Tmux.pane_border do
575
+ # The command and directory on the left, brighter on the active pane...
576
+ left do
577
+ style(fg: switch(active => "#cdd6f4", else: "#7f849c"), p: 1) do
578
+ text "#{Tmux.pane_current_command} #{Tmux.pane_current_path.substitute("^#{Dir.home}", "~")}"
579
+ end
580
+ end
581
+ # ...the window name in the middle...
582
+ center { append " #{Tmux.window_name} "}
583
+ # ...and the pane number as a chip on the right.
584
+ right { chip Tmux.pane_index, bg: accent, fg: "#11111b", left: :round, right: :round, p: 0 }
585
+ end
586
+ end
587
+ ```
588
+
589
+ <details>
590
+ <summary>Screenshot: two panes with these borders</summary>
591
+
592
+ ![Two panes whose top borders show the command and directory, the window name, and the pane number as a chip, highlighted on the active pane](docs/images/zones.png)
593
+
594
+ </details>
595
+
596
+ ### Expanding options
597
+
598
+ `expand_option` inserts a user option's value and expands it as a format, for
599
+ segments that a plugin or script maintains. Spell user-option names with
600
+ tmux's `@` prefix: `:@my_plugin_segment` as a Ruby symbol, or
601
+ `"@my_plugin_segment"` as a string. The same spelling works for
602
+ `Tmux.user_option` expressions and scoped assignments such as
603
+ `global[:@my_plugin_segment] = "..."`.
604
+
605
+ ```ruby
606
+ Tmux.format do
607
+ # Whatever format string a plugin keeps in @my_plugin_segment.
608
+ expand_option :@my_plugin_segment
609
+ end
610
+ ```
611
+
612
+ ## Expressions and conditions
613
+
614
+ Expressions represent tmux values that are evaluated when tmux uses them.
615
+ Use them as conditions in both configuration and format blocks, interpolate
616
+ them into `text`, or pass them to `append`, `style`, and the Powerline helpers.
617
+
618
+ ```ruby
619
+ Tmux.format do
620
+ # Built-in variables: client_prefix, pane_active, pane_current_command,
621
+ # pane_current_path, pane_in_mode, pane_index, pane_input_off, pane_mode,
622
+ # pane_synchronized, window_index, window_name, window_zoomed_flag,
623
+ # session_name, host, host_short.
624
+ text "#{Tmux.session_name} "
625
+ # Any other tmux variable by name.
626
+ text "#{Tmux.variable(:window_panes)} "
627
+ # A user option (@attn_state).
628
+ if?(Tmux.user_option(:@attn_state)) { text "!" }
629
+ # Raw tmux format code as an expression.
630
+ text Tmux.raw("\#{t:session_created}")
631
+ end
632
+ ```
633
+
634
+ Use `>`, `<`, `>=`, and `<=` for numeric comparisons, and `==` / `!=` for
635
+ string comparisons. Put the expression on the left of the operator;
636
+ literal operands are escaped for you:
637
+
638
+ ```ruby
639
+ command = Tmux.pane_current_command
640
+
641
+ Tmux.format do
642
+ # String comparison.
643
+ if?(command == "vim") { text "editing" }
644
+ if?(command != "zsh") { text "busy" }
645
+
646
+ # Numeric comparison.
647
+ if?(Tmux.pane_index > 9) { text "more than 9" }
648
+ if?(Tmux.pane_index < 2) { text "fewer than 2" }
649
+ if?(Tmux.pane_index >= 10) { text "10 or more" }
650
+ if?(Tmux.pane_index <= 1) { text "1 or fewer" }
651
+
652
+ # Regular-expression match.
653
+ if?(command.matches("^(bash|zsh|fish)$")) { text "shell" }
654
+
655
+ # Regular-expression substitution: shorten the home directory to "~/".
656
+ text Tmux.pane_current_path.substitute("^#{Dir.home}/", "~/")
657
+ end
658
+ ```
659
+
660
+ `substitute` does not accept `:` in its pattern or replacement.
661
+
662
+ ### Combining conditions
663
+
664
+ Write `&&` / `||` directly in `if?`, `.elsif?`, or `Tmux.if?` conditions, or
665
+ in inline `switch` / `switch_style` condition keys. Use `!` to negate an
666
+ expression. Comparisons bind more tightly than `&&` / `||`, and parentheses
667
+ work as in ordinary Ruby:
668
+
669
+ ```ruby
670
+ active = Tmux.pane_active
671
+
672
+ Tmux.format do
673
+ if?(active && Tmux.client_prefix) { text "prefix" }
674
+ .elsif?(active && Tmux.pane_in_mode) { text "copy mode" }
675
+ .else { text "normal" }
676
+
677
+ style fg: switch(active && Tmux.client_prefix => :red, else: :white)
678
+ end
679
+ ```
680
+
681
+ Use tmux expressions as logical operands, not Ruby booleans or `nil`. Keep
682
+ conditions to a single expression, without assignments, control flow, or
683
+ nested blocks. To reuse conditional content, build a `Tmux.if?` format.
684
+
685
+ `&&` / `||` require configuration files run through the CLI or `Tmux.load_file`,
686
+ not plain Ruby `require`, `load`, or direct library calls. Put combined
687
+ conditions directly in the calls above, not in intermediate assignments such
688
+ as `combined = active && Tmux.client_prefix`. Use `if?`, not Ruby's `if`, for
689
+ conditions that depend on tmux state.
690
+
691
+ ## Configuration DSL
692
+
693
+ Inside `Tmux.configure`, statements apply in the order they are written.
694
+
695
+ ### Options by name
696
+
697
+ `global` (session options), `window` (window and pane options), and `server`
698
+ support every built-in option available in the target tmux version, each in
699
+ its proper scope. Unknown names and settings in the wrong scope raise errors.
700
+ Use underscores for native dashes, group related names with dots, or use the
701
+ exact native name in brackets. These spellings are interchangeable and use
702
+ the same validation, as the equivalent assignments below show.
703
+ Assigning sets an option, `<<` appends to it, and `unset` reverts it to its
704
+ inherited value. Style options also accept a hash.
705
+
706
+ ```ruby
707
+ Tmux.configure do
708
+ # Three equivalent ways to set status-interval.
709
+ global.status_interval = 5 # set-option -g status-interval 5
710
+ global.status.interval = 5 # set-option -g status-interval 5
711
+ global[:"status-interval"] = 5 # set-option -g status-interval 5
712
+
713
+ # Session options.
714
+ global.prefix = "C-s" # set-option -g prefix "C-s"
715
+ global.prefix2 = "C-a" # set-option -g prefix2 "C-a"
716
+ global.base_index = 1 # set-option -g base-index 1
717
+ global.status.position = :top # set-option -g status-position "top"
718
+
719
+ # A style option, written as a hash.
720
+ global.status.style = {bg: :default, fg: :white} # set-option -g status-style "bg=default,fg=white"
721
+
722
+ # Window and pane options.
723
+ window.mode_keys = :vi # set-option -gw mode-keys "vi"
724
+
725
+ # The same flexibility applies to pane-border-lines.
726
+ window.pane_border_lines = :heavy # set-option -gw pane-border-lines "heavy"
727
+ window.pane.border.lines = :heavy # set-option -gw pane-border-lines "heavy"
728
+ window[:"pane-border-lines"] = :heavy # set-option -gw pane-border-lines "heavy"
729
+
730
+ # Server options; << appends (set-option -a).
731
+ server.escape_time = 0 # set-option -s escape-time 0
732
+ server.terminal_features << ",*:RGB" # set-option -sa terminal-features ",*:RGB"
733
+
734
+ # Revert to the inherited value.
735
+ window.status.bell_style.unset # set-option -gwu window-status-bell-style
736
+ end
737
+ ```
738
+
739
+ ### Native names, custom options, and arrays
740
+
741
+ Bracket notation accepts native option names with the same validation.
742
+ Use it for custom `@` options, whose names you or a plugin define. Integer
743
+ indices select elements of tmux array options.
744
+
745
+ ```ruby
746
+ Tmux.configure do
747
+ # Brackets demonstrate an exact native name; global.status_interval = 5 is equivalent.
748
+ global[:"status-interval"] = 5 # set-option -g status-interval 5
749
+ window.monitor_bell = true # set-option -gw monitor-bell on
750
+ server.exit_empty = false # set-option -s exit-empty off
751
+
752
+ # Custom options use their native @ names, in whichever scope you need.
753
+ global[:@my_plugin_color] = "colour37" # set-option -g @my_plugin_color "colour37"
754
+ window[:@my_plugin_segment] = "!" # set-option -gw @my_plugin_segment "!"
755
+
756
+ # A custom terminal key sequence in an array option.
757
+ server.user_keys[0] = "\033[5;30012~" # set-option -s user-keys[0] "\033[5;30012~"
758
+
759
+ # Append to the current value.
760
+ global.update_environment << "MY_VAR" # set-option -ga update-environment "MY_VAR"
761
+
762
+ # Revert an option.
763
+ global.status.right.unset # set-option -gu status-right
764
+ end
765
+ ```
766
+
767
+ ### Format-valued options
768
+
769
+ Assign a `Tmux.format` wherever an option takes a format string:
770
+
771
+ ```ruby
772
+ Tmux.configure do
773
+ # An option whose value is a format...
774
+ # set-option -g status-left " #{session_name} "
775
+ global.status.left = Tmux.format do
776
+ text " #{Tmux.session_name} "
777
+ end
778
+
779
+ # ...and a window option whose value is a format.
780
+ # set-option -gw window-status-format " #{window_index}:#{window_name} "
781
+ window.status.format = Tmux.format do
782
+ text " #{Tmux.window_index}:#{Tmux.window_name} "
783
+ end
784
+ end
785
+ ```
786
+
787
+ ### Variable expansion
788
+
789
+ When tmux loads the configuration it expands `$NAME` (a name starting with a
790
+ letter or `_`) and a leading `~` in option values, just as in hand-written
791
+ configuration. `expand: false` keeps a value literal until a shell or a
792
+ `#(...)` command sees it. Use an option's `set` method to pass `expand: false`
793
+ or `append: true` alongside its value.
794
+
795
+ ```ruby
796
+ Tmux.configure do
797
+ # tmux stores your home directory here, expanded when the file loads.
798
+ global[:@home] = "$HOME" # set-option -g @home "$HOME"
799
+
800
+ # Stored literally, as "~/bin/$COMMAND".
801
+ global[:@literal_path].set("~/bin/$COMMAND", expand: false) # set-option -g @literal_path "\~/bin/\$COMMAND"
802
+
803
+ # The shell, not tmux, expands $HOME each time the command runs.
804
+ # set-option -g status-right "#([ -f \$HOME/.name ] && cat \$HOME/.name || hostname)"
805
+ global.status.right.set(Tmux.format do
806
+ command '[ -f $HOME/.name ] && cat $HOME/.name || hostname'
807
+ end, expand: false)
808
+ end
809
+ ```
810
+
811
+ ### Environment, sourcing, and shell commands
812
+
813
+ ```ruby
814
+ Tmux.configure do
815
+ # A global environment variable for new panes.
816
+ environment "EDITOR", "nvim"
817
+
818
+ # Load another tmux file; quiet: true skips a missing one without an error.
819
+ source "~/.tmux.local.conf", quiet: true
820
+
821
+ # Run a shell command while the file loads (background: true for -b).
822
+ run_shell "~/.tmux/plugins/tpm/tpm", background: true
823
+ end
824
+ ```
825
+
826
+ ### Key bindings
827
+
828
+ `bind` maps a key to a tmux command string or a block of commands. Use the
829
+ block form to pass command arguments without quoting them by hand.
830
+ `table:` picks a key table, `no_prefix: true` binds without the prefix (`-n`),
831
+ `repeat: true` allows repeating (`-r`), and `note:` adds a description (`-N`).
832
+ `unbind` removes a binding.
833
+
834
+ ```ruby
835
+ Tmux.configure do
836
+ # A command string.
837
+ # tmux: bind-key "|" split-window -h -c "#{pane_current_path}"
838
+ bind "|", %q{split-window -h -c "#{pane_current_path}"}
839
+
840
+ # Repeatable, so prefix + < < < resizes three times.
841
+ # tmux: bind-key -r "<" resize-pane -L 10
842
+ bind "<", "resize-pane -L 10", repeat: true
843
+
844
+ # Without the prefix, with a description for list-keys -N.
845
+ # tmux: bind-key -n -N "Zoom the pane" "M-z" resize-pane -Z
846
+ bind "M-z", "resize-pane -Z", no_prefix: true, note: "Zoom the pane"
847
+
848
+ # A block in the copy-mode-vi key table.
849
+ # tmux: bind-key -T "copy-mode-vi" "WheelUpPane" select-pane \; send-keys -t "{mouse}" -X clear-selection \; send-keys -t "{mouse}" -X -N 5 scroll-up
850
+ bind "WheelUpPane", table: "copy-mode-vi" do
851
+ # Select the pane under the mouse...
852
+ select_pane
853
+
854
+ # ...then send copy-mode commands to it.
855
+ copy_mode :clear_selection, target: "{mouse}"
856
+ copy_mode :scroll_up, target: "{mouse}", count: 5
857
+ end
858
+
859
+ # Remove the default prefix binding, a root-table binding, and a
860
+ # copy-mode one.
861
+ # tmux: unbind-key "C-b"
862
+ unbind "C-b"
863
+
864
+ # tmux: unbind-key -n "M-z"
865
+ unbind "M-z", no_prefix: true
866
+
867
+ # tmux: unbind-key -T "copy-mode-vi" "v"
868
+ unbind "v", table: "copy-mode-vi"
869
+ end
870
+ ```
871
+
872
+ A binding block offers `command` for any tmux command (its arguments are
873
+ quoted for you), `select_pane`, `copy_mode`, `display_message`,
874
+ `display_menu`, `load_config`, and `raw`:
875
+
876
+ ```ruby
877
+ Tmux.configure do
878
+ # tmux: bind-key "m" display-menu -T "Layout" "Tiled" "t" "select-layout tiled" "" "Even horizontal" "h" "select-layout \"even-horizontal\" \\; display-message \"Layout: even horizontal\""
879
+ bind "m" do
880
+ # A menu titled "Layout"; each item runs a command or a command block.
881
+ display_menu title: "Layout" do
882
+ item "Tiled", key: "t", command: "select-layout tiled"
883
+ separator
884
+ item "Even horizontal", key: "h" do
885
+ command :select_layout, "even-horizontal"
886
+ display_message "Layout: even horizontal"
887
+ end
888
+ end
889
+ end
890
+
891
+ # Any command, with quoted arguments; select_pane can target a pane.
892
+ # tmux: bind-key "N" new-window "-c" "#{pane_current_path}" \; select-pane -t "{last}"
893
+ bind "N" do
894
+ command :new_window, "-c", "\#{pane_current_path}"
895
+ select_pane target: "{last}"
896
+ end
897
+
898
+ # raw adds a command exactly as written.
899
+ # tmux: bind-key "X" confirm-before -p 'kill window? (y/n)' kill-window
900
+ bind "X" do
901
+ raw "confirm-before -p 'kill window? (y/n)' kill-window"
902
+ end
903
+ end
904
+ ```
905
+
906
+ `commands` builds a command sequence outside a binding, so helper methods can
907
+ return one and several bindings can share it:
908
+
909
+ ```ruby
910
+ Tmux.configure do
911
+ def split_here(direction)
912
+ commands do
913
+ command :split_window, direction, "-c", "\#{pane_current_path}"
914
+ display_message "split"
915
+ end
916
+ end
917
+
918
+ # tmux: bind-key "|" split-window "-h" -c "#{pane_current_path}" \; display-message "split"
919
+ bind "|", split_here("-h")
920
+
921
+ # tmux: bind-key "-" split-window "-v" -c "#{pane_current_path}" \; display-message "split"
922
+ bind "-", split_here("-v")
923
+ end
924
+ ```
925
+
926
+ ### Reloading from Ruby
927
+
928
+ `load_config` returns commands that recompile a Ruby configuration and load
929
+ it, for use with `bind`. The binding passes the running server's socket, so
930
+ it reloads the server whose key was pressed.
931
+
932
+ ```ruby
933
+ Tmux.configure do
934
+ # A helper method, reusable in this configuration.
935
+ def reload_config
936
+ load_config __FILE__, message: "tmux reloaded"
937
+ end
938
+
939
+ # prefix + r recompiles this file and loads it.
940
+ bind "r", reload_config
941
+
942
+ # executable: names the tmux.rb to run, e.g. one outside $PATH.
943
+ bind "R", load_config(__FILE__, executable: File.expand_path("~/bin/tmux.rb"))
944
+ end
945
+ ```
946
+
947
+ ### Conditional configuration
948
+
949
+ Use the same [conditional chain](#conditionals) as in format blocks to choose
950
+ which configuration commands tmux runs. Pass a [tmux expression](#expressions-and-conditions) directly, or
951
+ use `shell("command")` to check whether a shell command exits successfully.
952
+ Shell checks run when tmux applies the configuration. Later branches are
953
+ checked only if the earlier ones fail. Use `Tmux.shell(...)` to define a
954
+ reusable check outside a configuration block.
955
+
956
+ Use `shell("command", background: true)` to let tmux continue without waiting
957
+ for the check. For an existing native format string, use `Tmux.raw('...')`
958
+ as the condition; `if?(expression, background: true)` also supports tmux's
959
+ background flag. Shell checks can be used in configuration conditionals;
960
+ format blocks use tmux expressions.
961
+
962
+ `tmux_from` and `tmux_below` apply configuration on tmux at least, or older
963
+ than, a `"major.minor"` version, based on the running server's version.
964
+
965
+ ```ruby
966
+ Tmux.configure do
967
+ # Choose the first available clipboard tool.
968
+ if?(shell("command -v pbcopy")) do
969
+ global[:@yank_command] = "pbcopy" # set-option -g @yank_command "pbcopy"
970
+ end.elsif?(shell("command -v wl-copy")) do
971
+ global[:@yank_command] = "wl-copy" # set-option -g @yank_command "wl-copy"
972
+ end.else do
973
+ global[:@yank_command] = "xclip -selection clipboard" # set-option -g @yank_command "xclip -selection clipboard"
974
+ end
975
+
976
+ # Choose a style based on the shell environment.
977
+ if?(shell('test -n "$SSH_CONNECTION"')) do
978
+ global.status.style = "bg=colour52" # set-option -g status-style "bg=colour52"
979
+ end.else do
980
+ global.status.style = "bg=colour236" # set-option -g status-style "bg=colour236"
981
+ end
982
+
983
+ # A format condition instead of a shell command.
984
+ if?(Tmux.host_short == "laptop") do
985
+ global.status.style = "bg=colour22" # set-option -g status-style "bg=colour22"
986
+ end
987
+
988
+ # Don't wait for a slow check while tmux starts.
989
+ if?(shell("command -v slow-check", background: true)) do
990
+ global[:@slow_check] = "yes" # set-option -g @slow_check "yes"
991
+ end
992
+
993
+ # Only on tmux 3.2 or newer...
994
+ tmux_from "3.2" do
995
+ server.extended_keys = :on # set-option -s extended-keys "on"
996
+ end
997
+
998
+ # ...and a fallback for older versions, with .else for the rest.
999
+ tmux_below "3.0" do
1000
+ global.status.style = "bg=black" # set-option -g status-style "bg=black"
1001
+ end.else do
1002
+ global.status.style = "bg=terminal" # set-option -g status-style "bg=terminal"
1003
+ end
1004
+ end
1005
+ ```
1006
+
1007
+ ### Raw commands and composition
1008
+
1009
+ `raw` passes tmux commands through verbatim (one per line; a trailing `\`
1010
+ continues a line), and `blank` adds an empty line. `append` includes another
1011
+ configuration's statements, and `use` accepts any object that responds to
1012
+ `to_config`, such as a plugin's Ruby package.
1013
+
1014
+ ```ruby
1015
+ shared = Tmux.configure { global.history_limit = 50_000 } # set-option -g history-limit 50000
1016
+
1017
+ # Anything with a to_config method can be used.
1018
+ clipboard = Object.new
1019
+ def clipboard.to_config = Tmux.configure { server.set_clipboard = :external } # set-option -s set-clipboard "external"
1020
+
1021
+ Tmux.configure do
1022
+ # Verbatim tmux commands, including a continued line.
1023
+ raw "set-option -g display-time 2000"
1024
+ raw "bind-key T \\\n clock-mode"
1025
+ blank
1026
+
1027
+ # Another configuration's statements, in place.
1028
+ append shared
1029
+ use clipboard
1030
+ end
1031
+ ```
1032
+
1033
+ ## Examples
1034
+
1035
+ - [examples/author_config.rb](https://github.com/cosmicbuffalo/tmux.rb/blob/main/examples/author_config.rb)
1036
+ is the author's complete tmux configuration: structured status bars, pane
1037
+ borders, and window tabs, plus integration with
1038
+ [tmux-ai-attn](https://github.com/cosmicbuffalo/tmux-ai-attn).
1039
+ - [examples/popular_configs](https://github.com/cosmicbuffalo/tmux.rb/blob/main/examples/popular_configs/README.md)
1040
+ contains ten ports of well-known public tmux configurations. Each pinned
1041
+ upstream config sits beside its Ruby version for comparison.
1042
+ - [examples/window_status.rb](https://github.com/cosmicbuffalo/tmux.rb/blob/main/examples/window_status.rb)
1043
+ is a standalone format file.
1044
+
1045
+ ## Development
1046
+
1047
+ ```bash
1048
+ bundle install
1049
+ bundle exec rake test
1050
+ ```
1051
+
1052
+ `bundle exec rake test` uses the tmux on your `PATH`.
1053
+ `bundle exec rake test:tmux` builds each tmux version CI tests (3.2a, the
1054
+ newest release of each later minor version, and the development branch)
1055
+ from source into `tmp/tmux` and runs the suite against each; set
1056
+ `VERSIONS="3.2a 3.8"` to pick some. Building needs a C compiler, bison,
1057
+ pkg-config, and the libevent and ncurses headers.
1058
+
1059
+ The suite starts throwaway tmux servers on sockets in temporary directories
1060
+ and never touches your running server. Tests that need tmux are skipped when
1061
+ it is not installed, except on CI, where they fail. After an intended change
1062
+ to the author config's output, regenerate its fixture with
1063
+ `UPDATE_FIXTURES=1 bundle exec rake test` and review the diff.
1064
+
1065
+ The README screenshots are regenerated from real tmux sessions with
1066
+ `bundle exec rake screenshots`; see
1067
+ [docs/screenshots](docs/screenshots/README.md).
1068
+
1069
+ Build the gem with `bundle exec rake build` (into `pkg/`), and publish with
1070
+ `bundle exec rake release`.
1071
+
1072
+ ## License
1073
+
1074
+ MIT. See [LICENSE](https://github.com/cosmicbuffalo/tmux.rb/blob/main/LICENSE).
1075
+ The ported configurations in `examples/popular_configs` keep their upstream
1076
+ licenses beside them.