shep 0.25.2

The shep binary: a process manager that keeps a flock of long-running processes alive on macOS, Linux and Windows, with logs, watch and cron restarts, and webhook alerts
Documentation
//! The box, the borderless fallback's frame, and the mute pass behind
//! them. Shared by the close dialog (1g) and the keymap overlay (1k).
//!
//! Both frames draw a bordered box over a dimmed body, differing in one
//! number: 1g's interior is 86 cells and 1k's is 126. Written once here
//! rather than twice, because two box drawers differing in a constant is
//! the shape four `create_*_file` helpers took in shep-core before one
//! helper replaced them.

use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Style;
use ratatui::text::Line;

use super::super::theme::Palette;

/// The border's four corners, checked against `unicodedata.east_asian_width`
/// and found Neutral, same as [`BOX_LEFT`].
///
/// `pub(super)`: `keymap`'s own tests build the boxed top border's full shape
/// from these directly, since several of them also appear in the keymap's
/// sheep art.
pub(super) const BOX_TOP_LEFT: char = '▛';
pub(super) const BOX_TOP_RIGHT: char = '▜';
const BOX_BOTTOM_LEFT: char = '▙';
const BOX_BOTTOM_RIGHT: char = '▟';

/// The left edge. Neutral, unlike the other three edge glyphs below.
const BOX_LEFT: char = '▐';

/// The top, bottom and right edges. All three are East-Asian Ambiguous,
/// checked the same way the rulings ask `▌` to be. Kept anyway: `─` already
/// draws every hairline rule in this pane and `█░` every gauge, both
/// Ambiguous too.
///
/// `the_border_vocabulary_is_the_one_that_was_checked` pins the set.
pub(super) const BOX_TOP: char = '▀';
const BOX_BOTTOM: char = '▄';
const BOX_RIGHT: char = '▌';

/// The narrowest terminal that draws a box with an `interior`-cell inside.
///
/// The interior, a border cell each side, and a margin cell each side. 1g's
/// 86 gives 90 and 1k's 126 gives 130, and `draw_boxed`'s own `margin`
/// arithmetic comes out at 1 at either floor.
pub(super) const fn floor_for(interior: u16) -> u16 {
    // saturating_add: this module protects every addition consistently, so
    // a bare `+` here would read as an oversight even though it is
    // unreachable at the real interior of 126.
    interior.saturating_add(4)
}

/// Whether a `width`-column terminal draws the box, or gives way to the
/// borderless form.
pub(super) const fn is_boxed(width: u16, interior: u16) -> bool {
    width >= floor_for(interior)
}

/// The rows a boxed overlay occupies: its own lines plus a border above
/// and below.
///
/// Both the fit check and the draw read this rather than each computing
/// it separately: a mismatch here would refuse to draw in one place and
/// draw a wrong-sized box in the other, unreachable with a dialog of a
/// dozen rows but exactly the class one shared function forecloses.
pub(super) fn boxed_height(lines: &[Line<'_>]) -> u16 {
    u16::try_from(lines.len())
        .unwrap_or(u16::MAX)
        .saturating_add(2)
}

/// The boxed form: `interior` cells wide, centred in `area`, its rows
/// vertically centred too.
///
/// `lines` comes from the caller, which has already measured them against
/// `area.height` to decide this form fits at all.
///
/// `ground` is the interior's own background: 1g passes [`Style::reset`].
pub(super) fn draw_boxed(
    lines: &[Line<'_>],
    interior: u16,
    palette: Palette,
    ground: Style,
    area: Rect,
    buffer: &mut Buffer,
) {
    let box_height = boxed_height(lines);
    let rows = box_height.saturating_sub(2);
    // `saturating_add`, not `+`, on `interior` specifically: it is a const
    // 126 at both call sites and 65534 is unreachable, but `boxed_height`
    // right above this line saturates the same class of addition. The rest
    // of this function's arithmetic is screen coordinates, and stays bare.
    let margin = area.width.saturating_sub(interior.saturating_add(2)) / 2;
    let box_x = area.x + margin;
    let box_y = area.y + area.height.saturating_sub(box_height) / 2;
    let line_style = palette.line();
    // Built once rather than per row. `Buffer::set_string` wants a `&str` and
    // the glyphs are `char` consts, so each row was allocating two one-glyph
    // strings and one `interior`-wide blank inside the loop below.
    let left = BOX_LEFT.to_string();
    let right = BOX_RIGHT.to_string();
    let blank = blank_of(interior);

    buffer.set_string(
        box_x,
        box_y,
        format!(
            "{BOX_TOP_LEFT}{}{BOX_TOP_RIGHT}",
            BOX_TOP.to_string().repeat(usize::from(interior))
        ),
        line_style,
    );
    for (offset, line) in lines.iter().enumerate() {
        // `expect`, not `unwrap_or(0)`: the fallback wrote line 65536 over
        // line 0, the silent-wrong-position failure this module's own
        // `boxed_height` doc argues against. The caller has already measured
        // `lines` against `area.height` to pick this form.
        let offset = u16::try_from(offset).expect("a boxed form is at most area.height lines");
        let y = box_y + 1 + offset;
        buffer.set_string(box_x, y, &left, line_style);
        blank_row(buffer, box_x + 1, y, &blank, ground);
        buffer.set_line(box_x + 1, y, line, interior);
        buffer.set_string(box_x + 1 + interior, y, &right, line_style);
    }
    buffer.set_string(
        box_x,
        box_y + 1 + rows,
        format!(
            "{BOX_BOTTOM_LEFT}{}{BOX_BOTTOM_RIGHT}",
            BOX_BOTTOM.to_string().repeat(usize::from(interior))
        ),
        line_style,
    );
}

/// `blank`'s worth of plain space at `(x, y)`, in `style`: the dialog itself
/// is never muted, only the pane behind it. [`blank_of`] builds the argument,
/// and every caller builds it once above its own row loop.
///
/// [`Buffer::set_line`] writes only the cells a `Line` carries content for, so
/// a blank separator row writes nothing and would leave whatever the field
/// list drew there showing through, muted, inside a solid dialog.
pub(super) fn blank_row(buffer: &mut Buffer, x: u16, y: u16, blank: &str, style: Style) {
    buffer.set_string(x, y, blank, style);
}

/// A row's worth of spaces, for [`blank_row`] to write.
///
/// Split from it because all three callers draw inside a loop over rows.
/// Hoisting the allocation here is free, unlike replacing a derived
/// constant with a literal: the width still comes from one expression.
pub(super) fn blank_of(width: u16) -> String {
    " ".repeat(usize::from(width))
}

/// Dims everything already drawn in `area`, so an overlay reads as a
/// question about what is behind it rather than as a new screen.
///
/// Two calls, not one: `Buffer::set_style` patches a cell rather than
/// replacing it, so a single `palette.muted()` would leave a title band's
/// reverse video and a selected row's ground under the new ink.
/// `Style::reset()` clears both first.
pub(super) fn mute(buffer: &mut Buffer, area: Rect, palette: Palette) {
    buffer.set_style(area, Style::reset());
    buffer.set_style(area, palette.muted());
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::lookout::view::fixtures;
    use crate::output::width::char_columns;

    /// The reset half: a title band's reverse video and a selected row's
    /// own ground both patch onto a cell rather than replace it, which is
    /// exactly what a single `palette.muted()` call would leave standing.
    #[test]
    fn mute_clears_the_ground_and_reverse_video_it_finds() {
        let area = Rect::new(0, 0, 1, 1);
        let mut buffer = Buffer::empty(area);
        buffer.set_style(
            area,
            Style::default()
                .bg(ratatui::style::Color::Red)
                .add_modifier(ratatui::style::Modifier::REVERSED),
        );
        let palette = fixtures::coloured();
        mute(&mut buffer, area, palette);
        let style = buffer[(0, 0)].style();
        assert!(
            !style
                .add_modifier
                .contains(ratatui::style::Modifier::REVERSED),
            "reverse video from underneath survived the mute: {style:?}"
        );
        assert_eq!(
            style.bg,
            Some(ratatui::style::Color::Reset),
            "the prior background survived the mute: {style:?}"
        );
        assert_eq!(
            style.fg,
            palette.muted().fg,
            "muted's own ink did not land: {style:?}"
        );
    }

    /// `boxed_height` is pinned on its own below; this is the margin and
    /// vertical-centre arithmetic around it, which nothing else in this
    /// module exercises directly. A 20-wide area with a 10-cell interior
    /// gives a 12-cell box, margin `(20 - 12) / 2 = 4`; a one-line box is
    /// three rows tall in a 10-row area, margin `(10 - 3) / 2 = 3`.
    #[test]
    fn draw_boxed_centres_the_box_in_the_area() {
        let area = Rect::new(0, 0, 20, 10);
        let mut buffer = Buffer::empty(area);
        let lines = [Line::default()];
        draw_boxed(
            &lines,
            10,
            fixtures::plain(),
            Style::reset(),
            area,
            &mut buffer,
        );
        assert_eq!(
            buffer[(4, 3)].symbol(),
            BOX_TOP_LEFT.to_string(),
            "top-left corner is not where the margin arithmetic puts it"
        );
        assert_eq!(
            buffer[(4, 5)].symbol(),
            BOX_BOTTOM_LEFT.to_string(),
            "bottom-left corner is not where the margin arithmetic puts it"
        );
    }

    /// The two rows a border costs, and the saturation this function's own
    /// doc spends a paragraph on.
    ///
    /// 65_534 lines is where `saturating_add` starts to bite, and it is
    /// unreachable from any caller: the overlay's own form is nineteen rows.
    /// Pinned anyway, because an unreachable arm the doc argues about gets
    /// "simplified" into a plain `+ 2`.
    #[test]
    fn the_box_costs_two_rows_and_saturates_rather_than_wrapping() {
        assert_eq!(boxed_height(&[]), 2);
        assert_eq!(boxed_height(&vec![Line::default(); 17]), 19);
        assert_eq!(
            boxed_height(&vec![Line::default(); usize::from(u16::MAX) - 1]),
            u16::MAX
        );
        assert_eq!(
            boxed_height(&vec![Line::default(); usize::from(u16::MAX) + 5]),
            u16::MAX,
            "past u16 the try_from fallback takes over, and it saturates too"
        );
    }

    /// The floor is the interior plus a border cell and a margin cell each
    /// side. 1g's own floor is 90 over an 86-cell interior and 1k's is 130
    /// over a 126-cell one, and both come out of this one expression,
    /// matching `docs/lookout/design-files/README.md`'s own corrected
    /// values.
    #[test]
    fn both_frames_floors_come_out_of_one_expression() {
        assert_eq!(floor_for(86), 90, "1g");
        assert_eq!(floor_for(126), 130, "1k");
    }

    /// The box draws at its floor and gives way one column under it.
    #[test]
    fn the_floor_is_the_narrowest_boxed_width() {
        assert!(is_boxed(90, 86));
        assert!(!is_boxed(89, 86));
        assert!(is_boxed(130, 126));
        assert!(!is_boxed(129, 126));
    }

    /// The three the check found. `▐` is Neutral and the four corners are
    /// too; `▀`, `▄` and `▌` are East-Asian Ambiguous, and a terminal that
    /// doubles the right edge shifts every interior row. Recorded rather
    /// than fixed, since no Neutral right-half block exists to swap in.
    #[test]
    fn the_border_vocabulary_is_the_one_that_was_checked() {
        for glyph in [
            BOX_TOP_LEFT,
            BOX_TOP_RIGHT,
            BOX_BOTTOM_LEFT,
            BOX_BOTTOM_RIGHT,
            BOX_LEFT,
            BOX_TOP,
            BOX_BOTTOM,
            BOX_RIGHT,
            '▘',
            '⌫',
        ] {
            assert_eq!(char_columns(glyph), 1, "{glyph}");
        }
    }
}