pager_rs/
state.rs

1use crossterm::{
2    event::KeyCode,
3    style::{Attribute, Color, ContentStyle, Stylize},
4    terminal,
5};
6
7use crate::{run, status_bar::StatusBar, StatusBarLayout, StatusBarLayoutItem};
8
9/// Type of [`Command`].
10#[derive(Clone, PartialEq)]
11pub enum CommandType {
12    /// Waits for `:` key and then the command input, until Enter is pressed.
13    Colon(String),
14    /// Waits for key input.
15    Key(KeyCode),
16}
17
18/// Command definition
19#[derive(Clone)]
20pub struct Command {
21    /// When any of the values matched with input from user, command will be executed.
22    pub cmd: Vec<CommandType>,
23    /// Description of the command, can be seen in help text.
24    pub desc: String,
25    /// The function that runs when command executed.
26    pub func: &'static dyn Fn(&mut State) -> bool,
27}
28
29/// Container of list of commands.
30pub struct CommandList(pub Vec<Command>);
31
32impl From<CommandList> for Vec<Command> {
33    fn from(val: CommandList) -> Self {
34        val.0
35    }
36}
37
38impl CommandList {
39    /// Combine [`CommandList`]'s into one.
40    pub fn combine<T>(list: Vec<T>) -> Self
41    where
42        T: Into<Vec<Command>>,
43    {
44        let mut v = vec![];
45        for item in list {
46            v.append(&mut item.into());
47        }
48        Self(v)
49    }
50
51    /// Default 'quit' command
52    pub fn quit() -> Self {
53        use CommandType::*;
54        Self(vec![Command {
55            cmd: vec![Key(KeyCode::Char('q')), Colon("quit".to_string())],
56            desc: "Quit".to_string(),
57            func: &|state: &mut State| {
58                state.quit();
59                false
60            },
61        }])
62    }
63
64    /// Default bundle of 'navigation' commands.
65    ///
66    /// Includes: `Arrow`, `Home/End`, `PageUp/PageDown` keys
67    pub fn navigation() -> Self {
68        use CommandType::*;
69        Self(vec![
70            Command {
71                cmd: vec![Key(KeyCode::Up)],
72                desc: "Cursor up".to_string(),
73                func: &|state: &mut State| state.up(),
74            },
75            Command {
76                cmd: vec![Key(KeyCode::Down)],
77                desc: "Cursor down".to_string(),
78                func: &|state: &mut State| state.down(),
79            },
80            Command {
81                cmd: vec![Key(KeyCode::Left)],
82                desc: "Cursor left".to_string(),
83                func: &|state: &mut State| state.left(),
84            },
85            Command {
86                cmd: vec![Key(KeyCode::Right)],
87                desc: "Cursor right".to_string(),
88                func: &|state: &mut State| state.right(),
89            },
90            Command {
91                cmd: vec![Key(KeyCode::Home), Key(KeyCode::Char('g'))],
92                desc: "Go to start".to_string(),
93                func: &|state: &mut State| state.home(),
94            },
95            Command {
96                cmd: vec![Key(KeyCode::End), Key(KeyCode::Char('G'))],
97                desc: "Go to end".to_string(),
98                func: &|state: &mut State| state.end(),
99            },
100            Command {
101                cmd: vec![Key(KeyCode::PageUp)],
102                desc: "One page up".to_string(),
103                func: &|state: &mut State| state.pgup(),
104            },
105            Command {
106                cmd: vec![Key(KeyCode::PageDown)],
107                desc: "One page down".to_string(),
108                func: &|state: &mut State| state.pgdown(),
109            },
110        ])
111    }
112
113    /// Default 'help' command
114    pub fn help() -> Self {
115        use CommandType::*;
116        Self(vec![Command {
117            cmd: vec![Key(KeyCode::Char('h')), Colon("help".to_string())],
118            desc: "Toggles help text visiblity".to_string(),
119            func: &|state: &mut State| {
120                let theme = ContentStyle::new()
121                    .with(Color::Black)
122                    .on(Color::White)
123                    .attribute(Attribute::Bold);
124                let commands =
125                    CommandList::combine(vec![CommandList::quit(), CommandList::navigation()]);
126
127                let mut help = State {
128                    pos: (0, 0),
129                    size: state.size,
130                    content: state.get_help_text(),
131                    status_bar: StatusBar {
132                        line_layouts: vec![StatusBarLayout {
133                            left: vec![StatusBarLayoutItem::Text("Quit (q)".to_owned())],
134                            right: vec![],
135                        }],
136                        title: "Help text".to_owned(),
137                        theme,
138                    },
139                    commands,
140                    running: true,
141                    show_line_numbers: false,
142                    word_wrap: false,
143                    word_wrap_option: textwrap::Options::new(0),
144                };
145                run(&mut help).unwrap();
146                true
147            },
148        }])
149    }
150
151    /// Default 'toggle line numbers' command
152    pub fn toggle_line_numbers() -> Self {
153        use CommandType::*;
154        Self(vec![Command {
155            cmd: vec![Key(KeyCode::Char('l'))],
156            desc: "Show/Hide line numbers".to_string(),
157            func: &|state: &mut State| {
158                state.show_line_numbers = !state.show_line_numbers;
159                true
160            },
161        }])
162    }
163
164    /// Default 'toggle word wrap' command
165    pub fn toggle_word_wrap() -> Self {
166        use CommandType::*;
167        Self(vec![Command {
168            cmd: vec![Key(KeyCode::Char('w'))],
169            desc: "Activate/Deactivate word wrap".to_string(),
170            func: &|state: &mut State| {
171                state.word_wrap = !state.word_wrap;
172
173                true
174            },
175        }])
176    }
177}
178
179impl Default for CommandList {
180    fn default() -> Self {
181        Self::combine(vec![
182            Self::quit(),
183            Self::navigation(),
184            Self::help(),
185            Self::toggle_line_numbers(),
186            Self::toggle_word_wrap(),
187        ])
188    }
189}
190
191/// State that can be ran with `pager_rs::run`
192pub struct State<'a> {
193    /// Cursor position in content.
194    ///
195    /// `(x, y)`
196    pub pos: (usize, usize),
197
198    /// Size of terminal screen.
199    ///
200    /// `(width, height)`
201    pub size: (u16, u16),
202
203    /// Content to show.
204    pub content: String,
205
206    /// status bar at the bottom.
207    pub status_bar: StatusBar,
208
209    /// List of `Commands` that runnable in this `State`.
210    pub commands: CommandList,
211
212    pub(crate) running: bool,
213
214    /// Show/Hide line numbers.
215    pub show_line_numbers: bool,
216
217    /// Enable/Disable word-wrap
218    pub word_wrap: bool,
219
220    /// [`textwrap::Options`] to use when word-wrap is enabled.
221    ///
222    /// The `width` is not important since it will be replaced by terminal screen width when rendering text.
223    pub word_wrap_option: textwrap::Options<'a>,
224}
225
226impl<'a> State<'a> {
227    /// Create new [`State`]
228    pub fn new(
229        content: String,
230        status_bar: StatusBar,
231        commands: CommandList,
232    ) -> std::io::Result<Self> {
233        Ok(Self {
234            pos: (0, 0),
235            size: terminal::size()?,
236            content,
237            status_bar,
238            commands,
239            running: true,
240            show_line_numbers: true,
241            word_wrap: false,
242            word_wrap_option: textwrap::Options::new(0),
243        })
244    }
245
246    /// Returns true if the State is still runing.
247    pub fn is_running(&self) -> bool {
248        self.running
249    }
250
251    /// Terminate [`State`]
252    pub fn quit(&mut self) {
253        self.running = false;
254    }
255
256    /// Default help text formatter
257    pub fn get_help_text(&self) -> String {
258        if self.commands.0.is_empty() {
259            return String::from("No commands");
260        }
261        let items = self.commands.0.iter().map(|command| {
262            let name = command
263                .cmd
264                .iter()
265                .map(|cmd_type| match cmd_type {
266                    CommandType::Key(code) => match *code {
267                        KeyCode::Backspace => "Backspace".to_string(),
268                        KeyCode::Enter => "Enter".to_string(),
269                        KeyCode::Left => "Left".to_string(),
270                        KeyCode::Right => "Right".to_string(),
271                        KeyCode::Up => "Up".to_string(),
272                        KeyCode::Down => "Down".to_string(),
273                        KeyCode::Home => "Home".to_string(),
274                        KeyCode::End => "End".to_string(),
275                        KeyCode::PageUp => "PageUp".to_string(),
276                        KeyCode::PageDown => "PageDown".to_string(),
277                        KeyCode::Tab => "Tab".to_string(),
278                        KeyCode::BackTab => "BackTab".to_string(),
279                        KeyCode::Delete => "Delete".to_string(),
280                        KeyCode::Insert => "Insert".to_string(),
281                        KeyCode::F(n) => format!("F{}", n),
282                        KeyCode::Char(c) => c.to_string(),
283                        KeyCode::Null => "Null".to_string(),
284                        KeyCode::Esc => "Esc".to_string(),
285                        KeyCode::CapsLock => "CapsLock".to_string(),
286                        KeyCode::NumLock => "NumLock".to_string(),
287                        KeyCode::ScrollLock => "ScrollLock".to_string(),
288                        KeyCode::PrintScreen => "PrintScreen".to_string(),
289                        KeyCode::Pause => "Pause".to_string(),
290                        KeyCode::Menu => "Menu".to_string(),
291                        KeyCode::KeypadBegin => "KeypadBegin".to_string(),
292                        KeyCode::Media(_) => "MediaKey".to_string(),
293                        KeyCode::Modifier(_) => "ModifierKey".to_string(),
294                    },
295                    CommandType::Colon(s) => format!(":{}", s),
296                })
297                .collect::<Vec<String>>()
298                .join(", ");
299            (name, command.desc.clone())
300        });
301        let max_name_len = items.clone().map(|item| item.0.len()).max().unwrap();
302        let padding = max_name_len + 2;
303
304        items
305            .map(|(name, desc)| {
306                let name_len = name.len();
307                format!(
308                    "{}{gap}{}",
309                    name,
310                    desc.lines()
311                        .collect::<Vec<&str>>()
312                        .join(format!("\n{}", " ".repeat(padding)).as_str()),
313                    gap = " ".repeat(padding - name_len),
314                )
315            })
316            .collect::<Vec<String>>()
317            .join("\n\n")
318    }
319}
320
321impl<'a> State<'a> {
322    /// Get line inducator of given line number.
323    fn get_line_inducator(
324        &self,
325        line_number: usize,
326        max_line_number_width: usize,
327        blank: bool,
328    ) -> String {
329        if self.show_line_numbers {
330            let content = if blank {
331                String::from(" ")
332            } else {
333                line_number.to_string()
334            };
335
336            format!(
337                "{:line_count$}│",
338                content,
339                line_count = max_line_number_width
340            )
341        } else {
342            String::new()
343        }
344    }
345
346    /// Get text to be printed on terminal except for the [`StatusBar`].
347    pub fn get_visible(&self) -> String {
348        let max_line_number_width = self.content.lines().count().to_string().len();
349
350        let line_indicator_len = max_line_number_width + 1;
351
352        let lines: Box<dyn Iterator<Item = (usize, String)>> = match &self.word_wrap {
353            true => Box::new(self.content.lines().enumerate().flat_map(|(index, line)| {
354                let option = self
355                    .word_wrap_option
356                    .clone()
357                    .width(self.size.0 as usize - line_indicator_len);
358                textwrap::wrap(line, option)
359                    .into_iter()
360                    .map(move |vline| (index, vline.to_string()))
361            })),
362            false => Box::new(
363                self.content
364                    .lines()
365                    .enumerate()
366                    .map(|(index, line)| (index, line.to_owned())),
367            ),
368        };
369
370        let mut last_index: usize = usize::MAX;
371
372        lines
373            .skip(self.pos.1)
374            .take(self.size.1 as usize - self.status_bar.line_layouts.len())
375            .map(|(index, line)| -> String {
376                let line = format!(
377                    "{line_indicator}{visible_content_line}",
378                    line_indicator = self.get_line_inducator(
379                        index + 1,
380                        max_line_number_width,
381                        last_index == index
382                    ),
383                    visible_content_line = line
384                        .chars()
385                        .skip(self.pos.0)
386                        .take(self.size.0 as usize - line_indicator_len)
387                        .collect::<String>()
388                );
389                last_index = index;
390                line
391            })
392            .collect::<Vec<String>>()
393            .join("\n")
394    }
395}
396
397impl<'a> State<'a> {
398    /// Move cursor up.
399    pub fn up(&mut self) -> bool {
400        if self.pos.1 != 0 {
401            self.pos.1 -= 1;
402            return true;
403        }
404        false
405    }
406
407    /// Move cursor down.
408    pub fn down(&mut self) -> bool {
409        if self.pos.1 != self.content.lines().count() - 1 {
410            self.pos.1 += 1;
411            return true;
412        }
413        false
414    }
415
416    /// Move cursor left.
417    pub fn left(&mut self) -> bool {
418        let amount = self.size.0 as usize / 2;
419        if self.pos.0 >= amount {
420            self.pos.0 -= amount;
421            return true;
422        } else if self.pos.0 != 0 {
423            self.pos.0 = 0;
424            return true;
425        }
426        false
427    }
428
429    /// Move cursor right.
430    pub fn right(&mut self) -> bool {
431        let amount = self.size.0 as usize / 2;
432        self.pos.0 += amount;
433        true
434    }
435
436    /// Move cursor one page up.
437    pub fn pgup(&mut self) -> bool {
438        if self.pos.1 >= self.size.1 as usize {
439            self.pos.1 -= self.size.1 as usize - 1;
440            return true;
441        } else if self.pos.1 != 0 {
442            self.pos.1 = 0;
443            return true;
444        }
445        false
446    }
447
448    /// Move cursor one page down.
449    pub fn pgdown(&mut self) -> bool {
450        let new = (self.pos.1 + self.size.1 as usize).min(self.content.lines().count()) - 1;
451        if new != self.pos.1 {
452            self.pos.1 = new;
453            return true;
454        }
455        false
456    }
457
458    /// Move cursor to the start.
459    pub fn home(&mut self) -> bool {
460        if self.pos.1 > 0 {
461            self.pos.1 = 0;
462            return true;
463        }
464        false
465    }
466
467    /// Move cursor to the end.
468    pub fn end(&mut self) -> bool {
469        let line_count = self.content.lines().count();
470        self.pos.1 = if line_count > self.size.1 as usize {
471            line_count - self.size.1 as usize + 1
472        } else {
473            0
474        };
475        true
476    }
477}
478
479impl<'a> State<'a> {
480    /// Find and execute command matching with pressed key.
481    pub fn match_key_event(&mut self, code: KeyCode) -> bool {
482        let mut commands = self.commands.0.clone();
483        let found = commands
484            .iter_mut()
485            .find(|command| command.cmd.contains(&CommandType::Key(code)));
486        if let Some(Command { func, .. }) = found {
487            return func(self);
488        }
489        false
490    }
491}