use super::cards;
use super::data::{DashboardData, Period};
use super::layout;
use crate::tui::app::dialog_keys::{DialogKey, DialogScope, dialog_keys};
use crate::tui::render::theme::{self, Role};
use crossterm::event::{KeyCode, KeyEvent};
use ratatui::{
Frame,
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph},
};
pub(crate) use super::layout::CARD_COUNT;
pub(crate) const CACHE_CARD: usize = 5;
#[derive(Debug, Clone)]
pub struct DashboardState {
pub period: Period,
pub focused_card: usize,
pub data: DashboardData,
pub cache_detail_open: bool,
}
impl Default for DashboardState {
fn default() -> Self {
Self {
period: Period::AllTime,
focused_card: 0,
data: DashboardData::default(),
cache_detail_open: false,
}
}
}
impl DashboardState {
pub fn focus_next(&mut self) {
self.focused_card = (self.focused_card + 1) % CARD_COUNT;
}
pub fn focus_prev(&mut self) {
self.focused_card = if self.focused_card == 0 {
CARD_COUNT - 1
} else {
self.focused_card - 1
};
}
pub fn set_period(&mut self, period: Period) -> bool {
if self.period != period {
self.period = period;
true
} else {
false
}
}
pub fn cache_card_focused(&self) -> bool {
self.focused_card == CACHE_CARD
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UsageKeyOutcome {
Consumed,
Period(Period),
Close,
NotConsumed,
}
pub fn decide(state: &mut DashboardState, event: KeyEvent) -> UsageKeyOutcome {
if state.cache_detail_open {
if event.code == KeyCode::Esc {
state.cache_detail_open = false;
}
return UsageKeyOutcome::Consumed;
}
match event.code {
KeyCode::Esc => UsageKeyOutcome::Close,
KeyCode::Tab => {
state.focus_next();
UsageKeyOutcome::Consumed
}
KeyCode::BackTab => {
state.focus_prev();
UsageKeyOutcome::Consumed
}
KeyCode::Enter if state.cache_card_focused() => {
state.cache_detail_open = true;
UsageKeyOutcome::Consumed
}
KeyCode::Char('t') | KeyCode::Char('T') => UsageKeyOutcome::Period(Period::Today),
KeyCode::Char('w') | KeyCode::Char('W') => UsageKeyOutcome::Period(Period::Week),
KeyCode::Char('m') | KeyCode::Char('M') => UsageKeyOutcome::Period(Period::Month),
KeyCode::Char('a') | KeyCode::Char('A') => UsageKeyOutcome::Period(Period::AllTime),
_ => UsageKeyOutcome::NotConsumed,
}
}
pub fn advertises(state: &DashboardState, code: KeyCode) -> bool {
if state.cache_detail_open {
return code == KeyCode::Esc;
}
match code {
KeyCode::Esc | KeyCode::Tab | KeyCode::BackTab => true,
KeyCode::Char('t' | 'T' | 'w' | 'W' | 'm' | 'M' | 'a' | 'A') => true,
KeyCode::Enter => state.cache_card_focused(),
_ => false,
}
}
pub fn advertised_keys(state: &DashboardState) -> Vec<DialogKey> {
dialog_keys(DialogScope::UsageDashboard)
.iter()
.filter(|key| key.events.iter().any(|(code, _)| advertises(state, *code)))
.copied()
.collect()
}
pub fn render(f: &mut Frame, state: &DashboardState, area: Rect) {
let border = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(theme::role(Role::AccentTeal)))
.title(Span::styled(
" Usage Dashboard ",
Style::default()
.fg(theme::role(Role::AccentTeal))
.add_modifier(Modifier::BOLD),
));
let inner = border.inner(area);
f.render_widget(border, area);
let layout = layout::compute(inner);
if layout.too_small {
render_too_small(f, inner);
return;
}
cards::render_summary(f, &state.data, layout.summary, state.period.label());
cards::render_daily(
f,
&state.data.daily,
layout.cards[0],
state.focused_card == 0,
);
cards::render_projects(
f,
&state.data.projects,
layout.cards[1],
state.focused_card == 1,
);
cards::render_models(
f,
&state.data.models,
layout.cards[2],
state.focused_card == 2,
);
cards::render_tools(
f,
&state.data.tools,
layout.cards[3],
state.focused_card == 3,
);
cards::render_activities(
f,
&state.data.activities,
layout.cards[4],
state.focused_card == 4,
);
cards::render_cache_efficiency(
f,
&state.data.cache,
layout.cards[5],
state.cache_card_focused(),
);
cards::render_footer(f, layout.footer, state);
if state.cache_detail_open {
let top = layout.cards[0].y;
let overlay = Rect {
x: layout.summary.x,
y: top,
width: layout.summary.width,
height: layout.footer.y.saturating_sub(top),
};
cards::render_cache_breakdown(f, &state.data.cache, overlay);
}
}
fn render_too_small(f: &mut Frame, area: Rect) {
let text = vec![
Line::from(Span::styled(
"Terminal too small for the usage dashboard",
Style::default()
.fg(theme::role(Role::AccentTeal))
.add_modifier(Modifier::BOLD),
)),
Line::from(""),
Line::from(Span::styled(
format!(
"Needs at least {} columns and {} rows.",
layout::MIN_WIDTH,
layout::required_height(1)
),
Style::default().fg(theme::role(Role::TextDim)),
)),
];
let rows = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Min(0),
Constraint::Length(text.len() as u16),
Constraint::Min(0),
])
.split(area);
f.render_widget(Paragraph::new(text).alignment(Alignment::Center), rows[1]);
}