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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
366pub enum AlertLevel {
367 Info,
369 Success,
371 Warning,
373 Error,
375}
376
377impl ToastState {
378 pub fn new() -> Self {
380 Self {
381 messages: Vec::new(),
382 }
383 }
384
385 pub fn info(&mut self, text: impl Into<String>, tick: u64) {
387 self.push(text, ToastLevel::Info, tick, 30);
388 }
389
390 pub fn success(&mut self, text: impl Into<String>, tick: u64) {
392 self.push(text, ToastLevel::Success, tick, 30);
393 }
394
395 pub fn warning(&mut self, text: impl Into<String>, tick: u64) {
397 self.push(text, ToastLevel::Warning, tick, 50);
398 }
399
400 pub fn error(&mut self, text: impl Into<String>, tick: u64) {
402 self.push(text, ToastLevel::Error, tick, 80);
403 }
404
405 pub fn push(
407 &mut self,
408 text: impl Into<String>,
409 level: ToastLevel,
410 tick: u64,
411 duration_ticks: u64,
412 ) {
413 self.messages.push(ToastMessage {
414 text: text.into(),
415 level,
416 created_tick: tick,
417 duration_ticks,
418 });
419 }
420
421 pub fn cleanup(&mut self, current_tick: u64) {
425 self.messages.retain(|message| {
426 current_tick < message.created_tick.saturating_add(message.duration_ticks)
427 });
428 }
429}
430
431impl Default for ToastState {
432 fn default() -> Self {
433 Self::new()
434 }
435}
436
437#[derive(Debug, Clone)]
442pub struct TextareaState {
443 pub lines: Vec<String>,
445 pub cursor_row: usize,
447 pub cursor_col: usize,
449 pub max_length: Option<usize>,
451 pub wrap_width: Option<u32>,
453 pub scroll_offset: usize,
455}
456
457impl TextareaState {
458 pub fn new() -> Self {
460 Self {
461 lines: vec![String::new()],
462 cursor_row: 0,
463 cursor_col: 0,
464 max_length: None,
465 wrap_width: None,
466 scroll_offset: 0,
467 }
468 }
469
470 pub fn value(&self) -> String {
472 self.lines.join("\n")
473 }
474
475 pub fn set_value(&mut self, text: impl Into<String>) {
479 let value = text.into();
480 self.lines = value.split('\n').map(str::to_string).collect();
481 if self.lines.is_empty() {
482 self.lines.push(String::new());
483 }
484 self.cursor_row = 0;
485 self.cursor_col = 0;
486 self.scroll_offset = 0;
487 }
488
489 pub fn max_length(mut self, len: usize) -> Self {
491 self.max_length = Some(len);
492 self
493 }
494
495 pub fn word_wrap(mut self, width: u32) -> Self {
497 self.wrap_width = Some(width);
498 self
499 }
500}
501
502impl Default for TextareaState {
503 fn default() -> Self {
504 Self::new()
505 }
506}
507
508#[derive(Debug, Clone)]
514pub struct SpinnerState {
515 chars: Vec<char>,
516}
517
518impl SpinnerState {
519 pub fn dots() -> Self {
523 Self {
524 chars: vec!['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
525 }
526 }
527
528 pub fn line() -> Self {
532 Self {
533 chars: vec!['|', '/', '-', '\\'],
534 }
535 }
536
537 pub fn frame(&self, tick: u64) -> char {
539 if self.chars.is_empty() {
540 return ' ';
541 }
542 self.chars[tick as usize % self.chars.len()]
543 }
544}
545
546impl Default for SpinnerState {
547 fn default() -> Self {
548 Self::dots()
549 }
550}
551
552#[derive(Debug, Clone, Default)]
557pub struct ListState {
558 pub items: Vec<String>,
560 pub selected: usize,
562 pub filter: String,
564 view_indices: Vec<usize>,
565}
566
567impl ListState {
568 pub fn new(items: Vec<impl Into<String>>) -> Self {
570 let len = items.len();
571 Self {
572 items: items.into_iter().map(Into::into).collect(),
573 selected: 0,
574 filter: String::new(),
575 view_indices: (0..len).collect(),
576 }
577 }
578
579 pub fn set_items(&mut self, items: Vec<impl Into<String>>) {
584 self.items = items.into_iter().map(Into::into).collect();
585 self.selected = self.selected.min(self.items.len().saturating_sub(1));
586 self.rebuild_view();
587 }
588
589 pub fn set_filter(&mut self, filter: impl Into<String>) {
593 self.filter = filter.into();
594 self.rebuild_view();
595 }
596
597 pub fn visible_indices(&self) -> &[usize] {
599 &self.view_indices
600 }
601
602 pub fn selected_item(&self) -> Option<&str> {
604 let data_idx = *self.view_indices.get(self.selected)?;
605 self.items.get(data_idx).map(String::as_str)
606 }
607
608 fn rebuild_view(&mut self) {
609 let tokens: Vec<String> = self
610 .filter
611 .split_whitespace()
612 .map(|t| t.to_lowercase())
613 .collect();
614 self.view_indices = if tokens.is_empty() {
615 (0..self.items.len()).collect()
616 } else {
617 (0..self.items.len())
618 .filter(|&i| {
619 tokens
620 .iter()
621 .all(|token| self.items[i].to_lowercase().contains(token.as_str()))
622 })
623 .collect()
624 };
625 if !self.view_indices.is_empty() && self.selected >= self.view_indices.len() {
626 self.selected = self.view_indices.len() - 1;
627 }
628 }
629}
630
631#[derive(Debug, Clone)]
635pub struct FilePickerState {
636 pub current_dir: PathBuf,
638 pub entries: Vec<FileEntry>,
640 pub selected: usize,
642 pub selected_file: Option<PathBuf>,
644 pub show_hidden: bool,
646 pub extensions: Vec<String>,
648 pub dirty: bool,
650}
651
652#[derive(Debug, Clone, Default)]
654pub struct FileEntry {
655 pub name: String,
657 pub path: PathBuf,
659 pub is_dir: bool,
661 pub size: u64,
663}
664
665impl FilePickerState {
666 pub fn new(dir: impl Into<PathBuf>) -> Self {
668 Self {
669 current_dir: dir.into(),
670 entries: Vec::new(),
671 selected: 0,
672 selected_file: None,
673 show_hidden: false,
674 extensions: Vec::new(),
675 dirty: true,
676 }
677 }
678
679 pub fn show_hidden(mut self, show: bool) -> Self {
681 self.show_hidden = show;
682 self.dirty = true;
683 self
684 }
685
686 pub fn extensions(mut self, exts: &[&str]) -> Self {
688 self.extensions = exts
689 .iter()
690 .map(|ext| ext.trim().trim_start_matches('.').to_ascii_lowercase())
691 .filter(|ext| !ext.is_empty())
692 .collect();
693 self.dirty = true;
694 self
695 }
696
697 pub fn selected(&self) -> Option<&PathBuf> {
699 self.selected_file.as_ref()
700 }
701
702 pub fn refresh(&mut self) {
704 let mut entries = Vec::new();
705
706 if let Ok(read_dir) = fs::read_dir(&self.current_dir) {
707 for dir_entry in read_dir.flatten() {
708 let name = dir_entry.file_name().to_string_lossy().to_string();
709 if !self.show_hidden && name.starts_with('.') {
710 continue;
711 }
712
713 let Ok(file_type) = dir_entry.file_type() else {
714 continue;
715 };
716 if file_type.is_symlink() {
717 continue;
718 }
719
720 let path = dir_entry.path();
721 let is_dir = file_type.is_dir();
722
723 if !is_dir && !self.extensions.is_empty() {
724 let ext = path
725 .extension()
726 .and_then(|e| e.to_str())
727 .map(|e| e.to_ascii_lowercase());
728 let Some(ext) = ext else {
729 continue;
730 };
731 if !self.extensions.iter().any(|allowed| allowed == &ext) {
732 continue;
733 }
734 }
735
736 let size = if is_dir {
737 0
738 } else {
739 fs::symlink_metadata(&path).map(|m| m.len()).unwrap_or(0)
740 };
741
742 entries.push(FileEntry {
743 name,
744 path,
745 is_dir,
746 size,
747 });
748 }
749 }
750
751 entries.sort_by(|a, b| match (a.is_dir, b.is_dir) {
752 (true, false) => std::cmp::Ordering::Less,
753 (false, true) => std::cmp::Ordering::Greater,
754 _ => a
755 .name
756 .to_ascii_lowercase()
757 .cmp(&b.name.to_ascii_lowercase())
758 .then_with(|| a.name.cmp(&b.name)),
759 });
760
761 self.entries = entries;
762 if self.entries.is_empty() {
763 self.selected = 0;
764 } else {
765 self.selected = self.selected.min(self.entries.len().saturating_sub(1));
766 }
767 self.dirty = false;
768 }
769}
770
771impl Default for FilePickerState {
772 fn default() -> Self {
773 Self::new(".")
774 }
775}
776
777#[derive(Debug, Clone, Default)]
782pub struct TabsState {
783 pub labels: Vec<String>,
785 pub selected: usize,
787}
788
789impl TabsState {
790 pub fn new(labels: Vec<impl Into<String>>) -> Self {
792 Self {
793 labels: labels.into_iter().map(Into::into).collect(),
794 selected: 0,
795 }
796 }
797
798 pub fn selected_label(&self) -> Option<&str> {
800 self.labels.get(self.selected).map(String::as_str)
801 }
802}
803
804#[derive(Debug, Clone)]
810pub struct TableState {
811 pub headers: Vec<String>,
813 pub rows: Vec<Vec<String>>,
815 pub selected: usize,
817 column_widths: Vec<u32>,
818 dirty: bool,
819 pub sort_column: Option<usize>,
821 pub sort_ascending: bool,
823 pub filter: String,
825 pub page: usize,
827 pub page_size: usize,
829 pub zebra: bool,
831 view_indices: Vec<usize>,
832}
833
834impl Default for TableState {
835 fn default() -> Self {
836 Self {
837 headers: Vec::new(),
838 rows: Vec::new(),
839 selected: 0,
840 column_widths: Vec::new(),
841 dirty: true,
842 sort_column: None,
843 sort_ascending: true,
844 filter: String::new(),
845 page: 0,
846 page_size: 0,
847 zebra: false,
848 view_indices: Vec::new(),
849 }
850 }
851}
852
853impl TableState {
854 pub fn new(headers: Vec<impl Into<String>>, rows: Vec<Vec<impl Into<String>>>) -> Self {
856 let headers: Vec<String> = headers.into_iter().map(Into::into).collect();
857 let rows: Vec<Vec<String>> = rows
858 .into_iter()
859 .map(|r| r.into_iter().map(Into::into).collect())
860 .collect();
861 let mut state = Self {
862 headers,
863 rows,
864 selected: 0,
865 column_widths: Vec::new(),
866 dirty: true,
867 sort_column: None,
868 sort_ascending: true,
869 filter: String::new(),
870 page: 0,
871 page_size: 0,
872 zebra: false,
873 view_indices: Vec::new(),
874 };
875 state.rebuild_view();
876 state.recompute_widths();
877 state
878 }
879
880 pub fn set_rows(&mut self, rows: Vec<Vec<impl Into<String>>>) {
885 self.rows = rows
886 .into_iter()
887 .map(|r| r.into_iter().map(Into::into).collect())
888 .collect();
889 self.rebuild_view();
890 }
891
892 pub fn toggle_sort(&mut self, column: usize) {
894 if self.sort_column == Some(column) {
895 self.sort_ascending = !self.sort_ascending;
896 } else {
897 self.sort_column = Some(column);
898 self.sort_ascending = true;
899 }
900 self.rebuild_view();
901 }
902
903 pub fn sort_by(&mut self, column: usize) {
905 self.sort_column = Some(column);
906 self.sort_ascending = true;
907 self.rebuild_view();
908 }
909
910 pub fn set_filter(&mut self, filter: impl Into<String>) {
914 self.filter = filter.into();
915 self.page = 0;
916 self.rebuild_view();
917 }
918
919 pub fn clear_sort(&mut self) {
921 self.sort_column = None;
922 self.sort_ascending = true;
923 self.rebuild_view();
924 }
925
926 pub fn next_page(&mut self) {
928 if self.page_size == 0 {
929 return;
930 }
931 let last_page = self.total_pages().saturating_sub(1);
932 self.page = (self.page + 1).min(last_page);
933 }
934
935 pub fn prev_page(&mut self) {
937 self.page = self.page.saturating_sub(1);
938 }
939
940 pub fn total_pages(&self) -> usize {
942 if self.page_size == 0 {
943 return 1;
944 }
945
946 let len = self.view_indices.len();
947 if len == 0 {
948 1
949 } else {
950 len.div_ceil(self.page_size)
951 }
952 }
953
954 pub fn visible_indices(&self) -> &[usize] {
956 &self.view_indices
957 }
958
959 pub fn selected_row(&self) -> Option<&[String]> {
961 if self.view_indices.is_empty() {
962 return None;
963 }
964 let data_idx = self.view_indices.get(self.selected)?;
965 self.rows.get(*data_idx).map(|r| r.as_slice())
966 }
967
968 fn rebuild_view(&mut self) {
970 let mut indices: Vec<usize> = (0..self.rows.len()).collect();
971
972 let tokens: Vec<String> = self
973 .filter
974 .split_whitespace()
975 .map(|t| t.to_lowercase())
976 .collect();
977 if !tokens.is_empty() {
978 indices.retain(|&idx| {
979 let row = match self.rows.get(idx) {
980 Some(r) => r,
981 None => return false,
982 };
983 tokens.iter().all(|token| {
984 row.iter()
985 .any(|cell| cell.to_lowercase().contains(token.as_str()))
986 })
987 });
988 }
989
990 if let Some(column) = self.sort_column {
991 indices.sort_by(|a, b| {
992 let left = self
993 .rows
994 .get(*a)
995 .and_then(|row| row.get(column))
996 .map(String::as_str)
997 .unwrap_or("");
998 let right = self
999 .rows
1000 .get(*b)
1001 .and_then(|row| row.get(column))
1002 .map(String::as_str)
1003 .unwrap_or("");
1004
1005 match (left.parse::<f64>(), right.parse::<f64>()) {
1006 (Ok(l), Ok(r)) => l.partial_cmp(&r).unwrap_or(std::cmp::Ordering::Equal),
1007 _ => left.to_lowercase().cmp(&right.to_lowercase()),
1008 }
1009 });
1010
1011 if !self.sort_ascending {
1012 indices.reverse();
1013 }
1014 }
1015
1016 self.view_indices = indices;
1017
1018 if self.page_size > 0 {
1019 self.page = self.page.min(self.total_pages().saturating_sub(1));
1020 } else {
1021 self.page = 0;
1022 }
1023
1024 self.selected = self.selected.min(self.view_indices.len().saturating_sub(1));
1025 self.dirty = true;
1026 }
1027
1028 pub(crate) fn recompute_widths(&mut self) {
1029 let col_count = self.headers.len();
1030 self.column_widths = vec![0u32; col_count];
1031 for (i, header) in self.headers.iter().enumerate() {
1032 let mut width = UnicodeWidthStr::width(header.as_str()) as u32;
1033 if self.sort_column == Some(i) {
1034 width += 2;
1035 }
1036 self.column_widths[i] = width;
1037 }
1038 for row in &self.rows {
1039 for (i, cell) in row.iter().enumerate() {
1040 if i < col_count {
1041 let w = UnicodeWidthStr::width(cell.as_str()) as u32;
1042 self.column_widths[i] = self.column_widths[i].max(w);
1043 }
1044 }
1045 }
1046 self.dirty = false;
1047 }
1048
1049 pub(crate) fn column_widths(&self) -> &[u32] {
1050 &self.column_widths
1051 }
1052
1053 pub(crate) fn is_dirty(&self) -> bool {
1054 self.dirty
1055 }
1056}
1057
1058#[derive(Debug, Clone)]
1064pub struct ScrollState {
1065 pub offset: usize,
1067 content_height: u32,
1068 viewport_height: u32,
1069}
1070
1071impl ScrollState {
1072 pub fn new() -> Self {
1074 Self {
1075 offset: 0,
1076 content_height: 0,
1077 viewport_height: 0,
1078 }
1079 }
1080
1081 pub fn can_scroll_up(&self) -> bool {
1083 self.offset > 0
1084 }
1085
1086 pub fn can_scroll_down(&self) -> bool {
1088 (self.offset as u32) + self.viewport_height < self.content_height
1089 }
1090
1091 pub fn content_height(&self) -> u32 {
1093 self.content_height
1094 }
1095
1096 pub fn viewport_height(&self) -> u32 {
1098 self.viewport_height
1099 }
1100
1101 pub fn progress(&self) -> f32 {
1103 let max = self.content_height.saturating_sub(self.viewport_height);
1104 if max == 0 {
1105 0.0
1106 } else {
1107 self.offset as f32 / max as f32
1108 }
1109 }
1110
1111 pub fn scroll_up(&mut self, amount: usize) {
1113 self.offset = self.offset.saturating_sub(amount);
1114 }
1115
1116 pub fn scroll_down(&mut self, amount: usize) {
1118 let max_offset = self.content_height.saturating_sub(self.viewport_height) as usize;
1119 self.offset = (self.offset + amount).min(max_offset);
1120 }
1121
1122 pub(crate) fn set_bounds(&mut self, content_height: u32, viewport_height: u32) {
1123 self.content_height = content_height;
1124 self.viewport_height = viewport_height;
1125 }
1126}
1127
1128impl Default for ScrollState {
1129 fn default() -> Self {
1130 Self::new()
1131 }
1132}
1133
1134#[derive(Debug, Clone)]
1136pub struct RichLogState {
1137 pub entries: Vec<RichLogEntry>,
1139 pub(crate) scroll_offset: usize,
1141 pub auto_scroll: bool,
1143 pub max_entries: Option<usize>,
1145}
1146
1147#[derive(Debug, Clone)]
1149pub struct RichLogEntry {
1150 pub segments: Vec<(String, Style)>,
1152}
1153
1154impl RichLogState {
1155 pub fn new() -> Self {
1157 Self {
1158 entries: Vec::new(),
1159 scroll_offset: 0,
1160 auto_scroll: true,
1161 max_entries: None,
1162 }
1163 }
1164
1165 pub fn push(&mut self, text: impl Into<String>, style: Style) {
1167 self.push_segments(vec![(text.into(), style)]);
1168 }
1169
1170 pub fn push_plain(&mut self, text: impl Into<String>) {
1172 self.push(text, Style::new());
1173 }
1174
1175 pub fn push_segments(&mut self, segments: Vec<(String, Style)>) {
1177 self.entries.push(RichLogEntry { segments });
1178
1179 if let Some(max_entries) = self.max_entries {
1180 if self.entries.len() > max_entries {
1181 let remove_count = self.entries.len() - max_entries;
1182 self.entries.drain(0..remove_count);
1183 self.scroll_offset = self.scroll_offset.saturating_sub(remove_count);
1184 }
1185 }
1186
1187 if self.auto_scroll {
1188 self.scroll_offset = usize::MAX;
1189 }
1190 }
1191
1192 pub fn clear(&mut self) {
1194 self.entries.clear();
1195 self.scroll_offset = 0;
1196 }
1197
1198 pub fn len(&self) -> usize {
1200 self.entries.len()
1201 }
1202
1203 pub fn is_empty(&self) -> bool {
1205 self.entries.is_empty()
1206 }
1207}
1208
1209impl Default for RichLogState {
1210 fn default() -> Self {
1211 Self::new()
1212 }
1213}
1214
1215#[derive(Debug, Clone)]
1217pub struct CalendarState {
1218 pub year: i32,
1220 pub month: u32,
1222 pub selected_day: Option<u32>,
1224 pub(crate) cursor_day: u32,
1225}
1226
1227impl CalendarState {
1228 pub fn new() -> Self {
1230 let (year, month) = Self::current_year_month();
1231 Self::from_ym(year, month)
1232 }
1233
1234 pub fn from_ym(year: i32, month: u32) -> Self {
1236 let month = month.clamp(1, 12);
1237 Self {
1238 year,
1239 month,
1240 selected_day: None,
1241 cursor_day: 1,
1242 }
1243 }
1244
1245 pub fn selected_date(&self) -> Option<(i32, u32, u32)> {
1247 self.selected_day.map(|day| (self.year, self.month, day))
1248 }
1249
1250 pub fn prev_month(&mut self) {
1252 if self.month == 1 {
1253 self.month = 12;
1254 self.year -= 1;
1255 } else {
1256 self.month -= 1;
1257 }
1258 self.clamp_days();
1259 }
1260
1261 pub fn next_month(&mut self) {
1263 if self.month == 12 {
1264 self.month = 1;
1265 self.year += 1;
1266 } else {
1267 self.month += 1;
1268 }
1269 self.clamp_days();
1270 }
1271
1272 pub(crate) fn days_in_month(year: i32, month: u32) -> u32 {
1273 match month {
1274 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
1275 4 | 6 | 9 | 11 => 30,
1276 2 => {
1277 if Self::is_leap_year(year) {
1278 29
1279 } else {
1280 28
1281 }
1282 }
1283 _ => 30,
1284 }
1285 }
1286
1287 pub(crate) fn first_weekday(year: i32, month: u32) -> u32 {
1288 let month = month.clamp(1, 12);
1289 let offsets = [0_i32, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4];
1290 let mut y = year;
1291 if month < 3 {
1292 y -= 1;
1293 }
1294 let sunday_based = (y + y / 4 - y / 100 + y / 400 + offsets[(month - 1) as usize] + 1) % 7;
1295 ((sunday_based + 6) % 7) as u32
1296 }
1297
1298 fn clamp_days(&mut self) {
1299 let max_day = Self::days_in_month(self.year, self.month);
1300 self.cursor_day = self.cursor_day.clamp(1, max_day);
1301 if let Some(day) = self.selected_day {
1302 self.selected_day = Some(day.min(max_day));
1303 }
1304 }
1305
1306 fn is_leap_year(year: i32) -> bool {
1307 (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
1308 }
1309
1310 fn current_year_month() -> (i32, u32) {
1311 let Ok(duration) = SystemTime::now().duration_since(UNIX_EPOCH) else {
1312 return (1970, 1);
1313 };
1314 let days_since_epoch = (duration.as_secs() / 86_400) as i64;
1315 let (year, month, _) = Self::civil_from_days(days_since_epoch);
1316 (year, month)
1317 }
1318
1319 fn civil_from_days(days_since_epoch: i64) -> (i32, u32, u32) {
1320 let z = days_since_epoch + 719_468;
1321 let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
1322 let doe = z - era * 146_097;
1323 let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
1324 let mut year = (yoe as i32) + (era as i32) * 400;
1325 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
1326 let mp = (5 * doy + 2) / 153;
1327 let day = (doy - (153 * mp + 2) / 5 + 1) as u32;
1328 let month = (mp + if mp < 10 { 3 } else { -9 }) as u32;
1329 if month <= 2 {
1330 year += 1;
1331 }
1332 (year, month, day)
1333 }
1334}
1335
1336impl Default for CalendarState {
1337 fn default() -> Self {
1338 Self::new()
1339 }
1340}
1341
1342#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
1352pub enum ButtonVariant {
1353 #[default]
1355 Default,
1356 Primary,
1358 Danger,
1360 Outline,
1362}
1363
1364#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1366pub enum Trend {
1367 Up,
1369 Down,
1371}
1372
1373#[derive(Debug, Clone, Default)]
1380pub struct SelectState {
1381 pub items: Vec<String>,
1383 pub selected: usize,
1385 pub open: bool,
1387 pub placeholder: String,
1389 cursor: usize,
1390}
1391
1392impl SelectState {
1393 pub fn new(items: Vec<impl Into<String>>) -> Self {
1395 Self {
1396 items: items.into_iter().map(Into::into).collect(),
1397 selected: 0,
1398 open: false,
1399 placeholder: String::new(),
1400 cursor: 0,
1401 }
1402 }
1403
1404 pub fn placeholder(mut self, p: impl Into<String>) -> Self {
1406 self.placeholder = p.into();
1407 self
1408 }
1409
1410 pub fn selected_item(&self) -> Option<&str> {
1412 self.items.get(self.selected).map(String::as_str)
1413 }
1414
1415 pub(crate) fn cursor(&self) -> usize {
1416 self.cursor
1417 }
1418
1419 pub(crate) fn set_cursor(&mut self, c: usize) {
1420 self.cursor = c;
1421 }
1422}
1423
1424#[derive(Debug, Clone, Default)]
1430pub struct RadioState {
1431 pub items: Vec<String>,
1433 pub selected: usize,
1435}
1436
1437impl RadioState {
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 }
1444 }
1445
1446 pub fn selected_item(&self) -> Option<&str> {
1448 self.items.get(self.selected).map(String::as_str)
1449 }
1450}
1451
1452#[derive(Debug, Clone)]
1458pub struct MultiSelectState {
1459 pub items: Vec<String>,
1461 pub cursor: usize,
1463 pub selected: HashSet<usize>,
1465}
1466
1467impl MultiSelectState {
1468 pub fn new(items: Vec<impl Into<String>>) -> Self {
1470 Self {
1471 items: items.into_iter().map(Into::into).collect(),
1472 cursor: 0,
1473 selected: HashSet::new(),
1474 }
1475 }
1476
1477 pub fn selected_items(&self) -> Vec<&str> {
1479 let mut indices: Vec<usize> = self.selected.iter().copied().collect();
1480 indices.sort();
1481 indices
1482 .iter()
1483 .filter_map(|&i| self.items.get(i).map(String::as_str))
1484 .collect()
1485 }
1486
1487 pub fn toggle(&mut self, index: usize) {
1489 if self.selected.contains(&index) {
1490 self.selected.remove(&index);
1491 } else {
1492 self.selected.insert(index);
1493 }
1494 }
1495}
1496
1497#[derive(Debug, Clone)]
1501pub struct TreeNode {
1502 pub label: String,
1504 pub children: Vec<TreeNode>,
1506 pub expanded: bool,
1508}
1509
1510impl TreeNode {
1511 pub fn new(label: impl Into<String>) -> Self {
1513 Self {
1514 label: label.into(),
1515 children: Vec::new(),
1516 expanded: false,
1517 }
1518 }
1519
1520 pub fn expanded(mut self) -> Self {
1522 self.expanded = true;
1523 self
1524 }
1525
1526 pub fn children(mut self, children: Vec<TreeNode>) -> Self {
1528 self.children = children;
1529 self
1530 }
1531
1532 pub fn is_leaf(&self) -> bool {
1534 self.children.is_empty()
1535 }
1536
1537 fn flatten(&self, depth: usize, out: &mut Vec<FlatTreeEntry>) {
1538 out.push(FlatTreeEntry {
1539 depth,
1540 label: self.label.clone(),
1541 is_leaf: self.is_leaf(),
1542 expanded: self.expanded,
1543 });
1544 if self.expanded {
1545 for child in &self.children {
1546 child.flatten(depth + 1, out);
1547 }
1548 }
1549 }
1550}
1551
1552pub(crate) struct FlatTreeEntry {
1553 pub depth: usize,
1554 pub label: String,
1555 pub is_leaf: bool,
1556 pub expanded: bool,
1557}
1558
1559#[derive(Debug, Clone)]
1561pub struct TreeState {
1562 pub nodes: Vec<TreeNode>,
1564 pub selected: usize,
1566}
1567
1568impl TreeState {
1569 pub fn new(nodes: Vec<TreeNode>) -> Self {
1571 Self { nodes, selected: 0 }
1572 }
1573
1574 pub(crate) fn flatten(&self) -> Vec<FlatTreeEntry> {
1575 let mut entries = Vec::new();
1576 for node in &self.nodes {
1577 node.flatten(0, &mut entries);
1578 }
1579 entries
1580 }
1581
1582 pub(crate) fn toggle_at(&mut self, flat_index: usize) {
1583 let mut counter = 0usize;
1584 Self::toggle_recursive(&mut self.nodes, flat_index, &mut counter);
1585 }
1586
1587 fn toggle_recursive(nodes: &mut [TreeNode], target: usize, counter: &mut usize) -> bool {
1588 for node in nodes.iter_mut() {
1589 if *counter == target {
1590 if !node.is_leaf() {
1591 node.expanded = !node.expanded;
1592 }
1593 return true;
1594 }
1595 *counter += 1;
1596 if node.expanded && Self::toggle_recursive(&mut node.children, target, counter) {
1597 return true;
1598 }
1599 }
1600 false
1601 }
1602}
1603
1604#[derive(Debug, Clone)]
1606pub struct DirectoryTreeState {
1607 pub tree: TreeState,
1609 pub show_icons: bool,
1611}
1612
1613impl DirectoryTreeState {
1614 pub fn new(nodes: Vec<TreeNode>) -> Self {
1616 Self {
1617 tree: TreeState::new(nodes),
1618 show_icons: true,
1619 }
1620 }
1621
1622 pub fn from_paths(paths: &[&str]) -> Self {
1624 let mut roots: Vec<TreeNode> = Vec::new();
1625
1626 for raw_path in paths {
1627 let parts: Vec<&str> = raw_path
1628 .split('/')
1629 .filter(|part| !part.is_empty())
1630 .collect();
1631 if parts.is_empty() {
1632 continue;
1633 }
1634 insert_path(&mut roots, &parts, 0);
1635 }
1636
1637 Self::new(roots)
1638 }
1639
1640 pub fn selected_label(&self) -> Option<&str> {
1642 let mut cursor = 0usize;
1643 selected_label_in_nodes(&self.tree.nodes, self.tree.selected, &mut cursor)
1644 }
1645}
1646
1647impl Default for DirectoryTreeState {
1648 fn default() -> Self {
1649 Self::new(Vec::<TreeNode>::new())
1650 }
1651}
1652
1653fn insert_path(nodes: &mut Vec<TreeNode>, parts: &[&str], depth: usize) {
1654 let Some(label) = parts.get(depth) else {
1655 return;
1656 };
1657
1658 let is_last = depth + 1 == parts.len();
1659 let idx = nodes
1660 .iter()
1661 .position(|node| node.label == *label)
1662 .unwrap_or_else(|| {
1663 let mut node = TreeNode::new(*label);
1664 if !is_last {
1665 node.expanded = true;
1666 }
1667 nodes.push(node);
1668 nodes.len() - 1
1669 });
1670
1671 if is_last {
1672 return;
1673 }
1674
1675 nodes[idx].expanded = true;
1676 insert_path(&mut nodes[idx].children, parts, depth + 1);
1677}
1678
1679fn selected_label_in_nodes<'a>(
1680 nodes: &'a [TreeNode],
1681 target: usize,
1682 cursor: &mut usize,
1683) -> Option<&'a str> {
1684 for node in nodes {
1685 if *cursor == target {
1686 return Some(node.label.as_str());
1687 }
1688 *cursor += 1;
1689 if node.expanded {
1690 if let Some(found) = selected_label_in_nodes(&node.children, target, cursor) {
1691 return Some(found);
1692 }
1693 }
1694 }
1695 None
1696}
1697
1698#[derive(Debug, Clone)]
1702pub struct PaletteCommand {
1703 pub label: String,
1705 pub description: String,
1707 pub shortcut: Option<String>,
1709}
1710
1711impl PaletteCommand {
1712 pub fn new(label: impl Into<String>, description: impl Into<String>) -> Self {
1714 Self {
1715 label: label.into(),
1716 description: description.into(),
1717 shortcut: None,
1718 }
1719 }
1720
1721 pub fn shortcut(mut self, s: impl Into<String>) -> Self {
1723 self.shortcut = Some(s.into());
1724 self
1725 }
1726}
1727
1728#[derive(Debug, Clone)]
1732pub struct CommandPaletteState {
1733 pub commands: Vec<PaletteCommand>,
1735 pub input: String,
1737 pub cursor: usize,
1739 pub open: bool,
1741 pub last_selected: Option<usize>,
1744 selected: usize,
1745}
1746
1747impl CommandPaletteState {
1748 pub fn new(commands: Vec<PaletteCommand>) -> Self {
1750 Self {
1751 commands,
1752 input: String::new(),
1753 cursor: 0,
1754 open: false,
1755 last_selected: None,
1756 selected: 0,
1757 }
1758 }
1759
1760 pub fn toggle(&mut self) {
1762 self.open = !self.open;
1763 if self.open {
1764 self.input.clear();
1765 self.cursor = 0;
1766 self.selected = 0;
1767 }
1768 }
1769
1770 fn fuzzy_score(pattern: &str, text: &str) -> Option<i32> {
1771 let pattern = pattern.trim();
1772 if pattern.is_empty() {
1773 return Some(0);
1774 }
1775
1776 let text_chars: Vec<char> = text.chars().collect();
1777 let mut score = 0;
1778 let mut search_start = 0usize;
1779 let mut prev_match: Option<usize> = None;
1780
1781 for p in pattern.chars() {
1782 let mut found = None;
1783 for (idx, ch) in text_chars.iter().enumerate().skip(search_start) {
1784 if ch.eq_ignore_ascii_case(&p) {
1785 found = Some(idx);
1786 break;
1787 }
1788 }
1789
1790 let idx = found?;
1791 if prev_match.is_some_and(|prev| idx == prev + 1) {
1792 score += 3;
1793 } else {
1794 score += 1;
1795 }
1796
1797 if idx == 0 {
1798 score += 2;
1799 } else {
1800 let prev = text_chars[idx - 1];
1801 let curr = text_chars[idx];
1802 if matches!(prev, ' ' | '_' | '-') || prev.is_uppercase() || curr.is_uppercase() {
1803 score += 2;
1804 }
1805 }
1806
1807 prev_match = Some(idx);
1808 search_start = idx + 1;
1809 }
1810
1811 Some(score)
1812 }
1813
1814 pub(crate) fn filtered_indices(&self) -> Vec<usize> {
1815 let query = self.input.trim();
1816 if query.is_empty() {
1817 return (0..self.commands.len()).collect();
1818 }
1819
1820 let mut scored: Vec<(usize, i32)> = self
1821 .commands
1822 .iter()
1823 .enumerate()
1824 .filter_map(|(i, cmd)| {
1825 let mut haystack =
1826 String::with_capacity(cmd.label.len() + cmd.description.len() + 1);
1827 haystack.push_str(&cmd.label);
1828 haystack.push(' ');
1829 haystack.push_str(&cmd.description);
1830 Self::fuzzy_score(query, &haystack).map(|score| (i, score))
1831 })
1832 .collect();
1833
1834 if scored.is_empty() {
1835 let tokens: Vec<String> = query.split_whitespace().map(|t| t.to_lowercase()).collect();
1836 return self
1837 .commands
1838 .iter()
1839 .enumerate()
1840 .filter(|(_, cmd)| {
1841 let label = cmd.label.to_lowercase();
1842 let desc = cmd.description.to_lowercase();
1843 tokens.iter().all(|token| {
1844 label.contains(token.as_str()) || desc.contains(token.as_str())
1845 })
1846 })
1847 .map(|(i, _)| i)
1848 .collect();
1849 }
1850
1851 scored.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
1852 scored.into_iter().map(|(idx, _)| idx).collect()
1853 }
1854
1855 pub(crate) fn selected(&self) -> usize {
1856 self.selected
1857 }
1858
1859 pub(crate) fn set_selected(&mut self, s: usize) {
1860 self.selected = s;
1861 }
1862}
1863
1864#[derive(Debug, Clone)]
1869pub struct StreamingTextState {
1870 pub content: String,
1872 pub streaming: bool,
1874 pub(crate) cursor_visible: bool,
1876 pub(crate) cursor_tick: u64,
1877}
1878
1879impl StreamingTextState {
1880 pub fn new() -> Self {
1882 Self {
1883 content: String::new(),
1884 streaming: false,
1885 cursor_visible: true,
1886 cursor_tick: 0,
1887 }
1888 }
1889
1890 pub fn push(&mut self, chunk: &str) {
1892 self.content.push_str(chunk);
1893 }
1894
1895 pub fn finish(&mut self) {
1897 self.streaming = false;
1898 }
1899
1900 pub fn start(&mut self) {
1902 self.content.clear();
1903 self.streaming = true;
1904 self.cursor_visible = true;
1905 self.cursor_tick = 0;
1906 }
1907
1908 pub fn clear(&mut self) {
1910 self.content.clear();
1911 self.streaming = false;
1912 self.cursor_visible = true;
1913 self.cursor_tick = 0;
1914 }
1915}
1916
1917impl Default for StreamingTextState {
1918 fn default() -> Self {
1919 Self::new()
1920 }
1921}
1922
1923#[derive(Debug, Clone)]
1928pub struct StreamingMarkdownState {
1929 pub content: String,
1931 pub streaming: bool,
1933 pub cursor_visible: bool,
1935 pub cursor_tick: u64,
1937 pub in_code_block: bool,
1939 pub code_block_lang: String,
1941}
1942
1943impl StreamingMarkdownState {
1944 pub fn new() -> Self {
1946 Self {
1947 content: String::new(),
1948 streaming: false,
1949 cursor_visible: true,
1950 cursor_tick: 0,
1951 in_code_block: false,
1952 code_block_lang: String::new(),
1953 }
1954 }
1955
1956 pub fn push(&mut self, chunk: &str) {
1958 self.content.push_str(chunk);
1959 }
1960
1961 pub fn start(&mut self) {
1963 self.content.clear();
1964 self.streaming = true;
1965 self.cursor_visible = true;
1966 self.cursor_tick = 0;
1967 self.in_code_block = false;
1968 self.code_block_lang.clear();
1969 }
1970
1971 pub fn finish(&mut self) {
1973 self.streaming = false;
1974 }
1975
1976 pub fn clear(&mut self) {
1978 self.content.clear();
1979 self.streaming = false;
1980 self.cursor_visible = true;
1981 self.cursor_tick = 0;
1982 self.in_code_block = false;
1983 self.code_block_lang.clear();
1984 }
1985}
1986
1987impl Default for StreamingMarkdownState {
1988 fn default() -> Self {
1989 Self::new()
1990 }
1991}
1992
1993#[derive(Debug, Clone)]
1998pub struct ScreenState {
1999 stack: Vec<String>,
2000}
2001
2002impl ScreenState {
2003 pub fn new(initial: impl Into<String>) -> Self {
2005 Self {
2006 stack: vec![initial.into()],
2007 }
2008 }
2009
2010 pub fn current(&self) -> &str {
2012 self.stack
2013 .last()
2014 .expect("ScreenState always contains at least one screen")
2015 .as_str()
2016 }
2017
2018 pub fn push(&mut self, name: impl Into<String>) {
2020 self.stack.push(name.into());
2021 }
2022
2023 pub fn pop(&mut self) {
2025 if self.can_pop() {
2026 self.stack.pop();
2027 }
2028 }
2029
2030 pub fn depth(&self) -> usize {
2032 self.stack.len()
2033 }
2034
2035 pub fn can_pop(&self) -> bool {
2037 self.stack.len() > 1
2038 }
2039
2040 pub fn reset(&mut self) {
2042 self.stack.truncate(1);
2043 }
2044}
2045
2046#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2048pub enum ApprovalAction {
2049 Pending,
2051 Approved,
2053 Rejected,
2055}
2056
2057#[derive(Debug, Clone)]
2063pub struct ToolApprovalState {
2064 pub tool_name: String,
2066 pub description: String,
2068 pub action: ApprovalAction,
2070}
2071
2072impl ToolApprovalState {
2073 pub fn new(tool_name: impl Into<String>, description: impl Into<String>) -> Self {
2075 Self {
2076 tool_name: tool_name.into(),
2077 description: description.into(),
2078 action: ApprovalAction::Pending,
2079 }
2080 }
2081
2082 pub fn reset(&mut self) {
2084 self.action = ApprovalAction::Pending;
2085 }
2086}
2087
2088#[derive(Debug, Clone)]
2090pub struct ContextItem {
2091 pub label: String,
2093 pub tokens: usize,
2095}
2096
2097impl ContextItem {
2098 pub fn new(label: impl Into<String>, tokens: usize) -> Self {
2100 Self {
2101 label: label.into(),
2102 tokens,
2103 }
2104 }
2105}
2106
2107#[cfg(test)]
2108mod tests {
2109 use super::*;
2110
2111 #[test]
2112 fn static_output_accumulates_and_drains_new_lines() {
2113 let mut output = StaticOutput::new();
2114 output.println("Building crate...");
2115 output.println("Compiling foo v0.1.0");
2116
2117 assert_eq!(
2118 output.lines(),
2119 &[
2120 "Building crate...".to_string(),
2121 "Compiling foo v0.1.0".to_string()
2122 ]
2123 );
2124
2125 let first = output.drain_new();
2126 assert_eq!(
2127 first,
2128 vec![
2129 "Building crate...".to_string(),
2130 "Compiling foo v0.1.0".to_string()
2131 ]
2132 );
2133 assert!(output.drain_new().is_empty());
2134
2135 output.println("Finished");
2136 assert_eq!(output.drain_new(), vec!["Finished".to_string()]);
2137 }
2138
2139 #[test]
2140 fn static_output_clear_resets_all_buffers() {
2141 let mut output = StaticOutput::new();
2142 output.println("line");
2143 output.clear();
2144
2145 assert!(output.lines().is_empty());
2146 assert!(output.drain_new().is_empty());
2147 }
2148
2149 #[test]
2150 fn form_field_default_values() {
2151 let field = FormField::default();
2152 assert_eq!(field.label, "");
2153 assert_eq!(field.input.value, "");
2154 assert_eq!(field.input.cursor, 0);
2155 assert_eq!(field.error, None);
2156 }
2157
2158 #[test]
2159 fn toast_message_default_values() {
2160 let msg = ToastMessage::default();
2161 assert_eq!(msg.text, "");
2162 assert!(matches!(msg.level, ToastLevel::Info));
2163 assert_eq!(msg.created_tick, 0);
2164 assert_eq!(msg.duration_ticks, 30);
2165 }
2166
2167 #[test]
2168 fn list_state_default_values() {
2169 let state = ListState::default();
2170 assert!(state.items.is_empty());
2171 assert_eq!(state.selected, 0);
2172 assert_eq!(state.filter, "");
2173 assert!(state.visible_indices().is_empty());
2174 assert_eq!(state.selected_item(), None);
2175 }
2176
2177 #[test]
2178 fn file_entry_default_values() {
2179 let entry = FileEntry::default();
2180 assert_eq!(entry.name, "");
2181 assert_eq!(entry.path, PathBuf::new());
2182 assert!(!entry.is_dir);
2183 assert_eq!(entry.size, 0);
2184 }
2185
2186 #[test]
2187 fn tabs_state_default_values() {
2188 let state = TabsState::default();
2189 assert!(state.labels.is_empty());
2190 assert_eq!(state.selected, 0);
2191 assert_eq!(state.selected_label(), None);
2192 }
2193
2194 #[test]
2195 fn table_state_default_values() {
2196 let state = TableState::default();
2197 assert!(state.headers.is_empty());
2198 assert!(state.rows.is_empty());
2199 assert_eq!(state.selected, 0);
2200 assert_eq!(state.sort_column, None);
2201 assert!(state.sort_ascending);
2202 assert_eq!(state.filter, "");
2203 assert_eq!(state.page, 0);
2204 assert_eq!(state.page_size, 0);
2205 assert!(!state.zebra);
2206 assert!(state.visible_indices().is_empty());
2207 }
2208
2209 #[test]
2210 fn select_state_default_values() {
2211 let state = SelectState::default();
2212 assert!(state.items.is_empty());
2213 assert_eq!(state.selected, 0);
2214 assert!(!state.open);
2215 assert_eq!(state.placeholder, "");
2216 assert_eq!(state.selected_item(), None);
2217 assert_eq!(state.cursor(), 0);
2218 }
2219
2220 #[test]
2221 fn radio_state_default_values() {
2222 let state = RadioState::default();
2223 assert!(state.items.is_empty());
2224 assert_eq!(state.selected, 0);
2225 assert_eq!(state.selected_item(), None);
2226 }
2227
2228 #[test]
2229 fn text_input_state_default_uses_new() {
2230 let state = TextInputState::default();
2231 assert_eq!(state.value, "");
2232 assert_eq!(state.cursor, 0);
2233 assert_eq!(state.placeholder, "");
2234 assert_eq!(state.max_length, None);
2235 assert_eq!(state.validation_error, None);
2236 assert!(!state.masked);
2237 }
2238
2239 #[test]
2240 fn tabs_state_new_sets_labels() {
2241 let state = TabsState::new(vec!["a", "b"]);
2242 assert_eq!(state.labels, vec!["a".to_string(), "b".to_string()]);
2243 assert_eq!(state.selected, 0);
2244 assert_eq!(state.selected_label(), Some("a"));
2245 }
2246
2247 #[test]
2248 fn list_state_new_selected_item_points_to_first_item() {
2249 let state = ListState::new(vec!["alpha", "beta"]);
2250 assert_eq!(state.items, vec!["alpha".to_string(), "beta".to_string()]);
2251 assert_eq!(state.selected, 0);
2252 assert_eq!(state.visible_indices(), &[0, 1]);
2253 assert_eq!(state.selected_item(), Some("alpha"));
2254 }
2255
2256 #[test]
2257 fn select_state_placeholder_builder_sets_value() {
2258 let state = SelectState::new(vec!["one", "two"]).placeholder("Pick one");
2259 assert_eq!(state.items, vec!["one".to_string(), "two".to_string()]);
2260 assert_eq!(state.placeholder, "Pick one");
2261 assert_eq!(state.selected_item(), Some("one"));
2262 }
2263
2264 #[test]
2265 fn radio_state_new_sets_items_and_selection() {
2266 let state = RadioState::new(vec!["red", "green"]);
2267 assert_eq!(state.items, vec!["red".to_string(), "green".to_string()]);
2268 assert_eq!(state.selected, 0);
2269 assert_eq!(state.selected_item(), Some("red"));
2270 }
2271
2272 #[test]
2273 fn table_state_new_sets_sort_ascending_true() {
2274 let state = TableState::new(vec!["Name"], vec![vec!["Alice"], vec!["Bob"]]);
2275 assert_eq!(state.headers, vec!["Name".to_string()]);
2276 assert_eq!(state.rows.len(), 2);
2277 assert!(state.sort_ascending);
2278 assert_eq!(state.sort_column, None);
2279 assert!(!state.zebra);
2280 assert_eq!(state.visible_indices(), &[0, 1]);
2281 }
2282
2283 #[test]
2284 fn command_palette_fuzzy_score_matches_gapped_pattern() {
2285 assert!(CommandPaletteState::fuzzy_score("sf", "Save File").is_some());
2286 assert!(CommandPaletteState::fuzzy_score("cmd", "Command Palette").is_some());
2287 assert_eq!(CommandPaletteState::fuzzy_score("xyz", "Save File"), None);
2288 }
2289
2290 #[test]
2291 fn command_palette_filtered_indices_uses_fuzzy_and_sorts() {
2292 let mut state = CommandPaletteState::new(vec![
2293 PaletteCommand::new("Save File", "Write buffer"),
2294 PaletteCommand::new("Search Files", "Find in workspace"),
2295 PaletteCommand::new("Quit", "Exit app"),
2296 ]);
2297
2298 state.input = "sf".to_string();
2299 let filtered = state.filtered_indices();
2300 assert_eq!(filtered, vec![0, 1]);
2301
2302 state.input = "buffer".to_string();
2303 let filtered = state.filtered_indices();
2304 assert_eq!(filtered, vec![0]);
2305 }
2306
2307 #[test]
2308 fn screen_state_push_pop_tracks_current_screen() {
2309 let mut screens = ScreenState::new("home");
2310 assert_eq!(screens.current(), "home");
2311 assert_eq!(screens.depth(), 1);
2312 assert!(!screens.can_pop());
2313
2314 screens.push("settings");
2315 assert_eq!(screens.current(), "settings");
2316 assert_eq!(screens.depth(), 2);
2317 assert!(screens.can_pop());
2318
2319 screens.push("profile");
2320 assert_eq!(screens.current(), "profile");
2321 assert_eq!(screens.depth(), 3);
2322
2323 screens.pop();
2324 assert_eq!(screens.current(), "settings");
2325 assert_eq!(screens.depth(), 2);
2326 }
2327
2328 #[test]
2329 fn screen_state_pop_never_removes_root() {
2330 let mut screens = ScreenState::new("home");
2331 screens.push("settings");
2332 screens.pop();
2333 screens.pop();
2334
2335 assert_eq!(screens.current(), "home");
2336 assert_eq!(screens.depth(), 1);
2337 assert!(!screens.can_pop());
2338 }
2339
2340 #[test]
2341 fn screen_state_reset_keeps_only_root() {
2342 let mut screens = ScreenState::new("home");
2343 screens.push("settings");
2344 screens.push("profile");
2345 assert_eq!(screens.current(), "profile");
2346
2347 screens.reset();
2348 assert_eq!(screens.current(), "home");
2349 assert_eq!(screens.depth(), 1);
2350 assert!(!screens.can_pop());
2351 }
2352
2353 #[test]
2354 fn calendar_days_in_month_handles_leap_years() {
2355 assert_eq!(CalendarState::days_in_month(2024, 2), 29);
2356 assert_eq!(CalendarState::days_in_month(2023, 2), 28);
2357 assert_eq!(CalendarState::days_in_month(2024, 1), 31);
2358 assert_eq!(CalendarState::days_in_month(2024, 4), 30);
2359 }
2360
2361 #[test]
2362 fn calendar_first_weekday_known_dates() {
2363 assert_eq!(CalendarState::first_weekday(2024, 1), 0);
2364 assert_eq!(CalendarState::first_weekday(2023, 10), 6);
2365 }
2366
2367 #[test]
2368 fn calendar_prev_next_month_handles_year_boundary() {
2369 let mut state = CalendarState::from_ym(2024, 12);
2370 state.prev_month();
2371 assert_eq!((state.year, state.month), (2024, 11));
2372
2373 let mut state = CalendarState::from_ym(2024, 1);
2374 state.prev_month();
2375 assert_eq!((state.year, state.month), (2023, 12));
2376
2377 state.next_month();
2378 assert_eq!((state.year, state.month), (2024, 1));
2379 }
2380}