@moejay/wrightty 0.0.0 → 0.1.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.
- package/dist/client.d.ts +14 -0
- package/dist/client.js +83 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +8 -0
- package/dist/terminal.d.ts +48 -0
- package/dist/terminal.js +210 -0
- package/dist/types.d.ts +90 -0
- package/dist/types.js +3 -0
- package/package.json +35 -15
- package/.github/workflows/ci.yml +0 -90
- package/.github/workflows/release.yml +0 -177
- package/Cargo.lock +0 -2662
- package/Cargo.toml +0 -38
- package/PROTOCOL.md +0 -1351
- package/README.md +0 -386
- package/agents/ceo/AGENTS.md +0 -24
- package/agents/ceo/HEARTBEAT.md +0 -72
- package/agents/ceo/SOUL.md +0 -33
- package/agents/ceo/TOOLS.md +0 -3
- package/agents/founding-engineer/AGENTS.md +0 -44
- package/crates/wrightty/Cargo.toml +0 -43
- package/crates/wrightty/src/client_cmds.rs +0 -366
- package/crates/wrightty/src/discover.rs +0 -78
- package/crates/wrightty/src/main.rs +0 -100
- package/crates/wrightty/src/server.rs +0 -100
- package/crates/wrightty/src/term.rs +0 -338
- package/crates/wrightty-bridge-ghostty/Cargo.toml +0 -27
- package/crates/wrightty-bridge-ghostty/src/ghostty.rs +0 -422
- package/crates/wrightty-bridge-ghostty/src/lib.rs +0 -2
- package/crates/wrightty-bridge-ghostty/src/main.rs +0 -146
- package/crates/wrightty-bridge-ghostty/src/rpc.rs +0 -307
- package/crates/wrightty-bridge-kitty/Cargo.toml +0 -26
- package/crates/wrightty-bridge-kitty/src/kitty.rs +0 -269
- package/crates/wrightty-bridge-kitty/src/lib.rs +0 -2
- package/crates/wrightty-bridge-kitty/src/main.rs +0 -124
- package/crates/wrightty-bridge-kitty/src/rpc.rs +0 -304
- package/crates/wrightty-bridge-tmux/Cargo.toml +0 -26
- package/crates/wrightty-bridge-tmux/src/lib.rs +0 -2
- package/crates/wrightty-bridge-tmux/src/main.rs +0 -119
- package/crates/wrightty-bridge-tmux/src/rpc.rs +0 -291
- package/crates/wrightty-bridge-tmux/src/tmux.rs +0 -215
- package/crates/wrightty-bridge-wezterm/Cargo.toml +0 -26
- package/crates/wrightty-bridge-wezterm/src/lib.rs +0 -2
- package/crates/wrightty-bridge-wezterm/src/main.rs +0 -119
- package/crates/wrightty-bridge-wezterm/src/rpc.rs +0 -339
- package/crates/wrightty-bridge-wezterm/src/wezterm.rs +0 -190
- package/crates/wrightty-bridge-zellij/Cargo.toml +0 -27
- package/crates/wrightty-bridge-zellij/src/lib.rs +0 -2
- package/crates/wrightty-bridge-zellij/src/main.rs +0 -125
- package/crates/wrightty-bridge-zellij/src/rpc.rs +0 -328
- package/crates/wrightty-bridge-zellij/src/zellij.rs +0 -199
- package/crates/wrightty-client/Cargo.toml +0 -16
- package/crates/wrightty-client/src/client.rs +0 -254
- package/crates/wrightty-client/src/lib.rs +0 -2
- package/crates/wrightty-core/Cargo.toml +0 -21
- package/crates/wrightty-core/src/input.rs +0 -212
- package/crates/wrightty-core/src/lib.rs +0 -4
- package/crates/wrightty-core/src/screen.rs +0 -325
- package/crates/wrightty-core/src/session.rs +0 -249
- package/crates/wrightty-core/src/session_manager.rs +0 -77
- package/crates/wrightty-protocol/Cargo.toml +0 -13
- package/crates/wrightty-protocol/src/error.rs +0 -8
- package/crates/wrightty-protocol/src/events.rs +0 -138
- package/crates/wrightty-protocol/src/lib.rs +0 -4
- package/crates/wrightty-protocol/src/methods.rs +0 -321
- package/crates/wrightty-protocol/src/types.rs +0 -201
- package/crates/wrightty-server/Cargo.toml +0 -23
- package/crates/wrightty-server/src/lib.rs +0 -2
- package/crates/wrightty-server/src/main.rs +0 -65
- package/crates/wrightty-server/src/rpc.rs +0 -455
- package/crates/wrightty-server/src/state.rs +0 -39
- package/examples/basic_command.py +0 -53
- package/examples/interactive_tui.py +0 -86
- package/examples/record_session.py +0 -96
- package/install.sh +0 -81
- package/sdks/node/package-lock.json +0 -85
- package/sdks/node/package.json +0 -44
- package/sdks/node/src/client.ts +0 -94
- package/sdks/node/src/index.ts +0 -19
- package/sdks/node/src/terminal.ts +0 -258
- package/sdks/node/src/types.ts +0 -105
- package/sdks/node/tsconfig.json +0 -17
- package/sdks/python/README.md +0 -96
- package/sdks/python/pyproject.toml +0 -42
- package/sdks/python/wrightty/__init__.py +0 -6
- package/sdks/python/wrightty/cli.py +0 -210
- package/sdks/python/wrightty/client.py +0 -136
- package/sdks/python/wrightty/mcp_server.py +0 -434
- package/sdks/python/wrightty/terminal.py +0 -333
- package/skills/wrightty/SKILL.md +0 -261
- package/src/lib.rs +0 -1
- package/tests/integration_test.rs +0 -618
|
@@ -1,339 +0,0 @@
|
|
|
1
|
-
//! jsonrpsee RPC module that maps wrightty protocol methods to wezterm cli commands.
|
|
2
|
-
|
|
3
|
-
use jsonrpsee::types::ErrorObjectOwned;
|
|
4
|
-
use jsonrpsee::RpcModule;
|
|
5
|
-
|
|
6
|
-
use wrightty_protocol::error;
|
|
7
|
-
use wrightty_protocol::methods::*;
|
|
8
|
-
use wrightty_protocol::types::*;
|
|
9
|
-
|
|
10
|
-
use crate::wezterm;
|
|
11
|
-
|
|
12
|
-
fn proto_err(code: i32, msg: impl Into<String>) -> ErrorObjectOwned {
|
|
13
|
-
ErrorObjectOwned::owned(code, msg.into(), None::<()>)
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
fn not_supported(method: &str) -> ErrorObjectOwned {
|
|
17
|
-
proto_err(error::NOT_SUPPORTED, format!("{method} is not supported by the WezTerm bridge"))
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/// Parse a wrightty session ID string into a WezTerm pane ID.
|
|
21
|
-
fn parse_pane_id(session_id: &str) -> Result<u64, ErrorObjectOwned> {
|
|
22
|
-
session_id
|
|
23
|
-
.parse::<u64>()
|
|
24
|
-
.map_err(|_| proto_err(error::SESSION_NOT_FOUND, format!("invalid session id: {session_id}")))
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/// Encode a list of KeyInput values into a string for `wezterm cli send-text`.
|
|
28
|
-
fn encode_keys_to_string(keys: &[KeyInput]) -> String {
|
|
29
|
-
let mut out = String::new();
|
|
30
|
-
for key in keys {
|
|
31
|
-
match key {
|
|
32
|
-
KeyInput::Shorthand(s) => encode_shorthand(s, &mut out),
|
|
33
|
-
KeyInput::Structured(event) => encode_key_event(event, &mut out),
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
out
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
fn encode_shorthand(s: &str, out: &mut String) {
|
|
40
|
-
// Check for modifier combos like "Ctrl+c"
|
|
41
|
-
if let Some((modifier_str, key_str)) = s.split_once('+') {
|
|
42
|
-
match modifier_str {
|
|
43
|
-
"Ctrl" => {
|
|
44
|
-
if key_str.len() == 1 {
|
|
45
|
-
let ch = key_str.chars().next().unwrap();
|
|
46
|
-
// Ctrl+letter: map to control character
|
|
47
|
-
let ctrl = (ch.to_ascii_uppercase() as u8).wrapping_sub(b'@');
|
|
48
|
-
out.push(ctrl as char);
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
if let Some(s) = named_key_str(key_str) {
|
|
52
|
-
out.push_str(s);
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
"Alt" => {
|
|
57
|
-
out.push('\x1b');
|
|
58
|
-
if key_str.len() == 1 {
|
|
59
|
-
out.push_str(key_str);
|
|
60
|
-
} else if let Some(s) = named_key_str(key_str) {
|
|
61
|
-
out.push_str(s);
|
|
62
|
-
}
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
_ => {}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Named keys
|
|
70
|
-
if let Some(s) = named_key_str(s) {
|
|
71
|
-
out.push_str(s);
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Literal text
|
|
76
|
-
out.push_str(s);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
fn encode_key_event(event: &KeyEvent, out: &mut String) {
|
|
80
|
-
let has_ctrl = event.modifiers.iter().any(|m| matches!(m, Modifier::Ctrl));
|
|
81
|
-
let has_alt = event.modifiers.iter().any(|m| matches!(m, Modifier::Alt));
|
|
82
|
-
|
|
83
|
-
if has_alt {
|
|
84
|
-
out.push('\x1b');
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
let base = match &event.key {
|
|
88
|
-
KeyType::Char => {
|
|
89
|
-
let ch = event.char.as_deref().unwrap_or("");
|
|
90
|
-
if has_ctrl && ch.len() == 1 {
|
|
91
|
-
let c = ch.chars().next().unwrap();
|
|
92
|
-
let ctrl = (c.to_ascii_uppercase() as u8).wrapping_sub(b'@');
|
|
93
|
-
out.push(ctrl as char);
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
ch
|
|
97
|
-
}
|
|
98
|
-
KeyType::Enter => "\r",
|
|
99
|
-
KeyType::Tab => "\t",
|
|
100
|
-
KeyType::Backspace => "\x7f",
|
|
101
|
-
KeyType::Delete => "\x1b[3~",
|
|
102
|
-
KeyType::Escape => "\x1b",
|
|
103
|
-
KeyType::ArrowUp => "\x1b[A",
|
|
104
|
-
KeyType::ArrowDown => "\x1b[B",
|
|
105
|
-
KeyType::ArrowRight => "\x1b[C",
|
|
106
|
-
KeyType::ArrowLeft => "\x1b[D",
|
|
107
|
-
KeyType::Home => "\x1b[H",
|
|
108
|
-
KeyType::End => "\x1b[F",
|
|
109
|
-
KeyType::PageUp => "\x1b[5~",
|
|
110
|
-
KeyType::PageDown => "\x1b[6~",
|
|
111
|
-
KeyType::Insert => "\x1b[2~",
|
|
112
|
-
KeyType::F => {
|
|
113
|
-
let n = event.n.unwrap_or(1);
|
|
114
|
-
match n {
|
|
115
|
-
1 => "\x1bOP",
|
|
116
|
-
2 => "\x1bOQ",
|
|
117
|
-
3 => "\x1bOR",
|
|
118
|
-
4 => "\x1bOS",
|
|
119
|
-
5 => "\x1b[15~",
|
|
120
|
-
6 => "\x1b[17~",
|
|
121
|
-
7 => "\x1b[18~",
|
|
122
|
-
8 => "\x1b[19~",
|
|
123
|
-
9 => "\x1b[20~",
|
|
124
|
-
10 => "\x1b[21~",
|
|
125
|
-
11 => "\x1b[23~",
|
|
126
|
-
12 => "\x1b[24~",
|
|
127
|
-
_ => "",
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
};
|
|
131
|
-
out.push_str(base);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
fn named_key_str(name: &str) -> Option<&'static str> {
|
|
135
|
-
match name {
|
|
136
|
-
"Enter" | "Return" => Some("\r"),
|
|
137
|
-
"Tab" => Some("\t"),
|
|
138
|
-
"Backspace" => Some("\x7f"),
|
|
139
|
-
"Delete" => Some("\x1b[3~"),
|
|
140
|
-
"Escape" | "Esc" => Some("\x1b"),
|
|
141
|
-
"ArrowUp" | "Up" => Some("\x1b[A"),
|
|
142
|
-
"ArrowDown" | "Down" => Some("\x1b[B"),
|
|
143
|
-
"ArrowRight" | "Right" => Some("\x1b[C"),
|
|
144
|
-
"ArrowLeft" | "Left" => Some("\x1b[D"),
|
|
145
|
-
"Home" => Some("\x1b[H"),
|
|
146
|
-
"End" => Some("\x1b[F"),
|
|
147
|
-
"PageUp" => Some("\x1b[5~"),
|
|
148
|
-
"PageDown" => Some("\x1b[6~"),
|
|
149
|
-
"Insert" => Some("\x1b[2~"),
|
|
150
|
-
_ => None,
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
pub fn build_rpc_module() -> anyhow::Result<RpcModule<()>> {
|
|
155
|
-
let mut module = RpcModule::new(());
|
|
156
|
-
|
|
157
|
-
// --- Wrightty.getInfo ---
|
|
158
|
-
module.register_async_method("Wrightty.getInfo", |_params, _state, _| async move {
|
|
159
|
-
serde_json::to_value(GetInfoResult {
|
|
160
|
-
info: ServerInfo {
|
|
161
|
-
version: "0.1.0".to_string(),
|
|
162
|
-
implementation: "wrightty-bridge-wezterm".to_string(),
|
|
163
|
-
capabilities: Capabilities {
|
|
164
|
-
screenshot: vec![ScreenshotFormat::Text],
|
|
165
|
-
max_sessions: 256,
|
|
166
|
-
supports_resize: false,
|
|
167
|
-
supports_scrollback: false,
|
|
168
|
-
supports_mouse: false,
|
|
169
|
-
supports_session_create: true,
|
|
170
|
-
supports_color_palette: false,
|
|
171
|
-
supports_raw_output: false,
|
|
172
|
-
supports_shell_integration: false,
|
|
173
|
-
events: vec![],
|
|
174
|
-
},
|
|
175
|
-
},
|
|
176
|
-
})
|
|
177
|
-
.map_err(|e| proto_err(-32603, e.to_string()))
|
|
178
|
-
})?;
|
|
179
|
-
|
|
180
|
-
// --- Session.create ---
|
|
181
|
-
module.register_async_method("Session.create", |_params, _state, _| async move {
|
|
182
|
-
let pane_id = wezterm::spawn_pane()
|
|
183
|
-
.await
|
|
184
|
-
.map_err(|e| proto_err(error::SPAWN_FAILED, e.to_string()))?;
|
|
185
|
-
|
|
186
|
-
serde_json::to_value(SessionCreateResult {
|
|
187
|
-
session_id: pane_id.to_string(),
|
|
188
|
-
})
|
|
189
|
-
.map_err(|e| proto_err(-32603, e.to_string()))
|
|
190
|
-
})?;
|
|
191
|
-
|
|
192
|
-
// --- Session.destroy ---
|
|
193
|
-
module.register_async_method("Session.destroy", |params, _state, _| async move {
|
|
194
|
-
let p: SessionDestroyParams = params.parse()?;
|
|
195
|
-
let pane_id = parse_pane_id(&p.session_id)?;
|
|
196
|
-
|
|
197
|
-
wezterm::kill_pane(pane_id)
|
|
198
|
-
.await
|
|
199
|
-
.map_err(|e| proto_err(error::SESSION_NOT_FOUND, e.to_string()))?;
|
|
200
|
-
|
|
201
|
-
serde_json::to_value(SessionDestroyResult { exit_code: None })
|
|
202
|
-
.map_err(|e| proto_err(-32603, e.to_string()))
|
|
203
|
-
})?;
|
|
204
|
-
|
|
205
|
-
// --- Session.list ---
|
|
206
|
-
module.register_async_method("Session.list", |_params, _state, _| async move {
|
|
207
|
-
let panes = wezterm::list_panes()
|
|
208
|
-
.await
|
|
209
|
-
.map_err(|e| proto_err(-32603, e.to_string()))?;
|
|
210
|
-
|
|
211
|
-
let sessions: Vec<SessionInfo> = panes
|
|
212
|
-
.into_iter()
|
|
213
|
-
.map(|p| SessionInfo {
|
|
214
|
-
session_id: p.pane_id.to_string(),
|
|
215
|
-
title: p.title,
|
|
216
|
-
cwd: if p.cwd.is_empty() { None } else { Some(p.cwd) },
|
|
217
|
-
cols: p.size.cols,
|
|
218
|
-
rows: p.size.rows,
|
|
219
|
-
pid: None,
|
|
220
|
-
running: true,
|
|
221
|
-
alternate_screen: false,
|
|
222
|
-
})
|
|
223
|
-
.collect();
|
|
224
|
-
|
|
225
|
-
serde_json::to_value(SessionListResult { sessions })
|
|
226
|
-
.map_err(|e| proto_err(-32603, e.to_string()))
|
|
227
|
-
})?;
|
|
228
|
-
|
|
229
|
-
// --- Session.getInfo ---
|
|
230
|
-
module.register_async_method("Session.getInfo", |params, _state, _| async move {
|
|
231
|
-
let p: SessionGetInfoParams = params.parse()?;
|
|
232
|
-
let pane_id = parse_pane_id(&p.session_id)?;
|
|
233
|
-
|
|
234
|
-
let pane = wezterm::find_pane(pane_id)
|
|
235
|
-
.await
|
|
236
|
-
.map_err(|e| proto_err(error::SESSION_NOT_FOUND, e.to_string()))?;
|
|
237
|
-
|
|
238
|
-
let info = SessionInfo {
|
|
239
|
-
session_id: pane.pane_id.to_string(),
|
|
240
|
-
title: pane.title,
|
|
241
|
-
cwd: if pane.cwd.is_empty() {
|
|
242
|
-
None
|
|
243
|
-
} else {
|
|
244
|
-
Some(pane.cwd)
|
|
245
|
-
},
|
|
246
|
-
cols: pane.size.cols,
|
|
247
|
-
rows: pane.size.rows,
|
|
248
|
-
pid: None,
|
|
249
|
-
running: true,
|
|
250
|
-
alternate_screen: false,
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
serde_json::to_value(info).map_err(|e| proto_err(-32603, e.to_string()))
|
|
254
|
-
})?;
|
|
255
|
-
|
|
256
|
-
// --- Input.sendText ---
|
|
257
|
-
module.register_async_method("Input.sendText", |params, _state, _| async move {
|
|
258
|
-
let p: InputSendTextParams = params.parse()?;
|
|
259
|
-
let pane_id = parse_pane_id(&p.session_id)?;
|
|
260
|
-
|
|
261
|
-
wezterm::send_text(pane_id, &p.text)
|
|
262
|
-
.await
|
|
263
|
-
.map_err(|e| proto_err(error::SESSION_NOT_FOUND, e.to_string()))?;
|
|
264
|
-
|
|
265
|
-
Ok::<_, ErrorObjectOwned>(serde_json::json!({}))
|
|
266
|
-
})?;
|
|
267
|
-
|
|
268
|
-
// --- Input.sendKeys ---
|
|
269
|
-
module.register_async_method("Input.sendKeys", |params, _state, _| async move {
|
|
270
|
-
let p: InputSendKeysParams = params.parse()?;
|
|
271
|
-
let pane_id = parse_pane_id(&p.session_id)?;
|
|
272
|
-
|
|
273
|
-
let text = encode_keys_to_string(&p.keys);
|
|
274
|
-
wezterm::send_text(pane_id, &text)
|
|
275
|
-
.await
|
|
276
|
-
.map_err(|e| proto_err(error::SESSION_NOT_FOUND, e.to_string()))?;
|
|
277
|
-
|
|
278
|
-
Ok::<_, ErrorObjectOwned>(serde_json::json!({}))
|
|
279
|
-
})?;
|
|
280
|
-
|
|
281
|
-
// --- Screen.getText ---
|
|
282
|
-
module.register_async_method("Screen.getText", |params, _state, _| async move {
|
|
283
|
-
let p: ScreenGetTextParams = params.parse()?;
|
|
284
|
-
let pane_id = parse_pane_id(&p.session_id)?;
|
|
285
|
-
|
|
286
|
-
let mut text = wezterm::get_text(pane_id)
|
|
287
|
-
.await
|
|
288
|
-
.map_err(|e| proto_err(error::SESSION_NOT_FOUND, e.to_string()))?;
|
|
289
|
-
|
|
290
|
-
if p.trim_trailing_whitespace {
|
|
291
|
-
text = text
|
|
292
|
-
.lines()
|
|
293
|
-
.map(|line| line.trim_end())
|
|
294
|
-
.collect::<Vec<_>>()
|
|
295
|
-
.join("\n");
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
serde_json::to_value(ScreenGetTextResult { text })
|
|
299
|
-
.map_err(|e| proto_err(-32603, e.to_string()))
|
|
300
|
-
})?;
|
|
301
|
-
|
|
302
|
-
// --- Terminal.getSize ---
|
|
303
|
-
module.register_async_method("Terminal.getSize", |params, _state, _| async move {
|
|
304
|
-
let p: TerminalGetSizeParams = params.parse()?;
|
|
305
|
-
let pane_id = parse_pane_id(&p.session_id)?;
|
|
306
|
-
|
|
307
|
-
let pane = wezterm::find_pane(pane_id)
|
|
308
|
-
.await
|
|
309
|
-
.map_err(|e| proto_err(error::SESSION_NOT_FOUND, e.to_string()))?;
|
|
310
|
-
|
|
311
|
-
serde_json::to_value(TerminalGetSizeResult {
|
|
312
|
-
cols: pane.size.cols,
|
|
313
|
-
rows: pane.size.rows,
|
|
314
|
-
})
|
|
315
|
-
.map_err(|e| proto_err(-32603, e.to_string()))
|
|
316
|
-
})?;
|
|
317
|
-
|
|
318
|
-
// --- Terminal.resize (not supported) ---
|
|
319
|
-
module.register_async_method("Terminal.resize", |_params, _state, _| async move {
|
|
320
|
-
Err::<serde_json::Value, _>(not_supported("Terminal.resize"))
|
|
321
|
-
})?;
|
|
322
|
-
|
|
323
|
-
// --- Screen.getContents (not supported) ---
|
|
324
|
-
module.register_async_method("Screen.getContents", |_params, _state, _| async move {
|
|
325
|
-
Err::<serde_json::Value, _>(not_supported("Screen.getContents"))
|
|
326
|
-
})?;
|
|
327
|
-
|
|
328
|
-
// --- Screen.screenshot (not supported) ---
|
|
329
|
-
module.register_async_method("Screen.screenshot", |_params, _state, _| async move {
|
|
330
|
-
Err::<serde_json::Value, _>(not_supported("Screen.screenshot"))
|
|
331
|
-
})?;
|
|
332
|
-
|
|
333
|
-
// --- Input.sendMouse (not supported) ---
|
|
334
|
-
module.register_async_method("Input.sendMouse", |_params, _state, _| async move {
|
|
335
|
-
Err::<serde_json::Value, _>(not_supported("Input.sendMouse"))
|
|
336
|
-
})?;
|
|
337
|
-
|
|
338
|
-
Ok(module)
|
|
339
|
-
}
|
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
//! Functions that shell out to `wezterm cli` and parse results.
|
|
2
|
-
|
|
3
|
-
use serde::Deserialize;
|
|
4
|
-
use tokio::process::Command;
|
|
5
|
-
|
|
6
|
-
/// A pane entry as returned by `wezterm cli list --format json`.
|
|
7
|
-
#[derive(Debug, Clone, Deserialize)]
|
|
8
|
-
pub struct WezTermPane {
|
|
9
|
-
pub pane_id: u64,
|
|
10
|
-
pub tab_id: u64,
|
|
11
|
-
pub window_id: u64,
|
|
12
|
-
pub workspace: String,
|
|
13
|
-
pub size: PaneSize,
|
|
14
|
-
pub title: String,
|
|
15
|
-
pub cwd: String,
|
|
16
|
-
#[serde(default)]
|
|
17
|
-
pub is_active: bool,
|
|
18
|
-
#[serde(default)]
|
|
19
|
-
pub is_zoomed: bool,
|
|
20
|
-
#[serde(default)]
|
|
21
|
-
pub cursor_x: u64,
|
|
22
|
-
#[serde(default)]
|
|
23
|
-
pub cursor_y: u64,
|
|
24
|
-
#[serde(default)]
|
|
25
|
-
pub cursor_shape: Option<String>,
|
|
26
|
-
#[serde(default)]
|
|
27
|
-
pub cursor_visibility: Option<String>,
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
#[derive(Debug, Clone, Deserialize)]
|
|
31
|
-
pub struct PaneSize {
|
|
32
|
-
pub rows: u16,
|
|
33
|
-
pub cols: u16,
|
|
34
|
-
#[serde(default)]
|
|
35
|
-
pub pixel_width: u32,
|
|
36
|
-
#[serde(default)]
|
|
37
|
-
pub pixel_height: u32,
|
|
38
|
-
#[serde(default)]
|
|
39
|
-
pub dpi: u32,
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
#[derive(Debug, thiserror::Error)]
|
|
43
|
-
pub enum WezTermError {
|
|
44
|
-
#[error("wezterm cli failed: {0}")]
|
|
45
|
-
CommandFailed(String),
|
|
46
|
-
#[error("failed to parse wezterm output: {0}")]
|
|
47
|
-
ParseError(String),
|
|
48
|
-
#[error("pane {0} not found")]
|
|
49
|
-
PaneNotFound(u64),
|
|
50
|
-
#[error("io error: {0}")]
|
|
51
|
-
Io(#[from] std::io::Error),
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/// Build a `Command` for wezterm CLI.
|
|
55
|
-
///
|
|
56
|
-
/// Checks `WEZTERM_CMD` env var for the command to use.
|
|
57
|
-
/// Supports flatpak: set `WEZTERM_CMD=flatpak run --command=wezterm org.wezfurlong.wezterm`
|
|
58
|
-
///
|
|
59
|
-
/// Falls back to just `wezterm` if not set.
|
|
60
|
-
fn wezterm_cmd(cli_args: &[&str]) -> Command {
|
|
61
|
-
let cmd_str = std::env::var("WEZTERM_CMD")
|
|
62
|
-
.unwrap_or_else(|_| "wezterm".to_string());
|
|
63
|
-
|
|
64
|
-
let parts: Vec<&str> = cmd_str.split_whitespace().collect();
|
|
65
|
-
let (program, prefix_args) = parts.split_first().expect("WEZTERM_CMD must not be empty");
|
|
66
|
-
|
|
67
|
-
let mut cmd = Command::new(program);
|
|
68
|
-
for arg in prefix_args {
|
|
69
|
-
cmd.arg(arg);
|
|
70
|
-
}
|
|
71
|
-
// Always prepend "cli" subcommand.
|
|
72
|
-
cmd.arg("cli");
|
|
73
|
-
for arg in cli_args {
|
|
74
|
-
cmd.arg(arg);
|
|
75
|
-
}
|
|
76
|
-
cmd
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/// Check if WezTerm is reachable. Returns Ok(()) if `wezterm cli list` succeeds.
|
|
80
|
-
pub async fn health_check() -> Result<(), WezTermError> {
|
|
81
|
-
let output = wezterm_cmd(&["list", "--format", "json"])
|
|
82
|
-
.output()
|
|
83
|
-
.await?;
|
|
84
|
-
|
|
85
|
-
if !output.status.success() {
|
|
86
|
-
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
87
|
-
return Err(WezTermError::CommandFailed(format!("WezTerm not reachable: {stderr}")));
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Verify we got valid JSON with at least one pane.
|
|
91
|
-
let panes: Vec<WezTermPane> = serde_json::from_slice(&output.stdout)
|
|
92
|
-
.map_err(|e| WezTermError::ParseError(e.to_string()))?;
|
|
93
|
-
|
|
94
|
-
if panes.is_empty() {
|
|
95
|
-
return Err(WezTermError::CommandFailed("WezTerm has no panes".to_string()));
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
Ok(())
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/// List all panes via `wezterm cli list --format json`.
|
|
102
|
-
pub async fn list_panes() -> Result<Vec<WezTermPane>, WezTermError> {
|
|
103
|
-
let output = wezterm_cmd(&["list", "--format", "json"])
|
|
104
|
-
.output()
|
|
105
|
-
.await?;
|
|
106
|
-
|
|
107
|
-
if !output.status.success() {
|
|
108
|
-
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
109
|
-
return Err(WezTermError::CommandFailed(stderr.into_owned()));
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
let panes: Vec<WezTermPane> = serde_json::from_slice(&output.stdout)
|
|
113
|
-
.map_err(|e| WezTermError::ParseError(e.to_string()))?;
|
|
114
|
-
|
|
115
|
-
Ok(panes)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/// Get the text content of a pane via `wezterm cli get-text --pane-id N`.
|
|
119
|
-
pub async fn get_text(pane_id: u64) -> Result<String, WezTermError> {
|
|
120
|
-
let pane_str = pane_id.to_string();
|
|
121
|
-
let output = wezterm_cmd(&["get-text", "--pane-id", &pane_str])
|
|
122
|
-
.output()
|
|
123
|
-
.await?;
|
|
124
|
-
|
|
125
|
-
if !output.status.success() {
|
|
126
|
-
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
127
|
-
return Err(WezTermError::CommandFailed(stderr.into_owned()));
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/// Send text to a pane via `wezterm cli send-text --pane-id N --no-paste "text"`.
|
|
134
|
-
pub async fn send_text(pane_id: u64, text: &str) -> Result<(), WezTermError> {
|
|
135
|
-
let pane_str = pane_id.to_string();
|
|
136
|
-
let output = wezterm_cmd(&["send-text", "--pane-id", &pane_str, "--no-paste", text])
|
|
137
|
-
.output()
|
|
138
|
-
.await?;
|
|
139
|
-
|
|
140
|
-
if !output.status.success() {
|
|
141
|
-
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
142
|
-
return Err(WezTermError::CommandFailed(stderr.into_owned()));
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
Ok(())
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/// Spawn a new pane via `wezterm cli spawn`. Returns the new pane ID.
|
|
149
|
-
pub async fn spawn_pane() -> Result<u64, WezTermError> {
|
|
150
|
-
let output = wezterm_cmd(&["spawn"])
|
|
151
|
-
.output()
|
|
152
|
-
.await?;
|
|
153
|
-
|
|
154
|
-
if !output.status.success() {
|
|
155
|
-
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
156
|
-
return Err(WezTermError::CommandFailed(stderr.into_owned()));
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
160
|
-
let pane_id: u64 = stdout
|
|
161
|
-
.trim()
|
|
162
|
-
.parse()
|
|
163
|
-
.map_err(|e: std::num::ParseIntError| WezTermError::ParseError(e.to_string()))?;
|
|
164
|
-
|
|
165
|
-
Ok(pane_id)
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/// Kill a pane via `wezterm cli kill-pane --pane-id N`.
|
|
169
|
-
pub async fn kill_pane(pane_id: u64) -> Result<(), WezTermError> {
|
|
170
|
-
let pane_str = pane_id.to_string();
|
|
171
|
-
let output = wezterm_cmd(&["kill-pane", "--pane-id", &pane_str])
|
|
172
|
-
.output()
|
|
173
|
-
.await?;
|
|
174
|
-
|
|
175
|
-
if !output.status.success() {
|
|
176
|
-
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
177
|
-
return Err(WezTermError::CommandFailed(stderr.into_owned()));
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
Ok(())
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/// Find a specific pane by ID from the list output.
|
|
184
|
-
pub async fn find_pane(pane_id: u64) -> Result<WezTermPane, WezTermError> {
|
|
185
|
-
let panes = list_panes().await?;
|
|
186
|
-
panes
|
|
187
|
-
.into_iter()
|
|
188
|
-
.find(|p| p.pane_id == pane_id)
|
|
189
|
-
.ok_or(WezTermError::PaneNotFound(pane_id))
|
|
190
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
[package]
|
|
2
|
-
name = "wrightty-bridge-zellij"
|
|
3
|
-
version.workspace = true
|
|
4
|
-
edition.workspace = true
|
|
5
|
-
license.workspace = true
|
|
6
|
-
authors.workspace = true
|
|
7
|
-
repository.workspace = true
|
|
8
|
-
homepage.workspace = true
|
|
9
|
-
description = "Bridge that translates wrightty protocol calls into zellij CLI action commands"
|
|
10
|
-
|
|
11
|
-
[[bin]]
|
|
12
|
-
name = "wrightty-bridge-zellij"
|
|
13
|
-
path = "src/main.rs"
|
|
14
|
-
|
|
15
|
-
[dependencies]
|
|
16
|
-
wrightty-protocol = { version = "0.1.0", path = "../wrightty-protocol" }
|
|
17
|
-
|
|
18
|
-
jsonrpsee = { version = "0.24", features = ["server"] }
|
|
19
|
-
tokio = { version = "1", features = ["full"] }
|
|
20
|
-
clap = { version = "4", features = ["derive"] }
|
|
21
|
-
tracing = "0.1"
|
|
22
|
-
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
|
23
|
-
anyhow = "1"
|
|
24
|
-
serde = { version = "1", features = ["derive"] }
|
|
25
|
-
serde_json = "1"
|
|
26
|
-
thiserror = "1"
|
|
27
|
-
tempfile = "3"
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
use std::net::{SocketAddr, TcpListener};
|
|
2
|
-
use std::process;
|
|
3
|
-
use std::time::Duration;
|
|
4
|
-
|
|
5
|
-
use clap::Parser;
|
|
6
|
-
use jsonrpsee::server::Server;
|
|
7
|
-
use tracing_subscriber::EnvFilter;
|
|
8
|
-
|
|
9
|
-
use wrightty_bridge_zellij::rpc::build_rpc_module;
|
|
10
|
-
use wrightty_bridge_zellij::zellij;
|
|
11
|
-
|
|
12
|
-
const PORT_RANGE_START: u16 = 9481;
|
|
13
|
-
const PORT_RANGE_END: u16 = 9500;
|
|
14
|
-
|
|
15
|
-
#[derive(Parser)]
|
|
16
|
-
#[command(
|
|
17
|
-
name = "wrightty-bridge-zellij",
|
|
18
|
-
about = "Bridge that translates wrightty protocol calls into zellij CLI action commands"
|
|
19
|
-
)]
|
|
20
|
-
struct Cli {
|
|
21
|
-
#[arg(long, default_value = "127.0.0.1")]
|
|
22
|
-
host: String,
|
|
23
|
-
|
|
24
|
-
/// Port to listen on. If not specified, auto-selects the next available port starting at 9481.
|
|
25
|
-
#[arg(long)]
|
|
26
|
-
port: Option<u16>,
|
|
27
|
-
|
|
28
|
-
/// Interval in seconds to check if zellij is still running. 0 to disable.
|
|
29
|
-
#[arg(long, default_value_t = 10)]
|
|
30
|
-
watchdog_interval: u64,
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
fn find_available_port(host: &str, start: u16, end: u16) -> Option<u16> {
|
|
34
|
-
for port in start..=end {
|
|
35
|
-
if TcpListener::bind(format!("{host}:{port}")).is_ok() {
|
|
36
|
-
return Some(port);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
None
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
#[tokio::main]
|
|
43
|
-
async fn main() -> anyhow::Result<()> {
|
|
44
|
-
tracing_subscriber::fmt()
|
|
45
|
-
.with_env_filter(
|
|
46
|
-
EnvFilter::from_default_env()
|
|
47
|
-
.add_directive("wrightty_bridge_zellij=info".parse()?),
|
|
48
|
-
)
|
|
49
|
-
.init();
|
|
50
|
-
|
|
51
|
-
let cli = Cli::parse();
|
|
52
|
-
|
|
53
|
-
// --- Startup health check ---
|
|
54
|
-
tracing::info!("Checking zellij connectivity...");
|
|
55
|
-
match zellij::health_check().await {
|
|
56
|
-
Ok(()) => {
|
|
57
|
-
let session = zellij::session_name().unwrap_or_else(|_| "unknown".to_string());
|
|
58
|
-
tracing::info!("zellij is reachable (session: {session})");
|
|
59
|
-
},
|
|
60
|
-
Err(e) => {
|
|
61
|
-
eprintln!("error: Cannot connect to zellij: {e}");
|
|
62
|
-
eprintln!();
|
|
63
|
-
eprintln!("This bridge must run from within a zellij session.");
|
|
64
|
-
eprintln!("Start zellij first:");
|
|
65
|
-
eprintln!(" zellij");
|
|
66
|
-
eprintln!("Then run this bridge from within the session.");
|
|
67
|
-
process::exit(1);
|
|
68
|
-
},
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
let port = match cli.port {
|
|
72
|
-
Some(p) => p,
|
|
73
|
-
None => find_available_port(&cli.host, PORT_RANGE_START, PORT_RANGE_END)
|
|
74
|
-
.ok_or_else(|| anyhow::anyhow!("No available port in range {PORT_RANGE_START}-{PORT_RANGE_END}"))?,
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
let addr: SocketAddr = format!("{}:{}", cli.host, port).parse()?;
|
|
78
|
-
|
|
79
|
-
let module = build_rpc_module()?;
|
|
80
|
-
|
|
81
|
-
let server = Server::builder().build(addr).await?;
|
|
82
|
-
|
|
83
|
-
let handle = server.start(module);
|
|
84
|
-
|
|
85
|
-
let session = zellij::session_name().unwrap_or_else(|_| "unknown".to_string());
|
|
86
|
-
tracing::info!("wrightty-bridge-zellij listening on ws://{addr} (session: {session})");
|
|
87
|
-
println!("wrightty-bridge-zellij listening on ws://{addr}");
|
|
88
|
-
|
|
89
|
-
// --- Watchdog: periodically check zellij is still alive ---
|
|
90
|
-
if cli.watchdog_interval > 0 {
|
|
91
|
-
let interval = Duration::from_secs(cli.watchdog_interval);
|
|
92
|
-
let server_handle = handle.clone();
|
|
93
|
-
tokio::spawn(async move {
|
|
94
|
-
let mut consecutive_failures = 0u32;
|
|
95
|
-
loop {
|
|
96
|
-
tokio::time::sleep(interval).await;
|
|
97
|
-
match zellij::health_check().await {
|
|
98
|
-
Ok(()) => {
|
|
99
|
-
if consecutive_failures > 0 {
|
|
100
|
-
tracing::info!("zellij reconnected");
|
|
101
|
-
consecutive_failures = 0;
|
|
102
|
-
}
|
|
103
|
-
},
|
|
104
|
-
Err(e) => {
|
|
105
|
-
consecutive_failures += 1;
|
|
106
|
-
tracing::warn!(
|
|
107
|
-
"zellij health check failed ({consecutive_failures}): {e}"
|
|
108
|
-
);
|
|
109
|
-
if consecutive_failures >= 3 {
|
|
110
|
-
tracing::error!(
|
|
111
|
-
"zellij unreachable after {consecutive_failures} checks, shutting down"
|
|
112
|
-
);
|
|
113
|
-
server_handle.stop().unwrap();
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
},
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
handle.stopped().await;
|
|
123
|
-
|
|
124
|
-
Ok(())
|
|
125
|
-
}
|