typout/output.rs
1use std::collections::HashMap;
2use std::io::{stdout, Write, Stdout};
3use crossterm::style::{Print};
4use crossterm::execute;
5use crossterm::cursor::{MoveToColumn, MoveUp, position};
6use crossterm::terminal::{Clear, ClearType};
7use crate::{OutputIntent};
8
9/// Output represents a wrapper around the standard output of the current
10/// process and is responsible for painting massages to the terminal.
11pub struct Output {
12 /// Line messages are stored in this internal buffer. The buffered data is
13 /// painted to the screen on flush, which also clears the buffer.
14 buffer: String,
15
16 /// One or more messages can be pinned to the end of the output stream.
17 /// These messages are inserted to the hash map with a unique id where each
18 /// entry represents one pinned message.
19 pins: HashMap<String, String>,
20
21 /// All messages are streamed to the standard output of the current process.
22 stdout: Stdout,
23
24 /// The handler needs to be aware of the last permanent character position.
25 /// This are the (left, bottom) coordinates in the terminal where the last
26 /// character, which will not be cleared from the screen anymore, is
27 /// located. The position is dynamically updated when data are painted on
28 /// the screen.
29 position: (u16, u16),
30}
31
32impl Output {
33 /// Handles the received intent.
34 pub fn handle(&mut self, intent: OutputIntent) {
35 match intent {
36 OutputIntent::Write(data) => self.write(&data),
37 OutputIntent::Drain => self.drain(),
38 OutputIntent::Pin(id, data) => self.pin(&id, &data),
39 OutputIntent::Unpin(id) => self.unpin(&id),
40 OutputIntent::Flush => self.flush(),
41 OutputIntent::Exit => {}, // handled by the Term struct
42 }
43 }
44
45 /// Writes the received data to the internal message buffer. The buffer is
46 /// sent to the output stream when calling the `flush()` method.
47 fn write(&mut self, data: &str) {
48 self.buffer.push_str(&data);
49 }
50
51 /// Removes data from the message buffer.
52 fn drain(&mut self) {
53 self.buffer.clear();
54 }
55
56 /// Sends the buffered message to the standard output of the current process
57 /// which displays the message in the terminal. The message buffer is
58 /// cleared afterwards.
59 fn flush(&mut self) {
60 self.clear_pins();
61
62 execute!(self.stdout, Print(&self.buffer)).unwrap();
63 self.buffer.clear();
64
65 let (left, _) = position().unwrap();
66 self.position = (left, 0);
67
68 self.paint_pins();
69 }
70
71 /// Creates a new pinned message or updates an existing one. Pinned messages
72 /// always stayed visible at the end of the output stream. An arbitrary
73 /// number of pinned messages is allowed. Pins are uniquely identified by
74 /// the received `id` parameter.
75 fn pin(&mut self, id: &str, data: &str) {
76 self.pins.insert(id.to_string(), data.to_string());
77 self.clear_pins();
78 self.paint_pins();
79 }
80
81 /// Removes a pinned message with the provided `id`.
82 fn unpin(&mut self, id: &str) {
83 self.pins.remove(id);
84 self.clear_pins();
85 self.paint_pins();
86 }
87
88 /// Displays all pinned messages in the terminal.
89 fn paint_pins(&mut self) {
90 let stdout = &mut self.stdout;
91 for (_, value) in self.pins.iter() {
92 self.position.1 += value.matches("\n").count() as u16;
93 execute!(stdout, Print(value)).unwrap();
94 }
95 }
96
97 /// Clears all pinned messages from the terminal.
98 fn clear_pins(&mut self) {
99 if self.position.1 > 0 {
100 execute!(self.stdout, MoveUp(self.position.1)).unwrap();
101 }
102 execute!(self.stdout, MoveToColumn(self.position.0)).unwrap();
103 self.position.1 = 0;
104
105 execute!(self.stdout, Clear(ClearType::UntilNewLine)).unwrap();
106 execute!(self.stdout, Clear(ClearType::FromCursorDown)).unwrap();
107 }
108}
109
110impl Default for Output {
111 fn default() -> Self {
112 Self {
113 buffer: String::new(),
114 pins: HashMap::new(),
115 stdout: stdout(),
116 position: (0, 0),
117 }
118 }
119}