Skip to main content

egui_mcp_client/
lib.rs

1//! Library to embed in egui apps for MCP integration
2//!
3//! This crate provides client-side integration for egui applications
4//! to support MCP automation features that require direct application access:
5//! - Screenshots
6//! - Coordinate-based input (clicks, drags)
7//! - Keyboard input
8//! - Scroll events
9//!
10//! Note: UI tree access and element-based interactions are handled via AT-SPI
11//! on the server side and don't require this client library.
12//!
13//! ## Usage in raw_input_hook
14//!
15//! ```rust,ignore
16//! impl eframe::App for MyApp {
17//!     fn raw_input_hook(&mut self, ctx: &egui::Context, raw_input: &mut egui::RawInput) {
18//!         let inputs = self.runtime.block_on(self.mcp_client.take_pending_inputs());
19//!         egui_mcp_client::inject_inputs(ctx, raw_input, inputs);
20//!     }
21//! }
22//! ```
23
24use std::path::PathBuf;
25use std::sync::Arc;
26use tokio::sync::{RwLock, oneshot};
27
28pub use egui_mcp_protocol::{FrameStats, LogEntry, MouseButton, PerfReport, Request, Response};
29
30mod log_layer;
31mod server;
32
33pub use log_layer::{DEFAULT_MAX_MESSAGE_LENGTH, LogBuffer, McpLogLayer, level_to_priority};
34pub use server::IpcServer;
35
36// Re-export egui types for convenience
37pub use egui;
38
39/// Pending input event to be processed by the egui application
40#[derive(Debug, Clone)]
41pub enum PendingInput {
42    /// Click at coordinates
43    Click { x: f32, y: f32, button: MouseButton },
44    /// Double click at coordinates
45    DoubleClick { x: f32, y: f32, button: MouseButton },
46    /// Move mouse to coordinates
47    MoveMouse { x: f32, y: f32 },
48    /// Keyboard input
49    Keyboard { key: String },
50    /// Scroll at coordinates
51    Scroll {
52        x: f32,
53        y: f32,
54        delta_x: f32,
55        delta_y: f32,
56    },
57    /// Drag operation
58    Drag {
59        start_x: f32,
60        start_y: f32,
61        end_x: f32,
62        end_y: f32,
63        button: MouseButton,
64    },
65}
66
67/// A visual highlight to be drawn over an element
68#[derive(Debug, Clone)]
69pub struct Highlight {
70    /// Bounding rectangle
71    pub rect: egui::Rect,
72    /// Highlight color (with alpha)
73    pub color: egui::Color32,
74    /// When the highlight should expire (None = never expires)
75    pub expires_at: Option<std::time::Instant>,
76}
77
78/// Shared state for the MCP client
79#[derive(Clone)]
80pub struct McpClient {
81    state: Arc<RwLock<ClientState>>,
82}
83
84struct ClientState {
85    socket_path: PathBuf,
86    /// Pending screenshot request sender (event-driven)
87    screenshot_sender: Option<oneshot::Sender<Vec<u8>>>,
88    /// Pending input events to be processed by the egui app
89    pending_inputs: Vec<PendingInput>,
90    /// Active highlights to be drawn
91    highlights: Vec<Highlight>,
92    /// Optional log buffer (shared with McpLogLayer)
93    log_buffer: Option<LogBuffer>,
94    /// Frame times for performance monitoring (rolling window)
95    frame_times: std::collections::VecDeque<std::time::Duration>,
96    /// Maximum number of frame times to keep
97    max_frame_samples: usize,
98    /// Performance recording state
99    perf_recording: Option<PerfRecording>,
100    /// Last frame instant for automatic timing
101    last_frame_instant: Option<std::time::Instant>,
102}
103
104/// State for an active performance recording session
105struct PerfRecording {
106    /// When the recording started
107    start_time: std::time::Instant,
108    /// Recorded frame times
109    frame_times: Vec<std::time::Duration>,
110    /// Optional auto-stop after duration
111    duration_ms: u64,
112}
113
114impl McpClient {
115    /// Create a new MCP client with default socket path
116    pub fn new() -> Self {
117        Self::with_socket_path(egui_mcp_protocol::default_socket_path())
118    }
119
120    /// Create a new MCP client with a custom socket path
121    pub fn with_socket_path(socket_path: PathBuf) -> Self {
122        Self {
123            state: Arc::new(RwLock::new(ClientState {
124                socket_path,
125                screenshot_sender: None,
126                pending_inputs: Vec::new(),
127                highlights: Vec::new(),
128                log_buffer: None,
129                frame_times: std::collections::VecDeque::with_capacity(120),
130                max_frame_samples: 120, // ~2 seconds at 60fps
131                perf_recording: None,
132                last_frame_instant: None,
133            })),
134        }
135    }
136
137    /// Set the log buffer (from McpLogLayer::new())
138    pub async fn with_log_buffer(self, buffer: LogBuffer) -> Self {
139        self.state.write().await.log_buffer = Some(buffer);
140        self
141    }
142
143    /// Set the log buffer synchronously (for initialization)
144    pub fn with_log_buffer_sync(self, buffer: LogBuffer) -> Self {
145        // Use try_write to avoid blocking
146        if let Ok(mut state) = self.state.try_write() {
147            state.log_buffer = Some(buffer);
148        }
149        self
150    }
151
152    /// Get the socket path
153    pub async fn socket_path(&self) -> PathBuf {
154        self.state.read().await.socket_path.clone()
155    }
156
157    // Screenshot methods (event-driven)
158
159    /// Request a screenshot and return a receiver to await the result.
160    /// This is more efficient than polling as it uses a oneshot channel.
161    pub async fn request_screenshot(&self) -> oneshot::Receiver<Vec<u8>> {
162        let (tx, rx) = oneshot::channel();
163        self.state.write().await.screenshot_sender = Some(tx);
164        rx
165    }
166
167    /// Check if screenshot is requested and return the sender if available.
168    /// Called by the UI to check if it should capture a screenshot.
169    pub async fn take_screenshot_request(&self) -> bool {
170        self.state.read().await.screenshot_sender.is_some()
171    }
172
173    /// Set screenshot data (PNG encoded) - sends through the oneshot channel.
174    /// Called by the UI after capturing a screenshot.
175    pub async fn set_screenshot(&self, data: Vec<u8>) {
176        let sender = self.state.write().await.screenshot_sender.take();
177        if let Some(tx) = sender {
178            // Ignore error if receiver was dropped (e.g., timeout)
179            let _ = tx.send(data);
180        }
181    }
182
183    // Input methods
184
185    /// Queue an input event to be processed by the egui app
186    pub async fn queue_input(&self, input: PendingInput) {
187        self.state.write().await.pending_inputs.push(input);
188    }
189
190    /// Take all pending input events (clears the queue)
191    pub async fn take_pending_inputs(&self) -> Vec<PendingInput> {
192        std::mem::take(&mut self.state.write().await.pending_inputs)
193    }
194
195    // Highlight methods
196
197    /// Add a highlight to be drawn
198    pub async fn add_highlight(&self, highlight: Highlight) {
199        self.state.write().await.highlights.push(highlight);
200    }
201
202    /// Clear all highlights
203    pub async fn clear_highlights(&self) {
204        self.state.write().await.highlights.clear();
205    }
206
207    /// Get active highlights (removes expired ones)
208    pub async fn get_highlights(&self) -> Vec<Highlight> {
209        let mut state = self.state.write().await;
210        let now = std::time::Instant::now();
211        // Remove expired highlights
212        state
213            .highlights
214            .retain(|h| h.expires_at.is_none() || h.expires_at.unwrap() > now);
215        state.highlights.clone()
216    }
217
218    // Log methods
219
220    /// Get log entries, optionally filtered by level and limited in count
221    pub async fn get_logs(&self, min_level: Option<&str>, limit: Option<usize>) -> Vec<LogEntry> {
222        let state = self.state.read().await;
223        if let Some(ref buffer) = state.log_buffer {
224            let buf = buffer.lock();
225            let min_priority = min_level.map(level_to_priority).unwrap_or(0);
226
227            let filtered: Vec<LogEntry> = buf
228                .iter()
229                .filter(|entry| level_to_priority(&entry.level) >= min_priority)
230                .cloned()
231                .collect();
232
233            match limit {
234                Some(n) => filtered.into_iter().rev().take(n).rev().collect(),
235                None => filtered,
236            }
237        } else {
238            Vec::new()
239        }
240    }
241
242    /// Clear all log entries
243    pub async fn clear_logs(&self) {
244        let state = self.state.read().await;
245        if let Some(ref buffer) = state.log_buffer {
246            buffer.lock().clear();
247        }
248    }
249
250    // Performance monitoring methods
251
252    /// Record a frame for performance monitoring (auto-timing version)
253    /// Call this once at the end of each frame (in eframe::App::update).
254    /// The frame time is automatically calculated from the previous call.
255    pub async fn record_frame_auto(&self) {
256        let mut state = self.state.write().await;
257        let now = std::time::Instant::now();
258
259        if let Some(last) = state.last_frame_instant {
260            let frame_time = now.duration_since(last);
261            let max_samples = state.max_frame_samples;
262
263            // Add to rolling window
264            state.frame_times.push_back(frame_time);
265            while state.frame_times.len() > max_samples {
266                state.frame_times.pop_front();
267            }
268
269            // Add to recording if active
270            if let Some(ref mut recording) = state.perf_recording {
271                recording.frame_times.push(frame_time);
272            }
273        }
274
275        state.last_frame_instant = Some(now);
276    }
277
278    /// Record a frame time for performance monitoring (manual timing version)
279    /// Call this at the end of each frame (in eframe::App::update)
280    pub async fn record_frame(&self, frame_time: std::time::Duration) {
281        let mut state = self.state.write().await;
282        let max_samples = state.max_frame_samples;
283
284        // Add to rolling window
285        state.frame_times.push_back(frame_time);
286        while state.frame_times.len() > max_samples {
287            state.frame_times.pop_front();
288        }
289
290        // Add to recording if active
291        if let Some(ref mut recording) = state.perf_recording {
292            recording.frame_times.push(frame_time);
293
294            // Check if recording should auto-stop
295            if recording.duration_ms > 0 {
296                let elapsed = recording.start_time.elapsed().as_millis() as u64;
297                if elapsed >= recording.duration_ms {
298                    // Recording will be stopped when get_perf_report is called
299                }
300            }
301        }
302    }
303
304    /// Get current frame statistics
305    pub async fn get_frame_stats(&self) -> FrameStats {
306        let state = self.state.read().await;
307
308        if state.frame_times.is_empty() {
309            return FrameStats {
310                fps: 0.0,
311                frame_time_ms: 0.0,
312                frame_time_min_ms: 0.0,
313                frame_time_max_ms: 0.0,
314                sample_count: 0,
315            };
316        }
317
318        let times: Vec<f32> = state
319            .frame_times
320            .iter()
321            .map(|d| d.as_secs_f32() * 1000.0)
322            .collect();
323
324        let sum: f32 = times.iter().sum();
325        let avg = sum / times.len() as f32;
326        let min = times.iter().cloned().fold(f32::INFINITY, f32::min);
327        let max = times.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
328
329        FrameStats {
330            fps: if avg > 0.0 { 1000.0 / avg } else { 0.0 },
331            frame_time_ms: avg,
332            frame_time_min_ms: min,
333            frame_time_max_ms: max,
334            sample_count: times.len(),
335        }
336    }
337
338    /// Start recording performance data
339    pub async fn start_perf_recording(&self, duration_ms: u64) {
340        let mut state = self.state.write().await;
341        state.perf_recording = Some(PerfRecording {
342            start_time: std::time::Instant::now(),
343            frame_times: Vec::new(),
344            duration_ms,
345        });
346    }
347
348    /// Stop recording and get the performance report
349    pub async fn get_perf_report(&self) -> Option<PerfReport> {
350        let mut state = self.state.write().await;
351        let recording = state.perf_recording.take()?;
352
353        if recording.frame_times.is_empty() {
354            return None;
355        }
356
357        let duration_ms = recording.start_time.elapsed().as_millis() as u64;
358        let total_frames = recording.frame_times.len();
359
360        let mut times_ms: Vec<f32> = recording
361            .frame_times
362            .iter()
363            .map(|d| d.as_secs_f32() * 1000.0)
364            .collect();
365
366        let sum: f32 = times_ms.iter().sum();
367        let avg_frame_time = sum / total_frames as f32;
368        let avg_fps = if avg_frame_time > 0.0 {
369            1000.0 / avg_frame_time
370        } else {
371            0.0
372        };
373        let min_frame_time = times_ms.iter().cloned().fold(f32::INFINITY, f32::min);
374        let max_frame_time = times_ms.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
375
376        // Calculate percentiles
377        times_ms.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
378        let p95_idx = (total_frames as f32 * 0.95) as usize;
379        let p99_idx = (total_frames as f32 * 0.99) as usize;
380        let p95_frame_time = times_ms
381            .get(p95_idx.min(total_frames - 1))
382            .copied()
383            .unwrap_or(0.0);
384        let p99_frame_time = times_ms
385            .get(p99_idx.min(total_frames - 1))
386            .copied()
387            .unwrap_or(0.0);
388
389        Some(PerfReport {
390            duration_ms,
391            total_frames,
392            avg_fps,
393            avg_frame_time_ms: avg_frame_time,
394            min_frame_time_ms: min_frame_time,
395            max_frame_time_ms: max_frame_time,
396            p95_frame_time_ms: p95_frame_time,
397            p99_frame_time_ms: p99_frame_time,
398        })
399    }
400
401    /// Start the IPC server in a background task
402    pub fn start_server(&self) -> tokio::task::JoinHandle<()> {
403        let client = self.clone();
404        tokio::spawn(async move {
405            if let Err(e) = IpcServer::run(client).await {
406                tracing::error!("IPC server error: {}", e);
407            }
408        })
409    }
410}
411
412impl Default for McpClient {
413    fn default() -> Self {
414        Self::new()
415    }
416}
417
418// ============================================================================
419// Input Injection Helpers
420// ============================================================================
421
422/// Convert MCP MouseButton to egui PointerButton
423fn convert_mouse_button(button: &MouseButton) -> egui::PointerButton {
424    match button {
425        MouseButton::Left => egui::PointerButton::Primary,
426        MouseButton::Right => egui::PointerButton::Secondary,
427        MouseButton::Middle => egui::PointerButton::Middle,
428    }
429}
430
431/// Parse a key string into egui Key for special keys
432fn parse_special_key(key: &str) -> Option<egui::Key> {
433    match key.to_lowercase().as_str() {
434        // Command keys
435        "enter" | "return" => Some(egui::Key::Enter),
436        "tab" => Some(egui::Key::Tab),
437        "backspace" => Some(egui::Key::Backspace),
438        "delete" => Some(egui::Key::Delete),
439        "escape" | "esc" => Some(egui::Key::Escape),
440        "space" => Some(egui::Key::Space),
441        "arrowup" | "up" => Some(egui::Key::ArrowUp),
442        "arrowdown" | "down" => Some(egui::Key::ArrowDown),
443        "arrowleft" | "left" => Some(egui::Key::ArrowLeft),
444        "arrowright" | "right" => Some(egui::Key::ArrowRight),
445        "home" => Some(egui::Key::Home),
446        "end" => Some(egui::Key::End),
447        "pageup" => Some(egui::Key::PageUp),
448        "pagedown" => Some(egui::Key::PageDown),
449        "insert" => Some(egui::Key::Insert),
450        "copy" => Some(egui::Key::Copy),
451        "cut" => Some(egui::Key::Cut),
452        "paste" => Some(egui::Key::Paste),
453
454        // Function keys F1-F35
455        "f1" => Some(egui::Key::F1),
456        "f2" => Some(egui::Key::F2),
457        "f3" => Some(egui::Key::F3),
458        "f4" => Some(egui::Key::F4),
459        "f5" => Some(egui::Key::F5),
460        "f6" => Some(egui::Key::F6),
461        "f7" => Some(egui::Key::F7),
462        "f8" => Some(egui::Key::F8),
463        "f9" => Some(egui::Key::F9),
464        "f10" => Some(egui::Key::F10),
465        "f11" => Some(egui::Key::F11),
466        "f12" => Some(egui::Key::F12),
467        "f13" => Some(egui::Key::F13),
468        "f14" => Some(egui::Key::F14),
469        "f15" => Some(egui::Key::F15),
470        "f16" => Some(egui::Key::F16),
471        "f17" => Some(egui::Key::F17),
472        "f18" => Some(egui::Key::F18),
473        "f19" => Some(egui::Key::F19),
474        "f20" => Some(egui::Key::F20),
475        "f21" => Some(egui::Key::F21),
476        "f22" => Some(egui::Key::F22),
477        "f23" => Some(egui::Key::F23),
478        "f24" => Some(egui::Key::F24),
479        "f25" => Some(egui::Key::F25),
480        "f26" => Some(egui::Key::F26),
481        "f27" => Some(egui::Key::F27),
482        "f28" => Some(egui::Key::F28),
483        "f29" => Some(egui::Key::F29),
484        "f30" => Some(egui::Key::F30),
485        "f31" => Some(egui::Key::F31),
486        "f32" => Some(egui::Key::F32),
487        "f33" => Some(egui::Key::F33),
488        "f34" => Some(egui::Key::F34),
489        "f35" => Some(egui::Key::F35),
490
491        // Punctuation keys
492        "colon" | ":" => Some(egui::Key::Colon),
493        "comma" | "," => Some(egui::Key::Comma),
494        "backslash" | "\\" => Some(egui::Key::Backslash),
495        "slash" | "/" => Some(egui::Key::Slash),
496        "pipe" | "|" => Some(egui::Key::Pipe),
497        "questionmark" | "?" => Some(egui::Key::Questionmark),
498        "exclamationmark" | "!" => Some(egui::Key::Exclamationmark),
499        "openbracket" | "[" => Some(egui::Key::OpenBracket),
500        "closebracket" | "]" => Some(egui::Key::CloseBracket),
501        "opencurlybracket" | "{" => Some(egui::Key::OpenCurlyBracket),
502        "closecurlybracket" | "}" => Some(egui::Key::CloseCurlyBracket),
503        "backtick" | "grave" | "`" => Some(egui::Key::Backtick),
504        "minus" | "-" => Some(egui::Key::Minus),
505        "period" | "." => Some(egui::Key::Period),
506        "plus" | "+" => Some(egui::Key::Plus),
507        "equals" | "=" => Some(egui::Key::Equals),
508        "semicolon" | ";" => Some(egui::Key::Semicolon),
509        "quote" | "'" => Some(egui::Key::Quote),
510
511        // Digit keys (Num0-Num9)
512        "num0" | "0" => Some(egui::Key::Num0),
513        "num1" | "1" => Some(egui::Key::Num1),
514        "num2" | "2" => Some(egui::Key::Num2),
515        "num3" | "3" => Some(egui::Key::Num3),
516        "num4" | "4" => Some(egui::Key::Num4),
517        "num5" | "5" => Some(egui::Key::Num5),
518        "num6" | "6" => Some(egui::Key::Num6),
519        "num7" | "7" => Some(egui::Key::Num7),
520        "num8" | "8" => Some(egui::Key::Num8),
521        "num9" | "9" => Some(egui::Key::Num9),
522
523        // Letter keys (A-Z)
524        "a" => Some(egui::Key::A),
525        "b" => Some(egui::Key::B),
526        "c" => Some(egui::Key::C),
527        "d" => Some(egui::Key::D),
528        "e" => Some(egui::Key::E),
529        "f" => Some(egui::Key::F),
530        "g" => Some(egui::Key::G),
531        "h" => Some(egui::Key::H),
532        "i" => Some(egui::Key::I),
533        "j" => Some(egui::Key::J),
534        "k" => Some(egui::Key::K),
535        "l" => Some(egui::Key::L),
536        "m" => Some(egui::Key::M),
537        "n" => Some(egui::Key::N),
538        "o" => Some(egui::Key::O),
539        "p" => Some(egui::Key::P),
540        "q" => Some(egui::Key::Q),
541        "r" => Some(egui::Key::R),
542        "s" => Some(egui::Key::S),
543        "t" => Some(egui::Key::T),
544        "u" => Some(egui::Key::U),
545        "v" => Some(egui::Key::V),
546        "w" => Some(egui::Key::W),
547        "x" => Some(egui::Key::X),
548        "y" => Some(egui::Key::Y),
549        "z" => Some(egui::Key::Z),
550
551        // Browser/multimedia keys
552        "browserback" => Some(egui::Key::BrowserBack),
553
554        _ => None,
555    }
556}
557
558/// Inject pending MCP inputs into egui's RawInput.
559///
560/// Call this function in your `eframe::App::raw_input_hook` implementation
561/// to convert MCP inputs into egui events.
562///
563/// # Example
564///
565/// ```rust,ignore
566/// impl eframe::App for MyApp {
567///     fn raw_input_hook(&mut self, ctx: &egui::Context, raw_input: &mut egui::RawInput) {
568///         let inputs = self.runtime.block_on(self.mcp_client.take_pending_inputs());
569///         egui_mcp_client::inject_inputs(ctx, raw_input, inputs);
570///     }
571/// }
572/// ```
573pub fn inject_inputs(
574    ctx: &egui::Context,
575    raw_input: &mut egui::RawInput,
576    inputs: Vec<PendingInput>,
577) {
578    if inputs.is_empty() {
579        return;
580    }
581
582    // Request repaint to ensure UI updates even in background
583    ctx.request_repaint();
584
585    for input in inputs {
586        match input {
587            PendingInput::MoveMouse { x, y } => {
588                tracing::debug!("Injecting mouse move to ({}, {})", x, y);
589                raw_input
590                    .events
591                    .push(egui::Event::PointerMoved(egui::pos2(x, y)));
592            }
593            PendingInput::Click { x, y, button } => {
594                tracing::debug!("Injecting click at ({}, {})", x, y);
595                let egui_button = convert_mouse_button(&button);
596                let pos = egui::pos2(x, y);
597
598                raw_input.events.push(egui::Event::PointerMoved(pos));
599                raw_input.events.push(egui::Event::PointerButton {
600                    pos,
601                    button: egui_button,
602                    pressed: true,
603                    modifiers: egui::Modifiers::NONE,
604                });
605                raw_input.events.push(egui::Event::PointerButton {
606                    pos,
607                    button: egui_button,
608                    pressed: false,
609                    modifiers: egui::Modifiers::NONE,
610                });
611            }
612            PendingInput::DoubleClick { x, y, button } => {
613                tracing::debug!("Injecting double click at ({}, {})", x, y);
614                let egui_button = convert_mouse_button(&button);
615                let pos = egui::pos2(x, y);
616
617                raw_input.events.push(egui::Event::PointerMoved(pos));
618                // First click
619                raw_input.events.push(egui::Event::PointerButton {
620                    pos,
621                    button: egui_button,
622                    pressed: true,
623                    modifiers: egui::Modifiers::NONE,
624                });
625                raw_input.events.push(egui::Event::PointerButton {
626                    pos,
627                    button: egui_button,
628                    pressed: false,
629                    modifiers: egui::Modifiers::NONE,
630                });
631                // Second click
632                raw_input.events.push(egui::Event::PointerButton {
633                    pos,
634                    button: egui_button,
635                    pressed: true,
636                    modifiers: egui::Modifiers::NONE,
637                });
638                raw_input.events.push(egui::Event::PointerButton {
639                    pos,
640                    button: egui_button,
641                    pressed: false,
642                    modifiers: egui::Modifiers::NONE,
643                });
644            }
645            PendingInput::Drag {
646                start_x,
647                start_y,
648                end_x,
649                end_y,
650                button,
651            } => {
652                tracing::debug!(
653                    "Injecting drag from ({}, {}) to ({}, {})",
654                    start_x,
655                    start_y,
656                    end_x,
657                    end_y
658                );
659                let egui_button = convert_mouse_button(&button);
660                let start_pos = egui::pos2(start_x, start_y);
661                let end_pos = egui::pos2(end_x, end_y);
662
663                raw_input.events.push(egui::Event::PointerMoved(start_pos));
664                raw_input.events.push(egui::Event::PointerButton {
665                    pos: start_pos,
666                    button: egui_button,
667                    pressed: true,
668                    modifiers: egui::Modifiers::NONE,
669                });
670                raw_input.events.push(egui::Event::PointerMoved(end_pos));
671                raw_input.events.push(egui::Event::PointerButton {
672                    pos: end_pos,
673                    button: egui_button,
674                    pressed: false,
675                    modifiers: egui::Modifiers::NONE,
676                });
677            }
678            PendingInput::Keyboard { key } => {
679                tracing::debug!("Injecting keyboard input: {}", key);
680                if let Some(egui_key) = parse_special_key(&key) {
681                    // Special key (Enter, Tab, Backspace, etc.)
682                    raw_input.events.push(egui::Event::Key {
683                        key: egui_key,
684                        physical_key: Some(egui_key),
685                        pressed: true,
686                        repeat: false,
687                        modifiers: egui::Modifiers::NONE,
688                    });
689                    raw_input.events.push(egui::Event::Key {
690                        key: egui_key,
691                        physical_key: Some(egui_key),
692                        pressed: false,
693                        repeat: false,
694                        modifiers: egui::Modifiers::NONE,
695                    });
696                } else {
697                    // Regular text input
698                    raw_input.events.push(egui::Event::Text(key));
699                }
700            }
701            PendingInput::Scroll {
702                x,
703                y,
704                delta_x,
705                delta_y,
706            } => {
707                tracing::debug!(
708                    "Injecting scroll at ({}, {}) delta ({}, {})",
709                    x,
710                    y,
711                    delta_x,
712                    delta_y
713                );
714                raw_input
715                    .events
716                    .push(egui::Event::PointerMoved(egui::pos2(x, y)));
717                raw_input.events.push(egui::Event::MouseWheel {
718                    unit: egui::MouseWheelUnit::Point,
719                    delta: egui::vec2(delta_x, delta_y),
720                    modifiers: egui::Modifiers::NONE,
721                });
722            }
723        }
724    }
725}
726
727// ============================================================================
728// Highlight Drawing Helper
729// ============================================================================
730
731/// Draw active highlights on the egui context.
732///
733/// Call this function at the end of your `eframe::App::update` implementation
734/// to draw element highlights over the UI.
735///
736/// # Example
737///
738/// ```rust,ignore
739/// impl eframe::App for MyApp {
740///     fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
741///         // ... your UI code ...
742///
743///         // Draw highlights at the end
744///         let highlights = self.runtime.block_on(self.mcp_client.get_highlights());
745///         egui_mcp_client::draw_highlights(ctx, &highlights);
746///     }
747/// }
748/// ```
749pub fn draw_highlights(ctx: &egui::Context, highlights: &[Highlight]) {
750    if highlights.is_empty() {
751        return;
752    }
753
754    // Request repaint to ensure highlights are updated (for expiration)
755    ctx.request_repaint();
756
757    // Use the debug painter to draw on top of everything
758    let painter = ctx.debug_painter();
759
760    for highlight in highlights {
761        // Draw a colored rectangle border
762        painter.rect_stroke(
763            highlight.rect,
764            0.0, // No rounding
765            egui::Stroke::new(3.0, highlight.color),
766            egui::StrokeKind::Outside,
767        );
768
769        // Draw a semi-transparent fill
770        let fill_color = egui::Color32::from_rgba_unmultiplied(
771            highlight.color.r(),
772            highlight.color.g(),
773            highlight.color.b(),
774            highlight.color.a() / 4, // 25% opacity for fill
775        );
776        painter.rect_filled(highlight.rect, 0.0, fill_color);
777    }
778}
779
780#[cfg(test)]
781mod tests {
782    use super::*;
783
784    #[test]
785    fn test_parse_special_key_command_keys() {
786        // Basic command keys
787        assert_eq!(parse_special_key("Enter"), Some(egui::Key::Enter));
788        assert_eq!(parse_special_key("return"), Some(egui::Key::Enter));
789        assert_eq!(parse_special_key("Tab"), Some(egui::Key::Tab));
790        assert_eq!(parse_special_key("Backspace"), Some(egui::Key::Backspace));
791        assert_eq!(parse_special_key("Delete"), Some(egui::Key::Delete));
792        assert_eq!(parse_special_key("Escape"), Some(egui::Key::Escape));
793        assert_eq!(parse_special_key("esc"), Some(egui::Key::Escape));
794        assert_eq!(parse_special_key("Space"), Some(egui::Key::Space));
795        assert_eq!(parse_special_key("Insert"), Some(egui::Key::Insert));
796    }
797
798    #[test]
799    fn test_parse_special_key_arrow_keys() {
800        assert_eq!(parse_special_key("ArrowUp"), Some(egui::Key::ArrowUp));
801        assert_eq!(parse_special_key("up"), Some(egui::Key::ArrowUp));
802        assert_eq!(parse_special_key("ArrowDown"), Some(egui::Key::ArrowDown));
803        assert_eq!(parse_special_key("down"), Some(egui::Key::ArrowDown));
804        assert_eq!(parse_special_key("ArrowLeft"), Some(egui::Key::ArrowLeft));
805        assert_eq!(parse_special_key("left"), Some(egui::Key::ArrowLeft));
806        assert_eq!(parse_special_key("ArrowRight"), Some(egui::Key::ArrowRight));
807        assert_eq!(parse_special_key("right"), Some(egui::Key::ArrowRight));
808    }
809
810    #[test]
811    fn test_parse_special_key_navigation_keys() {
812        assert_eq!(parse_special_key("Home"), Some(egui::Key::Home));
813        assert_eq!(parse_special_key("End"), Some(egui::Key::End));
814        assert_eq!(parse_special_key("PageUp"), Some(egui::Key::PageUp));
815        assert_eq!(parse_special_key("PageDown"), Some(egui::Key::PageDown));
816    }
817
818    #[test]
819    fn test_parse_special_key_clipboard_keys() {
820        assert_eq!(parse_special_key("Copy"), Some(egui::Key::Copy));
821        assert_eq!(parse_special_key("Cut"), Some(egui::Key::Cut));
822        assert_eq!(parse_special_key("Paste"), Some(egui::Key::Paste));
823    }
824
825    #[test]
826    fn test_parse_special_key_function_keys() {
827        // F1-F12
828        assert_eq!(parse_special_key("F1"), Some(egui::Key::F1));
829        assert_eq!(parse_special_key("f12"), Some(egui::Key::F12));
830
831        // F13-F35
832        assert_eq!(parse_special_key("F13"), Some(egui::Key::F13));
833        assert_eq!(parse_special_key("F20"), Some(egui::Key::F20));
834        assert_eq!(parse_special_key("F35"), Some(egui::Key::F35));
835    }
836
837    #[test]
838    fn test_parse_special_key_punctuation_by_name() {
839        assert_eq!(parse_special_key("colon"), Some(egui::Key::Colon));
840        assert_eq!(parse_special_key("comma"), Some(egui::Key::Comma));
841        assert_eq!(parse_special_key("backslash"), Some(egui::Key::Backslash));
842        assert_eq!(parse_special_key("slash"), Some(egui::Key::Slash));
843        assert_eq!(parse_special_key("pipe"), Some(egui::Key::Pipe));
844        assert_eq!(
845            parse_special_key("questionmark"),
846            Some(egui::Key::Questionmark)
847        );
848        assert_eq!(
849            parse_special_key("exclamationmark"),
850            Some(egui::Key::Exclamationmark)
851        );
852        assert_eq!(
853            parse_special_key("openbracket"),
854            Some(egui::Key::OpenBracket)
855        );
856        assert_eq!(
857            parse_special_key("closebracket"),
858            Some(egui::Key::CloseBracket)
859        );
860        assert_eq!(
861            parse_special_key("opencurlybracket"),
862            Some(egui::Key::OpenCurlyBracket)
863        );
864        assert_eq!(
865            parse_special_key("closecurlybracket"),
866            Some(egui::Key::CloseCurlyBracket)
867        );
868        assert_eq!(parse_special_key("backtick"), Some(egui::Key::Backtick));
869        assert_eq!(parse_special_key("grave"), Some(egui::Key::Backtick));
870        assert_eq!(parse_special_key("minus"), Some(egui::Key::Minus));
871        assert_eq!(parse_special_key("period"), Some(egui::Key::Period));
872        assert_eq!(parse_special_key("plus"), Some(egui::Key::Plus));
873        assert_eq!(parse_special_key("equals"), Some(egui::Key::Equals));
874        assert_eq!(parse_special_key("semicolon"), Some(egui::Key::Semicolon));
875        assert_eq!(parse_special_key("quote"), Some(egui::Key::Quote));
876    }
877
878    #[test]
879    fn test_parse_special_key_punctuation_by_symbol() {
880        assert_eq!(parse_special_key(":"), Some(egui::Key::Colon));
881        assert_eq!(parse_special_key(","), Some(egui::Key::Comma));
882        assert_eq!(parse_special_key("\\"), Some(egui::Key::Backslash));
883        assert_eq!(parse_special_key("/"), Some(egui::Key::Slash));
884        assert_eq!(parse_special_key("|"), Some(egui::Key::Pipe));
885        assert_eq!(parse_special_key("?"), Some(egui::Key::Questionmark));
886        assert_eq!(parse_special_key("!"), Some(egui::Key::Exclamationmark));
887        assert_eq!(parse_special_key("["), Some(egui::Key::OpenBracket));
888        assert_eq!(parse_special_key("]"), Some(egui::Key::CloseBracket));
889        assert_eq!(parse_special_key("{"), Some(egui::Key::OpenCurlyBracket));
890        assert_eq!(parse_special_key("}"), Some(egui::Key::CloseCurlyBracket));
891        assert_eq!(parse_special_key("`"), Some(egui::Key::Backtick));
892        assert_eq!(parse_special_key("-"), Some(egui::Key::Minus));
893        assert_eq!(parse_special_key("."), Some(egui::Key::Period));
894        assert_eq!(parse_special_key("+"), Some(egui::Key::Plus));
895        assert_eq!(parse_special_key("="), Some(egui::Key::Equals));
896        assert_eq!(parse_special_key(";"), Some(egui::Key::Semicolon));
897        assert_eq!(parse_special_key("'"), Some(egui::Key::Quote));
898    }
899
900    #[test]
901    fn test_parse_special_key_digit_keys() {
902        assert_eq!(parse_special_key("0"), Some(egui::Key::Num0));
903        assert_eq!(parse_special_key("1"), Some(egui::Key::Num1));
904        assert_eq!(parse_special_key("9"), Some(egui::Key::Num9));
905        assert_eq!(parse_special_key("num0"), Some(egui::Key::Num0));
906        assert_eq!(parse_special_key("num5"), Some(egui::Key::Num5));
907    }
908
909    #[test]
910    fn test_parse_special_key_letter_keys() {
911        assert_eq!(parse_special_key("a"), Some(egui::Key::A));
912        assert_eq!(parse_special_key("A"), Some(egui::Key::A));
913        assert_eq!(parse_special_key("z"), Some(egui::Key::Z));
914        assert_eq!(parse_special_key("Z"), Some(egui::Key::Z));
915        assert_eq!(parse_special_key("m"), Some(egui::Key::M));
916    }
917
918    #[test]
919    fn test_parse_special_key_browser_keys() {
920        assert_eq!(
921            parse_special_key("BrowserBack"),
922            Some(egui::Key::BrowserBack)
923        );
924        assert_eq!(
925            parse_special_key("browserback"),
926            Some(egui::Key::BrowserBack)
927        );
928    }
929
930    #[test]
931    fn test_parse_special_key_unknown() {
932        // Unknown keys return None
933        assert_eq!(parse_special_key("unknown"), None);
934        assert_eq!(parse_special_key("ctrl"), None);
935        assert_eq!(parse_special_key("shift"), None);
936        assert_eq!(parse_special_key("alt"), None);
937    }
938
939    #[test]
940    fn test_parse_special_key_case_insensitive() {
941        // All keys should be case-insensitive
942        assert_eq!(parse_special_key("ENTER"), Some(egui::Key::Enter));
943        assert_eq!(parse_special_key("Enter"), Some(egui::Key::Enter));
944        assert_eq!(parse_special_key("enter"), Some(egui::Key::Enter));
945        assert_eq!(parse_special_key("eNtEr"), Some(egui::Key::Enter));
946    }
947}