use std::io::{self, Write};
use std::sync::mpsc;
use std::time::Duration;
const RESET: &str = "\x1b[0m";
const BOLD: &str = "\x1b[1m";
const DIM: &str = "\x1b[2m";
const GREEN: &str = "\x1b[38;2;95;215;135m";
const CYAN: &str = "\x1b[38;2;100;210;230m";
const YELLOW: &str = "\x1b[38;2;230;190;80m";
const GRAY: &str = "\x1b[38;2;110;110;120m";
const WHITE: &str = "\x1b[38;2;220;220;220m";
const SPINNER: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
const TICK_MS: u64 = 80;
const TAGLINES: &[&str] = &[
"ship code. not excuses.",
"your relentless coding partner",
"think it. plan it. ship it.",
"code without limits",
"move fast. break nothing.",
"from idea to diff in seconds",
"the agent that never sleeps",
"your codebase, amplified",
];
const TIPS: &[&str] = &[
"/architect splits planning from coding",
"/proceed executes a saved plan",
"/rewind rolls back file changes",
"Alt+Y cycles approval mode",
"Tab switches agent configs",
"Esc × 2 cancels a running agent",
"/resume loads a previous session",
"/hive spawns parallel agents",
"/clear resets conversation context",
"Shift+Enter for multi-line input",
"/save-plan stores a plan to disk",
"Double-Esc also works as cancel",
];
enum Msg {
Step(String),
Done,
}
#[derive(Clone)]
pub struct SplashSender(mpsc::SyncSender<Msg>);
impl SplashSender {
pub fn step(&self, label: &str) {
let _ = self.0.try_send(Msg::Step(label.to_string()));
}
}
pub struct SplashScreen {
tx: mpsc::SyncSender<Msg>,
thread: Option<std::thread::JoinHandle<()>>,
}
impl SplashScreen {
pub fn start() -> (Self, SplashSender) {
let (tx, rx) = mpsc::sync_channel::<Msg>(64);
let sender = SplashSender(tx.clone());
let thread = std::thread::spawn(move || render_loop(rx));
(
Self {
tx,
thread: Some(thread),
},
sender,
)
}
pub fn finish(mut self) {
let _ = self.tx.try_send(Msg::Done);
if let Some(t) = self.thread.take() {
let _ = t.join();
}
}
}
impl Drop for SplashScreen {
fn drop(&mut self) {
let _ = self.tx.try_send(Msg::Done);
if let Some(t) = self.thread.take() {
let _ = t.join();
}
}
}
fn render_loop(rx: mpsc::Receiver<Msg>) {
let now_secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as usize)
.unwrap_or(0);
let tagline = TAGLINES[now_secs % TAGLINES.len()];
let tip = TIPS[(now_secs / 13) % TIPS.len()];
let term_width = term_width();
let box_width = term_width.clamp(52, 76);
print_header(box_width, tagline);
let _ = io::stdout().flush();
let mut completed: Vec<String> = Vec::new();
let mut current: Option<String> = None;
let mut frame: usize = 0;
let mut prev_lines: usize = 0;
loop {
let mut done = false;
loop {
match rx.try_recv() {
Ok(Msg::Step(s)) => {
if let Some(prev) = current.take() {
completed.push(prev);
}
current = Some(s);
}
Ok(Msg::Done) => {
done = true;
break;
}
Err(_) => break,
}
}
if prev_lines > 0 {
print!("\x1b[{}A", prev_lines);
}
if done {
if let Some(last) = current.take() {
completed.push(last);
}
prev_lines = render_steps(&completed, None, frame, box_width, tip);
let _ = io::stdout().flush();
std::thread::sleep(Duration::from_millis(120));
for _ in 0..prev_lines {
print!("\r\x1b[K\x1b[1A");
}
for _ in 0..HEADER_LINES {
print!("\r\x1b[K\x1b[1A");
}
print!("\r\x1b[K"); let _ = io::stdout().flush();
return;
}
prev_lines = render_steps(&completed, current.as_deref(), frame, box_width, tip);
let _ = io::stdout().flush();
match rx.recv_timeout(Duration::from_millis(TICK_MS)) {
Ok(Msg::Step(s)) => {
if let Some(prev) = current.take() {
completed.push(prev);
}
current = Some(s);
}
Ok(Msg::Done) => {
if let Some(last) = current.take() {
completed.push(last);
}
if prev_lines > 0 {
print!("\x1b[{}A", prev_lines);
}
prev_lines = render_steps(&completed, None, frame, box_width, tip);
let _ = io::stdout().flush();
std::thread::sleep(Duration::from_millis(120));
for _ in 0..prev_lines {
print!("\r\x1b[K\x1b[1A");
}
for _ in 0..HEADER_LINES {
print!("\r\x1b[K\x1b[1A");
}
print!("\r\x1b[K");
let _ = io::stdout().flush();
return;
}
Err(_) => {}
}
frame = frame.wrapping_add(1);
}
}
const HEADER_LINES: usize = 7;
fn print_header(box_width: usize, tagline: &str) {
let inner = box_width - 2;
let version = env!("CARGO_PKG_VERSION");
let top = format!("{CYAN}╭{}╮{RESET}", "─".repeat(inner));
let bot = format!("{CYAN}╰{}╯{RESET}", "─".repeat(inner));
let blank = format!("{CYAN}│{}{CYAN}│{RESET}", " ".repeat(inner));
let title_raw = format!(" ◆ collet v{version}");
let title_vis = visible_width(&title_raw);
let title_pad = inner.saturating_sub(title_vis + 1);
let title = format!(
"{CYAN}│{RESET} {BOLD}{CYAN} ◆ {WHITE}collet{RESET} {GRAY}v{version}{RESET}{}{CYAN}│{RESET}",
" ".repeat(title_pad)
);
let sub_raw = format!(" {tagline}");
let sub_vis = visible_width(&sub_raw);
let sub_pad = inner.saturating_sub(sub_vis + 1);
let subtitle = format!(
"{CYAN}│{RESET} {DIM}{WHITE}{sub_raw}{RESET}{}{CYAN}│{RESET}",
" ".repeat(sub_pad)
);
println!();
println!("{top}");
println!("{title}");
println!("{subtitle}");
println!("{blank}");
println!("{bot}");
println!();
}
fn render_steps(
completed: &[String],
current: Option<&str>,
frame: usize,
box_width: usize,
tip: &str,
) -> usize {
let mut lines = 0;
for step in completed {
let vis = visible_width(step);
let pad = box_width.saturating_sub(vis + 8);
println!(
"\r\x1b[K {GREEN}✓{RESET} {DIM}{step}{RESET}{}",
" ".repeat(pad)
);
lines += 1;
}
if let Some(step) = current {
let sp = SPINNER[(frame / 2) % SPINNER.len()];
let vis = visible_width(step);
let pad = box_width.saturating_sub(vis + 8);
println!("\r\x1b[K {YELLOW}{sp}{RESET} {step}{}", " ".repeat(pad));
lines += 1;
}
println!("\r\x1b[K");
println!("\r\x1b[K {GRAY}tip {tip}{RESET}");
lines += 2;
lines
}
fn term_width() -> usize {
crossterm::terminal::size()
.map(|(w, _)| w as usize)
.unwrap_or(80)
}
pub fn visible_width(s: &str) -> usize {
let mut width = 0;
let mut in_escape = false;
for ch in s.chars() {
if ch == '\x1b' {
in_escape = true;
} else if in_escape {
if ch.is_alphabetic() {
in_escape = false;
}
} else {
width += unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1);
}
}
width
}