use std::{
io::{self},
sync::Arc,
};
use anyhow::Result;
use crossterm::{
cursor,
event::{self, DisableBracketedPaste, EnableBracketedPaste},
execute,
style::{self, Stylize},
terminal::{self, disable_raw_mode, enable_raw_mode},
};
use tokio::sync::{mpsc, Mutex};
use crate::{
agent::{agent::DeepSeekAgent, types::ApprovalResult},
tui::{
app::App,
colorizer::StreamColorizer,
render::{render_footer, write_to_output, write_to_output_inner},
},
};
pub enum TuiEvent {
Input(event::KeyEvent),
Mouse(event::MouseEvent),
Paste(String),
Resize(u16, u16),
Tick,
Agent(crate::agent::types::AgentEvent),
Abort,
}
#[cfg(windows)]
extern "system" {
fn GetConsoleCP() -> u32;
fn GetConsoleOutputCP() -> u32;
fn SetConsoleCP(wCodePageID: u32) -> i32;
fn SetConsoleOutputCP(wCodePageID: u32) -> i32;
}
struct TerminalGuard {
#[cfg(windows)]
orig_cp: Option<(u32, u32)>,
}
impl TerminalGuard {
fn new() -> io::Result<Self> {
enable_raw_mode()?;
#[cfg(windows)]
let orig_cp = unsafe {
let cp = GetConsoleCP();
let ocp = GetConsoleOutputCP();
if SetConsoleCP(65001) != 0 && SetConsoleOutputCP(65001) != 0 {
Some((cp, ocp))
} else {
None
}
};
Ok(Self {
#[cfg(windows)]
orig_cp,
})
}
}
impl Drop for TerminalGuard {
fn drop(&mut self) {
let _ = disable_raw_mode();
let mut stdout = io::stdout();
let _ = execute!(
stdout,
style::Print("\x1b[r"), terminal::Clear(terminal::ClearType::All),
cursor::MoveTo(0, 0),
DisableBracketedPaste,
cursor::Show,
);
#[cfg(windows)]
if let Some((cp, ocp)) = self.orig_cp {
unsafe {
let _ = SetConsoleCP(cp);
let _ = SetConsoleOutputCP(ocp);
}
}
}
}
pub struct EventLoop {
pub rx: mpsc::Receiver<TuiEvent>,
pub app_tx: mpsc::Sender<ApprovalResult>,
pub cmd_tx: mpsc::Sender<(usize, String)>,
pub agent: Arc<Mutex<DeepSeekAgent>>,
pub cancel_token: Arc<std::sync::Mutex<tokio_util::sync::CancellationToken>>,
pub run_id: Arc<std::sync::atomic::AtomicUsize>,
}
impl EventLoop {
pub fn new(
rx: mpsc::Receiver<TuiEvent>,
_rx_tx: mpsc::Sender<TuiEvent>,
app_tx: mpsc::Sender<ApprovalResult>,
cmd_tx: mpsc::Sender<(usize, String)>,
agent: Arc<Mutex<DeepSeekAgent>>,
cancel_token: Arc<std::sync::Mutex<tokio_util::sync::CancellationToken>>,
run_id: Arc<std::sync::atomic::AtomicUsize>,
) -> Self {
Self {
rx,
app_tx,
cmd_tx,
agent,
cancel_token,
run_id,
}
}
fn handle_abort(&self, app: &mut App, stdout: &mut io::Stdout) -> Result<()> {
if app.queued_commands.is_empty() {
return Ok(());
}
if let Ok(token) = self.cancel_token.lock() {
token.cancel();
} else {
tracing::warn!("Cancel token mutex poisoned during abort");
}
self.run_id
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
app.aborted = true;
app.current_task = None;
app.task_start_time = None;
app.job_start_time = None;
app.awaiting_approval = false;
app.queued_commands.clear();
write_to_output(stdout, app, "🛑 Operation aborted by user.\n".to_string())?;
Ok(())
}
pub async fn run(mut self) -> Result<String> {
let mut full_message = String::new();
let mut app = App::new();
let mut reasoning_colorizer = StreamColorizer::new();
reasoning_colorizer.set_dimmed(true);
let mut content_colorizer = StreamColorizer::new();
{
if let Ok(agent) = self.agent.try_lock() {
app.model = agent.model.clone();
app.token_usage = agent.token_usage.clone();
}
}
let _guard = TerminalGuard::new()?;
let mut stdout = io::stdout();
execute!(stdout, EnableBracketedPaste)?;
let (term_width, term_height) = terminal::size().unwrap_or((80, 24));
let log_height = term_height.saturating_sub(app.footer_height);
execute!(
stdout,
terminal::Clear(terminal::ClearType::All),
style::Print(format!("\x1b[1;{}r", log_height)),
cursor::MoveTo(0, 0),
)?;
let logo_lines = vec![
format!(
" {} {} {}",
"██████╗ ".cyan().bold(),
" ██████╗".magenta().bold(),
"DeepSeek CLI Agent".cyan().bold()
),
format!(
" {} {} {}",
"██╔══██╗".cyan().bold(),
" ██╔════╝".magenta().bold(),
"Autonomous Terminal System".dim()
),
format!(
" {} {} {}",
"██║ ██║".cyan().bold(),
" ██║ ".magenta().bold(),
format!("Version {}", crate::version::VERSION).dim()
),
format!(
" {} {} {}",
"██║ ██║".cyan().bold(),
" ██║ ".magenta().bold(),
"Status: Ready".dim()
),
format!(
" {} {} {}",
"██████╔╝".cyan().bold(),
" ╚██████╗".magenta().bold(),
"Type /help for command list".dim()
),
format!(
" {} {} {}",
"╚═════╝ ".cyan().bold(),
" ╚═════╝".magenta().bold(),
""
),
];
for line in logo_lines {
write_to_output(&mut stdout, &mut app, format!("{}\n", line))?;
}
write_to_output(&mut stdout, &mut app, "\n".to_string())?;
let mut last_size = (term_width, term_height);
render_footer(&mut stdout, &app)?;
while let Some(event) = self.rx.recv().await {
match event {
TuiEvent::Abort => {
if !app.queued_commands.is_empty() {
self.handle_abort(&mut app, &mut stdout)?;
}
}
TuiEvent::Paste(text) => {
if !text.is_empty() {
let normalized = text.replace("\r\n", "\n").replace('\r', "\n");
let byte_pos = app.cursor_pos.min(app.input.len());
app.input.insert_str(byte_pos, &normalized);
app.cursor_pos = byte_pos + normalized.len();
}
}
TuiEvent::Mouse(_) => {}
TuiEvent::Input(key) => {
if self.handle_input(&mut app, &mut stdout, key)? {
break;
}
}
TuiEvent::Agent(agent_event) => {
self.handle_agent_event(
&mut app,
&mut stdout,
agent_event,
&mut full_message,
&mut reasoning_colorizer,
&mut content_colorizer,
)?;
}
TuiEvent::Resize(w, h) => {
if (w, h) != last_size {
if let Ok(mut size) = app.terminal_size.write() {
size.width = w;
size.height = h;
}
last_size = (w, h);
execute!(stdout, terminal::Clear(terminal::ClearType::All))?;
let log_height = h.saturating_sub(app.footer_height);
execute!(
stdout,
style::Print(format!("\x1b[1;{}r", log_height)),
cursor::MoveTo(0, 0)
)?;
app.log_x = 0;
app.log_y = 0;
let history = app.output_buffer.clone();
write_to_output_inner(&mut stdout, &mut app, &history, false)?;
}
}
TuiEvent::Tick => {
app.tick();
if let Ok(p) = std::env::current_dir() {
app.cwd = p.display().to_string();
}
let (w, h) = terminal::size().unwrap_or((80, 24));
if (w, h) != last_size {
if let Ok(mut size) = app.terminal_size.write() {
size.width = w;
size.height = h;
}
last_size = (w, h);
execute!(stdout, terminal::Clear(terminal::ClearType::All))?;
let log_height = h.saturating_sub(app.footer_height);
execute!(
stdout,
style::Print(format!("\x1b[1;{}r", log_height)),
cursor::MoveTo(0, 0)
)?;
app.log_x = 0;
app.log_y = 0;
let history = app.output_buffer.clone();
write_to_output_inner(&mut stdout, &mut app, &history, false)?;
}
}
}
render_footer(&mut stdout, &app)?;
}
let (_, _h) = terminal::size().unwrap_or((80, 24));
execute!(
stdout,
style::Print("\x1b[r"), terminal::Clear(terminal::ClearType::All),
cursor::MoveTo(0, 0),
DisableBracketedPaste,
)?;
disable_raw_mode()?;
println!();
Ok(full_message)
}
}