[WIP] Daemon & CLI

This commit is contained in:
Alex-Emmet 2026-07-23 23:13:02 +02:00
commit 8b158108bb
100 changed files with 6519 additions and 1596 deletions

View 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,
}
}
}

View file

@ -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);
}
}
}

View file

@ -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 {

View file

@ -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;
}

View file

@ -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,
);
}

View file

@ -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,
);
}