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