libtmux 0.1.0 → 0.1.1

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: 79156b66bcf107fc6cb87a8c8eee64b72a87fee01297547d7ae1c1fc00f78eda
4
- data.tar.gz: dbc00e2cc431568ed740fb97abe0e2183d8179d855f06331692c201304cb8fd0
3
+ metadata.gz: e121eada829f9816c2d77a1910ae0ff9c473bb10539250a80a732156d7cf2804
4
+ data.tar.gz: 725a646a69288b57ec37d9ded5f02444d40ac8968419bf0470706d2d3ec5505d
5
5
  SHA512:
6
- metadata.gz: f3452f20558e34c94ed20ff6556f2224f2ed2f60025ad5f85bcedaba71bce596fdb22f1df3a0dc20cbd5056500884c075cc6c3d88ffcd0629e5d90b5fdde050f
7
- data.tar.gz: e636d17486684ade7616fa70f59c8b64766d5e096f388c8047b939875651a21e21095cdd79f4f35a3221b31d59ef06ed69aad5719e29d4cd28781cbc12d337c5
6
+ metadata.gz: 062be2f34e7a702808bc822350dca3bca1ef456957aab5f0885b069c518ce68b3bcfb3914b758ff1607ed98b25edef72c560e6c28ce79a5ef09ca566389261bd
7
+ data.tar.gz: 0f2434316cd39f28f75618aa47b5fe3bb122f1dcec91017d256df182fcd564801602be30c7d84d803a3d4fa829a4af632fab3b5ff315f45f6ba3f29399788236
data/README.md CHANGED
@@ -19,6 +19,9 @@ as handles, [snapshots][snapshot] you filter in plain Ruby, and
19
19
  [Guides](https://libtmux.org/en/ruby/latest/guides/) ·
20
20
  [RubyGems](https://rubygems.org/gems/libtmux)
21
21
 
22
+ [![libtmux.org](https://img.shields.io/badge/docs-libtmux.org-0b7285)](https://libtmux.org/en/ruby/)
23
+ [![libtmux](https://img.shields.io/gem/v/libtmux?label=libtmux)](https://rubygems.org/gems/libtmux)
24
+
22
25
  </div>
23
26
 
24
27
  Part of [libtmux for Ruby](https://github.com/libtmux/libtmux-ruby#readme).
@@ -338,8 +341,10 @@ connected sessions: 1
338
341
  sessions after client close: 1
339
342
  ```
340
343
 
341
- - [`Endpoint.new`][endpoint-new] takes an explicit socket path or socket name.
342
- [`Endpoint.from_env`][endpoint-from_env] is the opt-in for `$TMUX`.
344
+ - [`Endpoint.new`][endpoint-new] takes a socket path or socket name. With
345
+ neither, it picks the socket tmux would: `LIBTMUX_SOCKET_PATH`,
346
+ `LIBTMUX_SOCKET`, the server `$TMUX` names, then tmux's default socket.
347
+ [`Endpoint.from_env`][endpoint-from_env] follows `$TMUX` alone.
343
348
  - A binding keeps a private link to the socket it bound, so commands never
344
349
  reach a different daemon that later takes the same path.
345
350
  - A ref is valid on the server that issued it. Another server raises
@@ -708,8 +713,11 @@ server and lists every option.
708
713
  [Live handles][behavior-live-handles].
709
714
  - **A terminal emulator.** [`Pane#capture`][pane-capture] returns the bytes
710
715
  tmux holds for the screen, with the flags above to shape them.
711
- - **An implied server.** [`Endpoint.new`][endpoint-new] needs an explicit
712
- socket, and no API in the recipes uses the default server.
716
+ - **A server it starts on its own.** [`Server.new`][server-new] and
717
+ [`Server.open`][server-open] bind a running server and never start one. With
718
+ no socket named, [`Endpoint.new`][endpoint-new] picks the one tmux would:
719
+ `LIBTMUX_SOCKET_PATH`, `LIBTMUX_SOCKET`, the server `TMUX` names, then tmux's
720
+ default socket. The recipes name a socket or own one.
713
721
  - **Replay.** A reconnected control connection reports a gap and never replays
714
722
  requests or missed output. See [Control flow][behavior-control-flow].
715
723
  - **Transactions.** A [group][server-run_group] runs in order and keeps the
@@ -735,10 +743,11 @@ See the [changelog](../../CHANGELOG.md).
735
743
  [optional feature table](../../docs/reference/behavior.md#optional-tmux-features)
736
744
  lists the other per-release requirements.
737
745
  - **Linux, including WSL, and macOS.** The [compatibility workflow][compat]
738
- runs Ruby 3.3, 3.4 and 4.0 with tmux 3.3a through 3.8 on Ubuntu 24.04.
739
- macOS is supported on a best-effort basis, and CI coverage for it is highly
746
+ runs Ruby 3.3, 3.4 and 4.0 with tmux 3.3a through 3.8 on Ubuntu 24.04. A push
747
+ and a pull request run the oldest and newest of those releases, 3.3a and 3.8,
748
+ and a release runs all of them. macOS is supported on a best-effort basis, and CI coverage for it is highly
740
749
  limited.
741
- - **tmux master and old daemons.** On Ubuntu the workflow also runs the suite
750
+ - **tmux master and old daemons.** On Ubuntu a release also runs the suite
742
751
  against a pinned tmux master, and checks that tmux 3.0a, 3.1c, 3.2a and 3.3
743
752
  daemons are refused.
744
753
  - **Per-revision results.** Read the workflow's results for your revision
@@ -91,10 +91,10 @@ module LibTmux
91
91
  summary: "Apply main-horizontal-mirrored and main-vertical-mirrored.",
92
92
  apis: ["LibTmux::Window#select_layout"],
93
93
  commands: [Command.new(command: "select-layout")]),
94
- Capability.new(id: "layout-json", name: "JSON layout strings", since: "3.8-rc2",
94
+ Capability.new(id: "layout-json", name: "JSON layout strings", since: "3.8",
95
95
  handling: :refuse, error: ERROR,
96
96
  summary: "Apply a layout saved in the JSON form tmux reports.",
97
- apis: ["LibTmux::Window#select_layout"],
97
+ apis: ["LibTmux::Window#select_layout", "LibTmux::Server#run", "LibTmux::ControlConnection#exchange"],
98
98
  commands: [Command.new(command: "select-layout")]),
99
99
  Capability.new(id: "clipboard-query", name: "Clipboard query", since: "3.6a", handling: :refuse, error: ERROR,
100
100
  summary: "Ask an attached client's terminal for its clipboard with refresh-client -l.",
@@ -5,38 +5,52 @@ require "securerandom"
5
5
  require_relative "errors"
6
6
 
7
7
  module LibTmux
8
- # Explicit executable and socket selection. Construction starts no clients.
8
+ # Executable and socket selection. Construction starts no clients.
9
+ #
10
+ # {#initialize Endpoint.new} takes a `socket_path:` or a `socket_name:`.
11
+ # With neither, it picks the socket as tmux does when it is given none, and
12
+ # `environment:` (the process environment by default) decides which: the
13
+ # path in `LIBTMUX_SOCKET_PATH`, else the name in `LIBTMUX_SOCKET`, else the
14
+ # socket `TMUX` names when run inside tmux, else tmux's `default` socket. A
15
+ # variable that is empty counts as unset, and a socket given as an argument
16
+ # ignores the variables. {.from_env} skips the `LIBTMUX_*` variables.
9
17
  class Endpoint
10
18
  attr_reader :executable, :socket_path
11
19
 
12
- def initialize(socket_path: nil, socket_name: nil, executable: "tmux", socket_directory: nil)
13
- unless [socket_path, socket_name].count { |value| !value.nil? } == 1
14
- raise ArgumentError, "choose exactly one socket_path or socket_name"
20
+ def initialize(socket_path: nil, socket_name: nil, executable: "tmux", socket_directory: nil, environment: ENV)
21
+ unless socket_path.nil? || socket_name.nil?
22
+ raise ArgumentError, "choose at most one socket_path or socket_name"
23
+ end
24
+ origin = path_label = nil
25
+ if socket_path.nil? && socket_name.nil?
26
+ raise ArgumentError, "socket_directory applies only to socket_name" if socket_directory
27
+ socket_path, socket_name, origin = Internal::SocketChoice.call(environment)
15
28
  end
16
29
  if socket_name
17
- validate_string(socket_name, "socket_name")
30
+ label = origin || "socket_name"
31
+ validate_string(socket_name, label)
18
32
  if socket_name.include?("/") || [".", ".."].include?(socket_name)
19
- raise ArgumentError, "socket_name must be a single filename"
33
+ raise ArgumentError, "#{label} must be a single filename"
20
34
  end
21
- directory = socket_directory || ENV["TMUX_TMPDIR"] || "/tmp"
35
+ directory = socket_directory || Internal::SocketChoice.present(environment["TMUX_TMPDIR"]) || "/tmp"
22
36
  validate_string(directory, "socket_directory")
37
+ path_label = socket_directory ? "socket_directory" : "TMUX_TMPDIR"
23
38
  socket_path = File.join(directory, "tmux-#{Process.uid}", socket_name)
24
39
  elsif socket_directory
25
40
  raise ArgumentError, "socket_directory applies only to socket_name"
26
41
  end
27
- validate_string(socket_path, "socket_path")
42
+ validate_string(socket_path, origin || "socket_path")
28
43
  validate_string(executable, "executable")
29
- @socket_path = File.expand_path(socket_path).freeze
44
+ @socket_path = expand(socket_path, path_label || origin || "socket_path").freeze
30
45
  @executable = resolve_executable(executable).freeze
31
46
  freeze
32
47
  end
33
48
 
49
+ # The socket of the tmux server this process runs inside, else tmux's
50
+ # `default` socket. It ignores `LIBTMUX_SOCKET_PATH` and `LIBTMUX_SOCKET`.
34
51
  def self.from_env(env = ENV, **options)
35
- if env["TMUX"] && !env["TMUX"].empty?
36
- new(socket_path: env["TMUX"].split(",", 3).first, **options)
37
- else
38
- new(socket_name: "default", socket_directory: env["TMUX_TMPDIR"], **options)
39
- end
52
+ socket_path, socket_name = Internal::SocketChoice.tmux(env)
53
+ new(socket_path: socket_path, socket_name: socket_name, environment: env, **options)
40
54
  end
41
55
 
42
56
  def ==(other)
@@ -49,11 +63,19 @@ module LibTmux
49
63
  end
50
64
 
51
65
  def inspect
52
- "#<#{self.class} explicit Unix socket>"
66
+ "#<#{self.class} Unix socket>"
53
67
  end
54
68
 
55
69
  private
56
70
 
71
+ # File.expand_path puts the user it cannot find in its message, so the
72
+ # message here names the argument or variable instead.
73
+ def expand(path, label, base = nil)
74
+ base ? File.expand_path(path, base) : File.expand_path(path)
75
+ rescue ArgumentError
76
+ raise ArgumentError, "#{label} cannot be expanded to a path; its leading ~ names no home directory"
77
+ end
78
+
57
79
  def validate_string(value, name)
58
80
  unless value.is_a?(String) && !value.empty? && !value.include?("\0")
59
81
  raise ArgumentError, "#{name} must be a nonempty String without NUL"
@@ -62,10 +84,10 @@ module LibTmux
62
84
 
63
85
  def resolve_executable(value)
64
86
  candidates = if value.include?(File::SEPARATOR)
65
- [File.expand_path(value)]
87
+ [expand(value, "executable")]
66
88
  else
67
89
  ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).map do |directory|
68
- File.expand_path(value, directory.empty? ? Dir.pwd : directory)
90
+ expand(value, "executable", directory.empty? ? Dir.pwd : directory)
69
91
  end
70
92
  end
71
93
  candidates.find { |path| File.file?(path) && File.executable?(path) } ||
@@ -74,6 +96,37 @@ module LibTmux
74
96
  end
75
97
 
76
98
  module Internal
99
+ # Picks the socket tmux would use if it were given neither -S nor -L, from
100
+ # the variables that stand for them and the ones tmux reads itself: -S
101
+ # beats -L, and -L beats TMUX. Each pick is [path, name, variable], with
102
+ # the variable named so that a bad value is reported by name.
103
+ module SocketChoice
104
+ module_function
105
+
106
+ def call(environment)
107
+ if (path = present(environment["LIBTMUX_SOCKET_PATH"]))
108
+ [path, nil, "LIBTMUX_SOCKET_PATH"]
109
+ elsif (name = present(environment["LIBTMUX_SOCKET"]))
110
+ [nil, name, "LIBTMUX_SOCKET"]
111
+ else
112
+ tmux(environment)
113
+ end
114
+ end
115
+
116
+ # tmux ignores a TMUX that is empty or starts with a comma, and takes
117
+ # the socket path before the first comma of one that does not.
118
+ def tmux(environment)
119
+ inside = present(environment["TMUX"])
120
+ return [nil, "default", nil] if inside.nil? || inside.start_with?(",")
121
+
122
+ [inside.split(",", 2).first, nil, "TMUX"]
123
+ end
124
+
125
+ def present(value)
126
+ value if value.is_a?(String) && !value.empty?
127
+ end
128
+ end
129
+
77
130
  # Keep the socket inode alive and route through it; PID timestamps can repeat.
78
131
  class SocketIdentity
79
132
  attr_reader :key, :release
@@ -135,7 +135,7 @@ 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
- # tmux 3.8-rc2 and later report and accept saved layouts as JSON.
138
+ # tmux 3.8 and later report and accept saved layouts as JSON.
139
139
  saved = /\A[0-9a-fA-F]{4},/.match?(value)
140
140
  if !saved && value.start_with?("{")
141
141
  json = Internal::Capabilities.capability("layout-json")
@@ -317,11 +317,17 @@ module LibTmux
317
317
  end
318
318
 
319
319
  # tmux 3.3a faults on a header that is not four hex digits and a comma, so
320
- # only checksummed layouts and built-in names pass before 3.4.
320
+ # only checksummed layouts and built-in names pass before 3.4. A JSON layout
321
+ # needs tmux 3.8.
321
322
  def check_layout(item, command, release)
323
+ given = flags(item, command)
324
+ json = Internal::Capabilities.capability("layout-json")
325
+ if !json.available?(release) && operands(item, command).first.to_s.start_with?("{") && given.slice("E", "n", "p").empty?
326
+ raise UnsupportedFeatureError.new("JSON layouts require tmux #{json.since} or newer " \
327
+ "(connected version #{release.text})", phase: :admission, delivery: :not_sent)
328
+ end
322
329
  return if Internal::Capabilities.capability("layout-without-checksum").available?(release)
323
330
 
324
- given = flags(item, command)
325
331
  return if given.key?("E") || given.key?("n") || given.key?("p")
326
332
 
327
333
  layout = operands(item, command).first
@@ -32,7 +32,10 @@ module LibTmux
32
32
  #
33
33
  # **Connecting.** {#initialize Server.new} and {Server.open} bind an endpoint
34
34
  # that already exists; {Server.open} with a block closes the server when the
35
- # block ends. {Server.start} starts a private daemon instead, creates no
35
+ # block ends. Given no `endpoint:`, `socket_path:` or `socket_name:`, they
36
+ # bind the socket {Endpoint#initialize Endpoint.new} picks from
37
+ # `LIBTMUX_SOCKET_PATH`, `LIBTMUX_SOCKET`, `TMUX` and tmux's default socket,
38
+ # in that order. {Server.start} starts a private daemon instead, creates no
36
39
  # session, and owns the daemon's exit. {#endpoint} answers the {Endpoint}
37
40
  # and {#owned?} says whether this server started its daemon. {#diagnostics}
38
41
  # reports the request and control slots in use, and the limits. {#close}
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LibTmux
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.1"
5
5
  end
data/sig/libtmux.rbs CHANGED
@@ -58,7 +58,7 @@ module LibTmux
58
58
  class Endpoint
59
59
  attr_reader executable: String
60
60
  attr_reader socket_path: String
61
- def initialize: (?socket_path: String?, ?socket_name: String?, ?executable: String, ?socket_directory: String?) -> void
61
+ def initialize: (?socket_path: String?, ?socket_name: String?, ?executable: String, ?socket_directory: String?, ?environment: untyped) -> void
62
62
  def self.from_env: (?untyped, **untyped) -> Endpoint
63
63
  def ==: (untyped) -> bool
64
64
  alias eql? ==
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
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - libtmux contributors
@@ -131,7 +131,7 @@ licenses:
131
131
  - MIT
132
132
  metadata:
133
133
  source_code_uri: https://github.com/libtmux/libtmux-ruby
134
- documentation_uri: https://libtmux.org/en/ruby/v0.1.0/
134
+ documentation_uri: https://libtmux.org/en/ruby/v0.1.1/
135
135
  rdoc_options: []
136
136
  require_paths:
137
137
  - lib