mi6_tui/
lib.rs

1//! mi6 - A top-like CLI for monitoring agentic coding sessions.
2
3#![allow(clippy::print_stdout, clippy::print_stderr)]
4// Test code can use unwrap/panic/etc. freely - these are fine in tests
5#![cfg_attr(
6    test,
7    expect(
8        clippy::unwrap_used,
9        clippy::panic,
10        clippy::approx_constant,
11        clippy::field_reassign_with_default
12    )
13)]
14
15mod app;
16mod domain;
17mod infra;
18mod render;
19
20use domain::stored_config::Config as StoredConfig;
21
22// Public API: Only expose what the binary crate needs
23pub use domain::{SessionField, parse_column_spec};
24
25use std::io::{self, IsTerminal};
26use std::sync::mpsc;
27use std::thread;
28use std::time::Duration;
29
30use anyhow::Result;
31use crossterm::{
32    event::{DisableMouseCapture, EnableMouseCapture},
33    execute,
34    terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
35};
36use ratatui::{Terminal, backend::CrosstermBackend};
37
38use app::App;
39use infra::{
40    DEFAULT_REFRESH_IDX, REFRESH_INTERVALS_MS, check_and_enable_new_frameworks, find_interval_idx,
41    refresh_thread, transcript_poll_thread,
42};
43
44/// CLI configuration passed from command-line arguments.
45#[derive(Debug, Default, Clone)]
46pub struct Config {
47    /// Initial refresh interval in milliseconds.
48    pub interval_ms: Option<u64>,
49    /// Transcript poll interval in milliseconds.
50    pub transcript_poll_ms: Option<u64>,
51    /// Show all sessions including dead ones.
52    pub show_all: bool,
53    /// Color theme name (overrides stored config).
54    pub theme: Option<String>,
55    /// Column visibility override from CLI (if specified).
56    pub columns: Option<Vec<bool>>,
57    /// Expert mode: shows mi6 process stats in global stats panel.
58    pub expert_mode: bool,
59}
60
61/// Run the mi6 TUI application.
62pub fn run(config: Config) -> Result<()> {
63    // Check for interactive terminal before attempting TUI setup
64    if !std::io::stdout().is_terminal() {
65        anyhow::bail!("mi6 requires an interactive terminal");
66    }
67
68    // Determine initial refresh interval: CLI > stored config > default
69    let stored_config = StoredConfig::load();
70
71    // Check for newly installed frameworks and enable them based on per-framework auto-enable settings.
72    // Uses a lock file to coordinate between mi6 instances.
73    let newly_activated = check_and_enable_new_frameworks(&stored_config.framework_auto_enable);
74    if !newly_activated.is_empty() {
75        eprintln!("Auto-enabled mi6 for: {}", newly_activated.join(", "));
76    }
77
78    let refresh_interval_idx = config.interval_ms.map_or_else(
79        || {
80            stored_config
81                .refresh_interval_ms
82                .map_or(DEFAULT_REFRESH_IDX, find_interval_idx)
83        },
84        find_interval_idx,
85    );
86    let initial_interval = Duration::from_millis(REFRESH_INTERVALS_MS[refresh_interval_idx]);
87
88    // Determine initial transcript poll interval: CLI > stored config > default
89    let transcript_poll_interval_idx = config.transcript_poll_ms.map_or_else(
90        || {
91            stored_config
92                .transcript_poll_interval_ms
93                .map_or(DEFAULT_REFRESH_IDX, find_interval_idx)
94        },
95        find_interval_idx,
96    );
97    let initial_transcript_poll_interval =
98        Duration::from_millis(REFRESH_INTERVALS_MS[transcript_poll_interval_idx]);
99
100    // Start background data loading FIRST - this opens DB and queries
101    let (data_tx, data_rx) = mpsc::channel();
102    let (interval_tx, interval_rx) = mpsc::channel();
103    thread::spawn(move || {
104        refresh_thread(&data_tx, &interval_rx, initial_interval);
105    });
106
107    // Start transcript poll thread with lock status reporting
108    let (transcript_poll_tx, transcript_poll_rx) = mpsc::channel();
109    let (lock_status_tx, lock_status_rx) = mpsc::channel();
110    thread::spawn(move || {
111        transcript_poll_thread(
112            &transcript_poll_rx,
113            &lock_status_tx,
114            initial_transcript_poll_interval,
115        );
116    });
117
118    // Start background update check (if enabled)
119    let check_for_updates_enabled = stored_config.check_for_updates.unwrap_or(true);
120    let (update_tx, update_rx) = mpsc::channel::<Vec<String>>();
121    if check_for_updates_enabled {
122        thread::spawn(move || {
123            // Check for updates in background - errors are silently ignored
124            // since this is a non-critical feature
125            if let Ok(Some(update_info)) = mi6_cli::check_for_update() {
126                // Send the update notifications (ignore send errors - receiver might be gone)
127                let _ = update_tx.send(update_info.notification_messages());
128            }
129        });
130    }
131
132    // Block-wait for first data BEFORE entering alternate screen
133    // This prevents the "empty frame" flash - screen switches with data ready
134    let first_data = data_rx.recv_timeout(Duration::from_millis(500)).ok();
135
136    // NOW setup terminal - user never sees empty screen
137    enable_raw_mode()?;
138    let mut stdout = io::stdout();
139    execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
140    let backend = CrosstermBackend::new(stdout);
141    let mut terminal = Terminal::new(backend)?;
142
143    // Build startup messages for the message log
144    let mut startup_messages: Vec<String> = Vec::new();
145
146    // Welcome message with version and git hash (if available)
147    let git_hash = env!("MI6_GIT_HASH");
148    if git_hash.is_empty() {
149        startup_messages.push(format!("Welcome to mi6 v{}", env!("CARGO_PKG_VERSION")));
150    } else {
151        startup_messages.push(format!(
152            "Welcome to mi6 v{} (0x{})",
153            env!("CARGO_PKG_VERSION"),
154            git_hash
155        ));
156    }
157
158    // Auto-enabled frameworks
159    for name in &newly_activated {
160        startup_messages.push(format!("Auto-enabled mi6 for {name}"));
161    }
162
163    // Create app with first data already loaded
164    let app = App::with_first_data(
165        data_rx,
166        interval_tx,
167        Some(transcript_poll_tx),
168        Some(lock_status_rx),
169        Some(update_rx),
170        first_data,
171        &stored_config,
172        refresh_interval_idx,
173        transcript_poll_interval_idx,
174        config.show_all,
175        config.theme.as_deref(),
176        config.columns,
177        startup_messages,
178        config.expert_mode,
179    );
180
181    let result = infra::run_app(&mut terminal, app);
182
183    // Restore terminal
184    disable_raw_mode()?;
185    execute!(
186        terminal.backend_mut(),
187        LeaveAlternateScreen,
188        DisableMouseCapture
189    )?;
190    terminal.show_cursor()?;
191
192    if let Err(e) = result {
193        eprintln!("Error: {e}");
194    }
195
196    Ok(())
197}