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

3.8 KiB
Raw Blame History

DOs

  • Prefer per-instance state: Pass terminal capability into UI components; avoid global mutables.
struct TuiState { kkp_enabled: bool }

impl TuiState {
    fn new(kkp_enabled: bool) -> Self { Self { kkp_enabled } }
}

let state = TuiState::new(detect_kkp()?);
let composer = ChatComposer::with_state(true, sender.clone(), state);
  • Initialize once, then freeze: If caching capability globally, set it once with OnceLock and never mutate.
static KKP_ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();

fn init_capabilities() {
    let kkp = detect_kkp().unwrap_or(false);
    let _ = KKP_ENABLED.set(kkp);
}

fn is_kkp_enabled() -> bool {
    *KKP_ENABLED.get().unwrap_or(&false)
}
  • Use dependency injection in tests: Make tests deterministic by supplying the capability explicitly.
let state_shift = TuiState::new(true);
let state_ctrlj = TuiState::new(false);

let composer_shift = ChatComposer::with_state(true, sender.clone(), state_shift);
let composer_ctrlj = ChatComposer::with_state(true, sender.clone(), state_ctrlj);
  • Detect features with portable APIs: Prefer crossterm polling and short timeouts; guard OS-specific code.
#[cfg(unix)]
fn detect_kkp() -> std::io::Result<bool> {
    use std::{io::{self, Write}, time::Duration};
    use crossterm::{event, ExecutableCommand};
    io::stdout().execute(crossterm::style::Print("\x1b[?u\x1b[c"))?;
    io::stdout().flush()?;
    if event::poll(Duration::from_millis(150))? {
        // Read raw bytes or translate from crossterm events here.
        return Ok(true); // Parse sequences as needed.
    }
    Ok(false)
}

#[cfg(not(unix))]
fn detect_kkp() -> std::io::Result<bool> { Ok(false) }
  • Reflect capability in hints: Compute the hint once and style with Stylize.
use ratatui::style::Stylize;

let newline_hint = if state.kkp_enabled { "Shift+⏎" } else { "Ctrl+J" };
let line = vec!["⏎".into(), " send   ".into(), newline_hint.cyan(), " newline".into()];
  • Keep snapshots stable: Capture both capability states explicitly and name snapshots clearly.
assert_snapshot!("composer_shift_enter", render(&composer_shift));
assert_snapshot!("composer_ctrl_j", render(&composer_ctrlj));
  • Inline variables in format!: Use braces for values in messages and errors.
return Err(anyhow::anyhow!("Failed to draw composer: {e}"));

DONTs

  • Dont mutate global flags in tests: Avoid static AtomicBool toggled per test; it invites race conditions and flaky snapshots.
/* Bad */
static KKP_ENABLED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
pub fn set_kkp_for_tests(v: bool) { KKP_ENABLED.store(v, std::sync::atomic::Ordering::Relaxed); }
  • Dont block initialization: No long or unbounded waits for terminal responses; keep detection fast or skip.
/* Bad */
std::thread::sleep(std::time::Duration::from_secs(2)); // blocks TUI startup
  • Dont assume KKP is present: Always provide a fallback accelerator and hint when KKP is missing.
/* Bad */
let newline_hint = "Shift+⏎"; // hardcoded, no fallback
  • Dont rely on unguarded OS-specific APIs: Avoid unconditional libc calls; use #[cfg] guards or portable crates.
/* Bad */
let rc = unsafe { libc::poll(pfd, 1, 200) }; // no cfg guard, non-portable
  • Dont couple tests to ambient environment: Tests should not depend on terminal/TTY; inject capability instead of probing.
/* Bad */
let kkp = detect_kkp().unwrap_or(false); // runs in CI, may flake
  • Dont share mutable test state across snapshots: If global state is unavoidable, serialize tests; better yet, remove the shared state.
/* Acceptable fallback when refactor isnt possible */
#[serial_test::serial]
#[test]
fn snapshots_with_global_state() { /* ... */ }