[WIP] Daemon & CLI
This commit is contained in:
parent
56aad3a023
commit
8b158108bb
100 changed files with 6519 additions and 1596 deletions
287
iota-cli/src/screens/daemon_setup.rs
Normal file
287
iota-cli/src/screens/daemon_setup.rs
Normal file
|
|
@ -0,0 +1,287 @@
|
|||
use crate::{
|
||||
controls::{
|
||||
button::{ActionButton, ButtonIntent, render_button},
|
||||
choice::{ChoiceKind, render_choice_line},
|
||||
radio_group::{RadioGroup, RadioItem},
|
||||
},
|
||||
interaction_result::InteractionResult,
|
||||
render_context::RenderContext,
|
||||
screens::screens::Screen,
|
||||
};
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use ratatui::{
|
||||
Frame,
|
||||
layout::{Constraint, Layout, Rect},
|
||||
text::{Line, Text},
|
||||
widgets::{Block, Borders, Paragraph, Wrap},
|
||||
};
|
||||
use std::any::Any;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
/// Kept on screen while the launcher waits for the daemon's IPC hello. The
|
||||
/// setup choice screen is intentionally closed before its decision is sent,
|
||||
/// so without this the terminal would otherwise be blank during startup.
|
||||
pub struct DaemonStartingScreen;
|
||||
impl Screen for DaemonStartingScreen {
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||
self
|
||||
}
|
||||
fn render(&self, frame: &mut Frame, area: Rect, context: &RenderContext<'_>) {
|
||||
let popup = crate::layout::fit::centered_rect(
|
||||
area,
|
||||
crate::layout::fit::RequiredSize {
|
||||
width: 48,
|
||||
height: 5,
|
||||
},
|
||||
);
|
||||
frame.render_widget(
|
||||
Paragraph::new(
|
||||
"Starting iota-daemon…\nWaiting for its IPC handshake.\nPress Ctrl+C to cancel.",
|
||||
)
|
||||
.wrap(Wrap { trim: true })
|
||||
.block(
|
||||
Block::default()
|
||||
.title(" Iota daemon ")
|
||||
.borders(Borders::ALL)
|
||||
.border_style(context.theme.borders.normal),
|
||||
),
|
||||
popup,
|
||||
);
|
||||
}
|
||||
fn handle_input(&mut self, _: KeyEvent) -> InteractionResult {
|
||||
InteractionResult::Handled
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum DaemonLaunchMode {
|
||||
Once,
|
||||
WithUi,
|
||||
WithSystem,
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LaunchOption {
|
||||
pub mode: DaemonLaunchMode,
|
||||
pub enabled: bool,
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum DaemonSetupDecision {
|
||||
Start(DaemonLaunchMode),
|
||||
Exit,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum Focus {
|
||||
Options,
|
||||
Exit,
|
||||
Action,
|
||||
}
|
||||
|
||||
/// The launcher owns the actual side effects. This screen only presents the
|
||||
/// capabilities discovered for this machine, keeping disabled choices visible.
|
||||
pub struct DaemonSetupScreen {
|
||||
choices: RadioGroup<DaemonLaunchMode>,
|
||||
focus: Focus,
|
||||
sender: Option<oneshot::Sender<DaemonSetupDecision>>,
|
||||
message: String,
|
||||
}
|
||||
impl DaemonSetupScreen {
|
||||
pub fn new(
|
||||
options: Vec<LaunchOption>,
|
||||
message: impl Into<String>,
|
||||
sender: oneshot::Sender<DaemonSetupDecision>,
|
||||
) -> Result<Self, crate::controls::radio_group::RadioGroupError> {
|
||||
let items: Vec<RadioItem<DaemonLaunchMode>> = options
|
||||
.into_iter()
|
||||
.map(|o| RadioItem {
|
||||
value: o.mode,
|
||||
label: match o.mode {
|
||||
DaemonLaunchMode::Once => "Start once",
|
||||
DaemonLaunchMode::WithUi => "Start with Iota UI",
|
||||
DaemonLaunchMode::WithSystem => "Start with the system",
|
||||
}
|
||||
.into(),
|
||||
description: o.reason,
|
||||
enabled: o.enabled,
|
||||
disabled_reason: None,
|
||||
})
|
||||
.collect();
|
||||
let default = items
|
||||
.iter()
|
||||
.find(|item| item.enabled)
|
||||
.map(|item| item.value)
|
||||
.ok_or(crate::controls::radio_group::RadioGroupError::NoEnabledItems)?;
|
||||
let mut choices = RadioGroup::new(items, None, default)?;
|
||||
choices.set_focus_policy(crate::controls::navigation::DisabledFocusPolicy::Include);
|
||||
Ok(Self {
|
||||
choices,
|
||||
focus: Focus::Options,
|
||||
sender: Some(sender),
|
||||
message: message.into(),
|
||||
})
|
||||
}
|
||||
fn complete(&mut self, d: DaemonSetupDecision) {
|
||||
if let Some(tx) = self.sender.take() {
|
||||
let _ = tx.send(d);
|
||||
}
|
||||
}
|
||||
fn activate(&mut self) -> InteractionResult {
|
||||
match self.focus {
|
||||
Focus::Options => {
|
||||
self.choices.select_focused();
|
||||
InteractionResult::Handled
|
||||
}
|
||||
Focus::Exit => {
|
||||
self.complete(DaemonSetupDecision::Exit);
|
||||
InteractionResult::CloseScreen
|
||||
}
|
||||
Focus::Action => {
|
||||
let choice = *self.choices.selected();
|
||||
if self
|
||||
.choices
|
||||
.items()
|
||||
.iter()
|
||||
.find(|i| i.value == choice)
|
||||
.is_some_and(|i| i.enabled)
|
||||
{
|
||||
self.complete(DaemonSetupDecision::Start(choice));
|
||||
InteractionResult::CloseScreen
|
||||
} else {
|
||||
InteractionResult::Handled
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn next(&mut self) {
|
||||
self.focus = match self.focus {
|
||||
Focus::Options => {
|
||||
self.choices.focus_next();
|
||||
if self.choices.focused_item().value == DaemonLaunchMode::Once {
|
||||
Focus::Exit
|
||||
} else {
|
||||
Focus::Options
|
||||
}
|
||||
}
|
||||
Focus::Exit => Focus::Action,
|
||||
Focus::Action => Focus::Options,
|
||||
};
|
||||
}
|
||||
fn previous(&mut self) {
|
||||
self.focus = match self.focus {
|
||||
Focus::Options => {
|
||||
self.choices.focus_previous();
|
||||
if self.choices.focused_item().value == DaemonLaunchMode::WithSystem {
|
||||
Focus::Action
|
||||
} else {
|
||||
Focus::Options
|
||||
}
|
||||
}
|
||||
Focus::Exit => Focus::Options,
|
||||
Focus::Action => Focus::Exit,
|
||||
};
|
||||
}
|
||||
}
|
||||
impl Screen for DaemonSetupScreen {
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||
self
|
||||
}
|
||||
fn render(&self, frame: &mut Frame, area: Rect, context: &RenderContext<'_>) {
|
||||
let popup = crate::layout::fit::centered_rect(
|
||||
area,
|
||||
crate::layout::fit::RequiredSize {
|
||||
width: 68,
|
||||
height: 16,
|
||||
},
|
||||
);
|
||||
let mut lines = vec![Line::from(self.message.as_str()), Line::from("")];
|
||||
for item in self.choices.items() {
|
||||
lines.push(render_choice_line(
|
||||
&item.label,
|
||||
ChoiceKind::Radio,
|
||||
self.choices.visual_state(&item.value),
|
||||
context.theme,
|
||||
));
|
||||
if let Some(reason) = &item.description {
|
||||
lines.push(Line::styled(
|
||||
format!(" {reason}"),
|
||||
context.theme.text.muted,
|
||||
));
|
||||
}
|
||||
}
|
||||
let rows = Layout::vertical([Constraint::Min(1), Constraint::Length(3)]).split(popup);
|
||||
frame.render_widget(
|
||||
Paragraph::new(Text::from(lines))
|
||||
.wrap(Wrap { trim: true })
|
||||
.block(
|
||||
Block::default()
|
||||
.title(" Iota daemon setup ")
|
||||
.borders(Borders::ALL),
|
||||
),
|
||||
rows[0],
|
||||
);
|
||||
let b = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.split(rows[1]);
|
||||
render_button(
|
||||
frame,
|
||||
b[0],
|
||||
ActionButton {
|
||||
label: "Exit",
|
||||
intent: ButtonIntent::Cancel,
|
||||
focused: self.focus == Focus::Exit,
|
||||
enabled: true,
|
||||
},
|
||||
context.theme,
|
||||
);
|
||||
let selected = *self.choices.selected();
|
||||
let enabled = self
|
||||
.choices
|
||||
.items()
|
||||
.iter()
|
||||
.find(|i| i.value == selected)
|
||||
.is_some_and(|i| i.enabled);
|
||||
let label = match selected {
|
||||
DaemonLaunchMode::Once => "Start once",
|
||||
DaemonLaunchMode::WithUi => "Save and start",
|
||||
DaemonLaunchMode::WithSystem => "Configure and start",
|
||||
};
|
||||
render_button(
|
||||
frame,
|
||||
b[1],
|
||||
ActionButton {
|
||||
label,
|
||||
intent: if enabled {
|
||||
ButtonIntent::Primary
|
||||
} else {
|
||||
ButtonIntent::Destructive
|
||||
},
|
||||
focused: self.focus == Focus::Action,
|
||||
enabled,
|
||||
},
|
||||
context.theme,
|
||||
);
|
||||
}
|
||||
fn handle_input(&mut self, event: KeyEvent) -> InteractionResult {
|
||||
match event.code {
|
||||
KeyCode::Esc => {
|
||||
self.complete(DaemonSetupDecision::Exit);
|
||||
InteractionResult::CloseScreen
|
||||
}
|
||||
KeyCode::Down | KeyCode::Right | KeyCode::Tab => {
|
||||
self.next();
|
||||
InteractionResult::Handled
|
||||
}
|
||||
KeyCode::Up | KeyCode::Left | KeyCode::BackTab => {
|
||||
self.previous();
|
||||
InteractionResult::Handled
|
||||
}
|
||||
KeyCode::Enter | KeyCode::Char(' ') => self.activate(),
|
||||
_ => InteractionResult::Unhandled,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,8 @@ use crate::{
|
|||
log_card::LogCard,
|
||||
},
|
||||
interaction_result::InteractionResult,
|
||||
ipc_client::IpcConnectionState,
|
||||
ipc_client::{DaemonStatus, IpcConnectionState},
|
||||
render_context::RenderContext,
|
||||
screens::screens::{NavDirection, Screen},
|
||||
ui::UI,
|
||||
};
|
||||
|
|
@ -27,6 +28,7 @@ pub struct MainScreen {
|
|||
selected_coords: (usize, usize),
|
||||
graphs_open: bool,
|
||||
connection_status_rx: watch::Receiver<IpcConnectionState>,
|
||||
daemon_status_rx: watch::Receiver<DaemonStatus>,
|
||||
}
|
||||
|
||||
impl MainScreen {
|
||||
|
|
@ -39,10 +41,17 @@ impl MainScreen {
|
|||
vec![Some(1), Some(4)],
|
||||
];
|
||||
|
||||
let state = ui.client_state();
|
||||
let state = ui
|
||||
.client_state()
|
||||
.await
|
||||
.expect("MainScreen requires an attached daemon");
|
||||
let mut log_card = LogCard::new(state.clone());
|
||||
log_card.set_borders(Borders::TOP.union(Borders::RIGHT).union(Borders::LEFT));
|
||||
let mut console_card = ConsoleCard::new("Console", "", ui.ipc());
|
||||
let ipc = ui
|
||||
.ipc()
|
||||
.await
|
||||
.expect("MainScreen requires an attached daemon");
|
||||
let mut console_card = ConsoleCard::new("Console", "", ipc.clone());
|
||||
console_card.set_joins(Borders::TOP);
|
||||
|
||||
elements.push(Box::new(log_card));
|
||||
|
|
@ -61,7 +70,8 @@ impl MainScreen {
|
|||
|
||||
let graphs_open = true;
|
||||
|
||||
let connection_status_rx = ui.ipc().connection_status();
|
||||
let connection_status_rx = ipc.connection_status();
|
||||
let daemon_status_rx = ipc.daemon_status();
|
||||
|
||||
let mut screen = MainScreen {
|
||||
elements,
|
||||
|
|
@ -69,6 +79,7 @@ impl MainScreen {
|
|||
selected_coords: (1, 0),
|
||||
graphs_open,
|
||||
connection_status_rx,
|
||||
daemon_status_rx,
|
||||
};
|
||||
screen.focus_current();
|
||||
screen
|
||||
|
|
@ -198,22 +209,46 @@ impl Screen for MainScreen {
|
|||
self
|
||||
}
|
||||
|
||||
fn render(&self, f: &mut Frame, rect: Rect) {
|
||||
let status = self.connection_status_rx.borrow();
|
||||
let status_text = match &*status {
|
||||
fn render(&self, f: &mut Frame, rect: Rect, context: &RenderContext<'_>) {
|
||||
// A watch Ref blocks senders until it is dropped. Rendering may do
|
||||
// terminal I/O, so retain only owned snapshots for the whole frame.
|
||||
let status = self.connection_status_rx.borrow().clone();
|
||||
let daemon = self.daemon_status_rx.borrow().clone();
|
||||
let status_text = match status {
|
||||
IpcConnectionState::Connected => "Connected".to_string(),
|
||||
IpcConnectionState::Connecting => "Connecting...".to_string(),
|
||||
IpcConnectionState::Reconnecting { attempt } => {
|
||||
format!("Reconnecting (attempt {})...", attempt)
|
||||
}
|
||||
IpcConnectionState::Incompatible { message } => {
|
||||
format!("Incompatible: {}", message)
|
||||
format!("Incompatible protocol: {}", message)
|
||||
}
|
||||
IpcConnectionState::Failed { message } => {
|
||||
format!("Connection failed: {}", message)
|
||||
}
|
||||
IpcConnectionState::Disconnected => "Disconnected".to_string(),
|
||||
};
|
||||
let readiness = daemon
|
||||
.startup_phase
|
||||
.map(|phase| format!("{:?}", phase))
|
||||
.unwrap_or_else(|| "Waiting for status".into());
|
||||
let health = daemon
|
||||
.degraded_reason
|
||||
.as_deref()
|
||||
.map(|reason| format!(" — {reason}"))
|
||||
.unwrap_or_default();
|
||||
let version = if daemon.version.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" v{}", daemon.version)
|
||||
};
|
||||
let main_block = Block::default()
|
||||
.title(format!("Main [{}]", status_text))
|
||||
.borders(Borders::ALL);
|
||||
.title(format!(
|
||||
"Iota{version} [{status_text}; {readiness}{health}]"
|
||||
))
|
||||
.borders(Borders::ALL)
|
||||
.border_style(context.theme.borders.normal)
|
||||
.title_style(context.theme.borders.title);
|
||||
f.render_widget(main_block, rect);
|
||||
|
||||
let inner = rect.inner(Margin {
|
||||
|
|
@ -221,7 +256,11 @@ impl Screen for MainScreen {
|
|||
horizontal: 1,
|
||||
});
|
||||
|
||||
let graphs_width = if self.graphs_open { 30 } else { 2 };
|
||||
let graphs_width = if self.graphs_open && inner.width >= 70 {
|
||||
30
|
||||
} else {
|
||||
2
|
||||
};
|
||||
let main_width = inner.width.saturating_sub(graphs_width);
|
||||
|
||||
let horizontal_chunks = Layout::default()
|
||||
|
|
@ -239,11 +278,11 @@ impl Screen for MainScreen {
|
|||
Layout::vertical([Constraint::Min(0), Constraint::Length(3)]).split(left_area);
|
||||
|
||||
if let Some(log) = self.elements.get(0) {
|
||||
log.as_element().render(f, left_rows[0]);
|
||||
log.as_element().render(f, left_rows[0], context);
|
||||
}
|
||||
|
||||
if let Some(console) = self.elements.get(1) {
|
||||
console.as_element().render(f, left_rows[1]);
|
||||
console.as_element().render(f, left_rows[1], context);
|
||||
}
|
||||
|
||||
let graph_elements: Vec<_> = self
|
||||
|
|
@ -262,7 +301,7 @@ impl Screen for MainScreen {
|
|||
.split(right_area);
|
||||
|
||||
for (el, area) in graph_elements.iter().zip(graph_chunks.iter()) {
|
||||
el.as_element().render(f, *area);
|
||||
el.as_element().render(f, *area, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,11 +7,16 @@ use ratatui::{
|
|||
};
|
||||
use std::{any::Any, time::Duration};
|
||||
|
||||
use crate::{interaction_result::InteractionResult, screens::screens::Screen};
|
||||
use crate::{
|
||||
interaction_result::InteractionResult,
|
||||
render_context::RenderContext,
|
||||
screens::screens::Screen,
|
||||
theme::{ResolvedTheme, TextSemantics, ThemeName},
|
||||
};
|
||||
|
||||
pub struct FileViewer {
|
||||
title: String,
|
||||
text: Vec<DisplayLine>,
|
||||
content: String,
|
||||
scroll: u16,
|
||||
scroll_x: u16,
|
||||
}
|
||||
|
|
@ -24,8 +29,8 @@ impl Screen for FileViewer {
|
|||
self
|
||||
}
|
||||
|
||||
fn render(&self, f: &mut Frame, rect: Rect) {
|
||||
self.draw(f, rect);
|
||||
fn render(&self, f: &mut Frame, rect: Rect, context: &RenderContext<'_>) {
|
||||
self.draw(f, rect, context.theme);
|
||||
}
|
||||
|
||||
fn handle_input(&mut self, event: KeyEvent) -> InteractionResult {
|
||||
|
|
@ -52,7 +57,7 @@ impl FileViewer {
|
|||
pub fn new(title: String, content: &str) -> Self {
|
||||
Self {
|
||||
title,
|
||||
text: parse_document(content.to_owned()),
|
||||
content: content.to_owned(),
|
||||
scroll: 0,
|
||||
scroll_x: 0,
|
||||
}
|
||||
|
|
@ -62,7 +67,7 @@ impl FileViewer {
|
|||
terminal
|
||||
.draw(|f| {
|
||||
let area = f.area();
|
||||
self.draw(f, area);
|
||||
self.draw(f, area, &crate::theme::resolve(ThemeName::Ansi));
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
|
|
@ -77,12 +82,13 @@ impl FileViewer {
|
|||
}
|
||||
terminal
|
||||
}
|
||||
fn draw(&self, f: &mut Frame, area: Rect) {
|
||||
fn draw(&self, f: &mut Frame, area: Rect, theme: &ResolvedTheme) {
|
||||
use ratatui::text::Text;
|
||||
|
||||
let mut rendered_lines = Vec::new();
|
||||
let text = parse_document(&self.content, theme);
|
||||
|
||||
for display_line in &self.text {
|
||||
for display_line in &text {
|
||||
if display_line.scrollable {
|
||||
let content: String = display_line
|
||||
.line
|
||||
|
|
@ -153,7 +159,7 @@ impl FileViewer {
|
|||
}
|
||||
}
|
||||
}
|
||||
fn parse_document(input: String) -> Vec<DisplayLine> {
|
||||
fn parse_document(input: &str, theme: &ResolvedTheme) -> Vec<DisplayLine> {
|
||||
let mut lines_vec = Vec::new();
|
||||
let mut in_code_block = false;
|
||||
let liness: Vec<String> = input.lines().map(String::from).collect();
|
||||
|
|
@ -172,7 +178,7 @@ fn parse_document(input: String) -> Vec<DisplayLine> {
|
|||
lines_vec.push(DisplayLine {
|
||||
line: Line::from(Span::styled(
|
||||
format!("────────{}────────", code),
|
||||
Style::default().fg(Color::DarkGray),
|
||||
theme.markdown.divider,
|
||||
)),
|
||||
scrollable: false,
|
||||
});
|
||||
|
|
@ -182,10 +188,7 @@ fn parse_document(input: String) -> Vec<DisplayLine> {
|
|||
|
||||
if in_code_block {
|
||||
lines_vec.push(DisplayLine {
|
||||
line: Line::from(Span::styled(
|
||||
raw.to_string(),
|
||||
Style::default().fg(Color::Yellow),
|
||||
)),
|
||||
line: Line::from(Span::styled(raw.to_string(), theme.markdown.code)),
|
||||
scrollable: false,
|
||||
});
|
||||
i += 1;
|
||||
|
|
@ -195,9 +198,13 @@ fn parse_document(input: String) -> Vec<DisplayLine> {
|
|||
lines_vec.push(DisplayLine {
|
||||
line: Line::from(Span::styled(
|
||||
raw.trim_start_matches("### ").to_string(),
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
theme.apply_text_semantics(
|
||||
theme.markdown.heading,
|
||||
TextSemantics {
|
||||
bold: true,
|
||||
underline: false,
|
||||
},
|
||||
),
|
||||
)),
|
||||
scrollable: false,
|
||||
});
|
||||
|
|
@ -208,9 +215,13 @@ fn parse_document(input: String) -> Vec<DisplayLine> {
|
|||
lines_vec.push(DisplayLine {
|
||||
line: Line::from(Span::styled(
|
||||
raw.trim_start_matches("## ").to_string(),
|
||||
Style::default()
|
||||
.fg(Color::LightCyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
theme.apply_text_semantics(
|
||||
theme.markdown.heading,
|
||||
TextSemantics {
|
||||
bold: true,
|
||||
underline: false,
|
||||
},
|
||||
),
|
||||
)),
|
||||
scrollable: false,
|
||||
});
|
||||
|
|
@ -221,9 +232,13 @@ fn parse_document(input: String) -> Vec<DisplayLine> {
|
|||
lines_vec.push(DisplayLine {
|
||||
line: Line::from(Span::styled(
|
||||
raw.trim_start_matches("# ").to_string(),
|
||||
Style::default()
|
||||
.fg(Color::Gray)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
theme.apply_text_semantics(
|
||||
theme.markdown.heading,
|
||||
TextSemantics {
|
||||
bold: true,
|
||||
underline: false,
|
||||
},
|
||||
),
|
||||
)),
|
||||
scrollable: false,
|
||||
});
|
||||
|
|
@ -254,13 +269,13 @@ fn parse_document(input: String) -> Vec<DisplayLine> {
|
|||
}
|
||||
|
||||
let table = parse_table(&table_lines.iter().map(|s| s.as_str()).collect::<Vec<_>>());
|
||||
lines_vec.extend(table_to_lines(table));
|
||||
lines_vec.extend(table_to_lines(table, theme));
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
|
||||
lines_vec.push(DisplayLine {
|
||||
line: Line::from(parse_inline(raw.as_str())),
|
||||
line: Line::from(parse_inline(raw.as_str(), theme)),
|
||||
scrollable: false,
|
||||
});
|
||||
i += 1;
|
||||
|
|
@ -269,7 +284,7 @@ fn parse_document(input: String) -> Vec<DisplayLine> {
|
|||
lines_vec
|
||||
}
|
||||
|
||||
fn parse_inline(input: &str) -> Vec<Span<'static>> {
|
||||
fn parse_inline(input: &str, theme: &ResolvedTheme) -> Vec<Span<'static>> {
|
||||
let mut spans = Vec::new();
|
||||
let mut buf = String::new();
|
||||
|
||||
|
|
@ -294,7 +309,11 @@ fn parse_inline(input: &str) -> Vec<Span<'static>> {
|
|||
};
|
||||
|
||||
if let Some(kind) = toggle {
|
||||
flush_span(&mut spans, &mut buf, current_style(bold, underline, code));
|
||||
flush_span(
|
||||
&mut spans,
|
||||
&mut buf,
|
||||
current_style(bold, underline, code, theme),
|
||||
);
|
||||
|
||||
match kind {
|
||||
"bold" => bold = !bold,
|
||||
|
|
@ -308,24 +327,21 @@ fn parse_inline(input: &str) -> Vec<Span<'static>> {
|
|||
buf.push(c);
|
||||
}
|
||||
|
||||
flush_span(&mut spans, &mut buf, current_style(bold, underline, code));
|
||||
flush_span(
|
||||
&mut spans,
|
||||
&mut buf,
|
||||
current_style(bold, underline, code, theme),
|
||||
);
|
||||
spans
|
||||
}
|
||||
|
||||
fn current_style(bold: bool, underline: bool, code: bool) -> Style {
|
||||
let mut style = Style::default();
|
||||
|
||||
if bold {
|
||||
style = style.add_modifier(Modifier::BOLD);
|
||||
}
|
||||
if underline {
|
||||
style = style.add_modifier(Modifier::UNDERLINED);
|
||||
}
|
||||
if code {
|
||||
style = style.fg(Color::Yellow);
|
||||
}
|
||||
|
||||
style
|
||||
fn current_style(bold: bool, underline: bool, code: bool, theme: &ResolvedTheme) -> Style {
|
||||
let base = if code {
|
||||
theme.markdown.code
|
||||
} else {
|
||||
theme.markdown.normal
|
||||
};
|
||||
theme.apply_text_semantics(base, TextSemantics { bold, underline })
|
||||
}
|
||||
#[derive(Clone)]
|
||||
pub struct DisplayLine {
|
||||
|
|
@ -333,7 +349,7 @@ pub struct DisplayLine {
|
|||
scrollable: bool,
|
||||
}
|
||||
|
||||
fn table_to_lines(table: Vec<Vec<String>>) -> Vec<DisplayLine> {
|
||||
fn table_to_lines(table: Vec<Vec<String>>, theme: &ResolvedTheme) -> Vec<DisplayLine> {
|
||||
if table.len() < 2 {
|
||||
return vec![];
|
||||
}
|
||||
|
|
@ -377,7 +393,7 @@ fn table_to_lines(table: Vec<Vec<String>>) -> Vec<DisplayLine> {
|
|||
.join("─┼─");
|
||||
|
||||
lines.push(DisplayLine {
|
||||
line: Line::from(Span::styled(divider, Style::default().fg(Color::DarkGray))),
|
||||
line: Line::from(Span::styled(divider, theme.markdown.divider)),
|
||||
scrollable: true,
|
||||
});
|
||||
continue;
|
||||
|
|
@ -403,11 +419,15 @@ fn table_to_lines(table: Vec<Vec<String>>) -> Vec<DisplayLine> {
|
|||
}
|
||||
|
||||
let style = if row_idx == 0 {
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
theme.apply_text_semantics(
|
||||
theme.markdown.table_header,
|
||||
TextSemantics {
|
||||
bold: true,
|
||||
underline: false,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
Style::default().fg(Color::Green)
|
||||
theme.markdown.table_text
|
||||
};
|
||||
|
||||
lines.push(DisplayLine {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use std::any::Any;
|
|||
use crossterm::event::KeyEvent;
|
||||
use ratatui::{Frame, layout::Rect};
|
||||
|
||||
use crate::interaction_result::InteractionResult;
|
||||
use crate::{interaction_result::InteractionResult, render_context::RenderContext};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum NavDirection {
|
||||
|
|
@ -20,6 +20,6 @@ pub trait Screen: Send + Sync + Any {
|
|||
fn as_any(&self) -> &dyn Any;
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||
|
||||
fn render(&self, f: &mut Frame, rect: Rect);
|
||||
fn render(&self, f: &mut Frame, rect: Rect, context: &RenderContext<'_>);
|
||||
fn handle_input(&mut self, event: KeyEvent) -> InteractionResult;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,19 @@
|
|||
use crate::{
|
||||
controls::choice::{ChoiceKind, ChoiceVisualState, render_choice_line},
|
||||
interaction_result::InteractionResult,
|
||||
render_context::RenderContext,
|
||||
screens::{md_viewer::FileViewer, screens::Screen},
|
||||
ui::UI,
|
||||
util::{
|
||||
buttons::{checkbox, draw_buttons},
|
||||
terms_focus::Focus,
|
||||
},
|
||||
util::{buttons::draw_buttons, terms_focus::Focus},
|
||||
};
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use iota_terms::{TermsType, get_link, get_terms};
|
||||
use ratatui::{
|
||||
Frame,
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Style},
|
||||
text::{Line, Span, Text},
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
};
|
||||
use std::{any::Any, pin::Pin, sync::Arc};
|
||||
use std::{any::Any, pin::Pin};
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
|
@ -26,9 +23,7 @@ pub enum UserChoice {
|
|||
AcceptAll,
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // ui is unused
|
||||
pub struct TermsCheckerScreen {
|
||||
_ui: Arc<UI>,
|
||||
sender: Option<oneshot::Sender<UserChoice>>,
|
||||
|
||||
eula: bool,
|
||||
|
|
@ -39,9 +34,8 @@ pub struct TermsCheckerScreen {
|
|||
}
|
||||
|
||||
impl TermsCheckerScreen {
|
||||
pub fn new(ui: Arc<UI>, sender: Option<oneshot::Sender<UserChoice>>) -> Self {
|
||||
pub fn new(sender: Option<oneshot::Sender<UserChoice>>) -> Self {
|
||||
Self {
|
||||
_ui: ui,
|
||||
sender,
|
||||
eula: false,
|
||||
tos: false,
|
||||
|
|
@ -59,7 +53,7 @@ impl Screen for TermsCheckerScreen {
|
|||
self
|
||||
}
|
||||
|
||||
fn render(&self, f: &mut Frame, size: Rect) {
|
||||
fn render(&self, f: &mut Frame, size: Rect, context: &RenderContext<'_>) {
|
||||
let mut needed_height = 5;
|
||||
|
||||
if size.height < 6 || size.width < 27 {
|
||||
|
|
@ -169,9 +163,36 @@ impl Screen for TermsCheckerScreen {
|
|||
)
|
||||
};
|
||||
let mut text_lines = vec![
|
||||
checkbox(eula_text, self.eula, self.focus == Focus::Eula, true),
|
||||
checkbox(tos_text, self.tos, self.focus == Focus::Tos, self.eula),
|
||||
checkbox(pp_text, self.pp, self.focus == Focus::Pp, self.eula),
|
||||
render_choice_line(
|
||||
eula_text,
|
||||
ChoiceKind::Checkbox,
|
||||
ChoiceVisualState {
|
||||
selected: self.eula,
|
||||
focused: self.focus == Focus::Eula,
|
||||
enabled: true,
|
||||
},
|
||||
context.theme,
|
||||
),
|
||||
render_choice_line(
|
||||
tos_text,
|
||||
ChoiceKind::Checkbox,
|
||||
ChoiceVisualState {
|
||||
selected: self.tos,
|
||||
focused: self.focus == Focus::Tos,
|
||||
enabled: self.eula,
|
||||
},
|
||||
context.theme,
|
||||
),
|
||||
render_choice_line(
|
||||
pp_text,
|
||||
ChoiceKind::Checkbox,
|
||||
ChoiceVisualState {
|
||||
selected: self.pp,
|
||||
focused: self.focus == Focus::Pp,
|
||||
enabled: self.eula,
|
||||
},
|
||||
context.theme,
|
||||
),
|
||||
Line::from(""),
|
||||
Line::from("¹ Necessary– required to run the program"),
|
||||
Line::from("² Optional – required only for Tensamin services"),
|
||||
|
|
@ -191,19 +212,19 @@ impl Screen for TermsCheckerScreen {
|
|||
|
||||
if size.width < 60 || size.height < needed_height as u16 {
|
||||
let width_style = if size.width > 76 {
|
||||
Style::default().fg(Color::Green)
|
||||
context.theme.status.success
|
||||
} else if size.width >= 60 {
|
||||
Style::default().fg(Color::Yellow)
|
||||
context.theme.status.warning
|
||||
} else {
|
||||
Style::default().fg(Color::Red)
|
||||
context.theme.status.error
|
||||
};
|
||||
|
||||
let height_style = if size.height > 19 {
|
||||
Style::default().fg(Color::Green)
|
||||
context.theme.status.success
|
||||
} else if size.height >= 13 {
|
||||
Style::default().fg(Color::Yellow)
|
||||
context.theme.status.warning
|
||||
} else {
|
||||
Style::default().fg(Color::Red)
|
||||
context.theme.status.error
|
||||
};
|
||||
|
||||
let warning_text = Text::from(vec![
|
||||
|
|
@ -245,6 +266,7 @@ impl Screen for TermsCheckerScreen {
|
|||
true,
|
||||
false,
|
||||
true,
|
||||
context.theme,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
use crate::screens::terms_checker::UserChoice;
|
||||
use crate::{
|
||||
controls::choice::{ChoiceKind, ChoiceVisualState, render_choice_line},
|
||||
interaction_result::InteractionResult,
|
||||
render_context::RenderContext,
|
||||
screens::{md_viewer::FileViewer, screens::Screen},
|
||||
util::{
|
||||
buttons::{checkbox, draw_buttons},
|
||||
terms_focus::Focus,
|
||||
},
|
||||
util::{buttons::draw_buttons, terms_focus::Focus},
|
||||
};
|
||||
use chrono::{Local, TimeZone, Utc};
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
|
|
@ -13,7 +12,6 @@ use iota_terms::{Doc, TermsType, get_newest_link, get_terms};
|
|||
use ratatui::{
|
||||
Frame,
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Style},
|
||||
text::{Line, Span, Text},
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
};
|
||||
|
|
@ -123,7 +121,19 @@ impl Screen for TermsUpdaterScreen {
|
|||
self
|
||||
}
|
||||
|
||||
fn render(&self, f: &mut Frame, size: Rect) {
|
||||
fn render(&self, f: &mut Frame, size: Rect, context: &RenderContext<'_>) {
|
||||
let checkbox = |label, selected, focused, enabled| {
|
||||
render_choice_line(
|
||||
label,
|
||||
ChoiceKind::Checkbox,
|
||||
ChoiceVisualState {
|
||||
selected,
|
||||
focused,
|
||||
enabled,
|
||||
},
|
||||
context.theme,
|
||||
)
|
||||
};
|
||||
let mut needed_height = 5;
|
||||
|
||||
if size.height < 6 || size.width < 27 {
|
||||
|
|
@ -523,19 +533,19 @@ impl Screen for TermsUpdaterScreen {
|
|||
};
|
||||
if size.width < 60 || size.height < needed_height as u16 {
|
||||
let width_style = if size.width > 76 {
|
||||
Style::default().fg(Color::Green)
|
||||
context.theme.status.success
|
||||
} else if size.width >= 60 {
|
||||
Style::default().fg(Color::Yellow)
|
||||
context.theme.status.warning
|
||||
} else {
|
||||
Style::default().fg(Color::Red)
|
||||
context.theme.status.error
|
||||
};
|
||||
|
||||
let height_style = if size.height > 20 {
|
||||
Style::default().fg(Color::Green)
|
||||
context.theme.status.success
|
||||
} else if size.height >= (header_lines as u16 + 10) {
|
||||
Style::default().fg(Color::Yellow)
|
||||
context.theme.status.warning
|
||||
} else {
|
||||
Style::default().fg(Color::Red)
|
||||
context.theme.status.error
|
||||
};
|
||||
|
||||
let warning_text = Text::from(vec![
|
||||
|
|
@ -579,6 +589,7 @@ impl Screen for TermsUpdaterScreen {
|
|||
self.update_needed,
|
||||
downgrade_scenario,
|
||||
self.pp_needed || self.tos_needed,
|
||||
context.theme,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue