[Wip] CLI & Daemon
This commit is contained in:
parent
3bc5cc959a
commit
6a535099bb
44 changed files with 4417 additions and 303 deletions
|
|
@ -3,7 +3,7 @@ use ratatui::{
|
|||
Frame,
|
||||
layout::{Alignment, Rect},
|
||||
text::Span,
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
widgets::Paragraph,
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
|
@ -39,9 +39,8 @@ pub fn render_button(
|
|||
}
|
||||
};
|
||||
frame.render_widget(
|
||||
Paragraph::new(Span::styled(button.label, style))
|
||||
.alignment(Alignment::Center)
|
||||
.block(Block::default().borders(Borders::ALL)),
|
||||
Paragraph::new(Span::styled(if button.focused { format!("› {}", button.label) } else { button.label.to_owned() }, style))
|
||||
.alignment(Alignment::Center),
|
||||
area,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
62
iota-cli/src/controls/header.rs
Normal file
62
iota-cli/src/controls/header.rs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
use crate::theme::ResolvedTheme;
|
||||
use crate::{
|
||||
controls::button::{ActionButton, ButtonIntent, render_button},
|
||||
screens::screens::{AppAction, HitMap},
|
||||
};
|
||||
use ratatui::{
|
||||
Frame,
|
||||
layout::{Constraint, Layout, Rect},
|
||||
text::Span,
|
||||
widgets::Paragraph,
|
||||
};
|
||||
|
||||
/// Shared application bar. The brand cell is deliberately an action so it is
|
||||
/// a reliable way home from every screen.
|
||||
pub fn render_header(
|
||||
frame: &mut Frame,
|
||||
area: Rect,
|
||||
title: &str,
|
||||
theme: &ResolvedTheme,
|
||||
hits: &mut HitMap,
|
||||
focused_action: Option<usize>,
|
||||
) {
|
||||
let cells = Layout::horizontal([
|
||||
Constraint::Min(28),
|
||||
Constraint::Length(12),
|
||||
Constraint::Length(12),
|
||||
Constraint::Length(12),
|
||||
Constraint::Length(8),
|
||||
])
|
||||
.split(area);
|
||||
frame.render_widget(
|
||||
Paragraph::new(Span::styled(format!(" {title}"), theme.surfaces.toolbar)),
|
||||
cells[0],
|
||||
);
|
||||
hits.register(cells[0], AppAction::OpenMain);
|
||||
for (index, (area, label, action)) in [
|
||||
(cells[1], "Overview", AppAction::OpenOverview),
|
||||
(cells[2], "Users", AppAction::OpenUsers),
|
||||
(cells[3], "Settings", AppAction::OpenSettings),
|
||||
(cells[4], "Quit", AppAction::Quit),
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
render_button(
|
||||
frame,
|
||||
area,
|
||||
ActionButton {
|
||||
label,
|
||||
intent: if action == AppAction::Quit {
|
||||
ButtonIntent::Destructive
|
||||
} else {
|
||||
ButtonIntent::Neutral
|
||||
},
|
||||
focused: focused_action == Some(index),
|
||||
enabled: true,
|
||||
},
|
||||
theme,
|
||||
);
|
||||
hits.register(area, action);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
pub mod action;
|
||||
pub mod button;
|
||||
pub mod checkbox_group;
|
||||
pub mod header;
|
||||
pub mod choice;
|
||||
pub mod navigation;
|
||||
pub mod panel;
|
||||
pub mod radio_group;
|
||||
pub mod scroll;
|
||||
|
|
|
|||
23
iota-cli/src/controls/panel.rs
Normal file
23
iota-cli/src/controls/panel.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
use crate::theme::{ChromeMode, ResolvedTheme};
|
||||
use ratatui::{Frame, layout::Rect, widgets::{Block, Borders, Paragraph}};
|
||||
|
||||
/// Draw a conventional outlined panel or a filled surface from the same call
|
||||
/// site. Screens can migrate without embedding theme branches in layouts.
|
||||
pub fn render_panel(frame: &mut Frame, area: Rect, title: &str, focused: bool, theme: &ResolvedTheme) -> Rect {
|
||||
match theme.chrome {
|
||||
ChromeMode::Bordered => {
|
||||
let block = Block::default().title(title).borders(Borders::ALL).border_style(if focused { theme.borders.focused } else { theme.borders.normal });
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
inner
|
||||
}
|
||||
ChromeMode::Surfaces => {
|
||||
frame.render_widget(Block::default().style(if focused { theme.surfaces.panel_focused } else { theme.surfaces.panel }), area);
|
||||
let header = Rect { x: area.x, y: area.y, width: area.width, height: area.height.min(1) };
|
||||
frame.render_widget(Paragraph::new(format!(" {title}")).style(theme.surfaces.panel_alternate), header);
|
||||
// Surface panels use a single header row. A one-cell inset keeps
|
||||
// compact controls such as the console usable at height three.
|
||||
Rect { x: area.x.saturating_add(1), y: area.y.saturating_add(1), width: area.width.saturating_sub(2), height: area.height.saturating_sub(1) }
|
||||
}
|
||||
}
|
||||
}
|
||||
20
iota-cli/src/controls/scroll.rs
Normal file
20
iota-cli/src/controls/scroll.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
use ratatui::{Frame, layout::Rect, widgets::{Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState}};
|
||||
|
||||
/// Reusable viewport policy for long, vertically stacked terminal content.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct ScrollOptions { pub show_scrollbar: bool, pub render_partial_components: bool }
|
||||
impl Default for ScrollOptions { fn default() -> Self { Self { show_scrollbar: true, render_partial_components: true } } }
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ScrollField { pub offset: u16, pub options: ScrollOptions }
|
||||
impl ScrollField {
|
||||
pub fn up(&mut self, amount: u16) { self.offset = self.offset.saturating_sub(amount); }
|
||||
pub fn down(&mut self, amount: u16, content_height: u16, viewport_height: u16) { self.offset = (self.offset.saturating_add(amount)).min(content_height.saturating_sub(viewport_height)); }
|
||||
pub fn render(&self, frame: &mut Frame, area: Rect, content: Paragraph<'_>, content_height: u16) {
|
||||
frame.render_widget(content.scroll((self.offset, 0)), area);
|
||||
if self.options.show_scrollbar && content_height > area.height {
|
||||
let mut state = ScrollbarState::new(content_height as usize).position(self.offset as usize);
|
||||
frame.render_stateful_widget(Scrollbar::new(ScrollbarOrientation::VerticalRight), area, &mut state);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue