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.
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ITerm2
4
+ class Session
5
+ attr_reader :client, :id, :window_id, :tab_id, :title
6
+
7
+ def initialize(client:, id:, window_id: nil, tab_id: nil, title: nil)
8
+ @client = client
9
+ @id = id
10
+ @window_id = window_id
11
+ @tab_id = tab_id
12
+ @title = title
13
+ end
14
+
15
+ def activate(select_tab: true, order_window_front: true)
16
+ client.activate_session(id, select_tab: select_tab, order_window_front: order_window_front)
17
+ end
18
+
19
+ def send_text(text, suppress_broadcast: false)
20
+ client.send_text(id, text, suppress_broadcast: suppress_broadcast)
21
+ end
22
+
23
+ def read_screen(trailing_lines: nil)
24
+ client.read_screen(id, trailing_lines: trailing_lines)
25
+ end
26
+
27
+ def info
28
+ client.session_info(id)
29
+ end
30
+
31
+ def get_variable(name)
32
+ client.get_variable(name, session_id: id)
33
+ end
34
+
35
+ def to_h
36
+ {
37
+ window_id: window_id,
38
+ tab_id: tab_id,
39
+ session_id: id,
40
+ title: title
41
+ }
42
+ end
43
+ end
44
+ end
data/lib/iterm2/tab.rb ADDED
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ITerm2
4
+ class Tab
5
+ attr_reader :client, :id, :window_id
6
+
7
+ def initialize(client:, id:, window_id:)
8
+ @client = client
9
+ @id = id
10
+ @window_id = window_id
11
+ end
12
+
13
+ def activate(order_window_front: true)
14
+ client.activate_tab(id, order_window_front: order_window_front)
15
+ end
16
+
17
+ def sessions
18
+ client.topology
19
+ .select { |s| s[:tab_id] == id && s[:window_id] == window_id }
20
+ .map { |s| Session.new(client: client, id: s[:session_id], window_id: s[:window_id], tab_id: s[:tab_id], title: s[:title]) }
21
+ end
22
+
23
+ def primary_session
24
+ sessions.first
25
+ end
26
+
27
+ def close(force: false)
28
+ client.close_tab(id, force: force)
29
+ end
30
+
31
+ def to_h
32
+ {
33
+ window_id: window_id,
34
+ tab_id: id,
35
+ sessions: sessions.map(&:to_h)
36
+ }
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ITerm2
4
+ VERSION = "0.2.0"
5
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ITerm2
4
+ class Window
5
+ attr_reader :client, :id
6
+
7
+ def initialize(client:, id:)
8
+ @client = client
9
+ @id = id
10
+ end
11
+
12
+ def activate
13
+ client.activate_window(id)
14
+ end
15
+
16
+ def tabs
17
+ tab_ids = client.topology.select { |s| s[:window_id] == id }.map { |s| s[:tab_id] }.uniq
18
+ tab_ids.map { |tab_id| Tab.new(client: client, id: tab_id, window_id: id) }
19
+ end
20
+
21
+ def create_tab(profile_name: nil)
22
+ result = client.create_tab(window_id: id, profile_name: profile_name)
23
+ Session.new(client: client, id: result[:session_id], window_id: result[:window_id], tab_id: result[:tab_id])
24
+ end
25
+
26
+ def to_h
27
+ {
28
+ window_id: id,
29
+ tabs: tabs.map(&:to_h)
30
+ }
31
+ end
32
+ end
33
+ end
data/lib/iterm2.rb ADDED
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "iterm2/version"
4
+ require_relative "iterm2/proto/api_pb"
5
+ require_relative "iterm2/connection"
6
+ require_relative "iterm2/client"
7
+ require_relative "iterm2/window"
8
+ require_relative "iterm2/tab"
9
+ require_relative "iterm2/session"
10
+
11
+ module ITerm2
12
+ # Alias the protoc-generated Iterm2 module for internal use
13
+ Proto = ::Iterm2
14
+
15
+ class Error < StandardError; end
16
+ class ConnectionError < Error; end
17
+ class AuthError < Error; end
18
+ class RPCError < Error; end
19
+ class NotFoundError < RPCError; end
20
+ class SubscriptionError < Error; end
21
+
22
+ # Convenience: open connection, yield client, close
23
+ def self.connect(app_name: "iterm2_ruby")
24
+ client = Client.new(app_name: app_name)
25
+ return client unless block_given?
26
+
27
+ yield client
28
+ ensure
29
+ client&.close if block_given?
30
+ end
31
+
32
+ # One-shot: list all sessions
33
+ def self.list_sessions
34
+ connect { |c| c.list_sessions }
35
+ end
36
+
37
+ # One-shot: flat topology
38
+ def self.topology
39
+ connect { |c| c.topology }
40
+ end
41
+
42
+ # One-shot: send text to a session
43
+ def self.send_text(session_id, text)
44
+ connect { |c| c.send_text(session_id, text) }
45
+ end
46
+
47
+ # One-shot: read screen contents
48
+ def self.read_screen(session_id, trailing_lines: nil)
49
+ connect { |c| c.read_screen(session_id, trailing_lines: trailing_lines) }
50
+ end
51
+
52
+ # One-shot: raise by title pattern
53
+ def self.raise_by_title(pattern)
54
+ connect { |c| c.raise_by_title(pattern) }
55
+ end
56
+
57
+ # One-shot: activate a session
58
+ def self.activate_session(session_id)
59
+ connect { |c| c.activate_session(session_id) }
60
+ end
61
+
62
+ # One-shot: enriched topology (with cwd, pid, tty)
63
+ def self.topology_enriched
64
+ connect { |c| c.topology_enriched }
65
+ end
66
+
67
+ # One-shot: raise by cwd pattern
68
+ def self.raise_by_cwd(pattern)
69
+ connect { |c| c.raise_by_cwd(pattern) }
70
+ end
71
+
72
+ # One-shot: session info (tty, pid, cwd, name, job)
73
+ def self.session_info(session_id)
74
+ connect { |c| c.session_info(session_id) }
75
+ end
76
+
77
+ # One-shot: get variable
78
+ def self.get_variable(name, **scope)
79
+ connect { |c| c.get_variable(name, **scope) }
80
+ end
81
+
82
+ # One-shot: current focus state
83
+ def self.focus
84
+ connect { |c| c.focus }
85
+ end
86
+
87
+ # One-shot: prompt state for a session
88
+ def self.get_prompt(session_id)
89
+ connect { |c| c.get_prompt(session_id) }
90
+ end
91
+
92
+ # One-shot: get profile properties for a session
93
+ def self.get_profile_property(session_id, *keys)
94
+ connect { |c| c.get_profile_property(session_id, *keys) }
95
+ end
96
+
97
+ # One-shot: list all profiles
98
+ def self.list_profiles(properties: nil, guids: nil)
99
+ connect { |c| c.list_profiles(properties: properties, guids: guids) }
100
+ end
101
+
102
+ # One-shot: inject data into a session
103
+ def self.inject(session_id, data)
104
+ connect { |c| c.inject(session_id, data) }
105
+ end
106
+ end
data/llms.txt ADDED
@@ -0,0 +1,114 @@
1
+ # iterm2_ruby
2
+
3
+ > Ruby gem + CLI for controlling iTerm2 via its native WebSocket + Protobuf API. ~20x faster than osascript/JXA, no focus stealing, real-time event notifications.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ gem install iterm2_ruby
9
+ ```
10
+
11
+ Requires macOS, iTerm2 with API enabled (Preferences > General > Magic > Enable Python API), Ruby >= 3.1.
12
+
13
+ ## Usage: One-Shot (connect, run, disconnect)
14
+
15
+ ```ruby
16
+ require "iterm2"
17
+
18
+ sessions = ITerm2.topology
19
+ ITerm2.send_text(session_id, "ls -la\n")
20
+ screen = ITerm2.read_screen(session_id)
21
+ ITerm2.raise_by_title("my-project")
22
+ ```
23
+
24
+ ## Usage: Persistent Client (reuse connection)
25
+
26
+ ```ruby
27
+ ITerm2.connect do |client|
28
+ sessions = client.topology
29
+ client.send_text(sessions.first[:session_id], "make test\n")
30
+ client.on_focus_change { |e| puts e.inspect }
31
+ sleep # keep alive for notifications
32
+ end
33
+ ```
34
+
35
+ ## Core Methods
36
+
37
+ ### `client.topology` -> Array<Hash>
38
+ Flat list of all sessions: `[{window_id:, tab_id:, session_id:, title:}, ...]`
39
+
40
+ ### `client.send_text(session_id, text, suppress_broadcast: false)` -> true/false
41
+ Send text to a session. Does NOT auto-append `\n` -- include it to execute a command.
42
+
43
+ ### `client.read_screen(session_id, trailing_lines: nil)` -> Hash
44
+ Read visible screen. Returns `{lines: ["line1", ...], cursor: {x:, y:}}`.
45
+ Pass `trailing_lines: N` for scrollback.
46
+
47
+ ### `client.activate_session(session_id)` -> true/false
48
+ Raise and focus a session (selects tab, brings window to front).
49
+
50
+ ### `client.raise_by_title(pattern)` -> true/false
51
+ Find first session matching title (case-insensitive regex) and activate it.
52
+ Raises `NotFoundError` if no match.
53
+
54
+ ### `client.focus` -> Hash
55
+ Current focus: `{active_session:, active_tab:, active_window:, app_active:}`.
56
+
57
+ ### `client.session_info(session_id)` -> Hash
58
+ Session details: `{tty:, pid:, cwd:, name:, job:}`.
59
+
60
+ ### `client.get_prompt(session_id)` -> Hash
61
+ Prompt state: `{state:, command:, working_directory:, exit_status:}`.
62
+ States: `:editing`, `:running`, `:at_prompt`, `:unavailable`.
63
+
64
+ ## Other Methods
65
+
66
+ `topology_enriched`, `list_sessions`, `activate_tab`, `activate_window`,
67
+ `raise_by_cwd`, `create_tab`, `split_pane`, `close_session`, `close_tab`,
68
+ `set_profile_property`, `get_profile_property`, `list_profiles`, `get_property`,
69
+ `get_variables`, `set_variables`, `inject`, `reorder_tabs`, `topology_for_aggregator`.
70
+
71
+ ## Notifications
72
+
73
+ ```ruby
74
+ client.on_focus_change { |e| } # {type: :focus, session:, ...}
75
+ client.on_new_session { |e| } # {type: :new_session, session_id:}
76
+ client.on_session_terminated { |e| } # {type: :session_terminated, session_id:}
77
+ client.on_prompt_change(sid) { |e| } # {type: :prompt, state:, ...}
78
+ client.on_screen_update(sid) { |e| } # {type: :screen_update, session:}
79
+ client.on_layout_change { |e| } # {type: :layout_change}
80
+ ```
81
+
82
+ ## CLI
83
+
84
+ ```bash
85
+ iterm2ctl list # list sessions
86
+ iterm2ctl send "cmd" # send text (auto-appends \n)
87
+ iterm2ctl read # read screen
88
+ iterm2ctl raise "pattern" # raise tab by title
89
+ iterm2ctl focus # show focus state
90
+ iterm2ctl info # show session details
91
+ iterm2ctl move --tab ID --to-window ID # move tab between windows
92
+ iterm2ctl watch # stream events as JSON
93
+ ```
94
+
95
+ Most commands accept `--session ID`, `--tab ID`, `--window ID`, `--json`.
96
+
97
+ ## Errors
98
+
99
+ `ITerm2::ConnectionError` (can't connect), `ITerm2::AuthError` (auth failed),
100
+ `ITerm2::RPCError` (iTerm2 error), `ITerm2::NotFoundError` (no match),
101
+ `ITerm2::SubscriptionError` (subscribe failed). All inherit `ITerm2::Error`.
102
+
103
+ ## Gotchas
104
+
105
+ 1. `send_text` does not append `\n` -- the CLI does, the API does not.
106
+ 2. iTerm2 sends each notification twice (server-side behavior).
107
+ 3. One-shot methods open a new connection per call -- use `ITerm2.connect { }` for batches.
108
+
109
+ ## Documentation
110
+
111
+ - [Full API Reference](docs/api.md) -- every method with signatures, return shapes, examples
112
+ - [CLI Reference](docs/cli.md) -- every command with flags and exit codes
113
+ - [Architecture](docs/architecture.md) -- connection lifecycle, threading, protobuf protocol
114
+ - [README](README.md) -- quick start guide