cockpit/
pane.rs

1//! Pane types and handles for controlling terminal panes.
2
3use 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/// Unique identifier for a pane.
12#[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/// Current state of a pane's process.
22#[derive(Clone, Debug)]
23pub enum PaneState {
24    /// Process is running.
25    Running,
26
27    /// Process exited normally with the given exit code.
28    Exited { code: i32 },
29
30    /// Process crashed or was killed by a signal.
31    Crashed {
32        /// Signal that killed the process (Unix only).
33        signal: Option<i32>,
34        /// Error description.
35        error: Option<String>,
36    },
37
38    /// Pane is paused (process suspended).
39    Paused,
40}
41
42impl PaneState {
43    /// Returns true if the pane's process is still alive.
44    #[must_use]
45    pub fn is_alive(&self) -> bool {
46        matches!(self, Self::Running | Self::Paused)
47    }
48}
49
50/// Pane dimensions in rows and columns.
51#[derive(Clone, Copy, Debug, Default)]
52pub struct PaneSize {
53    /// Number of rows.
54    pub rows: u16,
55    /// Number of columns.
56    pub cols: u16,
57}
58
59impl PaneSize {
60    /// Create a new pane size.
61    #[must_use]
62    pub fn new(rows: u16, cols: u16) -> Self {
63        Self { rows, cols }
64    }
65}
66
67/// Configuration for spawning a new pane.
68#[derive(Clone, Debug, Default)]
69pub struct SpawnConfig {
70    /// Command to run. If None, uses the default shell.
71    pub command: Option<String>,
72
73    /// Arguments to pass to the command.
74    pub args: Vec<String>,
75
76    /// Initial size of the pane.
77    pub size: PaneSize,
78
79    /// Working directory for the process.
80    pub cwd: Option<PathBuf>,
81
82    /// Additional environment variables.
83    pub env: HashMap<String, String>,
84
85    /// Scrollback buffer size in lines.
86    pub scrollback: usize,
87}
88
89impl SpawnConfig {
90    /// Create a new spawn config for the default shell.
91    ///
92    /// The pane size is calculated automatically by the `PaneManager`.
93    #[must_use]
94    pub fn new_shell() -> Self {
95        Self {
96            scrollback: 10_000,
97            ..Default::default()
98        }
99    }
100
101    /// Create a new spawn config for a specific command.
102    ///
103    /// The pane size is calculated automatically by the `PaneManager`.
104    #[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    /// Create a new spawn config with specified size.
114    ///
115    /// Note: The size may be overridden by the `PaneManager`'s automatic
116    /// layout calculations.
117    #[must_use]
118    pub fn new(size: PaneSize) -> Self {
119        Self {
120            size,
121            scrollback: 10_000,
122            ..Default::default()
123        }
124    }
125
126    /// Set the command to run.
127    #[must_use]
128    pub fn command(mut self, cmd: impl Into<String>) -> Self {
129        self.command = Some(cmd.into());
130        self
131    }
132
133    /// Set command arguments.
134    #[must_use]
135    pub fn args(mut self, args: Vec<String>) -> Self {
136        self.args = args;
137        self
138    }
139
140    /// Set the working directory.
141    #[must_use]
142    pub fn cwd(mut self, path: impl Into<PathBuf>) -> Self {
143        self.cwd = Some(path.into());
144        self
145    }
146
147    /// Add an environment variable.
148    #[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    /// Set the scrollback buffer size.
155    #[must_use]
156    pub fn scrollback(mut self, lines: usize) -> Self {
157        self.scrollback = lines;
158        self
159    }
160}
161
162/// A snapshot of the terminal screen state.
163#[derive(Clone, Debug)]
164pub struct ScreenSnapshot {
165    /// Screen content as a 2D grid of cells.
166    cells: Vec<Vec<ScreenCell>>,
167    /// Cursor position (row, col).
168    cursor: (u16, u16),
169    /// Screen size.
170    size: PaneSize,
171}
172
173/// A single cell in the terminal screen.
174#[derive(Clone, Debug, Default)]
175pub struct ScreenCell {
176    /// The character in this cell.
177    pub char: char,
178    /// Foreground color.
179    pub fg: ScreenColor,
180    /// Background color.
181    pub bg: ScreenColor,
182    /// Text is bold.
183    pub bold: bool,
184    /// Text is italic.
185    pub italic: bool,
186    /// Text is underlined.
187    pub underline: bool,
188    /// Text is inverse (swapped fg/bg).
189    pub inverse: bool,
190}
191
192/// Terminal color representation.
193#[derive(Clone, Copy, Debug, Default)]
194pub enum ScreenColor {
195    /// Default terminal color.
196    #[default]
197    Default,
198    /// Indexed color (0-255).
199    Indexed(u8),
200    /// RGB color.
201    Rgb(u8, u8, u8),
202}
203
204impl ScreenSnapshot {
205    /// Create a snapshot from a vt100 parser.
206    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    /// Get the screen size.
239    #[must_use]
240    pub fn size(&self) -> PaneSize {
241        self.size
242    }
243
244    /// Get the cursor position (row, col).
245    #[must_use]
246    pub fn cursor(&self) -> (u16, u16) {
247        self.cursor
248    }
249
250    /// Get a cell at the given position.
251    #[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    /// Iterate over all rows.
259    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/// Public handle for controlling a pane.
273///
274/// This handle can be cloned and shared across threads.
275#[derive(Clone)]
276pub struct PaneHandle {
277    /// Pane ID.
278    id: PaneId,
279
280    /// Child process ID.
281    child_pid: Option<u32>,
282
283    /// Channel to send input to the pane.
284    input_tx: mpsc::Sender<Vec<u8>>,
285
286    /// Watch channel for state changes.
287    state_rx: watch::Receiver<PaneState>,
288
289    /// Shared screen state for reading.
290    screen: Arc<RwLock<vt100::Parser>>,
291
292    /// Pane title.
293    title: Arc<RwLock<String>>,
294}
295
296impl PaneHandle {
297    /// Create a new pane handle.
298    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    /// Get the pane ID.
316    #[must_use]
317    pub fn id(&self) -> PaneId {
318        self.id
319    }
320
321    /// Get the child process ID.
322    #[must_use]
323    pub fn pid(&self) -> Option<u32> {
324        self.child_pid
325    }
326
327    /// Send input bytes to the pane's PTY.
328    ///
329    /// # Errors
330    /// Returns an error if the pane has been closed.
331    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    /// Get the current pane state.
339    #[must_use]
340    pub fn state(&self) -> PaneState {
341        self.state_rx.borrow().clone()
342    }
343
344    /// Check if the pane's process is still alive.
345    #[must_use]
346    pub fn is_alive(&self) -> bool {
347        self.state().is_alive()
348    }
349
350    /// Get a snapshot of the terminal screen.
351    #[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    /// Get direct access to the screen parser for widget rendering.
358    pub(crate) fn screen(&self) -> &Arc<RwLock<vt100::Parser>> {
359        &self.screen
360    }
361
362    /// Get the pane title.
363    #[must_use]
364    pub fn title(&self) -> String {
365        self.title.read().expect("title lock poisoned").clone()
366    }
367
368    /// Set the pane title.
369    #[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}