muxr 0.1.10 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,102 @@
1
+ require "json"
2
+ require "socket"
3
+
4
+ module Muxr
5
+ # Enumerates the panes of every muxr server currently running on this
6
+ # machine by asking each one over its control socket. Used to populate the
7
+ # attach picker, which is the only place muxr looks outside its own process.
8
+ #
9
+ # Every query is best-effort and deadline-bounded: a session whose server is
10
+ # wedged, mid-shutdown, or newer/older than us simply doesn't appear in the
11
+ # list rather than hanging the event loop that is asking.
12
+ module SessionDirectory
13
+ QUERY_TIMEOUT = 0.5
14
+
15
+ Entry = Struct.new(:session, :socket_path, :pane_id, :slot, :cwd, :rows, :cols, :focused, :name, keyword_init: true) do
16
+ def label
17
+ "#{session}:#{pane_id}"
18
+ end
19
+ end
20
+
21
+ # Session names with a live server, paired with their control socket path.
22
+ # A name is only reported when both its TTY socket and its control socket
23
+ # accept a connection, which is what makes a session actually attachable.
24
+ def self.live_sessions
25
+ dir = Application::SOCKETS_DIR
26
+ return {} unless File.directory?(dir)
27
+ Dir.children(dir).sort.each_with_object({}) do |entry, acc|
28
+ next unless entry.end_with?(".sock")
29
+ next if entry.end_with?(".ctrl.sock")
30
+ name = File.basename(entry, ".sock")
31
+ control = Application.control_socket_path_for(name)
32
+ next unless Application.alive_socket?(File.join(dir, entry))
33
+ next unless Application.alive_socket?(control)
34
+ acc[name] = control
35
+ end
36
+ end
37
+
38
+ # Every non-private pane of every live session other than +exclude+, in
39
+ # session then slot order. Private panes are omitted for the same reason
40
+ # the MCP surface hides them: the user marked them not-for-sharing.
41
+ def self.panes(exclude: nil)
42
+ live_sessions.flat_map do |name, control|
43
+ next [] if name == exclude
44
+ list = query(control, "panes.list")
45
+ next [] unless list
46
+ (list["panes"] || []).filter_map do |pane|
47
+ next if pane["private"]
48
+ next unless pane["alive"]
49
+ Entry.new(
50
+ session: name,
51
+ socket_path: control,
52
+ pane_id: pane["id"].to_s,
53
+ slot: pane["slot"],
54
+ cwd: pane["cwd"],
55
+ rows: pane["rows"],
56
+ cols: pane["cols"],
57
+ focused: !!pane["focused"],
58
+ name: pane["name"]
59
+ )
60
+ end
61
+ end
62
+ end
63
+
64
+ def self.query(control_path, method, params = {})
65
+ socket = UNIXSocket.new(control_path)
66
+ begin
67
+ socket.write(JSON.generate("id" => 1, "method" => method, "params" => params) + "\n")
68
+ read_response(socket)
69
+ ensure
70
+ socket.close rescue nil
71
+ end
72
+ rescue SystemCallError, IOError
73
+ nil
74
+ end
75
+
76
+ def self.read_response(socket)
77
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + QUERY_TIMEOUT
78
+ buffer = +""
79
+ loop do
80
+ while (line = buffer.slice!(/\A.*\n/))
81
+ msg = begin
82
+ JSON.parse(line)
83
+ rescue JSON::ParserError
84
+ next
85
+ end
86
+ next unless msg["id"] == 1
87
+ return msg["result"]
88
+ end
89
+ remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
90
+ return nil if remaining <= 0
91
+ return nil unless IO.select([socket], nil, nil, remaining)
92
+ begin
93
+ buffer << socket.read_nonblock(4096)
94
+ rescue IO::WaitReadable
95
+ next
96
+ end
97
+ end
98
+ rescue EOFError, SystemCallError, IOError
99
+ nil
100
+ end
101
+ end
102
+ end