Skip to main content

taba_cli/tui/view/widgets/
mod.rs

1//! The small pieces the screens share (doc 34, `view/widgets`): popup placement, scrolling text with its bar, the
2//! selected row, the "incomplete" seal and the missing-permission notice.
3
4use ratatui::layout::{Alignment, Constraint, Layout, Rect};
5use ratatui::style::Style;
6use ratatui::text::{Line, Span};
7use ratatui::widgets::{
8    Block, Clear, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, Wrap,
9};
10use ratatui::Frame;
11
12use crate::tui::api::{Missing, Severity};
13use crate::tui::model::{short_id, Fail, Model};
14use crate::tui::theme::Theme;
15
16/// A rectangle of at most `width` × `height`, centered in `area`.
17pub fn centered(area: Rect, width: u16, height: u16) -> Rect {
18    let width = width.min(area.width);
19    let height = height.min(area.height);
20    let [_, row, _] = Layout::vertical([
21        Constraint::Fill(1),
22        Constraint::Length(height),
23        Constraint::Fill(1),
24    ])
25    .areas(area);
26    let [_, cell, _] = Layout::horizontal([
27        Constraint::Fill(1),
28        Constraint::Length(width),
29        Constraint::Fill(1),
30    ])
31    .areas(row);
32    cell
33}
34
35/// Clears a popup's area and draws its frame; returns the inside.
36pub fn popup(f: &mut Frame, area: Rect, title: &str, style: Style) -> Rect {
37    f.render_widget(Clear, area);
38    let block = Block::bordered()
39        .title(format!(" {title} "))
40        .border_style(style);
41    let inner = block.inner(area);
42    f.render_widget(block, area);
43    inner
44}
45
46/// Text that scrolls by `offset` lines; the bar shows only when the text is longer than the area (TUI-FRM-04).
47/// Returns the offset actually used (clamped).
48pub fn scrolling(f: &mut Frame, area: Rect, lines: Vec<Line<'static>>, offset: u16) -> u16 {
49    let total = lines.len();
50    let height = usize::from(area.height);
51    let max = total.saturating_sub(height);
52    let used = usize::from(offset).min(max);
53    let overflow = total > height;
54    let text_area = if overflow && area.width > 2 {
55        Rect {
56            width: area.width - 1,
57            ..area
58        }
59    } else {
60        area
61    };
62    f.render_widget(
63        Paragraph::new(lines)
64            .wrap(Wrap { trim: false })
65            .scroll((u16::try_from(used).unwrap_or(u16::MAX), 0)),
66        text_area,
67    );
68    if overflow {
69        let mut state = ScrollbarState::new(max).position(used);
70        f.render_stateful_widget(
71            Scrollbar::new(ScrollbarOrientation::VerticalRight),
72            area,
73            &mut state,
74        );
75    }
76    u16::try_from(used).unwrap_or(u16::MAX)
77}
78
79/// The seal of an incomplete read: which nodes did not answer, and why (D-TUI-10).
80pub fn partial_seal(m: &Model, theme: Theme, missing: &[Missing]) -> Line<'static> {
81    let tx = m.tx();
82    let names: Vec<String> = missing
83        .iter()
84        .map(|n| {
85            let short = short_id(&n.node_id);
86            if n.reason.is_empty() {
87                short
88            } else {
89                format!("{short} ({})", n.reason)
90            }
91        })
92        .collect();
93    let text = if names.is_empty() {
94        tx.t("seal.partial")
95    } else {
96        tx.tw("seal.partial-nodes", &[("nodes", &names.join(", "))])
97    };
98    Line::from(Span::styled(
99        format!("{} {text}", theme.icon(Severity::Warning)),
100        theme.warning(),
101    ))
102}
103
104/// The explanation of a refused call (TUI-FRM-10): what is missing, in words.
105pub fn denied_text(m: &Model, fail: &Fail) -> String {
106    let tx = m.tx();
107    match fail.required.as_deref() {
108        Some("root") => tx.t("denied.root"),
109        Some(permission) => tx.tw("denied.permission", &[("permission", &permission)]),
110        None if fail.status == 401 => tx.t("denied.unauthorized"),
111        None => tx.t("denied.generic"),
112    }
113}
114
115pub fn centered_line(text: impl Into<String>, style: Style) -> Paragraph<'static> {
116    Paragraph::new(Line::from(Span::styled(text.into(), style))).alignment(Alignment::Center)
117}
118
119/// A short form of a node id for tables: the first eight characters.
120pub fn short(id: &str) -> String {
121    short_id(id)
122}
123
124#[cfg(test)]
125mod tests;