@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,291 +0,0 @@
1
- //! jsonrpsee RPC module that maps wrightty protocol methods to tmux 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::tmux;
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 tmux bridge"))
18
- }
19
-
20
- /// Encode wrightty KeyInput values into tmux key name strings.
21
- ///
22
- /// tmux `send-keys` without `-l` interprets key names like `Enter`, `C-c`, `M-x`, `Up`, etc.
23
- /// We use this for structured key events. For plain text we use `send-keys -l`.
24
- fn encode_key_to_tmux(key: &KeyInput) -> String {
25
- match key {
26
- KeyInput::Shorthand(s) => shorthand_to_tmux(s),
27
- KeyInput::Structured(event) => key_event_to_tmux(event),
28
- }
29
- }
30
-
31
- fn shorthand_to_tmux(s: &str) -> String {
32
- // Modifier combos like "Ctrl+c" -> "C-c", "Alt+x" -> "M-x"
33
- if let Some((modifier, key)) = s.split_once('+') {
34
- match modifier {
35
- "Ctrl" => return format!("C-{}", key.to_lowercase()),
36
- "Alt" => return format!("M-{key}"),
37
- _ => {}
38
- }
39
- }
40
-
41
- // Named keys
42
- match s {
43
- "Enter" | "Return" => "Enter".to_string(),
44
- "Tab" => "Tab".to_string(),
45
- "Backspace" => "BSpace".to_string(),
46
- "Delete" => "Delete".to_string(),
47
- "Escape" | "Esc" => "Escape".to_string(),
48
- "ArrowUp" | "Up" => "Up".to_string(),
49
- "ArrowDown" | "Down" => "Down".to_string(),
50
- "ArrowRight" | "Right" => "Right".to_string(),
51
- "ArrowLeft" | "Left" => "Left".to_string(),
52
- "Home" => "Home".to_string(),
53
- "End" => "End".to_string(),
54
- "PageUp" => "PPage".to_string(),
55
- "PageDown" => "NPage".to_string(),
56
- "Insert" => "Insert".to_string(),
57
- _ => s.to_string(),
58
- }
59
- }
60
-
61
- fn key_event_to_tmux(event: &KeyEvent) -> String {
62
- let has_ctrl = event.modifiers.iter().any(|m| matches!(m, Modifier::Ctrl));
63
- let has_alt = event.modifiers.iter().any(|m| matches!(m, Modifier::Alt));
64
-
65
- let base = match &event.key {
66
- KeyType::Char => event.char.as_deref().unwrap_or("").to_string(),
67
- KeyType::Enter => "Enter".to_string(),
68
- KeyType::Tab => "Tab".to_string(),
69
- KeyType::Backspace => "BSpace".to_string(),
70
- KeyType::Delete => "Delete".to_string(),
71
- KeyType::Escape => "Escape".to_string(),
72
- KeyType::ArrowUp => "Up".to_string(),
73
- KeyType::ArrowDown => "Down".to_string(),
74
- KeyType::ArrowRight => "Right".to_string(),
75
- KeyType::ArrowLeft => "Left".to_string(),
76
- KeyType::Home => "Home".to_string(),
77
- KeyType::End => "End".to_string(),
78
- KeyType::PageUp => "PPage".to_string(),
79
- KeyType::PageDown => "NPage".to_string(),
80
- KeyType::Insert => "Insert".to_string(),
81
- KeyType::F => format!("F{}", event.n.unwrap_or(1)),
82
- };
83
-
84
- let mut result = base;
85
- if has_ctrl {
86
- result = format!("C-{result}");
87
- }
88
- if has_alt {
89
- result = format!("M-{result}");
90
- }
91
- result
92
- }
93
-
94
- pub fn build_rpc_module() -> anyhow::Result<RpcModule<()>> {
95
- let mut module = RpcModule::new(());
96
-
97
- // --- Wrightty.getInfo ---
98
- module.register_async_method("Wrightty.getInfo", |_params, _state, _| async move {
99
- serde_json::to_value(GetInfoResult {
100
- info: ServerInfo {
101
- version: "0.1.0".to_string(),
102
- implementation: "wrightty-bridge-tmux".to_string(),
103
- capabilities: Capabilities {
104
- screenshot: vec![ScreenshotFormat::Text],
105
- max_sessions: 512,
106
- supports_resize: true,
107
- supports_scrollback: true,
108
- supports_mouse: false,
109
- supports_session_create: true,
110
- supports_color_palette: false,
111
- supports_raw_output: false,
112
- supports_shell_integration: false,
113
- events: vec![],
114
- },
115
- },
116
- })
117
- .map_err(|e| proto_err(-32603, e.to_string()))
118
- })?;
119
-
120
- // --- Session.create ---
121
- module.register_async_method("Session.create", |_params, _state, _| async move {
122
- let target = tmux::new_window(None)
123
- .await
124
- .map_err(|e| proto_err(error::SPAWN_FAILED, e.to_string()))?;
125
-
126
- serde_json::to_value(SessionCreateResult { session_id: target })
127
- .map_err(|e| proto_err(-32603, e.to_string()))
128
- })?;
129
-
130
- // --- Session.destroy ---
131
- module.register_async_method("Session.destroy", |params, _state, _| async move {
132
- let p: SessionDestroyParams = params.parse()?;
133
-
134
- tmux::kill_pane(&p.session_id)
135
- .await
136
- .map_err(|e| proto_err(error::SESSION_NOT_FOUND, e.to_string()))?;
137
-
138
- serde_json::to_value(SessionDestroyResult { exit_code: None })
139
- .map_err(|e| proto_err(-32603, e.to_string()))
140
- })?;
141
-
142
- // --- Session.list ---
143
- module.register_async_method("Session.list", |_params, _state, _| async move {
144
- let panes = tmux::list_panes()
145
- .await
146
- .map_err(|e| proto_err(-32603, e.to_string()))?;
147
-
148
- let sessions: Vec<SessionInfo> = panes
149
- .into_iter()
150
- .map(|p| SessionInfo {
151
- session_id: p.target,
152
- title: if p.title.is_empty() {
153
- format!("{}:{}.{}", p.session_name, p.window_index, p.pane_index)
154
- } else {
155
- p.title
156
- },
157
- cwd: None,
158
- cols: p.cols,
159
- rows: p.rows,
160
- pid: if p.pid > 0 { Some(p.pid) } else { None },
161
- running: true,
162
- alternate_screen: false,
163
- })
164
- .collect();
165
-
166
- serde_json::to_value(SessionListResult { sessions })
167
- .map_err(|e| proto_err(-32603, e.to_string()))
168
- })?;
169
-
170
- // --- Session.getInfo ---
171
- module.register_async_method("Session.getInfo", |params, _state, _| async move {
172
- let p: SessionGetInfoParams = params.parse()?;
173
-
174
- let pane = tmux::find_pane(&p.session_id)
175
- .await
176
- .map_err(|e| proto_err(error::SESSION_NOT_FOUND, e.to_string()))?;
177
-
178
- let info = SessionInfo {
179
- session_id: pane.target,
180
- title: if pane.title.is_empty() {
181
- format!("{}:{}.{}", pane.session_name, pane.window_index, pane.pane_index)
182
- } else {
183
- pane.title
184
- },
185
- cwd: None,
186
- cols: pane.cols,
187
- rows: pane.rows,
188
- pid: if pane.pid > 0 { Some(pane.pid) } else { None },
189
- running: true,
190
- alternate_screen: false,
191
- };
192
-
193
- serde_json::to_value(info).map_err(|e| proto_err(-32603, e.to_string()))
194
- })?;
195
-
196
- // --- Input.sendText ---
197
- module.register_async_method("Input.sendText", |params, _state, _| async move {
198
- let p: InputSendTextParams = params.parse()?;
199
-
200
- tmux::send_text(&p.session_id, &p.text)
201
- .await
202
- .map_err(|e| proto_err(error::SESSION_NOT_FOUND, e.to_string()))?;
203
-
204
- Ok::<_, ErrorObjectOwned>(serde_json::json!({}))
205
- })?;
206
-
207
- // --- Input.sendKeys ---
208
- module.register_async_method("Input.sendKeys", |params, _state, _| async move {
209
- let p: InputSendKeysParams = params.parse()?;
210
-
211
- for key in &p.keys {
212
- let tmux_key = encode_key_to_tmux(key);
213
- // Determine if this is literal text (single char shorthand) or a key name
214
- let is_literal = matches!(key, KeyInput::Shorthand(s) if s.len() == 1 && !s.contains('+'));
215
- if is_literal {
216
- tmux::send_text(&p.session_id, &tmux_key)
217
- .await
218
- .map_err(|e| proto_err(error::SESSION_NOT_FOUND, e.to_string()))?;
219
- } else {
220
- tmux::send_key(&p.session_id, &tmux_key)
221
- .await
222
- .map_err(|e| proto_err(error::SESSION_NOT_FOUND, e.to_string()))?;
223
- }
224
- }
225
-
226
- Ok::<_, ErrorObjectOwned>(serde_json::json!({}))
227
- })?;
228
-
229
- // --- Screen.getText ---
230
- module.register_async_method("Screen.getText", |params, _state, _| async move {
231
- let p: ScreenGetTextParams = params.parse()?;
232
-
233
- let mut text = tmux::capture_pane(&p.session_id)
234
- .await
235
- .map_err(|e| proto_err(error::SESSION_NOT_FOUND, e.to_string()))?;
236
-
237
- if p.trim_trailing_whitespace {
238
- text = text
239
- .lines()
240
- .map(|line| line.trim_end())
241
- .collect::<Vec<_>>()
242
- .join("\n");
243
- }
244
-
245
- serde_json::to_value(ScreenGetTextResult { text })
246
- .map_err(|e| proto_err(-32603, e.to_string()))
247
- })?;
248
-
249
- // --- Terminal.getSize ---
250
- module.register_async_method("Terminal.getSize", |params, _state, _| async move {
251
- let p: TerminalGetSizeParams = params.parse()?;
252
-
253
- let pane = tmux::find_pane(&p.session_id)
254
- .await
255
- .map_err(|e| proto_err(error::SESSION_NOT_FOUND, e.to_string()))?;
256
-
257
- serde_json::to_value(TerminalGetSizeResult {
258
- cols: pane.cols,
259
- rows: pane.rows,
260
- })
261
- .map_err(|e| proto_err(-32603, e.to_string()))
262
- })?;
263
-
264
- // --- Terminal.resize ---
265
- module.register_async_method("Terminal.resize", |params, _state, _| async move {
266
- let p: TerminalResizeParams = params.parse()?;
267
-
268
- tmux::resize_pane(&p.session_id, p.cols, p.rows)
269
- .await
270
- .map_err(|e| proto_err(error::SESSION_NOT_FOUND, e.to_string()))?;
271
-
272
- Ok::<_, ErrorObjectOwned>(serde_json::json!({}))
273
- })?;
274
-
275
- // --- Screen.getContents (not supported) ---
276
- module.register_async_method("Screen.getContents", |_params, _state, _| async move {
277
- Err::<serde_json::Value, _>(not_supported("Screen.getContents"))
278
- })?;
279
-
280
- // --- Screen.screenshot (not supported) ---
281
- module.register_async_method("Screen.screenshot", |_params, _state, _| async move {
282
- Err::<serde_json::Value, _>(not_supported("Screen.screenshot"))
283
- })?;
284
-
285
- // --- Input.sendMouse (not supported) ---
286
- module.register_async_method("Input.sendMouse", |_params, _state, _| async move {
287
- Err::<serde_json::Value, _>(not_supported("Input.sendMouse"))
288
- })?;
289
-
290
- Ok(module)
291
- }
@@ -1,215 +0,0 @@
1
- //! Functions that shell out to `tmux` CLI commands.
2
-
3
- use tokio::process::Command;
4
-
5
- #[derive(Debug, Clone)]
6
- pub struct TmuxPane {
7
- /// Full pane target in `<session>:<window>.<pane>` format, e.g. `main:0.1`
8
- pub target: String,
9
- pub session_name: String,
10
- pub window_index: u32,
11
- pub pane_index: u32,
12
- pub cols: u16,
13
- pub rows: u16,
14
- pub title: String,
15
- pub active: bool,
16
- pub pid: u32,
17
- }
18
-
19
- #[derive(Debug, thiserror::Error)]
20
- pub enum TmuxError {
21
- #[error("tmux command failed: {0}")]
22
- CommandFailed(String),
23
- #[error("failed to parse tmux output: {0}")]
24
- ParseError(String),
25
- #[error("pane {0} not found")]
26
- PaneNotFound(String),
27
- #[error("io error: {0}")]
28
- Io(#[from] std::io::Error),
29
- }
30
-
31
- fn tmux_cmd(args: &[&str]) -> Command {
32
- let cmd_str = std::env::var("TMUX_CMD").unwrap_or_else(|_| "tmux".to_string());
33
- let parts: Vec<&str> = cmd_str.split_whitespace().collect();
34
- let (program, prefix_args) = parts.split_first().expect("TMUX_CMD must not be empty");
35
-
36
- let mut cmd = Command::new(program);
37
- for arg in prefix_args {
38
- cmd.arg(arg);
39
- }
40
- for arg in args {
41
- cmd.arg(arg);
42
- }
43
- cmd
44
- }
45
-
46
- /// Check if tmux server is reachable.
47
- pub async fn health_check() -> Result<(), TmuxError> {
48
- let output = tmux_cmd(&["list-sessions"]).output().await?;
49
-
50
- if !output.status.success() {
51
- let stderr = String::from_utf8_lossy(&output.stderr);
52
- return Err(TmuxError::CommandFailed(format!("tmux not reachable: {stderr}")));
53
- }
54
-
55
- Ok(())
56
- }
57
-
58
- /// List all panes across all sessions.
59
- pub async fn list_panes() -> Result<Vec<TmuxPane>, TmuxError> {
60
- // Format: session_name:window_index.pane_index|cols|rows|title|active|pid
61
- let format = "#{session_name}:#{window_index}.#{pane_index}|#{pane_width}|#{pane_height}|#{pane_title}|#{pane_active}|#{pane_pid}";
62
- let output = tmux_cmd(&["list-panes", "-a", "-F", format]).output().await?;
63
-
64
- if !output.status.success() {
65
- let stderr = String::from_utf8_lossy(&output.stderr);
66
- return Err(TmuxError::CommandFailed(stderr.into_owned()));
67
- }
68
-
69
- let stdout = String::from_utf8_lossy(&output.stdout);
70
- let mut panes = Vec::new();
71
-
72
- for line in stdout.lines() {
73
- let line = line.trim();
74
- if line.is_empty() {
75
- continue;
76
- }
77
- let parts: Vec<&str> = line.splitn(6, '|').collect();
78
- if parts.len() < 6 {
79
- continue;
80
- }
81
- let target = parts[0].to_string();
82
- // Parse session:window.pane from target
83
- let (session_name, window_pane) = target
84
- .split_once(':')
85
- .ok_or_else(|| TmuxError::ParseError(format!("bad target: {target}")))?;
86
- let (window_str, pane_str) = window_pane
87
- .split_once('.')
88
- .ok_or_else(|| TmuxError::ParseError(format!("bad target: {target}")))?;
89
-
90
- panes.push(TmuxPane {
91
- target: target.clone(),
92
- session_name: session_name.to_string(),
93
- window_index: window_str.parse().unwrap_or(0),
94
- pane_index: pane_str.parse().unwrap_or(0),
95
- cols: parts[1].parse().unwrap_or(80),
96
- rows: parts[2].parse().unwrap_or(24),
97
- title: parts[3].to_string(),
98
- active: parts[4] == "1",
99
- pid: parts[5].trim().parse().unwrap_or(0),
100
- });
101
- }
102
-
103
- Ok(panes)
104
- }
105
-
106
- /// Get visible screen text for a pane.
107
- pub async fn capture_pane(target: &str) -> Result<String, TmuxError> {
108
- let output = tmux_cmd(&["capture-pane", "-t", target, "-p"]).output().await?;
109
-
110
- if !output.status.success() {
111
- let stderr = String::from_utf8_lossy(&output.stderr);
112
- return Err(TmuxError::CommandFailed(stderr.into_owned()));
113
- }
114
-
115
- Ok(String::from_utf8_lossy(&output.stdout).into_owned())
116
- }
117
-
118
- /// Get scrollback buffer for a pane.
119
- pub async fn capture_scrollback(target: &str) -> Result<String, TmuxError> {
120
- // -S - means start from the beginning of history; -E - means end at current line
121
- let output = tmux_cmd(&["capture-pane", "-t", target, "-p", "-S", "-", "-E", "-"])
122
- .output()
123
- .await?;
124
-
125
- if !output.status.success() {
126
- let stderr = String::from_utf8_lossy(&output.stderr);
127
- return Err(TmuxError::CommandFailed(stderr.into_owned()));
128
- }
129
-
130
- Ok(String::from_utf8_lossy(&output.stdout).into_owned())
131
- }
132
-
133
- /// Send literal text to a pane (no key name interpretation).
134
- pub async fn send_text(target: &str, text: &str) -> Result<(), TmuxError> {
135
- let output = tmux_cmd(&["send-keys", "-t", target, "-l", text]).output().await?;
136
-
137
- if !output.status.success() {
138
- let stderr = String::from_utf8_lossy(&output.stderr);
139
- return Err(TmuxError::CommandFailed(stderr.into_owned()));
140
- }
141
-
142
- Ok(())
143
- }
144
-
145
- /// Send a key sequence to a pane (tmux key name format, e.g. "Enter", "C-c").
146
- pub async fn send_key(target: &str, key: &str) -> Result<(), TmuxError> {
147
- let output = tmux_cmd(&["send-keys", "-t", target, key]).output().await?;
148
-
149
- if !output.status.success() {
150
- let stderr = String::from_utf8_lossy(&output.stderr);
151
- return Err(TmuxError::CommandFailed(stderr.into_owned()));
152
- }
153
-
154
- Ok(())
155
- }
156
-
157
- /// Create a new window and return its pane target.
158
- pub async fn new_window(session: Option<&str>) -> Result<String, TmuxError> {
159
- let mut args = vec!["new-window", "-P", "-F",
160
- "#{session_name}:#{window_index}.#{pane_index}"];
161
- if let Some(s) = session {
162
- args.push("-t");
163
- args.push(s);
164
- }
165
- let output = tmux_cmd(&args).output().await?;
166
-
167
- if !output.status.success() {
168
- let stderr = String::from_utf8_lossy(&output.stderr);
169
- return Err(TmuxError::CommandFailed(stderr.into_owned()));
170
- }
171
-
172
- let stdout = String::from_utf8_lossy(&output.stdout);
173
- Ok(stdout.trim().to_string())
174
- }
175
-
176
- /// Kill a pane.
177
- pub async fn kill_pane(target: &str) -> Result<(), TmuxError> {
178
- let output = tmux_cmd(&["kill-pane", "-t", target]).output().await?;
179
-
180
- if !output.status.success() {
181
- let stderr = String::from_utf8_lossy(&output.stderr);
182
- return Err(TmuxError::CommandFailed(stderr.into_owned()));
183
- }
184
-
185
- Ok(())
186
- }
187
-
188
- /// Resize a pane to exact dimensions.
189
- pub async fn resize_pane(target: &str, cols: u16, rows: u16) -> Result<(), TmuxError> {
190
- let cols_str = cols.to_string();
191
- let rows_str = rows.to_string();
192
- let output = tmux_cmd(&[
193
- "resize-pane", "-t", target,
194
- "-x", &cols_str,
195
- "-y", &rows_str,
196
- ])
197
- .output()
198
- .await?;
199
-
200
- if !output.status.success() {
201
- let stderr = String::from_utf8_lossy(&output.stderr);
202
- return Err(TmuxError::CommandFailed(stderr.into_owned()));
203
- }
204
-
205
- Ok(())
206
- }
207
-
208
- /// Find a pane by its target string.
209
- pub async fn find_pane(target: &str) -> Result<TmuxPane, TmuxError> {
210
- let panes = list_panes().await?;
211
- panes
212
- .into_iter()
213
- .find(|p| p.target == target)
214
- .ok_or_else(|| TmuxError::PaneNotFound(target.to_string()))
215
- }
@@ -1,26 +0,0 @@
1
- [package]
2
- name = "wrightty-bridge-wezterm"
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 wezterm cli commands"
10
-
11
- [[bin]]
12
- name = "wrightty-bridge-wezterm"
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"
@@ -1,2 +0,0 @@
1
- pub mod rpc;
2
- pub mod wezterm;
@@ -1,119 +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_wezterm::rpc::build_rpc_module;
10
- use wrightty_bridge_wezterm::wezterm;
11
-
12
- const PORT_RANGE_START: u16 = 9420;
13
- const PORT_RANGE_END: u16 = 9440;
14
-
15
- #[derive(Parser)]
16
- #[command(
17
- name = "wrightty-bridge-wezterm",
18
- about = "Bridge that translates wrightty protocol calls into wezterm cli 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 9420.
25
- #[arg(long)]
26
- port: Option<u16>,
27
-
28
- /// Interval in seconds to check if WezTerm is still running. 0 to disable.
29
- #[arg(long, default_value_t = 5)]
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_wezterm=info".parse()?),
48
- )
49
- .init();
50
-
51
- let cli = Cli::parse();
52
-
53
- // --- Startup health check ---
54
- tracing::info!("Checking WezTerm connectivity...");
55
- match wezterm::health_check().await {
56
- Ok(()) => tracing::info!("WezTerm is reachable"),
57
- Err(e) => {
58
- eprintln!("error: Cannot connect to WezTerm: {e}");
59
- eprintln!();
60
- eprintln!("Make sure WezTerm is running. If using flatpak, set:");
61
- eprintln!(" WEZTERM_CMD=\"flatpak run --command=wezterm org.wezfurlong.wezterm\"");
62
- process::exit(1);
63
- },
64
- }
65
-
66
- let port = match cli.port {
67
- Some(p) => p,
68
- None => find_available_port(&cli.host, PORT_RANGE_START, PORT_RANGE_END)
69
- .ok_or_else(|| anyhow::anyhow!("No available port in range {PORT_RANGE_START}-{PORT_RANGE_END}"))?,
70
- };
71
-
72
- let addr: SocketAddr = format!("{}:{}", cli.host, port).parse()?;
73
-
74
- let module = build_rpc_module()?;
75
-
76
- let server = Server::builder().build(addr).await?;
77
-
78
- let handle = server.start(module);
79
-
80
- tracing::info!("wrightty-bridge-wezterm listening on ws://{addr}");
81
- println!("wrightty-bridge-wezterm listening on ws://{addr}");
82
-
83
- // --- Watchdog: periodically check WezTerm is still alive ---
84
- if cli.watchdog_interval > 0 {
85
- let interval = Duration::from_secs(cli.watchdog_interval);
86
- let server_handle = handle.clone();
87
- tokio::spawn(async move {
88
- let mut consecutive_failures = 0u32;
89
- loop {
90
- tokio::time::sleep(interval).await;
91
- match wezterm::health_check().await {
92
- Ok(()) => {
93
- if consecutive_failures > 0 {
94
- tracing::info!("WezTerm reconnected");
95
- consecutive_failures = 0;
96
- }
97
- },
98
- Err(e) => {
99
- consecutive_failures += 1;
100
- tracing::warn!(
101
- "WezTerm health check failed ({consecutive_failures}): {e}"
102
- );
103
- if consecutive_failures >= 3 {
104
- tracing::error!(
105
- "WezTerm unreachable after {consecutive_failures} checks, shutting down"
106
- );
107
- server_handle.stop().unwrap();
108
- return;
109
- }
110
- },
111
- }
112
- }
113
- });
114
- }
115
-
116
- handle.stopped().await;
117
-
118
- Ok(())
119
- }