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},
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 mut last_size = (term_width, term_height);
178 let mut last_footer_height = app.footer_height; render_footer(&mut stdout, &app)?;
180
181 while let Some(event) = self.rx.recv().await {
182 match event {
183 TuiEvent::Abort => {
184 if !app.queued_commands.is_empty() {
185 self.handle_abort(&mut app, &mut stdout)?;
186 }
187 }
188 TuiEvent::Paste(text) => {
189 if !text.is_empty() {
190 let byte_pos = app.cursor_pos.min(app.input.len());
191 app.input.insert_str(byte_pos, &text);
192 app.cursor_pos = byte_pos + text.len();
193 }
194 }
195 TuiEvent::Mouse(_) => {}
196 TuiEvent::Input(key) => {
197 if self.handle_input(&mut app, &mut stdout, key)? {
198 break;
199 }
200 }
201 TuiEvent::Agent(agent_event) => {
202 self.handle_agent_event(
203 &mut app,
204 &mut stdout,
205 agent_event,
206 &mut full_message,
207 &mut reasoning_colorizer,
208 &mut content_colorizer,
209 )?;
210 }
211 TuiEvent::Tick => {
212 app.tick();
213 if let Ok(p) = std::env::current_dir() {
214 app.cwd = p.display().to_string();
215 }
216 let (w, h) = terminal::size().unwrap_or((80, 24));
217 if (w, h) != last_size {
218 last_size = (w, h);
219 last_footer_height = 0;
220 }
221 }
222 }
223 let new_fh = 4u16;
225 if new_fh != last_footer_height {
226 let (_w, h) = terminal::size().unwrap_or((80, 24));
227 let log_h = h.saturating_sub(new_fh);
228 execute!(stdout, style::Print(format!("\x1b[1;{}r", log_h)))?;
229 last_footer_height = new_fh;
230 if app.log_y >= log_h {
231 app.log_y = log_h.saturating_sub(1);
232 }
233 }
234 render_footer(&mut stdout, &app)?;
235 }
236
237 let (_, _h) = terminal::size().unwrap_or((80, 24));
239 execute!(
240 stdout,
241 style::Print("\x1b[r"), terminal::Clear(terminal::ClearType::All),
243 cursor::MoveTo(0, 0),
244 DisableBracketedPaste,
245 )?;
246
247 disable_raw_mode()?;
248 println!();
249 Ok(full_message)
250 }
251}