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