Skip to main content
Glama
input.rs11.6 kB
//! Input detector for text input fields with cursor tracking. use terminal_mcp_core::{Bounds, Element}; use terminal_mcp_emulator::Grid; use crate::detection::{Confidence, DetectedElement, DetectionContext, ElementDetector}; /// Input detector for text input fields. pub struct InputDetector { /// Minimum width for input field min_width: u16, } impl InputDetector { /// Safely get a substring using byte indices, returning None if indices are invalid. fn safe_slice(s: &str, start: usize, end: usize) -> Option<&str> { if start <= end && end <= s.len() && s.is_char_boundary(start) && s.is_char_boundary(end) { Some(&s[start..end]) } else { None } } /// Create a new input detector. pub fn new() -> Self { Self { min_width: 3 } } /// Extract text from a specific row within a region. fn extract_row_text(&self, grid: &Grid, row: u16, start_col: u16, width: u16) -> String { let mut text = String::new(); for col_offset in 0..width { let col = start_col + col_offset; if let Some(cell) = grid.cell(row, col) { text.push(cell.character); } } text } /// Check if a row looks like a labeled input field. /// Patterns: "Username: _____", "Enter name: ", "Password: ******" fn looks_like_labeled_input(&self, text: &str) -> Option<(usize, usize)> { // Look for label followed by colon if let Some(colon_pos) = text.find(':') { let after_colon = Self::safe_slice(text, colon_pos + 1, text.len())?; // Check if there's content after the colon (spaces, underscores, or text) let trimmed = after_colon.trim_start(); if !trimmed.is_empty() || after_colon.len() > 1 { // Value starts after colon and any leading spaces let value_start = colon_pos + 1 + (after_colon.len() - trimmed.len()); return Some((value_start, text.len())); } } None } /// Check if a row looks like a bracketed input field. /// Patterns: "[ ]", "(____)", "│ text │" fn looks_like_bracketed_input(&self, text: &str) -> Option<(usize, usize)> { let trimmed = text.trim(); // Check for bracket pairs let brackets = [('[', ']'), ('(', ')'), ('{', '}'), ('│', '│')]; for (open, close) in &brackets { if trimmed.starts_with(*open) && trimmed.ends_with(*close) { let start = text.find(*open).unwrap() + 1; let end = text.rfind(*close).unwrap(); if end > start && (end - start) >= self.min_width as usize { return Some((start, end)); } } } None } /// Detect input field at cursor position. fn detect_input_at_cursor( &self, grid: &Grid, cursor_row: u16, cursor_col: u16, ) -> Option<DetectedElement> { let dims = grid.dimensions(); // Extract the row containing the cursor let row_text = self.extract_row_text(grid, cursor_row, 0, dims.cols); // Try labeled input detection if let Some((value_start, value_end)) = self.looks_like_labeled_input(&row_text) { let value_slice = Self::safe_slice(&row_text, value_start, value_end)?; let value = value_slice.trim_end().to_string(); let cursor_pos = if cursor_col as usize >= value_start { (cursor_col as usize - value_start).min(value.len()) } else { 0 }; let ref_id = format!("input_{cursor_row}_{value_start}"); return Some(DetectedElement { element: Element::Input { ref_id, bounds: Bounds::new( cursor_row, value_start as u16, (value_end - value_start) as u16, 1, ), value, cursor_pos, }, bounds: Bounds::new( cursor_row, value_start as u16, (value_end - value_start) as u16, 1, ), confidence: Confidence::High, }); } // Try bracketed input detection if let Some((value_start, value_end)) = self.looks_like_bracketed_input(&row_text) { let value_slice = Self::safe_slice(&row_text, value_start, value_end)?; let value = value_slice.trim().to_string(); let cursor_pos = if (cursor_col as usize) >= value_start && (cursor_col as usize) < value_end { (cursor_col as usize - value_start).min(value.len()) } else { 0 }; let ref_id = format!("input_{cursor_row}_{value_start}"); return Some(DetectedElement { element: Element::Input { ref_id, bounds: Bounds::new( cursor_row, value_start as u16, (value_end - value_start) as u16, 1, ), value, cursor_pos, }, bounds: Bounds::new( cursor_row, value_start as u16, (value_end - value_start) as u16, 1, ), confidence: Confidence::Medium, }); } None } /// Detect input fields with reverse video (focused input). fn detect_reverse_video_input(&self, grid: &Grid, cursor_row: u16) -> Option<DetectedElement> { let dims = grid.dimensions(); // Find consecutive cells with reverse video on this row let mut start_col = None; let mut end_col = 0; let mut value = String::new(); for col in 0..dims.cols { if let Some(cell) = grid.cell(cursor_row, col) { if cell.attrs.reverse && cell.character != ' ' { if start_col.is_none() { start_col = Some(col); } end_col = col + 1; value.push(cell.character); } } } if let Some(start) = start_col { let width = end_col - start; if width >= self.min_width { let ref_id = format!("input_{cursor_row}_{start}"); let cursor_pos = value.len(); // Cursor typically at end for reverse video return Some(DetectedElement { element: Element::Input { ref_id, bounds: Bounds::new(cursor_row, start, width, 1), value: value.trim().to_string(), cursor_pos, }, bounds: Bounds::new(cursor_row, start, width, 1), confidence: Confidence::High, }); } } None } } impl Default for InputDetector { fn default() -> Self { Self::new() } } impl ElementDetector for InputDetector { fn name(&self) -> &'static str { "input" } fn priority(&self) -> u32 { 70 } fn detect(&self, grid: &Grid, context: &DetectionContext) -> Vec<DetectedElement> { let mut results = Vec::new(); let cursor_row = context.cursor.row; let cursor_col = context.cursor.col; // Strategy 1: Try to detect input at cursor position (highest confidence) if let Some(input) = self.detect_input_at_cursor(grid, cursor_row, cursor_col) { // Check if region is already claimed if !context.is_region_claimed(&input.bounds) { results.push(input); return results; } } // Strategy 2: Try reverse video detection (for focused inputs) if let Some(input) = self.detect_reverse_video_input(grid, cursor_row) { if !context.is_region_claimed(&input.bounds) { results.push(input); } } results } } #[cfg(test)] mod tests { use super::*; use terminal_mcp_core::{Dimensions, Position}; use terminal_mcp_emulator::{Grid, Parser}; fn create_grid_with_text(rows: u16, cols: u16, text: &str) -> Grid { let grid = Grid::new(Dimensions::new(rows, cols)); let mut parser = Parser::new(grid); parser.process(text.as_bytes()); parser.into_grid() } #[test] fn test_input_detector_labeled_field() { let text = "Username: john\r\n"; let grid = create_grid_with_text(5, 40, text); let detector = InputDetector::new(); // Cursor at position 14 (in "john") let context = DetectionContext::new(Position::new(0, 14)); let detected = detector.detect(&grid, &context); assert_eq!(detected.len(), 1); if let Element::Input { value, cursor_pos, .. } = &detected[0].element { assert_eq!(value, "john"); assert_eq!(*cursor_pos, 4); // Cursor at end of "john" } else { panic!("Expected Input element"); } } #[test] fn test_input_detector_empty_labeled_field() { let text = "Password: \r\n"; let grid = create_grid_with_text(5, 40, text); let detector = InputDetector::new(); // Cursor at position 10 (after colon and space) let context = DetectionContext::new(Position::new(0, 10)); let detected = detector.detect(&grid, &context); assert_eq!(detected.len(), 1); if let Element::Input { value, cursor_pos, .. } = &detected[0].element { assert_eq!(value, ""); assert_eq!(*cursor_pos, 0); } } #[test] fn test_input_detector_bracketed_field() { let text = "[ hello ]\r\n"; let grid = create_grid_with_text(5, 40, text); let detector = InputDetector::new(); // Cursor at position 5 (in "hello") let context = DetectionContext::new(Position::new(0, 5)); let detected = detector.detect(&grid, &context); assert_eq!(detected.len(), 1); if let Element::Input { value, .. } = &detected[0].element { assert_eq!(value, "hello"); } } #[test] fn test_input_detector_priority() { let detector = InputDetector::new(); assert_eq!(detector.priority(), 70); assert_eq!(detector.name(), "input"); } #[test] fn test_input_detector_cursor_position() { let text = "Email: user@example.com\r\n"; let grid = create_grid_with_text(5, 40, text); let detector = InputDetector::new(); // Cursor at position 12 (in middle of email) let context = DetectionContext::new(Position::new(0, 12)); let detected = detector.detect(&grid, &context); assert_eq!(detected.len(), 1); if let Element::Input { value, cursor_pos, .. } = &detected[0].element { assert_eq!(value, "user@example.com"); assert_eq!(*cursor_pos, 5); // Cursor at 'e' in user@example.com } } }

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