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