unsegen_pager/
lib.rs

1//! An `unsegen` widget for viewing files with additional features.
2//!
3//! # Examples:
4//! ```no_run
5//! extern crate unsegen;
6//!
7//! use std::io::{stdin, stdout};
8//! use unsegen::base::Terminal;
9//! use unsegen::input::{Input, Key, ScrollBehavior};
10//! use unsegen::widget::{RenderingHints, Widget};
11//!
12//! use unsegen_pager::{Pager, PagerContent, SyntaxSet, SyntectHighlighter, ThemeSet};
13//!
14//! fn main() {
15//!     let stdout = stdout();
16//!     let stdin = stdin();
17//!     let stdin = stdin.lock();
18//!
19//!     let file = "path/to/some/file";
20//!
21//!     let syntax_set = SyntaxSet::load_defaults_nonewlines();
22//!     let syntax = syntax_set
23//!         .find_syntax_for_file(&file)
24//!         .unwrap()
25//!         .unwrap_or(syntax_set.find_syntax_plain_text());
26//!
27//!     let theme_set = ThemeSet::load_defaults();
28//!     let theme = &theme_set.themes["base16-ocean.dark"];
29//!
30//!     let highlighter = SyntectHighlighter::new(syntax, theme);
31//!     let mut pager = Pager::new();
32//!     pager.load(
33//!         PagerContent::from_file(&file)
34//!             .unwrap()
35//!             .with_highlighter(&highlighter),
36//!     );
37//!
38//!     let mut term = Terminal::new(stdout.lock()).unwrap();
39//!
40//!     for input in Input::read_all(stdin) {
41//!         let input = input.unwrap();
42//!         input.chain(
43//!             ScrollBehavior::new(&mut pager)
44//!                 .forwards_on(Key::Down)
45//!                 .forwards_on(Key::Char('j'))
46//!                 .backwards_on(Key::Up)
47//!                 .backwards_on(Key::Char('k'))
48//!                 .to_beginning_on(Key::Home)
49//!                 .to_end_on(Key::End),
50//!         );
51//!         // Put more application logic here...
52//!
53//!         {
54//!             let win = term.create_root_window();
55//!             pager.as_widget().draw(win, RenderingHints::default());
56//!         }
57//!         term.present();
58//!     }
59//! }
60//! ```
61
62extern crate syntect;
63extern crate unsegen;
64
65mod decorating;
66mod highlighting;
67
68pub use decorating::*;
69pub use highlighting::*;
70
71pub use syntect::highlighting::{Theme, ThemeSet};
72pub use syntect::parsing::{SyntaxDefinition, SyntaxSet};
73
74use unsegen::base::{
75    basic_types::*, BoolModifyMode, Cursor, GraphemeCluster, StyleModifier, Window, WrappingMode,
76};
77use unsegen::input::{OperationResult, Scrollable};
78use unsegen::widget::{layout_linearly, Demand, Demand2D, RenderingHints, Widget};
79
80use std::cmp::{max, min};
81use std::ops::{Bound, RangeBounds};
82
83/// Main `Widget`, may (or may not) store content, but defines static types for content and
84/// decoration.
85///
86/// Use `load` to actually fill the widget with content.
87///
88/// In addition to the `PagerContent`, it has a concept of an 'active line' that can be updated via
89/// user interaction (using the `Scrollable` implementation) and is always displayed when drawn to
90/// a window.
91pub struct Pager<L, D = NoDecorator<L>>
92where
93    L: PagerLine,
94    D: LineDecorator,
95{
96    content: Option<PagerContent<L, D>>,
97    current_line: LineIndex,
98}
99
100impl<L, D> Default for Pager<L, D>
101where
102    L: PagerLine,
103    D: LineDecorator<Line = L>,
104{
105    fn default() -> Self {
106        Pager {
107            content: None,
108            current_line: LineIndex::new(0),
109        }
110    }
111}
112
113impl<L, D> Pager<L, D>
114where
115    L: PagerLine,
116    D: LineDecorator<Line = L>,
117{
118    /// Create an empty pager, with no current content.
119    pub fn new() -> Self {
120        Pager {
121            content: None,
122            current_line: LineIndex::new(0),
123        }
124    }
125
126    /// Load (and potentially overwrite previous) content to display in the pager.
127    ///
128    /// If possible, the current line position will be preserved.
129    pub fn load(&mut self, content: PagerContent<L, D>) {
130        self.content = Some(content);
131
132        // Go back to last available line
133        let current_line = self.current_line;
134        if !self.line_exists(current_line) {
135            let _ = self.scroll_to_end();
136        }
137    }
138
139    /// Clear the current content.
140    ///
141    /// On subsequent `draw` calls, nothing will be written to the window.
142    pub fn clear_content(&mut self) {
143        self.content = None;
144    }
145
146    /// Get a reference to the current content, if available.
147    pub fn content(&self) -> Option<&PagerContent<L, D>> {
148        self.content.as_ref()
149    }
150
151    /// Get a mutable reference to the current content, if available.
152    ///
153    /// Note that `PagerContent` does not allow mutable access to the stored lines, so it is
154    /// required to use `load` to update the contents. A pager is not a text editor.
155    pub fn content_mut(&mut self) -> Option<&mut PagerContent<L, D>> {
156        self.content.as_mut()
157    }
158
159    fn line_exists<I: Into<LineIndex>>(&mut self, line: I) -> bool {
160        let line: LineIndex = line.into();
161        if let Some(ref mut content) = self.content {
162            line.raw_value() < content.storage.len()
163        } else {
164            false
165        }
166    }
167
168    /// Go to the specified line, if present.
169    ///
170    /// If there is no such line, an error is returned.
171    pub fn go_to_line<I: Into<LineIndex>>(&mut self, line: I) -> Result<(), PagerError> {
172        let line: LineIndex = line.into();
173        if self.line_exists(line) {
174            self.current_line = line;
175            Ok(())
176        } else {
177            Err(PagerError::NoLineWithIndex(line))
178        }
179    }
180
181    /// Go to first line that matches the given predicate.
182    ///
183    /// If there is no such line, an error is returned.
184    pub fn go_to_line_if<F: Fn(LineIndex, &L) -> bool>(
185        &mut self,
186        predicate: F,
187    ) -> Result<(), PagerError> {
188        let line = if let Some(ref mut content) = self.content {
189            content
190                .view(LineIndex::new(0)..)
191                .find(|&(index, ref line)| predicate(index, line))
192                .map(|(index, _)| index)
193                .ok_or(PagerError::NoLineWithPredicate)
194        } else {
195            Err(PagerError::NoContent)
196        };
197        line.and_then(|index| self.go_to_line(index))
198    }
199
200    /// Get the index of the currently active line.
201    pub fn current_line_index(&self) -> LineIndex {
202        self.current_line
203    }
204
205    /// Get a reference to the currently active line.
206    pub fn current_line(&self) -> Option<&L> {
207        if let Some(ref content) = self.content {
208            content.storage.get(self.current_line_index().raw_value())
209        } else {
210            None
211        }
212    }
213
214    pub fn as_widget<'a>(&'a self) -> impl Widget + 'a {
215        PagerWidget { inner: self }
216    }
217}
218
219struct PagerWidget<'a, L, D>
220where
221    L: PagerLine,
222    D: LineDecorator<Line = L>,
223{
224    inner: &'a Pager<L, D>,
225}
226
227impl<'a, L, D> Widget for PagerWidget<'a, L, D>
228where
229    L: PagerLine,
230    D: LineDecorator<Line = L>,
231{
232    fn space_demand(&self) -> Demand2D {
233        Demand2D {
234            width: Demand::at_least(1),
235            height: Demand::at_least(1),
236        }
237    }
238    fn draw(&self, window: Window, _: RenderingHints) {
239        if let Some(ref content) = self.inner.content {
240            let height: Height = window.get_height();
241            // The highlighter might need a minimum number of lines to figure out the syntax:
242            // TODO: make this configurable?
243            let min_highlight_context = 40;
244            let num_adjacent_lines_to_load = max(height.into(), min_highlight_context / 2);
245            let min_line = self
246                .inner
247                .current_line
248                .checked_sub(num_adjacent_lines_to_load)
249                .unwrap_or_else(|| LineIndex::new(0));
250            let max_line = self.inner.current_line + num_adjacent_lines_to_load;
251
252            // Split window
253            let decorator_demand = content
254                .decorator
255                .horizontal_space_demand(content.view(min_line..max_line));
256            let split_pos = layout_linearly(
257                window.get_width(),
258                Width::new(0).unwrap(),
259                &[decorator_demand, Demand::at_least(1)],
260                &[0.0, 1.0],
261            )[0];
262
263            let (mut decoration_window, mut content_window) = window
264                .split(split_pos.from_origin())
265                .expect("valid split pos");
266
267            // Fill background with correct color
268            let bg_style = content.highlight_info.default_style();
269            content_window.set_default_style(bg_style.apply_to_default());
270            content_window.fill(GraphemeCluster::space());
271
272            let mut cursor = Cursor::new(&mut content_window)
273                .position(ColIndex::new(0), RowIndex::new(0))
274                .wrapping_mode(WrappingMode::Wrap);
275
276            let num_line_wraps_until_current_line = {
277                content
278                    .view(min_line..self.inner.current_line)
279                    .map(|(_, line)| (cursor.num_expected_wraps(line.get_content()) + 1) as i32)
280                    .sum::<i32>()
281            };
282            let num_line_wraps_from_current_line = {
283                content
284                    .view(self.inner.current_line..max_line)
285                    .map(|(_, line)| (cursor.num_expected_wraps(line.get_content()) + 1) as i32)
286                    .sum::<i32>()
287            };
288
289            let centered_current_line_start_pos: RowIndex = (height / (2 as usize)).from_origin();
290            let best_current_line_pos_for_bottom = max(
291                centered_current_line_start_pos,
292                height.from_origin() - num_line_wraps_from_current_line,
293            );
294            let required_start_pos = min(
295                RowIndex::new(0),
296                best_current_line_pos_for_bottom - num_line_wraps_until_current_line,
297            );
298
299            cursor.move_to(ColIndex::new(0), required_start_pos);
300
301            for (line_index, line) in content.view(min_line..max_line) {
302                let line_content = line.get_content();
303                let base_style = if line_index == self.inner.current_line {
304                    StyleModifier::new()
305                        .invert(BoolModifyMode::Toggle)
306                        .bold(true)
307                } else {
308                    StyleModifier::new()
309                };
310
311                let (_, start_y) = cursor.get_position();
312                let mut last_change_pos = 0;
313                for &(change_pos, style) in content.highlight_info.get_info_for_line(line_index) {
314                    cursor.write(&line_content[last_change_pos..change_pos]);
315
316                    cursor.set_style_modifier(style.on_top_of(base_style));
317                    last_change_pos = change_pos;
318                }
319                cursor.write(&line_content[last_change_pos..]);
320
321                cursor.set_style_modifier(base_style);
322                cursor.fill_and_wrap_line();
323                let (_, end_y) = cursor.get_position();
324
325                let range_start_y = min(max(start_y, RowIndex::new(0)), height.from_origin());
326                let range_end_y = min(max(end_y, RowIndex::new(0)), height.from_origin());
327                content.decorator.decorate(
328                    &line,
329                    line_index,
330                    self.inner.current_line,
331                    decoration_window.create_subwindow(.., range_start_y..range_end_y),
332                );
333                //decoration_window.create_subwindow(.., range_start_y..range_end_y).fill('X');
334            }
335        }
336    }
337}
338
339impl<L, D> Scrollable for Pager<L, D>
340where
341    L: PagerLine,
342    D: LineDecorator<Line = L>,
343{
344    fn scroll_backwards(&mut self) -> OperationResult {
345        if self.current_line > LineIndex::new(0) {
346            self.current_line -= 1;
347            Ok(())
348        } else {
349            Err(())
350        }
351    }
352    fn scroll_forwards(&mut self) -> OperationResult {
353        let new_line = self.current_line + 1;
354        self.go_to_line(new_line).map_err(|_| ())
355    }
356    fn scroll_to_beginning(&mut self) -> OperationResult {
357        if self.current_line == LineIndex::new(0) {
358            Err(())
359        } else {
360            self.current_line = LineIndex::new(0);
361            Ok(())
362        }
363    }
364    fn scroll_to_end(&mut self) -> OperationResult {
365        if let Some(ref content) = self.content {
366            if content.storage.is_empty() {
367                return Err(());
368            }
369            let last_line = LineIndex::new(content.storage.len() - 1);
370            if self.current_line == last_line {
371                Err(())
372            } else {
373                self.current_line = last_line;
374                Ok(())
375            }
376        } else {
377            Err(())
378        }
379    }
380}
381
382/// Anything that represents a single line in a pager. Other than the main content (something
383/// string-like) it may also store additional information that can be used by a `Highlighter`.
384pub trait PagerLine {
385    fn get_content(&self) -> &str;
386}
387
388impl PagerLine for String {
389    fn get_content(&self) -> &str {
390        self.as_str()
391    }
392}
393
394/// A collection of `PagerLines` including information about the highlighting state and (if
395/// present) a `LineDecorator`.
396///
397/// Use `from_lines` or `from_file` to build an initial content and add highlighter and decorator
398/// using `with_highlighter` and `with_decorator`.
399pub struct PagerContent<L: PagerLine, D: LineDecorator> {
400    storage: Vec<L>,
401    highlight_info: HighlightInfo,
402    decorator: D,
403}
404
405impl<L: PagerLine> PagerContent<L, NoDecorator<L>> {
406    /// Create a simple `PagerContent` from a ordered collection of lines. The lines the `Vec` will
407    /// be displayed top to bottom from beginning to end.
408    pub fn from_lines(storage: Vec<L>) -> Self {
409        PagerContent {
410            storage,
411            highlight_info: HighlightInfo::none(),
412            decorator: NoDecorator::default(),
413        }
414    }
415}
416
417impl PagerContent<String, NoDecorator<String>> {
418    /// Try to load lines (as strings) from the given file as the lines of PagerContent.
419    pub fn from_file<F: AsRef<::std::path::Path>>(file_path: F) -> ::std::io::Result<Self> {
420        use std::io::Read;
421        let mut file = ::std::fs::File::open(file_path)?;
422        let mut contents = String::new();
423        file.read_to_string(&mut contents)?;
424
425        Ok(PagerContent {
426            storage: contents.lines().map(|s| s.to_owned()).collect::<Vec<_>>(),
427            highlight_info: HighlightInfo::none(),
428            decorator: NoDecorator::default(),
429        })
430    }
431}
432
433impl<L, D> PagerContent<L, D>
434where
435    L: PagerLine,
436    D: LineDecorator<Line = L>,
437{
438    /// Add a `Highlighter` to `PagerContent` that previously did not have one.
439    pub fn with_highlighter<HN: Highlighter>(self, highlighter: &HN) -> PagerContent<L, D> {
440        let highlight_info =
441            highlighter.highlight(self.storage.iter().map(|l| l as &dyn PagerLine));
442        PagerContent {
443            storage: self.storage,
444            highlight_info,
445            decorator: self.decorator,
446        }
447    }
448}
449
450impl<L> PagerContent<L, NoDecorator<L>>
451where
452    L: PagerLine,
453{
454    /// Add a `Decorator` to `PagerContent` that previously did not have one.
455    pub fn with_decorator<DN: LineDecorator<Line = L>>(self, decorator: DN) -> PagerContent<L, DN> {
456        PagerContent {
457            storage: self.storage,
458            highlight_info: self.highlight_info,
459            decorator,
460        }
461    }
462}
463
464impl<L, D> PagerContent<L, D>
465where
466    L: PagerLine,
467    D: LineDecorator<Line = L>,
468{
469    /// Iterate over a specified range of lines stored.
470    ///
471    /// The specified range can be larger than what the `PagerContent` currently holds. In that
472    /// case the additional indices are simply not part of the returned iterator.
473    pub fn view<'a, I: Into<LineIndex> + Clone, R: RangeBounds<I>>(
474        &'a self,
475        range: R,
476    ) -> impl DoubleEndedIterator<Item = (LineIndex, &'a L)> + 'a
477    where
478        Self: ::std::marker::Sized,
479    {
480        // Not exactly sure, why this is needed... we only store a reference?!
481        let start: LineIndex = match range.start_bound() {
482            // Always inclusive
483            Bound::Unbounded => LineIndex::new(0),
484            Bound::Included(i) => i.clone().into(),
485            Bound::Excluded(i) => i.clone().into() + 1,
486        };
487        let end: LineIndex = match range.end_bound() {
488            // Always exclusive
489            Bound::Unbounded => LineIndex::new(self.storage.len()),
490            Bound::Included(i) => i.clone().into() + 1,
491            Bound::Excluded(i) => i.clone().into(),
492        };
493        let ustart = start.raw_value();
494        let uend = self.storage.len().min(end.raw_value());
495        let urange = ustart..uend;
496        urange
497            .clone()
498            .zip(self.storage[urange].iter())
499            .map(|(i, l)| (LineIndex::new(i), l))
500    }
501
502    /// Try to view a specific line with the given index.
503    pub fn view_line<I: Into<LineIndex>>(&self, line: I) -> Option<&L> {
504        self.storage.get(line.into().raw_value())
505    }
506
507    /// Overwrite the current decorator with a compatible one.
508    pub fn set_decorator(&mut self, decorator: D) {
509        self.decorator = decorator;
510    }
511}
512
513/// All errors that can occur when operating on a `Pager` or its contents.
514#[derive(Debug)]
515pub enum PagerError {
516    NoLineWithIndex(LineIndex),
517    NoLineWithPredicate,
518    NoContent,
519}