1use std::collections::HashMap;
4use std::path::PathBuf;
5use std::sync::{Arc, RwLock};
6
7use tokio::sync::{mpsc, watch};
8
9use crate::error::{Error, Result};
10
11#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
13pub struct PaneId(pub u64);
14
15impl std::fmt::Display for PaneId {
16 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17 write!(f, "{}", self.0)
18 }
19}
20
21#[derive(Clone, Debug)]
23pub enum PaneState {
24 Running,
26
27 Exited { code: i32 },
29
30 Crashed {
32 signal: Option<i32>,
34 error: Option<String>,
36 },
37
38 Paused,
40}
41
42impl PaneState {
43 #[must_use]
45 pub fn is_alive(&self) -> bool {
46 matches!(self, Self::Running | Self::Paused)
47 }
48}
49
50#[derive(Clone, Copy, Debug, Default)]
52pub struct PaneSize {
53 pub rows: u16,
55 pub cols: u16,
57}
58
59impl PaneSize {
60 #[must_use]
62 pub fn new(rows: u16, cols: u16) -> Self {
63 Self { rows, cols }
64 }
65}
66
67#[derive(Clone, Debug, Default)]
69pub struct SpawnConfig {
70 pub command: Option<String>,
72
73 pub args: Vec<String>,
75
76 pub size: PaneSize,
78
79 pub cwd: Option<PathBuf>,
81
82 pub env: HashMap<String, String>,
84
85 pub scrollback: usize,
87}
88
89impl SpawnConfig {
90 #[must_use]
94 pub fn new_shell() -> Self {
95 Self {
96 scrollback: 10_000,
97 ..Default::default()
98 }
99 }
100
101 #[must_use]
105 pub fn new_command(cmd: impl Into<String>) -> Self {
106 Self {
107 command: Some(cmd.into()),
108 scrollback: 10_000,
109 ..Default::default()
110 }
111 }
112
113 #[must_use]
118 pub fn new(size: PaneSize) -> Self {
119 Self {
120 size,
121 scrollback: 10_000,
122 ..Default::default()
123 }
124 }
125
126 #[must_use]
128 pub fn command(mut self, cmd: impl Into<String>) -> Self {
129 self.command = Some(cmd.into());
130 self
131 }
132
133 #[must_use]
135 pub fn args(mut self, args: Vec<String>) -> Self {
136 self.args = args;
137 self
138 }
139
140 #[must_use]
142 pub fn cwd(mut self, path: impl Into<PathBuf>) -> Self {
143 self.cwd = Some(path.into());
144 self
145 }
146
147 #[must_use]
149 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
150 self.env.insert(key.into(), value.into());
151 self
152 }
153
154 #[must_use]
156 pub fn scrollback(mut self, lines: usize) -> Self {
157 self.scrollback = lines;
158 self
159 }
160}
161
162#[derive(Clone, Debug)]
164pub struct ScreenSnapshot {
165 cells: Vec<Vec<ScreenCell>>,
167 cursor: (u16, u16),
169 size: PaneSize,
171}
172
173#[derive(Clone, Debug, Default)]
175pub struct ScreenCell {
176 pub char: char,
178 pub fg: ScreenColor,
180 pub bg: ScreenColor,
182 pub bold: bool,
184 pub italic: bool,
186 pub underline: bool,
188 pub inverse: bool,
190}
191
192#[derive(Clone, Copy, Debug, Default)]
194pub enum ScreenColor {
195 #[default]
197 Default,
198 Indexed(u8),
200 Rgb(u8, u8, u8),
202}
203
204impl ScreenSnapshot {
205 pub(crate) fn from_parser(parser: &vt100::Parser) -> Self {
207 let screen = parser.screen();
208 let size = PaneSize::new(screen.size().0, screen.size().1);
209 let (cursor_row, cursor_col) = screen.cursor_position();
210
211 let mut cells = Vec::with_capacity(size.rows as usize);
212 for row in 0..size.rows {
213 let mut row_cells = Vec::with_capacity(size.cols as usize);
214 for col in 0..size.cols {
215 let cell = screen
216 .cell(row, col)
217 .map_or_else(ScreenCell::default, |c| ScreenCell {
218 char: c.contents().chars().next().unwrap_or(' '),
219 fg: convert_vt100_color(c.fgcolor()),
220 bg: convert_vt100_color(c.bgcolor()),
221 bold: c.bold(),
222 italic: c.italic(),
223 underline: c.underline(),
224 inverse: c.inverse(),
225 });
226 row_cells.push(cell);
227 }
228 cells.push(row_cells);
229 }
230
231 Self {
232 cells,
233 cursor: (cursor_row, cursor_col),
234 size,
235 }
236 }
237
238 #[must_use]
240 pub fn size(&self) -> PaneSize {
241 self.size
242 }
243
244 #[must_use]
246 pub fn cursor(&self) -> (u16, u16) {
247 self.cursor
248 }
249
250 #[must_use]
252 pub fn cell(&self, row: u16, col: u16) -> Option<&ScreenCell> {
253 self.cells
254 .get(row as usize)
255 .and_then(|r| r.get(col as usize))
256 }
257
258 pub fn rows(&self) -> impl Iterator<Item = &[ScreenCell]> {
260 self.cells.iter().map(Vec::as_slice)
261 }
262}
263
264fn convert_vt100_color(color: vt100::Color) -> ScreenColor {
265 match color {
266 vt100::Color::Default => ScreenColor::Default,
267 vt100::Color::Idx(idx) => ScreenColor::Indexed(idx),
268 vt100::Color::Rgb(r, g, b) => ScreenColor::Rgb(r, g, b),
269 }
270}
271
272#[derive(Clone)]
276pub struct PaneHandle {
277 id: PaneId,
279
280 child_pid: Option<u32>,
282
283 input_tx: mpsc::Sender<Vec<u8>>,
285
286 state_rx: watch::Receiver<PaneState>,
288
289 screen: Arc<RwLock<vt100::Parser>>,
291
292 title: Arc<RwLock<String>>,
294}
295
296impl PaneHandle {
297 pub(crate) fn new(
299 id: PaneId,
300 child_pid: Option<u32>,
301 input_tx: mpsc::Sender<Vec<u8>>,
302 state_rx: watch::Receiver<PaneState>,
303 screen: Arc<RwLock<vt100::Parser>>,
304 ) -> Self {
305 Self {
306 id,
307 child_pid,
308 input_tx,
309 state_rx,
310 screen,
311 title: Arc::new(RwLock::new(String::new())),
312 }
313 }
314
315 #[must_use]
317 pub fn id(&self) -> PaneId {
318 self.id
319 }
320
321 #[must_use]
323 pub fn pid(&self) -> Option<u32> {
324 self.child_pid
325 }
326
327 pub async fn send_input(&self, data: &[u8]) -> Result<()> {
332 self.input_tx
333 .send(data.to_vec())
334 .await
335 .map_err(|_| Error::PaneClosed)
336 }
337
338 #[must_use]
340 pub fn state(&self) -> PaneState {
341 self.state_rx.borrow().clone()
342 }
343
344 #[must_use]
346 pub fn is_alive(&self) -> bool {
347 self.state().is_alive()
348 }
349
350 #[must_use]
352 pub fn screen_snapshot(&self) -> ScreenSnapshot {
353 let screen = self.screen.read().expect("screen lock poisoned");
354 ScreenSnapshot::from_parser(&screen)
355 }
356
357 pub(crate) fn screen(&self) -> &Arc<RwLock<vt100::Parser>> {
359 &self.screen
360 }
361
362 #[must_use]
364 pub fn title(&self) -> String {
365 self.title.read().expect("title lock poisoned").clone()
366 }
367
368 #[allow(dead_code)]
370 pub(crate) fn set_title(&self, title: String) {
371 *self.title.write().expect("title lock poisoned") = title;
372 }
373}
374
375impl std::fmt::Debug for PaneHandle {
376 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
377 f.debug_struct("PaneHandle")
378 .field("id", &self.id)
379 .field("state", &self.state())
380 .finish_non_exhaustive()
381 }
382}