Skip to main content
Glama
navigation.rs11.6 kB
//! Navigation calculator for determining keystrokes to reach target elements. use terminal_mcp_core::{Element, Error, Key, MenuItem, Result, TerminalStateTree}; /// Navigation calculator for click operations. /// /// Calculates the sequence of keystrokes needed to navigate to and activate /// a target element in the terminal. #[derive(Debug, Clone, Default)] pub struct NavigationCalculator; impl NavigationCalculator { /// Create a new navigation calculator. pub fn new() -> Self { Self } /// Calculate keystrokes to reach and activate target element. /// /// # Arguments /// * `snapshot` - Current terminal state tree /// * `target_ref` - Reference ID of the element to click /// /// # Returns /// Vector of keys to press to reach and activate the target. /// /// # Supported Elements /// - **Menu**: Navigate with Up/Down, activate with Enter /// - **Button**: Tab navigation, activate with Enter /// - **Checkbox**: Tab navigation, toggle with Space /// /// # Example /// ``` /// # use terminal_mcp_session::NavigationCalculator; /// # use terminal_mcp_core::{TerminalStateTree, Element, MenuItem, Bounds, Dimensions, Position}; /// let calc = NavigationCalculator::new(); /// // Create a simple TST with a menu /// let tst = TerminalStateTree { /// session_id: "test".to_string(), /// dimensions: Dimensions::new(24, 80), /// cursor: Position::new(0, 0), /// timestamp: "2025-11-30T00:00:00Z".to_string(), /// elements: vec![ /// Element::Menu { /// ref_id: "menu_0".to_string(), /// bounds: Bounds::new(0, 0, 20, 3), /// items: vec![ /// MenuItem { ref_id: "item_0".to_string(), text: "Item 1".to_string(), selected: true }, /// MenuItem { ref_id: "item_1".to_string(), text: "Item 2".to_string(), selected: false }, /// ], /// selected: 0, /// }, /// ], /// raw_text: "".to_string(), /// ansi_buffer: None, /// }; /// let keys = calc.calculate(&tst, "item_1").unwrap(); /// // Should navigate down once and press Enter /// ``` pub fn calculate(&self, snapshot: &TerminalStateTree, target_ref: &str) -> Result<Vec<Key>> { // Check if target is a menu item (ref_id starts with "item_") if target_ref.starts_with("item_") { // Find the menu containing this item for element in &snapshot.elements { if let Element::Menu { items, selected, .. } = element { // Check if this menu contains the target item if items.iter().any(|item| item.ref_id == target_ref) { return self.navigate_menu(items, *selected, target_ref); } } } return Err(Error::ElementNotFound(target_ref.to_string())); } // Find the target element (for non-menu-item elements) let target = snapshot .find_element(target_ref) .ok_or_else(|| Error::ElementNotFound(target_ref.to_string()))?; match target { Element::Button { .. } => self.navigate_to_button(snapshot, target), Element::Checkbox { .. } => self.navigate_to_checkbox(snapshot, target), _ => Err(Error::InvalidInput(format!( "Element type '{}' is not clickable", target.type_name() ))), } } /// Navigate within a menu to a target item. fn navigate_menu( &self, items: &[MenuItem], current_selected: usize, target_ref: &str, ) -> Result<Vec<Key>> { // Find target item index let target_idx = items .iter() .position(|i| i.ref_id == target_ref) .ok_or_else(|| Error::ElementNotFound(target_ref.to_string()))?; let mut keys = Vec::new(); // Calculate direction and count let diff = target_idx as i32 - current_selected as i32; if diff > 0 { // Navigate down for _ in 0..diff { keys.push(Key::Down); } } else if diff < 0 { // Navigate up for _ in 0..diff.abs() { keys.push(Key::Up); } } // Add activation key keys.push(Key::Enter); Ok(keys) } /// Navigate to a button using Tab and activate with Enter. /// /// This is a simplified implementation that assumes Tab navigation works. /// A more sophisticated approach would analyze the element positions and /// determine the optimal navigation strategy. fn navigate_to_button( &self, _snapshot: &TerminalStateTree, _target: &Element, ) -> Result<Vec<Key>> { // For now, just return Enter (assumes button is already focused) // In a real implementation, we would: // 1. Find all focusable elements // 2. Calculate Tab presses needed to reach target // 3. Return Tab sequence + Enter Ok(vec![Key::Enter]) } /// Navigate to a checkbox using Tab and toggle with Space. fn navigate_to_checkbox( &self, _snapshot: &TerminalStateTree, _target: &Element, ) -> Result<Vec<Key>> { // For now, just return Space (assumes checkbox is already focused) // In a real implementation, we would: // 1. Find all focusable elements // 2. Calculate Tab presses needed to reach target // 3. Return Tab sequence + Space Ok(vec![Key::Space]) } } #[cfg(test)] mod tests { use super::*; use terminal_mcp_core::{Bounds, Dimensions, Position}; fn create_test_snapshot() -> TerminalStateTree { TerminalStateTree { session_id: "test_session".to_string(), dimensions: Dimensions::new(24, 80), cursor: Position::new(0, 0), timestamp: "2025-11-30T00:00:00Z".to_string(), elements: vec![Element::Menu { ref_id: "menu_0".to_string(), bounds: Bounds::new(2, 5, 30, 5), items: vec![ MenuItem { ref_id: "item_0".to_string(), text: "View Status".to_string(), selected: true, }, MenuItem { ref_id: "item_1".to_string(), text: "Start Service".to_string(), selected: false, }, MenuItem { ref_id: "item_2".to_string(), text: "Stop Service".to_string(), selected: false, }, MenuItem { ref_id: "item_3".to_string(), text: "Quit".to_string(), selected: false, }, ], selected: 0, }], raw_text: "".to_string(), ansi_buffer: None, } } #[test] fn test_navigate_menu_down() { let calc = NavigationCalculator::new(); let snapshot = create_test_snapshot(); // Navigate from item 0 to item 2 (2 down) let keys = calc.calculate(&snapshot, "item_2").unwrap(); assert_eq!(keys.len(), 3); // 2x Down + 1x Enter assert_eq!(keys[0], Key::Down); assert_eq!(keys[1], Key::Down); assert_eq!(keys[2], Key::Enter); } #[test] fn test_navigate_menu_up() { let calc = NavigationCalculator::new(); let mut snapshot = create_test_snapshot(); // Change selected to item 3 if let Some(Element::Menu { selected, .. }) = snapshot.elements.get_mut(0) { *selected = 3; } // Navigate from item 3 to item 1 (2 up) let keys = calc.calculate(&snapshot, "item_1").unwrap(); assert_eq!(keys.len(), 3); // 2x Up + 1x Enter assert_eq!(keys[0], Key::Up); assert_eq!(keys[1], Key::Up); assert_eq!(keys[2], Key::Enter); } #[test] fn test_navigate_menu_same_item() { let calc = NavigationCalculator::new(); let snapshot = create_test_snapshot(); // Navigate to currently selected item (item 0) let keys = calc.calculate(&snapshot, "item_0").unwrap(); assert_eq!(keys.len(), 1); // Just Enter assert_eq!(keys[0], Key::Enter); } #[test] fn test_navigate_menu_not_found() { let calc = NavigationCalculator::new(); let snapshot = create_test_snapshot(); // Try to navigate to non-existent item let result = calc.calculate(&snapshot, "item_99"); assert!(result.is_err()); match result { Err(Error::ElementNotFound(ref_id)) => { assert!(ref_id.contains("item_99")); } _ => panic!("Expected ElementNotFound error"), } } #[test] fn test_navigate_button() { let calc = NavigationCalculator::new(); let snapshot = TerminalStateTree { session_id: "test".to_string(), dimensions: Dimensions::new(24, 80), cursor: Position::new(0, 0), timestamp: "2025-11-30T00:00:00Z".to_string(), elements: vec![Element::Button { ref_id: "button_0".to_string(), bounds: Bounds::new(10, 10, 8, 1), label: "OK".to_string(), }], raw_text: "".to_string(), ansi_buffer: None, }; let keys = calc.calculate(&snapshot, "button_0").unwrap(); // For now, should just return Enter assert_eq!(keys.len(), 1); assert_eq!(keys[0], Key::Enter); } #[test] fn test_navigate_checkbox() { let calc = NavigationCalculator::new(); let snapshot = TerminalStateTree { session_id: "test".to_string(), dimensions: Dimensions::new(24, 80), cursor: Position::new(0, 0), timestamp: "2025-11-30T00:00:00Z".to_string(), elements: vec![Element::Checkbox { ref_id: "checkbox_0".to_string(), bounds: Bounds::new(5, 5, 20, 1), label: "Enable feature".to_string(), checked: false, }], raw_text: "".to_string(), ansi_buffer: None, }; let keys = calc.calculate(&snapshot, "checkbox_0").unwrap(); // For now, should just return Space assert_eq!(keys.len(), 1); assert_eq!(keys[0], Key::Space); } #[test] fn test_navigate_non_clickable_element() { let calc = NavigationCalculator::new(); let snapshot = TerminalStateTree { session_id: "test".to_string(), dimensions: Dimensions::new(24, 80), cursor: Position::new(0, 0), timestamp: "2025-11-30T00:00:00Z".to_string(), elements: vec![Element::ProgressBar { ref_id: "progress_0".to_string(), bounds: Bounds::new(10, 10, 20, 1), percent: 50, }], raw_text: "".to_string(), ansi_buffer: None, }; let result = calc.calculate(&snapshot, "progress_0"); assert!(result.is_err()); } }

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