virtuoso_cli/tui/app/
overlay.rs1pub enum Overlay {
6 None,
7 Log(LogOverlay),
8 Confirm(ConfirmOverlay),
9 Form(ConfigFormState),
10 Help,
11}
12
13impl Overlay {
14 pub fn is_active(&self) -> bool {
15 !matches!(self, Overlay::None)
16 }
17}
18
19pub struct LogOverlay {
20 pub lines: Vec<String>,
21 pub scroll: usize,
22}
23
24impl LogOverlay {
25 pub fn new(lines: Vec<String>) -> Self {
26 let scroll = lines.len().saturating_sub(1);
27 Self { lines, scroll }
28 }
29}
30
31pub enum ConfirmAction {
32 CancelJob(usize),
33}
34
35pub struct ConfirmOverlay {
36 pub title: String,
37 pub message: String,
38 pub action: ConfirmAction,
39}
40
41pub struct ConfigFormState {
42 pub field_idx: usize,
43 pub key: String,
44 pub hint: &'static str,
45 pub value: TextInput,
46}
47
48#[derive(Default, Clone)]
51pub struct TextInput {
52 pub value: String,
53 pub cursor: usize,
54}
55
56impl TextInput {
57 pub fn new(s: &str) -> Self {
58 Self {
59 value: s.to_string(),
60 cursor: s.len(),
61 }
62 }
63
64 pub fn insert_char(&mut self, c: char) {
65 self.value.insert(self.cursor, c);
66 self.cursor += c.len_utf8();
67 }
68
69 pub fn backspace(&mut self) {
70 if self.cursor == 0 {
71 return;
72 }
73 let before = self.value[..self.cursor].chars().next_back();
74 if let Some(ch) = before {
75 let new_cursor = self.cursor - ch.len_utf8();
76 self.value.drain(new_cursor..self.cursor);
77 self.cursor = new_cursor;
78 }
79 }
80
81 pub fn move_left(&mut self) {
82 if let Some(ch) = self.value[..self.cursor].chars().next_back() {
83 self.cursor -= ch.len_utf8();
84 }
85 }
86
87 pub fn move_right(&mut self) {
88 if let Some(ch) = self.value[self.cursor..].chars().next() {
89 self.cursor += ch.len_utf8();
90 }
91 }
92
93 pub fn home(&mut self) {
94 self.cursor = 0;
95 }
96
97 pub fn end(&mut self) {
98 self.cursor = self.value.len();
99 }
100
101 pub fn as_str(&self) -> &str {
102 &self.value
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 #[test]
111 fn text_input_ascii_roundtrip() {
112 let mut ti = TextInput::default();
113 ti.insert_char('a');
114 ti.insert_char('b');
115 ti.insert_char('c');
116 assert_eq!(ti.as_str(), "abc");
117 assert_eq!(ti.cursor, 3);
118 ti.backspace();
119 assert_eq!(ti.as_str(), "ab");
120 }
121
122 #[test]
123 fn text_input_utf8_safe() {
124 let mut ti = TextInput::new("中文");
125 assert_eq!(ti.cursor, 6);
126 ti.move_left();
127 assert_eq!(ti.cursor, 3);
128 ti.backspace();
129 assert_eq!(ti.as_str(), "文");
130 assert_eq!(ti.cursor, 0);
131 }
132
133 #[test]
134 fn text_input_home_end() {
135 let mut ti = TextInput::new("hello");
136 ti.home();
137 assert_eq!(ti.cursor, 0);
138 ti.end();
139 assert_eq!(ti.cursor, 5);
140 }
141}