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
|
@@ -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,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
|