1
use crate::{Console, ConsoleSize, LineNumber, Position};
2
use std::cmp;
3
use std::fmt;
4
use std::io::{self, stdout, Write};
5
use termion::color;
6
use termion::cursor::{SteadyBar, SteadyBlock};
7
use termion::event::{Event, MouseEvent};
8
use termion::input::{MouseTerminal, TermRead};
9
use termion::raw::{IntoRawMode, RawTerminal};
10
use termion::screen::{AlternateScreen, ToAlternateScreen, ToMainScreen};
11

            
12
2
#[derive(Debug, PartialEq)]
13
pub struct AnsiPosition {
14
3
    pub x: u16,
15
4
    pub y: u16,
16
}
17

            
18
impl From<Position> for AnsiPosition {
19
    #[allow(clippy::cast_possible_truncation)]
20
1
    fn from(p: Position) -> Self {
21
1
        Self {
22
1
            x: (p.x as u16).saturating_add(1),
23
1
            y: (p.y as u16).saturating_add(1),
24
        }
25
1
    }
26
}
27

            
28
pub struct Terminal {
29
    _stdout: AlternateScreen<MouseTerminal<RawTerminal<std::io::Stdout>>>,
30
    stdin_event_stream: termion::input::Events<io::Stdin>,
31
}
32

            
33
impl fmt::Debug for Terminal {
34
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35
        f.debug_struct("Terminal").finish()
36
    }
37
}
38

            
39
impl Console for Terminal {
40
    fn clear_screen(&self) {
41
        print!("{}", termion::clear::All);
42
    }
43

            
44
    fn clear_current_line(&self) {
45
        print!("{}", termion::clear::CurrentLine);
46
    }
47

            
48
    /// # Errors
49
    ///
50
    /// Returns an error if stdout can't be flushed
51
    fn flush(&self) -> Result<(), std::io::Error> {
52
        std::io::stdout().flush()
53
    }
54

            
55
    /// # Errors
56
    ///
57
    /// Returns an error if a event can't be read
58
    fn read_event(&mut self) -> Result<Event, std::io::Error> {
59
        loop {
60
            let opt_key = self.stdin_event_stream.next();
61
            // at that point, event is a Result<Event, Error>, as the Option was unwrapped
62
            if let Some(event) = opt_key {
63
                return event;
64
            }
65
        }
66
    }
67

            
68
    fn hide_cursor(&self) {
69
        print!("{}", termion::cursor::Hide);
70
    }
71

            
72
    fn show_cursor(&self) {
73
        print!("{}", termion::cursor::Show);
74
    }
75

            
76
    fn set_bg_color(&self, color: color::Rgb) {
77
        print!("{}", color::Bg(color));
78
    }
79

            
80
    fn reset_bg_color(&self) {
81
        print!("{}", color::Bg(color::Reset));
82
    }
83

            
84
    fn set_fg_color(&self, color: color::Rgb) {
85
        print!("{}", color::Fg(color));
86
    }
87

            
88
    fn reset_fg_color(&self) {
89
        print!("{}", color::Fg(color::Reset));
90
    }
91

            
92
    fn to_alternate_screen(&self) {
93
        print!("{}", ToAlternateScreen);
94
    }
95

            
96
    fn to_main_screen(&self) {
97
        print!("{}", ToMainScreen);
98
    }
99

            
100
    fn clear_all(&self) {
101
        print!("{}", termion::clear::All);
102
    }
103

            
104
    fn size(&self) -> ConsoleSize {
105
        ConsoleSize::from(termion::terminal_size().unwrap_or_default())
106
    }
107

            
108
    fn text_area_size(&self) -> ConsoleSize {
109
        self.size().restrict_to_text_area()
110
    }
111

            
112
    fn middle_of_screen_line_number(&self) -> LineNumber {
113
        LineNumber::new(self.text_area_size().height as usize / 2)
114
    }
115

            
116
    fn bottom_of_screen_line_number(&self) -> LineNumber {
117
        LineNumber::new(self.text_area_size().height as usize)
118
    }
119

            
120
    fn set_cursor_position_in_text_area(&self, position: &Position, mut row_prefix_length: u8) {
121
        let ansi_position = AnsiPosition::from(*position);
122
        // hiding the fact that the terminal position is 1-based, while preventing an overflow
123
        row_prefix_length += if row_prefix_length > 0 { 1 } else { 0 };
124
        let text_area_size = self.size().restrict_to_text_area();
125
        print!(
126
            "{}",
127
            termion::cursor::Goto(
128
                cmp::min(
129
                    ansi_position.x.saturating_add(row_prefix_length.into()),
130
                    text_area_size.width
131
                ),
132
                cmp::min(ansi_position.y, text_area_size.height)
133
            )
134
        );
135
    }
136

            
137
    fn set_cursor_position_anywhere(&self, position: &Position) {
138
        let ansi_position = AnsiPosition::from(*position);
139
        let console_size = self.size();
140
        print!(
141
            "{}",
142
            termion::cursor::Goto(
143
                cmp::min(ansi_position.x, console_size.width),
144
                cmp::min(
145
                    ansi_position.y.saturating_add(2), // delta_y = 2 to account for the last 2 lines (status + message bars)
146
                    console_size.height.saturating_add(2)
147
                )
148
            )
149
        );
150
    }
151

            
152
    #[must_use]
153
    fn get_cursor_index_from_mouse_event(
154
        &self,
155
        mouse_event: MouseEvent,
156
        row_prefix_length: u8,
157
    ) -> Position {
158
        if let MouseEvent::Press(_, x, y) = mouse_event {
159
            let offset_adjustment: u8 = if row_prefix_length > 0 {
160
                row_prefix_length.saturating_add(1)
161
            } else {
162
                0
163
            };
164
            let ansi_position = AnsiPosition {
165
                x: x.saturating_sub(u16::from(offset_adjustment)),
166
                y,
167
            };
168
            Position::from(ansi_position)
169
        } else {
170
            Position::top_left()
171
        }
172
    }
173

            
174
    fn set_cursor_as_steady_bar(&self) {
175
        print!("{}", SteadyBar);
176
    }
177

            
178
    fn set_cursor_as_steady_block(&self) {
179
        print!("{}", SteadyBlock);
180
    }
181
}
182

            
183
impl Terminal {
184
    /// # Errors
185
    ///
186
    /// will return an error if the terminal size can't be acquired
187
    /// or if the stdout cannot be put into raw mode.
188
    pub fn default() -> Result<Self, std::io::Error> {
189
        let mut term_stdout = stdout();
190
        write!(term_stdout, "{}", termion::cursor::Goto(1, 1))?;
191
        term_stdout.flush()?;
192
        Ok(Self {
193
            _stdout: AlternateScreen::from(MouseTerminal::from(term_stdout.into_raw_mode()?)),
194
            stdin_event_stream: io::stdin().events(),
195
        })
196
    }
197
}
198

            
199
#[cfg(test)]
200
#[path = "./terminal_test.rs"]
201
mod terminal_test;