#[derive(Debug, Clone)]
pub struct CommandPaletteState {
pub commands: Vec<PaletteCommand>,
pub input: String,
pub cursor: usize,
pub open: bool,
pub last_selected: Option<usize>,
selected: usize,
filter_cache: Option<(String, Vec<usize>)>,
}
impl CommandPaletteState {
pub fn new(commands: Vec<PaletteCommand>) -> Self {
Self {
commands,
input: String::new(),
cursor: 0,
open: false,
last_selected: None,
selected: 0,
filter_cache: None,
}
}
pub fn toggle(&mut self) {
self.open = !self.open;
if self.open {
self.input.clear();
self.cursor = 0;
self.selected = 0;
self.filter_cache = None;
}
}
fn fuzzy_score(pattern: &str, text: &str) -> Option<i32> {
let pattern = pattern.trim();
if pattern.is_empty() {
return Some(0);
}
let text_chars: Vec<char> = text.chars().collect();
let mut score = 0;
let mut search_start = 0usize;
let mut prev_match: Option<usize> = None;
for p in pattern.chars() {
let mut found = None;
for (idx, ch) in text_chars.iter().enumerate().skip(search_start) {
if ch.eq_ignore_ascii_case(&p) {
found = Some(idx);
break;
}
}
let idx = found?;
if prev_match.is_some_and(|prev| idx == prev + 1) {
score += 3;
} else {
score += 1;
}
if idx == 0 {
score += 2;
} else {
let prev = text_chars[idx - 1];
let curr = text_chars[idx];
if matches!(prev, ' ' | '_' | '-') || prev.is_uppercase() || curr.is_uppercase() {
score += 2;
}
}
prev_match = Some(idx);
search_start = idx + 1;
}
Some(score)
}
pub(crate) fn filtered_indices_cached(&mut self) -> &[usize] {
let needs_recompute = match &self.filter_cache {
Some((cached_input, _)) => *cached_input != self.input,
None => true,
};
if needs_recompute {
let indices = self.filtered_indices();
self.filter_cache = Some((self.input.clone(), indices));
}
&self
.filter_cache
.as_ref()
.expect("filter_cache populated above")
.1
}
pub(crate) fn filtered_indices(&self) -> Vec<usize> {
let query = self.input.trim();
if query.is_empty() {
return (0..self.commands.len()).collect();
}
let mut scored: Vec<(usize, i32)> = self
.commands
.iter()
.enumerate()
.filter_map(|(i, cmd)| {
let mut haystack =
String::with_capacity(cmd.label.len() + cmd.description.len() + 1);
haystack.push_str(&cmd.label);
haystack.push(' ');
haystack.push_str(&cmd.description);
Self::fuzzy_score(query, &haystack).map(|score| (i, score))
})
.collect();
if scored.is_empty() {
let tokens: Vec<String> = query.split_whitespace().map(|t| t.to_lowercase()).collect();
return self
.commands
.iter()
.enumerate()
.filter(|(_, cmd)| {
let label = cmd.label.to_lowercase();
let desc = cmd.description.to_lowercase();
tokens.iter().all(|token| {
label.contains(token.as_str()) || desc.contains(token.as_str())
})
})
.map(|(i, _)| i)
.collect();
}
scored.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
scored.into_iter().map(|(idx, _)| idx).collect()
}
pub(crate) fn selected(&self) -> usize {
self.selected
}
pub(crate) fn set_selected(&mut self, s: usize) {
self.selected = s;
}
}
#[derive(Debug, Clone)]
pub struct StreamingTextState {
pub content: String,
pub streaming: bool,
pub(crate) cursor_visible: bool,
pub(crate) cursor_tick: u64,
}
impl StreamingTextState {
pub fn new() -> Self {
Self {
content: String::new(),
streaming: false,
cursor_visible: true,
cursor_tick: 0,
}
}
pub fn push(&mut self, chunk: &str) {
self.content.push_str(chunk);
}
pub fn finish(&mut self) {
self.streaming = false;
}
pub fn start(&mut self) {
self.content.clear();
self.streaming = true;
self.cursor_visible = true;
self.cursor_tick = 0;
}
pub fn clear(&mut self) {
self.content.clear();
self.streaming = false;
self.cursor_visible = true;
self.cursor_tick = 0;
}
}
impl Default for StreamingTextState {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct StreamingMarkdownState {
pub content: String,
pub streaming: bool,
pub cursor_visible: bool,
pub cursor_tick: u64,
pub in_code_block: bool,
pub code_block_lang: String,
}
impl StreamingMarkdownState {
pub fn new() -> Self {
Self {
content: String::new(),
streaming: false,
cursor_visible: true,
cursor_tick: 0,
in_code_block: false,
code_block_lang: String::new(),
}
}
pub fn push(&mut self, chunk: &str) {
self.content.push_str(chunk);
}
pub fn start(&mut self) {
self.content.clear();
self.streaming = true;
self.cursor_visible = true;
self.cursor_tick = 0;
self.in_code_block = false;
self.code_block_lang.clear();
}
pub fn finish(&mut self) {
self.streaming = false;
}
pub fn clear(&mut self) {
self.content.clear();
self.streaming = false;
self.cursor_visible = true;
self.cursor_tick = 0;
self.in_code_block = false;
self.code_block_lang.clear();
}
}
impl Default for StreamingMarkdownState {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct ScreenState {
stack: Vec<String>,
focus_state: std::collections::HashMap<String, (usize, usize)>,
}
impl ScreenState {
pub fn new(initial: impl Into<String>) -> Self {
Self {
stack: vec![initial.into()],
focus_state: std::collections::HashMap::new(),
}
}
pub fn current(&self) -> &str {
self.stack
.last()
.expect("ScreenState always contains at least one screen")
.as_str()
}
pub fn push(&mut self, name: impl Into<String>) {
self.stack.push(name.into());
}
pub fn pop(&mut self) {
if self.can_pop() {
self.stack.pop();
}
}
pub fn depth(&self) -> usize {
self.stack.len()
}
pub fn can_pop(&self) -> bool {
self.stack.len() > 1
}
pub fn reset(&mut self) {
self.stack.truncate(1);
}
pub(crate) fn save_focus(&mut self, name: &str, focus_index: usize, focus_count: usize) {
self.focus_state
.insert(name.to_string(), (focus_index, focus_count));
}
pub(crate) fn restore_focus(&self, name: &str) -> (usize, usize) {
self.focus_state.get(name).copied().unwrap_or((0, 0))
}
}
#[derive(Debug, Clone)]
pub struct ModeState {
modes: std::collections::HashMap<String, ScreenState>,
active: String,
}
impl ModeState {
pub fn new(mode: impl Into<String>, screen: impl Into<String>) -> Self {
let mode = mode.into();
let mut modes = std::collections::HashMap::new();
modes.insert(mode.clone(), ScreenState::new(screen));
Self {
modes,
active: mode,
}
}
pub fn add_mode(&mut self, mode: impl Into<String>, screen: impl Into<String>) {
let mode = mode.into();
self.modes
.entry(mode)
.or_insert_with(|| ScreenState::new(screen));
}
pub fn switch_mode(&mut self, mode: impl Into<String>) {
let mode = mode.into();
assert!(
self.modes.contains_key(&mode),
"mode '{}' not found",
mode
);
self.active = mode;
}
pub fn try_switch_mode(&mut self, mode: impl Into<String>) -> bool {
let mode = mode.into();
if !self.modes.contains_key(&mode) {
return false;
}
self.active = mode;
true
}
pub fn active_mode(&self) -> &str {
&self.active
}
pub fn screens(&self) -> &ScreenState {
self.modes
.get(&self.active)
.expect("active mode must exist")
}
pub fn screens_mut(&mut self) -> &mut ScreenState {
self.modes
.get_mut(&self.active)
.expect("active mode must exist")
}
}
#[cfg(test)]
mod mode_state_tests {
use super::ModeState;
#[test]
fn try_switch_mode_returns_false_for_unknown_mode() {
let mut modes = ModeState::new("app", "home");
modes.add_mode("settings", "general");
assert!(modes.try_switch_mode("settings"));
assert_eq!(modes.active_mode(), "settings");
assert!(!modes.try_switch_mode("nonexistent"));
assert_eq!(modes.active_mode(), "settings");
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ApprovalAction {
Pending,
Approved,
Rejected,
}
#[derive(Debug, Clone)]
pub struct ToolApprovalState {
pub tool_name: String,
pub description: String,
pub action: ApprovalAction,
}
impl ToolApprovalState {
pub fn new(tool_name: impl Into<String>, description: impl Into<String>) -> Self {
Self {
tool_name: tool_name.into(),
description: description.into(),
action: ApprovalAction::Pending,
}
}
pub fn reset(&mut self) {
self.action = ApprovalAction::Pending;
}
}
#[derive(Debug, Clone)]
pub struct ContextItem {
pub label: String,
pub tokens: usize,
}
impl ContextItem {
pub fn new(label: impl Into<String>, tokens: usize) -> Self {
Self {
label: label.into(),
tokens,
}
}
}