1use crate::{CursorConfig, PromptEditMode, PromptViMode};
2
3use {
4 super::utils::{coerce_crlf, line_width},
5 crate::{
6 menu::{Menu, ReedlineMenu},
7 painting::PromptLines,
8 Prompt,
9 },
10 crossterm::{
11 cursor::{self, MoveTo, RestorePosition, SavePosition},
12 style::{Attribute, Print, ResetColor, SetAttribute, SetForegroundColor},
13 terminal::{self, Clear, ClearType},
14 QueueableCommand,
15 },
16 std::io::{Result, Write},
17};
18#[cfg(feature = "external_printer")]
19use {crate::LineBuffer, crossterm::cursor::MoveUp};
20
21fn skip_buffer_lines(string: &str, skip: usize, offset: Option<usize>) -> &str {
24 let mut matches = string.match_indices('\n');
25 let index = if skip == 0 {
26 0
27 } else {
28 matches
29 .clone()
30 .nth(skip - 1)
31 .map(|(index, _)| index + 1)
32 .unwrap_or(string.len())
33 };
34
35 let limit = match offset {
36 Some(offset) => {
37 let offset = skip + offset;
38 matches
39 .nth(offset)
40 .map(|(index, _)| index)
41 .unwrap_or(string.len())
42 }
43 None => string.len(),
44 };
45
46 string[index..limit].trim_end_matches('\n')
47}
48
49pub type W = std::io::BufWriter<std::io::Stderr>;
51
52pub struct Painter {
54 stdout: W,
56 prompt_start_row: u16,
57 terminal_size: (u16, u16),
58 last_required_lines: u16,
59 large_buffer: bool,
60}
61
62impl Painter {
63 pub(crate) fn new(stdout: W) -> Self {
64 Painter {
65 stdout,
66 prompt_start_row: 0,
67 terminal_size: (0, 0),
68 last_required_lines: 0,
69 large_buffer: false,
70 }
71 }
72
73 pub fn screen_height(&self) -> u16 {
75 self.terminal_size.1
76 }
77
78 pub fn screen_width(&self) -> u16 {
80 self.terminal_size.0
81 }
82
83 pub fn remaining_lines(&self) -> u16 {
85 self.screen_height().saturating_sub(self.prompt_start_row)
86 }
87
88 pub(crate) fn initialize_prompt_position(&mut self) -> Result<()> {
94 self.terminal_size = {
96 let size = terminal::size()?;
97 if size == (0, 0) {
100 (80, 24)
101 } else {
102 size
103 }
104 };
105 let (column, row) = cursor::position()?;
107 let new_row = if column > 0 { row + 1 } else { row };
110 let new_row = if new_row == self.screen_height() {
115 self.print_crlf()?;
116 new_row.saturating_sub(1)
117 } else {
118 new_row
119 };
120 self.prompt_start_row = new_row;
121 Ok(())
122 }
123
124 pub(crate) fn repaint_buffer(
134 &mut self,
135 prompt: &dyn Prompt,
136 lines: &PromptLines,
137 prompt_mode: PromptEditMode,
138 menu: Option<&ReedlineMenu>,
139 use_ansi_coloring: bool,
140 cursor_config: &Option<CursorConfig>,
141 ) -> Result<()> {
142 self.stdout.queue(cursor::Hide)?;
143
144 let screen_width = self.screen_width();
145 let screen_height = self.screen_height();
146
147 let remaining_lines = self.remaining_lines();
149 let required_lines = lines.required_lines(screen_width, menu);
150
151 self.large_buffer = required_lines >= screen_height;
153
154 let is_reset = || match cursor::position() {
156 Ok(position) => position.1 + 1 < self.prompt_start_row,
160 Err(_) => false,
161 };
162
163 if self.large_buffer || is_reset() {
165 self.prompt_start_row = 0;
166 } else if required_lines >= remaining_lines {
167 let extra = required_lines.saturating_sub(remaining_lines);
168 self.queue_universal_scroll(extra)?;
169 self.prompt_start_row = self.prompt_start_row.saturating_sub(extra);
170 }
171
172 self.stdout
175 .queue(cursor::MoveTo(0, self.prompt_start_row))?
176 .queue(Clear(ClearType::FromCursorDown))?;
177
178 if self.large_buffer {
179 self.print_large_buffer(prompt, lines, menu, use_ansi_coloring)?;
180 } else {
181 self.print_small_buffer(prompt, lines, menu, use_ansi_coloring)?;
182 }
183
184 self.last_required_lines = required_lines;
187
188 self.stdout.queue(RestorePosition)?;
189
190 if let Some(shapes) = cursor_config {
191 let shape = match &prompt_mode {
192 PromptEditMode::Emacs => shapes.emacs,
193 PromptEditMode::Vi(PromptViMode::Insert) => shapes.vi_insert,
194 PromptEditMode::Vi(PromptViMode::Normal) => shapes.vi_normal,
195 _ => None,
196 };
197 if let Some(shape) = shape {
198 self.stdout.queue(shape)?;
199 }
200 }
201 self.stdout.queue(cursor::Show)?;
202
203 self.stdout.flush()
204 }
205
206 fn print_right_prompt(&mut self, lines: &PromptLines) -> Result<()> {
207 let prompt_length_right = line_width(&lines.prompt_str_right);
208 let start_position = self
209 .screen_width()
210 .saturating_sub(prompt_length_right as u16);
211 let screen_width = self.screen_width();
212 let input_width = lines.estimate_right_prompt_line_width(screen_width);
213
214 let mut row = self.prompt_start_row;
215 if lines.right_prompt_on_last_line {
216 row += lines.prompt_lines_with_wrap(screen_width);
217 }
218
219 if input_width <= start_position {
220 self.stdout
221 .queue(SavePosition)?
222 .queue(cursor::MoveTo(start_position, row))?
223 .queue(Print(&coerce_crlf(&lines.prompt_str_right)))?
224 .queue(RestorePosition)?;
225 }
226
227 Ok(())
228 }
229
230 fn print_menu(
231 &mut self,
232 menu: &dyn Menu,
233 lines: &PromptLines,
234 use_ansi_coloring: bool,
235 ) -> Result<()> {
236 let screen_width = self.screen_width();
237 let screen_height = self.screen_height();
238 let cursor_distance = lines.distance_from_prompt(screen_width);
239
240 let starting_row = if cursor_distance >= screen_height.saturating_sub(1) {
243 screen_height.saturating_sub(menu.min_rows())
244 } else {
245 self.prompt_start_row + cursor_distance + 1
246 };
247
248 let remaining_lines = screen_height.saturating_sub(starting_row);
249 let menu_string = menu.menu_string(remaining_lines, use_ansi_coloring);
250 self.stdout
251 .queue(cursor::MoveTo(0, starting_row))?
252 .queue(Clear(ClearType::FromCursorDown))?
253 .queue(Print(menu_string.trim_end_matches('\n')))?;
254
255 Ok(())
256 }
257
258 fn print_small_buffer(
259 &mut self,
260 prompt: &dyn Prompt,
261 lines: &PromptLines,
262 menu: Option<&ReedlineMenu>,
263 use_ansi_coloring: bool,
264 ) -> Result<()> {
265 if use_ansi_coloring {
267 self.stdout
268 .queue(SetForegroundColor(prompt.get_prompt_color()))?;
269 }
270
271 self.stdout
272 .queue(Print(&coerce_crlf(&lines.prompt_str_left)))?;
273
274 let prompt_indicator = match menu {
275 Some(menu) => menu.indicator(),
276 None => &lines.prompt_indicator,
277 };
278
279 if use_ansi_coloring {
280 self.stdout
281 .queue(SetForegroundColor(prompt.get_indicator_color()))?;
282 }
283
284 self.stdout.queue(Print(&coerce_crlf(prompt_indicator)))?;
285
286 if use_ansi_coloring {
287 self.stdout
288 .queue(SetForegroundColor(prompt.get_prompt_right_color()))?;
289 }
290
291 self.print_right_prompt(lines)?;
292
293 if use_ansi_coloring {
294 self.stdout
295 .queue(SetAttribute(Attribute::Reset))?
296 .queue(ResetColor)?;
297 }
298
299 self.stdout
300 .queue(Print(&lines.before_cursor))?
301 .queue(SavePosition)?
302 .queue(Print(&lines.after_cursor))?;
303
304 if let Some(menu) = menu {
305 self.print_menu(menu, lines, use_ansi_coloring)?;
306 } else {
307 self.stdout.queue(Print(&lines.hint))?;
308 }
309
310 Ok(())
311 }
312
313 fn print_large_buffer(
314 &mut self,
315 prompt: &dyn Prompt,
316 lines: &PromptLines,
317 menu: Option<&ReedlineMenu>,
318 use_ansi_coloring: bool,
319 ) -> Result<()> {
320 let screen_width = self.screen_width();
321 let screen_height = self.screen_height();
322 let cursor_distance = lines.distance_from_prompt(screen_width);
323 let remaining_lines = screen_height.saturating_sub(cursor_distance);
324
325 let prompt_lines = lines.prompt_lines_with_wrap(screen_width) as usize;
329
330 let prompt_indicator = match menu {
331 Some(menu) => menu.indicator(),
332 None => &lines.prompt_indicator,
333 };
334
335 let prompt_indicator_lines = prompt_indicator.lines().count();
336 let before_cursor_lines = lines.before_cursor.lines().count();
337 let total_lines_before = prompt_lines + prompt_indicator_lines + before_cursor_lines - 1;
338
339 let extra_rows = (total_lines_before).saturating_sub(screen_height as usize);
341
342 if use_ansi_coloring {
344 self.stdout
345 .queue(SetForegroundColor(prompt.get_prompt_color()))?;
346 }
347
348 let prompt_skipped = skip_buffer_lines(&lines.prompt_str_left, extra_rows, None);
351 self.stdout.queue(Print(&coerce_crlf(prompt_skipped)))?;
352
353 if extra_rows == 0 {
354 self.print_right_prompt(lines)?;
355 }
356
357 let extra_rows = extra_rows.saturating_sub(prompt_lines);
359
360 let indicator_skipped = skip_buffer_lines(prompt_indicator, extra_rows, None);
361 self.stdout.queue(Print(&coerce_crlf(indicator_skipped)))?;
362
363 if use_ansi_coloring {
364 self.stdout.queue(ResetColor)?;
365 }
366
367 let offset = menu.and_then(|menu| {
371 if cursor_distance >= screen_height.saturating_sub(1) {
372 let rows = lines
373 .before_cursor
374 .lines()
375 .count()
376 .saturating_sub(extra_rows)
377 .saturating_sub(menu.min_rows() as usize);
378 Some(rows)
379 } else {
380 None
381 }
382 });
383
384 let before_cursor_skipped = skip_buffer_lines(&lines.before_cursor, extra_rows, offset);
386 self.stdout.queue(Print(before_cursor_skipped))?;
387 self.stdout.queue(SavePosition)?;
388
389 if let Some(menu) = menu {
390 self.print_menu(menu, lines, use_ansi_coloring)?;
393 } else {
394 let offset = remaining_lines.saturating_sub(1) as usize;
399 let after_cursor_skipped = skip_buffer_lines(&lines.after_cursor, 0, Some(offset));
401 self.stdout.queue(Print(after_cursor_skipped))?;
402 let hint_skipped = skip_buffer_lines(&lines.hint, 0, Some(offset));
404 self.stdout.queue(Print(hint_skipped))?;
405 }
406
407 Ok(())
408 }
409
410 pub(crate) fn handle_resize(&mut self, width: u16, height: u16) {
412 self.terminal_size = (width, height);
413
414 if let Ok(position) = cursor::position() {
426 self.prompt_start_row = position.1;
427 }
428 }
429
430 pub(crate) fn paint_line(&mut self, line: &str) -> Result<()> {
432 self.stdout.queue(Print(line))?.queue(Print("\r\n"))?;
433
434 self.stdout.flush()
435 }
436
437 pub(crate) fn print_crlf(&mut self) -> Result<()> {
441 self.stdout.queue(Print("\r\n"))?;
442
443 self.stdout.flush()
444 }
445
446 pub(crate) fn clear_screen(&mut self) -> Result<()> {
449 self.stdout.queue(cursor::Hide)?;
450 let (_, num_lines) = terminal::size()?;
451 for _ in 0..2 * num_lines {
452 self.stdout.queue(Print("\n"))?;
453 }
454 self.stdout.queue(MoveTo(0, 0))?;
455 self.stdout.queue(cursor::Show)?;
456
457 self.stdout.flush()?;
458 self.initialize_prompt_position()
459 }
460
461 pub(crate) fn clear_scrollback(&mut self) -> Result<()> {
462 self.stdout
463 .queue(crossterm::terminal::Clear(ClearType::All))?
464 .queue(crossterm::terminal::Clear(ClearType::Purge))?
465 .queue(cursor::MoveTo(0, 0))?
466 .flush()?;
467 self.initialize_prompt_position()
468 }
469
470 pub(crate) fn move_cursor_to_end(&mut self) -> Result<()> {
474 let final_row = self.prompt_start_row + self.last_required_lines;
475 let scroll = final_row.saturating_sub(self.screen_height() - 1);
476 if scroll != 0 {
477 self.queue_universal_scroll(scroll)?;
478 }
479 self.stdout
480 .queue(MoveTo(0, final_row.min(self.screen_height() - 1)))?;
481
482 self.stdout.flush()
483 }
484
485 #[cfg(feature = "external_printer")]
490 pub(crate) fn print_external_message(
491 &mut self,
492 messages: Vec<String>,
493 line_buffer: &LineBuffer,
494 prompt: &dyn Prompt,
495 ) -> Result<()> {
496 let prompt_len = prompt.render_prompt_right().len() + 3;
498 let mut buffer_num_lines = 0_u16;
499 for (i, line) in line_buffer.get_buffer().lines().enumerate() {
500 let screen_lines = match i {
501 0 => {
502 let first_line_len = line.len() + prompt_len;
504 ((first_line_len as u16) / (self.screen_width())) + 1
506 }
507 _ => {
508 ((line.len() as u16) / self.screen_width()) + 1
510 }
511 };
512 buffer_num_lines = buffer_num_lines.saturating_add(screen_lines);
514 }
515 if buffer_num_lines > 1 {
517 self.stdout.queue(MoveUp(buffer_num_lines - 1))?;
518 }
519 let erase_line = format!("\r{}\r", " ".repeat(self.screen_width().into()));
520 for line in messages {
521 self.stdout.queue(Print(&erase_line))?;
522 self.stdout.queue(Print(line))?.queue(Print("\r\n"))?;
527 let new_start = self.prompt_start_row.saturating_add(1);
528 let height = self.screen_height();
529 if new_start >= height {
530 self.prompt_start_row = height - 1;
531 } else {
532 self.prompt_start_row = new_start;
533 }
534 }
535 Ok(())
536 }
537
538 fn queue_universal_scroll(&mut self, num: u16) -> Result<()> {
551 self.stdout.queue(MoveTo(0, self.screen_height() - 1))?;
554 for _ in 0..num {
555 self.stdout.queue(Print(&coerce_crlf("\n")))?;
556 }
557 Ok(())
558 }
559}
560
561#[cfg(test)]
562mod tests {
563 use super::*;
564 use pretty_assertions::assert_eq;
565
566 #[test]
567 fn test_skip_lines() {
568 let string = "sentence1\nsentence2\nsentence3\n";
569
570 assert_eq!(skip_buffer_lines(string, 1, None), "sentence2\nsentence3");
571 assert_eq!(skip_buffer_lines(string, 2, None), "sentence3");
572 assert_eq!(skip_buffer_lines(string, 3, None), "");
573 assert_eq!(skip_buffer_lines(string, 4, None), "");
574 }
575
576 #[test]
577 fn test_skip_lines_no_newline() {
578 let string = "sentence1";
579
580 assert_eq!(skip_buffer_lines(string, 0, None), "sentence1");
581 assert_eq!(skip_buffer_lines(string, 1, None), "");
582 }
583
584 #[test]
585 fn test_skip_lines_with_limit() {
586 let string = "sentence1\nsentence2\nsentence3\nsentence4\nsentence5";
587
588 assert_eq!(
589 skip_buffer_lines(string, 1, Some(1)),
590 "sentence2\nsentence3",
591 );
592
593 assert_eq!(
594 skip_buffer_lines(string, 1, Some(2)),
595 "sentence2\nsentence3\nsentence4",
596 );
597
598 assert_eq!(
599 skip_buffer_lines(string, 2, Some(1)),
600 "sentence3\nsentence4",
601 );
602
603 assert_eq!(
604 skip_buffer_lines(string, 1, Some(10)),
605 "sentence2\nsentence3\nsentence4\nsentence5",
606 );
607
608 assert_eq!(
609 skip_buffer_lines(string, 0, Some(1)),
610 "sentence1\nsentence2",
611 );
612
613 assert_eq!(skip_buffer_lines(string, 0, Some(0)), "sentence1",);
614 assert_eq!(skip_buffer_lines(string, 1, Some(0)), "sentence2",);
615 }
616}