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