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.
@@ -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
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tmux
4
+ VERSION = "0.1.0"
5
+ end
@@ -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: []