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