use std::sync::Arc;
use std::time::Instant;
use crate::api::types::TokenUsage;
#[derive(Debug, Clone, Copy)]
pub struct TerminalSize {
pub width: u16,
pub height: u16,
}
pub fn load_global_history() -> Vec<String> {
let path = std::path::PathBuf::from(".deep/input_history.json");
if let Ok(content) = std::fs::read_to_string(path) {
if let Ok(history) = serde_json::from_str::<Vec<String>>(&content) {
return history;
}
}
Vec::new()
}
pub fn save_global_history(history: &[String]) {
let path = std::path::PathBuf::from(".deep/input_history.json");
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
if let Ok(json) = serde_json::to_string_pretty(history) {
let _ = std::fs::write(path, json);
}
}
pub struct App {
pub input: String,
pub cursor_pos: usize,
pub awaiting_approval: bool,
pub spinner_frame: usize,
pub current_task: Option<String>,
pub task_start_time: Option<Instant>,
pub job_start_time: Option<Instant>,
pub cwd: String,
pub model: String,
pub history: Vec<String>,
pub history_index: Option<usize>,
pub footer_height: u16,
pub token_usage: TokenUsage,
pub aborted: bool,
pub queued_commands: Vec<String>,
pub log_x: u16,
pub log_y: u16,
pub reasoning_started: bool,
pub content_started: bool,
pub is_path_traversal_warning: bool,
pub terminal_size: Arc<std::sync::RwLock<TerminalSize>>,
pub output_buffer: String,
pub last_key_time: Option<Instant>,
}
impl Default for App {
fn default() -> Self {
Self::new()
}
}
impl App {
pub fn new() -> Self {
let history = load_global_history();
let (width, height) = crossterm::terminal::size().unwrap_or((80, 24));
Self {
input: String::new(),
cursor_pos: 0,
awaiting_approval: false,
spinner_frame: 0,
current_task: None,
task_start_time: None,
job_start_time: None,
cwd: std::env::current_dir()
.map(|p| p.display().to_string())
.unwrap_or_else(|_| ".".to_string()),
model: String::from("unknown"),
history,
history_index: None,
footer_height: 4, token_usage: TokenUsage::default(),
aborted: false,
queued_commands: Vec::new(),
log_x: 0,
log_y: 0,
reasoning_started: false,
content_started: false,
is_path_traversal_warning: false,
terminal_size: Arc::new(std::sync::RwLock::new(TerminalSize { width, height })),
output_buffer: String::new(),
last_key_time: None,
}
}
pub fn next_history(&mut self) {
if self.history.is_empty() {
return;
}
let next_index = match self.history_index {
Some(i) => {
if i > 0 {
Some(i - 1)
} else {
Some(0)
}
}
None => Some(self.history.len().saturating_sub(1)),
};
if let Some(idx) = next_index {
self.history_index = Some(idx);
self.input = self.history[idx].clone();
self.cursor_pos = self.input.len();
}
}
pub fn prev_history(&mut self) {
if self.history.is_empty() {
return;
}
let next_index = match self.history_index {
Some(i) => {
if i < self.history.len().saturating_sub(1) {
Some(i + 1)
} else {
self.input.clear();
self.cursor_pos = 0;
None
}
}
None => None,
};
self.history_index = next_index;
if let Some(idx) = self.history_index {
self.input = self.history[idx].clone();
self.cursor_pos = self.input.len();
}
}
pub fn start_task(&mut self, task: String) {
if self.job_start_time.is_none() {
self.job_start_time = Some(Instant::now());
}
if self.current_task.as_ref() != Some(&task) {
self.current_task = Some(task);
self.task_start_time = Some(Instant::now());
}
}
pub fn finish_task(&mut self) {
self.current_task = None;
self.task_start_time = None;
self.job_start_time = None;
self.awaiting_approval = false;
self.is_path_traversal_warning = false;
self.aborted = false;
if !self.queued_commands.is_empty() {
self.queued_commands.remove(0);
}
}
pub fn tick(&mut self) {
self.spinner_frame = self.spinner_frame.wrapping_add(1);
}
pub fn total_tokens(&self) -> u64 {
self.token_usage.prompt_tokens + self.token_usage.completion_tokens
}
}