tmux 0.1.0 → 0.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +27 -1
- data/README.md +162 -30
- data/lib/tmux/command_builder.rb +100 -2
- data/lib/tmux/config_builder.rb +49 -7
- data/lib/tmux/config_compiler.rb +4 -1
- data/lib/tmux/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 94ce03b8025bac7e2d13c5732de26e231259eb402c23c727bb071eece1e6bc6c
|
|
4
|
+
data.tar.gz: 1c1d49bdc328d9432c06097b9a7d4024a30cb8098fc1d69d1c529a695560072f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ca1563f81d135988a54d60342f89dcaa3ca5d1ebc6a40a474ac7a2338ee2f7eb7826c948b2f4c8bf101e5f6cac754a3d79c64eb4b981fdd6b75cf54410efd984
|
|
7
|
+
data.tar.gz: dd67416dba50395f82d136a9327bb72093c4b4618f64e31838b8aaf146e3309dbc4432e09460c882c1c182108840ccaedd1fba2b447c09751d5d42916954ea6e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.
|
|
3
|
+
## 0.2.0 (2026-10-23)
|
|
4
|
+
|
|
5
|
+
### Library
|
|
6
|
+
|
|
7
|
+
- `bind(key)` with no command or block returns a chain of named commands:
|
|
8
|
+
`bind("-").split(:vertical).cwd(:current)`, `bind("c").new_window`.
|
|
9
|
+
- Named binding commands `split`, `new_window`, `cwd`, `resize_pane`, `zoom`,
|
|
10
|
+
`kill_pane`, `kill_window`, and `source`, and a direction for
|
|
11
|
+
`select_pane`. `split` accepts `:vertical`/`:horizontal` or the shorthand
|
|
12
|
+
`:v`/`:h`. They work in chains and in binding blocks.
|
|
13
|
+
- `comment` writes `#` lines into the generated file, and `hook` sets a
|
|
14
|
+
tmux hook (command string or block, optional `index:` slot, `:global`,
|
|
15
|
+
`:window`, or `:pane` scope).
|
|
16
|
+
|
|
17
|
+
### Fixes
|
|
18
|
+
|
|
19
|
+
- `shell(...)` conditions reach the shell exactly as written. tmux no longer
|
|
20
|
+
expands `$` variables when the file loads or rejects shell syntax such as
|
|
21
|
+
`${VAR:-default}`.
|
|
22
|
+
|
|
23
|
+
### Documentation
|
|
24
|
+
|
|
25
|
+
- The README opens with a hand-written tmux.conf beside its tmux.rb version,
|
|
26
|
+
and Examples move up ahead of Why. Tests check that both versions load
|
|
27
|
+
identically and that the key binding examples match their tmux comments.
|
|
28
|
+
|
|
29
|
+
## 0.1.0 (2026-10-21)
|
|
4
30
|
|
|
5
31
|
First public release, as the `tmux` gem (`require "tmux"`), with the
|
|
6
32
|
`tmux.rb` command (also installed as `tmux_rb`).
|
data/README.md
CHANGED
|
@@ -3,6 +3,55 @@
|
|
|
3
3
|
tmux.rb is a Ruby DSL that compiles readable, structured block-based
|
|
4
4
|
configuration into native tmux configuration and format strings.
|
|
5
5
|
|
|
6
|
+
## At a glance
|
|
7
|
+
|
|
8
|
+
A small, hand-written `~/.tmux.conf`:
|
|
9
|
+
|
|
10
|
+
<!-- glance: tmux -->
|
|
11
|
+
```tmux
|
|
12
|
+
set -g prefix C-a
|
|
13
|
+
unbind C-b
|
|
14
|
+
bind C-a send-prefix
|
|
15
|
+
|
|
16
|
+
set -g mouse on
|
|
17
|
+
set -g base-index 1
|
|
18
|
+
setw -g mode-keys vi
|
|
19
|
+
|
|
20
|
+
bind | split-window -h -c "#{pane_current_path}"
|
|
21
|
+
bind - split-window -v -c "#{pane_current_path}"
|
|
22
|
+
bind r source-file ~/.tmux.conf \; display-message "Reloaded"
|
|
23
|
+
|
|
24
|
+
set -g status-right "#{?window_zoomed_flag,#[fg=yellow]ZOOM #[fg=default],}#S"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The same configuration in tmux.rb, which compiles to a file tmux loads
|
|
28
|
+
identically:
|
|
29
|
+
|
|
30
|
+
<!-- glance: ruby -->
|
|
31
|
+
```ruby
|
|
32
|
+
Tmux.configure do
|
|
33
|
+
global.prefix = "C-a"
|
|
34
|
+
unbind "C-b"
|
|
35
|
+
bind("C-a").command(:send_prefix)
|
|
36
|
+
|
|
37
|
+
global.mouse = true
|
|
38
|
+
global.base_index = 1
|
|
39
|
+
window.mode_keys = :vi
|
|
40
|
+
|
|
41
|
+
bind("|").split(:horizontal, cwd: :current)
|
|
42
|
+
bind("-").split(:vertical, cwd: :current)
|
|
43
|
+
bind("r").source("~/.tmux.conf").display_message("Reloaded")
|
|
44
|
+
|
|
45
|
+
global.status.right = Tmux.format do
|
|
46
|
+
if?(Tmux.window_zoomed_flag) { style(fg: :yellow) { text "ZOOM " } }
|
|
47
|
+
text Tmux.session_name
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The payoff grows with the configuration. Status lines are where hand-written
|
|
53
|
+
tmux gets hardest to read, and where the format DSL helps most:
|
|
54
|
+
|
|
6
55
|
<!-- example: intro -->
|
|
7
56
|
```ruby
|
|
8
57
|
# ~/.config/tmux/tmux.rb
|
|
@@ -19,7 +68,7 @@ Tmux.configure do
|
|
|
19
68
|
left_segment "#{Tmux.session_name}@#{Tmux.host}", bg: :blue, fg: :black
|
|
20
69
|
end
|
|
21
70
|
|
|
22
|
-
bind
|
|
71
|
+
bind("|").split(:horizontal, cwd: :current)
|
|
23
72
|
end
|
|
24
73
|
```
|
|
25
74
|
|
|
@@ -43,6 +92,18 @@ the status bar set to the terminal's own colors. Named colors such as `:blue`,
|
|
|
43
92
|
everywhere. Powerline edges need a font with Powerline glyphs, such as a
|
|
44
93
|
[Nerd Font](https://www.nerdfonts.com).
|
|
45
94
|
|
|
95
|
+
## Examples
|
|
96
|
+
|
|
97
|
+
- [examples/author_config.rb](https://github.com/cosmicbuffalo/tmux.rb/blob/main/examples/author_config.rb)
|
|
98
|
+
is the author's complete tmux configuration: structured status bars, pane
|
|
99
|
+
borders, and window tabs, plus integration with
|
|
100
|
+
[tmux-ai-attn](https://github.com/cosmicbuffalo/tmux-ai-attn).
|
|
101
|
+
- [examples/popular_configs](https://github.com/cosmicbuffalo/tmux.rb/blob/main/examples/popular_configs/README.md)
|
|
102
|
+
contains ten ports of well-known public tmux configurations. Each pinned
|
|
103
|
+
upstream config sits beside its Ruby version for comparison.
|
|
104
|
+
- [examples/window_status.rb](https://github.com/cosmicbuffalo/tmux.rb/blob/main/examples/window_status.rb)
|
|
105
|
+
is a standalone format file.
|
|
106
|
+
|
|
46
107
|
## Why
|
|
47
108
|
|
|
48
109
|
Hand-written tmux configuration gets hard to change once it grows: status
|
|
@@ -825,11 +886,74 @@ end
|
|
|
825
886
|
|
|
826
887
|
### Key bindings
|
|
827
888
|
|
|
828
|
-
`bind` maps a key to
|
|
829
|
-
|
|
830
|
-
`
|
|
831
|
-
|
|
832
|
-
|
|
889
|
+
`bind` maps a key to commands. Name the commands in a chain, write a block of
|
|
890
|
+
commands, or pass a tmux command string. `table:` picks a key table,
|
|
891
|
+
`no_prefix: true` binds without the prefix (`-n`), `repeat: true` allows
|
|
892
|
+
repeating (`-r`), and `note:` adds a description (`-N`). `unbind` removes a
|
|
893
|
+
binding.
|
|
894
|
+
|
|
895
|
+
Given only a key, `bind` returns a chain. Each method adds a command, and
|
|
896
|
+
`cwd` sets the directory for the `split` or `new_window` before it:
|
|
897
|
+
|
|
898
|
+
```ruby
|
|
899
|
+
Tmux.configure do
|
|
900
|
+
# tmux: bind-key "-" split-window -v -c "#{pane_current_path}"
|
|
901
|
+
bind("-").split(:vertical).cwd(:current)
|
|
902
|
+
|
|
903
|
+
# tmux: bind-key "|" split-window -h -l "30%" -c "~/src"
|
|
904
|
+
bind("|").split(:horizontal, size: "30%", cwd: "~/src")
|
|
905
|
+
|
|
906
|
+
# tmux: bind-key "c" new-window
|
|
907
|
+
bind("c").new_window
|
|
908
|
+
|
|
909
|
+
# Commands run in order: a named window, then back to the last pane.
|
|
910
|
+
# tmux: bind-key "C" new-window -n "scratch" -c "#{pane_current_path}" \; select-pane -t "{last}"
|
|
911
|
+
bind("C").new_window(name: "scratch").cwd(:current).select_pane(target: "{last}")
|
|
912
|
+
|
|
913
|
+
# tmux: bind-key -r "h" select-pane -L
|
|
914
|
+
bind("h", repeat: true).select_pane(:left)
|
|
915
|
+
|
|
916
|
+
# tmux: bind-key -r "<" resize-pane -L 10
|
|
917
|
+
bind("<", repeat: true).resize_pane(:left, 10)
|
|
918
|
+
|
|
919
|
+
# tmux: bind-key -n "M-z" resize-pane -Z
|
|
920
|
+
bind("M-z", no_prefix: true).zoom
|
|
921
|
+
|
|
922
|
+
# tmux: bind-key "r" source-file "~/.tmux.conf" \; display-message "Reloaded"
|
|
923
|
+
bind("r").source("~/.tmux.conf").display_message("Reloaded")
|
|
924
|
+
end
|
|
925
|
+
```
|
|
926
|
+
|
|
927
|
+
`split` takes tmux's direction names: `:vertical` (`-v`) puts the new pane
|
|
928
|
+
below, and `:horizontal` (`-h`) puts it to the right. `:v` and `:h` are
|
|
929
|
+
shorthand for them. Pane directions are
|
|
930
|
+
`:left`, `:right`, `:up`, and `:down`. The named commands are `split`,
|
|
931
|
+
`new_window`, `select_pane`, `resize_pane`, `zoom`, `kill_pane`,
|
|
932
|
+
`kill_window`, `source`, `display_message`, `display_menu`, `copy_mode`, and
|
|
933
|
+
`load_config`. Use `command` for any other tmux command (its arguments are
|
|
934
|
+
quoted for you) and `raw` for a command written exactly as tmux expects it.
|
|
935
|
+
|
|
936
|
+
The block form uses the same methods, one command per line, so `cwd` on its
|
|
937
|
+
own line applies to the `split` above it:
|
|
938
|
+
|
|
939
|
+
```ruby
|
|
940
|
+
Tmux.configure do
|
|
941
|
+
# tmux: bind-key "|" split-window -h -c "some-dir"
|
|
942
|
+
bind "|" do
|
|
943
|
+
split :h # or :horizontal
|
|
944
|
+
cwd "some-dir"
|
|
945
|
+
end
|
|
946
|
+
|
|
947
|
+
# tmux: bind-key "C" new-window -n "scratch" -c "#{pane_current_path}" \; select-pane -t "{last}"
|
|
948
|
+
bind "C" do
|
|
949
|
+
new_window name: "scratch"
|
|
950
|
+
cwd :current
|
|
951
|
+
select_pane target: "{last}"
|
|
952
|
+
end
|
|
953
|
+
end
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
Command strings pass through as written:
|
|
833
957
|
|
|
834
958
|
```ruby
|
|
835
959
|
Tmux.configure do
|
|
@@ -869,9 +993,8 @@ Tmux.configure do
|
|
|
869
993
|
end
|
|
870
994
|
```
|
|
871
995
|
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
`display_menu`, `load_config`, and `raw`:
|
|
996
|
+
Blocks suit longer bindings, such as a menu or a command with no named
|
|
997
|
+
helper:
|
|
875
998
|
|
|
876
999
|
```ruby
|
|
877
1000
|
Tmux.configure do
|
|
@@ -888,10 +1011,10 @@ Tmux.configure do
|
|
|
888
1011
|
end
|
|
889
1012
|
end
|
|
890
1013
|
|
|
891
|
-
# Any command, with quoted arguments
|
|
892
|
-
# tmux: bind-key "
|
|
893
|
-
bind "
|
|
894
|
-
command :
|
|
1014
|
+
# Any command, with quoted arguments.
|
|
1015
|
+
# tmux: bind-key "S" swap-pane "-s" "{last}" \; select-pane -t "{last}"
|
|
1016
|
+
bind "S" do
|
|
1017
|
+
command :swap_pane, "-s", "{last}"
|
|
895
1018
|
select_pane target: "{last}"
|
|
896
1019
|
end
|
|
897
1020
|
|
|
@@ -910,16 +1033,16 @@ return one and several bindings can share it:
|
|
|
910
1033
|
Tmux.configure do
|
|
911
1034
|
def split_here(direction)
|
|
912
1035
|
commands do
|
|
913
|
-
|
|
1036
|
+
split direction, cwd: :current
|
|
914
1037
|
display_message "split"
|
|
915
1038
|
end
|
|
916
1039
|
end
|
|
917
1040
|
|
|
918
|
-
# tmux: bind-key "|" split-window
|
|
919
|
-
bind "|", split_here(
|
|
1041
|
+
# tmux: bind-key "|" split-window -h -c "#{pane_current_path}" \; display-message "split"
|
|
1042
|
+
bind "|", split_here(:horizontal)
|
|
920
1043
|
|
|
921
|
-
# tmux: bind-key "-" split-window
|
|
922
|
-
bind "-", split_here(
|
|
1044
|
+
# tmux: bind-key "-" split-window -v -c "#{pane_current_path}" \; display-message "split"
|
|
1045
|
+
bind "-", split_here(:vertical)
|
|
923
1046
|
end
|
|
924
1047
|
```
|
|
925
1048
|
|
|
@@ -1004,6 +1127,27 @@ Tmux.configure do
|
|
|
1004
1127
|
end
|
|
1005
1128
|
```
|
|
1006
1129
|
|
|
1130
|
+
### Comments and hooks
|
|
1131
|
+
|
|
1132
|
+
`comment` writes `#` lines into the generated file, for whoever reads or edits
|
|
1133
|
+
it. `hook` sets a tmux hook from a command string or a block of commands, as
|
|
1134
|
+
`bind` does; `index:` gives it its own slot so setting it again replaces that
|
|
1135
|
+
slot rather than adding a copy, and `scope:` picks `:global` (session hooks),
|
|
1136
|
+
`:window`, or `:pane`.
|
|
1137
|
+
|
|
1138
|
+
```ruby
|
|
1139
|
+
Tmux.configure do
|
|
1140
|
+
# # Snapshot sessions whenever one closes.
|
|
1141
|
+
comment "Snapshot sessions whenever one closes."
|
|
1142
|
+
|
|
1143
|
+
# tmux: set-hook -g "session-closed[100]" "run-shell -b \"save-sessions\""
|
|
1144
|
+
hook "session-closed", %q{run-shell -b "save-sessions"}, index: 100
|
|
1145
|
+
|
|
1146
|
+
# tmux: set-hook -gw "pane-focus-in" "display-message \"focused\""
|
|
1147
|
+
hook("pane-focus-in", scope: :window) { display_message "focused" }
|
|
1148
|
+
end
|
|
1149
|
+
```
|
|
1150
|
+
|
|
1007
1151
|
### Raw commands and composition
|
|
1008
1152
|
|
|
1009
1153
|
`raw` passes tmux commands through verbatim (one per line; a trailing `\`
|
|
@@ -1030,18 +1174,6 @@ Tmux.configure do
|
|
|
1030
1174
|
end
|
|
1031
1175
|
```
|
|
1032
1176
|
|
|
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
1177
|
## Development
|
|
1046
1178
|
|
|
1047
1179
|
```bash
|
data/lib/tmux/command_builder.rb
CHANGED
|
@@ -32,6 +32,20 @@ module Tmux
|
|
|
32
32
|
# separate entries until the binding is compiled, so callers never write tmux
|
|
33
33
|
# command separators or quote command arguments by hand.
|
|
34
34
|
class CommandBuilder
|
|
35
|
+
# A command built by one of the named helpers below, kept in parts so a
|
|
36
|
+
# modifier such as #cwd can still add a flag before it compiles.
|
|
37
|
+
Typed = Struct.new(:name, :flags) do
|
|
38
|
+
def to_s = [name, *flags].join(" ")
|
|
39
|
+
end
|
|
40
|
+
private_constant :Typed
|
|
41
|
+
|
|
42
|
+
# tmux's own names: a vertical split (-v) stacks the new pane below, a
|
|
43
|
+
# horizontal one (-h) puts it to the right. :v and :h are shorthand.
|
|
44
|
+
SPLIT_DIRECTIONS = { vertical: "-v", v: "-v", horizontal: "-h", h: "-h" }.freeze
|
|
45
|
+
PANE_DIRECTIONS = { left: "-L", right: "-R", up: "-U", down: "-D" }.freeze
|
|
46
|
+
CWD_COMMANDS = %w[split-window new-window].freeze
|
|
47
|
+
private_constant :SPLIT_DIRECTIONS, :PANE_DIRECTIONS, :CWD_COMMANDS
|
|
48
|
+
|
|
35
49
|
def initialize = @commands = []
|
|
36
50
|
|
|
37
51
|
# Any tmux command. Arguments are quoted for you; tmux expands $VARIABLES
|
|
@@ -48,13 +62,59 @@ module Tmux
|
|
|
48
62
|
self
|
|
49
63
|
end
|
|
50
64
|
|
|
51
|
-
|
|
65
|
+
# split-window. `cwd:` starts the new pane in a directory; :current is the
|
|
66
|
+
# active pane's directory. `size:` is lines/columns or a "30%" string.
|
|
67
|
+
def split(direction, cwd: nil, size: nil)
|
|
68
|
+
flag = SPLIT_DIRECTIONS.fetch(direction) do
|
|
69
|
+
raise Error, "split direction must be :vertical (:v) or :horizontal (:h), not #{direction.inspect}"
|
|
70
|
+
end
|
|
71
|
+
flags = [flag]
|
|
72
|
+
flags.concat(["-l", ConfigCompiler.quote(size)]) if size
|
|
73
|
+
typed("split-window", flags, cwd:)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def new_window(name: nil, cwd: nil)
|
|
77
|
+
flags = []
|
|
78
|
+
flags.concat(["-n", ConfigCompiler.quote(name)]) if name
|
|
79
|
+
typed("new-window", flags, cwd:)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Start the previous command (split or new_window) in `directory`;
|
|
83
|
+
# :current is the active pane's directory.
|
|
84
|
+
def cwd(directory)
|
|
85
|
+
last = @commands.last
|
|
86
|
+
unless last.is_a?(Typed) && CWD_COMMANDS.include?(last.name)
|
|
87
|
+
raise Error, "cwd must follow split or new_window"
|
|
88
|
+
end
|
|
89
|
+
raise Error, "#{last.name} already has a directory" if last.flags.include?("-c")
|
|
90
|
+
|
|
91
|
+
last.flags.concat(["-c", ConfigCompiler.quote(directory == :current ? "\#{pane_current_path}" : directory)])
|
|
92
|
+
self
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# select-pane, by direction (:left, :right, :up, :down) and/or target.
|
|
96
|
+
def select_pane(direction = nil, target: nil)
|
|
52
97
|
parts = ["select-pane"]
|
|
98
|
+
parts << pane_direction(direction) if direction
|
|
53
99
|
parts.concat(["-t", ConfigCompiler.quote(target)]) if target
|
|
54
100
|
@commands << parts.join(" ")
|
|
55
101
|
self
|
|
56
102
|
end
|
|
57
103
|
|
|
104
|
+
# resize-pane by `amount` cells (tmux's default is 1) in a direction.
|
|
105
|
+
def resize_pane(direction, amount = nil)
|
|
106
|
+
@commands << ["resize-pane", pane_direction(direction), amount&.to_i].compact.join(" ")
|
|
107
|
+
self
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Toggle the active pane's zoom (resize-pane -Z).
|
|
111
|
+
def zoom = raw("resize-pane -Z")
|
|
112
|
+
def kill_pane = raw("kill-pane")
|
|
113
|
+
def kill_window = raw("kill-window")
|
|
114
|
+
|
|
115
|
+
# source-file; `quiet: true` (-q) skips a missing file without an error.
|
|
116
|
+
def source(path, quiet: false) = raw("source-file#{' -q' if quiet} #{ConfigCompiler.quote(path)}")
|
|
117
|
+
|
|
58
118
|
# Send one copy-mode command (`send-keys -X`) to a pane. `count:` maps to
|
|
59
119
|
# tmux's -N repeat count, used by actions such as scroll-up and scroll-down.
|
|
60
120
|
def copy_mode(action, target: nil, count: nil)
|
|
@@ -91,14 +151,52 @@ module Tmux
|
|
|
91
151
|
self
|
|
92
152
|
end
|
|
93
153
|
|
|
154
|
+
def empty? = @commands.empty?
|
|
155
|
+
|
|
94
156
|
def build
|
|
95
157
|
raise Error, "a command block must contain at least one command" if @commands.empty?
|
|
96
158
|
|
|
97
|
-
@commands.join(" \\; ")
|
|
159
|
+
@commands.map(&:to_s).join(" \\; ")
|
|
98
160
|
end
|
|
99
161
|
|
|
100
162
|
private
|
|
101
163
|
|
|
164
|
+
def typed(name, flags, cwd: nil)
|
|
165
|
+
@commands << Typed.new(name, flags)
|
|
166
|
+
cwd ? self.cwd(cwd) : self
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def pane_direction(direction)
|
|
170
|
+
PANE_DIRECTIONS.fetch(direction) do
|
|
171
|
+
raise Error, "pane direction must be :left, :right, :up, or :down, not #{direction.inspect}"
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
102
175
|
def normalize(name) = name.to_s.tr("_", "-")
|
|
103
176
|
end
|
|
177
|
+
|
|
178
|
+
# What `bind(key)` returns without a command or block. Each CommandBuilder
|
|
179
|
+
# method adds a command and rewrites the binding's configuration line, so the
|
|
180
|
+
# chain can stop anywhere: bind("c").new_window.cwd(:current)
|
|
181
|
+
class BindingChain
|
|
182
|
+
attr_reader :key
|
|
183
|
+
|
|
184
|
+
def initialize(key, prefix, nodes, index)
|
|
185
|
+
@key = key
|
|
186
|
+
@prefix = prefix
|
|
187
|
+
@nodes = nodes
|
|
188
|
+
@index = index
|
|
189
|
+
@commands = CommandBuilder.new
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def empty? = @commands.empty?
|
|
193
|
+
|
|
194
|
+
(CommandBuilder.public_instance_methods(false) - %i[build empty?]).each do |name|
|
|
195
|
+
define_method(name) do |*arguments, **options, &block|
|
|
196
|
+
@commands.public_send(name, *arguments, **options, &block)
|
|
197
|
+
@nodes[@index] = ConfigNode::Command.new("#{@prefix} #{@commands.build}")
|
|
198
|
+
self
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
104
202
|
end
|
data/lib/tmux/config_builder.rb
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module Tmux
|
|
4
4
|
# The context of a Tmux.configure block. Every DSL method returns the
|
|
5
|
-
# builder (conditionals return a handle for `.else`)
|
|
5
|
+
# builder (conditionals return a handle for `.else`, and `bind(key)` a
|
|
6
|
+
# BindingChain), so calls can be chained.
|
|
6
7
|
class ConfigBuilder
|
|
7
8
|
include Evaluation
|
|
8
9
|
|
|
@@ -13,6 +14,7 @@ module Tmux
|
|
|
13
14
|
@nodes = []
|
|
14
15
|
@else_filled = {}
|
|
15
16
|
@conditional_states = {}
|
|
17
|
+
@open_bindings = []
|
|
16
18
|
end
|
|
17
19
|
|
|
18
20
|
def switch(cases) = Tmux.switch(cases)
|
|
@@ -38,6 +40,32 @@ module Tmux
|
|
|
38
40
|
self
|
|
39
41
|
end
|
|
40
42
|
|
|
43
|
+
# A comment in the generated file: one "# " line per line of `text`, and a
|
|
44
|
+
# bare "#" for an empty one. tmux ignores comments; they are for whoever
|
|
45
|
+
# reads or edits the generated config.
|
|
46
|
+
def comment(text)
|
|
47
|
+
text.to_s.each_line(chomp: true) do |line|
|
|
48
|
+
@nodes << ConfigNode::Command.new(line.empty? ? "#" : "# #{line}")
|
|
49
|
+
end
|
|
50
|
+
self
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# A hook, as set-hook: `name` fires `command`, a tmux command string or a
|
|
54
|
+
# block of commands as for #bind. `index:` gives the hook its own slot
|
|
55
|
+
# (name[index]), so setting it again replaces that slot instead of adding
|
|
56
|
+
# another copy. `scope:` is :global (session hooks), :window, or :pane.
|
|
57
|
+
def hook(name, command = nil, index: nil, scope: :global, &block)
|
|
58
|
+
raise ArgumentError, "hook #{name}: give a command or a block, not both" if command && block
|
|
59
|
+
raise ArgumentError, "hook #{name}: give a command or a block" unless command || block
|
|
60
|
+
|
|
61
|
+
command = commands(&block) if block
|
|
62
|
+
flag = {global: "-g", window: "-gw", pane: "-gp"}.fetch(scope) do
|
|
63
|
+
raise ArgumentError, "hook #{name}: scope must be :global, :window, or :pane"
|
|
64
|
+
end
|
|
65
|
+
slot = index.nil? ? name.to_s : "#{name}[#{Integer(index)}]"
|
|
66
|
+
raw "set-hook #{flag} #{ConfigCompiler.quote(slot)} #{ConfigCompiler.quote(command.to_s, expand: false)}"
|
|
67
|
+
end
|
|
68
|
+
|
|
41
69
|
# Scoped options are generated from the selected target's complete catalog.
|
|
42
70
|
def global = option_root(:global)
|
|
43
71
|
def server = option_root(:server)
|
|
@@ -76,21 +104,29 @@ module Tmux
|
|
|
76
104
|
].compact.join(" ")
|
|
77
105
|
end
|
|
78
106
|
|
|
107
|
+
# With neither a command nor a block, bind returns a chain that adds each
|
|
108
|
+
# command to the binding: `bind("-").split(:vertical).cwd(:current)`.
|
|
79
109
|
def bind(key, command = nil, table: nil, repeat: false, no_prefix: false, note: nil, &block)
|
|
80
110
|
raise Error, "bind cannot combine table: and no_prefix:" if table && no_prefix
|
|
81
111
|
raise Error, "bind accepts either a command or a block, not both" if command && block
|
|
82
112
|
|
|
83
|
-
command = commands(&block) if block
|
|
84
|
-
raise Error, "bind requires a command or block" unless command
|
|
85
|
-
|
|
86
113
|
parts = ["bind-key"]
|
|
87
114
|
parts << "-r" if repeat
|
|
88
115
|
parts << "-n" if no_prefix
|
|
89
116
|
parts.concat(["-N", ConfigCompiler.quote(note)]) if note
|
|
90
117
|
parts.concat(["-T", ConfigCompiler.quote(table)]) if table
|
|
91
118
|
parts << ConfigCompiler.quote(key)
|
|
92
|
-
parts
|
|
93
|
-
|
|
119
|
+
prefix = parts.join(" ")
|
|
120
|
+
|
|
121
|
+
unless command || block
|
|
122
|
+
chain = BindingChain.new(key, prefix, @nodes, @nodes.size)
|
|
123
|
+
@nodes << ConfigNode::Blank.new # replaced by the chain's first command
|
|
124
|
+
@open_bindings << chain
|
|
125
|
+
return chain
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
command = commands(&block) if block
|
|
129
|
+
raw "#{prefix} #{command}"
|
|
94
130
|
end
|
|
95
131
|
|
|
96
132
|
# source-file; `quiet: true` (-q) skips a missing file without an error.
|
|
@@ -152,7 +188,13 @@ module Tmux
|
|
|
152
188
|
# package. Splices its nodes in call order.
|
|
153
189
|
def use(component) = append(component.to_config)
|
|
154
190
|
|
|
155
|
-
def build
|
|
191
|
+
def build
|
|
192
|
+
if (open = @open_bindings.find(&:empty?))
|
|
193
|
+
raise Error, "bind #{open.key.inspect} has no command: chain one, such as .split(:vertical), or pass a command or block"
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
Config.new(@nodes, target: target).validate!
|
|
197
|
+
end
|
|
156
198
|
|
|
157
199
|
private
|
|
158
200
|
|
data/lib/tmux/config_compiler.rb
CHANGED
|
@@ -58,7 +58,10 @@ module Tmux
|
|
|
58
58
|
parts = ["if-shell"]
|
|
59
59
|
parts << "-b" if node.background
|
|
60
60
|
parts << "-F" if node.format
|
|
61
|
-
|
|
61
|
+
# A shell test is for the shell: escape $ and a leading ~ so tmux's
|
|
62
|
+
# parser leaves them alone (it rejects shell syntax such as
|
|
63
|
+
# ${VAR:-default} in a double-quoted string).
|
|
64
|
+
parts << self.class.quote(node.test, expand: node.format)
|
|
62
65
|
text = "#{parts.join(' ')} #{block(node.success)}"
|
|
63
66
|
text += " #{block(node.failure)}" unless node.failure.empty?
|
|
64
67
|
text
|
data/lib/tmux/version.rb
CHANGED