Skip to main content
Glama
element.rs10.3 kB
//! Element types for Terminal State Tree (TST). use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::{Bounds, Dimensions, Position}; /// Menu item within a menu element. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct MenuItem { /// Reference ID for this item pub ref_id: String, /// Display text pub text: String, /// Whether this item is currently selected pub selected: bool, } impl MenuItem { /// Create a new menu item. pub fn new(ref_id: impl Into<String>, text: impl Into<String>, selected: bool) -> Self { Self { ref_id: ref_id.into(), text: text.into(), selected, } } } /// Detected UI element within the terminal. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] #[serde(tag = "type", rename_all = "snake_case")] pub enum Element { /// Vertical or horizontal menu with selectable items Menu { /// Unique reference ID ref_id: String, /// Bounding box bounds: Bounds, /// Menu items items: Vec<MenuItem>, /// Index of selected item selected: usize, }, /// Data table with headers and rows Table { /// Unique reference ID ref_id: String, /// Bounding box bounds: Bounds, /// Column headers headers: Vec<String>, /// Table rows (each row is a vec of cell values) rows: Vec<Vec<String>>, }, /// Text input field Input { /// Unique reference ID ref_id: String, /// Bounding box bounds: Bounds, /// Current input value value: String, /// Cursor position within value cursor_pos: usize, }, /// Clickable button Button { /// Unique reference ID ref_id: String, /// Bounding box bounds: Bounds, /// Button label label: String, }, /// Progress indicator ProgressBar { /// Unique reference ID ref_id: String, /// Bounding box bounds: Bounds, /// Progress percentage (0-100) percent: u8, }, /// Checkbox or toggle Checkbox { /// Unique reference ID ref_id: String, /// Bounding box bounds: Bounds, /// Label text label: String, /// Checked state checked: bool, }, /// Status bar (typically at bottom) StatusBar { /// Unique reference ID ref_id: String, /// Bounding box bounds: Bounds, /// Status content content: String, }, /// Bordered region containing other elements Border { /// Unique reference ID ref_id: String, /// Bounding box bounds: Bounds, /// Optional title title: Option<String>, /// Reference IDs of contained elements children: Vec<String>, }, /// Generic text region Text { /// Unique reference ID ref_id: String, /// Bounding box bounds: Bounds, /// Text content content: String, }, } impl Element { /// Get the reference ID of this element. pub fn ref_id(&self) -> &str { match self { Element::Menu { ref_id, .. } => ref_id, Element::Table { ref_id, .. } => ref_id, Element::Input { ref_id, .. } => ref_id, Element::Button { ref_id, .. } => ref_id, Element::ProgressBar { ref_id, .. } => ref_id, Element::Checkbox { ref_id, .. } => ref_id, Element::StatusBar { ref_id, .. } => ref_id, Element::Border { ref_id, .. } => ref_id, Element::Text { ref_id, .. } => ref_id, } } /// Get the element type name. pub fn type_name(&self) -> &'static str { match self { Element::Menu { .. } => "menu", Element::Table { .. } => "table", Element::Input { .. } => "input", Element::Button { .. } => "button", Element::ProgressBar { .. } => "progress_bar", Element::Checkbox { .. } => "checkbox", Element::StatusBar { .. } => "status_bar", Element::Border { .. } => "border", Element::Text { .. } => "text", } } /// Get the bounds of this element. pub fn bounds(&self) -> &Bounds { match self { Element::Menu { bounds, .. } => bounds, Element::Table { bounds, .. } => bounds, Element::Input { bounds, .. } => bounds, Element::Button { bounds, .. } => bounds, Element::ProgressBar { bounds, .. } => bounds, Element::Checkbox { bounds, .. } => bounds, Element::StatusBar { bounds, .. } => bounds, Element::Border { bounds, .. } => bounds, Element::Text { bounds, .. } => bounds, } } } /// Terminal State Tree - structured snapshot of terminal content. #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct TerminalStateTree { /// Session identifier pub session_id: String, /// Terminal dimensions pub dimensions: Dimensions, /// Current cursor position pub cursor: Position, /// Snapshot timestamp (ISO 8601) pub timestamp: String, /// Detected UI elements pub elements: Vec<Element>, /// Raw text content (stripped of formatting) pub raw_text: String, /// Raw ANSI buffer (optional, for debugging) #[serde(skip_serializing_if = "Option::is_none")] pub ansi_buffer: Option<String>, } impl TerminalStateTree { /// Find element by reference ID. pub fn find_element(&self, ref_id: &str) -> Option<&Element> { self.elements.iter().find(|e| e.ref_id() == ref_id) } /// Get all elements of a specific type. pub fn elements_of_type(&self, element_type: &str) -> Vec<&Element> { self.elements .iter() .filter(|e| e.type_name() == element_type) .collect() } /// Get all menu elements. pub fn menus(&self) -> Vec<&Element> { self.elements_of_type("menu") } /// Get all table elements. pub fn tables(&self) -> Vec<&Element> { self.elements_of_type("table") } /// Get all input elements. pub fn inputs(&self) -> Vec<&Element> { self.elements_of_type("input") } } #[cfg(test)] mod tests { use super::*; #[test] fn test_menu_item_creation() { let item = MenuItem::new("item1", "Option 1", true); assert_eq!(item.ref_id, "item1"); assert_eq!(item.text, "Option 1"); assert!(item.selected); } #[test] fn test_element_ref_id() { let button = Element::Button { ref_id: "btn1".to_string(), bounds: Bounds::new(0, 0, 10, 1), label: "Click me".to_string(), }; assert_eq!(button.ref_id(), "btn1"); } #[test] fn test_element_type_name() { let input = Element::Input { ref_id: "input1".to_string(), bounds: Bounds::new(0, 0, 20, 1), value: "test".to_string(), cursor_pos: 4, }; assert_eq!(input.type_name(), "input"); } #[test] fn test_element_bounds() { let table = Element::Table { ref_id: "table1".to_string(), bounds: Bounds::new(5, 10, 30, 15), headers: vec!["Col1".to_string(), "Col2".to_string()], rows: vec![], }; assert_eq!(table.bounds(), &Bounds::new(5, 10, 30, 15)); } #[test] fn test_tst_find_element() { let button = Element::Button { ref_id: "btn1".to_string(), bounds: Bounds::new(0, 0, 10, 1), label: "Click".to_string(), }; let input = Element::Input { ref_id: "input1".to_string(), bounds: Bounds::new(0, 2, 20, 1), value: "".to_string(), cursor_pos: 0, }; let tst = TerminalStateTree { session_id: "sess1".to_string(), dimensions: Dimensions::new(24, 80), cursor: Position::new(0, 0), timestamp: "2025-11-29T00:00:00Z".to_string(), elements: vec![button, input], raw_text: "".to_string(), ansi_buffer: None, }; assert!(tst.find_element("btn1").is_some()); assert!(tst.find_element("input1").is_some()); assert!(tst.find_element("nonexistent").is_none()); } #[test] fn test_tst_elements_of_type() { let button1 = Element::Button { ref_id: "btn1".to_string(), bounds: Bounds::new(0, 0, 10, 1), label: "Button 1".to_string(), }; let button2 = Element::Button { ref_id: "btn2".to_string(), bounds: Bounds::new(0, 2, 10, 1), label: "Button 2".to_string(), }; let input = Element::Input { ref_id: "input1".to_string(), bounds: Bounds::new(0, 4, 20, 1), value: "".to_string(), cursor_pos: 0, }; let tst = TerminalStateTree { session_id: "sess1".to_string(), dimensions: Dimensions::new(24, 80), cursor: Position::new(0, 0), timestamp: "2025-11-29T00:00:00Z".to_string(), elements: vec![button1, button2, input], raw_text: "".to_string(), ansi_buffer: None, }; let buttons = tst.elements_of_type("button"); assert_eq!(buttons.len(), 2); let inputs = tst.elements_of_type("input"); assert_eq!(inputs.len(), 1); } #[test] fn test_element_serialization() { let menu = Element::Menu { ref_id: "menu1".to_string(), bounds: Bounds::new(0, 0, 20, 5), items: vec![ MenuItem::new("item1", "Option 1", true), MenuItem::new("item2", "Option 2", false), ], selected: 0, }; let json = serde_json::to_string(&menu).unwrap(); let deserialized: Element = serde_json::from_str(&json).unwrap(); assert_eq!(menu, deserialized); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/aybelatchane/mcp-server-terminal'

If you have feedback or need assistance with the MCP directory API, please join our Discord server