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

6.1 KiB
Raw Blame History

DOs

  • Bold keyword: Integrate tokio EventStream for input; multiplex with app events using tokio::select!.
use crossterm::event::EventStream;
use tokio::select;
use tokio_stream::StreamExt;

let mut crossterm_events = EventStream::new();

while let Some(app_ev) = {
    select! {
        maybe = app_event_rx.recv() => maybe, // App events (our channel)
        Some(Ok(ev)) = crossterm_events.next() => match ev {
            crossterm::event::Event::Key(k) => Some(AppEvent::KeyEvent(k)),
            crossterm::event::Event::Resize(..) => Some(AppEvent::Redraw),
            crossterm::event::Event::Paste(p) => Some(AppEvent::Paste(p.replace("\r", "\n"))),
            _ => None,
        },
    }
} { handle_event(terminal, app_ev)?; }
  • Bold keyword: Use tokio::sync::mpsc::unbounded_channel for app events (and tests).
use tokio::sync::mpsc::{unbounded_channel, UnboundedSender, UnboundedReceiver};

let (tx, rx): (UnboundedSender<AppEvent>, UnboundedReceiver<AppEvent>) = unbounded_channel();

#[derive(Clone, Debug)]
pub(crate) struct AppEventSender { pub app_event_tx: UnboundedSender<AppEvent> }

impl AppEventSender {
    pub fn send(&self, ev: AppEvent) { let _ = self.app_event_tx.send(ev); }
}
  • Bold keyword: Factor event logic into a reusable handler that returns “keep running”.
fn handle_event(&mut self, terminal: &mut tui::Tui, ev: AppEvent) -> Result<bool> {
    match ev {
        AppEvent::RequestRedraw => self.schedule_frame_in(REDRAW_DEBOUNCE),
        AppEvent::Redraw => std::io::stdout().sync_update(|_| self.draw_next_frame(terminal))??,
        AppEvent::ExitRequest | AppEvent::DispatchCommand(SlashCommand::Quit) => return Ok(false),
        _ => { /* other cases... */ }
    }
    Ok(true)
}
  • Bold keyword: Normalize pasted text to LF; many terminals paste CR.
match event {
    crossterm::event::Event::Paste(p) => Some(AppEvent::Paste(p.replace("\r", "\n"))),
    _ => None,
}
  • Bold keyword: Debounce redraws to coalesce frames.
const REDRAW_DEBOUNCE: Duration = Duration::from_millis(1);

self.app_event_tx.send(AppEvent::RequestRedraw);
// ...
AppEvent::RequestRedraw => self.schedule_frame_in(REDRAW_DEBOUNCE),
  • Bold keyword: Restore cursor using tracked coordinates, not a live query (avoids event-lock issues).
use crossterm::cursor::MoveTo;
use crossterm::queue;

queue!(
    writer,
    MoveTo(terminal.last_known_cursor_pos.x, terminal.last_known_cursor_pos.y)
).ok();
  • Bold keyword: Keep key semantics consistent and predictable.
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};

match key_event {
    KeyEvent { code: KeyCode::Char('c'), modifiers: KeyModifiers::CONTROL, kind: KeyEventKind::Press, .. } => {
        self.app_event_tx.send(AppEvent::ExitRequest);
    }
    KeyEvent { code: KeyCode::Char('d'), modifiers: KeyModifiers::CONTROL, kind: KeyEventKind::Press, .. } => {
        if widget.composer_is_empty() { self.app_event_tx.send(AppEvent::ExitRequest); }
        else { self.dispatch_key_event(key_event); }
    }
    KeyEvent { kind: KeyEventKind::Press | KeyEventKind::Repeat, .. } => self.dispatch_key_event(key_event),
    _ => {} // Ignore KeyEventKind::Release
}
  • Bold keyword: Make entry points async and await the run loop.
pub async fn run_main(cli: Cli, config: Config, show_trust: bool) -> std::io::Result<()> {
    run_ratatui_app(cli, config, show_trust).await
        .map_err(|e| std::io::Error::other(e.to_string()))
}

async fn run_ratatui_app(cli: Cli, config: Config, show_trust: bool) -> eyre::Result<()> {
    let mut app = App::new(config.clone(), cli.prompt, cli.images, show_trust);
    app.run(&mut terminal).await?;
    Ok(())
}
  • Bold keyword: Explain non-obvious changes that are related but not obvious (e.g., cursor restore).
PR note: Switched to EventStream; querying cursor position can contend with crossterms event lock.
Use last_known_cursor_pos to restore cursor without locking; fixes resize-time failures.
  • Bold keyword: Map terminal resize → redraw, not a full rerender loop.
crossterm::event::Event::Resize(..) => Some(AppEvent::Redraw)

DONTs

  • Bold keyword: Dont block on crossterm::event::read() or hold the event lock.
// ❌ Blocking pattern (avoids async multiplexing and can deadlock):
if crossterm::event::poll(Duration::from_millis(100))? {
    let ev = crossterm::event::read()?; // holds event lock
}
  • Bold keyword: Dont use std::sync::mpsc in async code and tests.
// ❌
let (tx, rx) = std::sync::mpsc::channel::<AppEvent>();

// ✅
let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<AppEvent>();
  • Bold keyword: Dont call cursor::position() or terminal.get_cursor_position() during rendering.
// ❌ May contend with crossterms event lock:
let cursor_pos = terminal.get_cursor_position()?;

// ✅ Use tracked position:
MoveTo(terminal.last_known_cursor_pos.x, terminal.last_known_cursor_pos.y)
  • Bold keyword: Dont exit on Ctrl+D if the composer has content.
// ❌ Always exits:
if ctrl_d { return Ok(false); }

// ✅ Only exit when input is empty:
if ctrl_d && widget.composer_is_empty() { return Ok(false); }
  • Bold keyword: Dont handle KeyEventKind::Release; it causes duplicate actions.
match key_event.kind {
    KeyEventKind::Press | KeyEventKind::Repeat => self.dispatch_key_event(key_event),
    _ => {} // ignore Release
}
  • Bold keyword: Dont forget Cargo feature flags for crossterms event stream.
# ✅ Cargo.toml
crossterm = { version = "0.28.1", features = ["bracketed-paste", "event-stream"] }
tokio-stream = "0.1.17"
  • Bold keyword: Dont leave tests using try_iter() from std mpsc; use try_recv() in a loop.
// ✅ With tokio channel:
let mut events = Vec::new();
while let Ok(ev) = rx.try_recv() { events.push(ev); }
  • Bold keyword: Dont mix unrelated changes without context; add a brief rationale.
Commit message: “Use tracked cursor pos after EventStream refactor”
Rationale: Avoids event-lock contention introduced by async event handling.