use std::io::{self, Write as _};
use crossterm::{
cursor::MoveTo,
event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
queue,
style::{Color as CtColor, Print, ResetColor, SetForegroundColor},
terminal::{self, Clear, ClearType},
};
use newt_core::agentic::NEWT_ORANGE_CT;
use crate::logo_for_size;
pub enum SetupEvent {
Step(String),
Progress { done: u64, total: Option<u64> },
Done,
Failed(String),
}
pub struct SetupHandle {
pub what: String,
pub rx: std::sync::mpsc::Receiver<SetupEvent>,
pub cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
}
fn is_abort_key(ev: &Event) -> bool {
match ev {
Event::Key(KeyEvent {
code: KeyCode::Esc, ..
}) => true,
Event::Key(KeyEvent {
code: KeyCode::Char('c'),
modifiers,
..
}) => modifiers.contains(KeyModifiers::CONTROL),
_ => false,
}
}
fn setup_status_line(what: &str, step: &str, done: u64, total: Option<u64>) -> String {
let mb = |b: u64| b / 1_048_576;
match total.filter(|&t| t > 0) {
Some(t) => format!(
"setting up {what} — {step} {}/{} MB {}%",
mb(done),
mb(t),
done.saturating_mul(100) / t
),
None => format!("setting up {what} — {step} {} MB", mb(done)),
}
}
fn drain_setup(
rx: &std::sync::mpsc::Receiver<SetupEvent>,
step: &mut String,
done: &mut u64,
total: &mut Option<u64>,
) -> Option<Result<(), String>> {
use std::sync::mpsc::TryRecvError;
loop {
match rx.try_recv() {
Ok(SetupEvent::Step(s)) => {
*step = s;
*done = 0;
*total = None;
}
Ok(SetupEvent::Progress { done: d, total: t }) => {
*done = d;
*total = t;
}
Ok(SetupEvent::Done) => return Some(Ok(())),
Ok(SetupEvent::Failed(e)) => return Some(Err(e)),
Err(TryRecvError::Empty) => return None,
Err(TryRecvError::Disconnected) => return Some(Err("interrupted".into())),
}
}
}
pub(crate) fn run_setup_screen(
out: &mut io::Stdout,
color: bool,
setup: SetupHandle,
) -> anyhow::Result<()> {
use std::sync::atomic::Ordering;
let (cols, rows) = terminal::size().unwrap_or((80, 24));
let (logo, _logo_cols) = logo_for_size(cols, rows);
let logo_rows = logo.lines().count() as u16;
let mut frame = 0usize;
let mut aborts = 0u8;
let mut step = "starting".to_string();
let (mut done, mut total) = (0u64, None);
loop {
let finished = drain_setup(&setup.rx, &mut step, &mut done, &mut total);
queue!(out, Clear(ClearType::All), MoveTo(0, 0))?;
write!(out, "{}", logo.replace('\n', "\r\n"))?;
let row = logo_rows + 1;
let (glyph, line, hint) = match &finished {
None => (
newt_core::tty::SPINNER_FRAMES[frame % newt_core::tty::SPINNER_FRAMES.len()]
.to_string(),
setup_status_line(&setup.what, &step, done, total),
"triple-Esc to skip (uses the session model instead)",
),
Some(Ok(())) => (
"✓".to_string(),
format!("ready — {} set up", setup.what),
"",
),
Some(Err(e)) => (
"⚠".to_string(),
format!("setup skipped ({e}) — will use the session model"),
"",
),
};
queue!(out, MoveTo(2, row))?;
if color {
queue!(out, SetForegroundColor(NEWT_ORANGE_CT))?;
}
queue!(out, Print(format!("{glyph} {line}")), ResetColor)?;
if !hint.is_empty() {
queue!(
out,
MoveTo(2, row + 1),
SetForegroundColor(CtColor::DarkGrey),
Print(hint),
ResetColor
)?;
}
out.flush()?;
if finished.is_some() {
let _ = event::poll(std::time::Duration::from_millis(800))?;
while event::poll(std::time::Duration::from_millis(0))? {
let _ = event::read()?;
}
return Ok(());
}
if event::poll(std::time::Duration::from_millis(100))? {
if is_abort_key(&event::read()?) {
aborts += 1;
if aborts >= 3 {
setup.cancel.store(true, Ordering::SeqCst);
while event::poll(std::time::Duration::from_millis(0))? {
let _ = event::read()?;
}
return Ok(());
}
} else {
aborts = 0;
}
}
frame += 1;
}
}
pub(crate) fn run_setup_inline(setup: &SetupHandle) {
let mut frame = 0usize;
let mut step = "starting".to_string();
let (mut done, mut total) = (0u64, None);
loop {
match drain_setup(&setup.rx, &mut step, &mut done, &mut total) {
None => {
eprint!(
"\r {} {} ",
newt_core::tty::SPINNER_FRAMES[frame % newt_core::tty::SPINNER_FRAMES.len()],
setup_status_line(&setup.what, &step, done, total)
);
let _ = io::stderr().flush();
}
Some(r) => {
let msg = match r {
Ok(()) => format!("✓ {} ready", setup.what),
Err(e) => format!("⚠ setup skipped ({e}) — using the session model"),
};
eprintln!("\r {msg} ");
return;
}
}
std::thread::sleep(std::time::Duration::from_millis(100));
frame += 1;
}
}
#[cfg(test)]
mod setup_screen_tests {
use super::{drain_setup, is_abort_key, setup_status_line, SetupEvent};
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
const MB: u64 = 1_048_576;
#[test]
fn status_line_shows_pct_with_total_and_bare_mb_without() {
let known = setup_status_line("summarizer", "weights", 50 * MB, Some(100 * MB));
assert!(known.contains("50/100 MB"), "{known}");
assert!(known.contains("50%"), "{known}");
let unknown = setup_status_line("summarizer", "weights", 7 * MB, None);
assert!(
unknown.contains("7 MB") && !unknown.contains('%'),
"{unknown}"
);
}
#[test]
fn abort_key_is_esc_or_ctrl_c_only() {
assert!(is_abort_key(&Event::Key(KeyEvent::new(
KeyCode::Esc,
KeyModifiers::NONE
))));
assert!(is_abort_key(&Event::Key(KeyEvent::new(
KeyCode::Char('c'),
KeyModifiers::CONTROL
))));
assert!(!is_abort_key(&Event::Key(KeyEvent::new(
KeyCode::Char('c'),
KeyModifiers::NONE
))));
assert!(!is_abort_key(&Event::Key(KeyEvent::new(
KeyCode::Enter,
KeyModifiers::NONE
))));
}
#[test]
fn drain_folds_progress_and_reports_done() {
let (tx, rx) = std::sync::mpsc::channel();
let (mut step, mut done, mut total) = (String::new(), 0u64, None);
assert!(drain_setup(&rx, &mut step, &mut done, &mut total).is_none());
tx.send(SetupEvent::Step("weights".into())).unwrap();
tx.send(SetupEvent::Progress {
done: 42,
total: Some(100),
})
.unwrap();
assert!(drain_setup(&rx, &mut step, &mut done, &mut total).is_none());
assert_eq!((step.as_str(), done, total), ("weights", 42, Some(100)));
tx.send(SetupEvent::Done).unwrap();
assert!(matches!(
drain_setup(&rx, &mut step, &mut done, &mut total),
Some(Ok(()))
));
}
#[test]
fn drain_reports_sender_death_as_error() {
let (tx, rx) = std::sync::mpsc::channel::<SetupEvent>();
drop(tx);
let (mut step, mut done, mut total) = (String::new(), 0u64, None);
assert!(matches!(
drain_setup(&rx, &mut step, &mut done, &mut total),
Some(Err(_))
));
}
}