1use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
2use crossterm::{
3 cursor, execute, queue,
4 style::ResetColor,
5 terminal::{self, disable_raw_mode, enable_raw_mode},
6};
7use std::cmp;
8use std::io::{self, stdout, Write};
9
10fn get_slice_of_string(text: &str, start: usize, end: usize) -> String {
12 let mut new_text = String::new();
13 let length = text.chars().count();
14
15 for i in start..end {
16 if i >= length {
17 break;
18 }
19 new_text.insert(
20 new_text.len(),
21 text.chars().nth(i).expect("Char at pos should exist"),
22 );
23 }
24 new_text
25}
26
27pub fn set_terminal_line(
29 text: &str,
30 x: usize,
31 y: usize,
32 overwrite: bool,
33) -> Result<(), std::io::Error> {
34 if overwrite {
35 queue!(
36 stdout(),
37 cursor::MoveTo(x as u16, y as u16),
38 terminal::Clear(terminal::ClearType::CurrentLine)
39 )?;
40 print!("{text}");
41 } else {
42 queue!(stdout(), cursor::MoveTo(x as u16, y as u16))?;
43 print!("{text}");
44 }
45 Ok(())
46}
47
48pub struct DefaultInputHandler;
50impl CustomInputHandler for DefaultInputHandler {}
51
52pub enum KeyPressResult {
54 Handled,
56 Stop,
58 Continue,
60}
61
62pub struct HandlerContext<'a> {
64 pub text_data: &'a mut TextInputData,
65 pub terminal_size: &'a (u16, u16),
66}
67
68pub struct InputTransform {
70 pub size: (u16, u16),
71 pub offset: (u16, u16),
72}
73
74#[allow(unused_variables)]
76pub trait CustomInputHandler {
77 fn handle_key_press(&mut self, key: &Event, ctx: HandlerContext) -> KeyPressResult {
79 if let Event::Key(key_event) = key {
80 if key_event.kind == KeyEventKind::Press {
81 if let KeyCode::Esc = key_event.code {
83 return KeyPressResult::Stop;
84 }
85
86 if let KeyCode::Char(c) = key_event.code {
88 if c == 'c' && key_event.modifiers.contains(KeyModifiers::CONTROL) {
89 return KeyPressResult::Stop;
90 }
91 }
92 }
93 }
94 KeyPressResult::Continue
95 }
96 fn before_draw_text(&mut self, ctx: HandlerContext) {
98 let _ = queue!(stdout(), ResetColor);
99 }
100 fn after_draw_text(&mut self, ctx: HandlerContext) {}
102 fn after_update_cursor(&mut self, ctx: HandlerContext) {}
104 fn get_input_transform(&mut self, ctx: HandlerContext) -> InputTransform {
106 let size = *ctx.terminal_size;
107 let offset = (0, 0);
108 InputTransform { size, offset }
109 }
110}
111
112pub struct TextInputData {
114 pub text: String,
115 pub cursor_x: usize,
116 pub cursor_y: usize,
117 pub tab_width: usize,
118}
119
120pub struct CoolInput<H: CustomInputHandler> {
122 pub text_data: TextInputData,
123 pub scroll_x: usize,
124 pub scroll_y: usize,
125 pub listening: bool,
126 pub custom_input: H,
127}
128
129impl TextInputData {
130 pub fn write_char(&mut self, c: char) -> Result<(), std::io::Error> {
131 self.insert_char(c, self.cursor_x, self.cursor_y);
132 self.move_cursor_right()?;
133 Ok(())
134 }
135 pub fn insert_char(&mut self, c: char, x: usize, y: usize) {
136 let mut new = String::new();
137 let mut cur_x = 0;
138 let mut cur_y = 0;
139
140 if x == 0 && y == 0 {
141 self.text.insert(0, c);
142 } else {
143 for char in self.text.chars() {
144 cur_x += 1;
145 if char == '\n' {
146 cur_y += 1;
147 cur_x = 0;
148 }
149 new.insert(new.len(), char);
150 if cur_x == x && cur_y == y {
151 new.insert(new.len(), c);
152 }
153 }
154 self.text = new;
155 }
156 }
157 pub fn remove_character(&mut self, x: usize, y: usize) -> Result<(), std::io::Error> {
158 let mut new = String::new();
159 let mut cur_x = 0;
160 let mut cur_y = 0;
161
162 if x == 0 {
163 self.move_cursor_up()?;
164 self.cursor_x = self.get_current_line_length()?;
165 } else {
166 self.move_cursor_left()?;
167 }
168
169 if !self.text.is_empty() {
170 for char in self.text.chars() {
171 cur_x += 1;
172 if char == '\n' {
173 cur_y += 1;
174 cur_x = 0;
175 }
176 if cur_x != x || cur_y != y {
177 new.insert(new.len(), char);
178 }
179 }
180 }
181 self.text = new;
182 Ok(())
183 }
184 fn move_cursor_end(&mut self) -> Result<(), std::io::Error> {
185 if self.get_amt_lines() > 0 {
186 self.cursor_x = self.get_current_line_length()?;
187 }
188 Ok(())
189 }
190 fn move_cursor_up(&mut self) -> Result<(), std::io::Error> {
191 if self.cursor_y > 0 {
192 self.cursor_y -= 1;
193 self.cursor_x = cmp::min(self.get_current_line_length()?, self.cursor_x);
194 } else {
195 self.cursor_x = 0;
196 }
197 Ok(())
198 }
199 fn move_cursor_down(&mut self) -> Result<(), std::io::Error> {
200 if self.cursor_y < self.get_amt_lines() - 1 {
201 self.cursor_y += 1;
202 self.cursor_x = cmp::min(self.get_current_line_length()?, self.cursor_x);
203 } else {
204 self.move_cursor_end()?;
205 }
206 Ok(())
207 }
208 fn move_cursor_left(&mut self) -> Result<(), std::io::Error> {
209 if self.cursor_x > 0 || self.cursor_y != 0 {
210 if self.cursor_x > 0 {
211 self.cursor_x -= 1;
212 } else {
213 self.cursor_y -= 1;
214 self.cursor_x = self.get_current_line_length()?;
215 }
216 }
217 Ok(())
218 }
219 fn move_cursor_right(&mut self) -> Result<(), std::io::Error> {
220 if self.cursor_y != self.get_amt_lines() - 1
221 || self.cursor_x < self.get_current_line_length()?
222 {
223 if self.cursor_x != self.get_current_line_length()? {
224 self.cursor_x += 1;
225 } else {
226 self.cursor_y += 1;
227 self.cursor_x = 0;
228 }
229 }
230
231 Ok(())
232 }
233 pub fn get_amt_lines(&mut self) -> usize {
234 let mut amt = self.text.lines().count();
235 if self.text.ends_with("\n") {
236 amt += 1;
237 }
238 amt
239 }
240 pub fn get_line_at(&mut self, y: usize) -> Option<&str> {
241 if self.text.ends_with("\n") && y == self.text.lines().count() {
242 return Some("");
243 }
244 self.text.lines().nth(y)
245 }
246 pub fn get_current_line_length(&mut self) -> Result<usize, std::io::Error> {
247 let line = self.get_line_at(self.cursor_y);
248 match line {
249 Some(text) => Ok(text.chars().count()),
250 None => Err(std::io::Error::new(
251 io::ErrorKind::Other,
252 "Couldn't get length of current line because it doesn't exist.",
253 )),
254 }
255 }
256 fn handle_key_press(&mut self, key_event: KeyEvent) -> Result<(), std::io::Error> {
257 match key_event.code {
258 KeyCode::Char(c) => {
259 self.insert_char(c, self.cursor_x, self.cursor_y);
260 self.move_cursor_right()?;
261 }
262 KeyCode::Enter => {
263 self.insert_char('\n', self.cursor_x, self.cursor_y);
264 self.cursor_y += 1;
265 self.cursor_x = 0;
266 }
267 KeyCode::Backspace => {
268 if self.cursor_x > 0 || self.cursor_y != 0 {
269 self.remove_character(self.cursor_x, self.cursor_y)?;
270 }
271 }
272 KeyCode::Tab => {
273 for _ in 0..self.tab_width {
274 self.insert_char(' ', self.cursor_x, self.cursor_y);
275 }
276 self.cursor_x += self.tab_width;
277 }
278 KeyCode::Delete => {
279 if self.get_amt_lines() > 0 {
280 let line_length = self.get_current_line_length()?;
281 if self.cursor_x < line_length || self.cursor_y != self.get_amt_lines() - 1 {
282 if self.cursor_x == line_length {
283 self.cursor_x = 0;
284 self.cursor_y += 1;
285 } else {
286 self.cursor_x += 1;
287 }
288 self.remove_character(self.cursor_x, self.cursor_y)?;
289 }
290 }
291 }
292 KeyCode::Up => {
293 self.move_cursor_up()?;
294 }
295 KeyCode::Down => {
296 if self.get_amt_lines() > 0 {
297 self.move_cursor_down()?;
298 }
299 }
300 KeyCode::Left => {
301 self.move_cursor_left()?;
302 }
303 KeyCode::Right if self.get_amt_lines() > 0 => {
304 self.move_cursor_right()?;
305 }
306 KeyCode::Home => {
307 self.cursor_x = 0;
308 }
309 KeyCode::End => {
310 self.move_cursor_end()?;
311 }
312 _ => {}
313 }
314 Ok(())
315 }
316}
317
318impl<H: CustomInputHandler> CoolInput<H> {
319 pub fn new(handler: H, tab_width: usize) -> Self {
320 CoolInput {
321 text_data: TextInputData {
322 text: String::new(),
323 cursor_x: 0,
324 cursor_y: 0,
325 tab_width,
326 },
327 listening: false,
328 scroll_x: 0,
329 scroll_y: 0,
330 custom_input: handler,
331 }
332 }
333 pub fn get_terminal_size(&mut self) -> Result<(u16, u16), std::io::Error> {
335 let mut terminal_size = terminal::size()?;
336 terminal_size.1 -= 1;
337 Ok(terminal_size)
338 }
339 pub fn get_input_transform(&mut self) -> Result<InputTransform, std::io::Error> {
340 let terminal_size = self.get_terminal_size()?;
341 let input_transform = self.custom_input.get_input_transform(HandlerContext {
342 text_data: &mut self.text_data,
343 terminal_size: &terminal_size,
344 });
345 let mut size = input_transform.size;
346 let offset = input_transform.offset;
347 if size.0 + offset.0 > terminal_size.0 {
348 size.0 = terminal_size.0.saturating_sub(offset.0);
349 }
350 if size.1 + offset.1 > terminal_size.1 {
351 size.1 = terminal_size.1.saturating_sub(offset.1);
352 }
353 Ok(InputTransform { size, offset })
354 }
355 pub fn render(&mut self) -> Result<(), std::io::Error> {
357 self.update_text()?;
358 self.update_cursor()?;
359 io::stdout().flush()?;
360 Ok(())
361 }
362 fn update_cursor(&mut self) -> Result<(), std::io::Error> {
363 let terminal_size = self.get_terminal_size()?;
364 let input_transform = self.get_input_transform()?;
365
366 let x =
367 self.text_data.cursor_x as i16 + input_transform.offset.0 as i16 - self.scroll_x as i16;
368 let x: u16 = cmp::max(x, 0_i16) as u16;
369 let x = cmp::min(x, input_transform.offset.0 + input_transform.size.0);
370 let target_y = (self.text_data.cursor_y as u16) + input_transform.offset.1;
371 let target_y = target_y.saturating_sub(self.scroll_y as u16);
372 let y = cmp::min(
373 cmp::min(
374 target_y,
375 input_transform.offset.1 + input_transform.size.1 - 1,
376 ),
377 terminal_size.1 - 1,
378 );
379 queue!(stdout(), cursor::Show)?;
380 queue!(stdout(), cursor::MoveTo(x, y))?;
381
382 self.custom_input.after_update_cursor(HandlerContext {
383 text_data: &mut self.text_data,
384 terminal_size: &terminal_size,
385 });
386 Ok(())
387 }
388 fn update_text(&mut self) -> Result<(), std::io::Error> {
389 let terminal_size = self.get_terminal_size()?;
390 let input_transform = self.get_input_transform()?;
391
392 self.custom_input.before_draw_text(HandlerContext {
393 text_data: &mut self.text_data,
394 terminal_size: &terminal_size,
395 });
396
397 let offset_y = input_transform.offset.1 as i16;
398 for y in offset_y..offset_y + (input_transform.size.1 as i16) {
399 let y_line_index = y - offset_y + (self.scroll_y as i16);
400 if y_line_index >= 0 && y_line_index < (self.text_data.text.lines().count() as i16) {
401 if let Some(line) = self.text_data.get_line_at(y_line_index as usize) {
402 let text = get_slice_of_string(
403 line,
404 self.scroll_x,
405 self.scroll_x + input_transform.size.0 as usize,
406 );
407 set_terminal_line(&text, input_transform.offset.0 as usize, y as usize, true)?;
408 }
409 } else {
410 set_terminal_line("", input_transform.offset.0 as usize, y as usize, true)?;
411 }
412 }
413
414 self.custom_input.after_draw_text(HandlerContext {
415 text_data: &mut self.text_data,
416 terminal_size: &terminal_size,
417 });
418
419 Ok(())
420 }
421 fn scroll_in_view(
422 &mut self,
423 moving_right: bool,
424 moving_down: bool,
425 ) -> Result<(), std::io::Error> {
426 let input_transform = self.get_input_transform()?;
427 self.scroll_x = self.keep_scroll_axis_in_view(
428 self.scroll_x,
429 self.text_data.cursor_x,
430 input_transform.size.0 as usize,
431 moving_right,
432 )?;
433 self.scroll_y = self.keep_scroll_axis_in_view(
434 self.scroll_y,
435 self.text_data.cursor_y,
436 input_transform.size.1 as usize,
437 moving_down,
438 )?;
439 Ok(())
440 }
441 fn keep_scroll_axis_in_view(
442 &mut self,
443 scroll_amt: usize,
444 cursor_pos: usize,
445 bounds: usize,
446 moving_direction: bool,
447 ) -> Result<usize, std::io::Error> {
448 let mut scroll_amt = scroll_amt;
449 if moving_direction {
450 if cursor_pos > bounds - 1 {
451 scroll_amt = cmp::max(scroll_amt, cursor_pos - bounds + 1);
452 }
453 } else if cursor_pos < scroll_amt {
454 scroll_amt = cursor_pos;
455 }
456 Ok(scroll_amt)
457 }
458 pub fn handle_key_press(&mut self, key: Event) -> Result<(), std::io::Error> {
460 let terminal_size = self.get_terminal_size()?;
461 let old_cursor_x = self.text_data.cursor_x;
462 let old_cursor_y = self.text_data.cursor_y;
463 match self.custom_input.handle_key_press(
464 &key,
465 HandlerContext {
466 text_data: &mut self.text_data,
467 terminal_size: &terminal_size,
468 },
469 ) {
470 KeyPressResult::Handled => {
471 self.scroll_in_view(
472 self.text_data.cursor_x > old_cursor_x,
473 self.text_data.cursor_y > old_cursor_y,
474 )?;
475 self.render()?;
476 return Ok(());
477 }
478 KeyPressResult::Stop => {
479 self.listening = false;
480 return Ok(());
481 }
482 KeyPressResult::Continue => {
483 if let Event::Key(key_event) = key {
484 if key_event.kind == KeyEventKind::Press {
485 self.text_data.handle_key_press(key_event)?;
486 self.scroll_in_view(
487 self.text_data.cursor_x > old_cursor_x,
488 self.text_data.cursor_y > old_cursor_y,
489 )?;
490 self.render()?;
491 }
492 }
493 }
494 }
495 Ok(())
496 }
497 pub fn listen_quiet(&mut self) -> Result<(), std::io::Error> {
499 self.listening = true;
500 while self.listening {
501 self.handle_key_press(event::read()?)?;
502 }
503 Ok(())
504 }
505 pub fn pre_listen(&mut self) -> Result<(), std::io::Error> {
507 let input_transform = self.get_input_transform()?;
508 enable_raw_mode()?;
509 execute!(
510 stdout(),
511 terminal::Clear(terminal::ClearType::All),
512 cursor::MoveTo(
513 (self.text_data.cursor_x as u16) + input_transform.offset.0,
514 (self.text_data.cursor_y as u16) + input_transform.offset.1
515 )
516 )?;
517 Ok(())
518 }
519 pub fn post_listen(&mut self) -> Result<(), std::io::Error> {
521 execute!(
522 stdout(),
523 ResetColor,
524 terminal::Clear(terminal::ClearType::All),
525 cursor::MoveTo(0, 0),
526 cursor::Show,
527 )?;
528 disable_raw_mode()?;
529 Ok(())
530 }
531 pub fn listen(&mut self) -> Result<(), std::io::Error> {
533 self.pre_listen()?;
534 self.render()?;
535 self.listen_quiet()?;
536 self.post_listen()?;
537 Ok(())
538 }
539}