Class: ITerm2::Client
- Inherits:
-
Object
- Object
- ITerm2::Client
- Defined in:
- lib/iterm2/client.rb
Instance Method Summary collapse
-
#activate_session(session_id, select_tab: true, order_window_front: true) ⇒ Object
--- Activate (raise) ---.
- #activate_tab(tab_id, order_window_front: true) ⇒ Object
- #activate_window(window_id) ⇒ Object
- #close ⇒ Object
-
#close_session(session_id, force: false) ⇒ Object
--- Close ---.
- #close_tab(tab_id, force: false) ⇒ Object
-
#create_tab(window_id: nil, profile_name: nil) ⇒ Object
--- CreateTab ---.
-
#focus ⇒ Object
--- Focus ---.
-
#get_profile_property(session_id, *keys) ⇒ Object
--- GetProfileProperty ---.
-
#get_prompt(session_id) ⇒ Object
--- GetPrompt ---.
-
#get_property(name, session_id: nil, window_id: nil) ⇒ Object
--- GetProperty ---.
-
#get_variable(name, **scope) ⇒ Object
Convenience: get a single variable.
-
#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.
-
#initialize(app_name: "iterm2_ruby") ⇒ Client
constructor
A new instance of Client.
-
#inject(session_id, data) ⇒ Object
--- Inject ---.
-
#list_profiles(properties: nil, guids: nil) ⇒ Object
--- ListProfiles ---.
-
#list_sessions ⇒ Object
--- Topology ---.
- #on_focus_change(&block) ⇒ Object
- #on_layout_change(&block) ⇒ Object
- #on_new_session(&block) ⇒ Object
- #on_prompt_change(session_id, &block) ⇒ Object
- #on_screen_update(session_id, &block) ⇒ Object
- #on_session_terminated(&block) ⇒ Object
-
#raise_by_cwd(pattern) ⇒ Object
Raise first session whose cwd matches pattern.
-
#raise_by_title(pattern) ⇒ Object
Raise first session whose title matches pattern.
- #read_screen(session_id, trailing_lines: nil) ⇒ Object
-
#reorder_tabs(assignments) ⇒ Object
--- ReorderTabs ---.
-
#send_text(session_id, text, suppress_broadcast: false) ⇒ Object
--- Session Interaction ---.
-
#session_info(session_id) ⇒ Object
Convenience: get session's tty, pid, cwd, name in one call.
- #sessions(window_id: nil, tab_id: nil) ⇒ Object
-
#set_profile_property(session_id, key, value) ⇒ Object
--- SetProfileProperty ---.
-
#set_variables(vars, session_id: nil, tab_id: nil, window_id: nil, app: nil) ⇒ Object
Set user-defined variables (must begin with "user.").
-
#split_pane(session_id, vertical: true, profile_name: nil, profile_customizations: {}) ⇒ Object
--- SplitPane ---.
-
#subscribe(notification_type, session_id: nil, &callback) ⇒ Object
--- Notifications ---.
- #tabs(window_id: nil) ⇒ Object
-
#topology ⇒ Object
Returns a flat array of tab_id:, session_id:, title: hashes.
-
#topology_enriched ⇒ Object
Enriched topology with variables (cwd, pid, tty, job).
-
#topology_for_aggregator(mapping_file: nil) ⇒ Object
JXA-compatible topology: returns hash matching get-iterm-topology.js output format.
- #unsubscribe(token) ⇒ Object
- #windows ⇒ Object
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 |
#close ⇒ Object
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 ---
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 |
#focus ⇒ Object
--- 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 ---
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 ---
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
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_sessions ⇒ Object
--- 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
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
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
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 ---
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 ---
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 |
#topology ⇒ Object
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_enriched ⇒ Object
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.("~/.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 |