Files
codex/prs/bolinfest/study/PR-1683-study.md
2025-09-02 15:17:45 -07:00

3.8 KiB
Raw Blame History

DOs

  • Use poll() before read(): Check for input with a short timeout so the event reader doesnt hold Crossterms global event lock, which can block cursor::position() and cause resize crashes.
use std::time::{Duration, Instant};
use crossterm::event::{self, Event};

const EVENT_POLL: Duration = Duration::from_millis(100);

loop {
    if event::poll(EVENT_POLL)? {
        match event::read()? {
            Event::Key(k) => app_event_tx.send(AppEvent::KeyEvent(k)),
            Event::Resize(_, _) => app_event_tx.send(AppEvent::RequestRedraw),
            Event::Mouse(m) => match m.kind {
                MouseEventKind::ScrollUp => scroll_event_helper.scroll_up(),
                MouseEventKind::ScrollDown => scroll_event_helper.scroll_down(),
                _ => {}
            },
            Event::Paste(p) => app_event_tx.send(AppEvent::Paste(p.replace("\r", "\n"))),
            _ => {}
        }
    } else {
        // No input this tick; do lightweight work or continue.
    }
}
  • Keep timeout comments precise: Explain why poll() is used and distinguish the poll interval from Crossterms separate lock timeout used by cursor::position().
// Poll every 100ms to avoid holding Crossterms event lock.
// cursor::position() may wait up to ~2s to acquire that lock;
// if the reader holds it continuously, position reads can fail.
  • Cite upstream patterns: Add a brief comment linking to Ratatuis own example so future maintainers recognize this is a known, recommended pattern.
// Pattern inspired by Ratatui inline example:
// https://github.com/ratatui/ratatui/blob/9836f0760d4a053d9d1eba78171be89cb22dc850/examples/apps/inline/src/main.rs#L98-L118
  • Model Ratatuis tick/input loop: Optionally separate “ticks” from input so the UI can update periodically even without events (closer to Ratatui examples).
use std::time::{Duration, Instant};
use crossterm::event::{self, Event};

const TICK_RATE: Duration = Duration::from_millis(200);

let mut last_tick = Instant::now();
loop {
    let timeout = TICK_RATE.saturating_sub(last_tick.elapsed());
    if event::poll(timeout)? {
        match event::read()? {
            Event::Key(k) => app_event_tx.send(AppEvent::KeyEvent(k)),
            Event::Resize(_, _) => app_event_tx.send(AppEvent::RequestRedraw),
            _ => {}
        }
    }
    if last_tick.elapsed() >= TICK_RATE {
        app_event_tx.send(AppEvent::Tick);
        last_tick = Instant::now();
    }
}
  • Use named durations to avoid confusion: Make differing timeouts explicit and keep comments in sync.
const EVENT_POLL: Duration = Duration::from_millis(100);
const CURSOR_LOCK_TIMEOUT_DOC: &str = "cursor::position() may wait ~2s for event lock";

DONTs

  • Dont call read() in a tight loop: This can monopolize the event lock and make cursor::position() fail during resizes.
// BAD: holds the event lock continuously
while let Ok(event) = crossterm::event::read() {
    handle(event); // Other code needing the event lock may starve
}
  • Dont write ambiguous timeout comments: Avoid mixing numbers (e.g., “2 sec” vs “100ms”) without clarifying what each applies to.
// BAD: “Use 100ms so we dont hit the 2s timeout”
// Which timeout? For what call? Be explicit.
  • Dont diverge from proven upstream patterns without reason: If you implement a different loop structure, explain why its needed for your app.
// If not following Ratatuis poll+read+tick pattern,
// document the rationale and implications for event lock usage.
  • Dont redraw lazily on resize: Make resize explicit and cheap—request a redraw promptly.
match event {
    crossterm::event::Event::Resize(_, _) => app_event_tx.send(AppEvent::RequestRedraw),
    _ => {}
}