use std::io::{self, Write};
use crossterm::{
cursor,
style::{self, Stylize},
terminal, QueueableCommand,
};
use crate::{
tui::{
app::App,
utils::{strip_ansi, truncate_ansi_str, truncate_str},
},
version::VERSION,
};
pub fn render_footer(stdout: &mut io::Stdout, app: &App) -> io::Result<()> {
let (term_width, term_height) = {
let size = app.terminal_size.read().unwrap();
(size.width, size.height)
};
let fh = app.footer_height;
stdout.queue(cursor::Hide)?;
let line1_y = term_height.saturating_sub(fh);
stdout.queue(cursor::MoveTo(0, line1_y))?;
stdout.queue(style::SetBackgroundColor(style::Color::Black))?;
let spinner_chars = vec!['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
let spinner = if app.current_task.is_some() || app.awaiting_approval {
spinner_chars[app.spinner_frame % spinner_chars.len()]
.to_string()
.yellow()
.to_string()
} else {
"✨".to_string()
};
let status = if app.awaiting_approval {
if app.is_path_traversal_warning {
" ⚠️ AWAITING APPROVAL (y/n) ".red().to_string()
} else {
" ⚠️ AWAITING APPROVAL (y/n/a) ".red().to_string()
}
} else if let Some(task) = &app.current_task {
let elapsed = app
.job_start_time
.map(|s| format!(" ({:.1}s)", s.elapsed().as_secs_f32()))
.unwrap_or_default();
format!(" {}...{} ", task, elapsed).blue().to_string()
} else {
format!(" {} ", app.model).magenta().to_string()
};
let line1 = format!("v{} {}{}", VERSION, spinner, status);
stdout.queue(style::Print(line1))?;
stdout.queue(style::SetBackgroundColor(style::Color::Black))?;
stdout.queue(terminal::Clear(terminal::ClearType::UntilNewLine))?;
stdout.queue(cursor::MoveTo(0, term_height.saturating_sub(fh - 1)))?;
stdout.queue(style::SetBackgroundColor(style::Color::Black))?;
let total_tokens = app.total_tokens();
let token_str = if total_tokens > 0 {
format!(
" | 📊 {} prompt · {} comp · {} total",
app.token_usage.prompt_tokens, app.token_usage.completion_tokens, total_tokens
)
} else {
String::new()
};
let cwd_visible = format!("📂 {} ", app.cwd);
let token_visible_len = strip_ansi(&token_str).chars().count();
let cwd_visible_len = cwd_visible.chars().count();
let max_cwd_len = (term_width as usize).saturating_sub(token_visible_len + 2);
let cwd_display = if cwd_visible_len > max_cwd_len && max_cwd_len > 3 {
format!(
"📂 ...{} ",
&app.cwd[app.cwd.len().saturating_sub(max_cwd_len - 6)..]
)
} else {
cwd_visible
};
let line2 = format!("{}{}", cwd_display.blue(), token_str.dim());
stdout.queue(style::Print(line2))?;
stdout.queue(style::SetBackgroundColor(style::Color::Black))?;
stdout.queue(terminal::Clear(terminal::ClearType::UntilNewLine))?;
let line3_y = term_height.saturating_sub(2);
stdout.queue(cursor::MoveTo(0, line3_y))?;
stdout.queue(style::SetBackgroundColor(style::Color::Black))?;
let prompt = "> ";
let avail = (term_width as usize).saturating_sub(3); let input_display = if app.input.chars().count() <= avail || avail == 0 {
app.input.clone()
} else {
let skip = app.input.chars().count().saturating_sub(avail);
app.input.chars().skip(skip).collect()
};
let line3 = format!("{}{}", prompt.cyan(), input_display);
stdout.queue(style::Print(line3))?;
stdout.queue(style::SetBackgroundColor(style::Color::Black))?;
stdout.queue(terminal::Clear(terminal::ClearType::UntilNewLine))?;
let visible_input_chars = app.input.chars().count();
let visible_offset = if visible_input_chars > avail && avail > 0 {
visible_input_chars.saturating_sub(avail)
} else {
0
};
let cursor_byte_pos = app.cursor_pos.min(app.input.len());
let safe_cursor_pos = if app.input.is_char_boundary(cursor_byte_pos) {
cursor_byte_pos
} else {
let mut p = cursor_byte_pos;
while p > 0 && !app.input.is_char_boundary(p) {
p -= 1;
}
p
};
let cursor_char = app.input[..safe_cursor_pos].chars().count();
let cursor_x = 2 + ((cursor_char.saturating_sub(visible_offset)) as u16);
let line4_y = term_height.saturating_sub(1);
stdout.queue(cursor::MoveTo(0, line4_y))?;
stdout.queue(style::SetBackgroundColor(style::Color::Black))?;
if !app.queued_commands.is_empty() {
let mut parts: Vec<String> = Vec::new();
let separator = " ";
let max_entries = (term_width as usize / 15).max(1);
for i in 0..app.queued_commands.len().min(max_entries) {
if i > 0 {
parts.push(separator.to_string());
}
let cmd = &app.queued_commands[i];
let prefix = if i == 0 && app.current_task.is_some() {
format!("▶ q{}:", i + 1)
} else if i == 0 {
format!("✓ q{}:", i + 1)
} else {
format!("q{}:", i + 1)
};
let prefix_len = prefix.chars().count();
let cmd_max = 30usize.saturating_sub(prefix_len);
let truncated_cmd = truncate_str(cmd, cmd_max);
let entry: String = if i == 0 && app.current_task.is_some() {
format!("{}{}", prefix.green(), truncated_cmd)
} else if i == 0 {
format!("{}{}", prefix.dim(), truncated_cmd.dim())
} else {
format!("{}{}", prefix.yellow(), truncated_cmd.dim())
};
parts.push(entry);
}
let queue_line = parts.join("");
let truncated = truncate_ansi_str(&queue_line, term_width as usize);
stdout.queue(style::Print(truncated))?;
}
stdout.queue(terminal::Clear(terminal::ClearType::UntilNewLine))?;
stdout.queue(style::SetBackgroundColor(style::Color::Reset))?;
stdout.queue(style::ResetColor)?;
stdout.queue(cursor::MoveTo(cursor_x, line3_y))?;
stdout.queue(cursor::Show)?;
stdout.flush()?;
Ok(())
}
pub fn write_to_output(stdout: &mut io::Stdout, app: &mut App, text: String) -> io::Result<()> {
write_to_output_inner(stdout, app, &text, true)
}
pub fn write_to_output_inner(
stdout: &mut io::Stdout,
app: &mut App,
text: &str,
record: bool,
) -> io::Result<()> {
if record {
app.output_buffer.push_str(text);
}
let (term_width, term_height) = {
let size = app.terminal_size.read().unwrap();
(size.width, size.height)
};
let log_height = term_height.saturating_sub(app.footer_height);
let max_cols = term_width;
stdout.queue(cursor::MoveTo(app.log_x, app.log_y))?;
let chars: Vec<char> = text.chars().collect();
let mut i = 0;
let mut buffer = String::new();
while i < chars.len() {
if chars[i] == '\x1b' && i + 1 < chars.len() && chars[i + 1] == '[' {
if !buffer.is_empty() {
stdout.queue(style::Print(&buffer))?;
buffer.clear();
}
let mut seq = String::new();
seq.push('\x1b');
seq.push('[');
i += 2;
while i < chars.len() {
let c = chars[i];
seq.push(c);
i += 1;
if (c as u32) >= 64 && (c as u32) <= 126 {
break;
}
}
stdout.queue(style::Print(seq))?;
} else if chars[i] == '\n' {
if !buffer.is_empty() {
stdout.queue(style::Print(&buffer))?;
buffer.clear();
}
stdout.queue(style::Print("\r\n"))?;
app.log_x = 0;
if app.log_y < log_height.saturating_sub(1) {
app.log_y += 1;
}
i += 1;
} else if chars[i] == '\r' {
if !buffer.is_empty() {
stdout.queue(style::Print(&buffer))?;
buffer.clear();
}
app.log_x = 0;
i += 1;
} else {
if app.log_x >= max_cols {
if !buffer.is_empty() {
stdout.queue(style::Print(&buffer))?;
buffer.clear();
}
stdout.queue(style::Print("\r\n"))?;
app.log_x = 0;
if app.log_y < log_height.saturating_sub(1) {
app.log_y += 1;
}
}
buffer.push(chars[i]);
app.log_x += 1;
i += 1;
}
}
if !buffer.is_empty() {
stdout.queue(style::Print(buffer))?;
}
stdout.flush()?;
Ok(())
}