taba_cli/tui/view/widgets/
mod.rs1use 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
16pub 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
35pub 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
46pub 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
79pub 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
104pub 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
119pub fn short(id: &str) -> String {
121 short_id(id)
122}
123
124#[cfg(test)]
125mod tests;