mi6_cli/commands/
watch.rs

1use std::io::{self, BufReader, Read, Write};
2use std::time::Duration;
3
4use anyhow::{Context, Result};
5
6use mi6_core::{EventQuery, Order, Storage};
7use termion::async_stdin;
8use termion::raw::IntoRawMode;
9use termion::terminal_size;
10
11use crate::display::{calculate_details_width, print_event_row, print_table_header};
12
13/// Default poll interval in milliseconds
14const DEFAULT_POLL_MS: u64 = 500;
15
16/// Default terminal height if we can't detect it (conservative to avoid flooding)
17const DEFAULT_TERMINAL_HEIGHT: u16 = 20;
18
19/// Options for the `watch` command.
20///
21/// Using a struct instead of individual parameters makes the API more maintainable
22/// and prevents parameter order mistakes, especially with multiple `Option<String>` fields.
23#[derive(Debug, Clone)]
24pub struct WatchOptions {
25    /// Filter by session ID
26    pub session: Option<String>,
27    /// Filter by event type
28    pub event_type: Option<String>,
29    /// Filter by permission mode
30    pub permission_mode: Option<String>,
31    /// Filter by framework (claude, gemini, cursor, etc.)
32    pub framework: Option<String>,
33    /// Poll interval in milliseconds (default: 500)
34    pub poll_ms: u64,
35}
36
37impl Default for WatchOptions {
38    fn default() -> Self {
39        Self {
40            session: None,
41            event_type: None,
42            permission_mode: None,
43            framework: None,
44            poll_ms: DEFAULT_POLL_MS,
45        }
46    }
47}
48
49impl WatchOptions {
50    /// Create new options with default values.
51    pub fn new() -> Self {
52        Self::default()
53    }
54
55    /// Set the session filter.
56    pub fn session(mut self, session: impl Into<Option<String>>) -> Self {
57        self.session = session.into();
58        self
59    }
60
61    /// Set the event type filter.
62    pub fn event_type(mut self, event_type: impl Into<Option<String>>) -> Self {
63        self.event_type = event_type.into();
64        self
65    }
66
67    /// Set the permission mode filter.
68    pub fn permission_mode(mut self, permission_mode: impl Into<Option<String>>) -> Self {
69        self.permission_mode = permission_mode.into();
70        self
71    }
72
73    /// Set the framework filter.
74    pub fn framework(mut self, framework: impl Into<Option<String>>) -> Self {
75        self.framework = framework.into();
76        self
77    }
78
79    /// Set the poll interval in milliseconds.
80    pub fn poll_ms(mut self, poll_ms: u64) -> Self {
81        self.poll_ms = poll_ms;
82        self
83    }
84}
85
86/// Maximum initial events to display (prevents flooding on terminals that report huge heights)
87const MAX_INITIAL_LINES: usize = 50;
88
89/// Watch for new events and print them as they arrive
90pub fn run_watch<S: Storage>(storage: &S, options: WatchOptions) -> Result<()> {
91    // Always show the APP column
92    let show_app = true;
93    let details_width = calculate_details_width(show_app);
94
95    // Print header
96    print_table_header(show_app, details_width);
97    io::stdout().flush().context("failed to flush stdout")?;
98
99    // Display initial events to fill the terminal (minus 5 for header/separator/margin/footer)
100    let term_height = terminal_size()
101        .map(|(_, h)| h)
102        .unwrap_or(DEFAULT_TERMINAL_HEIGHT) as usize;
103    let initial_lines = term_height.saturating_sub(5).min(MAX_INITIAL_LINES);
104    let mut last_event_id = 0i64;
105
106    if initial_lines > 0 {
107        // Query most recent events (including API requests)
108        let mut initial_query = EventQuery::new()
109            .with_session(options.session.clone())
110            .with_event_type(options.event_type.clone())
111            .with_permission_mode(options.permission_mode.clone())
112            .with_direction(Order::Desc)
113            .with_limit(initial_lines);
114        if let Some(ref framework) = options.framework {
115            initial_query = initial_query.with_framework(framework);
116        }
117
118        let mut initial_events = storage
119            .query(&initial_query)
120            .context("failed to query initial events")?;
121
122        // Track highest ID
123        for event in &initial_events {
124            if let Some(id) = event.id {
125                last_event_id = last_event_id.max(id);
126            }
127        }
128
129        // Reverse to show oldest first
130        initial_events.reverse();
131
132        for event in &initial_events {
133            print_event_row(event, show_app, details_width, false);
134        }
135        io::stdout().flush().context("failed to flush stdout")?;
136    }
137
138    // Enable raw mode - the guard restores terminal on drop
139    let _raw = io::stdout()
140        .into_raw_mode()
141        .context("failed to enable raw mode")?;
142
143    // Get async stdin for non-blocking reads
144    let mut stdin = BufReader::new(async_stdin()).bytes();
145
146    loop {
147        // Check for keyboard input (non-blocking)
148        if let Some(Ok(b)) = stdin.next() {
149            // Quit on 'q' or Ctrl+C (0x03)
150            if b == b'q' || b == 0x03 {
151                return Ok(());
152            }
153        }
154
155        // Get new events after the last seen ID (includes API requests)
156        // Use order_by_id() for watching - returns events in insertion order
157        let mut watch_query = EventQuery::new()
158            .order_by_id()
159            .with_session(options.session.clone())
160            .with_event_type(options.event_type.clone())
161            .with_permission_mode(options.permission_mode.clone())
162            .with_after_id(last_event_id)
163            .with_limit(100);
164        if let Some(ref framework) = options.framework {
165            watch_query = watch_query.with_framework(framework);
166        }
167
168        let new_events = storage
169            .query(&watch_query)
170            .context("failed to query events")?;
171
172        // Track new highest ID and print new events
173        for event in &new_events {
174            if let Some(id) = event.id {
175                last_event_id = last_event_id.max(id);
176            }
177            print_event_row(event, show_app, details_width, true);
178            io::stdout().flush().context("failed to flush stdout")?;
179        }
180
181        // Sleep before polling again
182        std::thread::sleep(Duration::from_millis(options.poll_ms));
183    }
184}