@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,65 +0,0 @@
1
- use std::net::{SocketAddr, TcpListener};
2
-
3
- use clap::Parser;
4
- use jsonrpsee::server::Server;
5
- use tracing_subscriber::EnvFilter;
6
-
7
- use wrightty_server::rpc::build_rpc_module;
8
- use wrightty_server::state::AppState;
9
-
10
- const PORT_RANGE_START: u16 = 9420;
11
- const PORT_RANGE_END: u16 = 9440;
12
-
13
- #[derive(Parser)]
14
- #[command(name = "wrightty-server", about = "Wrightty terminal automation daemon")]
15
- struct Cli {
16
- #[arg(long, default_value = "127.0.0.1")]
17
- host: String,
18
-
19
- /// Port to listen on. If not specified, auto-selects the next available port starting at 9420.
20
- #[arg(long)]
21
- port: Option<u16>,
22
-
23
- #[arg(long, default_value_t = 64)]
24
- max_sessions: usize,
25
- }
26
-
27
- fn find_available_port(host: &str, start: u16, end: u16) -> Option<u16> {
28
- for port in start..=end {
29
- if TcpListener::bind(format!("{host}:{port}")).is_ok() {
30
- return Some(port);
31
- }
32
- }
33
- None
34
- }
35
-
36
- #[tokio::main]
37
- async fn main() -> anyhow::Result<()> {
38
- tracing_subscriber::fmt()
39
- .with_env_filter(EnvFilter::from_default_env().add_directive("wrightty=info".parse()?))
40
- .init();
41
-
42
- let cli = Cli::parse();
43
-
44
- let port = match cli.port {
45
- Some(p) => p,
46
- None => find_available_port(&cli.host, PORT_RANGE_START, PORT_RANGE_END)
47
- .ok_or_else(|| anyhow::anyhow!("No available port in range {PORT_RANGE_START}-{PORT_RANGE_END}"))?,
48
- };
49
-
50
- let addr: SocketAddr = format!("{}:{}", cli.host, port).parse()?;
51
-
52
- let state = AppState::new(cli.max_sessions);
53
- let module = build_rpc_module(state)?;
54
-
55
- let server = Server::builder().build(addr).await?;
56
-
57
- let handle = server.start(module);
58
-
59
- tracing::info!("wrightty-server listening on ws://{addr}");
60
- println!("wrightty-server listening on ws://{addr}");
61
-
62
- handle.stopped().await;
63
-
64
- Ok(())
65
- }
@@ -1,455 +0,0 @@
1
- use std::time::{Duration, Instant};
2
- use std::sync::Arc;
3
-
4
- use jsonrpsee::types::ErrorObjectOwned;
5
- use jsonrpsee::RpcModule;
6
-
7
- use wrightty_core::input;
8
- use wrightty_protocol::error;
9
- use wrightty_protocol::methods::*;
10
- use wrightty_protocol::types::*;
11
-
12
- use crate::state::{AppState, VideoFrame, VideoRecording};
13
-
14
- fn proto_err(code: i32, msg: impl Into<String>) -> ErrorObjectOwned {
15
- ErrorObjectOwned::owned(code, msg.into(), None::<()>)
16
- }
17
-
18
- pub fn build_rpc_module(state: AppState) -> anyhow::Result<RpcModule<AppState>> {
19
- let mut module = RpcModule::new(state);
20
-
21
- // --- Wrightty.getInfo ---
22
- module.register_method("Wrightty.getInfo", |_params, _state, _| {
23
- serde_json::to_value(GetInfoResult {
24
- info: ServerInfo {
25
- version: "0.1.0".to_string(),
26
- implementation: "wrightty-server".to_string(),
27
- capabilities: Capabilities {
28
- screenshot: vec![ScreenshotFormat::Text, ScreenshotFormat::Json],
29
- max_sessions: 64,
30
- supports_resize: true,
31
- supports_scrollback: true,
32
- supports_mouse: false,
33
- supports_session_create: true,
34
- supports_color_palette: false,
35
- supports_raw_output: true,
36
- supports_shell_integration: false,
37
- events: vec![
38
- "Screen.updated".to_string(),
39
- "Session.exited".to_string(),
40
- "Terminal.bell".to_string(),
41
- "Terminal.titleChanged".to_string(),
42
- ],
43
- },
44
- },
45
- })
46
- .map_err(|e| proto_err(-32603, e.to_string()))
47
- })?;
48
-
49
- // --- Session.create ---
50
- module.register_method("Session.create", |params, state, _| {
51
- let p: SessionCreateParams = params.parse()?;
52
- let mut mgr = state.session_manager.lock().unwrap();
53
- let id = mgr
54
- .create(p.shell, p.args, p.cols, p.rows, p.env, p.cwd)
55
- .map_err(|e| proto_err(error::SPAWN_FAILED, e.to_string()))?;
56
- serde_json::to_value(SessionCreateResult { session_id: id })
57
- .map_err(|e| proto_err(-32603, e.to_string()))
58
- })?;
59
-
60
- // --- Session.destroy ---
61
- module.register_method("Session.destroy", |params, state, _| {
62
- let p: SessionDestroyParams = params.parse()?;
63
- let mut mgr = state.session_manager.lock().unwrap();
64
- mgr.destroy(&p.session_id)
65
- .map_err(|_| proto_err(error::SESSION_NOT_FOUND, "session not found"))?;
66
- serde_json::to_value(SessionDestroyResult { exit_code: None })
67
- .map_err(|e| proto_err(-32603, e.to_string()))
68
- })?;
69
-
70
- // --- Session.list ---
71
- module.register_method("Session.list", |_params, state, _| {
72
- let mgr = state.session_manager.lock().unwrap();
73
- let sessions = mgr.list();
74
- serde_json::to_value(SessionListResult { sessions })
75
- .map_err(|e| proto_err(-32603, e.to_string()))
76
- })?;
77
-
78
- // --- Session.getInfo ---
79
- module.register_method("Session.getInfo", |params, state, _| {
80
- let p: SessionGetInfoParams = params.parse()?;
81
- let mgr = state.session_manager.lock().unwrap();
82
- let session = mgr
83
- .get(&p.session_id)
84
- .ok_or_else(|| proto_err(error::SESSION_NOT_FOUND, "session not found"))?;
85
- let (cols, rows) = session.size();
86
- let info = SessionInfo {
87
- session_id: session.id.clone(),
88
- title: session.title.clone(),
89
- cwd: None,
90
- cols,
91
- rows,
92
- pid: None,
93
- running: session.is_running(),
94
- alternate_screen: false,
95
- };
96
- serde_json::to_value(info).map_err(|e| proto_err(-32603, e.to_string()))
97
- })?;
98
-
99
- // --- Input.sendKeys ---
100
- module.register_method("Input.sendKeys", |params, state, _| {
101
- let p: InputSendKeysParams = params.parse()?;
102
- let mut mgr = state.session_manager.lock().unwrap();
103
- let session = mgr
104
- .get_mut(&p.session_id)
105
- .ok_or_else(|| proto_err(error::SESSION_NOT_FOUND, "session not found"))?;
106
-
107
- let bytes = input::encode_keys(&p.keys);
108
- session
109
- .write_bytes(&bytes)
110
- .map_err(|e| proto_err(-32603, e.to_string()))?;
111
-
112
- Ok::<_, ErrorObjectOwned>(serde_json::json!({}))
113
- })?;
114
-
115
- // --- Input.sendText ---
116
- module.register_method("Input.sendText", |params, state, _| {
117
- let p: InputSendTextParams = params.parse()?;
118
- let mut mgr = state.session_manager.lock().unwrap();
119
- let session = mgr
120
- .get_mut(&p.session_id)
121
- .ok_or_else(|| proto_err(error::SESSION_NOT_FOUND, "session not found"))?;
122
-
123
- session
124
- .write_bytes(p.text.as_bytes())
125
- .map_err(|e| proto_err(-32603, e.to_string()))?;
126
-
127
- Ok::<_, ErrorObjectOwned>(serde_json::json!({}))
128
- })?;
129
-
130
- // --- Screen.getContents ---
131
- module.register_method("Screen.getContents", |params, state, _| {
132
- let p: ScreenGetContentsParams = params.parse()?;
133
- let mgr = state.session_manager.lock().unwrap();
134
- let session = mgr
135
- .get(&p.session_id)
136
- .ok_or_else(|| proto_err(error::SESSION_NOT_FOUND, "session not found"))?;
137
-
138
- let data = session.get_contents();
139
- serde_json::to_value(ScreenGetContentsResult {
140
- rows: data.rows,
141
- cols: data.cols,
142
- cursor: data.cursor,
143
- cells: data.cells,
144
- alternate_screen: data.alternate_screen,
145
- })
146
- .map_err(|e| proto_err(-32603, e.to_string()))
147
- })?;
148
-
149
- // --- Screen.getText ---
150
- module.register_method("Screen.getText", |params, state, _| {
151
- let p: ScreenGetTextParams = params.parse()?;
152
- let mgr = state.session_manager.lock().unwrap();
153
- let session = mgr
154
- .get(&p.session_id)
155
- .ok_or_else(|| proto_err(error::SESSION_NOT_FOUND, "session not found"))?;
156
-
157
- let text = session.get_text();
158
- serde_json::to_value(ScreenGetTextResult { text })
159
- .map_err(|e| proto_err(-32603, e.to_string()))
160
- })?;
161
-
162
- // --- Screen.getScrollback ---
163
- module.register_method("Screen.getScrollback", |params, state, _| {
164
- let p: ScreenGetScrollbackParams = params.parse()?;
165
- let mgr = state.session_manager.lock().unwrap();
166
- let session = mgr
167
- .get(&p.session_id)
168
- .ok_or_else(|| proto_err(error::SESSION_NOT_FOUND, "session not found"))?;
169
-
170
- let (lines, total_scrollback) = session.get_scrollback(p.lines, p.offset);
171
- serde_json::to_value(ScreenGetScrollbackResult {
172
- lines,
173
- total_scrollback,
174
- })
175
- .map_err(|e| proto_err(-32603, e.to_string()))
176
- })?;
177
-
178
- // --- Screen.screenshot ---
179
- module.register_method("Screen.screenshot", |params, state, _| {
180
- let p: ScreenScreenshotParams = params.parse()?;
181
- let mgr = state.session_manager.lock().unwrap();
182
- let session = mgr
183
- .get(&p.session_id)
184
- .ok_or_else(|| proto_err(error::SESSION_NOT_FOUND, "session not found"))?;
185
-
186
- match p.format {
187
- ScreenshotFormat::Text => {
188
- let text = session.get_text();
189
- serde_json::to_value(ScreenScreenshotResult {
190
- format: ScreenshotFormat::Text,
191
- data: text,
192
- width: None,
193
- height: None,
194
- })
195
- .map_err(|e| proto_err(-32603, e.to_string()))
196
- }
197
- ScreenshotFormat::Json => {
198
- let data = session.get_contents();
199
- let json_data = serde_json::to_string(&data.cells)
200
- .map_err(|e| proto_err(-32603, e.to_string()))?;
201
- serde_json::to_value(ScreenScreenshotResult {
202
- format: ScreenshotFormat::Json,
203
- data: json_data,
204
- width: Some(data.cols),
205
- height: Some(data.rows),
206
- })
207
- .map_err(|e| proto_err(-32603, e.to_string()))
208
- }
209
- _ => Err(proto_err(error::NOT_SUPPORTED, "screenshot format not supported")),
210
- }
211
- })?;
212
-
213
- // --- Screen.waitForText ---
214
- module.register_async_method("Screen.waitForText", |params, state, _| async move {
215
- let p: ScreenWaitForTextParams = params.parse()?;
216
-
217
- let deadline = Instant::now() + Duration::from_millis(p.timeout);
218
- let interval = Duration::from_millis(p.interval.max(10));
219
-
220
- let re = if p.is_regex {
221
- Some(
222
- regex::Regex::new(&p.pattern)
223
- .map_err(|_| proto_err(error::INVALID_PATTERN, "invalid regex pattern"))?,
224
- )
225
- } else {
226
- None
227
- };
228
-
229
- loop {
230
- let text = {
231
- let mgr = state.session_manager.lock().unwrap();
232
- let session = mgr
233
- .get(&p.session_id)
234
- .ok_or_else(|| proto_err(error::SESSION_NOT_FOUND, "session not found"))?;
235
- session.get_text()
236
- };
237
-
238
- let matched = if let Some(ref re) = re {
239
- re.is_match(&text)
240
- } else {
241
- text.contains(&p.pattern)
242
- };
243
-
244
- if matched {
245
- let elapsed = deadline
246
- .checked_duration_since(Instant::now())
247
- .map(|remaining| p.timeout - remaining.as_millis() as u64)
248
- .unwrap_or(p.timeout);
249
-
250
- let matches = if let Some(ref re) = re {
251
- re.find_iter(&text)
252
- .map(|m| TextMatch {
253
- text: m.as_str().to_string(),
254
- row: 0,
255
- col: 0,
256
- length: m.len() as u32,
257
- })
258
- .collect()
259
- } else {
260
- text.match_indices(p.pattern.as_str())
261
- .map(|(_, s)| TextMatch {
262
- text: s.to_string(),
263
- row: 0,
264
- col: 0,
265
- length: s.len() as u32,
266
- })
267
- .collect()
268
- };
269
-
270
- return serde_json::to_value(ScreenWaitForTextResult {
271
- found: true,
272
- matches,
273
- elapsed,
274
- })
275
- .map_err(|e| proto_err(-32603, e.to_string()));
276
- }
277
-
278
- if Instant::now() >= deadline {
279
- return serde_json::to_value(ScreenWaitForTextResult {
280
- found: false,
281
- matches: vec![],
282
- elapsed: p.timeout,
283
- })
284
- .map_err(|e| proto_err(-32603, e.to_string()));
285
- }
286
-
287
- tokio::time::sleep(interval).await;
288
- }
289
- })?;
290
-
291
- // --- Terminal.resize ---
292
- module.register_method("Terminal.resize", |params, state, _| {
293
- let p: TerminalResizeParams = params.parse()?;
294
- let mut mgr = state.session_manager.lock().unwrap();
295
- let session = mgr
296
- .get_mut(&p.session_id)
297
- .ok_or_else(|| proto_err(error::SESSION_NOT_FOUND, "session not found"))?;
298
-
299
- session
300
- .resize(p.cols, p.rows)
301
- .map_err(|e| proto_err(-32603, e.to_string()))?;
302
-
303
- Ok::<_, ErrorObjectOwned>(serde_json::json!({}))
304
- })?;
305
-
306
- // --- Terminal.getSize ---
307
- module.register_method("Terminal.getSize", |params, state, _| {
308
- let p: TerminalGetSizeParams = params.parse()?;
309
- let mgr = state.session_manager.lock().unwrap();
310
- let session = mgr
311
- .get(&p.session_id)
312
- .ok_or_else(|| proto_err(error::SESSION_NOT_FOUND, "session not found"))?;
313
-
314
- let (cols, rows) = session.size();
315
- serde_json::to_value(TerminalGetSizeResult { cols, rows })
316
- .map_err(|e| proto_err(-32603, e.to_string()))
317
- })?;
318
-
319
- // --- Recording.captureScreen ---
320
- module.register_method("Recording.captureScreen", |params, state, _| {
321
- #[derive(serde::Deserialize)]
322
- #[serde(rename_all = "camelCase")]
323
- struct P { session_id: String }
324
- let p: P = params.parse()?;
325
- let mgr = state.session_manager.lock().unwrap();
326
- let session = mgr
327
- .get(&p.session_id)
328
- .ok_or_else(|| proto_err(error::SESSION_NOT_FOUND, "session not found"))?;
329
- let text = session.get_text();
330
- serde_json::to_value(serde_json::json!({ "data": text, "format": "text" }))
331
- .map_err(|e| proto_err(-32603, e.to_string()))
332
- })?;
333
-
334
- // --- Recording.startVideo ---
335
- module.register_async_method("Recording.startVideo", |params, state, _| async move {
336
- #[derive(serde::Deserialize)]
337
- #[serde(rename_all = "camelCase")]
338
- struct P {
339
- session_id: String,
340
- #[serde(default = "default_interval")]
341
- interval_ms: u64,
342
- }
343
- fn default_interval() -> u64 { 500 }
344
-
345
- let p: P = params.parse()?;
346
-
347
- // Validate session exists and get dimensions
348
- let (cols, rows) = {
349
- let mgr = state.session_manager.lock().unwrap();
350
- let s = mgr.get(&p.session_id)
351
- .ok_or_else(|| proto_err(error::SESSION_NOT_FOUND, "session not found"))?;
352
- s.size()
353
- };
354
-
355
- static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
356
- let rec_id = format!("vid-{}", COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed));
357
-
358
- {
359
- let mut recs = state.video_recordings.lock().unwrap();
360
- recs.insert(rec_id.clone(), VideoRecording {
361
- session_id: p.session_id.clone(),
362
- cols,
363
- rows,
364
- started_at: Instant::now(),
365
- interval_ms: p.interval_ms,
366
- frames: Vec::new(),
367
- running: true,
368
- });
369
- }
370
-
371
- // Spawn background task to capture frames
372
- let recs_arc = Arc::clone(&state.video_recordings);
373
- let rec_id_bg = rec_id.clone();
374
- let session_id_bg = p.session_id.clone();
375
- let session_mgr = Arc::clone(&state.session_manager);
376
- let interval = Duration::from_millis(p.interval_ms);
377
-
378
- tokio::spawn(async move {
379
- loop {
380
- tokio::time::sleep(interval).await;
381
-
382
- let still_running = {
383
- let recs = recs_arc.lock().unwrap();
384
- recs.get(&rec_id_bg).map(|r| r.running).unwrap_or(false)
385
- };
386
- if !still_running { break; }
387
-
388
- let frame_text = {
389
- let mgr = session_mgr.lock().unwrap();
390
- mgr.get(&session_id_bg).map(|s| s.get_text())
391
- };
392
- if let Some(text) = frame_text {
393
- let mut recs = recs_arc.lock().unwrap();
394
- if let Some(rec) = recs.get_mut(&rec_id_bg) {
395
- let elapsed = rec.started_at.elapsed().as_secs_f64();
396
- rec.frames.push(VideoFrame { elapsed_secs: elapsed, text });
397
- }
398
- }
399
- }
400
- });
401
-
402
- serde_json::to_value(serde_json::json!({ "recordingId": rec_id }))
403
- .map_err(|e| proto_err(-32603, e.to_string()))
404
- })?;
405
-
406
- // --- Recording.stopVideo ---
407
- module.register_method("Recording.stopVideo", |params, state, _| {
408
- #[derive(serde::Deserialize)]
409
- #[serde(rename_all = "camelCase")]
410
- struct P { recording_id: String }
411
- let p: P = params.parse()?;
412
-
413
- let rec = {
414
- let mut recs = state.video_recordings.lock().unwrap();
415
- recs.remove(&p.recording_id)
416
- .ok_or_else(|| proto_err(-32001, "recording not found"))?
417
- };
418
-
419
- // Serialise as asciicast v2
420
- let start_ts = std::time::SystemTime::now()
421
- .duration_since(std::time::UNIX_EPOCH)
422
- .unwrap_or_default()
423
- .as_secs();
424
-
425
- let mut cast = String::new();
426
- cast.push_str(&format!(
427
- "{{\"version\":2,\"width\":{},\"height\":{},\"timestamp\":{},\"title\":\"wrightty video\"}}\n",
428
- rec.cols, rec.rows, start_ts
429
- ));
430
-
431
- for frame in &rec.frames {
432
- // Each frame: clear screen then write content
433
- let clear = "\\u001b[2J\\u001b[H";
434
- let data = frame.text.replace('\\', "\\\\")
435
- .replace('"', "\\\"")
436
- .replace('\n', "\\r\\n")
437
- .replace('\r', "\\r");
438
- cast.push_str(&format!(
439
- "[{:.6},\"o\",\"{}{}\"]\\n",
440
- frame.elapsed_secs, clear, data
441
- ));
442
- }
443
- // Remove the trailing escaped newline and fix
444
- let cast = cast.replace("\\n", "\n");
445
-
446
- serde_json::to_value(serde_json::json!({
447
- "data": cast,
448
- "format": "asciicast",
449
- "frameCount": rec.frames.len()
450
- }))
451
- .map_err(|e| proto_err(-32603, e.to_string()))
452
- })?;
453
-
454
- Ok(module)
455
- }
@@ -1,39 +0,0 @@
1
- use std::collections::HashMap;
2
- use std::sync::{Arc, Mutex};
3
- use std::time::Instant;
4
-
5
- use wrightty_core::session_manager::SessionManager;
6
-
7
- /// A single frame captured during a video recording.
8
- #[derive(Clone)]
9
- pub struct VideoFrame {
10
- pub elapsed_secs: f64,
11
- pub text: String,
12
- }
13
-
14
- /// State for an active video recording.
15
- pub struct VideoRecording {
16
- pub session_id: String,
17
- pub cols: u16,
18
- pub rows: u16,
19
- pub started_at: Instant,
20
- pub interval_ms: u64,
21
- pub frames: Vec<VideoFrame>,
22
- pub running: bool,
23
- }
24
-
25
- #[derive(Clone)]
26
- pub struct AppState {
27
- pub session_manager: Arc<Mutex<SessionManager>>,
28
- /// Active video recordings keyed by recording ID.
29
- pub video_recordings: Arc<Mutex<HashMap<String, VideoRecording>>>,
30
- }
31
-
32
- impl AppState {
33
- pub fn new(max_sessions: usize) -> Self {
34
- Self {
35
- session_manager: Arc::new(Mutex::new(SessionManager::new(max_sessions))),
36
- video_recordings: Arc::new(Mutex::new(HashMap::new())),
37
- }
38
- }
39
- }
@@ -1,53 +0,0 @@
1
- #!/usr/bin/env python3
2
- """basic_command.py -- Connect to wrightty, run a command, read output, disconnect.
3
-
4
- Usage:
5
- python examples/basic_command.py
6
-
7
- Prerequisites:
8
- - wrightty-server running on ws://127.0.0.1:9420
9
- Start with: cargo run -p wrightty-server
10
-
11
- - Python SDK installed (from repo root):
12
- pip install -e sdks/python
13
- """
14
-
15
- from wrightty import Terminal
16
-
17
-
18
- def main():
19
- print("Connecting to wrightty server...")
20
-
21
- # Connect to a running server (auto-discovers on ports 9420-9440)
22
- with Terminal.connect() as term:
23
- info = term.get_info()
24
- print(f"Connected to {info['implementation']} v{info['version']}")
25
- print()
26
-
27
- # Run a simple command and capture output
28
- print("Running: uname -a")
29
- output = term.run("uname -a")
30
- print(f"Output:\n {output}")
31
- print()
32
-
33
- # Run a multi-line command
34
- print("Running: ls -la /tmp | head -5")
35
- output = term.run("ls -la /tmp | head -5")
36
- print("Output:")
37
- for line in output.splitlines():
38
- print(f" {line}")
39
- print()
40
-
41
- # Check the current directory
42
- output = term.run("pwd")
43
- print(f"Working directory: {output.strip()}")
44
-
45
- # Get terminal dimensions
46
- cols, rows = term.get_size()
47
- print(f"Terminal size: {cols}x{rows}")
48
-
49
- print("\nDone. Connection closed.")
50
-
51
-
52
- if __name__ == "__main__":
53
- main()
@@ -1,86 +0,0 @@
1
- #!/usr/bin/env python3
2
- """interactive_tui.py -- Launch a TUI app, interact with it, take a screenshot, exit.
3
-
4
- Demonstrates how to:
5
- - Spawn a fresh terminal session via wrightty-server
6
- - Launch an interactive TUI (htop or top as fallback)
7
- - Wait for the UI to render
8
- - Take an SVG screenshot
9
- - Send keystrokes to exit cleanly
10
-
11
- Usage:
12
- python examples/interactive_tui.py
13
-
14
- Prerequisites:
15
- - wrightty-server running on ws://127.0.0.1:9420
16
- Start with: cargo run -p wrightty-server
17
-
18
- - Python SDK installed (from repo root):
19
- pip install -e sdks/python
20
-
21
- - htop installed (or falls back to top)
22
- """
23
-
24
- import sys
25
-
26
- from wrightty import Terminal
27
-
28
-
29
- def main():
30
- print("Spawning a new terminal session on wrightty-server...")
31
-
32
- # Spawn creates a fresh PTY session via wrightty-server daemon
33
- with Terminal.spawn(cols=120, rows=40) as term:
34
- info = term.get_info()
35
- print(f"Connected to {info['implementation']} v{info['version']}")
36
- print()
37
-
38
- # Try htop first, fall back to top
39
- print("Launching htop (or top as fallback)...")
40
- term.send_text("htop 2>/dev/null || top\n")
41
-
42
- try:
43
- # htop shows "Load average" in its header
44
- screen = term.wait_for("Load average", timeout=5)
45
- tui_name = "htop"
46
- except TimeoutError:
47
- try:
48
- # top shows "load average" (lowercase)
49
- screen = term.wait_for("load average", timeout=5)
50
- tui_name = "top"
51
- except TimeoutError:
52
- print("Neither htop nor top appeared. Exiting.")
53
- term.send_keys("Ctrl+c")
54
- sys.exit(1)
55
-
56
- print(f"{tui_name} is running. Taking a screenshot...")
57
-
58
- # Capture an SVG screenshot of the rendered terminal
59
- screenshot_svg = term.screenshot(format="svg")
60
- out_path = "/tmp/wrightty_tui_screenshot.svg"
61
- with open(out_path, "w") as f:
62
- f.write(screenshot_svg)
63
- print(f"Screenshot saved to {out_path}")
64
- print()
65
-
66
- # Show a snippet of the current screen text
67
- screen_text = term.read_screen()
68
- lines = screen_text.splitlines()
69
- print("Screen preview (first 5 lines):")
70
- for line in lines[:5]:
71
- print(f" {line}")
72
- print()
73
-
74
- # Exit htop with 'q', or top with 'q' as well
75
- print(f"Sending 'q' to exit {tui_name}...")
76
- term.send_keys("q")
77
-
78
- # Wait for the shell prompt to return
79
- term.wait_for_prompt(timeout=5)
80
- print("Exited cleanly. Shell prompt is back.")
81
-
82
- print("\nDone. Session closed.")
83
-
84
-
85
- if __name__ == "__main__":
86
- main()