Skip to main content

mach/
lib.rs

1//! mach — a powerful yet easy-to-use todo TUI, built with ratatui.
2
3pub mod app;
4pub mod banner;
5pub mod body;
6pub mod cli;
7pub mod due;
8pub mod duepicker;
9pub mod form;
10pub mod fuzzy;
11pub mod image;
12pub mod input;
13pub mod model;
14pub mod open;
15pub mod settings;
16pub mod slash;
17pub mod store;
18pub mod text_input;
19pub mod theme;
20pub mod ui;
21pub mod undo;
22pub mod update;
23
24use std::io;
25use std::time::Duration;
26
27use ratatui::DefaultTerminal;
28use ratatui::crossterm::event::{
29    self, DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
30    KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
31};
32use ratatui::crossterm::execute;
33
34use crate::app::App;
35
36pub const VERSION: &str = env!("CARGO_PKG_VERSION");
37
38/// Entry point for the `mach` binary.
39pub fn run() {
40    cli::run();
41}
42
43pub fn run_tui() -> io::Result<()> {
44    // Probe graphics support before the event loop takes stdin.
45    // (ratatui-image prefers the alternate screen; answers are the same either way.)
46    let images = image::ImageStore::detect();
47
48    let mut terminal = ratatui::init();
49    let mut out = io::stdout();
50    let _ = execute!(out, EnableMouseCapture, EnableBracketedPaste);
51    // Disambiguate Ctrl/Alt+arrows. Do not enable REPORT_ALL_KEYS_AS_ESCAPE_CODES
52    // (breaks plain `/` on many terminals).
53    let enhanced = execute!(
54        out,
55        PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES)
56    )
57    .is_ok();
58
59    let mut app = App::new(VERSION);
60    app.images = images;
61    let result = event_loop(&mut terminal, &mut app);
62
63    if enhanced {
64        let _ = execute!(out, PopKeyboardEnhancementFlags);
65    }
66    let _ = execute!(out, DisableBracketedPaste, DisableMouseCapture);
67    ratatui::restore();
68    result
69}
70
71fn event_loop(terminal: &mut DefaultTerminal, app: &mut App) -> io::Result<()> {
72    let mut last_clock = String::new();
73    loop {
74        // Input first so keys are not blocked behind GIF encode on draw.
75        while event::poll(Duration::ZERO)? {
76            input::handle_event(app, event::read()?);
77            app.mark_dirty();
78            if app.should_quit {
79                app.flush_saves_now();
80                return Ok(());
81            }
82        }
83        let _ = app.expire_message();
84        if app.poll_update_check() {
85            app.mark_dirty();
86        }
87        if app.flush_saves() {
88            app.mark_dirty();
89        }
90        // Cell pixel size can change without a resize event (e.g. move display).
91        if app.images.recheck_cell_size() {
92            app.mark_dirty();
93        }
94        if app.images.poll_pending() {
95            app.mark_dirty();
96        }
97
98        let gif_advanced = app.form.as_mut().is_some_and(|f| f.tick_gif());
99        if gif_advanced {
100            app.mark_dirty();
101        }
102        let need_fast = app.form.as_ref().is_some_and(|f| f.gif_playing());
103
104        let clock = crate::due::now_string(&app.settings.date_format);
105        if clock != last_clock {
106            last_clock = clock;
107            app.mark_dirty();
108        }
109
110        if app.dirty {
111            terminal.draw(|frame| ui::draw(frame, app))?;
112            app.dirty = false;
113        }
114
115        let wait = if need_fast {
116            Duration::from_millis(30)
117        } else if app.images.has_pending() {
118            Duration::from_millis(16)
119        } else if app.tasks_dirty_pending() {
120            Duration::from_millis(50)
121        } else {
122            Duration::from_millis(500)
123        };
124        let _ = event::poll(wait)?;
125    }
126}