Skip to main content

vex_shell/
lib.rs

1use std::collections::HashMap;
2use std::path::Path;
3
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use thiserror::Error;
7use vex_domain::{ShellId, WorkstreamId};
8
9/// A managed PTY shell session.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct Shell {
12    pub id: ShellId,
13    pub workstream_id: WorkstreamId,
14    pub created_at: DateTime<Utc>,
15    pub is_alive: bool,
16}
17
18/// Output read from a shell.
19#[derive(Debug, Clone)]
20pub struct ShellOutput {
21    pub data: Vec<u8>,
22    pub bell_detected: bool,
23}
24
25/// Default terminal dimensions.
26pub const DEFAULT_COLS: u16 = 80;
27pub const DEFAULT_ROWS: u16 = 24;
28pub const DEFAULT_SCROLLBACK_LINES: usize = 10_000;
29
30#[derive(Debug, Error)]
31pub enum ShellError {
32    #[error("shell not found: {0}")]
33    NotFound(String),
34    #[error("shell already dead: {0}")]
35    AlreadyDead(String),
36    #[error("spawn failed: {0}")]
37    SpawnFailed(String),
38    #[error("PTY error: {0}")]
39    PtyError(String),
40    #[error("IO error: {0}")]
41    Io(#[from] std::io::Error),
42}
43
44/// Port trait for shell management. Implementations live in vex/app.
45pub trait ShellPort: Send + Sync {
46    fn spawn(
47        &self,
48        workstream_id: &WorkstreamId,
49        working_dir: &Path,
50        command: Option<&str>,
51        id_prefix: Option<&str>,
52        env: Option<&HashMap<String, String>>,
53    ) -> Result<ShellId, ShellError>;
54
55    fn write_stdin(&self, id: &ShellId, data: &[u8]) -> Result<(), ShellError>;
56
57    fn read_output(&self, id: &ShellId) -> Result<ShellOutput, ShellError>;
58
59    fn resize(&self, id: &ShellId, rows: u16, cols: u16) -> Result<(), ShellError>;
60
61    fn kill(&self, id: &ShellId) -> Result<(), ShellError>;
62
63    fn is_alive(&self, id: &ShellId) -> Result<bool, ShellError>;
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    #[test]
71    fn shell_output_default() {
72        let output = ShellOutput {
73            data: vec![0x41, 0x42],
74            bell_detected: false,
75        };
76        assert_eq!(output.data, vec![0x41, 0x42]);
77        assert!(!output.bell_detected);
78    }
79
80    #[test]
81    fn shell_error_display() {
82        let err = ShellError::NotFound("shell-1".into());
83        assert_eq!(err.to_string(), "shell not found: shell-1");
84    }
85
86    #[test]
87    fn default_dimensions() {
88        assert_eq!(DEFAULT_COLS, 80);
89        assert_eq!(DEFAULT_ROWS, 24);
90        assert_eq!(DEFAULT_SCROLLBACK_LINES, 10_000);
91    }
92}