moins/
lib.rs

1use std::cell::RefCell;
2use std::collections::HashMap;
3use std::io::Write;
4use std::io::{stdin, stdout, Stdout};
5use termion::event::Key;
6use termion::input::{MouseTerminal, TermRead};
7use termion::raw::{IntoRawMode, RawTerminal};
8use termion::screen::AlternateScreen;
9
10type Terminal = RefCell<MouseTerminal<AlternateScreen<RawTerminal<Stdout>>>>;
11
12pub struct Moins<'a> {
13    lines: Vec<&'a str>,
14    height: u16,
15    width: u16,
16    current_line: usize,
17    scroll: usize,
18    screen: Terminal,
19    options: Option<PagerOptions<'a>>,
20}
21
22/// options for `Moins` see the examples
23pub struct PagerOptions<'a> {
24    /// add color to the matching term
25    pub colors: HashMap<&'a str, Color>,
26    pub search: bool,
27    pub line_number: bool,
28}
29
30impl<'a> Moins<'a> {
31    /// run moins pager
32    pub fn run(content: &'a mut String, options: Option<PagerOptions>) {
33        let stdout = stdout().into_raw_mode().unwrap();
34        let screen = MouseTerminal::from(AlternateScreen::from(stdout));
35        let screen = RefCell::new(screen);
36
37        let mut pager = Moins::new(content, screen, options);
38
39        let stdin = stdin();
40
41        pager.clear();
42        pager.write();
43
44        for c in stdin.keys() {
45            // Input
46            match c.unwrap() {
47                Key::Char('q') => {
48                    write!(pager.screen.borrow_mut(), "{}", termion::cursor::Show).unwrap();
49                    break;
50                }
51                Key::Down | Key::Char('j') => pager.scroll_down(),
52                Key::Up | Key::Char('k') => pager.scroll_up(),
53                _ => (),
54            }
55        }
56    }
57
58    fn new(content: &'a mut String, screen: Terminal, options: Option<PagerOptions<'a>>) -> Self {
59        let size = termion::terminal_size().unwrap();
60        let width = size.0 as usize;
61        let mut lines = vec![];
62
63        content.lines().for_each(|line| {
64            if line.len() > width {
65                lines.push(&line[0..width]);
66                lines.push(&line[width..line.len()]);
67            } else {
68                lines.push(line);
69            }
70        });
71
72        let height = size.1 as usize;
73
74        let scroll = if lines.len() <= height {
75            lines.len()
76        } else {
77            height
78        };
79
80        Moins {
81            lines,
82            scroll,
83            screen,
84            current_line: 0,
85            height: size.1 - 2,
86            width: size.0,
87            options,
88        }
89    }
90
91    fn color(&self, line: String) -> String {
92        if let Some(options) = &self.options {
93            let reset = Color::Reset;
94            let mut colored_line = line.clone();
95
96            options.colors.iter().for_each(|(term, color)| {
97                let mut find_idx = 0;
98
99                while let Some(term_idx) = colored_line[find_idx..colored_line.len()].rfind(term) {
100                    let color = color.get();
101                    colored_line.insert_str(term_idx, color);
102                    find_idx = term_idx + color.len() + term.len();
103                    colored_line.insert_str(find_idx, reset.get());
104                    find_idx = find_idx + reset.get().len();
105                }
106            });
107            colored_line
108        } else {
109            line
110        }
111    }
112
113    fn clear(&mut self) {
114        write!(
115            self.screen.borrow_mut(),
116            "{}{}{}",
117            termion::clear::All,
118            termion::cursor::Goto(1, 1),
119            termion::cursor::Hide,
120        )
121        .unwrap();
122    }
123
124    fn flush(&mut self) {
125        self.screen.borrow_mut().flush().unwrap();
126    }
127
128    fn scroll_as_u16(&self) -> u16 {
129        self.scroll as u16
130    }
131
132    fn write(&mut self) {
133        write!(
134            self.screen.borrow_mut(),
135            "{}{}",
136            termion::cursor::Goto(1, self.scroll_as_u16()),
137            termion::clear::CurrentLine,
138        )
139        .unwrap();
140
141        let height = self.height as usize;
142
143        let offset = if self.current_line + height > self.lines.len() {
144            self.lines.len()
145        } else {
146            self.current_line + height
147        };
148
149        self.lines[self.current_line..offset]
150            .into_iter()
151            .enumerate()
152            .for_each(|(idx, line)| {
153                write!(
154                    self.screen.borrow_mut(),
155                    "{}{}{}{}",
156                    termion::cursor::Goto(1, (idx + 1) as u16),
157                    termion::clear::CurrentLine,
158                    self.color(line.to_string()),
159                    termion::cursor::Hide
160                )
161                .unwrap();
162            });
163
164        let acc = (0..self.width).map(|_| "_").collect::<String>();
165
166        write!(
167            self.screen.borrow_mut(),
168            "{}{}{}{}{}",
169            termion::cursor::Goto(1, self.height),
170            termion::clear::CurrentLine,
171            termion::style::Underline,
172            termion::cursor::Hide,
173            acc,
174        )
175        .unwrap();
176
177        write!(
178            self.screen.borrow_mut(),
179            "{}{}{}{}{}",
180            termion::cursor::Goto(1, self.height + 2),
181            termion::clear::CurrentLine,
182            termion::style::Reset,
183            termion::cursor::Hide,
184            "Ctrl+j, k, arrow_up ,arrow_down to move, q to quit",
185        )
186        .unwrap();
187
188        print!("{}", termion::style::Reset);
189
190        self.flush();
191    }
192
193    fn scroll_down(&mut self) {
194        self.scroll = if self.scroll == self.height as usize {
195            self.height as usize
196        } else {
197            self.scroll + 1
198        };
199
200        let height = self.height as usize;
201
202        self.current_line = if self.lines.len() < height {
203            0
204        } else if self.current_line == self.lines.len() - height {
205            self.lines.len() - height
206        } else {
207            self.current_line + 1
208        };
209
210        print!("{}", termion::scroll::Up(self.scroll_as_u16()));
211
212        self.write();
213    }
214
215    fn scroll_up(&mut self) {
216        self.scroll = if self.scroll == 1 { 1 } else { self.scroll - 1 };
217
218        self.current_line = if self.current_line == 0 {
219            0
220        } else {
221            self.current_line - 1
222        };
223
224        print!("{}", termion::scroll::Up(self.scroll_as_u16()));
225
226        self.write();
227    }
228}
229
230pub enum Color {
231    Black,
232    LightBlack,
233    Blue,
234    LightBlue,
235    Cyan,
236    LightCyan,
237    Green,
238    LightGreen,
239    Magenta,
240    LightMagenta,
241    Red,
242    LightRed,
243    White,
244    LightWhite,
245    Yellow,
246    LightYellow,
247    Reset,
248}
249
250impl Color {
251    fn get(&self) -> &'static str {
252        // this might seem a litle bit absurd but we don't want our user to reimport termion colors
253        match self {
254            Color::Black => termion::color::Black::fg_str(&termion::color::Black {}),
255            Color::LightBlack => termion::color::LightBlack::fg_str(&termion::color::LightBlack),
256            Color::Blue => termion::color::Blue::fg_str(&termion::color::Blue),
257            Color::LightBlue => termion::color::LightBlue::fg_str(&termion::color::LightBlue),
258            Color::Cyan => termion::color::Cyan::fg_str(&termion::color::Cyan),
259            Color::LightCyan => termion::color::LightCyan::fg_str(&termion::color::LightCyan),
260            Color::Green => termion::color::Green::fg_str(&termion::color::Green),
261            Color::LightGreen => termion::color::LightGreen::fg_str(&termion::color::LightGreen),
262            Color::Magenta => termion::color::Magenta::fg_str(&termion::color::Magenta),
263            Color::LightMagenta => {
264                termion::color::LightMagenta::fg_str(&termion::color::LightMagenta)
265            }
266            Color::Red => termion::color::Red::fg_str(&termion::color::Red),
267            Color::LightRed => termion::color::LightRed::fg_str(&termion::color::LightRed),
268            Color::White => termion::color::White::fg_str(&termion::color::White),
269            Color::LightWhite => termion::color::LightWhite::fg_str(&termion::color::LightWhite),
270            Color::Yellow => termion::color::Yellow::fg_str(&termion::color::Yellow),
271            Color::LightYellow => termion::color::LightYellow::fg_str(&termion::color::LightYellow),
272            Color::Reset => termion::color::Reset::fg_str(termion::color::Reset),
273        }
274    }
275}