Skip to main content

deepseek_rust_cli/tui/
app.rs

1use std::{sync::Arc, time::Instant};
2
3use crate::api::types::TokenUsage;
4
5#[derive(Debug, Clone, Copy)]
6pub struct TerminalSize {
7    pub width: u16,
8    pub height: u16,
9}
10
11pub fn load_global_history() -> Vec<String> {
12    let path = std::path::PathBuf::from(".deep/input_history.json");
13    if let Ok(content) = std::fs::read_to_string(path) {
14        if let Ok(history) = serde_json::from_str::<Vec<String>>(&content) {
15            return history;
16        }
17    }
18    Vec::new()
19}
20
21pub fn save_global_history(history: &[String]) {
22    let path = std::path::PathBuf::from(".deep/input_history.json");
23    if let Some(parent) = path.parent() {
24        let _ = std::fs::create_dir_all(parent);
25    }
26    if let Ok(json) = serde_json::to_string_pretty(history) {
27        let _ = std::fs::write(path, json);
28    }
29}
30
31pub struct App {
32    pub input: String,
33    /// Byte position of cursor within input (0 = start)
34    pub cursor_pos: usize,
35    pub awaiting_approval: bool,
36    pub spinner_frame: usize,
37    pub current_task: Option<String>,
38    /// When the current *task label* last changed (for display only)
39    pub task_start_time: Option<Instant>,
40    /// When the entire agent job started (never reset until finish_task)
41    pub job_start_time: Option<Instant>,
42    pub cwd: String,
43    pub model: String,
44    pub history: Vec<String>,
45    pub history_index: Option<usize>,
46    /// Footer is always 4 lines: status, folder+token, input, queue
47    pub footer_height: u16,
48    pub token_usage: TokenUsage,
49    /// When true, ignore incoming AgentEvents (after abort)
50    pub aborted: bool,
51    /// Pending commands that were sent but not yet completed.
52    /// Index 0 is the currently-running command; the rest are queued.
53    pub queued_commands: Vec<String>,
54    pub log_x: u16,
55    pub log_y: u16,
56    pub reasoning_started: bool,
57    pub content_started: bool,
58    pub is_path_traversal_warning: bool,
59    pub terminal_size: Arc<std::sync::RwLock<TerminalSize>>,
60    pub output_buffer: String,
61    pub last_key_time: Option<Instant>,
62}
63
64impl Default for App {
65    fn default() -> Self {
66        Self::new()
67    }
68}
69
70impl App {
71    pub fn new() -> Self {
72        let history = load_global_history();
73        let (width, height) = crossterm::terminal::size().unwrap_or((80, 24));
74        Self {
75            input: String::new(),
76            cursor_pos: 0,
77            awaiting_approval: false,
78            spinner_frame: 0,
79            current_task: None,
80            task_start_time: None,
81            job_start_time: None,
82            cwd: std::env::current_dir()
83                .map(|p| p.display().to_string())
84                .unwrap_or_else(|_| ".".to_string()),
85            model: String::from("unknown"),
86            history,
87            history_index: None,
88            footer_height: 4, // Fixed: Status, Folder+Token, Input, Queue
89            token_usage: TokenUsage::default(),
90            aborted: false,
91            queued_commands: Vec::new(),
92            log_x: 0,
93            log_y: 0,
94            reasoning_started: false,
95            content_started: false,
96            is_path_traversal_warning: false,
97            terminal_size: Arc::new(std::sync::RwLock::new(TerminalSize { width, height })),
98            output_buffer: String::new(),
99            last_key_time: None,
100        }
101    }
102
103    pub fn next_history(&mut self) {
104        if self.history.is_empty() {
105            return;
106        }
107        let next_index = match self.history_index {
108            Some(i) => {
109                if i > 0 {
110                    Some(i - 1)
111                } else {
112                    Some(0)
113                }
114            }
115            None => Some(self.history.len().saturating_sub(1)),
116        };
117        if let Some(idx) = next_index {
118            self.history_index = Some(idx);
119            self.input = self.history[idx].clone();
120            self.cursor_pos = self.input.len();
121        }
122    }
123
124    pub fn prev_history(&mut self) {
125        if self.history.is_empty() {
126            return;
127        }
128        let next_index = match self.history_index {
129            Some(i) => {
130                if i < self.history.len().saturating_sub(1) {
131                    Some(i + 1)
132                } else {
133                    self.input.clear();
134                    self.cursor_pos = 0;
135                    None
136                }
137            }
138            None => None,
139        };
140        self.history_index = next_index;
141        if let Some(idx) = self.history_index {
142            self.input = self.history[idx].clone();
143            self.cursor_pos = self.input.len();
144        }
145    }
146
147    pub fn start_task(&mut self, task: String) {
148        // Set job-wide timer once at the beginning of the agent job
149        if self.job_start_time.is_none() {
150            self.job_start_time = Some(Instant::now());
151        }
152        if self.current_task.as_ref() != Some(&task) {
153            self.current_task = Some(task);
154            self.task_start_time = Some(Instant::now());
155        }
156    }
157
158    pub fn finish_task(&mut self) {
159        self.current_task = None;
160        self.task_start_time = None;
161        self.job_start_time = None;
162        self.awaiting_approval = false;
163        self.is_path_traversal_warning = false;
164        self.aborted = false;
165        // Pop the just-completed command from queue
166        if !self.queued_commands.is_empty() {
167            self.queued_commands.remove(0);
168        }
169    }
170
171    pub fn tick(&mut self) {
172        self.spinner_frame = self.spinner_frame.wrapping_add(1);
173    }
174
175    /// Total tokens used so far
176    pub fn total_tokens(&self) -> u64 {
177        self.token_usage.prompt_tokens + self.token_usage.completion_tokens
178    }
179}