@moejay/wrightty 0.0.0 → 0.1.1

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.
Files changed (94) hide show
  1. package/dist/cli.d.ts +3 -0
  2. package/dist/cli.js +144 -0
  3. package/dist/client.d.ts +14 -0
  4. package/dist/client.js +83 -0
  5. package/dist/index.d.ts +3 -0
  6. package/dist/index.js +8 -0
  7. package/dist/terminal.d.ts +48 -0
  8. package/dist/terminal.js +210 -0
  9. package/dist/types.d.ts +90 -0
  10. package/dist/types.js +3 -0
  11. package/package.json +38 -15
  12. package/.github/workflows/ci.yml +0 -90
  13. package/.github/workflows/release.yml +0 -177
  14. package/Cargo.lock +0 -2662
  15. package/Cargo.toml +0 -38
  16. package/PROTOCOL.md +0 -1351
  17. package/README.md +0 -386
  18. package/agents/ceo/AGENTS.md +0 -24
  19. package/agents/ceo/HEARTBEAT.md +0 -72
  20. package/agents/ceo/SOUL.md +0 -33
  21. package/agents/ceo/TOOLS.md +0 -3
  22. package/agents/founding-engineer/AGENTS.md +0 -44
  23. package/crates/wrightty/Cargo.toml +0 -43
  24. package/crates/wrightty/src/client_cmds.rs +0 -366
  25. package/crates/wrightty/src/discover.rs +0 -78
  26. package/crates/wrightty/src/main.rs +0 -100
  27. package/crates/wrightty/src/server.rs +0 -100
  28. package/crates/wrightty/src/term.rs +0 -338
  29. package/crates/wrightty-bridge-ghostty/Cargo.toml +0 -27
  30. package/crates/wrightty-bridge-ghostty/src/ghostty.rs +0 -422
  31. package/crates/wrightty-bridge-ghostty/src/lib.rs +0 -2
  32. package/crates/wrightty-bridge-ghostty/src/main.rs +0 -146
  33. package/crates/wrightty-bridge-ghostty/src/rpc.rs +0 -307
  34. package/crates/wrightty-bridge-kitty/Cargo.toml +0 -26
  35. package/crates/wrightty-bridge-kitty/src/kitty.rs +0 -269
  36. package/crates/wrightty-bridge-kitty/src/lib.rs +0 -2
  37. package/crates/wrightty-bridge-kitty/src/main.rs +0 -124
  38. package/crates/wrightty-bridge-kitty/src/rpc.rs +0 -304
  39. package/crates/wrightty-bridge-tmux/Cargo.toml +0 -26
  40. package/crates/wrightty-bridge-tmux/src/lib.rs +0 -2
  41. package/crates/wrightty-bridge-tmux/src/main.rs +0 -119
  42. package/crates/wrightty-bridge-tmux/src/rpc.rs +0 -291
  43. package/crates/wrightty-bridge-tmux/src/tmux.rs +0 -215
  44. package/crates/wrightty-bridge-wezterm/Cargo.toml +0 -26
  45. package/crates/wrightty-bridge-wezterm/src/lib.rs +0 -2
  46. package/crates/wrightty-bridge-wezterm/src/main.rs +0 -119
  47. package/crates/wrightty-bridge-wezterm/src/rpc.rs +0 -339
  48. package/crates/wrightty-bridge-wezterm/src/wezterm.rs +0 -190
  49. package/crates/wrightty-bridge-zellij/Cargo.toml +0 -27
  50. package/crates/wrightty-bridge-zellij/src/lib.rs +0 -2
  51. package/crates/wrightty-bridge-zellij/src/main.rs +0 -125
  52. package/crates/wrightty-bridge-zellij/src/rpc.rs +0 -328
  53. package/crates/wrightty-bridge-zellij/src/zellij.rs +0 -199
  54. package/crates/wrightty-client/Cargo.toml +0 -16
  55. package/crates/wrightty-client/src/client.rs +0 -254
  56. package/crates/wrightty-client/src/lib.rs +0 -2
  57. package/crates/wrightty-core/Cargo.toml +0 -21
  58. package/crates/wrightty-core/src/input.rs +0 -212
  59. package/crates/wrightty-core/src/lib.rs +0 -4
  60. package/crates/wrightty-core/src/screen.rs +0 -325
  61. package/crates/wrightty-core/src/session.rs +0 -249
  62. package/crates/wrightty-core/src/session_manager.rs +0 -77
  63. package/crates/wrightty-protocol/Cargo.toml +0 -13
  64. package/crates/wrightty-protocol/src/error.rs +0 -8
  65. package/crates/wrightty-protocol/src/events.rs +0 -138
  66. package/crates/wrightty-protocol/src/lib.rs +0 -4
  67. package/crates/wrightty-protocol/src/methods.rs +0 -321
  68. package/crates/wrightty-protocol/src/types.rs +0 -201
  69. package/crates/wrightty-server/Cargo.toml +0 -23
  70. package/crates/wrightty-server/src/lib.rs +0 -2
  71. package/crates/wrightty-server/src/main.rs +0 -65
  72. package/crates/wrightty-server/src/rpc.rs +0 -455
  73. package/crates/wrightty-server/src/state.rs +0 -39
  74. package/examples/basic_command.py +0 -53
  75. package/examples/interactive_tui.py +0 -86
  76. package/examples/record_session.py +0 -96
  77. package/install.sh +0 -81
  78. package/sdks/node/package-lock.json +0 -85
  79. package/sdks/node/package.json +0 -44
  80. package/sdks/node/src/client.ts +0 -94
  81. package/sdks/node/src/index.ts +0 -19
  82. package/sdks/node/src/terminal.ts +0 -258
  83. package/sdks/node/src/types.ts +0 -105
  84. package/sdks/node/tsconfig.json +0 -17
  85. package/sdks/python/README.md +0 -96
  86. package/sdks/python/pyproject.toml +0 -42
  87. package/sdks/python/wrightty/__init__.py +0 -6
  88. package/sdks/python/wrightty/cli.py +0 -210
  89. package/sdks/python/wrightty/client.py +0 -136
  90. package/sdks/python/wrightty/mcp_server.py +0 -434
  91. package/sdks/python/wrightty/terminal.py +0 -333
  92. package/skills/wrightty/SKILL.md +0 -261
  93. package/src/lib.rs +0 -1
  94. 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,2 +0,0 @@
1
- pub mod rpc;
2
- pub mod zellij;
@@ -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
- }