1use std::{
2 io::{self},
3 sync::Arc,
4};
5
6use anyhow::Result;
7use crossterm::{
8 cursor,
9 event::{self, DisableBracketedPaste, EnableBracketedPaste},
10 execute,
11 style::{self, Stylize},
12 terminal::{self, disable_raw_mode, enable_raw_mode},
13};
14use tokio::sync::{mpsc, Mutex};
15
16use crate::{
17 agent::{agent::DeepSeekAgent, types::ApprovalResult},
18 tui::{
19 app::App,
20 colorizer::StreamColorizer,
21 render::{render_footer, write_to_output, write_to_output_inner},
22 },
23};
24
25pub enum TuiEvent {
26 Input(event::KeyEvent),
27 Mouse(event::MouseEvent),
28 Paste(String),
30 Resize(u16, u16),
31 Tick,
32 Agent(crate::agent::types::AgentEvent),
33 Abort,
34}
35
36#[cfg(windows)]
37extern "system" {
38 fn GetConsoleCP() -> u32;
39 fn GetConsoleOutputCP() -> u32;
40 fn SetConsoleCP(wCodePageID: u32) -> i32;
41 fn SetConsoleOutputCP(wCodePageID: u32) -> i32;
42}
43
44struct TerminalGuard {
45 #[cfg(windows)]
46 orig_cp: Option<(u32, u32)>,
47}
48
49impl TerminalGuard {
50 fn new() -> io::Result<Self> {
51 enable_raw_mode()?;
52
53 #[cfg(windows)]
54 let orig_cp = unsafe {
55 let cp = GetConsoleCP();
56 let ocp = GetConsoleOutputCP();
57 if SetConsoleCP(65001) != 0 && SetConsoleOutputCP(65001) != 0 {
58 Some((cp, ocp))
59 } else {
60 None
61 }
62 };
63
64 Ok(Self {
65 #[cfg(windows)]
66 orig_cp,
67 })
68 }
69}
70
71impl Drop for TerminalGuard {
72 fn drop(&mut self) {
73 let _ = disable_raw_mode();
74 let mut stdout = io::stdout();
75 let _ = execute!(
76 stdout,
77 style::Print("\x1b[r"), terminal::Clear(terminal::ClearType::All),
79 cursor::MoveTo(0, 0),
80 DisableBracketedPaste,
81 cursor::Show,
82 );
83
84 #[cfg(windows)]
85 if let Some((cp, ocp)) = self.orig_cp {
86 unsafe {
87 let _ = SetConsoleCP(cp);
88 let _ = SetConsoleOutputCP(ocp);
89 }
90 }
91 }
92}
93
94pub struct EventLoop {
95 pub rx: mpsc::Receiver<TuiEvent>,
96 pub app_tx: mpsc::Sender<ApprovalResult>,
97 pub cmd_tx: mpsc::Sender<(usize, String)>,
98 pub agent: Arc<Mutex<DeepSeekAgent>>,
99 pub cancel_token: Arc<std::sync::Mutex<tokio_util::sync::CancellationToken>>,
101 pub run_id: Arc<std::sync::atomic::AtomicUsize>,
102}
103
104impl EventLoop {
105 pub fn new(
106 rx: mpsc::Receiver<TuiEvent>,
107 _rx_tx: mpsc::Sender<TuiEvent>,
108 app_tx: mpsc::Sender<ApprovalResult>,
109 cmd_tx: mpsc::Sender<(usize, String)>,
110 agent: Arc<Mutex<DeepSeekAgent>>,
111 cancel_token: Arc<std::sync::Mutex<tokio_util::sync::CancellationToken>>,
112 run_id: Arc<std::sync::atomic::AtomicUsize>,
113 ) -> Self {
114 Self {
115 rx,
116 app_tx,
117 cmd_tx,
118 agent,
119 cancel_token,
120 run_id,
121 }
122 }
123
124 fn handle_abort(&self, app: &mut App, stdout: &mut io::Stdout) -> Result<()> {
125 if app.queued_commands.is_empty() {
126 return Ok(());
127 }
128 if let Ok(token) = self.cancel_token.lock() {
130 token.cancel();
131 } else {
132 tracing::warn!("Cancel token mutex poisoned during abort");
133 }
134 self.run_id
136 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
137 app.aborted = true;
139 app.current_task = None;
140 app.task_start_time = None;
141 app.job_start_time = None;
142 app.awaiting_approval = false;
143 app.queued_commands.clear();
144 write_to_output(stdout, app, "🛑 Operation aborted by user.\n".to_string())?;
145 Ok(())
146 }
147
148 pub async fn run(mut self) -> Result<String> {
149 let mut full_message = String::new();
150 let mut app = App::new();
151 let mut reasoning_colorizer = StreamColorizer::new();
152 reasoning_colorizer.set_dimmed(true);
153 let mut content_colorizer = StreamColorizer::new();
154
155 {
156 if let Ok(agent) = self.agent.try_lock() {
157 app.model = agent.model.clone();
158 app.token_usage = agent.token_usage.clone();
159 }
160 }
161
162 let _guard = TerminalGuard::new()?;
163 let mut stdout = io::stdout();
164 execute!(stdout, EnableBracketedPaste)?;
166
167 let (term_width, term_height) = terminal::size().unwrap_or((80, 24));
169 let log_height = term_height.saturating_sub(app.footer_height);
170 execute!(
171 stdout,
172 terminal::Clear(terminal::ClearType::All),
173 style::Print(format!("\x1b[1;{}r", log_height)),
175 cursor::MoveTo(0, 0),
176 )?;
177
178 let logo_lines = vec![
180 format!(
181 " {} {} {}",
182 "██████╗ ".cyan().bold(),
183 " ██████╗".magenta().bold(),
184 "DeepSeek CLI Agent".cyan().bold()
185 ),
186 format!(
187 " {} {} {}",
188 "██╔══██╗".cyan().bold(),
189 " ██╔════╝".magenta().bold(),
190 "Autonomous Terminal System".dim()
191 ),
192 format!(
193 " {} {} {}",
194 "██║ ██║".cyan().bold(),
195 " ██║ ".magenta().bold(),
196 format!("Version {}", crate::version::VERSION).dim()
197 ),
198 format!(
199 " {} {} {}",
200 "██║ ██║".cyan().bold(),
201 " ██║ ".magenta().bold(),
202 "Status: Ready".dim()
203 ),
204 format!(
205 " {} {} {}",
206 "██████╔╝".cyan().bold(),
207 " ╚██████╗".magenta().bold(),
208 "Type /help for command list".dim()
209 ),
210 format!(
211 " {} {} {}",
212 "╚═════╝ ".cyan().bold(),
213 " ╚═════╝".magenta().bold(),
214 ""
215 ),
216 ];
217
218 for line in logo_lines {
219 write_to_output(&mut stdout, &mut app, format!("{}\n", line))?;
220 }
221 write_to_output(&mut stdout, &mut app, "\n".to_string())?;
222
223 let mut last_size = (term_width, term_height);
224 render_footer(&mut stdout, &app)?;
225
226 while let Some(event) = self.rx.recv().await {
227 match event {
228 TuiEvent::Abort => {
229 if !app.queued_commands.is_empty() {
230 self.handle_abort(&mut app, &mut stdout)?;
231 }
232 }
233 TuiEvent::Paste(text) => {
234 if !text.is_empty() {
235 let normalized = text.replace("\r\n", "\n").replace('\r', "\n");
236 let byte_pos = app.cursor_pos.min(app.input.len());
237 app.input.insert_str(byte_pos, &normalized);
238 app.cursor_pos = byte_pos + normalized.len();
239 }
240 }
241 TuiEvent::Mouse(_) => {}
242 TuiEvent::Input(key) => {
243 if self.handle_input(&mut app, &mut stdout, key)? {
244 break;
245 }
246 }
247 TuiEvent::Agent(agent_event) => {
248 self.handle_agent_event(
249 &mut app,
250 &mut stdout,
251 agent_event,
252 &mut full_message,
253 &mut reasoning_colorizer,
254 &mut content_colorizer,
255 )?;
256 }
257 TuiEvent::Resize(w, h) => {
258 if (w, h) != last_size {
259 if let Ok(mut size) = app.terminal_size.write() {
260 size.width = w;
261 size.height = h;
262 }
263 last_size = (w, h);
264
265 execute!(stdout, terminal::Clear(terminal::ClearType::All))?;
266 let log_height = h.saturating_sub(app.footer_height);
267 execute!(
268 stdout,
269 style::Print(format!("\x1b[1;{}r", log_height)),
270 cursor::MoveTo(0, 0)
271 )?;
272 app.log_x = 0;
273 app.log_y = 0;
274 let history = app.output_buffer.clone();
275 write_to_output_inner(&mut stdout, &mut app, &history, false)?;
276 }
277 }
278 TuiEvent::Tick => {
279 app.tick();
280 if let Ok(p) = std::env::current_dir() {
281 app.cwd = p.display().to_string();
282 }
283 let (w, h) = terminal::size().unwrap_or((80, 24));
284 if (w, h) != last_size {
285 if let Ok(mut size) = app.terminal_size.write() {
286 size.width = w;
287 size.height = h;
288 }
289 last_size = (w, h);
290
291 execute!(stdout, terminal::Clear(terminal::ClearType::All))?;
292 let log_height = h.saturating_sub(app.footer_height);
293 execute!(
294 stdout,
295 style::Print(format!("\x1b[1;{}r", log_height)),
296 cursor::MoveTo(0, 0)
297 )?;
298 app.log_x = 0;
299 app.log_y = 0;
300 let history = app.output_buffer.clone();
301 write_to_output_inner(&mut stdout, &mut app, &history, false)?;
302 }
303 }
304 }
305 render_footer(&mut stdout, &app)?;
306 }
307
308 let (_, _h) = terminal::size().unwrap_or((80, 24));
310 execute!(
311 stdout,
312 style::Print("\x1b[r"), terminal::Clear(terminal::ClearType::All),
314 cursor::MoveTo(0, 0),
315 DisableBracketedPaste,
316 )?;
317
318 disable_raw_mode()?;
319 println!();
320 Ok(full_message)
321 }
322}