@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.
- package/dist/client.d.ts +14 -0
- package/dist/client.js +83 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +8 -0
- package/dist/terminal.d.ts +48 -0
- package/dist/terminal.js +210 -0
- package/dist/types.d.ts +90 -0
- package/dist/types.js +3 -0
- package/package.json +35 -15
- package/.github/workflows/ci.yml +0 -90
- package/.github/workflows/release.yml +0 -177
- package/Cargo.lock +0 -2662
- package/Cargo.toml +0 -38
- package/PROTOCOL.md +0 -1351
- package/README.md +0 -386
- package/agents/ceo/AGENTS.md +0 -24
- package/agents/ceo/HEARTBEAT.md +0 -72
- package/agents/ceo/SOUL.md +0 -33
- package/agents/ceo/TOOLS.md +0 -3
- package/agents/founding-engineer/AGENTS.md +0 -44
- package/crates/wrightty/Cargo.toml +0 -43
- package/crates/wrightty/src/client_cmds.rs +0 -366
- package/crates/wrightty/src/discover.rs +0 -78
- package/crates/wrightty/src/main.rs +0 -100
- package/crates/wrightty/src/server.rs +0 -100
- package/crates/wrightty/src/term.rs +0 -338
- package/crates/wrightty-bridge-ghostty/Cargo.toml +0 -27
- package/crates/wrightty-bridge-ghostty/src/ghostty.rs +0 -422
- package/crates/wrightty-bridge-ghostty/src/lib.rs +0 -2
- package/crates/wrightty-bridge-ghostty/src/main.rs +0 -146
- package/crates/wrightty-bridge-ghostty/src/rpc.rs +0 -307
- package/crates/wrightty-bridge-kitty/Cargo.toml +0 -26
- package/crates/wrightty-bridge-kitty/src/kitty.rs +0 -269
- package/crates/wrightty-bridge-kitty/src/lib.rs +0 -2
- package/crates/wrightty-bridge-kitty/src/main.rs +0 -124
- package/crates/wrightty-bridge-kitty/src/rpc.rs +0 -304
- package/crates/wrightty-bridge-tmux/Cargo.toml +0 -26
- package/crates/wrightty-bridge-tmux/src/lib.rs +0 -2
- package/crates/wrightty-bridge-tmux/src/main.rs +0 -119
- package/crates/wrightty-bridge-tmux/src/rpc.rs +0 -291
- package/crates/wrightty-bridge-tmux/src/tmux.rs +0 -215
- package/crates/wrightty-bridge-wezterm/Cargo.toml +0 -26
- package/crates/wrightty-bridge-wezterm/src/lib.rs +0 -2
- package/crates/wrightty-bridge-wezterm/src/main.rs +0 -119
- package/crates/wrightty-bridge-wezterm/src/rpc.rs +0 -339
- package/crates/wrightty-bridge-wezterm/src/wezterm.rs +0 -190
- package/crates/wrightty-bridge-zellij/Cargo.toml +0 -27
- package/crates/wrightty-bridge-zellij/src/lib.rs +0 -2
- package/crates/wrightty-bridge-zellij/src/main.rs +0 -125
- package/crates/wrightty-bridge-zellij/src/rpc.rs +0 -328
- package/crates/wrightty-bridge-zellij/src/zellij.rs +0 -199
- package/crates/wrightty-client/Cargo.toml +0 -16
- package/crates/wrightty-client/src/client.rs +0 -254
- package/crates/wrightty-client/src/lib.rs +0 -2
- package/crates/wrightty-core/Cargo.toml +0 -21
- package/crates/wrightty-core/src/input.rs +0 -212
- package/crates/wrightty-core/src/lib.rs +0 -4
- package/crates/wrightty-core/src/screen.rs +0 -325
- package/crates/wrightty-core/src/session.rs +0 -249
- package/crates/wrightty-core/src/session_manager.rs +0 -77
- package/crates/wrightty-protocol/Cargo.toml +0 -13
- package/crates/wrightty-protocol/src/error.rs +0 -8
- package/crates/wrightty-protocol/src/events.rs +0 -138
- package/crates/wrightty-protocol/src/lib.rs +0 -4
- package/crates/wrightty-protocol/src/methods.rs +0 -321
- package/crates/wrightty-protocol/src/types.rs +0 -201
- package/crates/wrightty-server/Cargo.toml +0 -23
- package/crates/wrightty-server/src/lib.rs +0 -2
- package/crates/wrightty-server/src/main.rs +0 -65
- package/crates/wrightty-server/src/rpc.rs +0 -455
- package/crates/wrightty-server/src/state.rs +0 -39
- package/examples/basic_command.py +0 -53
- package/examples/interactive_tui.py +0 -86
- package/examples/record_session.py +0 -96
- package/install.sh +0 -81
- package/sdks/node/package-lock.json +0 -85
- package/sdks/node/package.json +0 -44
- package/sdks/node/src/client.ts +0 -94
- package/sdks/node/src/index.ts +0 -19
- package/sdks/node/src/terminal.ts +0 -258
- package/sdks/node/src/types.ts +0 -105
- package/sdks/node/tsconfig.json +0 -17
- package/sdks/python/README.md +0 -96
- package/sdks/python/pyproject.toml +0 -42
- package/sdks/python/wrightty/__init__.py +0 -6
- package/sdks/python/wrightty/cli.py +0 -210
- package/sdks/python/wrightty/client.py +0 -136
- package/sdks/python/wrightty/mcp_server.py +0 -434
- package/sdks/python/wrightty/terminal.py +0 -333
- package/skills/wrightty/SKILL.md +0 -261
- package/src/lib.rs +0 -1
- 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()
|