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