Skip to main content

deepseek_rust_cli/tui/
app.rs

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