tty_form/step/
textblock.rs1use crossterm::event::{KeyCode, KeyEvent};
2use tty_interface::{pos, Interface, Position};
3use tty_text::Key;
4
5use crate::{
6 dependency::DependencyState,
7 style::{error_style, help_style},
8 text::{set_segment_subset_style, DrawerContents, Segment, Text},
9 utility::render_segment,
10 Form,
11};
12
13use super::{InputResult, Step};
14
15pub struct TextBlockStep {
31 prompt: String,
32 text: tty_text::Text,
33 top_margin: Option<u16>,
34 bottom_margin: Option<u16>,
35 max_line_length: Option<u16>,
36 trim_trailing_whitespace: bool,
37}
38
39impl TextBlockStep {
40 pub fn new(prompt: &str) -> Self {
42 Self {
43 prompt: prompt.to_string(),
44 text: tty_text::Text::new(true),
45 top_margin: None,
46 bottom_margin: None,
47 max_line_length: None,
48 trim_trailing_whitespace: true,
49 }
50 }
51
52 pub fn set_margins(&mut self, top_margin: Option<u16>, bottom_margin: Option<u16>) {
54 self.top_margin = top_margin;
55 self.bottom_margin = bottom_margin;
56 }
57
58 pub fn set_max_line_length(&mut self, max_length: u16) {
60 self.max_line_length = Some(max_length);
61 }
62
63 pub fn set_trim_trailing_whitespace(&mut self, trim: bool) {
65 self.trim_trailing_whitespace = trim;
66 }
67}
68
69impl Step for TextBlockStep {
70 fn initialize(&mut self, _dependency_state: &mut DependencyState, _index: usize) {}
71
72 fn render(
73 &self,
74 interface: &mut Interface,
75 _dependency_state: &DependencyState,
76 position: Position,
77 is_focused: bool,
78 ) -> u16 {
79 if !is_focused && self.text.value().is_empty() {
80 return 1;
81 }
82
83 let mut offset_y = 0;
84 if let Some(top_margin) = self.top_margin {
85 for line in 0..top_margin {
86 interface.clear_line(position.y() + line);
87 }
88
89 offset_y += top_margin;
90 }
91
92 let lines = self.text.lines();
93 for (line_index, line) in lines.iter().enumerate() {
94 let line_position = pos!(0, position.y() + line_index as u16 + offset_y);
95
96 if let Some(max_length) = self.max_line_length {
98 let line_length = line.len() as u16;
99 if line_length > max_length {
100 let mut segment = Text::new(line.to_string()).as_segment();
101
102 set_segment_subset_style(
103 &mut segment,
104 max_length.into(),
105 line_length.into(),
106 error_style(),
107 );
108
109 render_segment(interface, line_position, segment);
110 continue;
111 }
112 }
113
114 interface.set(line_position, line);
115 }
116
117 if is_focused {
118 let cursor = self.text.cursor();
119 let (x, y) = (cursor.0 as u16, cursor.1 as u16);
120 interface.set_cursor(Some(pos!(x, y + position.y() + offset_y)));
121 }
122
123 if let Some(bottom_margin) = self.bottom_margin {
124 for line in 0..bottom_margin {
125 interface.clear_line(position.y() + line + offset_y + lines.len() as u16);
126 }
127
128 offset_y += bottom_margin;
129 }
130
131 lines.len() as u16 + offset_y
132 }
133
134 fn update(
135 &mut self,
136 _dependency_state: &mut DependencyState,
137 input: KeyEvent,
138 ) -> Option<InputResult> {
139 if input.code == KeyCode::Enter || input.code == KeyCode::Tab {
141 let lines = self.text.lines().to_vec();
142 if lines.len() >= 2 {
143 let last_lines_empty =
144 lines[lines.len() - 1].is_empty() && lines[lines.len() - 2].is_empty();
145
146 if last_lines_empty {
147 if self.trim_trailing_whitespace {
149 self.text.handle_input(Key::Backspace);
150 self.text.handle_input(Key::Backspace);
151 }
152
153 return Some(InputResult::AdvanceForm);
154 }
155 }
156 }
157
158 if input.code == KeyCode::Esc || input.code == KeyCode::BackTab {
159 return Some(InputResult::RetreatForm);
160 }
161
162 match input.code {
163 KeyCode::Enter => self.text.handle_input(Key::Enter),
164 KeyCode::Char(ch) => self.text.handle_input(Key::Char(ch)),
165 KeyCode::Backspace => self.text.handle_input(Key::Backspace),
166 KeyCode::Up => self.text.handle_input(Key::Up),
167 KeyCode::Down => self.text.handle_input(Key::Down),
168 KeyCode::Left => self.text.handle_input(Key::Left),
169 KeyCode::Right => self.text.handle_input(Key::Right),
170 _ => {}
171 };
172
173 None
174 }
175
176 fn help(&self) -> Segment {
177 Text::new_styled(self.prompt.to_string(), help_style()).as_segment()
178 }
179
180 fn drawer(&self) -> Option<DrawerContents> {
181 None
182 }
183
184 fn result(&self, _dependency_state: &DependencyState) -> String {
185 if self.text.value().is_empty() {
186 return "\n".to_string();
187 }
188
189 let mut result = String::new();
190
191 if let Some(top_margin) = self.top_margin {
192 for _ in 0..top_margin {
193 result.push('\n');
194 }
195 }
196
197 result.push_str(&self.text.value());
198
199 if let Some(bottom_margin) = self.bottom_margin {
200 for _ in 0..bottom_margin + 1 {
201 result.push('\n');
202 }
203 }
204
205 result
206 }
207
208 fn add_to(self, form: &mut Form) {
209 form.add_step(Box::new(self));
210 }
211}