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