caching
This commit is contained in:
parent
6a535099bb
commit
009173a97d
49 changed files with 1788 additions and 389 deletions
|
|
@ -39,8 +39,15 @@ pub fn render_button(
|
|||
}
|
||||
};
|
||||
frame.render_widget(
|
||||
Paragraph::new(Span::styled(if button.focused { format!("› {}", button.label) } else { button.label.to_owned() }, style))
|
||||
.alignment(Alignment::Center),
|
||||
Paragraph::new(Span::styled(
|
||||
if button.focused {
|
||||
format!("› {}", button.label)
|
||||
} else {
|
||||
button.label.to_owned()
|
||||
},
|
||||
style,
|
||||
))
|
||||
.alignment(Alignment::Center),
|
||||
area,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
pub mod action;
|
||||
pub mod button;
|
||||
pub mod checkbox_group;
|
||||
pub mod header;
|
||||
pub mod choice;
|
||||
pub mod header;
|
||||
pub mod navigation;
|
||||
pub mod panel;
|
||||
pub mod radio_group;
|
||||
|
|
|
|||
|
|
@ -1,23 +1,60 @@
|
|||
use crate::theme::{ChromeMode, ResolvedTheme};
|
||||
use ratatui::{Frame, layout::Rect, widgets::{Block, Borders, Paragraph}};
|
||||
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 {
|
||||
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 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);
|
||||
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) }
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,53 @@
|
|||
use ratatui::{Frame, layout::Rect, widgets::{Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState}};
|
||||
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 } } }
|
||||
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 }
|
||||
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) {
|
||||
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);
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -174,12 +174,7 @@ impl ConsoleCard {
|
|||
fn is_destructive(command: &str) -> bool {
|
||||
matches!(
|
||||
command.trim_start_matches('/').trim(),
|
||||
"restart"
|
||||
| "reload"
|
||||
| "stop"
|
||||
| "shutdown"
|
||||
| "regenerate keys"
|
||||
| "identity rotate"
|
||||
"restart" | "reload" | "stop" | "shutdown" | "regenerate keys" | "identity rotate"
|
||||
) || command
|
||||
.trim_start_matches('/')
|
||||
.trim_start()
|
||||
|
|
|
|||
|
|
@ -40,9 +40,18 @@ impl GRAPHS {
|
|||
Err(_) => return Vec::new(),
|
||||
};
|
||||
match self {
|
||||
GRAPHS::Ram => state.with_width(sample_width.min(u16::MAX as usize) as u16).ram.clone(),
|
||||
GRAPHS::Cpu => state.with_width(sample_width.min(u16::MAX as usize) as u16).cpu.clone(),
|
||||
GRAPHS::Ping => state.with_width(sample_width.min(u16::MAX as usize) as u16).ping.clone(),
|
||||
GRAPHS::Ram => state
|
||||
.with_width(sample_width.min(u16::MAX as usize) as u16)
|
||||
.ram
|
||||
.clone(),
|
||||
GRAPHS::Cpu => state
|
||||
.with_width(sample_width.min(u16::MAX as usize) as u16)
|
||||
.cpu
|
||||
.clone(),
|
||||
GRAPHS::Ping => state
|
||||
.with_width(sample_width.min(u16::MAX as usize) as u16)
|
||||
.ping
|
||||
.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -141,17 +150,32 @@ impl Element for GraphCard {
|
|||
};
|
||||
|
||||
let surface = matches!(context.theme.chrome, crate::theme::ChromeMode::Surfaces);
|
||||
let title = format!("{}: {}{} {}min/{}max", self.title, graph.last().unwrap_or(&(0.0, 0.0)).1 as i64, unit, min_y as i64, max_y as i64);
|
||||
let plot_area = if surface { crate::controls::panel::render_panel(f, r, &title, self.focused, context.theme) } else { r };
|
||||
let title = format!(
|
||||
"{}: {}{} {}min/{}max",
|
||||
self.title,
|
||||
graph.last().unwrap_or(&(0.0, 0.0)).1 as i64,
|
||||
unit,
|
||||
min_y as i64,
|
||||
max_y as i64
|
||||
);
|
||||
let plot_area = if surface {
|
||||
crate::controls::panel::render_panel(f, r, &title, self.focused, context.theme)
|
||||
} else {
|
||||
r
|
||||
};
|
||||
let block = Block::default()
|
||||
.title(if surface { String::new() } else { format!(
|
||||
"{}:─{}{}─{}min/{}max",
|
||||
self.title,
|
||||
graph.last().unwrap_or(&(0.0, 0.0)).1 as i64,
|
||||
unit,
|
||||
min_y as i64,
|
||||
max_y as i64,
|
||||
) })
|
||||
.title(if surface {
|
||||
String::new()
|
||||
} else {
|
||||
format!(
|
||||
"{}:─{}{}─{}min/{}max",
|
||||
self.title,
|
||||
graph.last().unwrap_or(&(0.0, 0.0)).1 as i64,
|
||||
unit,
|
||||
min_y as i64,
|
||||
max_y as i64,
|
||||
)
|
||||
})
|
||||
.borders(if surface { Borders::NONE } else { self.borders })
|
||||
.border_style(if self.focused {
|
||||
context.theme.graphs.focused_border
|
||||
|
|
@ -186,17 +210,19 @@ impl Element for GraphCard {
|
|||
});
|
||||
f.render_widget(block, r);
|
||||
}
|
||||
if !matches!(context.theme.chrome, crate::theme::ChromeMode::Surfaces) { draw_block_joins(
|
||||
f,
|
||||
r,
|
||||
self.borders,
|
||||
self.joins,
|
||||
if self.focused {
|
||||
context.theme.borders.focused
|
||||
} else {
|
||||
context.theme.borders.normal
|
||||
},
|
||||
); }
|
||||
if !matches!(context.theme.chrome, crate::theme::ChromeMode::Surfaces) {
|
||||
draw_block_joins(
|
||||
f,
|
||||
r,
|
||||
self.borders,
|
||||
self.joins,
|
||||
if self.focused {
|
||||
context.theme.borders.focused
|
||||
} else {
|
||||
context.theme.borders.normal
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -322,9 +322,22 @@ impl Element for LogCard {
|
|||
let entries = self.get_logs();
|
||||
|
||||
let inner_area = if matches!(context.theme.chrome, crate::theme::ChromeMode::Surfaces) {
|
||||
crate::controls::panel::render_panel(f, area, &self.build_title(), self.focused, context.theme)
|
||||
crate::controls::panel::render_panel(
|
||||
f,
|
||||
area,
|
||||
&self.build_title(),
|
||||
self.focused,
|
||||
context.theme,
|
||||
)
|
||||
} else {
|
||||
let block = Block::default().title(self.build_title()).borders(self.borders).border_style(if self.focused { context.theme.logs.focused_border } else { context.theme.logs.border });
|
||||
let block = Block::default()
|
||||
.title(self.build_title())
|
||||
.borders(self.borders)
|
||||
.border_style(if self.focused {
|
||||
context.theme.logs.focused_border
|
||||
} else {
|
||||
context.theme.logs.border
|
||||
});
|
||||
let inner = block.inner(area);
|
||||
f.render_widget(block, area);
|
||||
inner
|
||||
|
|
@ -363,7 +376,11 @@ impl Element for LogCard {
|
|||
let mut spans = Vec::new();
|
||||
|
||||
let (prefix, rest) = Self::split_line_prefix(line);
|
||||
let prefix = if matches!(context.theme.chrome, crate::theme::ChromeMode::Surfaces) { "" } else { prefix };
|
||||
let prefix = if matches!(context.theme.chrome, crate::theme::ChromeMode::Surfaces) {
|
||||
""
|
||||
} else {
|
||||
prefix
|
||||
};
|
||||
|
||||
if !prefix.is_empty() {
|
||||
spans.push(Span::styled(
|
||||
|
|
@ -404,17 +421,19 @@ impl Element for LogCard {
|
|||
f.render_widget(Paragraph::new(line.clone()), line_area);
|
||||
}
|
||||
|
||||
if !matches!(context.theme.chrome, crate::theme::ChromeMode::Surfaces) { draw_block_joins(
|
||||
f,
|
||||
area,
|
||||
self.borders,
|
||||
self.joins,
|
||||
if self.focused {
|
||||
context.theme.borders.focused
|
||||
} else {
|
||||
context.theme.borders.normal
|
||||
},
|
||||
); }
|
||||
if !matches!(context.theme.chrome, crate::theme::ChromeMode::Surfaces) {
|
||||
draw_block_joins(
|
||||
f,
|
||||
area,
|
||||
self.borders,
|
||||
self.joins,
|
||||
if self.focused {
|
||||
context.theme.borders.focused
|
||||
} else {
|
||||
context.theme.borders.normal
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -459,14 +459,22 @@ impl IpcClient {
|
|||
if tasks.is_empty() {
|
||||
"No active tasks.".into()
|
||||
} else {
|
||||
tasks.iter().map(|t| t.name.as_str()).collect::<Vec<_>>().join(", ")
|
||||
tasks
|
||||
.iter()
|
||||
.map(|t| t.name.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
}
|
||||
}
|
||||
ResponsePayload::Users(users) => {
|
||||
if users.is_empty() {
|
||||
"No users.".into()
|
||||
} else {
|
||||
users.iter().map(|u| format!("{} ({})", u.username, u.user_id)).collect::<Vec<_>>().join("\n")
|
||||
users
|
||||
.iter()
|
||||
.map(|u| format!("{} ({})", u.username, u.user_id))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
}
|
||||
ResponsePayload::UserCreated { user_id, username } => {
|
||||
|
|
@ -489,14 +497,18 @@ impl IpcClient {
|
|||
if components.is_empty() {
|
||||
"No component health data available.".into()
|
||||
} else {
|
||||
components.iter().map(|c| {
|
||||
let status_str = match c.status {
|
||||
iota_ipc::HealthStatus::Healthy => "healthy",
|
||||
iota_ipc::HealthStatus::Degraded => "degraded",
|
||||
iota_ipc::HealthStatus::Failed => "failed",
|
||||
};
|
||||
format!("{:?}: {}", c.id, status_str)
|
||||
}).collect::<Vec<_>>().join("\n")
|
||||
components
|
||||
.iter()
|
||||
.map(|c| {
|
||||
let status_str = match c.status {
|
||||
iota_ipc::HealthStatus::Healthy => "healthy",
|
||||
iota_ipc::HealthStatus::Degraded => "degraded",
|
||||
iota_ipc::HealthStatus::Failed => "failed",
|
||||
};
|
||||
format!("{:?}: {}", c.id, status_str)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
}
|
||||
ResponsePayload::UserDetail(user) => {
|
||||
|
|
@ -510,20 +522,31 @@ impl IpcClient {
|
|||
}
|
||||
msg
|
||||
}
|
||||
ResponsePayload::LogEntries(logs) => {
|
||||
logs.entries.iter().map(|e| {
|
||||
ResponsePayload::LogEntries(logs) => logs
|
||||
.entries
|
||||
.iter()
|
||||
.map(|e| {
|
||||
let level = if e.is_error { "ERR" } else { "INF" };
|
||||
format!("[{}] {level} {}: {}", e.timestamp_ms, e.sender, e.message)
|
||||
}).collect::<Vec<_>>().join("\n")
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n"),
|
||||
ResponsePayload::UpdateStatus(status) => {
|
||||
if status.available { "Update available.".into() } else { "Up to date.".into() }
|
||||
if status.available {
|
||||
"Update available.".into()
|
||||
} else {
|
||||
"Up to date.".into()
|
||||
}
|
||||
}
|
||||
ResponsePayload::Communities(communities) => {
|
||||
if communities.is_empty() {
|
||||
"No communities.".into()
|
||||
} else {
|
||||
communities.iter().map(|c| format!("{} ({})", c.title, c.name)).collect::<Vec<_>>().join("\n")
|
||||
communities
|
||||
.iter()
|
||||
.map(|c| format!("{} ({})", c.title, c.name))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,13 @@ impl Screen for DaemonStartingScreen {
|
|||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||
self
|
||||
}
|
||||
fn render(&self, frame: &mut Frame, area: Rect, context: &RenderContext<'_>, _hits: &mut HitMap) {
|
||||
fn render(
|
||||
&self,
|
||||
frame: &mut Frame,
|
||||
area: Rect,
|
||||
context: &RenderContext<'_>,
|
||||
_hits: &mut HitMap,
|
||||
) {
|
||||
let popup = crate::layout::fit::centered_rect(
|
||||
area,
|
||||
crate::layout::fit::RequiredSize {
|
||||
|
|
@ -191,7 +197,13 @@ impl Screen for DaemonSetupScreen {
|
|||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||
self
|
||||
}
|
||||
fn render(&self, frame: &mut Frame, area: Rect, context: &RenderContext<'_>, _hits: &mut HitMap) {
|
||||
fn render(
|
||||
&self,
|
||||
frame: &mut Frame,
|
||||
area: Rect,
|
||||
context: &RenderContext<'_>,
|
||||
_hits: &mut HitMap,
|
||||
) {
|
||||
let popup = crate::layout::fit::centered_rect(
|
||||
area,
|
||||
crate::layout::fit::RequiredSize {
|
||||
|
|
@ -267,7 +279,9 @@ impl Screen for DaemonSetupScreen {
|
|||
);
|
||||
}
|
||||
fn handle_event(&mut self, event: UiEvent) -> InteractionResult {
|
||||
let UiEvent::Key(event) = event else { return InteractionResult::Unhandled; };
|
||||
let UiEvent::Key(event) = event else {
|
||||
return InteractionResult::Unhandled;
|
||||
};
|
||||
match event.code {
|
||||
KeyCode::Esc => {
|
||||
self.complete(DaemonSetupDecision::Exit);
|
||||
|
|
|
|||
|
|
@ -212,9 +212,7 @@ impl MainScreen {
|
|||
let mut seen: Vec<Option<usize>> = Vec::new();
|
||||
for (y, row) in self.nav_grid.iter().enumerate() {
|
||||
for (x, elem_opt) in row.iter().enumerate() {
|
||||
if x == 1
|
||||
&& (!self.graphs_open || self.layout_width.load(Ordering::Relaxed) < 70)
|
||||
{
|
||||
if x == 1 && (!self.graphs_open || self.layout_width.load(Ordering::Relaxed) < 70) {
|
||||
continue;
|
||||
}
|
||||
if elem_opt.is_some() && !seen.contains(elem_opt) {
|
||||
|
|
@ -511,25 +509,64 @@ impl Screen for MainScreen {
|
|||
fn key_hints(&self) -> Vec<KeyHint> {
|
||||
if self.selected_coords == (2, 0) {
|
||||
vec![
|
||||
KeyHint { keys: "Enter", action: "Send" },
|
||||
KeyHint { keys: "Up/Down", action: "History" },
|
||||
KeyHint { keys: "Tab", action: "Complete" },
|
||||
KeyHint { keys: "F6", action: "Header" },
|
||||
KeyHint {
|
||||
keys: "Enter",
|
||||
action: "Send",
|
||||
},
|
||||
KeyHint {
|
||||
keys: "Up/Down",
|
||||
action: "History",
|
||||
},
|
||||
KeyHint {
|
||||
keys: "Tab",
|
||||
action: "Complete",
|
||||
},
|
||||
KeyHint {
|
||||
keys: "F6",
|
||||
action: "Header",
|
||||
},
|
||||
]
|
||||
} else if self.selected_coords == (0, 0) {
|
||||
vec![
|
||||
KeyHint { keys: "J/K", action: "Scroll logs" },
|
||||
KeyHint { keys: "Enter", action: "Lock scroll" },
|
||||
KeyHint { keys: "/", action: "Filter" },
|
||||
KeyHint { keys: "M", action: "Metrics screen" },
|
||||
KeyHint { keys: "Tab", action: "Next panel" },
|
||||
KeyHint { keys: "F6", action: "Header" },
|
||||
KeyHint {
|
||||
keys: "J/K",
|
||||
action: "Scroll logs",
|
||||
},
|
||||
KeyHint {
|
||||
keys: "Enter",
|
||||
action: "Lock scroll",
|
||||
},
|
||||
KeyHint {
|
||||
keys: "/",
|
||||
action: "Filter",
|
||||
},
|
||||
KeyHint {
|
||||
keys: "M",
|
||||
action: "Metrics screen",
|
||||
},
|
||||
KeyHint {
|
||||
keys: "Tab",
|
||||
action: "Next panel",
|
||||
},
|
||||
KeyHint {
|
||||
keys: "F6",
|
||||
action: "Header",
|
||||
},
|
||||
]
|
||||
} else {
|
||||
vec![
|
||||
KeyHint { keys: "Enter", action: "Toggle metrics" },
|
||||
KeyHint { keys: "Tab", action: "Next panel" },
|
||||
KeyHint { keys: "F6", action: "Header" },
|
||||
KeyHint {
|
||||
keys: "Enter",
|
||||
action: "Toggle metrics",
|
||||
},
|
||||
KeyHint {
|
||||
keys: "Tab",
|
||||
action: "Next panel",
|
||||
},
|
||||
KeyHint {
|
||||
keys: "F6",
|
||||
action: "Header",
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,9 @@ impl Screen for FileViewer {
|
|||
}
|
||||
|
||||
fn handle_event(&mut self, event: UiEvent) -> InteractionResult {
|
||||
let UiEvent::Key(event) = event else { return InteractionResult::Unhandled; };
|
||||
let UiEvent::Key(event) = event else {
|
||||
return InteractionResult::Unhandled;
|
||||
};
|
||||
match event.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => {
|
||||
return InteractionResult::CloseScreen;
|
||||
|
|
|
|||
|
|
@ -63,7 +63,13 @@ impl Screen for MetricsScreen {
|
|||
self
|
||||
}
|
||||
|
||||
fn render(&self, frame: &mut Frame, area: Rect, context: &RenderContext<'_>, hits: &mut HitMap) {
|
||||
fn render(
|
||||
&self,
|
||||
frame: &mut Frame,
|
||||
area: Rect,
|
||||
context: &RenderContext<'_>,
|
||||
hits: &mut HitMap,
|
||||
) {
|
||||
let block = Block::default()
|
||||
.title(" Metrics ")
|
||||
.borders(Borders::ALL)
|
||||
|
|
@ -80,8 +86,7 @@ impl Screen for MetricsScreen {
|
|||
frame.render_widget(
|
||||
Paragraph::new(format!(
|
||||
"Range: {} ({} samples) Left/Right to change",
|
||||
RANGES[self.range_index].1,
|
||||
RANGES[self.range_index].0
|
||||
RANGES[self.range_index].1, RANGES[self.range_index].0
|
||||
))
|
||||
.style(context.theme.text.heading),
|
||||
rows[0],
|
||||
|
|
|
|||
|
|
@ -53,17 +53,11 @@ impl OverviewScreen {
|
|||
|
||||
let mut lines = Vec::new();
|
||||
|
||||
lines.push(Line::from(Span::styled(
|
||||
"Connection",
|
||||
theme.text.heading,
|
||||
)));
|
||||
lines.push(Line::from(Span::styled("Connection", theme.text.heading)));
|
||||
lines.push(Line::from(format!(" State: {}", connection_label(&conn))));
|
||||
lines.push(Line::from(""));
|
||||
|
||||
lines.push(Line::from(Span::styled(
|
||||
"Daemon",
|
||||
theme.text.heading,
|
||||
)));
|
||||
lines.push(Line::from(Span::styled("Daemon", theme.text.heading)));
|
||||
lines.push(Line::from(format!(
|
||||
" Version: {}",
|
||||
version_or_unknown(&daemon.version)
|
||||
|
|
@ -113,10 +107,7 @@ impl OverviewScreen {
|
|||
|
||||
if !daemon.components.is_empty() {
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::from(Span::styled(
|
||||
"Components",
|
||||
theme.text.heading,
|
||||
)));
|
||||
lines.push(Line::from(Span::styled("Components", theme.text.heading)));
|
||||
for (id, health) in &daemon.components {
|
||||
let status_str = match health.status {
|
||||
iota_ipc::HealthStatus::Healthy => "[OK] healthy",
|
||||
|
|
@ -187,7 +178,13 @@ impl Screen for OverviewScreen {
|
|||
.borders(Borders::ALL)
|
||||
.border_style(context.theme.borders.normal)
|
||||
.title_style(context.theme.borders.title);
|
||||
let inner = if matches!(context.theme.chrome, crate::theme::ChromeMode::Surfaces) { crate::controls::panel::render_panel(f, rect, "Overview", false, context.theme) } else { let inner = block.inner(rect); f.render_widget(block, rect); inner };
|
||||
let inner = if matches!(context.theme.chrome, crate::theme::ChromeMode::Surfaces) {
|
||||
crate::controls::panel::render_panel(f, rect, "Overview", false, context.theme)
|
||||
} else {
|
||||
let inner = block.inner(rect);
|
||||
f.render_widget(block, rect);
|
||||
inner
|
||||
};
|
||||
let rows = ratatui::layout::Layout::vertical([
|
||||
ratatui::layout::Constraint::Min(1),
|
||||
ratatui::layout::Constraint::Length(1),
|
||||
|
|
@ -273,10 +270,22 @@ impl Screen for OverviewScreen {
|
|||
}
|
||||
fn key_hints(&self) -> Vec<KeyHint> {
|
||||
vec![
|
||||
KeyHint { keys: "Up/Down", action: "Scroll" },
|
||||
KeyHint { keys: "PgUp/PgDn", action: "Page" },
|
||||
KeyHint { keys: "Esc/B", action: "Back" },
|
||||
KeyHint { keys: "F6", action: "Header" },
|
||||
KeyHint {
|
||||
keys: "Up/Down",
|
||||
action: "Scroll",
|
||||
},
|
||||
KeyHint {
|
||||
keys: "PgUp/PgDn",
|
||||
action: "Page",
|
||||
},
|
||||
KeyHint {
|
||||
keys: "Esc/B",
|
||||
action: "Back",
|
||||
},
|
||||
KeyHint {
|
||||
keys: "F6",
|
||||
action: "Header",
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,8 +49,12 @@ impl SettingsScreen {
|
|||
selected,
|
||||
saved: current,
|
||||
message: "Left/Right previews. Enter saves.".into(),
|
||||
color: UiConfig::load().map(|config| config.color).unwrap_or_default(),
|
||||
unicode: UiConfig::load().map(|config| config.unicode).unwrap_or_default(),
|
||||
color: UiConfig::load()
|
||||
.map(|config| config.color)
|
||||
.unwrap_or_default(),
|
||||
unicode: UiConfig::load()
|
||||
.map(|config| config.unicode)
|
||||
.unwrap_or_default(),
|
||||
focus: Focus::Theme,
|
||||
dialog: None,
|
||||
pending: false,
|
||||
|
|
@ -64,14 +68,16 @@ impl SettingsScreen {
|
|||
fn apply(&self, persist: bool) -> InteractionResult {
|
||||
let theme = self.selected_theme();
|
||||
InteractionResult::AppTask {
|
||||
task: Box::pin(async move {
|
||||
UiEvent::App(AppEvent::ApplyTheme { theme, persist })
|
||||
}),
|
||||
task: Box::pin(async move { UiEvent::App(AppEvent::ApplyTheme { theme, persist }) }),
|
||||
}
|
||||
}
|
||||
|
||||
fn next_policy(policy: TerminalPolicy) -> TerminalPolicy {
|
||||
match policy { TerminalPolicy::Auto => TerminalPolicy::Always, TerminalPolicy::Always => TerminalPolicy::Never, TerminalPolicy::Never => TerminalPolicy::Auto }
|
||||
match policy {
|
||||
TerminalPolicy::Auto => TerminalPolicy::Always,
|
||||
TerminalPolicy::Always => TerminalPolicy::Never,
|
||||
TerminalPolicy::Never => TerminalPolicy::Auto,
|
||||
}
|
||||
}
|
||||
|
||||
fn next_focus(&mut self) {
|
||||
|
|
@ -100,9 +106,7 @@ impl SettingsScreen {
|
|||
self.pending = true;
|
||||
self.message = "Regenerating keys…".into();
|
||||
return InteractionResult::AppTask {
|
||||
task: Box::pin(async {
|
||||
UiEvent::App(AppEvent::RegenerateKeysRequested)
|
||||
}),
|
||||
task: Box::pin(async { UiEvent::App(AppEvent::RegenerateKeysRequested) }),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -113,7 +117,15 @@ impl SettingsScreen {
|
|||
let theme = self.selected_theme();
|
||||
let color = self.color;
|
||||
let unicode = self.unicode;
|
||||
InteractionResult::AppTask { task: Box::pin(async move { UiEvent::App(AppEvent::SaveSettings { theme, color, unicode }) }) }
|
||||
InteractionResult::AppTask {
|
||||
task: Box::pin(async move {
|
||||
UiEvent::App(AppEvent::SaveSettings {
|
||||
theme,
|
||||
color,
|
||||
unicode,
|
||||
})
|
||||
}),
|
||||
}
|
||||
}
|
||||
Focus::RegenerateKeys => {
|
||||
self.dialog = Some(Dialog::ConfirmRegenerateKeys);
|
||||
|
|
@ -146,8 +158,7 @@ impl Screen for SettingsScreen {
|
|||
.border_style(context.theme.borders.focused);
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
let rows =
|
||||
Layout::vertical([Constraint::Length(2), Constraint::Min(1)]).split(inner);
|
||||
let rows = Layout::vertical([Constraint::Length(2), Constraint::Min(1)]).split(inner);
|
||||
frame.render_widget(
|
||||
Paragraph::new(format!(
|
||||
"Theme: < {} >{}\nColor: {:?} (C) Unicode: {:?} (U)",
|
||||
|
|
@ -156,7 +167,9 @@ impl Screen for SettingsScreen {
|
|||
" [saved]"
|
||||
} else {
|
||||
" [preview]"
|
||||
}, self.color, self.unicode
|
||||
},
|
||||
self.color,
|
||||
self.unicode
|
||||
))
|
||||
.style(context.theme.text.heading),
|
||||
rows[0],
|
||||
|
|
@ -331,8 +344,14 @@ impl Screen for SettingsScreen {
|
|||
self.prev_focus();
|
||||
InteractionResult::Handled
|
||||
}
|
||||
KeyCode::Char('c') | KeyCode::Char('C') => { self.color = Self::next_policy(self.color); InteractionResult::Handled }
|
||||
KeyCode::Char('u') | KeyCode::Char('U') => { self.unicode = Self::next_policy(self.unicode); InteractionResult::Handled }
|
||||
KeyCode::Char('c') | KeyCode::Char('C') => {
|
||||
self.color = Self::next_policy(self.color);
|
||||
InteractionResult::Handled
|
||||
}
|
||||
KeyCode::Char('u') | KeyCode::Char('U') => {
|
||||
self.unicode = Self::next_policy(self.unicode);
|
||||
InteractionResult::Handled
|
||||
}
|
||||
KeyCode::Esc | KeyCode::Char('b') | KeyCode::Char('B') => {
|
||||
InteractionResult::CloseScreen
|
||||
}
|
||||
|
|
@ -378,8 +397,14 @@ impl Screen for SettingsScreen {
|
|||
keys: "Enter",
|
||||
action: "Save/Activate",
|
||||
},
|
||||
KeyHint { keys: "Tab", action: "Move focus" },
|
||||
KeyHint { keys: "C/U", action: "Color/Unicode" },
|
||||
KeyHint {
|
||||
keys: "Tab",
|
||||
action: "Move focus",
|
||||
},
|
||||
KeyHint {
|
||||
keys: "C/U",
|
||||
action: "Color/Unicode",
|
||||
},
|
||||
KeyHint {
|
||||
keys: "Esc/B",
|
||||
action: "Back",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@ use crate::{
|
|||
controls::choice::{ChoiceKind, ChoiceVisualState, render_choice_line},
|
||||
interaction_result::InteractionResult,
|
||||
render_context::RenderContext,
|
||||
screens::{md_viewer::FileViewer, screens::{HitMap, Screen, UiEvent}},
|
||||
screens::{
|
||||
md_viewer::FileViewer,
|
||||
screens::{HitMap, Screen, UiEvent},
|
||||
},
|
||||
util::{buttons::draw_buttons, terms_focus::Focus},
|
||||
};
|
||||
use crossterm::event::KeyCode;
|
||||
|
|
@ -271,7 +274,9 @@ impl Screen for TermsCheckerScreen {
|
|||
}
|
||||
|
||||
fn handle_event(&mut self, event: UiEvent) -> InteractionResult {
|
||||
let UiEvent::Key(event) = event else { return InteractionResult::Unhandled; };
|
||||
let UiEvent::Key(event) = event else {
|
||||
return InteractionResult::Unhandled;
|
||||
};
|
||||
let mut possible_states = vec![Focus::Eula, Focus::Tos, Focus::Pp, Focus::Cancel];
|
||||
|
||||
if self.eula {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@ use crate::{
|
|||
controls::choice::{ChoiceKind, ChoiceVisualState, render_choice_line},
|
||||
interaction_result::InteractionResult,
|
||||
render_context::RenderContext,
|
||||
screens::{md_viewer::FileViewer, screens::{HitMap, Screen, UiEvent}},
|
||||
screens::{
|
||||
md_viewer::FileViewer,
|
||||
screens::{HitMap, Screen, UiEvent},
|
||||
},
|
||||
util::{buttons::draw_buttons, terms_focus::Focus},
|
||||
};
|
||||
use chrono::{Local, TimeZone, Utc};
|
||||
|
|
@ -594,7 +597,9 @@ impl Screen for TermsUpdaterScreen {
|
|||
}
|
||||
|
||||
fn handle_event(&mut self, event: UiEvent) -> InteractionResult {
|
||||
let UiEvent::Key(event) = event else { return InteractionResult::Unhandled; };
|
||||
let UiEvent::Key(event) = event else {
|
||||
return InteractionResult::Unhandled;
|
||||
};
|
||||
let mut possible_states = Vec::new();
|
||||
|
||||
if self.eula_needed {
|
||||
|
|
|
|||
|
|
@ -89,7 +89,12 @@ impl UsersScreen {
|
|||
let title = if self.filter.is_empty() {
|
||||
format!("Users ({})", self.users.len())
|
||||
} else {
|
||||
format!("Users ({}/{}) filter: {}", visible_indices.len(), self.users.len(), self.filter)
|
||||
format!(
|
||||
"Users ({}/{}) filter: {}",
|
||||
visible_indices.len(),
|
||||
self.users.len(),
|
||||
self.filter
|
||||
)
|
||||
};
|
||||
let inner = if matches!(context.theme.chrome, crate::theme::ChromeMode::Surfaces) {
|
||||
crate::controls::panel::render_panel(
|
||||
|
|
@ -124,14 +129,18 @@ impl UsersScreen {
|
|||
}
|
||||
|
||||
let mut lines = Vec::new();
|
||||
self.viewport_height.store(inner.height as usize, Ordering::Relaxed);
|
||||
self.viewport_height
|
||||
.store(inner.height as usize, Ordering::Relaxed);
|
||||
let labels: Vec<(usize, String)> = visible_indices
|
||||
.iter()
|
||||
.skip(self.scroll_offset)
|
||||
.take(inner.height as usize)
|
||||
.map(|user_index| {
|
||||
let user = &self.users[*user_index];
|
||||
(*user_index, format!("{:>6} {}", user.user_id, user.username))
|
||||
(
|
||||
*user_index,
|
||||
format!("{:>6} {}", user.user_id, user.username),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
for (user_index, label) in &labels {
|
||||
|
|
@ -291,7 +300,10 @@ impl UsersScreen {
|
|||
|
||||
fn keep_focused_user_visible(&mut self) {
|
||||
let indices = self.filtered_indices();
|
||||
let Some(position) = indices.iter().position(|index| *index == self.focused_index) else {
|
||||
let Some(position) = indices
|
||||
.iter()
|
||||
.position(|index| *index == self.focused_index)
|
||||
else {
|
||||
self.scroll_offset = 0;
|
||||
return;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -25,11 +25,20 @@ pub struct BorderStyles {
|
|||
}
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SurfaceStyles {
|
||||
pub canvas: Style, pub toolbar: Style, pub panel: Style, pub panel_alternate: Style,
|
||||
pub panel_focused: Style, pub panel_selected: Style, pub footer: Style, pub overlay: Style,
|
||||
pub canvas: Style,
|
||||
pub toolbar: Style,
|
||||
pub panel: Style,
|
||||
pub panel_alternate: Style,
|
||||
pub panel_focused: Style,
|
||||
pub panel_selected: Style,
|
||||
pub footer: Style,
|
||||
pub overlay: Style,
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ChromeMode { Bordered, Surfaces }
|
||||
pub enum ChromeMode {
|
||||
Bordered,
|
||||
Surfaces,
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ChoiceItemStyle {
|
||||
pub marker: Style,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use super::{
|
||||
BorderStyles, ButtonStyles, ChoiceItemStyle, ChoiceStyles, ConsoleStyles, CursorPresentation,
|
||||
ChromeMode, GraphStyles, LogStyles, MarkdownStyles, MarkerSet, ResolvedTheme, StatusStyles, SurfaceStyles, TextStyles,
|
||||
ThemeName,
|
||||
BorderStyles, ButtonStyles, ChoiceItemStyle, ChoiceStyles, ChromeMode, ConsoleStyles,
|
||||
CursorPresentation, GraphStyles, LogStyles, MarkdownStyles, MarkerSet, ResolvedTheme,
|
||||
StatusStyles, SurfaceStyles, TextStyles, ThemeName,
|
||||
};
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
|
||||
|
|
@ -46,7 +46,16 @@ fn base(
|
|||
ResolvedTheme {
|
||||
name,
|
||||
unicode: true,
|
||||
surfaces: SurfaceStyles { canvas: Style::default(), toolbar: Style::default(), panel: Style::default(), panel_alternate: Style::default(), panel_focused: focused, panel_selected: selected, footer: Style::default(), overlay: Style::default() },
|
||||
surfaces: SurfaceStyles {
|
||||
canvas: Style::default(),
|
||||
toolbar: Style::default(),
|
||||
panel: Style::default(),
|
||||
panel_alternate: Style::default(),
|
||||
panel_focused: focused,
|
||||
panel_selected: selected,
|
||||
footer: Style::default(),
|
||||
overlay: Style::default(),
|
||||
},
|
||||
chrome: ChromeMode::Bordered,
|
||||
text: TextStyles {
|
||||
normal,
|
||||
|
|
@ -300,11 +309,14 @@ pub fn resolve(name: ThemeName) -> ResolvedTheme {
|
|||
CursorPresentation::StyledCell(plain.fg(Color::Black).bg(Color::Yellow));
|
||||
theme.chrome = ChromeMode::Surfaces;
|
||||
theme.surfaces = SurfaceStyles {
|
||||
canvas: plain.bg(Color::Black), toolbar: plain.fg(Color::White).bg(Color::DarkGray),
|
||||
canvas: plain.bg(Color::Black),
|
||||
toolbar: plain.fg(Color::White).bg(Color::DarkGray),
|
||||
panel: plain.fg(Color::White).bg(Color::Rgb(30, 35, 45)),
|
||||
panel_alternate: plain.fg(Color::White).bg(Color::Rgb(45, 52, 66)),
|
||||
panel_focused: plain.fg(Color::White).bg(Color::Rgb(48, 58, 78)),
|
||||
panel_selected: selected, footer: plain.fg(Color::DarkGray).bg(Color::Black), overlay: plain.fg(Color::White).bg(Color::Rgb(45, 52, 66)),
|
||||
panel_selected: selected,
|
||||
footer: plain.fg(Color::DarkGray).bg(Color::Black),
|
||||
overlay: plain.fg(Color::White).bg(Color::Rgb(45, 52, 66)),
|
||||
};
|
||||
theme
|
||||
}
|
||||
|
|
|
|||
|
|
@ -328,7 +328,12 @@ impl UI {
|
|||
}
|
||||
return;
|
||||
}
|
||||
if let UiEvent::App(AppEvent::SaveSettings { theme, color, unicode }) = &event {
|
||||
if let UiEvent::App(AppEvent::SaveSettings {
|
||||
theme,
|
||||
color,
|
||||
unicode,
|
||||
}) = &event
|
||||
{
|
||||
self.set_theme(theme::resolve(*theme)).await;
|
||||
let mut config = theme::UiConfig::load().unwrap_or_default();
|
||||
config.theme = *theme;
|
||||
|
|
@ -337,7 +342,9 @@ impl UI {
|
|||
let result = config
|
||||
.save()
|
||||
.map_err(|error| format!("Could not save UI settings: {error}"));
|
||||
let _ = self.app_event_tx.send(UiEvent::App(AppEvent::ThemeSaved(result)));
|
||||
let _ = self
|
||||
.app_event_tx
|
||||
.send(UiEvent::App(AppEvent::ThemeSaved(result)));
|
||||
return;
|
||||
}
|
||||
if matches!(&event, UiEvent::App(AppEvent::RegenerateKeysRequested)) {
|
||||
|
|
@ -386,12 +393,14 @@ impl UI {
|
|||
KeyCode::Left | KeyCode::BackTab => *focus = Some((index + 3) % 4),
|
||||
KeyCode::Right | KeyCode::Tab => *focus = Some((index + 1) % 4),
|
||||
KeyCode::Enter | KeyCode::Char(' ') => {
|
||||
action = Some([
|
||||
AppAction::OpenOverview,
|
||||
AppAction::OpenUsers,
|
||||
AppAction::OpenSettings,
|
||||
AppAction::Quit,
|
||||
][index]);
|
||||
action = Some(
|
||||
[
|
||||
AppAction::OpenOverview,
|
||||
AppAction::OpenUsers,
|
||||
AppAction::OpenSettings,
|
||||
AppAction::Quit,
|
||||
][index],
|
||||
);
|
||||
*focus = None;
|
||||
}
|
||||
KeyCode::Esc => *focus = None,
|
||||
|
|
@ -516,7 +525,8 @@ impl UI {
|
|||
AppAction::OpenUsers => self.open_users().await,
|
||||
AppAction::OpenSettings => {
|
||||
let current = self.theme_name().await;
|
||||
self.set_screen(Box::new(SettingsScreen::new(current))).await;
|
||||
self.set_screen(Box::new(SettingsScreen::new(current)))
|
||||
.await;
|
||||
}
|
||||
AppAction::OpenMetrics => {
|
||||
if let Some(screen) = MetricsScreen::new(self.clone()).await {
|
||||
|
|
@ -552,7 +562,9 @@ impl UI {
|
|||
})
|
||||
.collect())
|
||||
}
|
||||
Ok(iota_ipc::ResponseResult::Error(error)) => Err(format!("Cannot load users: {error}")),
|
||||
Ok(iota_ipc::ResponseResult::Error(error)) => {
|
||||
Err(format!("Cannot load users: {error}"))
|
||||
}
|
||||
Ok(_) => Err("Daemon returned an unexpected response while loading users.".into()),
|
||||
Err(error) => Err(format!("Cannot load users: {error}")),
|
||||
};
|
||||
|
|
@ -611,8 +623,13 @@ impl UI {
|
|||
.join(" ")
|
||||
};
|
||||
f.render_widget(
|
||||
ratatui::widgets::Paragraph::new(format!(" {hints}"))
|
||||
.style(context.theme.surfaces.footer.patch(context.theme.text.muted)),
|
||||
ratatui::widgets::Paragraph::new(format!(" {hints}")).style(
|
||||
context
|
||||
.theme
|
||||
.surfaces
|
||||
.footer
|
||||
.patch(context.theme.text.muted),
|
||||
),
|
||||
rows[2],
|
||||
);
|
||||
screen.render(f, rows[1], &context, &mut hits);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use iota_cli::{
|
||||
interaction_result::InteractionResult,
|
||||
render_context::RenderContext,
|
||||
|
|
@ -8,7 +9,6 @@ use iota_cli::{
|
|||
theme::{ThemeName, resolve},
|
||||
};
|
||||
use ratatui::{Terminal, backend::TestBackend};
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
|
||||
fn buffer_text(terminal: &Terminal<TestBackend>) -> String {
|
||||
terminal
|
||||
|
|
|
|||
Loading…
Reference in a new issue