deepseek_rust_cli/tui/
render.rs1use std::io::{self, Write};
2
3use crossterm::{
4 cursor,
5 style::{self, Stylize},
6 terminal, QueueableCommand,
7};
8
9use crate::{
10 tui::{
11 app::App,
12 utils::{strip_ansi, truncate_ansi_str, truncate_str},
13 },
14 version::VERSION,
15};
16
17pub fn render_footer(stdout: &mut io::Stdout, app: &App) -> io::Result<()> {
18 let (term_width, term_height) = terminal::size().unwrap_or((80, 24));
19 let fh = app.footer_height; stdout.queue(cursor::Hide)?;
22
23 let line1_y = term_height.saturating_sub(fh);
25 stdout.queue(cursor::MoveTo(0, line1_y))?;
26 stdout.queue(style::SetBackgroundColor(style::Color::Black))?;
27
28 let spinner_chars = vec!['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
29 let spinner = if app.current_task.is_some() || app.awaiting_approval {
30 spinner_chars[app.spinner_frame % spinner_chars.len()]
31 .to_string()
32 .yellow()
33 .to_string()
34 } else {
35 "✨".to_string()
36 };
37
38 let status = if app.awaiting_approval {
39 if app.is_path_traversal_warning {
40 " ⚠️ AWAITING APPROVAL (y/n) ".red().to_string()
41 } else {
42 " ⚠️ AWAITING APPROVAL (y/n/a) ".red().to_string()
43 }
44 } else if let Some(task) = &app.current_task {
45 let elapsed = app
46 .job_start_time
47 .map(|s| format!(" ({:.1}s)", s.elapsed().as_secs_f32()))
48 .unwrap_or_default();
49 format!(" {}...{} ", task, elapsed).blue().to_string()
50 } else {
51 format!(" {} ", app.model).magenta().to_string()
52 };
53
54 let line1 = format!("v{} {}{}", VERSION, spinner, status);
55 stdout.queue(style::Print(line1))?;
56 stdout.queue(style::SetBackgroundColor(style::Color::Black))?;
57 stdout.queue(terminal::Clear(terminal::ClearType::UntilNewLine))?;
58
59 stdout.queue(cursor::MoveTo(0, term_height.saturating_sub(fh - 1)))?;
61 stdout.queue(style::SetBackgroundColor(style::Color::Black))?;
62
63 let total_tokens = app.total_tokens();
64 let token_str = if total_tokens > 0 {
65 format!(
66 " | 📊 {} prompt · {} comp · {} total",
67 app.token_usage.prompt_tokens, app.token_usage.completion_tokens, total_tokens
68 )
69 } else {
70 String::new()
71 };
72
73 let cwd_visible = format!("📂 {} ", app.cwd);
74 let token_visible_len = strip_ansi(&token_str).chars().count();
75 let cwd_visible_len = cwd_visible.chars().count();
76 let max_cwd_len = (term_width as usize).saturating_sub(token_visible_len + 2);
77
78 let cwd_display = if cwd_visible_len > max_cwd_len && max_cwd_len > 3 {
79 format!(
80 "📂 ...{} ",
81 &app.cwd[app.cwd.len().saturating_sub(max_cwd_len - 6)..]
82 )
83 } else {
84 cwd_visible
85 };
86
87 let line2 = format!("{}{}", cwd_display.blue(), token_str.dim());
88 stdout.queue(style::Print(line2))?;
89 stdout.queue(style::SetBackgroundColor(style::Color::Black))?;
90 stdout.queue(terminal::Clear(terminal::ClearType::UntilNewLine))?;
91
92 let line3_y = term_height.saturating_sub(2);
94 stdout.queue(cursor::MoveTo(0, line3_y))?;
95 stdout.queue(style::SetBackgroundColor(style::Color::Black))?;
96
97 let prompt = "> ";
98 let avail = (term_width as usize).saturating_sub(3); let input_display = if app.input.chars().count() <= avail || avail == 0 {
100 app.input.clone()
101 } else {
102 let skip = app.input.chars().count().saturating_sub(avail);
104 app.input.chars().skip(skip).collect()
105 };
106 let line3 = format!("{}{}", prompt.cyan(), input_display);
107 stdout.queue(style::Print(line3))?;
108 stdout.queue(style::SetBackgroundColor(style::Color::Black))?;
109 stdout.queue(terminal::Clear(terminal::ClearType::UntilNewLine))?;
110
111 let visible_input_chars = app.input.chars().count();
113 let visible_offset = if visible_input_chars > avail && avail > 0 {
114 visible_input_chars.saturating_sub(avail)
115 } else {
116 0
117 };
118 let cursor_byte_pos = app.cursor_pos.min(app.input.len());
119 let safe_cursor_pos = if app.input.is_char_boundary(cursor_byte_pos) {
120 cursor_byte_pos
121 } else {
122 let mut p = cursor_byte_pos;
123 while p > 0 && !app.input.is_char_boundary(p) {
124 p -= 1;
125 }
126 p
127 };
128 let cursor_char = app.input[..safe_cursor_pos].chars().count();
129 let cursor_x = 2 + ((cursor_char.saturating_sub(visible_offset)) as u16);
130
131 let line4_y = term_height.saturating_sub(1);
133 stdout.queue(cursor::MoveTo(0, line4_y))?;
134 stdout.queue(style::SetBackgroundColor(style::Color::Black))?;
135
136 if !app.queued_commands.is_empty() {
137 let mut parts: Vec<String> = Vec::new();
139 let separator = " ";
140
141 let max_entries = (term_width as usize / 15).max(1);
143
144 for i in 0..app.queued_commands.len().min(max_entries) {
145 if i > 0 {
146 parts.push(separator.to_string());
147 }
148 let cmd = &app.queued_commands[i];
149 let prefix = if i == 0 && app.current_task.is_some() {
150 format!("▶ q{}:", i + 1)
151 } else if i == 0 {
152 format!("✓ q{}:", i + 1)
153 } else {
154 format!("q{}:", i + 1)
155 };
156 let prefix_len = prefix.chars().count();
157 let cmd_max = 30usize.saturating_sub(prefix_len);
158 let truncated_cmd = truncate_str(cmd, cmd_max);
159
160 let entry: String = if i == 0 && app.current_task.is_some() {
162 format!("{}{}", prefix.green(), truncated_cmd)
163 } else if i == 0 {
164 format!("{}{}", prefix.dim(), truncated_cmd.dim())
165 } else {
166 format!("{}{}", prefix.yellow(), truncated_cmd.dim())
167 };
168 parts.push(entry);
169 }
170
171 let queue_line = parts.join("");
172 let truncated = truncate_ansi_str(&queue_line, term_width as usize);
174 stdout.queue(style::Print(truncated))?;
175 }
176
177 stdout.queue(terminal::Clear(terminal::ClearType::UntilNewLine))?;
178
179 stdout.queue(style::SetBackgroundColor(style::Color::Reset))?;
181 stdout.queue(style::ResetColor)?;
182
183 stdout.queue(cursor::MoveTo(cursor_x, line3_y))?;
185 stdout.queue(cursor::Show)?;
186 stdout.flush()?;
187
188 Ok(())
189}
190
191pub fn write_to_output(stdout: &mut io::Stdout, app: &mut App, text: String) -> io::Result<()> {
192 let (term_width, term_height) = terminal::size().unwrap_or((80, 24));
193 let log_height = term_height.saturating_sub(app.footer_height);
194 let max_cols = term_width;
195
196 stdout.queue(cursor::MoveTo(app.log_x, app.log_y))?;
198
199 let chars: Vec<char> = text.chars().collect();
200 let mut i = 0;
201 let mut buffer = String::new();
202
203 while i < chars.len() {
204 if chars[i] == '\x1b' && i + 1 < chars.len() && chars[i + 1] == '[' {
205 if !buffer.is_empty() {
207 stdout.queue(style::Print(&buffer))?;
208 buffer.clear();
209 }
210
211 let mut seq = String::new();
212 seq.push('\x1b');
213 seq.push('[');
214 i += 2;
215 while i < chars.len() {
216 let c = chars[i];
217 seq.push(c);
218 i += 1;
219 if (c as u32) >= 64 && (c as u32) <= 126 {
220 break;
221 }
222 }
223 stdout.queue(style::Print(seq))?;
224 } else if chars[i] == '\n' {
225 if !buffer.is_empty() {
227 stdout.queue(style::Print(&buffer))?;
228 buffer.clear();
229 }
230
231 stdout.queue(style::Print("\r\n"))?;
232 app.log_x = 0;
233 if app.log_y < log_height.saturating_sub(1) {
234 app.log_y += 1;
235 }
236 i += 1;
237 } else if chars[i] == '\r' {
238 if !buffer.is_empty() {
240 stdout.queue(style::Print(&buffer))?;
241 buffer.clear();
242 }
243 app.log_x = 0;
244 i += 1;
245 } else {
246 if app.log_x >= max_cols {
247 if !buffer.is_empty() {
249 stdout.queue(style::Print(&buffer))?;
250 buffer.clear();
251 }
252 stdout.queue(style::Print("\r\n"))?;
253 app.log_x = 0;
254 if app.log_y < log_height.saturating_sub(1) {
255 app.log_y += 1;
256 }
257 }
258 buffer.push(chars[i]);
259 app.log_x += 1;
260 i += 1;
261 }
262 }
263
264 if !buffer.is_empty() {
265 stdout.queue(style::Print(buffer))?;
266 }
267
268 stdout.flush()?;
269 Ok(())
270}