yuru-tui 0.1.9

Terminal user interface for Yuru
Documentation
use crossterm::terminal;

use crate::api::TuiLayout;
use crate::preview::{PreviewGeometry, PreviewRender};

#[derive(Clone, Copy, Debug)]
pub(crate) struct Viewport {
    pub(crate) width: usize,
    pub(crate) rows: usize,
}

impl Viewport {
    pub(crate) fn from_terminal(height: Option<usize>, reserve_prompt_row: bool) -> Self {
        let (width, terminal_rows) = terminal::size().unwrap_or((80, 24));
        let max_rows = if reserve_prompt_row {
            usize::from(terminal_rows).saturating_sub(1).max(1)
        } else {
            usize::from(terminal_rows).max(1)
        };
        Self {
            width: usize::from(width).max(1),
            rows: height.unwrap_or(max_rows).clamp(1, max_rows),
        }
    }
}

pub(super) fn visible_line_count(text: Option<&str>) -> usize {
    text.map(|text| text.lines().count()).unwrap_or(0)
}

pub(super) fn content_start_row(layout: TuiLayout, has_prompt: bool) -> usize {
    usize::from(has_prompt && !layout.prompt_at_bottom())
}

pub(super) fn footer_start_row(
    content_top: usize,
    viewport_rows: usize,
    footer_rows: usize,
) -> usize {
    if footer_rows == 0 {
        return 0;
    }

    content_top
        .saturating_add(viewport_rows)
        .saturating_sub(footer_rows)
}

pub(super) fn preview_width(total_width: usize, preview: Option<&PreviewRender<'_>>) -> usize {
    preview_width_for_presence(total_width, preview.is_some())
}

fn preview_width_for_presence(total_width: usize, has_preview: bool) -> usize {
    if !has_preview || total_width < 30 {
        0
    } else {
        (total_width / 2).clamp(12, total_width.saturating_sub(12))
    }
}

pub(crate) fn preview_geometry(
    viewport: Viewport,
    layout: TuiLayout,
    has_prompt: bool,
    has_preview: bool,
) -> Option<PreviewGeometry> {
    let columns = preview_width_for_presence(viewport.width, has_preview);
    if columns == 0 {
        return None;
    }
    let left = viewport.width.saturating_sub(columns);
    let top = content_start_row(layout, has_prompt);
    Some(PreviewGeometry {
        columns,
        lines: viewport.rows,
        left,
        top,
    })
}

pub(super) fn scroll_offset(selected: usize, len: usize, rows: usize) -> usize {
    if len == 0 || selected < rows {
        0
    } else {
        selected + 1 - rows
    }
}

pub(super) fn truncate_to_width_with_ellipsis(text: &str, width: usize, ellipsis: &str) -> String {
    let char_count = text.chars().count();
    if char_count <= width {
        return text.to_string();
    }
    if width == 0 {
        return String::new();
    }

    let ellipsis_width = ellipsis.chars().count().min(width);
    let mut out: String = text
        .chars()
        .take(width.saturating_sub(ellipsis_width))
        .collect();
    out.extend(ellipsis.chars().take(ellipsis_width));
    out
}