1use std::collections::HashSet;
8use std::fs;
9use std::path::PathBuf;
10use std::time::{SystemTime, UNIX_EPOCH};
11use unicode_width::UnicodeWidthStr;
12
13use crate::Style;
14
15type FormValidator = fn(&str) -> Result<(), String>;
16type TextInputValidator = Box<dyn Fn(&str) -> Result<(), String>>;
17
18#[derive(Debug, Clone, Default)]
22pub struct StaticOutput {
23 lines: Vec<String>,
24 new_lines: Vec<String>,
25}
26
27impl StaticOutput {
28 pub fn new() -> Self {
30 Self::default()
31 }
32
33 pub fn println(&mut self, line: impl Into<String>) {
35 let line = line.into();
36 self.lines.push(line.clone());
37 self.new_lines.push(line);
38 }
39
40 pub fn lines(&self) -> &[String] {
42 &self.lines
43 }
44
45 pub fn drain_new(&mut self) -> Vec<String> {
47 std::mem::take(&mut self.new_lines)
48 }
49
50 pub fn clear(&mut self) {
52 self.lines.clear();
53 self.new_lines.clear();
54 }
55}
56
57pub struct TextInputState {
73 pub value: String,
75 pub cursor: usize,
77 pub placeholder: String,
79 pub max_length: Option<usize>,
81 pub validation_error: Option<String>,
83 pub masked: bool,
85 pub suggestions: Vec<String>,
87 pub suggestion_index: usize,
89 pub show_suggestions: bool,
91 validators: Vec<TextInputValidator>,
93 validation_errors: Vec<String>,
95}
96
97impl std::fmt::Debug for TextInputState {
98 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99 f.debug_struct("TextInputState")
100 .field("value", &self.value)
101 .field("cursor", &self.cursor)
102 .field("placeholder", &self.placeholder)
103 .field("max_length", &self.max_length)
104 .field("validation_error", &self.validation_error)
105 .field("masked", &self.masked)
106 .field("suggestions", &self.suggestions)
107 .field("suggestion_index", &self.suggestion_index)
108 .field("show_suggestions", &self.show_suggestions)
109 .field("validators_len", &self.validators.len())
110 .field("validation_errors", &self.validation_errors)
111 .finish()
112 }
113}
114
115impl Clone for TextInputState {
116 fn clone(&self) -> Self {
117 Self {
118 value: self.value.clone(),
119 cursor: self.cursor,
120 placeholder: self.placeholder.clone(),
121 max_length: self.max_length,
122 validation_error: self.validation_error.clone(),
123 masked: self.masked,
124 suggestions: self.suggestions.clone(),
125 suggestion_index: self.suggestion_index,
126 show_suggestions: self.show_suggestions,
127 validators: Vec::new(),
128 validation_errors: self.validation_errors.clone(),
129 }
130 }
131}
132
133impl TextInputState {
134 pub fn new() -> Self {
136 Self {
137 value: String::new(),
138 cursor: 0,
139 placeholder: String::new(),
140 max_length: None,
141 validation_error: None,
142 masked: false,
143 suggestions: Vec::new(),
144 suggestion_index: 0,
145 show_suggestions: false,
146 validators: Vec::new(),
147 validation_errors: Vec::new(),
148 }
149 }
150
151 pub fn with_placeholder(p: impl Into<String>) -> Self {
153 Self {
154 placeholder: p.into(),
155 ..Self::new()
156 }
157 }
158
159 pub fn max_length(mut self, len: usize) -> Self {
161 self.max_length = Some(len);
162 self
163 }
164
165 pub fn validate(&mut self, validator: impl Fn(&str) -> Result<(), String>) {
173 self.validation_error = validator(&self.value).err();
174 }
175
176 pub fn add_validator(&mut self, f: impl Fn(&str) -> Result<(), String> + 'static) {
181 self.validators.push(Box::new(f));
182 }
183
184 pub fn run_validators(&mut self) {
189 self.validation_errors.clear();
190 for validator in &self.validators {
191 if let Err(err) = validator(&self.value) {
192 self.validation_errors.push(err);
193 }
194 }
195 self.validation_error = self.validation_errors.first().cloned();
196 }
197
198 pub fn errors(&self) -> &[String] {
200 &self.validation_errors
201 }
202
203 pub fn set_suggestions(&mut self, suggestions: Vec<String>) {
205 self.suggestions = suggestions;
206 self.suggestion_index = 0;
207 self.show_suggestions = !self.suggestions.is_empty();
208 }
209
210 pub fn matched_suggestions(&self) -> Vec<&str> {
212 if self.value.is_empty() {
213 return Vec::new();
214 }
215 let lower = self.value.to_lowercase();
216 self.suggestions
217 .iter()
218 .filter(|s| s.to_lowercase().starts_with(&lower))
219 .map(|s| s.as_str())
220 .collect()
221 }
222}
223
224impl Default for TextInputState {
225 fn default() -> Self {
226 Self::new()
227 }
228}
229
230#[derive(Debug, Default)]
232pub struct FormField {
233 pub label: String,
235 pub input: TextInputState,
237 pub error: Option<String>,
239}
240
241impl FormField {
242 pub fn new(label: impl Into<String>) -> Self {
244 Self {
245 label: label.into(),
246 input: TextInputState::new(),
247 error: None,
248 }
249 }
250
251 pub fn placeholder(mut self, p: impl Into<String>) -> Self {
253 self.input.placeholder = p.into();
254 self
255 }
256}
257
258#[derive(Debug)]
260pub struct FormState {
261 pub fields: Vec<FormField>,
263 pub submitted: bool,
265}
266
267impl FormState {
268 pub fn new() -> Self {
270 Self {
271 fields: Vec::new(),
272 submitted: false,
273 }
274 }
275
276 pub fn field(mut self, field: FormField) -> Self {
278 self.fields.push(field);
279 self
280 }
281
282 pub fn validate(&mut self, validators: &[FormValidator]) -> bool {
286 let mut all_valid = true;
287 for (i, field) in self.fields.iter_mut().enumerate() {
288 if let Some(validator) = validators.get(i) {
289 match validator(&field.input.value) {
290 Ok(()) => field.error = None,
291 Err(msg) => {
292 field.error = Some(msg);
293 all_valid = false;
294 }
295 }
296 }
297 }
298 all_valid
299 }
300
301 pub fn value(&self, index: usize) -> &str {
303 self.fields
304 .get(index)
305 .map(|f| f.input.value.as_str())
306 .unwrap_or("")
307 }
308}
309
310impl Default for FormState {
311 fn default() -> Self {
312 Self::new()
313 }
314}
315
316#[derive(Debug, Clone)]
322pub struct ToastState {
323 pub messages: Vec<ToastMessage>,
325}
326
327#[derive(Debug, Clone)]
329pub struct ToastMessage {
330 pub text: String,
332 pub level: ToastLevel,
334 pub created_tick: u64,
336 pub duration_ticks: u64,
338}
339
340impl Default for ToastMessage {
341 fn default() -> Self {
342 Self {
343 text: String::new(),
344 level: ToastLevel::Info,
345 created_tick: 0,
346 duration_ticks: 30,
347 }
348 }
349}
350
351#[derive(Debug, Clone, Copy, PartialEq, Eq)]
353pub enum ToastLevel {
354 Info,
356 Success,
358 Warning,
360 Error,
362}
363
364#[non_exhaustive]
366#[derive(Debug, Clone, Copy, PartialEq, Eq)]
367pub enum AlertLevel {
368 Info,
370 Success,
372 Warning,
374 Error,
376}
377
378impl ToastState {
379 pub fn new() -> Self {
381 Self {
382 messages: Vec::new(),
383 }
384 }
385
386 pub fn info(&mut self, text: impl Into<String>, tick: u64) {
388 self.push(text, ToastLevel::Info, tick, 30);
389 }
390
391 pub fn success(&mut self, text: impl Into<String>, tick: u64) {
393 self.push(text, ToastLevel::Success, tick, 30);
394 }
395
396 pub fn warning(&mut self, text: impl Into<String>, tick: u64) {
398 self.push(text, ToastLevel::Warning, tick, 50);
399 }
400
401 pub fn error(&mut self, text: impl Into<String>, tick: u64) {
403 self.push(text, ToastLevel::Error, tick, 80);
404 }
405
406 pub fn push(
408 &mut self,
409 text: impl Into<String>,
410 level: ToastLevel,
411 tick: u64,
412 duration_ticks: u64,
413 ) {
414 self.messages.push(ToastMessage {
415 text: text.into(),
416 level,
417 created_tick: tick,
418 duration_ticks,
419 });
420 }
421
422 pub fn cleanup(&mut self, current_tick: u64) {
426 self.messages.retain(|message| {
427 current_tick < message.created_tick.saturating_add(message.duration_ticks)
428 });
429 }
430}
431
432impl Default for ToastState {
433 fn default() -> Self {
434 Self::new()
435 }
436}
437
438#[derive(Debug, Clone)]
443pub struct TextareaState {
444 pub lines: Vec<String>,
446 pub cursor_row: usize,
448 pub cursor_col: usize,
450 pub max_length: Option<usize>,
452 pub wrap_width: Option<u32>,
454 pub scroll_offset: usize,
456}
457
458impl TextareaState {
459 pub fn new() -> Self {
461 Self {
462 lines: vec![String::new()],
463 cursor_row: 0,
464 cursor_col: 0,
465 max_length: None,
466 wrap_width: None,
467 scroll_offset: 0,
468 }
469 }
470
471 pub fn value(&self) -> String {
473 self.lines.join("\n")
474 }
475
476 pub fn set_value(&mut self, text: impl Into<String>) {
480 let value = text.into();
481 self.lines = value.split('\n').map(str::to_string).collect();
482 if self.lines.is_empty() {
483 self.lines.push(String::new());
484 }
485 self.cursor_row = 0;
486 self.cursor_col = 0;
487 self.scroll_offset = 0;
488 }
489
490 pub fn max_length(mut self, len: usize) -> Self {
492 self.max_length = Some(len);
493 self
494 }
495
496 pub fn word_wrap(mut self, width: u32) -> Self {
498 self.wrap_width = Some(width);
499 self
500 }
501}
502
503impl Default for TextareaState {
504 fn default() -> Self {
505 Self::new()
506 }
507}
508
509#[derive(Debug, Clone)]
515pub struct SpinnerState {
516 chars: Vec<char>,
517}
518
519impl SpinnerState {
520 pub fn dots() -> Self {
524 Self {
525 chars: vec!['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
526 }
527 }
528
529 pub fn line() -> Self {
533 Self {
534 chars: vec!['|', '/', '-', '\\'],
535 }
536 }
537
538 pub fn frame(&self, tick: u64) -> char {
540 if self.chars.is_empty() {
541 return ' ';
542 }
543 self.chars[tick as usize % self.chars.len()]
544 }
545}
546
547impl Default for SpinnerState {
548 fn default() -> Self {
549 Self::dots()
550 }
551}
552
553#[derive(Debug, Clone, Default)]
558pub struct ListState {
559 pub items: Vec<String>,
561 pub selected: usize,
563 pub filter: String,
565 view_indices: Vec<usize>,
566}
567
568impl ListState {
569 pub fn new(items: Vec<impl Into<String>>) -> Self {
571 let len = items.len();
572 Self {
573 items: items.into_iter().map(Into::into).collect(),
574 selected: 0,
575 filter: String::new(),
576 view_indices: (0..len).collect(),
577 }
578 }
579
580 pub fn set_items(&mut self, items: Vec<impl Into<String>>) {
585 self.items = items.into_iter().map(Into::into).collect();
586 self.selected = self.selected.min(self.items.len().saturating_sub(1));
587 self.rebuild_view();
588 }
589
590 pub fn set_filter(&mut self, filter: impl Into<String>) {
594 self.filter = filter.into();
595 self.rebuild_view();
596 }
597
598 pub fn visible_indices(&self) -> &[usize] {
600 &self.view_indices
601 }
602
603 pub fn selected_item(&self) -> Option<&str> {
605 let data_idx = *self.view_indices.get(self.selected)?;
606 self.items.get(data_idx).map(String::as_str)
607 }
608
609 fn rebuild_view(&mut self) {
610 let tokens: Vec<String> = self
611 .filter
612 .split_whitespace()
613 .map(|t| t.to_lowercase())
614 .collect();
615 self.view_indices = if tokens.is_empty() {
616 (0..self.items.len()).collect()
617 } else {
618 (0..self.items.len())
619 .filter(|&i| {
620 tokens
621 .iter()
622 .all(|token| self.items[i].to_lowercase().contains(token.as_str()))
623 })
624 .collect()
625 };
626 if !self.view_indices.is_empty() && self.selected >= self.view_indices.len() {
627 self.selected = self.view_indices.len() - 1;
628 }
629 }
630}
631
632#[derive(Debug, Clone)]
636pub struct FilePickerState {
637 pub current_dir: PathBuf,
639 pub entries: Vec<FileEntry>,
641 pub selected: usize,
643 pub selected_file: Option<PathBuf>,
645 pub show_hidden: bool,
647 pub extensions: Vec<String>,
649 pub dirty: bool,
651}
652
653#[derive(Debug, Clone, Default)]
655pub struct FileEntry {
656 pub name: String,
658 pub path: PathBuf,
660 pub is_dir: bool,
662 pub size: u64,
664}
665
666impl FilePickerState {
667 pub fn new(dir: impl Into<PathBuf>) -> Self {
669 Self {
670 current_dir: dir.into(),
671 entries: Vec::new(),
672 selected: 0,
673 selected_file: None,
674 show_hidden: false,
675 extensions: Vec::new(),
676 dirty: true,
677 }
678 }
679
680 pub fn show_hidden(mut self, show: bool) -> Self {
682 self.show_hidden = show;
683 self.dirty = true;
684 self
685 }
686
687 pub fn extensions(mut self, exts: &[&str]) -> Self {
689 self.extensions = exts
690 .iter()
691 .map(|ext| ext.trim().trim_start_matches('.').to_ascii_lowercase())
692 .filter(|ext| !ext.is_empty())
693 .collect();
694 self.dirty = true;
695 self
696 }
697
698 pub fn selected(&self) -> Option<&PathBuf> {
700 self.selected_file.as_ref()
701 }
702
703 pub fn refresh(&mut self) {
705 let mut entries = Vec::new();
706
707 if let Ok(read_dir) = fs::read_dir(&self.current_dir) {
708 for dir_entry in read_dir.flatten() {
709 let name = dir_entry.file_name().to_string_lossy().to_string();
710 if !self.show_hidden && name.starts_with('.') {
711 continue;
712 }
713
714 let Ok(file_type) = dir_entry.file_type() else {
715 continue;
716 };
717 if file_type.is_symlink() {
718 continue;
719 }
720
721 let path = dir_entry.path();
722 let is_dir = file_type.is_dir();
723
724 if !is_dir && !self.extensions.is_empty() {
725 let ext = path
726 .extension()
727 .and_then(|e| e.to_str())
728 .map(|e| e.to_ascii_lowercase());
729 let Some(ext) = ext else {
730 continue;
731 };
732 if !self.extensions.iter().any(|allowed| allowed == &ext) {
733 continue;
734 }
735 }
736
737 let size = if is_dir {
738 0
739 } else {
740 fs::symlink_metadata(&path).map(|m| m.len()).unwrap_or(0)
741 };
742
743 entries.push(FileEntry {
744 name,
745 path,
746 is_dir,
747 size,
748 });
749 }
750 }
751
752 entries.sort_by(|a, b| match (a.is_dir, b.is_dir) {
753 (true, false) => std::cmp::Ordering::Less,
754 (false, true) => std::cmp::Ordering::Greater,
755 _ => a
756 .name
757 .to_ascii_lowercase()
758 .cmp(&b.name.to_ascii_lowercase())
759 .then_with(|| a.name.cmp(&b.name)),
760 });
761
762 self.entries = entries;
763 if self.entries.is_empty() {
764 self.selected = 0;
765 } else {
766 self.selected = self.selected.min(self.entries.len().saturating_sub(1));
767 }
768 self.dirty = false;
769 }
770}
771
772impl Default for FilePickerState {
773 fn default() -> Self {
774 Self::new(".")
775 }
776}
777
778#[derive(Debug, Clone, Default)]
783pub struct TabsState {
784 pub labels: Vec<String>,
786 pub selected: usize,
788}
789
790impl TabsState {
791 pub fn new(labels: Vec<impl Into<String>>) -> Self {
793 Self {
794 labels: labels.into_iter().map(Into::into).collect(),
795 selected: 0,
796 }
797 }
798
799 pub fn selected_label(&self) -> Option<&str> {
801 self.labels.get(self.selected).map(String::as_str)
802 }
803}
804
805#[derive(Debug, Clone)]
811pub struct TableState {
812 pub headers: Vec<String>,
814 pub rows: Vec<Vec<String>>,
816 pub selected: usize,
818 column_widths: Vec<u32>,
819 widths_dirty: bool,
820 pub sort_column: Option<usize>,
822 pub sort_ascending: bool,
824 pub filter: String,
826 pub page: usize,
828 pub page_size: usize,
830 pub zebra: bool,
832 view_indices: Vec<usize>,
833 row_search_cache: Vec<String>,
834 filter_tokens: Vec<String>,
835}
836
837impl Default for TableState {
838 fn default() -> Self {
839 Self {
840 headers: Vec::new(),
841 rows: Vec::new(),
842 selected: 0,
843 column_widths: Vec::new(),
844 widths_dirty: true,
845 sort_column: None,
846 sort_ascending: true,
847 filter: String::new(),
848 page: 0,
849 page_size: 0,
850 zebra: false,
851 view_indices: Vec::new(),
852 row_search_cache: Vec::new(),
853 filter_tokens: Vec::new(),
854 }
855 }
856}
857
858impl TableState {
859 pub fn new(headers: Vec<impl Into<String>>, rows: Vec<Vec<impl Into<String>>>) -> Self {
861 let headers: Vec<String> = headers.into_iter().map(Into::into).collect();
862 let rows: Vec<Vec<String>> = rows
863 .into_iter()
864 .map(|r| r.into_iter().map(Into::into).collect())
865 .collect();
866 let mut state = Self {
867 headers,
868 rows,
869 selected: 0,
870 column_widths: Vec::new(),
871 widths_dirty: true,
872 sort_column: None,
873 sort_ascending: true,
874 filter: String::new(),
875 page: 0,
876 page_size: 0,
877 zebra: false,
878 view_indices: Vec::new(),
879 row_search_cache: Vec::new(),
880 filter_tokens: Vec::new(),
881 };
882 state.rebuild_row_search_cache();
883 state.rebuild_view();
884 state.recompute_widths();
885 state
886 }
887
888 pub fn set_rows(&mut self, rows: Vec<Vec<impl Into<String>>>) {
893 self.rows = rows
894 .into_iter()
895 .map(|r| r.into_iter().map(Into::into).collect())
896 .collect();
897 self.rebuild_row_search_cache();
898 self.rebuild_view();
899 }
900
901 pub fn toggle_sort(&mut self, column: usize) {
903 if self.sort_column == Some(column) {
904 self.sort_ascending = !self.sort_ascending;
905 } else {
906 self.sort_column = Some(column);
907 self.sort_ascending = true;
908 }
909 self.rebuild_view();
910 }
911
912 pub fn sort_by(&mut self, column: usize) {
914 if self.sort_column == Some(column) && self.sort_ascending {
915 return;
916 }
917 self.sort_column = Some(column);
918 self.sort_ascending = true;
919 self.rebuild_view();
920 }
921
922 pub fn set_filter(&mut self, filter: impl Into<String>) {
926 let filter = filter.into();
927 if self.filter == filter {
928 return;
929 }
930 self.filter = filter;
931 self.filter_tokens = Self::tokenize_filter(&self.filter);
932 self.page = 0;
933 self.rebuild_view();
934 }
935
936 pub fn clear_sort(&mut self) {
938 if self.sort_column.is_none() && self.sort_ascending {
939 return;
940 }
941 self.sort_column = None;
942 self.sort_ascending = true;
943 self.rebuild_view();
944 }
945
946 pub fn next_page(&mut self) {
948 if self.page_size == 0 {
949 return;
950 }
951 let last_page = self.total_pages().saturating_sub(1);
952 self.page = (self.page + 1).min(last_page);
953 }
954
955 pub fn prev_page(&mut self) {
957 self.page = self.page.saturating_sub(1);
958 }
959
960 pub fn total_pages(&self) -> usize {
962 if self.page_size == 0 {
963 return 1;
964 }
965
966 let len = self.view_indices.len();
967 if len == 0 {
968 1
969 } else {
970 len.div_ceil(self.page_size)
971 }
972 }
973
974 pub fn visible_indices(&self) -> &[usize] {
976 &self.view_indices
977 }
978
979 pub fn selected_row(&self) -> Option<&[String]> {
981 if self.view_indices.is_empty() {
982 return None;
983 }
984 let data_idx = self.view_indices.get(self.selected)?;
985 self.rows.get(*data_idx).map(|r| r.as_slice())
986 }
987
988 fn rebuild_view(&mut self) {
990 let mut indices: Vec<usize> = (0..self.rows.len()).collect();
991
992 if !self.filter_tokens.is_empty() {
993 indices.retain(|&idx| {
994 let searchable = match self.row_search_cache.get(idx) {
995 Some(row) => row,
996 None => return false,
997 };
998 self.filter_tokens
999 .iter()
1000 .all(|token| searchable.contains(token.as_str()))
1001 });
1002 }
1003
1004 if let Some(column) = self.sort_column {
1005 indices.sort_by(|a, b| {
1006 let left = self
1007 .rows
1008 .get(*a)
1009 .and_then(|row| row.get(column))
1010 .map(String::as_str)
1011 .unwrap_or("");
1012 let right = self
1013 .rows
1014 .get(*b)
1015 .and_then(|row| row.get(column))
1016 .map(String::as_str)
1017 .unwrap_or("");
1018
1019 match (left.parse::<f64>(), right.parse::<f64>()) {
1020 (Ok(l), Ok(r)) => l.partial_cmp(&r).unwrap_or(std::cmp::Ordering::Equal),
1021 _ => left
1022 .chars()
1023 .flat_map(char::to_lowercase)
1024 .cmp(right.chars().flat_map(char::to_lowercase)),
1025 }
1026 });
1027
1028 if !self.sort_ascending {
1029 indices.reverse();
1030 }
1031 }
1032
1033 self.view_indices = indices;
1034
1035 if self.page_size > 0 {
1036 self.page = self.page.min(self.total_pages().saturating_sub(1));
1037 } else {
1038 self.page = 0;
1039 }
1040
1041 self.selected = self.selected.min(self.view_indices.len().saturating_sub(1));
1042 self.widths_dirty = true;
1043 }
1044
1045 fn rebuild_row_search_cache(&mut self) {
1046 self.row_search_cache = self
1047 .rows
1048 .iter()
1049 .map(|row| {
1050 let mut searchable = String::new();
1051 for (idx, cell) in row.iter().enumerate() {
1052 if idx > 0 {
1053 searchable.push('\n');
1054 }
1055 searchable.extend(cell.chars().flat_map(char::to_lowercase));
1056 }
1057 searchable
1058 })
1059 .collect();
1060 self.filter_tokens = Self::tokenize_filter(&self.filter);
1061 self.widths_dirty = true;
1062 }
1063
1064 fn tokenize_filter(filter: &str) -> Vec<String> {
1065 filter
1066 .split_whitespace()
1067 .map(|t| t.to_lowercase())
1068 .collect()
1069 }
1070
1071 pub(crate) fn recompute_widths(&mut self) {
1072 let col_count = self.headers.len();
1073 self.column_widths = vec![0u32; col_count];
1074 for (i, header) in self.headers.iter().enumerate() {
1075 let mut width = UnicodeWidthStr::width(header.as_str()) as u32;
1076 if self.sort_column == Some(i) {
1077 width += 2;
1078 }
1079 self.column_widths[i] = width;
1080 }
1081 for row in &self.rows {
1082 for (i, cell) in row.iter().enumerate() {
1083 if i < col_count {
1084 let w = UnicodeWidthStr::width(cell.as_str()) as u32;
1085 self.column_widths[i] = self.column_widths[i].max(w);
1086 }
1087 }
1088 }
1089 self.widths_dirty = false;
1090 }
1091
1092 pub(crate) fn column_widths(&self) -> &[u32] {
1093 &self.column_widths
1094 }
1095
1096 pub(crate) fn is_dirty(&self) -> bool {
1097 self.widths_dirty
1098 }
1099}
1100
1101#[derive(Debug, Clone)]
1107pub struct ScrollState {
1108 pub offset: usize,
1110 content_height: u32,
1111 viewport_height: u32,
1112}
1113
1114impl ScrollState {
1115 pub fn new() -> Self {
1117 Self {
1118 offset: 0,
1119 content_height: 0,
1120 viewport_height: 0,
1121 }
1122 }
1123
1124 pub fn can_scroll_up(&self) -> bool {
1126 self.offset > 0
1127 }
1128
1129 pub fn can_scroll_down(&self) -> bool {
1131 (self.offset as u32) + self.viewport_height < self.content_height
1132 }
1133
1134 pub fn content_height(&self) -> u32 {
1136 self.content_height
1137 }
1138
1139 pub fn viewport_height(&self) -> u32 {
1141 self.viewport_height
1142 }
1143
1144 pub fn progress(&self) -> f32 {
1146 let max = self.content_height.saturating_sub(self.viewport_height);
1147 if max == 0 {
1148 0.0
1149 } else {
1150 self.offset as f32 / max as f32
1151 }
1152 }
1153
1154 pub fn scroll_up(&mut self, amount: usize) {
1156 self.offset = self.offset.saturating_sub(amount);
1157 }
1158
1159 pub fn scroll_down(&mut self, amount: usize) {
1161 let max_offset = self.content_height.saturating_sub(self.viewport_height) as usize;
1162 self.offset = (self.offset + amount).min(max_offset);
1163 }
1164
1165 pub(crate) fn set_bounds(&mut self, content_height: u32, viewport_height: u32) {
1166 self.content_height = content_height;
1167 self.viewport_height = viewport_height;
1168 }
1169}
1170
1171impl Default for ScrollState {
1172 fn default() -> Self {
1173 Self::new()
1174 }
1175}
1176
1177#[derive(Debug, Clone)]
1179pub struct RichLogState {
1180 pub entries: Vec<RichLogEntry>,
1182 pub(crate) scroll_offset: usize,
1184 pub auto_scroll: bool,
1186 pub max_entries: Option<usize>,
1188}
1189
1190#[derive(Debug, Clone)]
1192pub struct RichLogEntry {
1193 pub segments: Vec<(String, Style)>,
1195}
1196
1197impl RichLogState {
1198 pub fn new() -> Self {
1200 Self {
1201 entries: Vec::new(),
1202 scroll_offset: 0,
1203 auto_scroll: true,
1204 max_entries: None,
1205 }
1206 }
1207
1208 pub fn push(&mut self, text: impl Into<String>, style: Style) {
1210 self.push_segments(vec![(text.into(), style)]);
1211 }
1212
1213 pub fn push_plain(&mut self, text: impl Into<String>) {
1215 self.push(text, Style::new());
1216 }
1217
1218 pub fn push_segments(&mut self, segments: Vec<(String, Style)>) {
1220 self.entries.push(RichLogEntry { segments });
1221
1222 if let Some(max_entries) = self.max_entries {
1223 if self.entries.len() > max_entries {
1224 let remove_count = self.entries.len() - max_entries;
1225 self.entries.drain(0..remove_count);
1226 self.scroll_offset = self.scroll_offset.saturating_sub(remove_count);
1227 }
1228 }
1229
1230 if self.auto_scroll {
1231 self.scroll_offset = usize::MAX;
1232 }
1233 }
1234
1235 pub fn clear(&mut self) {
1237 self.entries.clear();
1238 self.scroll_offset = 0;
1239 }
1240
1241 pub fn len(&self) -> usize {
1243 self.entries.len()
1244 }
1245
1246 pub fn is_empty(&self) -> bool {
1248 self.entries.is_empty()
1249 }
1250}
1251
1252impl Default for RichLogState {
1253 fn default() -> Self {
1254 Self::new()
1255 }
1256}
1257
1258#[derive(Debug, Clone)]
1260pub struct CalendarState {
1261 pub year: i32,
1263 pub month: u32,
1265 pub selected_day: Option<u32>,
1267 pub(crate) cursor_day: u32,
1268}
1269
1270impl CalendarState {
1271 pub fn new() -> Self {
1273 let (year, month) = Self::current_year_month();
1274 Self::from_ym(year, month)
1275 }
1276
1277 pub fn from_ym(year: i32, month: u32) -> Self {
1279 let month = month.clamp(1, 12);
1280 Self {
1281 year,
1282 month,
1283 selected_day: None,
1284 cursor_day: 1,
1285 }
1286 }
1287
1288 pub fn selected_date(&self) -> Option<(i32, u32, u32)> {
1290 self.selected_day.map(|day| (self.year, self.month, day))
1291 }
1292
1293 pub fn prev_month(&mut self) {
1295 if self.month == 1 {
1296 self.month = 12;
1297 self.year -= 1;
1298 } else {
1299 self.month -= 1;
1300 }
1301 self.clamp_days();
1302 }
1303
1304 pub fn next_month(&mut self) {
1306 if self.month == 12 {
1307 self.month = 1;
1308 self.year += 1;
1309 } else {
1310 self.month += 1;
1311 }
1312 self.clamp_days();
1313 }
1314
1315 pub(crate) fn days_in_month(year: i32, month: u32) -> u32 {
1316 match month {
1317 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
1318 4 | 6 | 9 | 11 => 30,
1319 2 => {
1320 if Self::is_leap_year(year) {
1321 29
1322 } else {
1323 28
1324 }
1325 }
1326 _ => 30,
1327 }
1328 }
1329
1330 pub(crate) fn first_weekday(year: i32, month: u32) -> u32 {
1331 let month = month.clamp(1, 12);
1332 let offsets = [0_i32, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4];
1333 let mut y = year;
1334 if month < 3 {
1335 y -= 1;
1336 }
1337 let sunday_based = (y + y / 4 - y / 100 + y / 400 + offsets[(month - 1) as usize] + 1) % 7;
1338 ((sunday_based + 6) % 7) as u32
1339 }
1340
1341 fn clamp_days(&mut self) {
1342 let max_day = Self::days_in_month(self.year, self.month);
1343 self.cursor_day = self.cursor_day.clamp(1, max_day);
1344 if let Some(day) = self.selected_day {
1345 self.selected_day = Some(day.min(max_day));
1346 }
1347 }
1348
1349 fn is_leap_year(year: i32) -> bool {
1350 (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
1351 }
1352
1353 fn current_year_month() -> (i32, u32) {
1354 let Ok(duration) = SystemTime::now().duration_since(UNIX_EPOCH) else {
1355 return (1970, 1);
1356 };
1357 let days_since_epoch = (duration.as_secs() / 86_400) as i64;
1358 let (year, month, _) = Self::civil_from_days(days_since_epoch);
1359 (year, month)
1360 }
1361
1362 fn civil_from_days(days_since_epoch: i64) -> (i32, u32, u32) {
1363 let z = days_since_epoch + 719_468;
1364 let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
1365 let doe = z - era * 146_097;
1366 let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
1367 let mut year = (yoe as i32) + (era as i32) * 400;
1368 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
1369 let mp = (5 * doy + 2) / 153;
1370 let day = (doy - (153 * mp + 2) / 5 + 1) as u32;
1371 let month = (mp + if mp < 10 { 3 } else { -9 }) as u32;
1372 if month <= 2 {
1373 year += 1;
1374 }
1375 (year, month, day)
1376 }
1377}
1378
1379impl Default for CalendarState {
1380 fn default() -> Self {
1381 Self::new()
1382 }
1383}
1384
1385#[non_exhaustive]
1395#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
1396pub enum ButtonVariant {
1397 #[default]
1399 Default,
1400 Primary,
1402 Danger,
1404 Outline,
1406}
1407
1408#[non_exhaustive]
1410#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1411pub enum Trend {
1412 Up,
1414 Down,
1416}
1417
1418#[derive(Debug, Clone, Default)]
1425pub struct SelectState {
1426 pub items: Vec<String>,
1428 pub selected: usize,
1430 pub open: bool,
1432 pub placeholder: String,
1434 cursor: usize,
1435}
1436
1437impl SelectState {
1438 pub fn new(items: Vec<impl Into<String>>) -> Self {
1440 Self {
1441 items: items.into_iter().map(Into::into).collect(),
1442 selected: 0,
1443 open: false,
1444 placeholder: String::new(),
1445 cursor: 0,
1446 }
1447 }
1448
1449 pub fn placeholder(mut self, p: impl Into<String>) -> Self {
1451 self.placeholder = p.into();
1452 self
1453 }
1454
1455 pub fn selected_item(&self) -> Option<&str> {
1457 self.items.get(self.selected).map(String::as_str)
1458 }
1459
1460 pub(crate) fn cursor(&self) -> usize {
1461 self.cursor
1462 }
1463
1464 pub(crate) fn set_cursor(&mut self, c: usize) {
1465 self.cursor = c;
1466 }
1467}
1468
1469#[derive(Debug, Clone, Default)]
1475pub struct RadioState {
1476 pub items: Vec<String>,
1478 pub selected: usize,
1480}
1481
1482impl RadioState {
1483 pub fn new(items: Vec<impl Into<String>>) -> Self {
1485 Self {
1486 items: items.into_iter().map(Into::into).collect(),
1487 selected: 0,
1488 }
1489 }
1490
1491 pub fn selected_item(&self) -> Option<&str> {
1493 self.items.get(self.selected).map(String::as_str)
1494 }
1495}
1496
1497#[derive(Debug, Clone)]
1503pub struct MultiSelectState {
1504 pub items: Vec<String>,
1506 pub cursor: usize,
1508 pub selected: HashSet<usize>,
1510}
1511
1512impl MultiSelectState {
1513 pub fn new(items: Vec<impl Into<String>>) -> Self {
1515 Self {
1516 items: items.into_iter().map(Into::into).collect(),
1517 cursor: 0,
1518 selected: HashSet::new(),
1519 }
1520 }
1521
1522 pub fn selected_items(&self) -> Vec<&str> {
1524 let mut indices: Vec<usize> = self.selected.iter().copied().collect();
1525 indices.sort();
1526 indices
1527 .iter()
1528 .filter_map(|&i| self.items.get(i).map(String::as_str))
1529 .collect()
1530 }
1531
1532 pub fn toggle(&mut self, index: usize) {
1534 if self.selected.contains(&index) {
1535 self.selected.remove(&index);
1536 } else {
1537 self.selected.insert(index);
1538 }
1539 }
1540}
1541
1542#[derive(Debug, Clone)]
1546pub struct TreeNode {
1547 pub label: String,
1549 pub children: Vec<TreeNode>,
1551 pub expanded: bool,
1553}
1554
1555impl TreeNode {
1556 pub fn new(label: impl Into<String>) -> Self {
1558 Self {
1559 label: label.into(),
1560 children: Vec::new(),
1561 expanded: false,
1562 }
1563 }
1564
1565 pub fn expanded(mut self) -> Self {
1567 self.expanded = true;
1568 self
1569 }
1570
1571 pub fn children(mut self, children: Vec<TreeNode>) -> Self {
1573 self.children = children;
1574 self
1575 }
1576
1577 pub fn is_leaf(&self) -> bool {
1579 self.children.is_empty()
1580 }
1581
1582 fn flatten(&self, depth: usize, out: &mut Vec<FlatTreeEntry>) {
1583 out.push(FlatTreeEntry {
1584 depth,
1585 label: self.label.clone(),
1586 is_leaf: self.is_leaf(),
1587 expanded: self.expanded,
1588 });
1589 if self.expanded {
1590 for child in &self.children {
1591 child.flatten(depth + 1, out);
1592 }
1593 }
1594 }
1595}
1596
1597pub(crate) struct FlatTreeEntry {
1598 pub depth: usize,
1599 pub label: String,
1600 pub is_leaf: bool,
1601 pub expanded: bool,
1602}
1603
1604#[derive(Debug, Clone)]
1606pub struct TreeState {
1607 pub nodes: Vec<TreeNode>,
1609 pub selected: usize,
1611}
1612
1613impl TreeState {
1614 pub fn new(nodes: Vec<TreeNode>) -> Self {
1616 Self { nodes, selected: 0 }
1617 }
1618
1619 pub(crate) fn flatten(&self) -> Vec<FlatTreeEntry> {
1620 let mut entries = Vec::new();
1621 for node in &self.nodes {
1622 node.flatten(0, &mut entries);
1623 }
1624 entries
1625 }
1626
1627 pub(crate) fn toggle_at(&mut self, flat_index: usize) {
1628 let mut counter = 0usize;
1629 Self::toggle_recursive(&mut self.nodes, flat_index, &mut counter);
1630 }
1631
1632 fn toggle_recursive(nodes: &mut [TreeNode], target: usize, counter: &mut usize) -> bool {
1633 for node in nodes.iter_mut() {
1634 if *counter == target {
1635 if !node.is_leaf() {
1636 node.expanded = !node.expanded;
1637 }
1638 return true;
1639 }
1640 *counter += 1;
1641 if node.expanded && Self::toggle_recursive(&mut node.children, target, counter) {
1642 return true;
1643 }
1644 }
1645 false
1646 }
1647}
1648
1649#[derive(Debug, Clone)]
1651pub struct DirectoryTreeState {
1652 pub tree: TreeState,
1654 pub show_icons: bool,
1656}
1657
1658impl DirectoryTreeState {
1659 pub fn new(nodes: Vec<TreeNode>) -> Self {
1661 Self {
1662 tree: TreeState::new(nodes),
1663 show_icons: true,
1664 }
1665 }
1666
1667 pub fn from_paths(paths: &[&str]) -> Self {
1669 let mut roots: Vec<TreeNode> = Vec::new();
1670
1671 for raw_path in paths {
1672 let parts: Vec<&str> = raw_path
1673 .split('/')
1674 .filter(|part| !part.is_empty())
1675 .collect();
1676 if parts.is_empty() {
1677 continue;
1678 }
1679 insert_path(&mut roots, &parts, 0);
1680 }
1681
1682 Self::new(roots)
1683 }
1684
1685 pub fn selected_label(&self) -> Option<&str> {
1687 let mut cursor = 0usize;
1688 selected_label_in_nodes(&self.tree.nodes, self.tree.selected, &mut cursor)
1689 }
1690}
1691
1692impl Default for DirectoryTreeState {
1693 fn default() -> Self {
1694 Self::new(Vec::<TreeNode>::new())
1695 }
1696}
1697
1698fn insert_path(nodes: &mut Vec<TreeNode>, parts: &[&str], depth: usize) {
1699 let Some(label) = parts.get(depth) else {
1700 return;
1701 };
1702
1703 let is_last = depth + 1 == parts.len();
1704 let idx = nodes
1705 .iter()
1706 .position(|node| node.label == *label)
1707 .unwrap_or_else(|| {
1708 let mut node = TreeNode::new(*label);
1709 if !is_last {
1710 node.expanded = true;
1711 }
1712 nodes.push(node);
1713 nodes.len() - 1
1714 });
1715
1716 if is_last {
1717 return;
1718 }
1719
1720 nodes[idx].expanded = true;
1721 insert_path(&mut nodes[idx].children, parts, depth + 1);
1722}
1723
1724fn selected_label_in_nodes<'a>(
1725 nodes: &'a [TreeNode],
1726 target: usize,
1727 cursor: &mut usize,
1728) -> Option<&'a str> {
1729 for node in nodes {
1730 if *cursor == target {
1731 return Some(node.label.as_str());
1732 }
1733 *cursor += 1;
1734 if node.expanded {
1735 if let Some(found) = selected_label_in_nodes(&node.children, target, cursor) {
1736 return Some(found);
1737 }
1738 }
1739 }
1740 None
1741}
1742
1743#[derive(Debug, Clone)]
1747pub struct PaletteCommand {
1748 pub label: String,
1750 pub description: String,
1752 pub shortcut: Option<String>,
1754}
1755
1756impl PaletteCommand {
1757 pub fn new(label: impl Into<String>, description: impl Into<String>) -> Self {
1759 Self {
1760 label: label.into(),
1761 description: description.into(),
1762 shortcut: None,
1763 }
1764 }
1765
1766 pub fn shortcut(mut self, s: impl Into<String>) -> Self {
1768 self.shortcut = Some(s.into());
1769 self
1770 }
1771}
1772
1773#[derive(Debug, Clone)]
1777pub struct CommandPaletteState {
1778 pub commands: Vec<PaletteCommand>,
1780 pub input: String,
1782 pub cursor: usize,
1784 pub open: bool,
1786 pub last_selected: Option<usize>,
1789 selected: usize,
1790}
1791
1792impl CommandPaletteState {
1793 pub fn new(commands: Vec<PaletteCommand>) -> Self {
1795 Self {
1796 commands,
1797 input: String::new(),
1798 cursor: 0,
1799 open: false,
1800 last_selected: None,
1801 selected: 0,
1802 }
1803 }
1804
1805 pub fn toggle(&mut self) {
1807 self.open = !self.open;
1808 if self.open {
1809 self.input.clear();
1810 self.cursor = 0;
1811 self.selected = 0;
1812 }
1813 }
1814
1815 fn fuzzy_score(pattern: &str, text: &str) -> Option<i32> {
1816 let pattern = pattern.trim();
1817 if pattern.is_empty() {
1818 return Some(0);
1819 }
1820
1821 let text_chars: Vec<char> = text.chars().collect();
1822 let mut score = 0;
1823 let mut search_start = 0usize;
1824 let mut prev_match: Option<usize> = None;
1825
1826 for p in pattern.chars() {
1827 let mut found = None;
1828 for (idx, ch) in text_chars.iter().enumerate().skip(search_start) {
1829 if ch.eq_ignore_ascii_case(&p) {
1830 found = Some(idx);
1831 break;
1832 }
1833 }
1834
1835 let idx = found?;
1836 if prev_match.is_some_and(|prev| idx == prev + 1) {
1837 score += 3;
1838 } else {
1839 score += 1;
1840 }
1841
1842 if idx == 0 {
1843 score += 2;
1844 } else {
1845 let prev = text_chars[idx - 1];
1846 let curr = text_chars[idx];
1847 if matches!(prev, ' ' | '_' | '-') || prev.is_uppercase() || curr.is_uppercase() {
1848 score += 2;
1849 }
1850 }
1851
1852 prev_match = Some(idx);
1853 search_start = idx + 1;
1854 }
1855
1856 Some(score)
1857 }
1858
1859 pub(crate) fn filtered_indices(&self) -> Vec<usize> {
1860 let query = self.input.trim();
1861 if query.is_empty() {
1862 return (0..self.commands.len()).collect();
1863 }
1864
1865 let mut scored: Vec<(usize, i32)> = self
1866 .commands
1867 .iter()
1868 .enumerate()
1869 .filter_map(|(i, cmd)| {
1870 let mut haystack =
1871 String::with_capacity(cmd.label.len() + cmd.description.len() + 1);
1872 haystack.push_str(&cmd.label);
1873 haystack.push(' ');
1874 haystack.push_str(&cmd.description);
1875 Self::fuzzy_score(query, &haystack).map(|score| (i, score))
1876 })
1877 .collect();
1878
1879 if scored.is_empty() {
1880 let tokens: Vec<String> = query.split_whitespace().map(|t| t.to_lowercase()).collect();
1881 return self
1882 .commands
1883 .iter()
1884 .enumerate()
1885 .filter(|(_, cmd)| {
1886 let label = cmd.label.to_lowercase();
1887 let desc = cmd.description.to_lowercase();
1888 tokens.iter().all(|token| {
1889 label.contains(token.as_str()) || desc.contains(token.as_str())
1890 })
1891 })
1892 .map(|(i, _)| i)
1893 .collect();
1894 }
1895
1896 scored.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
1897 scored.into_iter().map(|(idx, _)| idx).collect()
1898 }
1899
1900 pub(crate) fn selected(&self) -> usize {
1901 self.selected
1902 }
1903
1904 pub(crate) fn set_selected(&mut self, s: usize) {
1905 self.selected = s;
1906 }
1907}
1908
1909#[derive(Debug, Clone)]
1914pub struct StreamingTextState {
1915 pub content: String,
1917 pub streaming: bool,
1919 pub(crate) cursor_visible: bool,
1921 pub(crate) cursor_tick: u64,
1922}
1923
1924impl StreamingTextState {
1925 pub fn new() -> Self {
1927 Self {
1928 content: String::new(),
1929 streaming: false,
1930 cursor_visible: true,
1931 cursor_tick: 0,
1932 }
1933 }
1934
1935 pub fn push(&mut self, chunk: &str) {
1937 self.content.push_str(chunk);
1938 }
1939
1940 pub fn finish(&mut self) {
1942 self.streaming = false;
1943 }
1944
1945 pub fn start(&mut self) {
1947 self.content.clear();
1948 self.streaming = true;
1949 self.cursor_visible = true;
1950 self.cursor_tick = 0;
1951 }
1952
1953 pub fn clear(&mut self) {
1955 self.content.clear();
1956 self.streaming = false;
1957 self.cursor_visible = true;
1958 self.cursor_tick = 0;
1959 }
1960}
1961
1962impl Default for StreamingTextState {
1963 fn default() -> Self {
1964 Self::new()
1965 }
1966}
1967
1968#[derive(Debug, Clone)]
1973pub struct StreamingMarkdownState {
1974 pub content: String,
1976 pub streaming: bool,
1978 pub cursor_visible: bool,
1980 pub cursor_tick: u64,
1982 pub in_code_block: bool,
1984 pub code_block_lang: String,
1986}
1987
1988impl StreamingMarkdownState {
1989 pub fn new() -> Self {
1991 Self {
1992 content: String::new(),
1993 streaming: false,
1994 cursor_visible: true,
1995 cursor_tick: 0,
1996 in_code_block: false,
1997 code_block_lang: String::new(),
1998 }
1999 }
2000
2001 pub fn push(&mut self, chunk: &str) {
2003 self.content.push_str(chunk);
2004 }
2005
2006 pub fn start(&mut self) {
2008 self.content.clear();
2009 self.streaming = true;
2010 self.cursor_visible = true;
2011 self.cursor_tick = 0;
2012 self.in_code_block = false;
2013 self.code_block_lang.clear();
2014 }
2015
2016 pub fn finish(&mut self) {
2018 self.streaming = false;
2019 }
2020
2021 pub fn clear(&mut self) {
2023 self.content.clear();
2024 self.streaming = false;
2025 self.cursor_visible = true;
2026 self.cursor_tick = 0;
2027 self.in_code_block = false;
2028 self.code_block_lang.clear();
2029 }
2030}
2031
2032impl Default for StreamingMarkdownState {
2033 fn default() -> Self {
2034 Self::new()
2035 }
2036}
2037
2038#[derive(Debug, Clone)]
2043pub struct ScreenState {
2044 stack: Vec<String>,
2045}
2046
2047impl ScreenState {
2048 pub fn new(initial: impl Into<String>) -> Self {
2050 Self {
2051 stack: vec![initial.into()],
2052 }
2053 }
2054
2055 pub fn current(&self) -> &str {
2057 self.stack
2058 .last()
2059 .expect("ScreenState always contains at least one screen")
2060 .as_str()
2061 }
2062
2063 pub fn push(&mut self, name: impl Into<String>) {
2065 self.stack.push(name.into());
2066 }
2067
2068 pub fn pop(&mut self) {
2070 if self.can_pop() {
2071 self.stack.pop();
2072 }
2073 }
2074
2075 pub fn depth(&self) -> usize {
2077 self.stack.len()
2078 }
2079
2080 pub fn can_pop(&self) -> bool {
2082 self.stack.len() > 1
2083 }
2084
2085 pub fn reset(&mut self) {
2087 self.stack.truncate(1);
2088 }
2089}
2090
2091#[non_exhaustive]
2093#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2094pub enum ApprovalAction {
2095 Pending,
2097 Approved,
2099 Rejected,
2101}
2102
2103#[derive(Debug, Clone)]
2109pub struct ToolApprovalState {
2110 pub tool_name: String,
2112 pub description: String,
2114 pub action: ApprovalAction,
2116}
2117
2118impl ToolApprovalState {
2119 pub fn new(tool_name: impl Into<String>, description: impl Into<String>) -> Self {
2121 Self {
2122 tool_name: tool_name.into(),
2123 description: description.into(),
2124 action: ApprovalAction::Pending,
2125 }
2126 }
2127
2128 pub fn reset(&mut self) {
2130 self.action = ApprovalAction::Pending;
2131 }
2132}
2133
2134#[derive(Debug, Clone)]
2136pub struct ContextItem {
2137 pub label: String,
2139 pub tokens: usize,
2141}
2142
2143impl ContextItem {
2144 pub fn new(label: impl Into<String>, tokens: usize) -> Self {
2146 Self {
2147 label: label.into(),
2148 tokens,
2149 }
2150 }
2151}
2152
2153#[cfg(test)]
2154mod tests {
2155 use super::*;
2156
2157 #[test]
2158 fn static_output_accumulates_and_drains_new_lines() {
2159 let mut output = StaticOutput::new();
2160 output.println("Building crate...");
2161 output.println("Compiling foo v0.1.0");
2162
2163 assert_eq!(
2164 output.lines(),
2165 &[
2166 "Building crate...".to_string(),
2167 "Compiling foo v0.1.0".to_string()
2168 ]
2169 );
2170
2171 let first = output.drain_new();
2172 assert_eq!(
2173 first,
2174 vec![
2175 "Building crate...".to_string(),
2176 "Compiling foo v0.1.0".to_string()
2177 ]
2178 );
2179 assert!(output.drain_new().is_empty());
2180
2181 output.println("Finished");
2182 assert_eq!(output.drain_new(), vec!["Finished".to_string()]);
2183 }
2184
2185 #[test]
2186 fn static_output_clear_resets_all_buffers() {
2187 let mut output = StaticOutput::new();
2188 output.println("line");
2189 output.clear();
2190
2191 assert!(output.lines().is_empty());
2192 assert!(output.drain_new().is_empty());
2193 }
2194
2195 #[test]
2196 fn form_field_default_values() {
2197 let field = FormField::default();
2198 assert_eq!(field.label, "");
2199 assert_eq!(field.input.value, "");
2200 assert_eq!(field.input.cursor, 0);
2201 assert_eq!(field.error, None);
2202 }
2203
2204 #[test]
2205 fn toast_message_default_values() {
2206 let msg = ToastMessage::default();
2207 assert_eq!(msg.text, "");
2208 assert!(matches!(msg.level, ToastLevel::Info));
2209 assert_eq!(msg.created_tick, 0);
2210 assert_eq!(msg.duration_ticks, 30);
2211 }
2212
2213 #[test]
2214 fn list_state_default_values() {
2215 let state = ListState::default();
2216 assert!(state.items.is_empty());
2217 assert_eq!(state.selected, 0);
2218 assert_eq!(state.filter, "");
2219 assert!(state.visible_indices().is_empty());
2220 assert_eq!(state.selected_item(), None);
2221 }
2222
2223 #[test]
2224 fn file_entry_default_values() {
2225 let entry = FileEntry::default();
2226 assert_eq!(entry.name, "");
2227 assert_eq!(entry.path, PathBuf::new());
2228 assert!(!entry.is_dir);
2229 assert_eq!(entry.size, 0);
2230 }
2231
2232 #[test]
2233 fn tabs_state_default_values() {
2234 let state = TabsState::default();
2235 assert!(state.labels.is_empty());
2236 assert_eq!(state.selected, 0);
2237 assert_eq!(state.selected_label(), None);
2238 }
2239
2240 #[test]
2241 fn table_state_default_values() {
2242 let state = TableState::default();
2243 assert!(state.headers.is_empty());
2244 assert!(state.rows.is_empty());
2245 assert_eq!(state.selected, 0);
2246 assert_eq!(state.sort_column, None);
2247 assert!(state.sort_ascending);
2248 assert_eq!(state.filter, "");
2249 assert_eq!(state.page, 0);
2250 assert_eq!(state.page_size, 0);
2251 assert!(!state.zebra);
2252 assert!(state.visible_indices().is_empty());
2253 assert!(state.row_search_cache.is_empty());
2254 assert!(state.filter_tokens.is_empty());
2255 }
2256
2257 #[test]
2258 fn table_state_builds_lowercase_search_cache() {
2259 let state = TableState::new(vec!["Name"], vec![vec!["Alice Smith"], vec!["Bob"]]);
2260
2261 assert_eq!(state.row_search_cache.len(), 2);
2262 assert_eq!(state.row_search_cache[0], "alice smith");
2263 assert_eq!(state.row_search_cache[1], "bob");
2264 }
2265
2266 #[test]
2267 fn table_filter_tokens_are_cached_and_reused() {
2268 let mut state = TableState::new(
2269 vec!["Name", "Role"],
2270 vec![vec!["Alice", "Admin"], vec!["Bob", "Viewer"]],
2271 );
2272
2273 state.set_filter("AL admin");
2274 assert_eq!(
2275 state.filter_tokens,
2276 vec!["al".to_string(), "admin".to_string()]
2277 );
2278 assert_eq!(state.visible_indices(), &[0]);
2279
2280 state.set_filter("AL admin");
2281 assert_eq!(
2282 state.filter_tokens,
2283 vec!["al".to_string(), "admin".to_string()]
2284 );
2285 assert_eq!(state.visible_indices(), &[0]);
2286 }
2287
2288 #[test]
2289 fn select_state_default_values() {
2290 let state = SelectState::default();
2291 assert!(state.items.is_empty());
2292 assert_eq!(state.selected, 0);
2293 assert!(!state.open);
2294 assert_eq!(state.placeholder, "");
2295 assert_eq!(state.selected_item(), None);
2296 assert_eq!(state.cursor(), 0);
2297 }
2298
2299 #[test]
2300 fn radio_state_default_values() {
2301 let state = RadioState::default();
2302 assert!(state.items.is_empty());
2303 assert_eq!(state.selected, 0);
2304 assert_eq!(state.selected_item(), None);
2305 }
2306
2307 #[test]
2308 fn text_input_state_default_uses_new() {
2309 let state = TextInputState::default();
2310 assert_eq!(state.value, "");
2311 assert_eq!(state.cursor, 0);
2312 assert_eq!(state.placeholder, "");
2313 assert_eq!(state.max_length, None);
2314 assert_eq!(state.validation_error, None);
2315 assert!(!state.masked);
2316 }
2317
2318 #[test]
2319 fn tabs_state_new_sets_labels() {
2320 let state = TabsState::new(vec!["a", "b"]);
2321 assert_eq!(state.labels, vec!["a".to_string(), "b".to_string()]);
2322 assert_eq!(state.selected, 0);
2323 assert_eq!(state.selected_label(), Some("a"));
2324 }
2325
2326 #[test]
2327 fn list_state_new_selected_item_points_to_first_item() {
2328 let state = ListState::new(vec!["alpha", "beta"]);
2329 assert_eq!(state.items, vec!["alpha".to_string(), "beta".to_string()]);
2330 assert_eq!(state.selected, 0);
2331 assert_eq!(state.visible_indices(), &[0, 1]);
2332 assert_eq!(state.selected_item(), Some("alpha"));
2333 }
2334
2335 #[test]
2336 fn select_state_placeholder_builder_sets_value() {
2337 let state = SelectState::new(vec!["one", "two"]).placeholder("Pick one");
2338 assert_eq!(state.items, vec!["one".to_string(), "two".to_string()]);
2339 assert_eq!(state.placeholder, "Pick one");
2340 assert_eq!(state.selected_item(), Some("one"));
2341 }
2342
2343 #[test]
2344 fn radio_state_new_sets_items_and_selection() {
2345 let state = RadioState::new(vec!["red", "green"]);
2346 assert_eq!(state.items, vec!["red".to_string(), "green".to_string()]);
2347 assert_eq!(state.selected, 0);
2348 assert_eq!(state.selected_item(), Some("red"));
2349 }
2350
2351 #[test]
2352 fn table_state_new_sets_sort_ascending_true() {
2353 let state = TableState::new(vec!["Name"], vec![vec!["Alice"], vec!["Bob"]]);
2354 assert_eq!(state.headers, vec!["Name".to_string()]);
2355 assert_eq!(state.rows.len(), 2);
2356 assert!(state.sort_ascending);
2357 assert_eq!(state.sort_column, None);
2358 assert!(!state.zebra);
2359 assert_eq!(state.visible_indices(), &[0, 1]);
2360 }
2361
2362 #[test]
2363 fn command_palette_fuzzy_score_matches_gapped_pattern() {
2364 assert!(CommandPaletteState::fuzzy_score("sf", "Save File").is_some());
2365 assert!(CommandPaletteState::fuzzy_score("cmd", "Command Palette").is_some());
2366 assert_eq!(CommandPaletteState::fuzzy_score("xyz", "Save File"), None);
2367 }
2368
2369 #[test]
2370 fn command_palette_filtered_indices_uses_fuzzy_and_sorts() {
2371 let mut state = CommandPaletteState::new(vec![
2372 PaletteCommand::new("Save File", "Write buffer"),
2373 PaletteCommand::new("Search Files", "Find in workspace"),
2374 PaletteCommand::new("Quit", "Exit app"),
2375 ]);
2376
2377 state.input = "sf".to_string();
2378 let filtered = state.filtered_indices();
2379 assert_eq!(filtered, vec![0, 1]);
2380
2381 state.input = "buffer".to_string();
2382 let filtered = state.filtered_indices();
2383 assert_eq!(filtered, vec![0]);
2384 }
2385
2386 #[test]
2387 fn screen_state_push_pop_tracks_current_screen() {
2388 let mut screens = ScreenState::new("home");
2389 assert_eq!(screens.current(), "home");
2390 assert_eq!(screens.depth(), 1);
2391 assert!(!screens.can_pop());
2392
2393 screens.push("settings");
2394 assert_eq!(screens.current(), "settings");
2395 assert_eq!(screens.depth(), 2);
2396 assert!(screens.can_pop());
2397
2398 screens.push("profile");
2399 assert_eq!(screens.current(), "profile");
2400 assert_eq!(screens.depth(), 3);
2401
2402 screens.pop();
2403 assert_eq!(screens.current(), "settings");
2404 assert_eq!(screens.depth(), 2);
2405 }
2406
2407 #[test]
2408 fn screen_state_pop_never_removes_root() {
2409 let mut screens = ScreenState::new("home");
2410 screens.push("settings");
2411 screens.pop();
2412 screens.pop();
2413
2414 assert_eq!(screens.current(), "home");
2415 assert_eq!(screens.depth(), 1);
2416 assert!(!screens.can_pop());
2417 }
2418
2419 #[test]
2420 fn screen_state_reset_keeps_only_root() {
2421 let mut screens = ScreenState::new("home");
2422 screens.push("settings");
2423 screens.push("profile");
2424 assert_eq!(screens.current(), "profile");
2425
2426 screens.reset();
2427 assert_eq!(screens.current(), "home");
2428 assert_eq!(screens.depth(), 1);
2429 assert!(!screens.can_pop());
2430 }
2431
2432 #[test]
2433 fn calendar_days_in_month_handles_leap_years() {
2434 assert_eq!(CalendarState::days_in_month(2024, 2), 29);
2435 assert_eq!(CalendarState::days_in_month(2023, 2), 28);
2436 assert_eq!(CalendarState::days_in_month(2024, 1), 31);
2437 assert_eq!(CalendarState::days_in_month(2024, 4), 30);
2438 }
2439
2440 #[test]
2441 fn calendar_first_weekday_known_dates() {
2442 assert_eq!(CalendarState::first_weekday(2024, 1), 0);
2443 assert_eq!(CalendarState::first_weekday(2023, 10), 6);
2444 }
2445
2446 #[test]
2447 fn calendar_prev_next_month_handles_year_boundary() {
2448 let mut state = CalendarState::from_ym(2024, 12);
2449 state.prev_month();
2450 assert_eq!((state.year, state.month), (2024, 11));
2451
2452 let mut state = CalendarState::from_ym(2024, 1);
2453 state.prev_month();
2454 assert_eq!((state.year, state.month), (2023, 12));
2455
2456 state.next_month();
2457 assert_eq!((state.year, state.month), (2024, 1));
2458 }
2459}