iterm2_ruby 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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +23 -0
- data/Gemfile +10 -0
- data/LICENSE +21 -0
- data/README.md +265 -0
- data/Rakefile +7 -0
- data/bin/iterm2ctl +620 -0
- data/docs/api.md +523 -0
- data/docs/architecture.md +91 -0
- data/docs/cli.md +257 -0
- data/iterm2_ruby.gemspec +29 -0
- data/lib/iterm2/client.rb +690 -0
- data/lib/iterm2/connection.rb +267 -0
- data/lib/iterm2/proto/api_pb.rb +233 -0
- data/lib/iterm2/session.rb +44 -0
- data/lib/iterm2/tab.rb +39 -0
- data/lib/iterm2/version.rb +5 -0
- data/lib/iterm2/window.rb +33 -0
- data/lib/iterm2.rb +106 -0
- data/llms.txt +114 -0
- data/proto/api.proto +1642 -0
- metadata +82 -0
data/docs/api.md
ADDED
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
# API Reference
|
|
2
|
+
|
|
3
|
+
Complete reference for the `iterm2_ruby` Ruby API.
|
|
4
|
+
|
|
5
|
+
## Connection
|
|
6
|
+
|
|
7
|
+
### `ITerm2.connect(app_name: "iterm2_ruby") { |client| ... }` -> Client
|
|
8
|
+
|
|
9
|
+
Opens a connection, yields the client, auto-closes when the block returns. Without a block, returns an open client (you must call `close` yourself).
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
# Block form (preferred)
|
|
13
|
+
ITerm2.connect do |client|
|
|
14
|
+
puts client.topology.size
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Manual form
|
|
18
|
+
client = ITerm2.connect
|
|
19
|
+
client.topology
|
|
20
|
+
client.close
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### `ITerm2::Client.new(app_name: "iterm2_ruby")` -> Client
|
|
24
|
+
|
|
25
|
+
Creates a persistent client with an open connection. Equivalent to `ITerm2.connect` without a block.
|
|
26
|
+
|
|
27
|
+
### `client.close` -> nil
|
|
28
|
+
|
|
29
|
+
Closes the connection. Unsubscribes all notification listeners and stops the dispatch thread if running.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Topology
|
|
34
|
+
|
|
35
|
+
### `client.list_sessions` -> ListSessionsResponse
|
|
36
|
+
|
|
37
|
+
Returns the raw protobuf `ListSessionsResponse` from iTerm2. Includes the full window/tab/session tree with split pane hierarchy.
|
|
38
|
+
|
|
39
|
+
### `client.topology` -> Array\<Hash\>
|
|
40
|
+
|
|
41
|
+
Flattened session list. Each hash:
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
{
|
|
45
|
+
window_id: String, # e.g. "pty-0B3C..."
|
|
46
|
+
tab_id: String, # e.g. "4A7D..."
|
|
47
|
+
session_id: String, # e.g. "E8F2..."
|
|
48
|
+
title: String # e.g. "~/work/myproject"
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### `client.topology_enriched` -> Array\<Hash\>
|
|
53
|
+
|
|
54
|
+
Same as `topology` but with additional keys from `session_info`:
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
{
|
|
58
|
+
window_id:, tab_id:, session_id:, title:,
|
|
59
|
+
tty: String, # e.g. "/dev/ttys042"
|
|
60
|
+
pid: Integer, # e.g. 12345
|
|
61
|
+
cwd: String, # e.g. "/Users/you/work/project"
|
|
62
|
+
name: String, # session name
|
|
63
|
+
job: String # e.g. "ruby"
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Note:** Makes one `get_variables` RPC per session. For many sessions, this can be slow.
|
|
68
|
+
|
|
69
|
+
### `client.session_info(session_id)` -> Hash
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
{
|
|
73
|
+
tty: String | nil, # "/dev/ttys042"
|
|
74
|
+
pid: Integer | nil, # 12345
|
|
75
|
+
cwd: String | nil, # "/Users/you/work/project"
|
|
76
|
+
name: String | nil, # session name
|
|
77
|
+
job: String | nil # foreground process name
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### `client.topology_for_aggregator(mapping_file: nil)` -> Hash
|
|
82
|
+
|
|
83
|
+
JXA-compatible topology for `claude_code_history`'s SessionAggregator. Maps iTerm session GUIDs to Claude session IDs via `~/.claude/session-iterm-mapping.json`.
|
|
84
|
+
|
|
85
|
+
Returns nested `{ "windows" => [{ "tabs" => [...] }] }` matching the format from `get-iterm-topology.js`.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Session Interaction
|
|
90
|
+
|
|
91
|
+
### `client.send_text(session_id, text, suppress_broadcast: false)` -> true | false
|
|
92
|
+
|
|
93
|
+
Sends text to a session as if typed. Returns `true` on success.
|
|
94
|
+
|
|
95
|
+
- **session_id** (String) -- target session identifier
|
|
96
|
+
- **text** (String) -- text to send
|
|
97
|
+
- **suppress_broadcast** (Boolean) -- if `true`, don't broadcast to other sessions in broadcast group
|
|
98
|
+
|
|
99
|
+
**Gotcha:** Does NOT auto-append `\n`. To execute a command, include `"\n"`:
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
client.send_text(sid, "ls -la\n")
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### `client.read_screen(session_id, trailing_lines: nil)` -> Hash
|
|
106
|
+
|
|
107
|
+
Reads visible screen contents (or scrollback).
|
|
108
|
+
|
|
109
|
+
- **session_id** (String) -- target session
|
|
110
|
+
- **trailing_lines** (Integer | nil) -- number of trailing scrollback lines. `nil` = visible screen only.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
{
|
|
116
|
+
lines: Array<String>, # each line of text
|
|
117
|
+
cursor: { x: Integer, y: Integer } | nil
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Raises:** `RPCError` if the buffer request fails.
|
|
122
|
+
|
|
123
|
+
### `client.inject(session_id, data)` -> true | false
|
|
124
|
+
|
|
125
|
+
Injects data into a session as if it came from the running process (not as user input). The data appears in the terminal output without being sent to the shell.
|
|
126
|
+
|
|
127
|
+
- **session_id** (String) -- target session
|
|
128
|
+
- **data** (String) -- raw bytes to inject (will be encoded as BINARY)
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Window Management
|
|
133
|
+
|
|
134
|
+
### `client.activate_session(session_id, select_tab: true, order_window_front: true)` -> true | false
|
|
135
|
+
|
|
136
|
+
Raises and focuses a session. Selects its tab and brings the window to front by default.
|
|
137
|
+
|
|
138
|
+
### `client.activate_tab(tab_id, order_window_front: true)` -> true | false
|
|
139
|
+
|
|
140
|
+
Raises a tab by ID.
|
|
141
|
+
|
|
142
|
+
### `client.activate_window(window_id)` -> true | false
|
|
143
|
+
|
|
144
|
+
Raises a window by ID. Always orders window to front.
|
|
145
|
+
|
|
146
|
+
### `client.raise_by_title(pattern)` -> true | false
|
|
147
|
+
|
|
148
|
+
Finds the first session whose title matches `pattern` (case-insensitive regex) and activates it.
|
|
149
|
+
|
|
150
|
+
**Raises:** `NotFoundError` if no session matches.
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
client.raise_by_title("my-project") # substring match
|
|
154
|
+
client.raise_by_title("^claude") # regex anchor
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### `client.raise_by_cwd(pattern)` -> true | false
|
|
158
|
+
|
|
159
|
+
Finds the first session whose working directory matches `pattern` (case-insensitive regex) and activates it.
|
|
160
|
+
|
|
161
|
+
**Raises:** `NotFoundError` if no session matches.
|
|
162
|
+
|
|
163
|
+
**Note:** Calls `topology_enriched` internally, so it makes one `get_variables` RPC per session.
|
|
164
|
+
|
|
165
|
+
### `client.create_tab(window_id: nil, profile_name: nil)` -> Hash
|
|
166
|
+
|
|
167
|
+
Creates a new tab (or window if `window_id` is nil and iTerm2 decides to).
|
|
168
|
+
|
|
169
|
+
- **window_id** (String | nil) -- create tab in this window, or new window if nil
|
|
170
|
+
- **profile_name** (String | nil) -- use this profile
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
{ window_id: String, tab_id: String, session_id: String }
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Raises:** `RPCError` on failure.
|
|
179
|
+
|
|
180
|
+
### `client.split_pane(session_id, vertical: true, profile_name: nil, profile_customizations: {})` -> String
|
|
181
|
+
|
|
182
|
+
Splits a pane. Returns the new session ID.
|
|
183
|
+
|
|
184
|
+
- **session_id** (String) -- session to split
|
|
185
|
+
- **vertical** (Boolean) -- `true` for vertical split, `false` for horizontal
|
|
186
|
+
- **profile_name** (String | nil) -- profile for the new pane
|
|
187
|
+
- **profile_customizations** (Hash) -- profile property overrides (key => value)
|
|
188
|
+
|
|
189
|
+
**Raises:** `RPCError` on failure.
|
|
190
|
+
|
|
191
|
+
### `client.close_session(session_id, force: false)` -> true | false
|
|
192
|
+
|
|
193
|
+
Closes a session. With `force: true`, skips the confirmation prompt.
|
|
194
|
+
|
|
195
|
+
**Gotcha:** iTerm2 does NOT fire `NOTIFY_ON_TERMINATE_SESSION` for API-initiated closes.
|
|
196
|
+
|
|
197
|
+
### `client.close_tab(tab_id, force: false)` -> true | false
|
|
198
|
+
|
|
199
|
+
Closes an entire tab. With `force: true`, skips the confirmation prompt.
|
|
200
|
+
|
|
201
|
+
### `client.reorder_tabs(assignments)` -> true
|
|
202
|
+
|
|
203
|
+
Moves and reorders tabs across windows. `assignments` is a Hash of `{ window_id => [tab_ids] }`. Each entry defines the complete tab order for that window. A tab can be moved from one window to another by including it in the target window's list.
|
|
204
|
+
|
|
205
|
+
```ruby
|
|
206
|
+
# Move tab_b to window_1, placing it after tab_a
|
|
207
|
+
client.reorder_tabs("window_1" => ["tab_a", "tab_b"])
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**Raises:** `RPCError` with status `INVALID_ASSIGNMENT` (duplicate tab ID), `INVALID_WINDOW_ID`, or `INVALID_TAB_ID`.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Profile & Properties
|
|
215
|
+
|
|
216
|
+
### `client.set_profile_property(session_id, key, value)` -> true | false
|
|
217
|
+
|
|
218
|
+
Sets a profile property on a session. The value is JSON-encoded automatically.
|
|
219
|
+
|
|
220
|
+
```ruby
|
|
221
|
+
client.set_profile_property(sid, "Background Color", { "Red Component" => 0.1, ... })
|
|
222
|
+
client.set_profile_property(sid, "Name", "My Custom Profile")
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### `client.get_profile_property(session_id, *keys)` -> Hash
|
|
226
|
+
|
|
227
|
+
Gets profile properties. Returns a hash of `{ key => value }`. With no keys, returns all properties.
|
|
228
|
+
|
|
229
|
+
```ruby
|
|
230
|
+
client.get_profile_property(sid, "Name", "Guid")
|
|
231
|
+
# => {"Name" => "Default", "Guid" => "ABC-123"}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**Raises:** `RPCError` on failure.
|
|
235
|
+
|
|
236
|
+
### `client.list_profiles(properties: nil, guids: nil)` -> Array\<Hash\>
|
|
237
|
+
|
|
238
|
+
Lists all iTerm2 profiles. Each profile is a hash of `{ key => value }`.
|
|
239
|
+
|
|
240
|
+
- **properties** (Array\<String\> | nil) -- only include these property keys. `nil` = all.
|
|
241
|
+
- **guids** (Array\<String\> | nil) -- only include profiles with these GUIDs
|
|
242
|
+
|
|
243
|
+
```ruby
|
|
244
|
+
client.list_profiles(properties: ["Name", "Guid"])
|
|
245
|
+
# => [{"Name" => "Default", "Guid" => "..."}, ...]
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### `client.get_property(name, session_id: nil, window_id: nil)` -> Object
|
|
249
|
+
|
|
250
|
+
Gets a named property from a session or window. Returns the JSON-decoded value.
|
|
251
|
+
|
|
252
|
+
```ruby
|
|
253
|
+
client.get_property("columns", session_id: sid) # => 120
|
|
254
|
+
client.get_property("frame", window_id: wid) # => {"origin" => {...}, "size" => {...}}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**Raises:** `RPCError` on failure.
|
|
258
|
+
|
|
259
|
+
### `client.get_variables(*names, session_id: nil, tab_id: nil, window_id: nil, app: nil)` -> varies
|
|
260
|
+
|
|
261
|
+
Gets variables from a scope (session, tab, window, or app).
|
|
262
|
+
|
|
263
|
+
- With `"*"` -- returns Hash of all variables
|
|
264
|
+
- With one name -- returns the value directly
|
|
265
|
+
- With multiple names -- returns Hash of `{ name => value }`
|
|
266
|
+
|
|
267
|
+
Scope is required -- pass exactly one of `session_id:`, `tab_id:`, `window_id:`, or `app: true`.
|
|
268
|
+
|
|
269
|
+
```ruby
|
|
270
|
+
client.get_variables("path", session_id: sid) # => "/Users/you/work"
|
|
271
|
+
client.get_variables("*", app: true) # => {"effectiveTheme" => "dark", ...}
|
|
272
|
+
client.get_variables("tty", "pid", session_id: sid) # => {"tty" => "/dev/ttys042", "pid" => 123}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
**Raises:** `RPCError` on failure, `ArgumentError` if no scope given.
|
|
276
|
+
|
|
277
|
+
### `client.set_variables(vars, session_id: nil, tab_id: nil, window_id: nil, app: nil)` -> true | false
|
|
278
|
+
|
|
279
|
+
Sets user-defined variables. Variable names must begin with `"user."`.
|
|
280
|
+
|
|
281
|
+
```ruby
|
|
282
|
+
client.set_variables({ "user.project" => "myapp" }, session_id: sid)
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### `client.get_variable(name, **scope)` -> Object
|
|
286
|
+
|
|
287
|
+
Convenience wrapper for `get_variables` with a single name.
|
|
288
|
+
|
|
289
|
+
### `client.focus` -> Hash
|
|
290
|
+
|
|
291
|
+
Returns the current focus state:
|
|
292
|
+
|
|
293
|
+
```ruby
|
|
294
|
+
{
|
|
295
|
+
active_session: String | nil, # currently focused session ID
|
|
296
|
+
active_tab: String | nil, # currently selected tab ID
|
|
297
|
+
active_window: String | nil, # key window ID
|
|
298
|
+
app_active: Boolean # whether iTerm2 is the frontmost app
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### `client.get_prompt(session_id)` -> Hash
|
|
303
|
+
|
|
304
|
+
Gets the shell prompt state for a session (requires shell integration).
|
|
305
|
+
|
|
306
|
+
```ruby
|
|
307
|
+
{
|
|
308
|
+
state: Symbol, # :editing, :running, :at_prompt, :unavailable
|
|
309
|
+
command: String | nil, # last/current command
|
|
310
|
+
working_directory: String | nil, # shell's cwd
|
|
311
|
+
exit_status: Integer | nil # last command's exit code
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
**Gotcha:** Returns `{ state: :unavailable, ... }` for sessions without iTerm2 shell integration installed.
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## Notifications
|
|
320
|
+
|
|
321
|
+
Notifications use a background dispatch loop. The first call to any `subscribe` or `on_*` method starts the dispatch thread automatically.
|
|
322
|
+
|
|
323
|
+
### Lifecycle
|
|
324
|
+
|
|
325
|
+
1. Call `on_*` or `subscribe` -- dispatch thread starts
|
|
326
|
+
2. Events arrive and fire callbacks on the dispatch thread
|
|
327
|
+
3. Call `client.close` -- unsubscribes all, stops dispatch thread
|
|
328
|
+
|
|
329
|
+
### `client.subscribe(notification_type, session_id: nil, &callback)` -> token
|
|
330
|
+
|
|
331
|
+
Low-level subscribe. `notification_type` is a symbol like `:NOTIFY_ON_FOCUS_CHANGE`. Returns a token for `unsubscribe`.
|
|
332
|
+
|
|
333
|
+
- **session_id** (String | nil) -- scope to a specific session, or `nil` for global
|
|
334
|
+
|
|
335
|
+
**Raises:** `SubscriptionError` on failure.
|
|
336
|
+
|
|
337
|
+
### `client.unsubscribe(token)` -> nil
|
|
338
|
+
|
|
339
|
+
Unsubscribes a previously created subscription.
|
|
340
|
+
|
|
341
|
+
### `client.on_focus_change { |event| }` -> token
|
|
342
|
+
|
|
343
|
+
Fires when focus changes (session, tab, window, or app activation).
|
|
344
|
+
|
|
345
|
+
```ruby
|
|
346
|
+
# event shape:
|
|
347
|
+
{
|
|
348
|
+
type: :focus,
|
|
349
|
+
app_active: Boolean, # optional
|
|
350
|
+
window: String, # optional, window ID
|
|
351
|
+
window_status: Symbol, # optional, :terminal_window_became_key, etc.
|
|
352
|
+
selected_tab: String, # optional, tab ID
|
|
353
|
+
session: String # optional, session ID
|
|
354
|
+
}
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### `client.on_new_session { |event| }` -> token
|
|
358
|
+
|
|
359
|
+
Fires when a new session is created.
|
|
360
|
+
|
|
361
|
+
```ruby
|
|
362
|
+
{ type: :new_session, session_id: String }
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### `client.on_session_terminated { |event| }` -> token
|
|
366
|
+
|
|
367
|
+
Fires when a session terminates.
|
|
368
|
+
|
|
369
|
+
```ruby
|
|
370
|
+
{ type: :session_terminated, session_id: String }
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
**Gotcha:** Does NOT fire for sessions closed via the API (`close_session`/`close_tab`). Only fires for user-initiated closes or process exits.
|
|
374
|
+
|
|
375
|
+
### `client.on_prompt_change(session_id) { |event| }` -> token
|
|
376
|
+
|
|
377
|
+
Fires when prompt state changes in a session (requires shell integration).
|
|
378
|
+
|
|
379
|
+
```ruby
|
|
380
|
+
# event shapes (varies by state):
|
|
381
|
+
{ type: :prompt, session: String, state: :prompt, unique_prompt_id: String }
|
|
382
|
+
{ type: :prompt, session: String, state: :command_start, command: String }
|
|
383
|
+
{ type: :prompt, session: String, state: :command_end, exit_status: Integer }
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### `client.on_screen_update(session_id) { |event| }` -> token
|
|
387
|
+
|
|
388
|
+
Fires when screen content changes in a session.
|
|
389
|
+
|
|
390
|
+
```ruby
|
|
391
|
+
{ type: :screen_update, session: String }
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### `client.on_layout_change { |event| }` -> token
|
|
395
|
+
|
|
396
|
+
Fires when window/tab layout changes (splits, tab reorder, etc.).
|
|
397
|
+
|
|
398
|
+
```ruby
|
|
399
|
+
{ type: :layout_change }
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
**Gotcha:** iTerm2 sends duplicate notification events (each event arrives twice). This is server-side behavior, not a bug.
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
406
|
+
## One-Shot Module Methods
|
|
407
|
+
|
|
408
|
+
These class methods on `ITerm2` open a connection, run the command, and close. Convenient for single operations but inefficient for multiple calls (each opens a new connection).
|
|
409
|
+
|
|
410
|
+
| One-shot method | Equivalent |
|
|
411
|
+
|---|---|
|
|
412
|
+
| `ITerm2.topology` | `client.topology` |
|
|
413
|
+
| `ITerm2.topology_enriched` | `client.topology_enriched` |
|
|
414
|
+
| `ITerm2.list_sessions` | `client.list_sessions` |
|
|
415
|
+
| `ITerm2.session_info(sid)` | `client.session_info(sid)` |
|
|
416
|
+
| `ITerm2.send_text(sid, text)` | `client.send_text(sid, text)` |
|
|
417
|
+
| `ITerm2.read_screen(sid)` | `client.read_screen(sid)` |
|
|
418
|
+
| `ITerm2.inject(sid, data)` | `client.inject(sid, data)` |
|
|
419
|
+
| `ITerm2.activate_session(sid)` | `client.activate_session(sid)` |
|
|
420
|
+
| `ITerm2.raise_by_title(pat)` | `client.raise_by_title(pat)` |
|
|
421
|
+
| `ITerm2.raise_by_cwd(pat)` | `client.raise_by_cwd(pat)` |
|
|
422
|
+
| `ITerm2.focus` | `client.focus` |
|
|
423
|
+
| `ITerm2.get_prompt(sid)` | `client.get_prompt(sid)` |
|
|
424
|
+
| `ITerm2.get_variable(name, **scope)` | `client.get_variable(name, **scope)` |
|
|
425
|
+
| `ITerm2.get_profile_property(sid, *keys)` | `client.get_profile_property(sid, *keys)` |
|
|
426
|
+
| `ITerm2.list_profiles(...)` | `client.list_profiles(...)` |
|
|
427
|
+
|
|
428
|
+
**Note:** Notifications (`on_*`) are NOT available in one-shot mode. They require a persistent client.
|
|
429
|
+
|
|
430
|
+
---
|
|
431
|
+
|
|
432
|
+
## Common Patterns
|
|
433
|
+
|
|
434
|
+
### Get active session and read its screen
|
|
435
|
+
|
|
436
|
+
```ruby
|
|
437
|
+
ITerm2.connect do |client|
|
|
438
|
+
active = client.focus[:active_session]
|
|
439
|
+
screen = client.read_screen(active)
|
|
440
|
+
puts screen[:lines].join("\n")
|
|
441
|
+
end
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### Find session by title and send a command
|
|
445
|
+
|
|
446
|
+
```ruby
|
|
447
|
+
ITerm2.connect do |client|
|
|
448
|
+
match = client.topology.find { |s| s[:title] =~ /my-project/i }
|
|
449
|
+
client.send_text(match[:session_id], "make test\n") if match
|
|
450
|
+
end
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
### Monitor sessions for changes
|
|
454
|
+
|
|
455
|
+
```ruby
|
|
456
|
+
ITerm2.connect do |client|
|
|
457
|
+
client.on_new_session { |e| puts "New: #{e[:session_id]}" }
|
|
458
|
+
client.on_session_terminated { |e| puts "Gone: #{e[:session_id]}" }
|
|
459
|
+
client.on_focus_change { |e| puts "Focus: #{e}" }
|
|
460
|
+
sleep
|
|
461
|
+
end
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
### Batch query all session details
|
|
465
|
+
|
|
466
|
+
```ruby
|
|
467
|
+
ITerm2.connect do |client|
|
|
468
|
+
client.topology_enriched.each do |s|
|
|
469
|
+
puts "#{s[:session_id]} | #{s[:title]} | #{s[:cwd]} | #{s[:job]}"
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### Wait for a command to finish (shell integration required)
|
|
475
|
+
|
|
476
|
+
```ruby
|
|
477
|
+
ITerm2.connect do |client|
|
|
478
|
+
sid = client.focus[:active_session]
|
|
479
|
+
client.send_text(sid, "sleep 3\n")
|
|
480
|
+
|
|
481
|
+
done = Queue.new
|
|
482
|
+
token = client.on_prompt_change(sid) do |e|
|
|
483
|
+
done.push(e) if e[:state] == :command_end
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
result = done.pop
|
|
487
|
+
puts "Exit status: #{result[:exit_status]}"
|
|
488
|
+
client.unsubscribe(token)
|
|
489
|
+
end
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
---
|
|
493
|
+
|
|
494
|
+
## Error Classes
|
|
495
|
+
|
|
496
|
+
All errors inherit from `ITerm2::Error`.
|
|
497
|
+
|
|
498
|
+
| Class | When |
|
|
499
|
+
|---|---|
|
|
500
|
+
| `ITerm2::Error` | Base class |
|
|
501
|
+
| `ITerm2::ConnectionError` | Can't connect to iTerm2 (not running, API disabled) |
|
|
502
|
+
| `ITerm2::AuthError` | osascript authentication failed |
|
|
503
|
+
| `ITerm2::RPCError` | iTerm2 returned an error status for an RPC |
|
|
504
|
+
| `ITerm2::NotFoundError` | `raise_by_title`/`raise_by_cwd` found no match (subclass of RPCError) |
|
|
505
|
+
| `ITerm2::SubscriptionError` | Notification subscription failed |
|
|
506
|
+
|
|
507
|
+
---
|
|
508
|
+
|
|
509
|
+
## Gotchas
|
|
510
|
+
|
|
511
|
+
1. **`send_text` does not append `\n`** -- you must include it to execute a command. The CLI auto-appends it.
|
|
512
|
+
|
|
513
|
+
2. **Duplicate notifications** -- iTerm2 sends each notification event twice. This is server-side behavior.
|
|
514
|
+
|
|
515
|
+
3. **One-shot mode opens a new connection per call** -- use `ITerm2.connect { |c| ... }` for multiple operations.
|
|
516
|
+
|
|
517
|
+
4. **`close_session` doesn't fire termination notifications** -- iTerm2 only fires `NOTIFY_ON_TERMINATE_SESSION` for user-initiated closes and process exits.
|
|
518
|
+
|
|
519
|
+
5. **`get_prompt` returns `:unavailable`** -- sessions without iTerm2 shell integration return `{ state: :unavailable }`.
|
|
520
|
+
|
|
521
|
+
6. **`topology_enriched` is slow with many sessions** -- makes one RPC per session. Use plain `topology` when you only need IDs and titles.
|
|
522
|
+
|
|
523
|
+
7. **Variable scope is required** -- `get_variables`/`set_variables` raise `ArgumentError` if you don't specify `session_id:`, `tab_id:`, `window_id:`, or `app: true`.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
How `iterm2_ruby` connects to and communicates with iTerm2.
|
|
4
|
+
|
|
5
|
+
## Connection Lifecycle
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
osascript (one-time auth)
|
|
9
|
+
|
|
|
10
|
+
v
|
|
11
|
+
Unix socket (~/.../iTerm2/private/socket) --or--> TCP localhost:1912
|
|
12
|
+
|
|
|
13
|
+
v
|
|
14
|
+
WebSocket upgrade (RFC 6455, subprotocol: api.iterm2.com)
|
|
15
|
+
|
|
|
16
|
+
v
|
|
17
|
+
Protobuf RPC over binary WebSocket frames
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
1. **Authentication**: A single `osascript` call asks iTerm2 for a cookie and key pair. This triggers an iTerm2 confirmation dialog on first use for each `app_name`.
|
|
21
|
+
|
|
22
|
+
2. **Socket**: Prefers the Unix domain socket at `~/Library/Application Support/iTerm2/private/socket`. Falls back to TCP on `127.0.0.1:1912` if the socket file doesn't exist.
|
|
23
|
+
|
|
24
|
+
3. **WebSocket handshake**: Standard HTTP upgrade with custom iTerm2 headers (`x-iterm2-cookie`, `x-iterm2-key`, `x-iterm2-library-version`, `x-iterm2-advisory-name`). The framing is hand-rolled per RFC 6455 -- no external WebSocket gem.
|
|
25
|
+
|
|
26
|
+
4. **Ready**: After the `101 Switching Protocols` response, the connection is ready for protobuf RPCs.
|
|
27
|
+
|
|
28
|
+
## Protobuf Protocol
|
|
29
|
+
|
|
30
|
+
All messages use Google Protocol Buffers, defined in iTerm2's [`api.proto`](https://github.com/gnachman/iTerm2/blob/master/proto/api.proto).
|
|
31
|
+
|
|
32
|
+
- **Client -> Server**: `ClientOriginatedMessage` with an `id` field and a oneof request field (e.g., `list_sessions_request`, `send_text_request`)
|
|
33
|
+
- **Server -> Client**: `ServerOriginatedMessage` with a matching `id` and a oneof response field, OR a `notification` field for async events
|
|
34
|
+
|
|
35
|
+
Ruby bindings are generated with `protoc --ruby_out` from `api.proto`. The generated module is `Iterm2` (lowercase t), aliased as `ITerm2::Proto` in `lib/iterm2.rb`.
|
|
36
|
+
|
|
37
|
+
## Sync vs Dispatch Mode
|
|
38
|
+
|
|
39
|
+
The connection has two operating modes:
|
|
40
|
+
|
|
41
|
+
### Sync Mode (default)
|
|
42
|
+
|
|
43
|
+
Simple request-response. `rpc_sync` sends a frame and blocks reading the next frame as the response. Used when no notifications are active.
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
Thread: send(request) --> recv(response) --> return
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Dispatch Mode (activated by first notification subscription)
|
|
50
|
+
|
|
51
|
+
A background reader thread runs continuously, routing incoming frames:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
Main thread: send(request) --> wait on Queue --> return response
|
|
55
|
+
Reader thread: recv(frame) --> RPC response? --> push to request's Queue
|
|
56
|
+
--> notification? --> fire subscriber callbacks
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The dispatch loop starts automatically on the first `subscribe` or `on_*` call. It stops when `client.close` is called.
|
|
60
|
+
|
|
61
|
+
## Threading Model
|
|
62
|
+
|
|
63
|
+
| Mutex | Protects |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `@mutex` | `@pending_responses` hash (request ID -> Queue) and `@id_counter` |
|
|
66
|
+
| `@write_mutex` | Socket writes (only one thread writes at a time) |
|
|
67
|
+
| `@subscriber_mutex` | `@subscribers` hash (callbacks for each notification type) |
|
|
68
|
+
|
|
69
|
+
The reader thread (`dispatch_loop`) is the only thread that reads from the socket in dispatch mode. RPC responses are delivered to the calling thread via a per-request `Queue`.
|
|
70
|
+
|
|
71
|
+
## Notification Dispatch
|
|
72
|
+
|
|
73
|
+
Subscribers are keyed by `[session_id, notification_type]`:
|
|
74
|
+
|
|
75
|
+
- **Session-specific**: `[session_id, :NOTIFY_ON_PROMPT]` -- only fires for that session
|
|
76
|
+
- **Global**: `[nil, :NOTIFY_ON_FOCUS_CHANGE]` -- fires for all events of that type
|
|
77
|
+
|
|
78
|
+
When a notification arrives, the dispatch loop checks session-specific subscribers first, then global. Both are called if both exist.
|
|
79
|
+
|
|
80
|
+
Notification type detection uses `has_*?` methods on the protobuf `Notification` message (e.g., `has_focus_changed_notification?`) rather than checking `submessage`, because the protobuf oneof accessor is more reliable.
|
|
81
|
+
|
|
82
|
+
## File Layout
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
lib/iterm2.rb # Entry point, ITerm2.connect, one-shot methods, error classes
|
|
86
|
+
lib/iterm2/version.rb # VERSION constant
|
|
87
|
+
lib/iterm2/connection.rb # WebSocket + auth + sync/dispatch RPC
|
|
88
|
+
lib/iterm2/client.rb # High-level API (all public methods)
|
|
89
|
+
lib/iterm2/proto/api_pb.rb # protoc-generated bindings from api.proto
|
|
90
|
+
bin/iterm2ctl # CLI entry point
|
|
91
|
+
```
|