@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,422 +0,0 @@
1
- //! Ghostty IPC client.
2
- //!
3
- //! Ghostty exposes a Unix domain socket for inter-process communication.
4
- //! The socket path can be configured via the `GHOSTTY_SOCKET` environment variable,
5
- //! defaulting to `$XDG_RUNTIME_DIR/ghostty/sock` on Linux and
6
- //! `$TMPDIR/ghostty-<uid>.sock` on macOS.
7
- //!
8
- //! Messages are exchanged as newline-delimited JSON (one JSON object per line).
9
- //! Requests use `{"type":"<command>", ...fields}` and responses mirror the
10
- //! same structure with a `"result"` field.
11
- //!
12
- //! Input (text / key injection) is delegated to `xdotool` on Linux (X11) or
13
- //! `osascript` on macOS because Ghostty does not yet expose a send-text IPC
14
- //! call. Set `GHOSTTY_INPUT_BACKEND=xdotool|osascript|none` to override.
15
-
16
- use std::env;
17
-
18
- use serde::{Deserialize, Serialize};
19
- use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
20
- use tokio::net::UnixStream;
21
- use tokio::process::Command;
22
-
23
- // ---------------------------------------------------------------------------
24
- // Public types
25
- // ---------------------------------------------------------------------------
26
-
27
- /// A Ghostty window as returned by the `list_windows` IPC call.
28
- #[derive(Debug, Clone, Deserialize)]
29
- pub struct GhosttyWindow {
30
- pub id: u64,
31
- pub title: String,
32
- pub is_focused: bool,
33
- pub cols: u16,
34
- pub rows: u16,
35
- #[serde(default)]
36
- pub pid: Option<u32>,
37
- #[serde(default)]
38
- pub cwd: Option<String>,
39
- }
40
-
41
- // ---------------------------------------------------------------------------
42
- // Error type
43
- // ---------------------------------------------------------------------------
44
-
45
- #[derive(Debug, thiserror::Error)]
46
- pub enum GhosttyError {
47
- #[error("ghostty socket not found at {0}; is Ghostty running?")]
48
- SocketNotFound(String),
49
- #[error("ghostty IPC call failed: {0}")]
50
- IpcFailed(String),
51
- #[error("failed to parse ghostty response: {0}")]
52
- ParseError(String),
53
- #[error("window {0} not found")]
54
- WindowNotFound(u64),
55
- #[error("input backend error: {0}")]
56
- InputBackend(String),
57
- #[error("io error: {0}")]
58
- Io(#[from] std::io::Error),
59
- }
60
-
61
- // ---------------------------------------------------------------------------
62
- // Socket helpers
63
- // ---------------------------------------------------------------------------
64
-
65
- fn socket_path() -> String {
66
- if let Ok(p) = env::var("GHOSTTY_SOCKET") {
67
- return p;
68
- }
69
-
70
- #[cfg(target_os = "macos")]
71
- {
72
- let uid = unsafe { libc::getuid() };
73
- let tmp = env::var("TMPDIR").unwrap_or_else(|_| "/tmp".to_string());
74
- return format!("{tmp}/ghostty-{uid}.sock");
75
- }
76
-
77
- // Linux default: $XDG_RUNTIME_DIR/ghostty/sock
78
- let runtime_dir = env::var("XDG_RUNTIME_DIR")
79
- .unwrap_or_else(|_| format!("/run/user/{}", unsafe { libc::getuid() }));
80
- format!("{runtime_dir}/ghostty/sock")
81
- }
82
-
83
- /// Open a connection to the Ghostty IPC socket.
84
- async fn connect() -> Result<UnixStream, GhosttyError> {
85
- let path = socket_path();
86
- UnixStream::connect(&path)
87
- .await
88
- .map_err(|_| GhosttyError::SocketNotFound(path))
89
- }
90
-
91
- // ---------------------------------------------------------------------------
92
- // IPC request / response
93
- // ---------------------------------------------------------------------------
94
-
95
- #[derive(Serialize)]
96
- struct IpcRequest<'a, T: Serialize> {
97
- #[serde(rename = "type")]
98
- kind: &'a str,
99
- #[serde(flatten)]
100
- payload: T,
101
- }
102
-
103
- #[derive(Deserialize, Debug)]
104
- struct IpcResponse {
105
- #[serde(default)]
106
- error: Option<String>,
107
- #[serde(flatten)]
108
- fields: serde_json::Value,
109
- }
110
-
111
- /// Send one IPC request and read the response line.
112
- async fn ipc_call<T: Serialize>(
113
- kind: &str,
114
- payload: T,
115
- ) -> Result<serde_json::Value, GhosttyError> {
116
- let mut stream = connect().await?;
117
-
118
- let req = IpcRequest { kind, payload };
119
- let mut line = serde_json::to_string(&req)
120
- .map_err(|e| GhosttyError::ParseError(e.to_string()))?;
121
- line.push('\n');
122
-
123
- stream
124
- .write_all(line.as_bytes())
125
- .await
126
- .map_err(GhosttyError::Io)?;
127
-
128
- let mut reader = BufReader::new(stream);
129
- let mut resp_line = String::new();
130
- reader
131
- .read_line(&mut resp_line)
132
- .await
133
- .map_err(GhosttyError::Io)?;
134
-
135
- let resp: IpcResponse = serde_json::from_str(resp_line.trim())
136
- .map_err(|e| GhosttyError::ParseError(format!("{e}: {resp_line}")))?;
137
-
138
- if let Some(err) = resp.error {
139
- return Err(GhosttyError::IpcFailed(err));
140
- }
141
-
142
- Ok(resp.fields)
143
- }
144
-
145
- // ---------------------------------------------------------------------------
146
- // Public API
147
- // ---------------------------------------------------------------------------
148
-
149
- /// Check that Ghostty is reachable by listing windows.
150
- pub async fn health_check() -> Result<(), GhosttyError> {
151
- list_windows().await?;
152
- Ok(())
153
- }
154
-
155
- /// List all Ghostty windows.
156
- pub async fn list_windows() -> Result<Vec<GhosttyWindow>, GhosttyError> {
157
- #[derive(Serialize)]
158
- struct Empty {}
159
- let value = ipc_call("list_windows", Empty {}).await?;
160
-
161
- let windows: Vec<GhosttyWindow> = serde_json::from_value(
162
- value
163
- .get("windows")
164
- .cloned()
165
- .unwrap_or(serde_json::Value::Array(vec![])),
166
- )
167
- .map_err(|e| GhosttyError::ParseError(e.to_string()))?;
168
-
169
- Ok(windows)
170
- }
171
-
172
- /// Find a window by its numeric ID.
173
- pub async fn find_window(window_id: u64) -> Result<GhosttyWindow, GhosttyError> {
174
- let windows = list_windows().await?;
175
- windows
176
- .into_iter()
177
- .find(|w| w.id == window_id)
178
- .ok_or(GhosttyError::WindowNotFound(window_id))
179
- }
180
-
181
- /// Open a new Ghostty window and return its ID.
182
- pub async fn new_window() -> Result<u64, GhosttyError> {
183
- #[derive(Serialize)]
184
- struct NewWindowReq {
185
- action: &'static str,
186
- }
187
- let value = ipc_call("action", NewWindowReq { action: "new_window" }).await?;
188
-
189
- let window_id = value
190
- .get("window_id")
191
- .and_then(|v| v.as_u64())
192
- .ok_or_else(|| GhosttyError::ParseError("missing window_id in new_window response".into()))?;
193
-
194
- Ok(window_id)
195
- }
196
-
197
- /// Close a Ghostty window.
198
- pub async fn close_window(window_id: u64) -> Result<(), GhosttyError> {
199
- #[derive(Serialize)]
200
- struct CloseReq {
201
- action: &'static str,
202
- window_id: u64,
203
- }
204
- ipc_call(
205
- "action",
206
- CloseReq {
207
- action: "close_window",
208
- window_id,
209
- },
210
- )
211
- .await?;
212
- Ok(())
213
- }
214
-
215
- /// Focus a Ghostty window (bring it to front).
216
- pub async fn focus_window(window_id: u64) -> Result<(), GhosttyError> {
217
- #[derive(Serialize)]
218
- struct FocusReq {
219
- action: &'static str,
220
- window_id: u64,
221
- }
222
- ipc_call(
223
- "action",
224
- FocusReq {
225
- action: "focus_window",
226
- window_id,
227
- },
228
- )
229
- .await?;
230
- Ok(())
231
- }
232
-
233
- // ---------------------------------------------------------------------------
234
- // Input injection
235
- // ---------------------------------------------------------------------------
236
-
237
- /// The backend to use for injecting keystrokes / text.
238
- #[derive(Debug, Clone, PartialEq)]
239
- pub enum InputBackend {
240
- /// `xdotool type` / `xdotool key` (Linux X11)
241
- Xdotool,
242
- /// `osascript` System Events keystroke (macOS)
243
- Osascript,
244
- /// No-op (useful for testing or headless envs)
245
- None,
246
- }
247
-
248
- impl InputBackend {
249
- pub fn detect() -> Self {
250
- if let Ok(val) = env::var("GHOSTTY_INPUT_BACKEND") {
251
- return match val.to_lowercase().as_str() {
252
- "xdotool" => Self::Xdotool,
253
- "osascript" => Self::Osascript,
254
- "none" => Self::None,
255
- _ => Self::detect_auto(),
256
- };
257
- }
258
- Self::detect_auto()
259
- }
260
-
261
- fn detect_auto() -> Self {
262
- #[cfg(target_os = "macos")]
263
- return Self::Osascript;
264
-
265
- // Linux: probe xdotool
266
- if std::process::Command::new("xdotool")
267
- .arg("version")
268
- .output()
269
- .map(|o| o.status.success())
270
- .unwrap_or(false)
271
- {
272
- return Self::Xdotool;
273
- }
274
-
275
- Self::None
276
- }
277
- }
278
-
279
- /// Send literal text to the focused Ghostty window.
280
- ///
281
- /// The caller is responsible for focusing the target window first.
282
- pub async fn send_text(window_id: u64, text: &str) -> Result<(), GhosttyError> {
283
- focus_window(window_id).await?;
284
- // Small yield to let the WM process the focus event before we inject
285
- tokio::time::sleep(std::time::Duration::from_millis(30)).await;
286
-
287
- match InputBackend::detect() {
288
- InputBackend::Xdotool => xdotool_type(text).await,
289
- InputBackend::Osascript => osascript_keystroke(text).await,
290
- InputBackend::None => Err(GhosttyError::InputBackend(
291
- "no input backend available; install xdotool or set GHOSTTY_INPUT_BACKEND".into(),
292
- )),
293
- }
294
- }
295
-
296
- /// Send a key name (e.g. `"ctrl+c"`, `"Return"`) to the focused window.
297
- pub async fn send_key(window_id: u64, key: &str) -> Result<(), GhosttyError> {
298
- focus_window(window_id).await?;
299
- tokio::time::sleep(std::time::Duration::from_millis(30)).await;
300
-
301
- match InputBackend::detect() {
302
- InputBackend::Xdotool => xdotool_key(key).await,
303
- InputBackend::Osascript => osascript_key(key).await,
304
- InputBackend::None => Err(GhosttyError::InputBackend(
305
- "no input backend available; install xdotool or set GHOSTTY_INPUT_BACKEND".into(),
306
- )),
307
- }
308
- }
309
-
310
- // ---------------------------------------------------------------------------
311
- // xdotool helpers
312
- // ---------------------------------------------------------------------------
313
-
314
- async fn xdotool_type(text: &str) -> Result<(), GhosttyError> {
315
- let output = Command::new("xdotool")
316
- .args(["type", "--clearmodifiers", "--delay", "0", "--", text])
317
- .output()
318
- .await
319
- .map_err(GhosttyError::Io)?;
320
-
321
- if !output.status.success() {
322
- let stderr = String::from_utf8_lossy(&output.stderr);
323
- return Err(GhosttyError::InputBackend(format!(
324
- "xdotool type failed: {stderr}"
325
- )));
326
- }
327
- Ok(())
328
- }
329
-
330
- async fn xdotool_key(key: &str) -> Result<(), GhosttyError> {
331
- let output = Command::new("xdotool")
332
- .args(["key", "--clearmodifiers", "--", key])
333
- .output()
334
- .await
335
- .map_err(GhosttyError::Io)?;
336
-
337
- if !output.status.success() {
338
- let stderr = String::from_utf8_lossy(&output.stderr);
339
- return Err(GhosttyError::InputBackend(format!(
340
- "xdotool key failed: {stderr}"
341
- )));
342
- }
343
- Ok(())
344
- }
345
-
346
- // ---------------------------------------------------------------------------
347
- // osascript helpers (macOS)
348
- // ---------------------------------------------------------------------------
349
-
350
- async fn osascript_keystroke(text: &str) -> Result<(), GhosttyError> {
351
- // Escape the string for AppleScript
352
- let escaped = text.replace('\\', "\\\\").replace('"', "\\\"");
353
- let script = format!(
354
- r#"tell application "System Events" to keystroke "{escaped}""#
355
- );
356
- let output = Command::new("osascript")
357
- .args(["-e", &script])
358
- .output()
359
- .await
360
- .map_err(GhosttyError::Io)?;
361
-
362
- if !output.status.success() {
363
- let stderr = String::from_utf8_lossy(&output.stderr);
364
- return Err(GhosttyError::InputBackend(format!(
365
- "osascript keystroke failed: {stderr}"
366
- )));
367
- }
368
- Ok(())
369
- }
370
-
371
- async fn osascript_key(key: &str) -> Result<(), GhosttyError> {
372
- // Map wrightty key names to osascript key code names
373
- let key_code = map_key_to_applescript(key);
374
- let script = format!(
375
- r#"tell application "System Events" to key code {key_code}"#
376
- );
377
- let output = Command::new("osascript")
378
- .args(["-e", &script])
379
- .output()
380
- .await
381
- .map_err(GhosttyError::Io)?;
382
-
383
- if !output.status.success() {
384
- let stderr = String::from_utf8_lossy(&output.stderr);
385
- return Err(GhosttyError::InputBackend(format!(
386
- "osascript key code failed: {stderr}"
387
- )));
388
- }
389
- Ok(())
390
- }
391
-
392
- /// Map a key name (xdotool-style) to an AppleScript key code number.
393
- fn map_key_to_applescript(key: &str) -> &'static str {
394
- match key {
395
- "Return" | "return" | "KP_Return" => "36",
396
- "Tab" | "tab" => "48",
397
- "BackSpace" | "backspace" => "51",
398
- "Delete" | "delete" => "117",
399
- "Escape" | "escape" => "53",
400
- "Up" | "KP_Up" => "126",
401
- "Down" | "KP_Down" => "125",
402
- "Left" | "KP_Left" => "123",
403
- "Right" | "KP_Right" => "124",
404
- "Home" => "115",
405
- "End" => "119",
406
- "Page_Up" => "116",
407
- "Page_Down" => "121",
408
- "F1" => "122",
409
- "F2" => "120",
410
- "F3" => "99",
411
- "F4" => "118",
412
- "F5" => "96",
413
- "F6" => "97",
414
- "F7" => "98",
415
- "F8" => "100",
416
- "F9" => "101",
417
- "F10" => "109",
418
- "F11" => "103",
419
- "F12" => "111",
420
- _ => "36", // fall back to Return
421
- }
422
- }
@@ -1,2 +0,0 @@
1
- pub mod ghostty;
2
- pub mod rpc;
@@ -1,146 +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_ghostty::ghostty;
10
- use wrightty_bridge_ghostty::rpc::build_rpc_module;
11
-
12
- const PORT_RANGE_START: u16 = 9501;
13
- const PORT_RANGE_END: u16 = 9520;
14
-
15
- #[derive(Parser)]
16
- #[command(
17
- name = "wrightty-bridge-ghostty",
18
- about = "Bridge that translates wrightty protocol calls into Ghostty IPC commands",
19
- long_about = "\
20
- Connects to a running Ghostty terminal emulator via its Unix IPC socket and \
21
- exposes the wrightty WebSocket JSON-RPC 2.0 interface.\n\n\
22
- REQUIREMENTS:\n\
23
- - Ghostty must be running (the IPC socket is created on startup).\n\
24
- - For text/key injection, xdotool must be installed on Linux (X11) or\n\
25
- Accessibility must be enabled on macOS.\n\n\
26
- ENVIRONMENT:\n\
27
- GHOSTTY_SOCKET Override the IPC socket path.\n\
28
- GHOSTTY_INPUT_BACKEND Force input backend: xdotool | osascript | none."
29
- )]
30
- struct Cli {
31
- #[arg(long, default_value = "127.0.0.1")]
32
- host: String,
33
-
34
- /// Port to listen on. If not specified, auto-selects the next available
35
- /// port starting at 9481.
36
- #[arg(long)]
37
- port: Option<u16>,
38
-
39
- /// Interval in seconds to check if Ghostty is still running. 0 to disable.
40
- #[arg(long, default_value_t = 10)]
41
- watchdog_interval: u64,
42
- }
43
-
44
- fn find_available_port(host: &str, start: u16, end: u16) -> Option<u16> {
45
- for port in start..=end {
46
- if TcpListener::bind(format!("{host}:{port}")).is_ok() {
47
- return Some(port);
48
- }
49
- }
50
- None
51
- }
52
-
53
- #[tokio::main]
54
- async fn main() -> anyhow::Result<()> {
55
- tracing_subscriber::fmt()
56
- .with_env_filter(
57
- EnvFilter::from_default_env()
58
- .add_directive("wrightty_bridge_ghostty=info".parse()?),
59
- )
60
- .init();
61
-
62
- let cli = Cli::parse();
63
-
64
- // --- Startup health check ---
65
- tracing::info!("Checking Ghostty connectivity...");
66
- match ghostty::health_check().await {
67
- Ok(()) => tracing::info!("Ghostty IPC socket is reachable"),
68
- Err(e) => {
69
- eprintln!("error: Cannot connect to Ghostty: {e}");
70
- eprintln!();
71
- eprintln!("Make sure Ghostty is running. The bridge connects to:");
72
- eprintln!(" $XDG_RUNTIME_DIR/ghostty/sock (Linux)");
73
- eprintln!(" $TMPDIR/ghostty-<uid>.sock (macOS)");
74
- eprintln!("Override with: GHOSTTY_SOCKET=/path/to/sock");
75
- process::exit(1);
76
- }
77
- }
78
-
79
- // Report which input backend will be used
80
- let backend = ghostty::InputBackend::detect();
81
- if backend == ghostty::InputBackend::None {
82
- tracing::warn!(
83
- "No input backend detected. \
84
- Install xdotool (Linux/X11) or enable Accessibility (macOS) for \
85
- Input.sendText / Input.sendKeys support."
86
- );
87
- } else {
88
- tracing::info!("Input backend: {:?}", backend);
89
- }
90
-
91
- let port = match cli.port {
92
- Some(p) => p,
93
- None => find_available_port(&cli.host, PORT_RANGE_START, PORT_RANGE_END)
94
- .ok_or_else(|| {
95
- anyhow::anyhow!(
96
- "No available port in range {PORT_RANGE_START}-{PORT_RANGE_END}"
97
- )
98
- })?,
99
- };
100
-
101
- let addr: SocketAddr = format!("{}:{}", cli.host, port).parse()?;
102
-
103
- let module = build_rpc_module()?;
104
- let server = Server::builder().build(addr).await?;
105
- let handle = server.start(module);
106
-
107
- tracing::info!("wrightty-bridge-ghostty listening on ws://{addr}");
108
- println!("wrightty-bridge-ghostty listening on ws://{addr}");
109
-
110
- // --- Watchdog: periodically check Ghostty is still running ---
111
- if cli.watchdog_interval > 0 {
112
- let interval = Duration::from_secs(cli.watchdog_interval);
113
- let server_handle = handle.clone();
114
- tokio::spawn(async move {
115
- let mut consecutive_failures = 0u32;
116
- loop {
117
- tokio::time::sleep(interval).await;
118
- match ghostty::health_check().await {
119
- Ok(()) => {
120
- if consecutive_failures > 0 {
121
- tracing::info!("Ghostty reconnected");
122
- consecutive_failures = 0;
123
- }
124
- }
125
- Err(e) => {
126
- consecutive_failures += 1;
127
- tracing::warn!(
128
- "Ghostty health check failed ({consecutive_failures}): {e}"
129
- );
130
- if consecutive_failures >= 3 {
131
- tracing::error!(
132
- "Ghostty unreachable after {consecutive_failures} checks, shutting down"
133
- );
134
- server_handle.stop().unwrap();
135
- return;
136
- }
137
- }
138
- }
139
- }
140
- });
141
- }
142
-
143
- handle.stopped().await;
144
-
145
- Ok(())
146
- }