mod chain;
mod config;
mod footer;
mod format;
mod header;
mod modals;
mod overview;
mod panes;
mod tabs;
mod toasts;
mod usage;
use ratatui::Frame;
use ratatui::layout::{Constraint, Direction, Layout};
use ratatui::widgets::Block;
use super::app::{App, Tab};
use super::theme;
pub(super) const HEADER_HEIGHT: u16 = 3;
pub(crate) fn draw(frame: &mut Frame<'_>, app: &App) {
let area = frame.area();
let background = Block::default().style(theme::base());
frame.render_widget(background, area);
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(HEADER_HEIGHT),
Constraint::Min(5),
Constraint::Length(1),
])
.split(area);
header::draw(frame, chunks[0], app);
match app.tab {
Tab::Overview => overview::draw(frame, chunks[1], app),
Tab::Usage => usage::draw(frame, chunks[1], app),
Tab::Config => config::draw(frame, chunks[1], app),
Tab::Fallback => chain::draw(frame, chunks[1], app),
}
footer::draw(frame, chunks[2], app);
toasts::draw(frame, area, app);
if let Some(modal) = app.modals.last() {
modals::draw(frame, area, app, modal);
}
}
#[cfg(test)]
mod render_smoke {
use super::*;
use crate::profile::{AppConfig, AppState, Profile, ProfileName};
use crate::tui::app::{App, ConfigFocus};
use crate::usage::{UsageInfo, UsageWindow};
use ratatui::Terminal;
use ratatui::backend::TestBackend;
use std::collections::BTreeMap;
fn oauth(name: &str, five: f64, seven: f64, auto: bool) -> Profile {
Profile {
name: name.into(),
base_url: None,
api_key: None,
auto_start: auto,
env: BTreeMap::new(),
fallback_threshold: Some(80.0),
credentials: None,
usage: Some(UsageInfo {
plan: None,
five_hour: Some(UsageWindow {
utilization: five,
resets_at: None,
}),
seven_day: Some(UsageWindow {
utilization: seven,
resets_at: None,
}),
..UsageInfo::default()
}),
fetch_status: None,
}
}
fn dump(app: &App, w: u16, h: u16) -> String {
let mut term = Terminal::new(TestBackend::new(w, h)).unwrap();
term.draw(|f| super::draw(f, app)).unwrap();
let buf = term.backend().buffer().clone();
let mut out = String::new();
for y in 0..h {
for x in 0..w {
out.push_str(buf.content[(y as usize) * (w as usize) + (x as usize)].symbol());
}
out.push('\n');
}
out
}
#[test]
fn all_tabs_render() {
let profiles = vec![
oauth("uwuclxdy", 42.0, 18.0, true),
oauth("work", 12.0, 3.0, false),
oauth("spare", 0.0, 0.0, false),
];
let names: Vec<ProfileName> = profiles.iter().map(|p| p.name.clone()).collect();
let config = AppConfig {
state: AppState {
active_profile: Some("uwuclxdy".into()),
profiles: names.clone(),
fallback_chain: vec!["uwuclxdy".into(), "work".into()],
..AppState::default()
},
profiles,
};
let mut app = App::new(config);
for (tab, focus) in [
(Tab::Overview, ConfigFocus::Profiles),
(Tab::Usage, ConfigFocus::Profiles),
(Tab::Config, ConfigFocus::Profiles),
(Tab::Config, ConfigFocus::Actions),
(Tab::Fallback, ConfigFocus::Profiles),
] {
app.tab = tab;
app.config_focus = focus;
assert!(dump(&app, 90, 20).contains("clauth"));
}
}
#[test]
fn empty_state_renders() {
let config = AppConfig {
state: AppState::default(),
profiles: Vec::new(),
};
let mut app = App::new(config);
for tab in Tab::ALL {
app.tab = tab;
assert!(dump(&app, 90, 20).contains("clauth"));
}
}
}