use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use chrono::Datelike;
use compact_str::CompactString;
use crossterm::event;
use crossterm::event::{KeyEventKind, MouseButton, MouseEventKind};
use crossterm::style::Color;
use tokio::sync::mpsc;
use crate::cli::Cli;
use crate::config::Config;
use crate::context::ContextFiles;
use crate::event::UserEvent;
use crate::session::{MessageRole, Session};
use crate::ui::markdown;
use crate::ui::renderer::Renderer;
pub fn spawn_event_thread(
user_tx: mpsc::Sender<UserEvent>,
running: Arc<AtomicBool>,
) -> std::thread::JoinHandle<()> {
std::thread::spawn(move || {
while running.load(Ordering::Relaxed) {
if let Ok(true) = event::poll(Duration::from_millis(50)) {
match event::read() {
Ok(event::Event::Key(key)) => {
if key.kind == KeyEventKind::Press
&& user_tx.blocking_send(UserEvent::Key(key)).is_err()
{
break;
}
}
Ok(event::Event::Mouse(m)) => match m.kind {
MouseEventKind::ScrollUp => {
if user_tx.blocking_send(UserEvent::ScrollUp).is_err() {
break;
}
}
MouseEventKind::ScrollDown => {
if user_tx.blocking_send(UserEvent::ScrollDown).is_err() {
break;
}
}
MouseEventKind::Down(MouseButton::Left) => {
let _ = user_tx.blocking_send(UserEvent::MouseDown {
row: m.row,
col: m.column,
});
}
MouseEventKind::Drag(MouseButton::Left) => {
let _ = user_tx.blocking_send(UserEvent::MouseDrag {
row: m.row,
col: m.column,
});
}
MouseEventKind::Up(MouseButton::Left) => {
let _ = user_tx.blocking_send(UserEvent::MouseUp {
row: m.row,
col: m.column,
});
}
_ => {}
},
Ok(event::Event::Resize(_cols, _rows)) => {
let _ = user_tx.blocking_send(UserEvent::Resize);
}
Ok(event::Event::Paste(data)) => {
let _ = user_tx.blocking_send(UserEvent::Paste(data));
}
Err(_) => break,
_ => {}
}
}
}
})
}
pub fn format_time(rfc3339: &str) -> CompactString {
let dt = chrono::DateTime::parse_from_rfc3339(rfc3339).ok();
let dt = match dt {
Some(dt) => dt,
None => return CompactString::new(rfc3339),
};
let local = dt.with_timezone(&chrono::Local);
let now = chrono::Local::now();
if local.date_naive() == now.date_naive() {
CompactString::new(local.format("%H:%M").to_string())
} else if local.year() == now.year() {
CompactString::new(local.format("%b %d %H:%M").to_string())
} else {
CompactString::new(local.format("%Y-%m-%d %H:%M").to_string())
}
}
pub fn render_session(
renderer: &mut Renderer,
session: &Session,
cli: &Cli,
cfg: &Config,
context: &ContextFiles,
) -> anyhow::Result<()> {
renderer.clear_content()?;
let welcome = format!(
"zerostack {} {} {}",
cli.resolve_provider(cfg),
cli.resolve_model(cfg),
env!("CARGO_PKG_VERSION")
);
renderer.write_line(&welcome, Color::Cyan)?;
renderer.write_line("", Color::White)?;
if context.agents.is_some() {
renderer.write_line("loaded AGENTS.md", Color::DarkGrey)?;
renderer.write_line("", Color::White)?;
}
#[cfg(feature = "archmd")]
if context.architecture.is_some() {
renderer.write_line("loaded ARCHITECTURE.md", Color::DarkGrey)?;
renderer.write_line("", Color::White)?;
}
if !session.compactions.is_empty() {
renderer.write_line(
&format!(
"compacted {} times (saved ~{} tokens)",
session.compactions.len(),
session
.compactions
.last()
.map(|c| c.token_savings)
.unwrap_or(0),
),
Color::DarkGrey,
)?;
renderer.write_line("", Color::White)?;
}
for msg in &session.messages {
let (prefix, _c) = match msg.role {
MessageRole::User => (">", Color::Green),
MessageRole::Assistant => ("<", Color::White),
MessageRole::System => ("#", Color::DarkGrey),
};
if msg.role == MessageRole::Assistant {
let max_width = renderer.line_width();
let mut styled = markdown::markdown_to_styled(&msg.content, max_width);
if !styled.is_empty() {
styled[0].text = CompactString::from(format!("{} {}", prefix, styled[0].text));
}
for entry in styled {
renderer.write_line(&entry.text, entry.color)?;
}
} else {
for line in msg.content.lines() {
renderer.write_line(&format!("{} {}", prefix, line), _c)?;
}
}
renderer.write_line("", Color::White)?;
}
Ok(())
}
pub fn sanitize_output(text: &str) -> CompactString {
let mut result = String::with_capacity(text.len());
let mut chars = text.chars();
while let Some(c) = chars.next() {
if c == '\x1b' {
match chars.next() {
Some('[') | Some(']') => {
for next in &mut chars {
if next.is_ascii_alphabetic() || next == '~' {
break;
}
}
}
Some(_) => {}
None => break,
}
} else if c.is_ascii_control() && c != '\n' && c != '\t' && c != '\r' {
continue;
} else {
result.push(c);
}
}
CompactString::from(result)
}