Skip to main content

mold_ai_tui/
lib.rs

1// Phase 1 skeleton — many types are defined for future phases.
2#![allow(dead_code)]
3
4mod action;
5mod animation;
6mod app;
7mod backend;
8mod event;
9mod gallery_scan;
10mod history;
11mod model_info;
12mod session;
13#[cfg(test)]
14pub(crate) mod test_env;
15mod thumbnails;
16mod ui;
17
18use std::io;
19use std::panic;
20
21use anyhow::Result;
22use crossterm::{
23    event::{DisableMouseCapture, EnableMouseCapture},
24    execute,
25    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
26};
27use ratatui::prelude::*;
28
29use app::App;
30
31/// Launch the mold TUI.
32///
33/// `host` overrides the default MOLD_HOST for remote generation.
34/// `local` forces local-only inference (no server connection).
35pub async fn run_tui(host: Option<String>, local: bool) -> Result<()> {
36    // Probe the terminal image protocol *before* entering raw mode / alternate screen,
37    // because the query writes to stdout and reads the terminal's reply.
38    let picker = ratatui_image::picker::Picker::from_query_stdio()
39        .unwrap_or_else(|_| ratatui_image::picker::Picker::from_fontsize((8, 16)));
40
41    // Set up the terminal
42    enable_raw_mode()?;
43    let mut stdout = io::stdout();
44    execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
45    let backend = CrosstermBackend::new(stdout);
46    let mut terminal = Terminal::new(backend)?;
47
48    // Install a panic hook that restores the terminal before printing the panic
49    let original_hook = panic::take_hook();
50    panic::set_hook(Box::new(move |panic_info| {
51        let _ = disable_raw_mode();
52        let _ = execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture);
53        original_hook(panic_info);
54    }));
55
56    // Create the app and run
57    let version = mold_core::build_info::version_string();
58    tracing::info!(%version, "starting mold tui");
59    let mut app = App::new(host, local, picker)?;
60    let result = run_event_loop(&mut terminal, &mut app).await;
61
62    // Clean up background server process
63    app.shutdown();
64
65    // Restore the terminal
66    disable_raw_mode()?;
67    execute!(
68        terminal.backend_mut(),
69        LeaveAlternateScreen,
70        DisableMouseCapture
71    )?;
72    terminal.show_cursor()?;
73
74    result
75}
76
77async fn run_event_loop(
78    terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
79    app: &mut App,
80) -> Result<()> {
81    let mut last_resource_refresh = std::time::Instant::now();
82    // Initial resource info refresh
83    if app.should_poll_remote() {
84        app.spawn_server_status_fetch();
85    } else {
86        app.resource_info.refresh_local();
87    }
88
89    loop {
90        terminal.draw(|frame| ui::render(frame, app))?;
91
92        // Poll for crossterm events with a short timeout (~60fps)
93        if crossterm::event::poll(std::time::Duration::from_millis(16))? {
94            let event = crossterm::event::read()?;
95            app.handle_crossterm_event(event);
96        }
97
98        // Process any background task results
99        app.process_background_events();
100
101        // Advance any animated previews so the next draw shows the next frame
102        // when its delay has elapsed.
103        app.tick_animations();
104
105        // Refresh resource info every 2 seconds
106        if last_resource_refresh.elapsed() >= std::time::Duration::from_secs(2) {
107            if app.should_poll_remote() {
108                app.spawn_server_status_fetch();
109            } else {
110                app.resource_info.refresh_local();
111            }
112            last_resource_refresh = std::time::Instant::now();
113        }
114
115        if app.should_quit {
116            return Ok(());
117        }
118    }
119}