Skip to main content

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}