minus 5.7.2

An asynchronous data feedable terminal paging library for Rust
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
#![allow(dead_code)]

use crossterm::{
    cursor::MoveTo,
    execute, queue,
    terminal::{Clear, ClearType},
};

use std::{cmp::Ordering, convert::TryInto, fmt::Display, io::Write};

use super::term;
use crate::{LineNumbers, PagerState, error::MinusError, minus_core};

/// How should the incoming text be drawn on the screen
#[derive(Debug, PartialEq, Eq)]
pub enum AppendStyle {
    /// Draw only the region that needs to change
    PartialUpdate((usize, usize)),

    /// Redraw the entire screen
    FullRedraw,
}

/// Handles drawing of screen based on movement
///
/// Refreshing the entire terminal can be costly, especially on high resolution displays and this cost can turns out to be
/// very high if that redrawing is required on every movement of the pager, even for small changes.
/// This function calculates what part of screen needs to be redrawed on scrolling up/down and based on that, it redraws
/// only that part of the terminal.
pub fn draw_for_change(
    out: &mut impl Write,
    ps: &mut PagerState,
    new_upper_mark: &mut usize,
) -> Result<(), MinusError> {
    let line_count = ps.screen.formatted_lines_count();

    // Reduce one row for prompt/messages
    //
    // NOTE This should be the value of rows that should be used throughout this function.
    // Don't use PagerState::rows, it might lead to wrong output
    let writable_rows = ps.rows.saturating_sub(1);

    // Calculate the lower_bound for current and new upper marks
    // by adding either the rows or line_count depending on the minimality
    let lower_bound = ps.upper_mark.saturating_add(writable_rows.min(line_count));
    let mut new_lower_bound = new_upper_mark.saturating_add(writable_rows.min(line_count));

    // If the lower_bound is greater than the available line count, we set it to such a value
    // so that the last page can be displayed entirely, i.e never scroll past the last line
    if new_lower_bound > line_count {
        *new_upper_mark = line_count.saturating_sub(writable_rows);
        new_lower_bound = line_count;
    }

    let delta = new_upper_mark.abs_diff(ps.upper_mark);
    // Sometimes the value of delta is too large that we can rather use the value of the writable rows to
    // achieve the same effect with better performance. This means that we have draw to less lines to the terminal
    //
    // Think of it like this:-
    // Let's say the current upper mark is at 100 and writable rows is 25. Now if there is a jump of 200th line,
    // then instead of writing 100 lines, we can just jump to the 200 line and display the next 25 lines from there on.
    //
    // Hence here we can take the minimum of the delta or writable rows for displaying
    //
    // NOTE that the large delta case may not always be true in case of scrolling down. Actually this method produces
    // wrong output if this is not the case hence we still rely on using lower bounds method. But for scrolling up, we
    // need this value whatever the value of delta be.
    let normalized_delta = delta.min(writable_rows);

    let (start, end) = match (*new_upper_mark).cmp(&ps.upper_mark) {
        Ordering::Greater => {
            // Scroll down `normalized_delta` lines, and put the cursor one line above, where the old prompt would present.
            // Clear it off and start displaying new dta.
            queue!(
                out,
                crossterm::terminal::ScrollUp(normalized_delta.try_into().unwrap())
            )?;
            term::move_cursor(
                out,
                0,
                ps.rows
                    .saturating_sub(normalized_delta + 1)
                    .try_into()
                    .unwrap(),
                false,
            )?;
            queue!(out, Clear(ClearType::CurrentLine))?;

            if delta < writable_rows {
                (lower_bound, new_lower_bound)
            } else {
                (
                    *new_upper_mark,
                    new_upper_mark.saturating_add(normalized_delta),
                )
            }
        }
        Ordering::Less => {
            execute!(
                out,
                crossterm::terminal::ScrollDown(normalized_delta.try_into().unwrap())
            )?;
            term::move_cursor(out, 0, 0, false)?;

            (
                *new_upper_mark,
                new_upper_mark.saturating_add(normalized_delta),
            )
        }
        Ordering::Equal => return Ok(()),
    };

    let lines = ps.render_rows_for_display(start, end);
    write_raw_lines(out, &lines, Some("\r"))?;

    ps.upper_mark = *new_upper_mark;

    if ps.show_prompt {
        super::display::write_prompt(out, &ps.displayed_prompt, ps.rows.try_into().unwrap())?;
    }
    out.flush()?;

    Ok(())
}

/// Write given text at the prompt site
pub fn write_prompt(out: &mut impl Write, text: &str, rows: u16) -> Result<(), MinusError> {
    write!(out, "{mv}\r{prompt}", mv = MoveTo(0, rows), prompt = text)?;
    out.flush()?;
    Ok(())
}

// The below functions are just a subset of functionality of the above draw_for_change function.
// Although, separate they are tightly coupled together.

/// Completely redraws the screen
///
/// The function will first print out the lines from the current upper mark. This is handled inside the [`write_lines`]
/// function.
///
/// Then it will check if there is any message to display.
///   - If there is one, it will display it at the prompt site
///   - If there isn't one, it will display the prompt in place of it
pub fn draw_full(out: &mut impl Write, ps: &mut PagerState) -> Result<(), MinusError> {
    super::term::move_cursor(out, 0, 0, false)?;
    queue!(out, Clear(ClearType::All))?;

    write_from_pagerstate(out, ps)?;

    let pager_rows: u16 = ps.rows.try_into().map_err(|_| MinusError::Conversion)?;

    if ps.show_prompt {
        write_prompt(out, &ps.displayed_prompt, pager_rows)?;
    }

    out.flush().map_err(MinusError::Draw)
}

pub fn draw_append_text<L: Display + AsRef<str>>(
    out: &mut impl Write,
    rows: usize,
    prev_unterminated: usize,
    prev_fmt_lines_count: usize,
    fmt_text: &[L],
) -> Result<(), MinusError> {
    if prev_fmt_lines_count < rows {
        // Move the cursor to the very next line after the last displayed line
        term::move_cursor(
            out,
            0,
            prev_fmt_lines_count
                .saturating_sub(prev_unterminated)
                .try_into()
                .unwrap(),
            false,
        )?;
        // available_rows -> Rows that are still unfilled
        //      rows - number of lines displayed -1 (for prompt)
        // For example if 20 rows are in total in a terminal
        // and 10 rows are already occupied, then this will be equal to 9
        let available_rows = rows.saturating_sub(
            prev_fmt_lines_count
                .saturating_sub(prev_unterminated)
                .saturating_add(1),
        );
        // Minimum amount of text that an be appended
        // If available_rows is less, than this will be available rows else it will be
        // the length of the formatted text
        //
        // If number of rows in terminal is 23 with 20 rows filled and another 5 lines are given
        // This woll be equal to 3 as available rows will be 3
        // If in the above example only 2 lines need to be added, this will be equal to 2
        let num_appendable = fmt_text.len().min(available_rows);
        if num_appendable >= 1 {
            crossterm::execute!(out, crossterm::terminal::Clear(ClearType::CurrentLine))?;
        }
        for line in &fmt_text[0..num_appendable] {
            write!(out, "{line}\n\r")?;
        }
        out.flush()?;
    }
    Ok(())
}

/// Write the lines to the terminal
///
/// Note: Although this function can take any type that implements [`Write`] however it assumes that
/// it behaves like a terminal i.e it must set rows and cols in [`PagerState`].
/// If you want to write directly to a file without this preassumption, then use the [`write_lines`]
/// function.
///
/// Draws (at most) `rows -1` lines, where the first line to display is
/// [`PagerState::upper_mark`]. This function will always try to display as much lines as
/// possible within `rows -1`.
///
/// It always skips one row at the bottom as a site for the prompt or any message that may be sent.
///
/// This function ensures that upper mark never exceeds a value such that adding upper mark and available rows exceeds
/// the number of lines of text data. This rule is disobeyed in only one special case which is if number of lines of
/// text is less than available rows. In this situation, upper mark is always 0.
#[allow(clippy::too_many_arguments)]
pub fn write_text_checked<L: Display + AsRef<str>>(
    out: &mut impl Write,
    lines: &[L],
    mut upper_mark: usize,
    rows: usize,
    cols: usize,
    line_wrapping: bool,
    left_mark: usize,
    line_numbers: LineNumbers,
    total_line_count: usize,
) -> Result<(), MinusError> {
    let line_count = lines.len();

    // Reduce one row for prompt/messages
    let writable_rows = rows.saturating_sub(1);

    // Calculate the lower_mark by adding either the rows or line_count depending
    // on the minimality
    let mut lower_mark = upper_mark.saturating_add(writable_rows.min(line_count));

    // If the lower_bound is greater than the available line count, we set it to such a value
    // so that the last page can be displayed entirely, i.e never scroll past the last line
    if lower_mark > line_count {
        upper_mark = line_count.saturating_sub(writable_rows);
        lower_mark = upper_mark.saturating_add(writable_rows.min(line_count));
    }

    let display_lines = &lines[upper_mark..lower_mark];

    term::move_cursor(out, 0, 0, false)?;
    term::clear_entire_screen(out, false)?;

    write_lines(
        out,
        display_lines,
        cols,
        line_wrapping,
        left_mark,
        line_numbers.is_on(),
        total_line_count,
    )
}

pub fn write_from_pagerstate(out: &mut impl Write, ps: &mut PagerState) -> Result<(), MinusError> {
    let line_count = ps.screen.formatted_lines_count();

    // Reduce one row for prompt/messages
    let writable_rows = ps.rows.saturating_sub(1);

    // Calculate the lower_mark by adding either the rows or line_count depending
    // on the minimality
    let lower_mark = ps.upper_mark.saturating_add(writable_rows.min(line_count));
    if lower_mark > line_count {
        ps.upper_mark = line_count.saturating_sub(writable_rows);
    }

    let display_lines = ps.render_rows_for_display(ps.upper_mark, lower_mark);
    write_raw_lines(out, &display_lines, Some("\r"))
}

pub fn write_lines<L: Display + AsRef<str>>(
    out: &mut impl Write,
    lines: &[L],
    cols: usize,
    line_wrapping: bool,
    left_mark: usize,
    line_numbers: bool,
    line_count: usize,
) -> crate::Result {
    if line_wrapping {
        write_raw_lines(out, lines, Some("\r"))
    } else {
        write_lines_in_horizontal_scroll(out, lines, cols, left_mark, line_numbers, line_count)
    }
}

pub fn write_lines_in_horizontal_scroll<L: Display + AsRef<str>>(
    out: &mut impl Write,
    lines: &[L],
    cols: usize,
    start: usize,
    line_numbers: bool,
    line_count: usize,
) -> crate::Result {
    for line in lines {
        let line_str = line.as_ref();
        let (first_end, second_start, second_end) =
            get_horizontal_scroll_bounds(line_str, cols, start, line_numbers, line_count);

        if start < line.as_ref().len() {
            if line_numbers {
                writeln!(
                    out,
                    "\r{}{}",
                    &line_str[0..first_end],
                    &line_str[second_start..second_end]
                )?;
            } else {
                writeln!(out, "\r{}", &line_str[second_start..second_end])?;
            }
        } else {
            writeln!(out, "\r")?;
        }
    }
    Ok(())
}

#[must_use]
pub fn get_horizontal_scroll_bounds(
    line: &str,
    cols: usize,
    start: usize,
    line_numbers: bool,
    line_count: usize,
) -> (usize, usize, usize) {
    // Length of the bold+reset sequence that wraps the line numbers
    let line_number_ascii_seq_len = if line_numbers { 8 } else { 0 };
    let line_number_padding = if line_numbers {
        minus_core::utils::digits(line_count) + LineNumbers::EXTRA_PADDING + 2
    } else {
        0
    };
    let shifted_start = if line_numbers {
        start + line_number_padding + line_number_ascii_seq_len
    } else {
        start
    };

    let end = if line_numbers {
        shifted_start
            + cols
                .saturating_sub(line_number_padding)
                .min(line.len().saturating_sub(shifted_start))
    } else {
        shifted_start + cols.min(line.len().saturating_sub(shifted_start))
    };

    if line_numbers {
        // make sure "line_number_padding + line_number_ascii_seq_len" not overflow
        let first_end = (line_number_padding + line_number_ascii_seq_len).min(line.len());
        // make sure "shifted_start" not downflow
        let second_start = shifted_start.min(line.len());
        // make sure end not overflow
        let second_end = end.min(line.len());

        // check char boundary, for example "▲" takes up 3 bytes,
        // we must promise that second_start points to the first
        // byte or the fourth byte;
        let mut i = second_start;
        while i < second_end && !line.is_char_boundary(i) {
            i += 1;
        }
        (first_end, i, second_end)
    } else {
        // make sure "shifted_start" not overflow
        let resolved_start = shifted_start.min(line.len());
        // make sure end not overflow
        let resolved_end = end.min(line.len());

        // check char boundary like above
        let mut i = resolved_start;
        while i < resolved_end && !line.is_char_boundary(i) {
            i += 1;
        }
        (0, i, resolved_end)
    }
}

/// Write lines to the the output
///
/// Outputs all the `lines` to `out` without any preassumption about terminals.
/// `initial` tells any extra text to be inserted before each line. For functions that use this
/// function over terminals, this should be set to `\r` to avoid broken display.
/// The `\r` resets the cursor to the start of the line.
pub fn write_raw_lines<L: Display>(
    out: &mut impl Write,
    lines: &[L],
    initial: Option<&str>,
) -> Result<(), MinusError> {
    for line in lines {
        writeln!(out, "{}{line}", initial.unwrap_or(""))?;
    }
    Ok(())
}

#[cfg(test)]
mod tests;