libtmux 0.1.0.alpha.1 → 0.1.0.alpha.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1c19838a471ee26a9953d27f31bbaece230b6b8d2422d58dd5c57d0512a186f8
4
- data.tar.gz: ce84cd707102bcd05b9a32d7dfdd7d59791fa8a802aa4eb81068bc36f81caae6
3
+ metadata.gz: 8c42fbd138b8dcb5637f8a47ee771a288f318c9a34dfb83a8836ca9be6cec7a9
4
+ data.tar.gz: a3801bfd5c494129d0bee1872f756c03c2ec148262d245075336e9f54869ab95
5
5
  SHA512:
6
- metadata.gz: 99d71b78ddff682bbafbcb3cd24a60989cb8e5ea052bd34e83902276ccdf044b13ade3d8916fd7342e5627ebc752d8f4a7a30e85d22e703455ca423b5a5dcfbb
7
- data.tar.gz: 835a4ff8a6185b3db991705836e229a5cf574b90ed8805d0110a21028878cbb9141368d4c52a88da2c9441342c7e1791bf300012f262b88dd73bc0c53103fcc0
6
+ metadata.gz: c8b889852d311651fdc65cff4a76776b7d3ab6dda81947d722ea4f268754361380e58095e99cc545c93c7a7d69b7ffc7ca4b76c09ab12ab4f620c32123f69b26
7
+ data.tar.gz: 98f5b864675439967d3377a465423b5719e8be045eb96e8ab53c06047e5d6c2bf3f7a6db54c6a87ab8382cfe8b272ddac1e16df444ee209144bd24579cad9406
data/README.md CHANGED
@@ -1,3 +1,14 @@
1
+ <!-- libtmux-logo -->
2
+ <div align="center">
3
+ <picture>
4
+ <source srcset="assets/logo.svg" type="image/svg+xml">
5
+ <img src="assets/logo.png" width="128" height="128" alt="libtmux for Ruby">
6
+ </picture>
7
+ </div>
8
+ <!-- /libtmux-logo -->
9
+
10
+ <div align="center">
11
+
1
12
  # libtmux
2
13
 
3
14
  Ruby tmux orchestration core. `Server.open` borrows an
@@ -8,6 +19,12 @@ Owned startup uses Linux and Darwin readiness backends. The
8
19
  [compatibility workflow](https://github.com/libtmux/libtmux-ruby/actions/workflows/compatibility.yml)
9
20
  retains exact per-revision platform and version results.
10
21
 
22
+ </div>
23
+
24
+ Requires tmux 3.3a or newer; unpatched 3.3 is below the minimum. A server
25
+ handle checks the connected daemon's release before any command is
26
+ dispatched, and refuses an older daemon even when its client is newer.
27
+
11
28
  Install the alpha with `gem install libtmux --pre`, then require `libtmux`.
12
29
  Imports do not
13
30
  start tmux, a scheduler or an MCP server. See the repository's contribution
@@ -96,8 +113,8 @@ and retains `previous_generation`. Every new subscription begins with a
96
113
  subscriptions stay closed, and requests are never replayed. Event sequences
97
114
  describe one connection's observations, not durable pane history.
98
115
 
99
- Typed command coverage includes hierarchy creation/listing,
100
- rename/split/resize/swap/join/break/respawn/layout operations, link operations,
116
+ Typed command coverage includes hierarchy creation/listing; rename, split,
117
+ resize, swap, join, break, respawn, and layout operations; link operations;
101
118
  options/hooks/environment, capture/send/paste/pipe/buffers, copy commands,
102
119
  display/source-file/wait-for. It does not establish complete flag parity or
103
120
  every compatibility cell; consult the workflow results. RBS validation checks declarations;
data/assets/logo.png ADDED
Binary file
data/assets/logo.svg ADDED
@@ -0,0 +1,2 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 135.46667 135.46666" role="img" aria-label="libtmux for Ruby" class="libtmux-logo" data-language="ruby" data-variant="library" focusable="false"><title>libtmux for Ruby</title><g transform="translate(-210.28959,-409.98108)" data-part="artwork"><g transform="matrix(1.024,0,0,1.0240011,-5.0469507,-9.8399997)" data-part="primary-tile"><path d="M 227.78906,0 213.98242,3.6992188 V 460.62109 H 241.4082 V 15.564453 Z" transform="matrix(0.26458333,0,0,0.26458333,210.28959,409.98108)" fill="#2D0002" data-part="primary-edge"></path><path d="m 210.28981,417.13553 v 113.86425 l 60.26885,2.60089 V 409.98109 h -53.11441 c -3.95129,0 -7.15444,3.20315 -7.15444,7.15444 z" fill="#701516" data-part="primary"></path></g><g transform="matrix(1.024,0,0,1.0240011,-5.0469507,-9.8399997)" data-part="lower-tile"><path d="m 486.38086,246.03906 -5.24805,4.99805 v 207.2207 l 5.24805,-0.93164 L 500,465.18945 V 261.60352 Z" transform="matrix(0.26458333,0,0,0.26458333,210.28959,409.98108)" fill="#67221F" data-part="lower-edge"></path><path d="m 278.70874,475.07917 v 64.8875 l 60.26883,-8.98455 v -55.90295 z" fill="#984640" data-part="lower"></path></g><path d="m 210.28959,533.88585 v 4.23589 c 9e-5,4.04619 3.2803,7.32617 7.32649,7.32598 h 28.63105 l 81.26967,-1.0223 14.5495,-10.53906 v 0 z" fill="#231F20" data-part="base"></path><g transform="matrix(1.024,0,0,1.0240011,-5.0469507,-9.8399997)" data-part="upper-tile"><path d="m 459.33984,0 -25.33593,2.1074219 h 21.38281 c 14.93397,0 27.04101,12.1070061 27.04101,27.0410161 V 129.83008 l 6.17579,13.24609 L 500,112.43359 V 42.603516 C 499.99913,8.8176763 474.27408,-6.8031496e-4 459.33984,0 Z" transform="matrix(0.26458333,0,0,0.26458333,210.28959,409.98108)" fill="#854A46" data-part="upper-side"></path><path d="m 331.82331,409.98109 h -53.11442 v 56.93261 h 60.26885 v -49.77817 c 0,-3.95129 -3.20315,-7.15444 -7.15443,-7.15444 z" fill="#B6706A" data-part="upper"></path><path d="m 278.7089,466.9137 h 60.26885 l 3.6035,4.11782 h -60.26932 z" fill="#733A36" data-part="upper-bottom"></path></g><g transform="matrix(1.024,0,0,1.0240011,-5.0469507,-9.8399997)" data-part="gear-group"><path d="m 298.35661,419.37889 c -3.307,0 -6.01087,2.63649 -6.0946,5.94228 l -0.33026,13.04677 c -0.0503,1.98883 -1.3048,3.74588 -3.16843,4.44175 -1.63527,0.6105 -3.23561,1.29298 -4.79768,2.04293 -1.82786,0.87796 -4.00547,0.54134 -5.47527,-0.85609 l -9.17457,-8.72115 c -2.39692,-2.27863 -6.17361,-2.23122 -8.512,0.10759 l -14.3945,14.39447 c -1.27625,1.27604 -1.86544,2.98029 -1.77147,4.65491 -0.0118,0.22021 -0.0123,0.44029 0,0.66053 -0.0118,0.22023 -0.0123,0.44029 0,0.66053 -0.0118,0.22021 -0.0123,0.44029 0,0.66053 -0.0118,0.22024 -0.0123,0.44029 0,0.66053 -0.0118,0.22024 -0.0123,0.44029 0,0.66053 -0.0118,0.22024 -0.0123,0.44029 0,0.66053 -0.0118,0.22024 -0.0123,0.44029 0,0.66053 -0.0838,1.61437 0.46467,3.25669 1.6634,4.51768 l 8.99461,9.46208 c 0.23074,0.24278 0.42181,0.51038 0.59418,0.78721 -0.60023,1.35769 -1.15279,2.74067 -1.64984,4.15071 -0.6727,1.90958 -2.45019,3.20822 -4.47443,3.25942 l -12.65165,0.31971 c -3.30618,0.0837 -5.94279,2.7886 -5.94279,6.0956 v 0.66053 0.66053 0.66053 0.66056 2.64112 15.07361 0.66053 0.66053 0.66053 0.66053 0.66053 0.66053 0.66053 0.66053 c 0,3.30703 2.63661,6.01136 5.94279,6.09511 l 13.04677,0.32975 c 1.98885,0.0503 3.74539,1.30529 4.44124,3.16895 0.53531,1.43379 1.12957,2.83886 1.7725,4.21655 -0.17026,0.27145 -0.35881,0.53398 -0.58564,0.77264 l -8.72163,9.17405 c -1.19922,1.26148 -1.74723,2.90475 -1.66291,4.51969 -0.0124,0.22024 -0.0118,0.44029 0,0.66053 v 0.0457 h 0.0875 l 10.03868,-9.51839 29.63949,8.95286 h 43.47603 l 13.935,-16.69426 v -0.0216 c -8.04912,9.72841 -20.24315,15.90068 -33.88319,15.7844 -22.83086,-0.19494 -41.62302,-18.24181 -43.0251,-40.7806 1.34121,-22.94291 20.4588,-41.11551 43.78415,-40.91631 13.2902,0.11325 25.20451,6.28323 33.12414,15.86034 v -3.90741 -20.27843 -11.02398 l -3.97024,3.7742 c -1.44419,1.37264 -3.57681,1.72648 -5.38882,0.89729 -1.57465,-0.72024 -3.18724,-1.3729 -4.83384,-1.95295 -1.90955,-0.67268 -3.2077,-2.45012 -3.25892,-4.47392 l -0.32023,-12.65265 c -0.0836,-3.30579 -2.78757,-5.94228 -6.09457,-5.94228 z" fill="#999999" data-part="gear-edge"></path><path d="m 298.35715,419.3789 c -3.30703,0 -6.01139,2.63697 -6.09511,5.94277 l -0.33075,13.04677 c -0.0504,1.98885 -1.30431,3.74539 -3.16795,4.44126 -1.63521,0.6105 -3.23553,1.29298 -4.79716,2.04293 -1.8283,0.87755 -4.0065,0.54134 -5.47627,-0.85609 l -9.17408,-8.72115 c -2.39692,-2.27863 -6.17372,-2.23122 -8.51252,0.10759 l -14.39447,14.39498 c -2.33842,2.33839 -2.38624,6.1146 -0.10759,8.51152 l 8.9951,9.46211 c 1.37305,1.44416 1.72558,3.57678 0.89681,5.38881 -0.72068,1.57466 -1.37293,3.18676 -1.95295,4.83335 -0.67271,1.90958 -2.45022,3.20823 -4.47444,3.25942 l -12.65164,0.32023 c -3.30621,0.0837 -5.9428,2.78759 -5.9428,6.09459 v 20.35786 c 0,3.30703 2.63659,6.01087 5.9428,6.09459 l 13.04677,0.33027 c 1.98843,0.0504 3.74578,1.30479 4.44123,3.16846 0.61094,1.63521 1.29301,3.2356 2.04295,4.79765 0.87797,1.82789 0.54145,4.00591 -0.85559,5.47527 l -8.72164,9.17457 c -1.37514,1.44653 -1.88755,3.39384 -1.57592,5.22595 h 0.7465 24.32055 61.26465 3.60327 c 3.95126,0 7.15424,-3.20297 7.15424,-7.15427 v -2.056 -8.04952 c -8.05257,9.71866 -20.25002,15.87899 -33.88271,15.76278 -23.61284,-0.2016 -42.90943,-19.4986 -43.11104,-43.11106 -0.20729,-24.31847 19.55063,-44.07731 43.86912,-43.8696 13.28515,0.11319 25.2041,6.27146 33.12463,15.84172 v -18.88347 -11.02398 l -3.97073,3.77469 c -1.44417,1.37264 -3.57578,1.72599 -5.38782,0.8968 -1.57506,-0.72024 -3.18776,-1.37292 -4.83435,-1.95295 -1.90958,-0.67309 -3.20729,-2.45063 -3.25892,-4.47441 l -0.32023,-12.65167 c -0.0833,-3.3058 -2.78767,-5.94277 -6.09509,-5.94277 z" fill="#E6E6E6" data-part="gear"></path></g></g></svg>
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "snapshot"
4
4
  require_relative "metadata"
5
+ require_relative "tmux_version"
5
6
 
6
7
  module LibTmux
7
8
  module Internal
@@ -53,19 +54,16 @@ module LibTmux
53
54
  end
54
55
  version, pid, start_time = rows.first
55
56
  version = version.dup.force_encoding(Encoding::UTF_8)
56
- match = /\A(?:next-)?([0-9]+)\.([0-9]+)([a-z]?)(?:-[A-Za-z0-9.-]+)?\z/.match(version) if version.valid_encoding?
57
- unless match
58
- raise UnsupportedFeatureError.new("unrecognized tmux version for capture", delivery: :observed, phase: :capture)
59
- end
60
- supported = ([match[1].to_i, match[2].to_i, match[3]] <=> [3, 2, "a"]) >= 0
61
- unless supported
62
- raise UnsupportedFeatureError.new("captured metadata requires tmux 3.2a or newer", delivery: :observed, phase: :capture)
57
+ begin
58
+ TmuxVersion.require_supported(version)
59
+ rescue UnsupportedFeatureError => error
60
+ raise UnsupportedFeatureError.new(error.message, delivery: :observed, phase: :capture)
63
61
  end
64
62
  unless pid.match?(/\A[1-9][0-9]{0,9}\z/n) && start_time.match?(/\A-?[0-9]{1,19}\z/n)
65
63
  raise ProtocolError.new("invalid capture server identity metadata", delivery: :observed, phase: :capture)
66
64
  end
67
65
  {version: version, pid: Integer(pid, 10), start_time: Integer(start_time, 10),
68
- capabilities: {metadata: :byte_counted, field_baseline: "3.2a"}}
66
+ capabilities: {metadata: :byte_counted, field_baseline: TmuxVersion::MINIMUM}}
69
67
  end
70
68
 
71
69
  def acquire_rows(budget, attempt, clients)
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "tmux_version"
4
+
3
5
  module LibTmux
4
6
  module Internal
5
7
  # Names and bounds are explicit: wire spelling is not Ruby case conversion.
@@ -23,7 +25,7 @@ module LibTmux
23
25
  Field.new(id: id, name: name, wire_name: wire_name, format: format, type: type,
24
26
  nullable: nullable, empty_is_null: empty_is_null, min: min, max: max,
25
27
  operators: operators || {text: TEXT, integer: INTEGER, boolean: EQUALITY}.fetch(type),
26
- min_version: "3.2a", scope: scope, capture_requirements: [scope].freeze)
28
+ min_version: Internal::TmuxVersion::MINIMUM, scope: scope, capture_requirements: [scope].freeze)
27
29
  end
28
30
  private_class_method :field
29
31
 
@@ -40,7 +42,7 @@ module LibTmux
40
42
  end
41
43
  private_class_method :schema
42
44
 
43
- # 3.2a is the supported capture baseline, not a claim about first introduction.
45
+ # The minimum supported tmux is the capture baseline, not a claim about first introduction.
44
46
  SCHEMAS = {
45
47
  session: schema(:session, "session", [
46
48
  field(:session, "session.id", :id, "id", "session_id", :text, operators: EQUALITY),
@@ -70,7 +72,7 @@ module LibTmux
70
72
  field(:pane, "pane.id", :id, "id", "pane_id", :text, operators: EQUALITY),
71
73
  field(:pane, "pane.window_id", :window_id, "windowId", "window_id", :text, operators: EQUALITY),
72
74
  field(:pane, "pane.index", :index, "index", "pane_index", :integer, min: 0, max: UINT_MAX),
73
- field(:pane, "pane.pid", :pid, "pid", "pane_pid", :integer, min: 0, max: INT_MAX),
75
+ field(:pane, "pane.pid", :pid, "pid", "pane_pid", :integer, nullable: true, empty_is_null: true, min: 0, max: INT_MAX),
74
76
  field(:pane, "pane.current_command", :current_command, "currentCommand", "pane_current_command", :text),
75
77
  field(:pane, "pane.current_path", :current_path, "currentPath", "pane_current_path", :text, nullable: true, empty_is_null: true),
76
78
  field(:pane, "pane.title", :title, "title", "pane_title", :text),
@@ -135,16 +135,16 @@ module LibTmux
135
135
  value = layout.is_a?(Symbol) ? layout.to_s.tr("_", "-") : layout
136
136
  budget = server.__send__(:operation_budget, timeout, cancel)
137
137
  # Older tmux can access an invalid pointer for a malformed layout header.
138
- unless LAYOUTS.include?(value) || /\A[0-9a-fA-F]{4},/.match?(value)
138
+ # tmux 3.8-rc2 and later report and accept saved layouts as JSON.
139
+ saved = /\A[0-9a-fA-F]{4},/.match?(value) ||
140
+ (value.start_with?("{") && server.__send__(:tmux_release) >= Internal::TmuxVersion.parse("3.8-rc2"))
141
+ unless LAYOUTS.include?(value) || saved
139
142
  choices = (LAYOUTS + MIRRORED_LAYOUTS).select { |name| name.start_with?(value) }
140
143
  raise ArgumentError, "layout must be a native name or a saved layout" if value.empty? || choices.empty?
141
144
 
142
145
  if choices.any? { |name| MIRRORED_LAYOUTS.include?(name) }
143
- version = server.display('#{version}', **budget.options).text
144
- release = /\A(\d+)\.(\d+)/.match(version)
145
- raise ProtocolError.new("tmux version is invalid", phase: :decode, delivery: :observed) unless release
146
-
147
- if ([release[1].to_i, release[2].to_i] <=> [3, 5]).negative?
146
+ release = server.__send__(:tmux_release)
147
+ if release < Internal::TmuxVersion.parse("3.5")
148
148
  choices &= LAYOUTS
149
149
  if choices.empty?
150
150
  raise UnsupportedFeatureError.new("mirrored layouts require tmux 3.5+", phase: :admission)
@@ -16,7 +16,7 @@ module LibTmux
16
16
  def unquote(value)
17
17
  escapes = {"a" => "\a", "b" => "\b", "t" => "\t", "n" => "\n", "v" => "\v",
18
18
  "f" => "\f", "r" => "\r", "s" => " ", "E" => "\e"}
19
- value.b.gsub(/\\([0-7]{3}|.)/n) do
19
+ value.b.gsub(/\\([0-7]{3}|.)/nm) do
20
20
  escaped = Regexp.last_match(1)
21
21
  if escaped.match?(/\A[0-7]{3}\z/)
22
22
  byte = escaped.to_i(8)
data/lib/libtmux/owned.rb CHANGED
@@ -49,6 +49,9 @@ module LibTmux
49
49
  if @endpoint.socket_path.bytesize > 103
50
50
  raise UnsupportedFeatureError.new("owned Unix socket path exceeds the platform limit", phase: :startup)
51
51
  end
52
+ Internal::TmuxVersion.executable(@endpoint.executable) do |argv|
53
+ ProcessExecutor.new.run(argv, timeout: deadline - clock, cancel: cancel)
54
+ end
52
55
  @readiness = SocketReadiness.new(@directory)
53
56
  @child = OwnedChild.new
54
57
  pid = nil
@@ -104,14 +104,17 @@ module LibTmux
104
104
  @drain_timeout = drain_timeout
105
105
  end
106
106
 
107
- def run(argv, input: "".b, env: {}, timeout: 5.0, cancel: nil)
108
- Execution.new(argv, input, env, timeout, cancel, @limits, @cleanup_timeout, @drain_timeout).call
107
+ # +discard_stdout+ gives the child the null device for stdout, so the
108
+ # result reports no stdout and the call does not wait for a descriptor
109
+ # that a daemon older than tmux 3.2 keeps open after its client exits.
110
+ def run(argv, input: "".b, env: {}, timeout: 5.0, cancel: nil, discard_stdout: false)
111
+ Execution.new(argv, input, env, timeout, cancel, @limits, @cleanup_timeout, @drain_timeout, discard_stdout).call
109
112
  end
110
113
 
111
114
  class Execution
112
115
  CHUNK_BYTES = 16_384
113
116
 
114
- def initialize(argv, input, env, timeout, cancel, limits, cleanup_timeout, drain_timeout)
117
+ def initialize(argv, input, env, timeout, cancel, limits, cleanup_timeout, drain_timeout, discard_stdout)
115
118
  @started = monotonic
116
119
  unless argv.is_a?(Array) && !argv.empty? && argv.all? { |argument| argument.is_a?(String) && !argument.include?("\0") }
117
120
  raise ArgumentError, "argv must contain strings without NUL"
@@ -130,6 +133,7 @@ module LibTmux
130
133
  @limits = limits
131
134
  @cleanup_timeout = cleanup_timeout
132
135
  @drain_timeout = drain_timeout
136
+ @discard_stdout = discard_stdout
133
137
  @process_wait = ProcessWait.new
134
138
  @owned = []
135
139
  @buffers = {stdout: +"".b, stderr: +"".b}
@@ -180,7 +184,12 @@ module LibTmux
180
184
 
181
185
  def spawn
182
186
  child_input, @input_writer = pipe
183
- @output_reader, child_output = pipe
187
+ if @discard_stdout
188
+ child_output = File.open(File::NULL, "wb")
189
+ @owned << child_output
190
+ else
191
+ @output_reader, child_output = pipe
192
+ end
184
193
  @error_reader, child_error = pipe
185
194
  @child = OwnedChild.new(@process_wait)
186
195
  @exit_reader = @child.reader
@@ -198,7 +207,8 @@ module LibTmux
198
207
  end
199
208
 
200
209
  def communicate
201
- reading = {@output_reader => :stdout, @error_reader => :stderr}
210
+ reading = {@error_reader => :stderr}
211
+ reading[@output_reader] = :stdout if @output_reader
202
212
  loop do
203
213
  return result if @status && reading.empty?
204
214
 
@@ -4,9 +4,101 @@ require_relative "endpoint"
4
4
  require_relative "process"
5
5
  require_relative "metadata"
6
6
  require_relative "entity"
7
+ require_relative "tmux_version"
7
8
 
8
9
  module LibTmux
9
10
  # Connects to an existing endpoint. Closing retires clients, never the daemon.
11
+ #
12
+ # A server is bound to one tmux executable and one Unix socket. It keeps a
13
+ # private link to the socket it bound, so commands never reach a different
14
+ # daemon that later takes over the same path. An {EntityRef} is valid only on
15
+ # the server that issued it; another server raises {TargetNotFoundError}.
16
+ #
17
+ # Methods that run a tmux command take `timeout:` and `cancel:`. The typed
18
+ # methods raise {CommandError} when tmux rejects a command, and a listing
19
+ # that fails raises, so an empty array means tmux answered with nothing to
20
+ # list. {#run} and {#run_group} answer their result whatever tmux's exit
21
+ # status. `max_requests:` bounds concurrent commands and `max_controls:`
22
+ # bounds control connections; a call past either limit raises
23
+ # {CapacityError} instead of waiting. After {#close} the server refuses new
24
+ # requests with {ClosedError}, and {#diagnostics} still answers. A forked
25
+ # child can close an inherited server but cannot use it.
26
+ #
27
+ # # What is on here
28
+ #
29
+ # The methods fall into the groups below. A {Session}, {Window}, or {Pane}
30
+ # carries the operations on that one thing.
31
+ #
32
+ # **Connecting.** {#initialize Server.new} and {Server.open} bind an endpoint
33
+ # that already exists; {Server.open} with a block closes the server when the
34
+ # block ends. {Server.start} starts a private daemon instead, creates no
35
+ # session, and owns the daemon's exit. {#endpoint} answers the {Endpoint}
36
+ # and {#owned?} says whether this server started its daemon. {#diagnostics}
37
+ # reports the request and control slots in use, and the limits. {#close}
38
+ # retires the server's clients; on an owned server it also stops the daemon.
39
+ # {#open_control} opens a {ControlConnection} to a session.
40
+ #
41
+ # **Finding one thing.** {#session}, {#window}, {#pane}, and {#window_link}
42
+ # wrap an {EntityRef} in a handle without running a command. Take the ref
43
+ # from a handle or a snapshot record. A ref of the wrong kind raises
44
+ # {TargetNotFoundError}; one whose entity has gone fails at its first
45
+ # command.
46
+ #
47
+ # **Listing everything.** {#list_sessions}, {#list_windows}, and
48
+ # {#list_panes} answer frozen arrays of handles in numeric ID order, and
49
+ # {#list_window_links} answers every {WindowLink}. {#list_clients} answers
50
+ # one hash per client. {#snapshot} captures the session, window, and pane
51
+ # tree, and clients with `clients: true`, as one validated {Snapshot}.
52
+ #
53
+ # **Querying things and getting around.** {#search_panes} filters the panes
54
+ # of a snapshot with `where:`, a hash of criteria or a {FilterExpr}, and
55
+ # answers a {Selection}; {#explain_panes} answers the plan for a query
56
+ # without running tmux. A {Selection} narrows with {Selection#where},
57
+ # {Selection#select}, {Selection#filter}, and {Selection#reject}, tests with
58
+ # {Selection#exists?}, and takes a single match with {Selection#one}, which
59
+ # raises {NoMatchError} or {MultipleMatchesError} otherwise, or
60
+ # {Selection#one_or_nil}. Build and compose criteria with
61
+ # {FilterExpr.build}, {FilterExpr#and}, {FilterExpr#or}, {FilterExpr#not},
62
+ # and {FilterExpr.from_json}.
63
+ #
64
+ # A {Snapshot} answers relational questions without another command:
65
+ # {Snapshot#sessions}, {Snapshot#windows}, {Snapshot#panes}, and
66
+ # {Snapshot#clients}. Its records follow relations by name: a
67
+ # {SessionSnapshot} reaches `windows`, `panes`, and `current_window`, a
68
+ # {WindowSnapshot} reaches `panes` and `active_pane`, and a {PaneSnapshot}
69
+ # reaches its `window`. {Entity#snapshot} reads one handle back as a record,
70
+ # and {CapturedRecord#ref} with {#session}, {#window}, or {#pane} turns a
71
+ # record into a handle. Live handles go down one command at a time with
72
+ # {Session#list_windows}, {Session#list_panes}, and {Window#list_panes}, and
73
+ # back up with {Entity#server}.
74
+ #
75
+ # **Changing things.** {#new_session} starts a detached session running
76
+ # `command:`, an argv array rather than a shell string, and answers the
77
+ # {Session} or, with `receipt: true`, a {CreationReceipt}. {#kill} stops the
78
+ # tmux server, which {#close} does only for an owned server.
79
+ # {#switch_client} points the client named by `client:` at a session.
80
+ #
81
+ # **Options and hooks.** {#options} answers {Options} at server scope, or at
82
+ # the global session or window scope. {#hooks} answers {Hooks} at the global
83
+ # session or window scope. {#environment}, {#set_environment}, and
84
+ # {#unset_environment} read, set, and unset one variable in the server's
85
+ # global environment.
86
+ #
87
+ # **Everything else tmux keeps.** Paste buffers: {#list_buffers},
88
+ # {#read_buffer}, {#write_buffer}, and {#delete_buffer}. {#source_file}
89
+ # loads a configuration file. {#display} expands a tmux format; the argument
90
+ # is a format, not literal text. {#wait_for} waits on, signals, locks, or
91
+ # unlocks a channel, as `action:` says.
92
+ #
93
+ # **Things that need a terminal.** {#attach} runs an interactive client on a
94
+ # TTY the caller supplies and keeps, blocks until that client exits, and
95
+ # answers a {TerminalResult}. Cancelling detaches the client and does not
96
+ # stop pane commands.
97
+ #
98
+ # Anything else tmux can do is reachable through {#run}, which runs one
99
+ # command from an argv array and keeps tmux's separators and format
100
+ # semantics, or {#run_group}, which runs several in one tmux invocation. A
101
+ # {GroupResult} cannot attribute the client's final status to a single step.
10
102
  class Server
11
103
  attr_reader :endpoint
12
104
 
@@ -42,7 +134,8 @@ module LibTmux
42
134
  result
43
135
  end
44
136
 
45
- def initialize(endpoint: nil, socket_path: nil, socket_name: nil, executable: "tmux", max_requests: 32, max_controls: 4, close_timeout: 0.5)
137
+ def initialize(endpoint: nil, socket_path: nil, socket_name: nil, executable: "tmux", max_requests: 32, max_controls: 4, close_timeout: 0.5,
138
+ version_timeout: 5.0)
46
139
  if endpoint && (socket_path || socket_name || executable != "tmux")
47
140
  raise ArgumentError, "endpoint cannot be combined with socket or executable options"
48
141
  end
@@ -57,6 +150,10 @@ module LibTmux
57
150
  raise ArgumentError, "close_timeout must be positive and finite"
58
151
  end
59
152
 
153
+ unless version_timeout.is_a?(Numeric) && version_timeout.finite? && version_timeout.positive?
154
+ raise ArgumentError, "version_timeout must be positive and finite"
155
+ end
156
+
60
157
  @endpoint = endpoint || Endpoint.new(socket_path: socket_path, socket_name: socket_name, executable: executable)
61
158
  @owner_pid = Process.pid
62
159
  @max_requests = max_requests
@@ -69,6 +166,19 @@ module LibTmux
69
166
  @closed = false
70
167
  @executor = Internal::ProcessExecutor.new
71
168
  @pin = Internal::SocketIdentity.new(@endpoint)
169
+ begin
170
+ @release = Internal::TmuxVersion.connected(@pin.command_prefix) { |argv| @executor.run(argv, timeout: version_timeout, discard_stdout: true) }
171
+ argv, marker = Internal::TmuxVersion.fence_check_argv
172
+ Internal::TmuxVersion.verify_fence(@executor.run(@pin.command_prefix + argv, timeout: version_timeout), marker)
173
+ rescue Exception
174
+ @pin.close
175
+ raise
176
+ end
177
+ end
178
+
179
+ # The daemon's release text, such as "3.7d" or "next-3.9".
180
+ def tmux_version
181
+ @release.text
72
182
  end
73
183
 
74
184
  # Raw tmux arguments retain tmux's separators and format semantics.
@@ -79,8 +189,10 @@ module LibTmux
79
189
  raise ArgumentError, "raw commands cannot override endpoint flags" if argv.first.start_with?("-")
80
190
  raise ArgumentError, "timeout must be finite" unless timeout.is_a?(Numeric) && timeout.finite?
81
191
  perform_request(cancel: cancel) do |view|
82
- @executor.run(@pin.command_prefix + argv, input: input,
192
+ fence, marker = Internal::TmuxVersion.fence(@release)
193
+ result = @executor.run(@pin.command_prefix + fence + argv, input: input,
83
194
  timeout: timeout - (monotonic - started), cancel: view)
195
+ Internal::TmuxVersion.check_fence(result, marker, @release)
84
196
  end
85
197
  end
86
198
 
@@ -283,6 +395,10 @@ module LibTmux
283
395
  end
284
396
  private_constant :CancellationView
285
397
 
398
+ def tmux_release
399
+ with_bound_endpoint { |_endpoint, _pin, release| release }
400
+ end
401
+
286
402
  def monotonic
287
403
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
288
404
  end
@@ -298,7 +414,7 @@ module LibTmux
298
414
  ensure_owner
299
415
  @mutex.synchronize do
300
416
  ensure_open
301
- yield @endpoint, @pin
417
+ yield @endpoint, @pin, @release
302
418
  end
303
419
  end
304
420
 
@@ -147,6 +147,10 @@ module LibTmux
147
147
  end
148
148
 
149
149
  class LogSocketReadiness
150
+ # tmux 3.8 parses more default key bindings under -v than 3.7, so the
151
+ # log reaches "server loop enter" past 1 MiB.
152
+ LIMIT = 1 << 22
153
+
150
154
  attr_reader :bytes_read
151
155
 
152
156
  def initialize(directory)
@@ -184,7 +188,7 @@ module LibTmux
184
188
  end
185
189
  while (bytes = @log.read(16_384))
186
190
  @bytes_read += bytes.bytesize
187
- if @bytes_read > 1 << 20
191
+ if @bytes_read > LIMIT
188
192
  raise CapacityError.new("owned startup log exceeded byte limit", phase: :startup, pid: child.pid, delivery: :possibly_sent)
189
193
  end
190
194
  text = @tail + bytes
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "libtmux/errors"
4
+ require "libtmux/tmux_version"
5
+
6
+ module LibTmux
7
+ # Here "support" means which tmux releases libtmux supports, not helper
8
+ # code. Every server handle checks its daemon's release before any command
9
+ # runs; this module exposes that check.
10
+ module Support
11
+ # The version check: is this tmux daemon supported at all?
12
+ module Versions
13
+ MINIMUM = Internal::TmuxVersion::MINIMUM
14
+
15
+ module_function
16
+
17
+ # Returns the release text when +value+ is a supported release and
18
+ # raises UnsupportedFeatureError otherwise.
19
+ def require_supported(value)
20
+ Internal::TmuxVersion.require_supported(value).text
21
+ end
22
+
23
+ # The release text of the daemon behind +server+, such as "3.7d".
24
+ def server_version(server)
25
+ server.tmux_version
26
+ end
27
+ end
28
+
29
+ MINIMUM = Versions::MINIMUM
30
+
31
+ module_function
32
+
33
+ def require_supported(value) = Versions.require_supported(value)
34
+
35
+ def server_version(server) = Versions.server_version(server)
36
+ end
37
+ end
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "libtmux/errors"
4
+ require "securerandom"
5
+
6
+ module LibTmux
7
+ module Internal
8
+ # Version check for the tmux daemon a server handle is bound to.
9
+ #
10
+ # The connected daemon decides support, not the configured client. A
11
+ # release is read once through the retained route and every dispatch is
12
+ # then fenced against that exact text.
13
+ module TmuxVersion
14
+ MINIMUM = "3.3a"
15
+ # The forms tmux has reported: X.Y, X.Y plus a patch letter, X.Y-rc and
16
+ # X.Y-rcN candidates, and next-X.Y development builds. Anything else,
17
+ # including master and openbsd-*, is not a release number. The candidate
18
+ # number is bounded so that it can never outrank the release it precedes.
19
+ PATTERN = /\A(?:next-(?<next_major>\d+)\.(?<next_minor>\d+)|(?<major>\d+)\.(?<minor>\d+)(?:(?<patch>[a-z])|(?<candidate>-rc(?<number>\d{1,3})?))?)\z/
20
+ # tmux resolves a command-alias entry by exact name before it resolves a
21
+ # command, so the full names run-shell and if-shell can be hijacked by
22
+ # one alias, as the existing link guards avoid with builtin_spellings.
23
+ # These unique prefixes are out of reach of such an entry, and
24
+ # verify_fence proves at the version check that the fence still rejects
25
+ # its marker, so an alias named for a prefix, or a future command
26
+ # sharing it, refuses the server instead of silently disabling the fence.
27
+ FENCE_COMMAND = "if-shel"
28
+ PROBE_COMMAND = "run-shel"
29
+ ABSENT = /no server running|error connecting to .*\((?:No such file or directory|Connection refused)\)/
30
+ # What a daemon older than 3.2 answers to the probe. run-shell gained -C
31
+ # in 3.2, so 3.1 reports its usage line; a 3.0 daemon speaks a protocol
32
+ # the client refuses before any command runs.
33
+ PRE_3_2_ANSWER = /\A(?:usage: run-shell \[-b\] \[-t target-pane\] shell-command|server version is too old for client)\n\z/
34
+ REMEDY = "upgrade tmux and restart the server, or pin the previous libtmux release"
35
+ # Marks the refusals that mean "this daemon is not the supported one",
36
+ # so a caller that cannot show the full message can still name the floor.
37
+ EXPECTED = "tmux #{MINIMUM} or newer"
38
+
39
+ # `key` orders releases: major, minor, patch letter, then the build kind.
40
+ # A development build or release candidate ranks below the release it
41
+ # names: next-X.Y < X.Y-rc < X.Y-rcN < X.Y < X.Ya.
42
+ STABLE = 1 << 20
43
+
44
+ Release = Data.define(:text, :key) do
45
+ include Comparable
46
+
47
+ def <=>(other)
48
+ key <=> other.key if other.is_a?(Release)
49
+ end
50
+ end
51
+
52
+ module_function
53
+
54
+ def parse(value, source: "the daemon")
55
+ match = value.is_a?(String) && PATTERN.match(value)
56
+ raise unrecognized(value, source) unless match
57
+
58
+ key = if match[:next_major]
59
+ [match[:next_major].to_i, match[:next_minor].to_i, 0, 0]
60
+ else
61
+ patch = match[:patch] ? match[:patch].ord - 96 : 0
62
+ kind = match[:candidate] ? 1 + match[:number].to_i : STABLE
63
+ [match[:major].to_i, match[:minor].to_i, patch, kind]
64
+ end
65
+ Release.new(text: value.dup.freeze, key: key.freeze)
66
+ end
67
+
68
+ def require_supported(value, minimum: MINIMUM, source: "the daemon")
69
+ release = parse(value, source: source)
70
+ if (release <=> parse(minimum)).negative?
71
+ reported = (source == "the daemon") ? "connected version" : "#{source} reports"
72
+ raise UnsupportedFeatureError.new("tmux #{minimum} or newer is required (#{reported} #{release.text}); #{REMEDY}",
73
+ phase: :admission, expected: EXPECTED)
74
+ end
75
+ release
76
+ end
77
+
78
+ # Names what the daemon or executable reported, shortened so that a
79
+ # long report cannot flood the message.
80
+ def unrecognized(value, source)
81
+ shown = value.is_a?(String) ? value.byteslice(0, 64).scrub("?") : value
82
+ UnsupportedFeatureError.new("tmux version is unrecognized (#{source} reports #{shown.inspect}); " \
83
+ "tmux #{MINIMUM} or newer is required; #{REMEDY}", phase: :admission, expected: EXPECTED)
84
+ end
85
+
86
+ # Reads the daemon behind +prefix+. The block runs the argv and returns a
87
+ # CommandResult. Unlike display-message, run-shell -C fires no after-*
88
+ # hook and no command-error hook: tmux parses the expanded string after
89
+ # the command returned, so the private unknown command it reports has no
90
+ # effect and the version travels in the error text.
91
+ def connected(prefix)
92
+ marker = "libtmux-version-#{SecureRandom.hex(16)}-"
93
+ result = yield(prefix + [PROBE_COMMAND, "-C", marker + '#{version}'])
94
+ stderr = result.stderr.dup.force_encoding(Encoding::UTF_8)
95
+ if !result.success? && result.stdout.empty? && (match = /\Aunknown command: #{Regexp.escape(marker)}([^\n]+)\n\z/.match(stderr))
96
+ return require_supported(match[1])
97
+ end
98
+ if !result.success? && result.stdout.empty? && PRE_3_2_ANSWER.match?(stderr)
99
+ raise UnsupportedFeatureError.new("tmux #{MINIMUM} or newer is required (the connected daemon is older than 3.2); #{REMEDY}",
100
+ phase: :admission, expected: EXPECTED)
101
+ end
102
+ if ABSENT.match?(stderr)
103
+ raise TargetNotFoundError.new("no tmux server is running at the selected endpoint", phase: :admission)
104
+ end
105
+
106
+ raise CommandError.new("tmux did not report a daemon version", result: result, phase: :admission, delivery: :not_sent)
107
+ rescue Error => error
108
+ raise not_sent(error), cause: nil
109
+ end
110
+
111
+ # Reads the executable that would start a new daemon. tmux 3.3a prints
112
+ # its own file name where later releases print "tmux".
113
+ def executable(path)
114
+ result = yield([path, "-V"])
115
+ unless result.success?
116
+ raise CommandError.new("tmux did not report its version", result: result, phase: :admission, delivery: :not_sent)
117
+ end
118
+
119
+ names = ["tmux", File.basename(path)].uniq.map { |name| Regexp.escape(name.b) }.join("|")
120
+ match = /\A(?:#{names}) ([^\n]+)\n?\z/n.match(result.stdout)
121
+ raise unrecognized(result.stdout.strip, "the executable") unless match
122
+
123
+ require_supported(match[1], source: "the executable")
124
+ rescue Error => error
125
+ raise not_sent(error), cause: nil
126
+ end
127
+
128
+ # Leading commands for one dispatch. A daemon that no longer reports
129
+ # +release+ fails the if-shell condition and reaches the private marker,
130
+ # which tmux rejects before the commands that follow.
131
+ def fence(release)
132
+ marker = "libtmux-version-#{SecureRandom.hex(16)}"
133
+ [[FENCE_COMMAND, "-F", "\#{==:\#{version},#{release.text}}", "", marker, ";"], marker]
134
+ end
135
+
136
+ # A server runs this once when it is checked. A true if-shell condition
137
+ # runs a probe whose parse error names the marker, which shows that the
138
+ # fence command and the probe command are the real ones, with no failing
139
+ # command and so no hook.
140
+ def fence_check_argv
141
+ marker = "libtmux-fence-#{SecureRandom.hex(16)}"
142
+ [[FENCE_COMMAND, "-F", "1", "#{PROBE_COMMAND} -C #{marker}"], marker]
143
+ end
144
+
145
+ def verify_fence(result, marker)
146
+ return result if !result.success? && result.stdout.empty? && result.stderr.b == "unknown command: #{marker}\n".b
147
+
148
+ raise UnsupportedFeatureError.new("a tmux command-alias or command shadows #{FENCE_COMMAND} or " \
149
+ "#{PROBE_COMMAND}; remove it before opening a server", phase: :admission, delivery: :not_sent)
150
+ end
151
+
152
+ def check_fence(result, marker, release)
153
+ return result unless result.stderr.b.each_line.any? { |line| line.chomp == "unknown command: #{marker}".b }
154
+
155
+ raise UnsupportedFeatureError.new("the tmux daemon no longer reports #{release.text}; reopen the server",
156
+ phase: :admission, delivery: :not_sent, expected: EXPECTED)
157
+ end
158
+
159
+ # True for the refusals that name the supported floor: an unsupported,
160
+ # unreadable or changed daemon. Other unsupported features are not.
161
+ def refusal?(error)
162
+ error.is_a?(UnsupportedFeatureError) && error.expected == EXPECTED
163
+ end
164
+
165
+ # A fixed line for results that cannot carry the full message.
166
+ def hint
167
+ "tmux #{MINIMUM} or newer is required, and the daemon must stay the " \
168
+ "one the server opened with; upgrade tmux and restart the server"
169
+ end
170
+
171
+ # A failed probe sent nothing, whatever stage it failed in, so only the
172
+ # delivery is forced; the error keeps its own phase.
173
+ def not_sent(error)
174
+ details = {delivery: :not_sent, phase: error.phase, pid: error.pid, cleanup_errors: error.cleanup_errors,
175
+ expected: error.expected}
176
+ return error.class.new(error.message, result: error.result, **details) if error.is_a?(CommandError)
177
+
178
+ error.class.new(error.message, **details)
179
+ end
180
+ end
181
+ end
182
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LibTmux
4
- VERSION = "0.1.0.alpha.1"
4
+ VERSION = "0.1.0.alpha.2"
5
5
  end
data/lib/libtmux.rb CHANGED
@@ -8,6 +8,7 @@ require "libtmux/metadata"
8
8
  require "libtmux/selection"
9
9
  require "libtmux/entity"
10
10
  require "libtmux/server"
11
+ require "libtmux/support"
11
12
  require "libtmux/operations"
12
13
  require "libtmux/criteria"
13
14
  require "libtmux/capture"
data/schema/where-v1.json CHANGED
@@ -994,9 +994,16 @@
994
994
  "pane.pid": {
995
995
  "anyOf": [
996
996
  {
997
- "type": "integer",
998
- "minimum": 0,
999
- "maximum": 2147483647
997
+ "anyOf": [
998
+ {
999
+ "type": "integer",
1000
+ "minimum": 0,
1001
+ "maximum": 2147483647
1002
+ },
1003
+ {
1004
+ "type": "null"
1005
+ }
1006
+ ]
1000
1007
  },
1001
1008
  {
1002
1009
  "type": "object",
@@ -1004,9 +1011,16 @@
1004
1011
  "additionalProperties": false,
1005
1012
  "properties": {
1006
1013
  "equals": {
1007
- "type": "integer",
1008
- "minimum": 0,
1009
- "maximum": 2147483647
1014
+ "anyOf": [
1015
+ {
1016
+ "type": "integer",
1017
+ "minimum": 0,
1018
+ "maximum": 2147483647
1019
+ },
1020
+ {
1021
+ "type": "null"
1022
+ }
1023
+ ]
1010
1024
  },
1011
1025
  "not": {
1012
1026
  "$ref": "#/$defs/pane.pid"
@@ -1015,9 +1029,16 @@
1015
1029
  "type": "array",
1016
1030
  "maxItems": 1024,
1017
1031
  "items": {
1018
- "type": "integer",
1019
- "minimum": 0,
1020
- "maximum": 2147483647
1032
+ "anyOf": [
1033
+ {
1034
+ "type": "integer",
1035
+ "minimum": 0,
1036
+ "maximum": 2147483647
1037
+ },
1038
+ {
1039
+ "type": "null"
1040
+ }
1041
+ ]
1021
1042
  }
1022
1043
  },
1023
1044
  "lt": {
data/sig/fields.rbs CHANGED
@@ -28,7 +28,7 @@ module LibTmux
28
28
  def id: () -> String
29
29
  def window_id: () -> String
30
30
  def index: () -> Integer
31
- def pid: () -> Integer
31
+ def pid: () -> Integer?
32
32
  def current_command: () -> String
33
33
  def current_path: () -> String?
34
34
  def title: () -> String
data/sig/libtmux.rbs CHANGED
@@ -139,11 +139,24 @@ module LibTmux
139
139
  def send_keys: (*String, ?timeout: Numeric, ?cancel: untyped) -> CommandResult
140
140
  end
141
141
 
142
+ module Support
143
+ MINIMUM: String
144
+ def self.require_supported: (String value) -> String
145
+ def self.server_version: (Server server) -> String
146
+
147
+ module Versions
148
+ MINIMUM: String
149
+ def self.require_supported: (String value) -> String
150
+ def self.server_version: (Server server) -> String
151
+ end
152
+ end
153
+
142
154
  class Server
143
155
  attr_reader endpoint: Endpoint
144
- def self.open: [T] (?endpoint: Endpoint?, ?socket_path: String?, ?socket_name: String?, ?executable: String, ?max_requests: Integer, ?max_controls: Integer, ?close_timeout: Numeric) { (Server) -> T } -> T
145
- | (?endpoint: Endpoint?, ?socket_path: String?, ?socket_name: String?, ?executable: String, ?max_requests: Integer, ?max_controls: Integer, ?close_timeout: Numeric) -> Server
146
- def initialize: (?endpoint: Endpoint?, ?socket_path: String?, ?socket_name: String?, ?executable: String, ?max_requests: Integer, ?max_controls: Integer, ?close_timeout: Numeric) -> void
156
+ def self.open: [T] (?endpoint: Endpoint?, ?socket_path: String?, ?socket_name: String?, ?executable: String, ?max_requests: Integer, ?max_controls: Integer, ?close_timeout: Numeric, ?version_timeout: Numeric) { (Server) -> T } -> T
157
+ | (?endpoint: Endpoint?, ?socket_path: String?, ?socket_name: String?, ?executable: String, ?max_requests: Integer, ?max_controls: Integer, ?close_timeout: Numeric, ?version_timeout: Numeric) -> Server
158
+ def initialize: (?endpoint: Endpoint?, ?socket_path: String?, ?socket_name: String?, ?executable: String, ?max_requests: Integer, ?max_controls: Integer, ?close_timeout: Numeric, ?version_timeout: Numeric) -> void
159
+ def tmux_version: () -> String
147
160
  def run: (Array[String], ?input: String, ?timeout: Numeric, ?cancel: untyped) -> CommandResult
148
161
  def close: () -> nil
149
162
  def diagnostics: () -> {admitted_requests: Integer, reserved_process_slots: Integer, control_connections: Integer, closed: bool, limits: {max_requests: Integer, max_controls: Integer, close_timeout: Numeric}}
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: libtmux
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.alpha.1
4
+ version: 0.1.0.alpha.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - libtmux contributors
@@ -87,6 +87,8 @@ extra_rdoc_files: []
87
87
  files:
88
88
  - LICENSE
89
89
  - README.md
90
+ - assets/logo.png
91
+ - assets/logo.svg
90
92
  - lib/libtmux.rb
91
93
  - lib/libtmux/capture.rb
92
94
  - lib/libtmux/catalog.rb
@@ -108,7 +110,9 @@ files:
108
110
  - lib/libtmux/snapshot.rb
109
111
  - lib/libtmux/socket_readiness.rb
110
112
  - lib/libtmux/source_query.rb
113
+ - lib/libtmux/support.rb
111
114
  - lib/libtmux/terminal.rb
115
+ - lib/libtmux/tmux_version.rb
112
116
  - lib/libtmux/version.rb
113
117
  - schema/where-v1.json
114
118
  - sig/control.rbs
@@ -125,6 +129,7 @@ licenses:
125
129
  - MIT
126
130
  metadata:
127
131
  source_code_uri: https://github.com/libtmux/libtmux-ruby
132
+ documentation_uri: https://libtmux.org/en/ruby/v0.1.0.alpha.2/
128
133
  rdoc_options: []
129
134
  require_paths:
130
135
  - lib