@moejay/wrightty 0.0.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/dist/cli.d.ts +3 -0
  2. package/dist/cli.js +144 -0
  3. package/dist/client.d.ts +14 -0
  4. package/dist/client.js +83 -0
  5. package/dist/index.d.ts +3 -0
  6. package/dist/index.js +8 -0
  7. package/dist/terminal.d.ts +48 -0
  8. package/dist/terminal.js +210 -0
  9. package/dist/types.d.ts +90 -0
  10. package/dist/types.js +3 -0
  11. package/package.json +38 -15
  12. package/.github/workflows/ci.yml +0 -90
  13. package/.github/workflows/release.yml +0 -177
  14. package/Cargo.lock +0 -2662
  15. package/Cargo.toml +0 -38
  16. package/PROTOCOL.md +0 -1351
  17. package/README.md +0 -386
  18. package/agents/ceo/AGENTS.md +0 -24
  19. package/agents/ceo/HEARTBEAT.md +0 -72
  20. package/agents/ceo/SOUL.md +0 -33
  21. package/agents/ceo/TOOLS.md +0 -3
  22. package/agents/founding-engineer/AGENTS.md +0 -44
  23. package/crates/wrightty/Cargo.toml +0 -43
  24. package/crates/wrightty/src/client_cmds.rs +0 -366
  25. package/crates/wrightty/src/discover.rs +0 -78
  26. package/crates/wrightty/src/main.rs +0 -100
  27. package/crates/wrightty/src/server.rs +0 -100
  28. package/crates/wrightty/src/term.rs +0 -338
  29. package/crates/wrightty-bridge-ghostty/Cargo.toml +0 -27
  30. package/crates/wrightty-bridge-ghostty/src/ghostty.rs +0 -422
  31. package/crates/wrightty-bridge-ghostty/src/lib.rs +0 -2
  32. package/crates/wrightty-bridge-ghostty/src/main.rs +0 -146
  33. package/crates/wrightty-bridge-ghostty/src/rpc.rs +0 -307
  34. package/crates/wrightty-bridge-kitty/Cargo.toml +0 -26
  35. package/crates/wrightty-bridge-kitty/src/kitty.rs +0 -269
  36. package/crates/wrightty-bridge-kitty/src/lib.rs +0 -2
  37. package/crates/wrightty-bridge-kitty/src/main.rs +0 -124
  38. package/crates/wrightty-bridge-kitty/src/rpc.rs +0 -304
  39. package/crates/wrightty-bridge-tmux/Cargo.toml +0 -26
  40. package/crates/wrightty-bridge-tmux/src/lib.rs +0 -2
  41. package/crates/wrightty-bridge-tmux/src/main.rs +0 -119
  42. package/crates/wrightty-bridge-tmux/src/rpc.rs +0 -291
  43. package/crates/wrightty-bridge-tmux/src/tmux.rs +0 -215
  44. package/crates/wrightty-bridge-wezterm/Cargo.toml +0 -26
  45. package/crates/wrightty-bridge-wezterm/src/lib.rs +0 -2
  46. package/crates/wrightty-bridge-wezterm/src/main.rs +0 -119
  47. package/crates/wrightty-bridge-wezterm/src/rpc.rs +0 -339
  48. package/crates/wrightty-bridge-wezterm/src/wezterm.rs +0 -190
  49. package/crates/wrightty-bridge-zellij/Cargo.toml +0 -27
  50. package/crates/wrightty-bridge-zellij/src/lib.rs +0 -2
  51. package/crates/wrightty-bridge-zellij/src/main.rs +0 -125
  52. package/crates/wrightty-bridge-zellij/src/rpc.rs +0 -328
  53. package/crates/wrightty-bridge-zellij/src/zellij.rs +0 -199
  54. package/crates/wrightty-client/Cargo.toml +0 -16
  55. package/crates/wrightty-client/src/client.rs +0 -254
  56. package/crates/wrightty-client/src/lib.rs +0 -2
  57. package/crates/wrightty-core/Cargo.toml +0 -21
  58. package/crates/wrightty-core/src/input.rs +0 -212
  59. package/crates/wrightty-core/src/lib.rs +0 -4
  60. package/crates/wrightty-core/src/screen.rs +0 -325
  61. package/crates/wrightty-core/src/session.rs +0 -249
  62. package/crates/wrightty-core/src/session_manager.rs +0 -77
  63. package/crates/wrightty-protocol/Cargo.toml +0 -13
  64. package/crates/wrightty-protocol/src/error.rs +0 -8
  65. package/crates/wrightty-protocol/src/events.rs +0 -138
  66. package/crates/wrightty-protocol/src/lib.rs +0 -4
  67. package/crates/wrightty-protocol/src/methods.rs +0 -321
  68. package/crates/wrightty-protocol/src/types.rs +0 -201
  69. package/crates/wrightty-server/Cargo.toml +0 -23
  70. package/crates/wrightty-server/src/lib.rs +0 -2
  71. package/crates/wrightty-server/src/main.rs +0 -65
  72. package/crates/wrightty-server/src/rpc.rs +0 -455
  73. package/crates/wrightty-server/src/state.rs +0 -39
  74. package/examples/basic_command.py +0 -53
  75. package/examples/interactive_tui.py +0 -86
  76. package/examples/record_session.py +0 -96
  77. package/install.sh +0 -81
  78. package/sdks/node/package-lock.json +0 -85
  79. package/sdks/node/package.json +0 -44
  80. package/sdks/node/src/client.ts +0 -94
  81. package/sdks/node/src/index.ts +0 -19
  82. package/sdks/node/src/terminal.ts +0 -258
  83. package/sdks/node/src/types.ts +0 -105
  84. package/sdks/node/tsconfig.json +0 -17
  85. package/sdks/python/README.md +0 -96
  86. package/sdks/python/pyproject.toml +0 -42
  87. package/sdks/python/wrightty/__init__.py +0 -6
  88. package/sdks/python/wrightty/cli.py +0 -210
  89. package/sdks/python/wrightty/client.py +0 -136
  90. package/sdks/python/wrightty/mcp_server.py +0 -434
  91. package/sdks/python/wrightty/terminal.py +0 -333
  92. package/skills/wrightty/SKILL.md +0 -261
  93. package/src/lib.rs +0 -1
  94. package/tests/integration_test.rs +0 -618
@@ -1,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;