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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +60 -0
- data/LICENSE +22 -0
- data/README.md +1076 -0
- data/bin/tmux.rb +6 -0
- data/bin/tmux_rb +4 -0
- data/lib/tmux/cli.rb +224 -0
- data/lib/tmux/command_builder.rb +104 -0
- data/lib/tmux/condition_compiler.rb +127 -0
- data/lib/tmux/config.rb +40 -0
- data/lib/tmux/config_builder.rb +247 -0
- data/lib/tmux/config_compiler.rb +82 -0
- data/lib/tmux/dsl.rb +118 -0
- data/lib/tmux/errors.rb +5 -0
- data/lib/tmux/escape.rb +68 -0
- data/lib/tmux/evaluation.rb +18 -0
- data/lib/tmux/expression.rb +109 -0
- data/lib/tmux/format.rb +94 -0
- data/lib/tmux/format_builder.rb +586 -0
- data/lib/tmux/format_compiler.rb +41 -0
- data/lib/tmux/glyph.rb +45 -0
- data/lib/tmux/option_tree.rb +148 -0
- data/lib/tmux/target.rb +200 -0
- data/lib/tmux/version.rb +5 -0
- data/lib/tmux/zone_builder.rb +31 -0
- data/lib/tmux.rb +47 -0
- metadata +92 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tmux
|
|
4
|
+
# Formats are compiled for the places tmux expands them with the time
|
|
5
|
+
# (status line, window tabs, pane borders, titles, display-message): tmux
|
|
6
|
+
# runs strftime over the top level and again over each conditional branch it
|
|
7
|
+
# chooses, so a literal "%" in text needs doubling once per level.
|
|
8
|
+
class FormatCompiler
|
|
9
|
+
def compile(nodes, depth: 0)
|
|
10
|
+
nodes.map { |node| compile_node(node, depth) }.join
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def compile_node(node, depth)
|
|
16
|
+
branch = depth.positive?
|
|
17
|
+
case node
|
|
18
|
+
when Node::Text
|
|
19
|
+
Escape.literal(node.value, branch:).gsub("%", "%" * (2**(depth + 1)))
|
|
20
|
+
when Node::Strftime
|
|
21
|
+
Escape.literal(node.value, branch:)
|
|
22
|
+
when Node::Dynamic
|
|
23
|
+
branch ? Escape.structural(node.expression.source) : node.expression.source
|
|
24
|
+
when Node::Style
|
|
25
|
+
"#[#{node.attributes.join(branch ? '#,' : ',')}]"
|
|
26
|
+
when Node::ExpandOption
|
|
27
|
+
"#{'#{'}E:#{node.name}}"
|
|
28
|
+
when Node::Command
|
|
29
|
+
"#(#{branch ? Escape.structural(node.command) : node.command})"
|
|
30
|
+
when Node::Conditional
|
|
31
|
+
condition = node.condition.condition_source
|
|
32
|
+
on_true = compile(node.on_true, depth: depth + 1)
|
|
33
|
+
on_false = compile(node.on_false, depth: depth + 1)
|
|
34
|
+
"#{'#{'}?#{condition},#{on_true},#{on_false}}"
|
|
35
|
+
else
|
|
36
|
+
raise Error, "unknown format node: #{node.inspect}"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
private_constant :FormatCompiler
|
|
41
|
+
end
|
data/lib/tmux/glyph.rb
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tmux
|
|
4
|
+
# Named edge/separator glyphs (Powerline private-use codepoints), so configs
|
|
5
|
+
# write `:left_triangle` instead of an opaque "\u{e0b2}". Referenced by name in
|
|
6
|
+
# the flag helpers, or reachable directly as Tmux::Glyph::LEFT_TRIANGLE.
|
|
7
|
+
module Glyph
|
|
8
|
+
CHARS = {
|
|
9
|
+
right_triangle: "\u{e0b0}", right_thin: "\u{e0b1}",
|
|
10
|
+
left_triangle: "\u{e0b2}", left_thin: "\u{e0b3}",
|
|
11
|
+
right_round: "\u{e0b4}", right_round_thin: "\u{e0b5}",
|
|
12
|
+
left_round: "\u{e0b6}", left_round_thin: "\u{e0b7}",
|
|
13
|
+
lower_left_triangle: "\u{e0b8}", upper_left_triangle: "\u{e0bc}",
|
|
14
|
+
lower_right_triangle: "\u{e0ba}", upper_right_triangle: "\u{e0be}"
|
|
15
|
+
}.freeze
|
|
16
|
+
CHARS.each { |name, char| const_set(name.to_s.upcase, char) }
|
|
17
|
+
|
|
18
|
+
# Shapes describe an edge independently of its position. A slant keeps
|
|
19
|
+
# its slope on both sides, using complementary filled triangles.
|
|
20
|
+
SHAPES = {
|
|
21
|
+
triangle: {left: :left_triangle, right: :right_triangle},
|
|
22
|
+
thin: {left: :left_thin, right: :right_thin},
|
|
23
|
+
round: {left: :left_round, right: :right_round},
|
|
24
|
+
round_thin: {left: :left_round_thin, right: :right_round_thin},
|
|
25
|
+
rslant: {left: :lower_right_triangle, right: :upper_left_triangle},
|
|
26
|
+
lslant: {left: :upper_right_triangle, right: :lower_left_triangle}
|
|
27
|
+
}.transform_values(&:freeze).freeze
|
|
28
|
+
private_constant :SHAPES
|
|
29
|
+
|
|
30
|
+
THIN_GLYPHS = CHARS.values_at(:left_thin, :right_thin, :left_round_thin, :right_round_thin).freeze
|
|
31
|
+
private_constant :THIN_GLYPHS
|
|
32
|
+
|
|
33
|
+
def self.[](name) = CHARS.fetch(name) { raise Error, "unknown glyph: #{name}" }
|
|
34
|
+
def self.thin?(glyph) = THIN_GLYPHS.include?(glyph)
|
|
35
|
+
|
|
36
|
+
def self.resolve(value, side: nil)
|
|
37
|
+
return value.to_s unless value.is_a?(Symbol)
|
|
38
|
+
|
|
39
|
+
if (shape = SHAPES[value])
|
|
40
|
+
value = shape.fetch(side) { raise Error, "glyph #{value.inspect} needs a side (:left or :right)" }
|
|
41
|
+
end
|
|
42
|
+
self[value]
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tmux
|
|
4
|
+
def self.style_string(attributes)
|
|
5
|
+
attributes.flat_map do |name, value|
|
|
6
|
+
case name
|
|
7
|
+
when :attrs then Array(value).map(&:to_s)
|
|
8
|
+
when :fg, :bg
|
|
9
|
+
raise Error, "style #{name}: needs a color, got nil" if value.nil?
|
|
10
|
+
|
|
11
|
+
"#{name}=#{value}"
|
|
12
|
+
else raise Error, "unknown style attribute: #{name}"
|
|
13
|
+
end
|
|
14
|
+
end.join(",")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class OptionNode
|
|
18
|
+
def self.root(builder, scope)
|
|
19
|
+
root = {children: {}, prefix: nil}
|
|
20
|
+
builder.target.catalog.scopes.fetch(scope).each do |name, metadata|
|
|
21
|
+
parts = name.split("-")
|
|
22
|
+
paths = [parts]
|
|
23
|
+
paths << parts.drop(1) if scope == :window && parts.first == "window"
|
|
24
|
+
paths.each do |path|
|
|
25
|
+
node = root
|
|
26
|
+
offset = parts.length - path.length
|
|
27
|
+
path.each_with_index do |key, index|
|
|
28
|
+
node = (node[:children][key.to_sym] ||= {
|
|
29
|
+
children: {}, prefix: parts.take(index + offset + 1).join("-")
|
|
30
|
+
})
|
|
31
|
+
end
|
|
32
|
+
node.merge!(option: name, array: metadata[:array], style: style?(name))
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
# Ergonomic aliases, not a list of valid options. Their availability is
|
|
36
|
+
# still checked against the selected target, including in version gates.
|
|
37
|
+
if scope == :window
|
|
38
|
+
clock = root[:children][:clock] ||= {children: {}, prefix: "clock"}
|
|
39
|
+
clock[:children][:colour] = {children: {}, prefix: "clock-mode-colour", option: "clock-mode-colour"}
|
|
40
|
+
clock[:children][:hour_format] = {children: {}, prefix: "clock-mode-style", option: "clock-mode-style"}
|
|
41
|
+
end
|
|
42
|
+
new(builder, root, scope: scope)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.style?(name) = name.end_with?("-style") && name != "clock-mode-style"
|
|
46
|
+
|
|
47
|
+
def initialize(builder, descriptor, scope:)
|
|
48
|
+
@builder = builder
|
|
49
|
+
@descriptor = descriptor
|
|
50
|
+
@scope = scope
|
|
51
|
+
# Kernel#display otherwise swallows namespaces such as display-panes.
|
|
52
|
+
# Exact-name brackets remain available for collisions with this proxy's
|
|
53
|
+
# own API (for example a future option literally named "set").
|
|
54
|
+
inherited = Object.public_instance_methods - OptionNode.public_instance_methods(false)
|
|
55
|
+
(descriptor[:children].keys & inherited).each do |key|
|
|
56
|
+
define_singleton_method(key) do |*arguments|
|
|
57
|
+
raise Error, "#{key} takes no arguments" unless arguments.empty?
|
|
58
|
+
|
|
59
|
+
node_for(key.to_s)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def <<(value)
|
|
65
|
+
write(value, append: true)
|
|
66
|
+
self
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def unset
|
|
70
|
+
@builder.__send__(:remove_option, settable!("unset"), scope: @scope)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def set(*arguments, append: false, expand: true)
|
|
74
|
+
return node_for("set") if arguments.empty? && known_child("set")
|
|
75
|
+
raise Error, "set expects one value" unless arguments.length == 1
|
|
76
|
+
|
|
77
|
+
write(arguments.first, append: append, expand: expand)
|
|
78
|
+
self
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Exact tmux names also cover custom @options and names that collide with
|
|
82
|
+
# Ruby methods. On a leaf, integer indexing selects a tmux array element.
|
|
83
|
+
def [](key)
|
|
84
|
+
name = if @descriptor[:prefix].nil?
|
|
85
|
+
key.to_s.start_with?("@") ? key.to_s : key.to_s.tr("_", "-")
|
|
86
|
+
else
|
|
87
|
+
unless key.is_a?(Integer) && key >= 0
|
|
88
|
+
raise Error, "option array index must be a nonnegative Integer"
|
|
89
|
+
end
|
|
90
|
+
"#{settable!('[]')}[#{key}]"
|
|
91
|
+
end
|
|
92
|
+
OptionNode.new(@builder, {children: {}, prefix: name, option: name, style: OptionNode.style?(name)}, scope: @scope)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def []=(key, value)
|
|
96
|
+
self[key].__send__(:write, value)
|
|
97
|
+
value
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def respond_to_missing?(name, include_private = false)
|
|
101
|
+
!!known_child(name.to_s.delete_suffix("=")) || super
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def method_missing(name, *arguments)
|
|
105
|
+
key = name.to_s
|
|
106
|
+
if key.end_with?("=")
|
|
107
|
+
raise Error, "#{key} expects one value" unless arguments.length == 1
|
|
108
|
+
|
|
109
|
+
node_for(key.delete_suffix("=")).__send__(:write, arguments.first)
|
|
110
|
+
return arguments.first
|
|
111
|
+
end
|
|
112
|
+
raise Error, "#{key} takes no arguments" unless arguments.empty?
|
|
113
|
+
|
|
114
|
+
node_for(key)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
def known_child(key)
|
|
120
|
+
return @descriptor[:children][key.to_sym] if @descriptor[:children].key?(key.to_sym)
|
|
121
|
+
|
|
122
|
+
key.split("_").reduce(@descriptor) { |node, part| node && node[:children][part.to_sym] }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def node_for(key)
|
|
126
|
+
descriptor = known_child(key)
|
|
127
|
+
unless descriptor
|
|
128
|
+
prefix = [@descriptor[:prefix], key.tr("_", "-")].compact.join("-")
|
|
129
|
+
descriptor = {children: {}, prefix: prefix, style: OptionNode.style?(prefix)}
|
|
130
|
+
end
|
|
131
|
+
OptionNode.new(@builder, descriptor, scope: @scope)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def settable!(operation)
|
|
135
|
+
return @descriptor[:option] if @descriptor[:option]
|
|
136
|
+
return @descriptor[:prefix] if @descriptor[:children].empty? && @descriptor[:prefix]
|
|
137
|
+
|
|
138
|
+
raise Error, "#{operation} needs a settable option, not a namespace"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def write(value, append: false, expand: true)
|
|
142
|
+
name = settable!("assignment")
|
|
143
|
+
value = Tmux.style_string(value) if @descriptor[:style] && value.is_a?(Hash)
|
|
144
|
+
@builder.__send__(:write_option, name, value, scope: @scope, append: append, expand: expand)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
private_constant :OptionNode
|
|
148
|
+
end
|
data/lib/tmux/target.rb
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
require "did_you_mean"
|
|
6
|
+
|
|
7
|
+
module Tmux
|
|
8
|
+
# A target is an executable, not a guessed list keyed by a version string.
|
|
9
|
+
# Each discovered catalog belongs to the exact binary (including its stat),
|
|
10
|
+
# so changing PATH or replacing a binary cannot reuse another build's data.
|
|
11
|
+
class Target
|
|
12
|
+
CONTEXT_KEY = :tmux_rb_target
|
|
13
|
+
|
|
14
|
+
def self.current = Thread.current[CONTEXT_KEY] || for_executable("tmux")
|
|
15
|
+
|
|
16
|
+
def self.with(target)
|
|
17
|
+
previous = Thread.current[CONTEXT_KEY]
|
|
18
|
+
Thread.current[CONTEXT_KEY] = target
|
|
19
|
+
yield
|
|
20
|
+
ensure
|
|
21
|
+
Thread.current[CONTEXT_KEY] = previous
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.for_executable(executable)
|
|
25
|
+
return executable if executable.is_a?(self)
|
|
26
|
+
|
|
27
|
+
name = executable.to_s
|
|
28
|
+
path = if name.include?(File::SEPARATOR)
|
|
29
|
+
File.expand_path(name)
|
|
30
|
+
else
|
|
31
|
+
ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).map { |dir| File.expand_path(name, dir) }
|
|
32
|
+
.find { |candidate| File.file?(candidate) && File.executable?(candidate) }
|
|
33
|
+
end
|
|
34
|
+
unless path && File.file?(path) && File.executable?(path)
|
|
35
|
+
raise Error, "tmux executable not found: #{name.inspect}; install tmux or select it with --tmux PATH"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
real = File.realpath(path)
|
|
39
|
+
stat = File.stat(real)
|
|
40
|
+
key = [real, stat.ino, stat.size, stat.mtime, stat.ctime]
|
|
41
|
+
@targets ||= {}
|
|
42
|
+
@targets[key] ||= new(real)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
attr_reader :executable
|
|
46
|
+
|
|
47
|
+
def initialize(executable)
|
|
48
|
+
@executable = executable
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def version
|
|
52
|
+
@version ||= begin
|
|
53
|
+
output = run!("-V").strip
|
|
54
|
+
unless output.start_with?("tmux ") && output.length > 5
|
|
55
|
+
raise Error, "not a tmux executable: #{executable.inspect} (-V returned #{output.inspect})"
|
|
56
|
+
end
|
|
57
|
+
output.delete_prefix("tmux ")
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def at_least?(threshold)
|
|
62
|
+
actual = version.match(/(\d+)\.(\d+)/)
|
|
63
|
+
return true unless actual # Match the DSL's treatment of unnumbered master builds.
|
|
64
|
+
|
|
65
|
+
expected = threshold.split(".").map(&:to_i)
|
|
66
|
+
(actual.captures.map(&:to_i) <=> expected) >= 0
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def catalog
|
|
70
|
+
version
|
|
71
|
+
@catalog ||= isolated do |socket|
|
|
72
|
+
scopes = {global: "-g", server: "-s", window: "-gw"}.to_h do |scope, flags|
|
|
73
|
+
stdout, _stderr, status = run("-S", socket, "show-options", "#{flags}H")
|
|
74
|
+
stdout = run!("-S", socket, "show-options", flags) unless status.success?
|
|
75
|
+
[scope, OptionCatalog.parse(stdout)]
|
|
76
|
+
end
|
|
77
|
+
OptionCatalog.new(self, scopes)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Resolve aliases using tmux itself. This also handles aliases introduced by
|
|
82
|
+
# newer builds, without maintaining a second hardcoded compatibility list.
|
|
83
|
+
def canonical_name(name)
|
|
84
|
+
isolated do |socket|
|
|
85
|
+
stdout, _stderr, status = run("-S", socket, "show-options", "-g", name)
|
|
86
|
+
next unless status.success?
|
|
87
|
+
|
|
88
|
+
stdout.lines.first&.split&.first&.sub(/\[\d+\]\z/, "")
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def run(*arguments) = Open3.capture3(executable, *arguments)
|
|
93
|
+
|
|
94
|
+
def run!(*arguments)
|
|
95
|
+
stdout, stderr, status = run(*arguments)
|
|
96
|
+
raise Error, "tmux #{version_label} #{arguments.first} failed: #{stderr.strip}" unless status.success?
|
|
97
|
+
|
|
98
|
+
stdout
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def isolated
|
|
102
|
+
Dir.mktmpdir("tmux-rb-options") do |directory|
|
|
103
|
+
socket = File.join(directory, "tmux.sock")
|
|
104
|
+
begin
|
|
105
|
+
run!("-S", socket, "-f", "/dev/null", "new-session", "-d", "-s", "tmux_rb_options", "sleep 3600")
|
|
106
|
+
yield socket
|
|
107
|
+
ensure
|
|
108
|
+
run("-S", socket, "kill-server")
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
def version_label = @version || executable
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
class OptionCatalog
|
|
119
|
+
def self.parse(output)
|
|
120
|
+
output.each_line.each_with_object({}) do |line, entries|
|
|
121
|
+
token, value = line.strip.split(/\s+/, 2)
|
|
122
|
+
next unless token&.match?(/\A[a-z0-9][a-z0-9-]*(?:\[\d+\])?\z/)
|
|
123
|
+
|
|
124
|
+
name = token.sub(/\[\d+\]\z/, "")
|
|
125
|
+
entries[name] = {array: token.include?("[") || value.nil?}.freeze
|
|
126
|
+
end.freeze
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
attr_reader :target, :scopes
|
|
130
|
+
|
|
131
|
+
def initialize(target, scopes)
|
|
132
|
+
@target = target
|
|
133
|
+
@scopes = scopes.freeze
|
|
134
|
+
@aliases = {}
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def validate!(name, scope: nil)
|
|
138
|
+
return name if name.start_with?("@") && name.length > 1
|
|
139
|
+
|
|
140
|
+
match = name.match(/\A([a-z0-9][a-z0-9-]*)(?:\[(\d+)\])?\z/)
|
|
141
|
+
unknown!(name) unless match
|
|
142
|
+
base, index = match.captures
|
|
143
|
+
canonical = canonical(base)
|
|
144
|
+
unknown!(name) unless canonical
|
|
145
|
+
owners = scopes.keys.select { |owner| scopes.fetch(owner).key?(canonical) }
|
|
146
|
+
if scope && !owners.include?(scope)
|
|
147
|
+
raise Error, "#{name.inspect} is not a #{scope} option in tmux #{target.version}; " \
|
|
148
|
+
"use #{owners.first}.#{canonical.tr('-', '_')}"
|
|
149
|
+
end
|
|
150
|
+
if index && !scopes.fetch(owners.first).fetch(canonical).fetch(:array)
|
|
151
|
+
raise Error, "#{base.inspect} is not an array option in tmux #{target.version}"
|
|
152
|
+
end
|
|
153
|
+
name
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
private
|
|
157
|
+
|
|
158
|
+
def canonical(base)
|
|
159
|
+
return base if scopes.values.any? { |entries| entries.key?(base) }
|
|
160
|
+
return @aliases[base] if @aliases.key?(base)
|
|
161
|
+
|
|
162
|
+
resolved = target.canonical_name(base)
|
|
163
|
+
@aliases[base] = if resolved && scopes.values.any? { |entries| entries.key?(resolved) }
|
|
164
|
+
resolved
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def unknown!(name)
|
|
169
|
+
suggestions = DidYouMean::SpellChecker.new(dictionary: scopes.values.flat_map(&:keys).uniq).correct(name)
|
|
170
|
+
hint = suggestions.empty? ? "" : "; did you mean #{suggestions.first.inspect}?"
|
|
171
|
+
raise Error, "unknown option #{name.inspect} for tmux #{target.version}#{hint}"
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Validate both shell branches (either may run), but only the applicable
|
|
176
|
+
# version branch. Imports are rechecked against the destination's target.
|
|
177
|
+
class OptionValidator
|
|
178
|
+
def initialize(target) = @target = target
|
|
179
|
+
|
|
180
|
+
def validate(nodes)
|
|
181
|
+
nodes.each do |node|
|
|
182
|
+
case node
|
|
183
|
+
when ConfigNode::SetOption, ConfigNode::UnsetOption
|
|
184
|
+
next if node.name.start_with?("@") && node.name.length > 1
|
|
185
|
+
|
|
186
|
+
@target.catalog.validate!(node.name, scope: node.scope)
|
|
187
|
+
when ConfigNode::IfShell
|
|
188
|
+
validate(node.success)
|
|
189
|
+
validate(node.failure)
|
|
190
|
+
when ConfigNode::Condition
|
|
191
|
+
active = @target.at_least?(node.version)
|
|
192
|
+
active = !active if node.direction == :below
|
|
193
|
+
validate(active ? node.body : node.otherwise)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
private_constant :Target, :OptionCatalog, :OptionValidator
|
|
200
|
+
end
|
data/lib/tmux/version.rb
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tmux
|
|
4
|
+
# Collects left/centre/right zone blocks and assembles them, in that order
|
|
5
|
+
# regardless of definition order, into one aligned format — for a status line
|
|
6
|
+
# or pane border. Each zone's body is an ordinary format block.
|
|
7
|
+
class ZoneBuilder
|
|
8
|
+
def left(&block) = @left = block
|
|
9
|
+
def center(&block) = @center = block
|
|
10
|
+
def centre(&block) = @center = block
|
|
11
|
+
def right(&block) = @right = block
|
|
12
|
+
|
|
13
|
+
def to_format
|
|
14
|
+
left, center, right = @left, @center, @right
|
|
15
|
+
Tmux.format do
|
|
16
|
+
evaluate(left) if left
|
|
17
|
+
if center
|
|
18
|
+
reset_style
|
|
19
|
+
style attrs: "align=centre"
|
|
20
|
+
evaluate(center)
|
|
21
|
+
end
|
|
22
|
+
if right
|
|
23
|
+
reset_style
|
|
24
|
+
style attrs: "align=right"
|
|
25
|
+
evaluate(right)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
private_constant :ZoneBuilder
|
|
31
|
+
end
|
data/lib/tmux.rb
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "tmux/version"
|
|
4
|
+
require_relative "tmux/errors"
|
|
5
|
+
require_relative "tmux/escape"
|
|
6
|
+
require_relative "tmux/evaluation"
|
|
7
|
+
require_relative "tmux/expression"
|
|
8
|
+
require_relative "tmux/glyph"
|
|
9
|
+
require_relative "tmux/format"
|
|
10
|
+
require_relative "tmux/format_compiler"
|
|
11
|
+
require_relative "tmux/format_builder"
|
|
12
|
+
require_relative "tmux/zone_builder"
|
|
13
|
+
require_relative "tmux/config"
|
|
14
|
+
require_relative "tmux/config_compiler"
|
|
15
|
+
require_relative "tmux/command_builder"
|
|
16
|
+
require_relative "tmux/target"
|
|
17
|
+
require_relative "tmux/config_builder"
|
|
18
|
+
require_relative "tmux/option_tree"
|
|
19
|
+
require_relative "tmux/condition_compiler"
|
|
20
|
+
require_relative "tmux/dsl"
|
|
21
|
+
|
|
22
|
+
module Tmux
|
|
23
|
+
# Evaluates a configuration file and returns the Tmux::Config or
|
|
24
|
+
# Tmux::Format its final expression produces. The file runs inside a fresh
|
|
25
|
+
# anonymous module, which keeps its constants and methods out of the global
|
|
26
|
+
# namespace. Configuration files are trusted Ruby: the module is not a
|
|
27
|
+
# sandbox. Logical operators in DSL conditions are captured before Ruby
|
|
28
|
+
# evaluates them, so && / || build deferred tmux expressions.
|
|
29
|
+
def self.load_file(path, tmux: nil)
|
|
30
|
+
target = tmux ? Target.for_executable(tmux) : Target.current
|
|
31
|
+
Target.with(target) { evaluate_file(path) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.evaluate_file(path)
|
|
35
|
+
absolute = File.expand_path(path)
|
|
36
|
+
namespace = Module.new
|
|
37
|
+
# Top-level `def` helpers become callable from the file's own code,
|
|
38
|
+
# including blocks that take the builder as a parameter.
|
|
39
|
+
namespace.extend(namespace)
|
|
40
|
+
source = ConditionCompiler.rewrite(File.read(absolute), absolute)
|
|
41
|
+
result = namespace.module_eval(source, absolute)
|
|
42
|
+
return result if result.is_a?(Config) || result.is_a?(Format)
|
|
43
|
+
|
|
44
|
+
raise Error, "#{path} must evaluate to a Tmux::Config or Tmux::Format"
|
|
45
|
+
end
|
|
46
|
+
private_class_method :evaluate_file
|
|
47
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: tmux
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Nick deLannoy
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: prism
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.19'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '2'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '0.19'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '2'
|
|
32
|
+
description: |
|
|
33
|
+
tmux.rb compiles structured Ruby into deterministic tmux configuration and
|
|
34
|
+
format strings. Ruby runs only while building; tmux evaluates the output
|
|
35
|
+
natively, so status rendering never spawns Ruby.
|
|
36
|
+
executables:
|
|
37
|
+
- tmux.rb
|
|
38
|
+
- tmux_rb
|
|
39
|
+
extensions: []
|
|
40
|
+
extra_rdoc_files: []
|
|
41
|
+
files:
|
|
42
|
+
- CHANGELOG.md
|
|
43
|
+
- LICENSE
|
|
44
|
+
- README.md
|
|
45
|
+
- bin/tmux.rb
|
|
46
|
+
- bin/tmux_rb
|
|
47
|
+
- lib/tmux.rb
|
|
48
|
+
- lib/tmux/cli.rb
|
|
49
|
+
- lib/tmux/command_builder.rb
|
|
50
|
+
- lib/tmux/condition_compiler.rb
|
|
51
|
+
- lib/tmux/config.rb
|
|
52
|
+
- lib/tmux/config_builder.rb
|
|
53
|
+
- lib/tmux/config_compiler.rb
|
|
54
|
+
- lib/tmux/dsl.rb
|
|
55
|
+
- lib/tmux/errors.rb
|
|
56
|
+
- lib/tmux/escape.rb
|
|
57
|
+
- lib/tmux/evaluation.rb
|
|
58
|
+
- lib/tmux/expression.rb
|
|
59
|
+
- lib/tmux/format.rb
|
|
60
|
+
- lib/tmux/format_builder.rb
|
|
61
|
+
- lib/tmux/format_compiler.rb
|
|
62
|
+
- lib/tmux/glyph.rb
|
|
63
|
+
- lib/tmux/option_tree.rb
|
|
64
|
+
- lib/tmux/target.rb
|
|
65
|
+
- lib/tmux/version.rb
|
|
66
|
+
- lib/tmux/zone_builder.rb
|
|
67
|
+
homepage: https://github.com/cosmicbuffalo/tmux.rb
|
|
68
|
+
licenses:
|
|
69
|
+
- MIT
|
|
70
|
+
metadata:
|
|
71
|
+
source_code_uri: https://github.com/cosmicbuffalo/tmux.rb
|
|
72
|
+
changelog_uri: https://github.com/cosmicbuffalo/tmux.rb/blob/main/CHANGELOG.md
|
|
73
|
+
bug_tracker_uri: https://github.com/cosmicbuffalo/tmux.rb/issues
|
|
74
|
+
rubygems_mfa_required: 'true'
|
|
75
|
+
rdoc_options: []
|
|
76
|
+
require_paths:
|
|
77
|
+
- lib
|
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '3.2'
|
|
83
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - ">="
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '0'
|
|
88
|
+
requirements: []
|
|
89
|
+
rubygems_version: 3.6.7
|
|
90
|
+
specification_version: 4
|
|
91
|
+
summary: A readable Ruby DSL that compiles to native tmux configuration
|
|
92
|
+
test_files: []
|