@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,325 +0,0 @@
|
|
|
1
|
-
use alacritty_terminal::event::VoidListener;
|
|
2
|
-
use alacritty_terminal::grid::Dimensions;
|
|
3
|
-
use alacritty_terminal::index::{Column, Line, Point};
|
|
4
|
-
use alacritty_terminal::term::cell::Flags;
|
|
5
|
-
use alacritty_terminal::term::Term;
|
|
6
|
-
use alacritty_terminal::vte::ansi::CursorShape as AlacCursorShape;
|
|
7
|
-
|
|
8
|
-
use wrightty_protocol::types::*;
|
|
9
|
-
|
|
10
|
-
/// Extract the visible screen as a grid of CellData.
|
|
11
|
-
pub fn extract_contents(term: &Term<VoidListener>) -> ScreenGetContentsData {
|
|
12
|
-
let grid = term.grid();
|
|
13
|
-
let num_cols = grid.columns();
|
|
14
|
-
let num_lines = grid.screen_lines();
|
|
15
|
-
|
|
16
|
-
let mut cells = Vec::with_capacity(num_lines);
|
|
17
|
-
|
|
18
|
-
for line_idx in 0..num_lines {
|
|
19
|
-
let line = Line(line_idx as i32);
|
|
20
|
-
let mut row = Vec::with_capacity(num_cols);
|
|
21
|
-
|
|
22
|
-
for col_idx in 0..num_cols {
|
|
23
|
-
let point = Point::new(line, Column(col_idx));
|
|
24
|
-
let cell = &grid[point];
|
|
25
|
-
|
|
26
|
-
let c = cell.c;
|
|
27
|
-
let flags = cell.flags;
|
|
28
|
-
|
|
29
|
-
let width = if flags.contains(Flags::WIDE_CHAR) {
|
|
30
|
-
2u8
|
|
31
|
-
} else if flags.contains(Flags::WIDE_CHAR_SPACER) {
|
|
32
|
-
0u8
|
|
33
|
-
} else {
|
|
34
|
-
1u8
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
// Resolve colors to RGB
|
|
38
|
-
let fg = resolve_color(cell.fg);
|
|
39
|
-
let bg = resolve_color(cell.bg);
|
|
40
|
-
|
|
41
|
-
let underline = if flags.contains(Flags::DOUBLE_UNDERLINE) {
|
|
42
|
-
UnderlineStyle::Double
|
|
43
|
-
} else if flags.contains(Flags::UNDERCURL) {
|
|
44
|
-
UnderlineStyle::Curly
|
|
45
|
-
} else if flags.contains(Flags::DOTTED_UNDERLINE) {
|
|
46
|
-
UnderlineStyle::Dotted
|
|
47
|
-
} else if flags.contains(Flags::DASHED_UNDERLINE) {
|
|
48
|
-
UnderlineStyle::Dashed
|
|
49
|
-
} else if flags.contains(Flags::ALL_UNDERLINES) {
|
|
50
|
-
UnderlineStyle::Single
|
|
51
|
-
} else {
|
|
52
|
-
UnderlineStyle::None
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
row.push(CellData {
|
|
56
|
-
char: c.to_string(),
|
|
57
|
-
width,
|
|
58
|
-
fg,
|
|
59
|
-
bg,
|
|
60
|
-
attrs: CellAttrs {
|
|
61
|
-
bold: flags.contains(Flags::BOLD),
|
|
62
|
-
italic: flags.contains(Flags::ITALIC),
|
|
63
|
-
underline,
|
|
64
|
-
underline_color: None, // TODO: extract underline color
|
|
65
|
-
strikethrough: flags.contains(Flags::STRIKEOUT),
|
|
66
|
-
dim: flags.contains(Flags::DIM),
|
|
67
|
-
blink: false, // alacritty_terminal doesn't track blink state on cells
|
|
68
|
-
reverse: flags.contains(Flags::INVERSE),
|
|
69
|
-
hidden: flags.contains(Flags::HIDDEN),
|
|
70
|
-
},
|
|
71
|
-
hyperlink: cell.hyperlink().map(|h| h.uri().to_string()),
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
cells.push(row);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
let cursor = term.grid().cursor.point;
|
|
79
|
-
let style = term.cursor_style();
|
|
80
|
-
let cursor_state = CursorState {
|
|
81
|
-
row: cursor.line.0 as u32,
|
|
82
|
-
col: cursor.column.0 as u32,
|
|
83
|
-
visible: true,
|
|
84
|
-
shape: match style.shape {
|
|
85
|
-
AlacCursorShape::Block => CursorShape::Block,
|
|
86
|
-
AlacCursorShape::Underline => CursorShape::Underline,
|
|
87
|
-
AlacCursorShape::Beam => CursorShape::Bar,
|
|
88
|
-
_ => CursorShape::Block,
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
ScreenGetContentsData {
|
|
93
|
-
rows: num_lines as u32,
|
|
94
|
-
cols: num_cols as u32,
|
|
95
|
-
cursor: cursor_state,
|
|
96
|
-
cells,
|
|
97
|
-
alternate_screen: term.mode().contains(alacritty_terminal::term::TermMode::ALT_SCREEN),
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/// Extract the visible screen as plain text.
|
|
102
|
-
pub fn extract_text(term: &Term<VoidListener>) -> String {
|
|
103
|
-
let grid = term.grid();
|
|
104
|
-
let num_cols = grid.columns();
|
|
105
|
-
let num_lines = grid.screen_lines();
|
|
106
|
-
let mut lines = Vec::with_capacity(num_lines);
|
|
107
|
-
|
|
108
|
-
for line_idx in 0..num_lines {
|
|
109
|
-
let line = Line(line_idx as i32);
|
|
110
|
-
let mut row_text = String::with_capacity(num_cols);
|
|
111
|
-
|
|
112
|
-
for col_idx in 0..num_cols {
|
|
113
|
-
let point = Point::new(line, Column(col_idx));
|
|
114
|
-
let cell = &grid[point];
|
|
115
|
-
|
|
116
|
-
if cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
row_text.push(cell.c);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Trim trailing whitespace
|
|
124
|
-
let trimmed = row_text.trim_end();
|
|
125
|
-
lines.push(trimmed.to_string());
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Remove trailing empty lines
|
|
129
|
-
while lines.last().is_some_and(|l| l.is_empty()) {
|
|
130
|
-
lines.pop();
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
lines.join("\n")
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/// Extract scrollback history lines as plain text.
|
|
137
|
-
/// Returns up to `lines` lines starting from `offset` lines before the most recent history line.
|
|
138
|
-
pub fn extract_scrollback(
|
|
139
|
-
term: &Term<VoidListener>,
|
|
140
|
-
lines: u32,
|
|
141
|
-
offset: u32,
|
|
142
|
-
) -> (Vec<wrightty_protocol::methods::ScrollbackLine>, u32) {
|
|
143
|
-
let grid = term.grid();
|
|
144
|
-
let history = grid.history_size() as u32;
|
|
145
|
-
let num_cols = grid.columns();
|
|
146
|
-
|
|
147
|
-
let total_scrollback = history;
|
|
148
|
-
let start = offset;
|
|
149
|
-
let end = (offset + lines).min(history);
|
|
150
|
-
|
|
151
|
-
let mut result = Vec::new();
|
|
152
|
-
for i in start..end {
|
|
153
|
-
// Line(-(i+1)) is the (i+1)th most-recent history line
|
|
154
|
-
let line_idx = Line(-((i as i32) + 1));
|
|
155
|
-
let mut row_text = String::with_capacity(num_cols);
|
|
156
|
-
for col_idx in 0..num_cols {
|
|
157
|
-
let point = Point::new(line_idx, Column(col_idx));
|
|
158
|
-
let cell = &grid[point];
|
|
159
|
-
if cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
row_text.push(cell.c);
|
|
163
|
-
}
|
|
164
|
-
let text = row_text.trim_end().to_string();
|
|
165
|
-
result.push(wrightty_protocol::methods::ScrollbackLine {
|
|
166
|
-
text,
|
|
167
|
-
line_number: -((i as i32) + 1),
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
(result, total_scrollback)
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/// Data returned by extract_contents (before serialization to protocol type).
|
|
175
|
-
pub struct ScreenGetContentsData {
|
|
176
|
-
pub rows: u32,
|
|
177
|
-
pub cols: u32,
|
|
178
|
-
pub cursor: CursorState,
|
|
179
|
-
pub cells: Vec<Vec<CellData>>,
|
|
180
|
-
pub alternate_screen: bool,
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/// Resolve an alacritty color to RGB.
|
|
184
|
-
/// For now, use a simple default palette. Full palette support comes later.
|
|
185
|
-
fn resolve_color(color: alacritty_terminal::vte::ansi::Color) -> Rgb {
|
|
186
|
-
use alacritty_terminal::vte::ansi::Color;
|
|
187
|
-
use alacritty_terminal::vte::ansi::NamedColor;
|
|
188
|
-
|
|
189
|
-
match color {
|
|
190
|
-
Color::Spec(rgb) => Rgb {
|
|
191
|
-
r: rgb.r,
|
|
192
|
-
g: rgb.g,
|
|
193
|
-
b: rgb.b,
|
|
194
|
-
},
|
|
195
|
-
Color::Named(named) => {
|
|
196
|
-
// Default xterm colors
|
|
197
|
-
match named {
|
|
198
|
-
NamedColor::Black => Rgb { r: 0, g: 0, b: 0 },
|
|
199
|
-
NamedColor::Red => Rgb {
|
|
200
|
-
r: 205,
|
|
201
|
-
g: 0,
|
|
202
|
-
b: 0,
|
|
203
|
-
},
|
|
204
|
-
NamedColor::Green => Rgb {
|
|
205
|
-
r: 0,
|
|
206
|
-
g: 205,
|
|
207
|
-
b: 0,
|
|
208
|
-
},
|
|
209
|
-
NamedColor::Yellow => Rgb {
|
|
210
|
-
r: 205,
|
|
211
|
-
g: 205,
|
|
212
|
-
b: 0,
|
|
213
|
-
},
|
|
214
|
-
NamedColor::Blue => Rgb {
|
|
215
|
-
r: 0,
|
|
216
|
-
g: 0,
|
|
217
|
-
b: 238,
|
|
218
|
-
},
|
|
219
|
-
NamedColor::Magenta => Rgb {
|
|
220
|
-
r: 205,
|
|
221
|
-
g: 0,
|
|
222
|
-
b: 205,
|
|
223
|
-
},
|
|
224
|
-
NamedColor::Cyan => Rgb {
|
|
225
|
-
r: 0,
|
|
226
|
-
g: 205,
|
|
227
|
-
b: 205,
|
|
228
|
-
},
|
|
229
|
-
NamedColor::White => Rgb {
|
|
230
|
-
r: 229,
|
|
231
|
-
g: 229,
|
|
232
|
-
b: 229,
|
|
233
|
-
},
|
|
234
|
-
NamedColor::BrightBlack => Rgb {
|
|
235
|
-
r: 127,
|
|
236
|
-
g: 127,
|
|
237
|
-
b: 127,
|
|
238
|
-
},
|
|
239
|
-
NamedColor::BrightRed => Rgb {
|
|
240
|
-
r: 255,
|
|
241
|
-
g: 0,
|
|
242
|
-
b: 0,
|
|
243
|
-
},
|
|
244
|
-
NamedColor::BrightGreen => Rgb {
|
|
245
|
-
r: 0,
|
|
246
|
-
g: 255,
|
|
247
|
-
b: 0,
|
|
248
|
-
},
|
|
249
|
-
NamedColor::BrightYellow => Rgb {
|
|
250
|
-
r: 255,
|
|
251
|
-
g: 255,
|
|
252
|
-
b: 0,
|
|
253
|
-
},
|
|
254
|
-
NamedColor::BrightBlue => Rgb {
|
|
255
|
-
r: 92,
|
|
256
|
-
g: 92,
|
|
257
|
-
b: 255,
|
|
258
|
-
},
|
|
259
|
-
NamedColor::BrightMagenta => Rgb {
|
|
260
|
-
r: 255,
|
|
261
|
-
g: 0,
|
|
262
|
-
b: 255,
|
|
263
|
-
},
|
|
264
|
-
NamedColor::BrightCyan => Rgb {
|
|
265
|
-
r: 0,
|
|
266
|
-
g: 255,
|
|
267
|
-
b: 255,
|
|
268
|
-
},
|
|
269
|
-
NamedColor::BrightWhite => Rgb {
|
|
270
|
-
r: 255,
|
|
271
|
-
g: 255,
|
|
272
|
-
b: 255,
|
|
273
|
-
},
|
|
274
|
-
NamedColor::Foreground => Rgb {
|
|
275
|
-
r: 255,
|
|
276
|
-
g: 255,
|
|
277
|
-
b: 255,
|
|
278
|
-
},
|
|
279
|
-
NamedColor::Background => Rgb { r: 0, g: 0, b: 0 },
|
|
280
|
-
_ => Rgb {
|
|
281
|
-
r: 200,
|
|
282
|
-
g: 200,
|
|
283
|
-
b: 200,
|
|
284
|
-
},
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
Color::Indexed(idx) => {
|
|
288
|
-
// 256-color palette
|
|
289
|
-
static ANSI_COLORS: [(u8, u8, u8); 16] = [
|
|
290
|
-
(0, 0, 0), // 0 black
|
|
291
|
-
(205, 0, 0), // 1 red
|
|
292
|
-
(0, 205, 0), // 2 green
|
|
293
|
-
(205, 205, 0), // 3 yellow
|
|
294
|
-
(0, 0, 238), // 4 blue
|
|
295
|
-
(205, 0, 205), // 5 magenta
|
|
296
|
-
(0, 205, 205), // 6 cyan
|
|
297
|
-
(229, 229, 229), // 7 white
|
|
298
|
-
(127, 127, 127), // 8 bright black
|
|
299
|
-
(255, 0, 0), // 9 bright red
|
|
300
|
-
(0, 255, 0), // 10 bright green
|
|
301
|
-
(255, 255, 0), // 11 bright yellow
|
|
302
|
-
(92, 92, 255), // 12 bright blue
|
|
303
|
-
(255, 0, 255), // 13 bright magenta
|
|
304
|
-
(0, 255, 255), // 14 bright cyan
|
|
305
|
-
(255, 255, 255), // 15 bright white
|
|
306
|
-
];
|
|
307
|
-
|
|
308
|
-
if (idx as usize) < 16 {
|
|
309
|
-
let (r, g, b) = ANSI_COLORS[idx as usize];
|
|
310
|
-
Rgb { r, g, b }
|
|
311
|
-
} else if idx < 232 {
|
|
312
|
-
// 6x6x6 color cube
|
|
313
|
-
let i = idx - 16;
|
|
314
|
-
let r = (i / 36) * 51;
|
|
315
|
-
let g = ((i / 6) % 6) * 51;
|
|
316
|
-
let b = (i % 6) * 51;
|
|
317
|
-
Rgb { r, g, b }
|
|
318
|
-
} else {
|
|
319
|
-
// Grayscale ramp
|
|
320
|
-
let v = 8 + (idx - 232) * 10;
|
|
321
|
-
Rgb { r: v, g: v, b: v }
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
}
|
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
use std::io::{Read, Write};
|
|
2
|
-
use std::sync::{Arc, Mutex};
|
|
3
|
-
|
|
4
|
-
use alacritty_terminal::event::VoidListener;
|
|
5
|
-
use alacritty_terminal::grid::Dimensions;
|
|
6
|
-
use alacritty_terminal::term::{Config as TermConfig, Term};
|
|
7
|
-
use portable_pty::{native_pty_system, Child, CommandBuilder, MasterPty, PtySize};
|
|
8
|
-
use tokio::sync::broadcast;
|
|
9
|
-
use vte::ansi::Processor;
|
|
10
|
-
|
|
11
|
-
use wrightty_protocol::types::SessionId;
|
|
12
|
-
|
|
13
|
-
use crate::screen;
|
|
14
|
-
|
|
15
|
-
/// Terminal dimensions for alacritty_terminal.
|
|
16
|
-
struct TermSize {
|
|
17
|
-
cols: usize,
|
|
18
|
-
lines: usize,
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
impl Dimensions for TermSize {
|
|
22
|
-
fn total_lines(&self) -> usize {
|
|
23
|
-
self.lines
|
|
24
|
-
}
|
|
25
|
-
fn screen_lines(&self) -> usize {
|
|
26
|
-
self.lines
|
|
27
|
-
}
|
|
28
|
-
fn columns(&self) -> usize {
|
|
29
|
-
self.cols
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/// Internal terminal state protected by a mutex.
|
|
34
|
-
pub struct TermState {
|
|
35
|
-
pub term: Term<VoidListener>,
|
|
36
|
-
pub parser: Processor,
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/// A terminal session: owns a PTY, a virtual terminal, and the reader task.
|
|
40
|
-
pub struct Session {
|
|
41
|
-
pub id: SessionId,
|
|
42
|
-
pub state: Arc<Mutex<TermState>>,
|
|
43
|
-
pub master: Box<dyn MasterPty + Send>,
|
|
44
|
-
pub writer: Box<dyn Write + Send>,
|
|
45
|
-
pub child: Box<dyn Child + Send + Sync>,
|
|
46
|
-
pub update_tx: broadcast::Sender<()>,
|
|
47
|
-
reader_handle: tokio::task::JoinHandle<()>,
|
|
48
|
-
cols: u16,
|
|
49
|
-
rows: u16,
|
|
50
|
-
pub title: String,
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
impl Session {
|
|
54
|
-
pub fn spawn(
|
|
55
|
-
id: SessionId,
|
|
56
|
-
shell: Option<String>,
|
|
57
|
-
args: Vec<String>,
|
|
58
|
-
cols: u16,
|
|
59
|
-
rows: u16,
|
|
60
|
-
env: std::collections::HashMap<String, String>,
|
|
61
|
-
cwd: Option<String>,
|
|
62
|
-
) -> Result<Self, SessionError> {
|
|
63
|
-
let pty_system = native_pty_system();
|
|
64
|
-
let pair = pty_system
|
|
65
|
-
.openpty(PtySize {
|
|
66
|
-
rows,
|
|
67
|
-
cols,
|
|
68
|
-
pixel_width: 0,
|
|
69
|
-
pixel_height: 0,
|
|
70
|
-
})
|
|
71
|
-
.map_err(|e| SessionError::Spawn(e.to_string()))?;
|
|
72
|
-
|
|
73
|
-
let shell_path = shell.unwrap_or_else(|| {
|
|
74
|
-
std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string())
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
let mut cmd = CommandBuilder::new(&shell_path);
|
|
78
|
-
for arg in &args {
|
|
79
|
-
cmd.arg(arg);
|
|
80
|
-
}
|
|
81
|
-
cmd.env("TERM", "xterm-256color");
|
|
82
|
-
for (k, v) in &env {
|
|
83
|
-
cmd.env(k, v);
|
|
84
|
-
}
|
|
85
|
-
if let Some(ref dir) = cwd {
|
|
86
|
-
cmd.cwd(dir);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
let child = pair
|
|
90
|
-
.slave
|
|
91
|
-
.spawn_command(cmd)
|
|
92
|
-
.map_err(|e| SessionError::Spawn(e.to_string()))?;
|
|
93
|
-
|
|
94
|
-
// Drop the slave side — we only talk through the master
|
|
95
|
-
drop(pair.slave);
|
|
96
|
-
|
|
97
|
-
let reader = pair
|
|
98
|
-
.master
|
|
99
|
-
.try_clone_reader()
|
|
100
|
-
.map_err(|e| SessionError::Spawn(e.to_string()))?;
|
|
101
|
-
let writer = pair
|
|
102
|
-
.master
|
|
103
|
-
.take_writer()
|
|
104
|
-
.map_err(|e| SessionError::Spawn(e.to_string()))?;
|
|
105
|
-
|
|
106
|
-
// Create the virtual terminal
|
|
107
|
-
let size = TermSize {
|
|
108
|
-
cols: cols as usize,
|
|
109
|
-
lines: rows as usize,
|
|
110
|
-
};
|
|
111
|
-
let term = Term::new(TermConfig::default(), &size, VoidListener);
|
|
112
|
-
let parser = Processor::new();
|
|
113
|
-
|
|
114
|
-
let state = Arc::new(Mutex::new(TermState { term, parser }));
|
|
115
|
-
let (update_tx, _) = broadcast::channel(64);
|
|
116
|
-
|
|
117
|
-
// Spawn a blocking reader task that feeds PTY output into the terminal
|
|
118
|
-
let reader_state = Arc::clone(&state);
|
|
119
|
-
let reader_tx = update_tx.clone();
|
|
120
|
-
let reader_handle = tokio::task::spawn_blocking(move || {
|
|
121
|
-
Self::reader_loop(reader, reader_state, reader_tx);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
Ok(Session {
|
|
125
|
-
id,
|
|
126
|
-
state,
|
|
127
|
-
master: pair.master,
|
|
128
|
-
writer,
|
|
129
|
-
child,
|
|
130
|
-
update_tx,
|
|
131
|
-
reader_handle,
|
|
132
|
-
cols,
|
|
133
|
-
rows,
|
|
134
|
-
title: shell_path,
|
|
135
|
-
})
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
fn reader_loop(
|
|
139
|
-
mut reader: Box<dyn Read + Send>,
|
|
140
|
-
state: Arc<Mutex<TermState>>,
|
|
141
|
-
update_tx: broadcast::Sender<()>,
|
|
142
|
-
) {
|
|
143
|
-
let mut buf = [0u8; 4096];
|
|
144
|
-
loop {
|
|
145
|
-
match reader.read(&mut buf) {
|
|
146
|
-
Ok(0) => break, // EOF — child exited
|
|
147
|
-
Ok(n) => {
|
|
148
|
-
let mut s = state.lock().unwrap();
|
|
149
|
-
let TermState { ref mut parser, ref mut term } = *s;
|
|
150
|
-
parser.advance(term, &buf[..n]);
|
|
151
|
-
drop(s);
|
|
152
|
-
// Notify subscribers (ignore error if no receivers)
|
|
153
|
-
let _ = update_tx.send(());
|
|
154
|
-
}
|
|
155
|
-
Err(_) => break,
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/// Send raw bytes to the PTY.
|
|
161
|
-
pub fn write_bytes(&mut self, data: &[u8]) -> Result<(), SessionError> {
|
|
162
|
-
self.writer
|
|
163
|
-
.write_all(data)
|
|
164
|
-
.map_err(|e| SessionError::Io(e.to_string()))?;
|
|
165
|
-
self.writer
|
|
166
|
-
.flush()
|
|
167
|
-
.map_err(|e| SessionError::Io(e.to_string()))?;
|
|
168
|
-
Ok(())
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/// Resize the PTY and terminal.
|
|
172
|
-
pub fn resize(&mut self, cols: u16, rows: u16) -> Result<(), SessionError> {
|
|
173
|
-
self.master
|
|
174
|
-
.resize(PtySize {
|
|
175
|
-
rows,
|
|
176
|
-
cols,
|
|
177
|
-
pixel_width: 0,
|
|
178
|
-
pixel_height: 0,
|
|
179
|
-
})
|
|
180
|
-
.map_err(|e| SessionError::Io(e.to_string()))?;
|
|
181
|
-
|
|
182
|
-
let size = TermSize {
|
|
183
|
-
cols: cols as usize,
|
|
184
|
-
lines: rows as usize,
|
|
185
|
-
};
|
|
186
|
-
let mut s = self.state.lock().unwrap();
|
|
187
|
-
s.term.resize(size);
|
|
188
|
-
drop(s);
|
|
189
|
-
|
|
190
|
-
self.cols = cols;
|
|
191
|
-
self.rows = rows;
|
|
192
|
-
Ok(())
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/// Get the current screen as plain text.
|
|
196
|
-
pub fn get_text(&self) -> String {
|
|
197
|
-
let s = self.state.lock().unwrap();
|
|
198
|
-
screen::extract_text(&s.term)
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/// Get the current screen as a structured cell grid.
|
|
202
|
-
pub fn get_contents(&self) -> screen::ScreenGetContentsData {
|
|
203
|
-
let s = self.state.lock().unwrap();
|
|
204
|
-
screen::extract_contents(&s.term)
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/// Get scrollback history lines.
|
|
208
|
-
pub fn get_scrollback(
|
|
209
|
-
&self,
|
|
210
|
-
lines: u32,
|
|
211
|
-
offset: u32,
|
|
212
|
-
) -> (Vec<wrightty_protocol::methods::ScrollbackLine>, u32) {
|
|
213
|
-
let s = self.state.lock().unwrap();
|
|
214
|
-
screen::extract_scrollback(&s.term, lines, offset)
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/// Get the current terminal size.
|
|
218
|
-
pub fn size(&self) -> (u16, u16) {
|
|
219
|
-
(self.cols, self.rows)
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/// Check if the child process is still running.
|
|
223
|
-
pub fn is_running(&self) -> bool {
|
|
224
|
-
// try_wait returns Ok(Some(status)) if exited, Ok(None) if still running
|
|
225
|
-
// portable-pty Child doesn't have try_wait in the trait, so we'll just
|
|
226
|
-
// return true for now and handle exit via the reader loop
|
|
227
|
-
true
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/// Get a broadcast receiver for screen update notifications.
|
|
231
|
-
pub fn subscribe_updates(&self) -> broadcast::Receiver<()> {
|
|
232
|
-
self.update_tx.subscribe()
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
impl Drop for Session {
|
|
237
|
-
fn drop(&mut self) {
|
|
238
|
-
self.reader_handle.abort();
|
|
239
|
-
let _ = self.child.kill();
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
#[derive(Debug, thiserror::Error)]
|
|
244
|
-
pub enum SessionError {
|
|
245
|
-
#[error("failed to spawn session: {0}")]
|
|
246
|
-
Spawn(String),
|
|
247
|
-
#[error("I/O error: {0}")]
|
|
248
|
-
Io(String),
|
|
249
|
-
}
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
use std::collections::HashMap;
|
|
2
|
-
|
|
3
|
-
use wrightty_protocol::types::{SessionId, SessionInfo};
|
|
4
|
-
|
|
5
|
-
use crate::session::{Session, SessionError};
|
|
6
|
-
|
|
7
|
-
pub struct SessionManager {
|
|
8
|
-
sessions: HashMap<SessionId, Session>,
|
|
9
|
-
max_sessions: usize,
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
impl SessionManager {
|
|
13
|
-
pub fn new(max_sessions: usize) -> Self {
|
|
14
|
-
Self {
|
|
15
|
-
sessions: HashMap::new(),
|
|
16
|
-
max_sessions,
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
pub fn create(
|
|
21
|
-
&mut self,
|
|
22
|
-
shell: Option<String>,
|
|
23
|
-
args: Vec<String>,
|
|
24
|
-
cols: u16,
|
|
25
|
-
rows: u16,
|
|
26
|
-
env: std::collections::HashMap<String, String>,
|
|
27
|
-
cwd: Option<String>,
|
|
28
|
-
) -> Result<SessionId, SessionError> {
|
|
29
|
-
if self.sessions.len() >= self.max_sessions {
|
|
30
|
-
return Err(SessionError::Spawn("max sessions reached".to_string()));
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
let id = uuid::Uuid::new_v4().to_string();
|
|
34
|
-
let session = Session::spawn(id.clone(), shell, args, cols, rows, env, cwd)?;
|
|
35
|
-
self.sessions.insert(id.clone(), session);
|
|
36
|
-
Ok(id)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
pub fn destroy(&mut self, id: &str) -> Result<(), SessionError> {
|
|
40
|
-
self.sessions
|
|
41
|
-
.remove(id)
|
|
42
|
-
.ok_or_else(|| SessionError::Spawn(format!("session not found: {id}")))?;
|
|
43
|
-
// Session::drop will kill the child and abort the reader
|
|
44
|
-
Ok(())
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
pub fn get(&self, id: &str) -> Option<&Session> {
|
|
48
|
-
self.sessions.get(id)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
pub fn get_mut(&mut self, id: &str) -> Option<&mut Session> {
|
|
52
|
-
self.sessions.get_mut(id)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
pub fn list(&self) -> Vec<SessionInfo> {
|
|
56
|
-
self.sessions
|
|
57
|
-
.values()
|
|
58
|
-
.map(|s| {
|
|
59
|
-
let (cols, rows) = s.size();
|
|
60
|
-
SessionInfo {
|
|
61
|
-
session_id: s.id.clone(),
|
|
62
|
-
title: s.title.clone(),
|
|
63
|
-
cwd: None,
|
|
64
|
-
cols,
|
|
65
|
-
rows,
|
|
66
|
-
pid: None,
|
|
67
|
-
running: s.is_running(),
|
|
68
|
-
alternate_screen: false,
|
|
69
|
-
}
|
|
70
|
-
})
|
|
71
|
-
.collect()
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
pub fn session_count(&self) -> usize {
|
|
75
|
-
self.sessions.len()
|
|
76
|
-
}
|
|
77
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
[package]
|
|
2
|
-
name = "wrightty-protocol"
|
|
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 = "Protocol types for the Wrightty terminal automation protocol"
|
|
10
|
-
|
|
11
|
-
[dependencies]
|
|
12
|
-
serde = { version = "1", features = ["derive"] }
|
|
13
|
-
serde_json = "1"
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
pub const SESSION_NOT_FOUND: i32 = 1001;
|
|
2
|
-
pub const SESSION_DESTROYED: i32 = 1002;
|
|
3
|
-
pub const WAIT_TIMEOUT: i32 = 1003;
|
|
4
|
-
pub const INVALID_PATTERN: i32 = 1004;
|
|
5
|
-
pub const SPAWN_FAILED: i32 = 1005;
|
|
6
|
-
pub const NOT_SUPPORTED: i32 = 1006;
|
|
7
|
-
pub const MAX_SESSIONS_REACHED: i32 = 1007;
|
|
8
|
-
pub const SUBSCRIPTION_NOT_FOUND: i32 = 1008;
|