Class: ITerm2::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/iterm2/client.rb

Instance Method Summary collapse

Constructor Details

#initialize(app_name: "iterm2_ruby") ⇒ Client

Returns a new instance of Client.



7
8
9
10
11
# File 'lib/iterm2/client.rb', line 7

def initialize(app_name: "iterm2_ruby")
  @connection = Connection.new(app_name: app_name)
  @subscribers = {}
  @subscriber_mutex = Mutex.new
end

Instance Method Details

#activate_session(session_id, select_tab: true, order_window_front: true) ⇒ Object

--- Activate (raise) ---



90
91
92
93
94
95
96
97
98
99
# File 'lib/iterm2/client.rb', line 90

def activate_session(session_id, select_tab: true, order_window_front: true)
  response = request(:activate_request, Proto::ActivateRequest.new(
    session_id: session_id,
    select_tab: select_tab,
    select_session: true,
    order_window_front: order_window_front,
    activate_app: Proto::ActivateRequest::App.new(raise_all_windows: false, ignoring_other_apps: true)
  ))
  response.activate_response.status == :OK
end

#activate_tab(tab_id, order_window_front: true) ⇒ Object



101
102
103
104
105
106
107
108
# File 'lib/iterm2/client.rb', line 101

def activate_tab(tab_id, order_window_front: true)
  response = request(:activate_request, Proto::ActivateRequest.new(
    tab_id: tab_id,
    order_window_front: order_window_front,
    activate_app: Proto::ActivateRequest::App.new(raise_all_windows: false, ignoring_other_apps: true)
  ))
  response.activate_response.status == :OK
end

#activate_window(window_id) ⇒ Object



110
111
112
113
114
115
116
117
# File 'lib/iterm2/client.rb', line 110

def activate_window(window_id)
  response = request(:activate_request, Proto::ActivateRequest.new(
    window_id: window_id,
    order_window_front: true,
    activate_app: Proto::ActivateRequest::App.new(raise_all_windows: false, ignoring_other_apps: true)
  ))
  response.activate_response.status == :OK
end

#closeObject



13
14
15
16
# File 'lib/iterm2/client.rb', line 13

def close
  unsubscribe_all
  @connection.close
end

#close_session(session_id, force: false) ⇒ Object

--- Close ---



177
178
179
180
181
182
183
# File 'lib/iterm2/client.rb', line 177

def close_session(session_id, force: false)
  response = request(:close_request, Proto::CloseRequest.new(
    sessions: Proto::CloseRequest::CloseSessions.new(session_ids: [session_id]),
    force: force
  ))
  response.close_response.statuses.first == :OK
end

#close_tab(tab_id, force: false) ⇒ Object



185
186
187
188
189
190
191
# File 'lib/iterm2/client.rb', line 185

def close_tab(tab_id, force: false)
  response = request(:close_request, Proto::CloseRequest.new(
    tabs: Proto::CloseRequest::CloseTabs.new(tab_ids: [tab_id]),
    force: force
  ))
  response.close_response.statuses.first == :OK
end

#create_tab(window_id: nil, profile_name: nil) ⇒ Object

--- CreateTab ---

Raises:



130
131
132
133
134
135
136
137
138
139
# File 'lib/iterm2/client.rb', line 130

def create_tab(window_id: nil, profile_name: nil)
  req = Proto::CreateTabRequest.new
  req.window_id = window_id if window_id
  req.profile_name = profile_name if profile_name

  resp = request(:create_tab_request, req).create_tab_response
  raise RPCError, "CreateTab failed: #{resp.status}" unless resp.status == :OK

  { window_id: resp.window_id, tab_id: resp.tab_id, session_id: resp.session_id }
end

#focusObject

--- Focus ---



362
363
364
# File 'lib/iterm2/client.rb', line 362

def focus
  parse_focus_response(request(:focus_request, Proto::FocusRequest.new).focus_response)
end

#get_profile_property(session_id, *keys) ⇒ Object

--- GetProfileProperty ---

Raises:



327
328
329
330
331
332
333
334
335
# File 'lib/iterm2/client.rb', line 327

def get_profile_property(session_id, *keys)
  req = Proto::GetProfilePropertyRequest.new(session: session_id)
  req.keys.replace(keys) unless keys.empty?

  resp = request(:get_profile_property_request, req).get_profile_property_response
  raise RPCError, "GetProfileProperty failed: #{resp.status}" unless resp.status == :OK

  resp.properties.to_h { |p| [p.key, JSON.parse(p.json_value)] }
end

#get_prompt(session_id) ⇒ Object

--- GetPrompt ---



368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
# File 'lib/iterm2/client.rb', line 368

def get_prompt(session_id)
  resp = request(:get_prompt_request, Proto::GetPromptRequest.new(session: session_id)).get_prompt_response

  case resp.status
  when :OK
    {
      state: resp.prompt_state.to_s.downcase.to_sym,
      command: resp.command.empty? ? nil : resp.command,
      working_directory: resp.working_directory.empty? ? nil : resp.working_directory,
      exit_status: resp.exit_status
    }
  when :PROMPT_UNAVAILABLE
    { state: :unavailable, command: nil, working_directory: nil, exit_status: nil }
  else
    raise RPCError, "GetPrompt failed: #{resp.status}"
  end
end

#get_property(name, session_id: nil, window_id: nil) ⇒ Object

--- GetProperty ---

Raises:



388
389
390
391
392
393
394
395
396
397
# File 'lib/iterm2/client.rb', line 388

def get_property(name, session_id: nil, window_id: nil)
  req = Proto::GetPropertyRequest.new(name: name)
  req.session_id = session_id if session_id
  req.window_id = window_id if window_id

  resp = request(:get_property_request, req).get_property_response
  raise RPCError, "GetProperty failed: #{resp.status}" unless resp.status == :OK

  JSON.parse(resp.json_value)
end

#get_variable(name, **scope) ⇒ Object

Convenience: get a single variable



239
240
241
# File 'lib/iterm2/client.rb', line 239

def get_variable(name, **scope)
  get_variables(name, **scope)
end

#get_variables(*names, session_id: nil, tab_id: nil, window_id: nil, app: nil) ⇒ Object

Get one or more variables from a session, tab, window, or app scope Use "*" to get all variables as a hash

Raises:



212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/iterm2/client.rb', line 212

def get_variables(*names, session_id: nil, tab_id: nil, window_id: nil, app: nil)
  req = Proto::VariableRequest.new(get: names)
  set_variable_scope!(req, session_id: session_id, tab_id: tab_id, window_id: window_id, app: app)

  resp = request(:variable_request, req).variable_response
  raise RPCError, "GetVariables failed: #{resp.status}" unless resp.status == :OK

  if names == ["*"]
    JSON.parse(resp.values.first || "{}")
  elsif names.size == 1
    val = resp.values.first
    val == "null" ? nil : JSON.parse(val)
  else
    names.zip(resp.values).to_h { |name, val| [name, val == "null" ? nil : JSON.parse(val)] }
  end
end

#inject(session_id, data) ⇒ Object

--- Inject ---



352
353
354
355
356
357
358
# File 'lib/iterm2/client.rb', line 352

def inject(session_id, data)
  resp = request(:inject_request, Proto::InjectRequest.new(
    session_id: [session_id],
    data: data.encode("BINARY")
  )).inject_response
  resp.status.first == :OK
end

#list_profiles(properties: nil, guids: nil) ⇒ Object

--- ListProfiles ---



339
340
341
342
343
344
345
346
347
348
# File 'lib/iterm2/client.rb', line 339

def list_profiles(properties: nil, guids: nil)
  req = Proto::ListProfilesRequest.new
  req.properties.replace(properties) if properties
  req.guids.replace(guids) if guids

  resp = request(:list_profiles_request, req).list_profiles_response
  resp.profiles.map do |profile|
    profile.properties.to_h { |p| [p.key, JSON.parse(p.json_value)] }
  end
end

#list_sessionsObject

--- Topology ---



20
21
22
# File 'lib/iterm2/client.rb', line 20

def list_sessions
  request(:list_sessions_request, Proto::ListSessionsRequest.new).list_sessions_response
end

#on_focus_change(&block) ⇒ Object



445
446
447
448
449
# File 'lib/iterm2/client.rb', line 445

def on_focus_change(&block)
  subscribe(:NOTIFY_ON_FOCUS_CHANGE) do |n|
    block.call(parse_focus_notification(n))
  end
end

#on_layout_change(&block) ⇒ Object



475
476
477
478
479
# File 'lib/iterm2/client.rb', line 475

def on_layout_change(&block)
  subscribe(:NOTIFY_ON_LAYOUT_CHANGE) do |n|
    block.call({ type: :layout_change })
  end
end

#on_new_session(&block) ⇒ Object



451
452
453
454
455
# File 'lib/iterm2/client.rb', line 451

def on_new_session(&block)
  subscribe(:NOTIFY_ON_NEW_SESSION) do |n|
    block.call({ type: :new_session, session_id: n.new_session_notification.session_id })
  end
end

#on_prompt_change(session_id, &block) ⇒ Object



463
464
465
466
467
# File 'lib/iterm2/client.rb', line 463

def on_prompt_change(session_id, &block)
  subscribe(:NOTIFY_ON_PROMPT, session_id: session_id) do |n|
    block.call(parse_prompt_notification(n))
  end
end

#on_screen_update(session_id, &block) ⇒ Object



469
470
471
472
473
# File 'lib/iterm2/client.rb', line 469

def on_screen_update(session_id, &block)
  subscribe(:NOTIFY_ON_SCREEN_UPDATE, session_id: session_id) do |n|
    block.call({ type: :screen_update, session: n.screen_update_notification.session })
  end
end

#on_session_terminated(&block) ⇒ Object



457
458
459
460
461
# File 'lib/iterm2/client.rb', line 457

def on_session_terminated(&block)
  subscribe(:NOTIFY_ON_TERMINATE_SESSION) do |n|
    block.call({ type: :session_terminated, session_id: n.terminate_session_notification.session_id })
  end
end

#raise_by_cwd(pattern) ⇒ Object

Raise first session whose cwd matches pattern

Raises:



266
267
268
269
270
271
272
# File 'lib/iterm2/client.rb', line 266

def raise_by_cwd(pattern)
  regex = Regexp.new(pattern, Regexp::IGNORECASE)
  match = topology_enriched.find { |s| s[:cwd]&.match?(regex) }
  raise NotFoundError, "No session with cwd matching #{pattern.inspect}" unless match

  activate_session(match[:session_id])
end

#raise_by_title(pattern) ⇒ Object

Raise first session whose title matches pattern

Raises:



120
121
122
123
124
125
126
# File 'lib/iterm2/client.rb', line 120

def raise_by_title(pattern)
  regex = Regexp.new(pattern, Regexp::IGNORECASE)
  match = topology.find { |s| s[:title]&.match?(regex) }
  raise NotFoundError, "No session matching #{pattern.inspect}" unless match

  activate_session(match[:session_id])
end

#read_screen(session_id, trailing_lines: nil) ⇒ Object

Raises:



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/iterm2/client.rb', line 68

def read_screen(session_id, trailing_lines: nil)
  line_range = if trailing_lines
    Proto::LineRange.new(trailing_lines: trailing_lines)
  else
    Proto::LineRange.new(screen_contents_only: true)
  end

  response = request(:get_buffer_request, Proto::GetBufferRequest.new(
    session: session_id,
    line_range: line_range
  ))
  buf = response.get_buffer_response
  raise RPCError, "GetBuffer failed: #{buf.status}" unless buf.status == :OK

  {
    lines: buf.contents.map(&:text),
    cursor: buf.cursor ? { x: buf.cursor.x, y: buf.cursor.y } : nil
  }
end

#reorder_tabs(assignments) ⇒ Object

--- ReorderTabs ---

Raises:



164
165
166
167
168
169
170
171
172
173
# File 'lib/iterm2/client.rb', line 164

def reorder_tabs(assignments)
  protos = assignments.map do |window_id, tab_ids|
    Proto::ReorderTabsRequest::Assignment.new(window_id: window_id, tab_ids: tab_ids)
  end

  resp = request(:reorder_tabs_request, Proto::ReorderTabsRequest.new(assignments: protos)).reorder_tabs_response
  raise RPCError, "ReorderTabs failed: #{resp.status}" unless resp.status == :OK

  true
end

#send_text(session_id, text, suppress_broadcast: false) ⇒ Object

--- Session Interaction ---



59
60
61
62
63
64
65
66
# File 'lib/iterm2/client.rb', line 59

def send_text(session_id, text, suppress_broadcast: false)
  response = request(:send_text_request, Proto::SendTextRequest.new(
    session: session_id,
    text: text,
    suppress_broadcast: suppress_broadcast
  ))
  response.send_text_response.status == :OK
end

#session_info(session_id) ⇒ Object

Convenience: get session's tty, pid, cwd, name in one call



244
245
246
247
248
249
250
251
252
253
# File 'lib/iterm2/client.rb', line 244

def session_info(session_id)
  vars = get_variables("tty", "pid", "path", "name", "jobName", session_id: session_id)
  {
    tty: vars["tty"],
    pid: vars["pid"],
    cwd: vars["path"],
    name: vars["name"],
    job: vars["jobName"]
  }
end

#sessions(window_id: nil, tab_id: nil) ⇒ Object



50
51
52
53
54
55
# File 'lib/iterm2/client.rb', line 50

def sessions(window_id: nil, tab_id: nil)
  rows = topology
  rows = rows.select { |s| s[:window_id] == window_id } if window_id
  rows = rows.select { |s| s[:tab_id] == tab_id } if tab_id
  rows.map { |s| Session.new(client: self, id: s[:session_id], window_id: s[:window_id], tab_id: s[:tab_id], title: s[:title]) }
end

#set_profile_property(session_id, key, value) ⇒ Object

--- SetProfileProperty ---



195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/iterm2/client.rb', line 195

def set_profile_property(session_id, key, value)
  assignment = Proto::SetProfilePropertyRequest::Assignment.new(
    key: key,
    json_value: JSON.dump(value)
  )

  response = request(:set_profile_property_request, Proto::SetProfilePropertyRequest.new(
    session: session_id,
    assignments: [assignment]
  ))
  response.set_profile_property_response.status == :OK
end

#set_variables(vars, session_id: nil, tab_id: nil, window_id: nil, app: nil) ⇒ Object

Set user-defined variables (must begin with "user.")



230
231
232
233
234
235
236
# File 'lib/iterm2/client.rb', line 230

def set_variables(vars, session_id: nil, tab_id: nil, window_id: nil, app: nil)
  sets = vars.map { |k, v| Proto::VariableRequest::Set.new(name: k, value: JSON.dump(v)) }
  req = Proto::VariableRequest.new(set: sets)
  set_variable_scope!(req, session_id: session_id, tab_id: tab_id, window_id: window_id, app: app)

  request(:variable_request, req).variable_response.status == :OK
end

#split_pane(session_id, vertical: true, profile_name: nil, profile_customizations: {}) ⇒ Object

--- SplitPane ---

Raises:



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/iterm2/client.rb', line 143

def split_pane(session_id, vertical: true, profile_name: nil, profile_customizations: {})
  props = profile_customizations.map do |key, value|
    Proto::ProfileProperty.new(key: key, json_value: JSON.dump(value))
  end

  split_req = Proto::SplitPaneRequest.new(
    split_direction: vertical ? :VERTICAL : :HORIZONTAL,
    before: false
  )
  split_req.session = session_id if session_id
  split_req.profile_name = profile_name if profile_name
  split_req.custom_profile_properties.replace(props) unless props.empty?

  resp = request(:split_pane_request, split_req).split_pane_response
  raise RPCError, "SplitPane failed: #{resp.status}" unless resp.status == :OK

  resp.session_id.first
end

#subscribe(notification_type, session_id: nil, &callback) ⇒ Object

--- Notifications ---



401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
# File 'lib/iterm2/client.rb', line 401

def subscribe(notification_type, session_id: nil, &callback)
  ensure_dispatch_loop!

  req = Proto::NotificationRequest.new(
    subscribe: true,
    notification_type: notification_type
  )
  req.session = session_id if session_id

  resp = request(:notification_request, req).notification_response
  unless resp.status == :OK || resp.status == :ALREADY_SUBSCRIBED
    raise SubscriptionError, "Subscribe failed: #{resp.status}"
  end

  key = [session_id, notification_type]
  token = [key, callback]
  @subscriber_mutex.synchronize do
    (@subscribers[key] ||= []) << callback
  end

  token
end

#tabs(window_id: nil) ⇒ Object



42
43
44
45
46
47
48
# File 'lib/iterm2/client.rb', line 42

def tabs(window_id: nil)
  rows = topology
  rows = rows.select { |s| s[:window_id] == window_id } if window_id
  rows.map { |s| [s[:window_id], s[:tab_id]] }.uniq.map do |wid, tid|
    Tab.new(client: self, id: tid, window_id: wid)
  end
end

#topologyObject

Returns a flat array of tab_id:, session_id:, title: hashes



25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/iterm2/client.rb', line 25

def topology
  resp = list_sessions
  sessions = []

  resp.windows.each do |window|
    window.tabs.each do |tab|
      extract_sessions(tab.root, sessions, window_id: window.window_id, tab_id: tab.tab_id)
    end
  end

  sessions
end

#topology_enrichedObject

Enriched topology with variables (cwd, pid, tty, job)



256
257
258
259
260
261
262
263
# File 'lib/iterm2/client.rb', line 256

def topology_enriched
  sessions = topology
  sessions.each do |s|
    info = session_info(s[:session_id])
    s.merge!(info)
  end
  sessions
end

#topology_for_aggregator(mapping_file: nil) ⇒ Object

JXA-compatible topology: returns hash matching get-iterm-topology.js output format. Maps iTerm session GUIDs to Claude session IDs via session-iterm-mapping.json. Used by claude_code_history's SessionAggregator as a drop-in replacement.



277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
# File 'lib/iterm2/client.rb', line 277

def topology_for_aggregator(mapping_file: nil)
  mapping_file ||= File.expand_path("~/.claude/session-iterm-mapping.json")
  tty_to_claude = build_tty_mapping(mapping_file)

  resp = list_sessions
  tab_index = 0
  windows = resp.windows.map.with_index(1) do |window, widx|
    tabs = window.tabs.map do |tab|
      sessions_in_tab = extract_sessions_flat(tab.root)
      # Use first session in tab (primary session)
      primary = sessions_in_tab.first
      next nil unless primary

      tab_index += 1
      info = session_info(primary[:session_id])
      claude_id = tty_to_claude[info[:tty]]

      # Parse title for project_name, session_number, status
      title = info[:name] || primary[:title] || ""
      project_name, session_number, status = parse_tab_title(title)

      {
        "tab_index" => tab_index,
        "title" => title,
        "project_name" => project_name,
        "session_number" => session_number,
        "status" => status,
        "tty" => info[:tty],
        "cwd" => info[:cwd],
        "session_id" => claude_id,
        "foreground_process" => info[:job],
        "iterm_session_id" => primary[:session_id],
        "iterm_tab_id" => tab.tab_id,
        "pid" => info[:pid]
      }
    end.compact

    {
      "window_index" => widx,
      "window_id" => window.window_id,
      "name" => "Window #{widx}",
      "tabs" => tabs
    }
  end

  { "windows" => windows }
end

#unsubscribe(token) ⇒ Object



424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
# File 'lib/iterm2/client.rb', line 424

def unsubscribe(token)
  key, callback = token
  session_id, notification_type = key

  @subscriber_mutex.synchronize do
    list = @subscribers[key]
    list&.delete(callback)
    @subscribers.delete(key) if list&.empty?
  end

  req = Proto::NotificationRequest.new(
    subscribe: false,
    notification_type: notification_type
  )
  req.session = session_id if session_id

  request(:notification_request, req)
rescue IOError, ConnectionError
  # Connection may already be closed during shutdown
end

#windowsObject



38
39
40
# File 'lib/iterm2/client.rb', line 38

def windows
  topology.map { |s| s[:window_id] }.uniq.map { |id| Window.new(client: self, id: id) }
end