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
22pub struct PagerOptions<'a> {
24 pub colors: HashMap<&'a str, Color>,
26 pub search: bool,
27 pub line_number: bool,
28}
29
30impl<'a> Moins<'a> {
31 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 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 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}