Files
zesdex/src/app/state/misc.rs
T

268 lines
6.8 KiB
Rust
Raw Normal View History

use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::RwLock;
use super::types::Overlay;
#[derive(Clone)]
pub struct DirCache {
entries: Arc<RwLock<Vec<PathBuf>>>,
}
impl DirCache {
pub fn new() -> Self {
DirCache {
entries: Arc::new(RwLock::new(Vec::new())),
}
}
pub async fn set(&self, paths: Vec<PathBuf>) {
let mut w = self.entries.write().await;
*w = paths;
}
}
#[derive(Debug, Clone)]
pub struct ScrollState {
pub offset: usize,
pub max_visible: usize,
}
impl ScrollState {
pub fn new() -> Self {
ScrollState {
offset: 0,
max_visible: 30,
}
}
pub fn scroll_up(&mut self, amount: usize) {
self.offset = self.offset.saturating_add(amount);
}
pub fn scroll_down(&mut self, amount: usize) {
self.offset = self.offset.saturating_sub(amount);
}
pub fn set_max_visible(&mut self, max: usize) {
self.max_visible = max;
}
}
#[derive(Debug, Clone)]
pub struct InputState {
pub buffer: String,
pub cursor: usize,
pub history: Vec<String>,
pub history_idx: Option<usize>,
pub autocomplete_prefix: String,
pub autocomplete_candidates: Vec<String>,
pub autocomplete_idx: usize,
pub autocomplete_visible: bool,
}
const COMMANDS: &[&str] = &[
"/help",
"/quit",
"/clear",
"/lesson",
"/lesson ls",
"/lesson export",
"/lesson import",
"/lesson accept",
"/lesson reject",
"/login",
"/login zen",
"/login openai",
"/edit",
"/mcp add",
"/model",
"/model ls",
"/model add",
];
impl InputState {
pub fn new() -> Self {
InputState {
buffer: String::new(),
cursor: 0,
history: Vec::new(),
history_idx: None,
autocomplete_prefix: String::new(),
autocomplete_candidates: Vec::new(),
autocomplete_idx: 0,
autocomplete_visible: false,
}
}
pub fn close_autocomplete(&mut self) {
self.autocomplete_visible = false;
self.autocomplete_candidates.clear();
self.autocomplete_prefix.clear();
self.autocomplete_idx = 0;
}
pub fn open_autocomplete(&mut self) {
let trimmed = self.buffer.trim().to_string();
if trimmed.is_empty() || !trimmed.starts_with('/') {
self.close_autocomplete();
return;
}
let prefix = trimmed.to_lowercase();
self.autocomplete_candidates = COMMANDS
.iter()
.filter(|c| c.starts_with(&prefix))
.map(|c| c.to_string())
.collect();
self.autocomplete_prefix = prefix;
self.autocomplete_idx = 0;
self.autocomplete_visible = !self.autocomplete_candidates.is_empty();
}
pub fn cycle_autocomplete(&mut self, forward: bool) {
let n = self.autocomplete_candidates.len();
if n == 0 { return; }
if forward {
self.autocomplete_idx = (self.autocomplete_idx + 1) % n;
} else {
self.autocomplete_idx = if self.autocomplete_idx == 0 { n - 1 } else { self.autocomplete_idx - 1 };
}
}
pub fn select_autocomplete(&mut self) -> bool {
if let Some(candidate) = self.autocomplete_candidates.get(self.autocomplete_idx) {
self.buffer = candidate.clone();
self.cursor = self.buffer.len();
self.close_autocomplete();
true
} else {
false
}
}
pub fn tab_complete(&mut self) {
// Legacy inline tab-complete — used as a fallback when the dropdown
// isn't visible yet. Opens the dropdown on the first Tab press.
if !self.autocomplete_visible {
self.open_autocomplete();
} else {
self.cycle_autocomplete(true);
}
}
pub fn char_left(&mut self) {
if self.cursor > 0 {
self.cursor -= 1;
}
}
pub fn char_right(&mut self) {
if self.cursor < self.buffer.len() {
self.cursor += 1;
}
}
pub fn insert(&mut self, c: char) {
self.buffer.insert(self.cursor, c);
self.cursor += 1;
}
pub fn delete_left(&mut self) {
if self.cursor > 0 {
self.cursor -= 1;
self.buffer.remove(self.cursor);
}
}
pub fn delete_right(&mut self) {
if self.cursor < self.buffer.len() {
self.buffer.remove(self.cursor);
}
}
pub fn submit(&mut self) -> String {
let result = self.buffer.clone();
if !result.is_empty() {
self.history.push(result.clone());
self.history_idx = None;
}
self.buffer.clear();
self.cursor = 0;
result
}
pub fn history_up(&mut self) {
if self.history.is_empty() {
return;
}
let idx = match self.history_idx {
Some(i) if i > 0 => i - 1,
None => self.history.len() - 1,
Some(_) => return,
};
self.history_idx = Some(idx);
self.buffer = self.history[idx].clone();
self.cursor = self.buffer.len();
}
pub fn history_down(&mut self) {
match self.history_idx {
Some(i) if i < self.history.len() - 1 => {
let idx = i + 1;
self.history_idx = Some(idx);
self.buffer = self.history[idx].clone();
self.cursor = self.buffer.len();
}
Some(_) => {
self.history_idx = None;
self.buffer.clear();
self.cursor = 0;
}
None => {}
}
}
}
#[derive(Debug, Clone)]
pub struct MiscState {
pub overlay: Overlay,
pub toasts: Vec<super::types::Toast>,
pub yolo_armed: bool,
pub security_armed: bool,
pub esc_press_count: u32,
pub last_staleness_sweep_ms: i64,
pub thinking: bool,
pub effort_level: usize,
pub selected_index: usize,
pub editor: Option<super::super::mode::editor::EditorState>,
pub api_connected: bool,
}
impl MiscState {
pub fn new() -> Self {
MiscState {
overlay: Overlay::None,
toasts: Vec::new(),
yolo_armed: false,
security_armed: false,
esc_press_count: 0,
last_staleness_sweep_ms: 0,
thinking: false,
effort_level: 1,
selected_index: 0,
editor: None,
api_connected: false,
}
}
pub fn push_toast(&mut self, toast: super::types::Toast) {
self.toasts.push(toast);
}
pub fn drain_expired_toasts(&mut self, now_ms: i64) -> Vec<super::types::Toast> {
let expired: Vec<_> = self.toasts.iter().filter(|t| t.expired(now_ms)).cloned().collect();
self.toasts.retain(|t| !t.expired(now_ms));
expired
}
}