use std::borrow::Cow;
use super::super::theme::{accent, secondary, warning};
use super::splash::{
NO_DATABASE_FOOTER, NO_DATABASE_HEADLINE, NO_DATABASE_STEPS, NO_WORKSPACE_LINES, splash_art,
};
use crate::interactive::tui::types::App;
use ratatui::{
Frame,
layout::{Alignment, Rect},
style::{Modifier, Style},
text::{Line, Span, Text},
widgets::Paragraph,
};
const CHAPTER_CONCEPT: &str = "Each request becomes a chapter — activity, answer, and results.";
const KEYBOARD_HINT: &str = "/ commands @ tables ? help Ctrl+C quit";
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum Tier {
Always,
State,
Concept,
Examples,
Decoration,
}
type Section<'a> = (Tier, Vec<Line<'a>>);
fn line<'a>(text: impl Into<Cow<'a, str>>, style: Style) -> Line<'a> {
Line::from(Span::styled(text, style))
}
fn plain<'a>(text: impl Into<Cow<'a, str>>) -> Line<'a> {
line(text, Style::default().fg(secondary()))
}
fn missing<'a>(text: impl Into<Cow<'a, str>>) -> Line<'a> {
line(
text,
Style::default().fg(warning()).add_modifier(Modifier::BOLD),
)
}
fn identity() -> Section<'static> {
(
Tier::Decoration,
vec![
line(
"◆ saya",
Style::default().fg(accent()).add_modifier(Modifier::BOLD),
),
plain("Ask your databases in plain language."),
],
)
}
fn state<'a>(app: &'a App) -> Section<'a> {
let mut lines = vec![Line::from("")];
if app.profiles.is_empty() {
lines.push(missing(NO_DATABASE_HEADLINE));
for step in NO_DATABASE_STEPS {
lines.push(plain(step));
}
lines.push(Line::from(""));
lines.push(plain(NO_DATABASE_FOOTER));
} else {
let mut spans = vec![Span::styled(
"databases ",
Style::default().fg(secondary()),
)];
for (i, profile) in app.profiles.iter().enumerate() {
if i > 0 {
spans.push(Span::styled(" · ", Style::default().fg(secondary())));
}
spans.push(Span::styled(
profile.as_str(),
Style::default().fg(accent()),
));
}
lines.push(Line::from(spans));
}
(Tier::State, lines)
}
fn workspace(workspace_bound: Option<bool>) -> Section<'static> {
if workspace_bound != Some(false) {
return (Tier::State, Vec::new());
}
let mut lines = vec![Line::from("")];
for line_text in NO_WORKSPACE_LINES {
lines.push(missing(line_text));
}
(Tier::State, lines)
}
fn concept() -> Section<'static> {
(Tier::Concept, vec![Line::from(""), plain(CHAPTER_CONCEPT)])
}
fn examples() -> Section<'static> {
let prompt = |text: &'static str| {
line(
text,
Style::default()
.fg(secondary())
.add_modifier(Modifier::ITALIC),
)
};
(
Tier::Examples,
vec![
Line::from(""),
plain("try asking"),
prompt(" which tables track billing?"),
prompt(" top 5 customers by revenue"),
prompt(" compare row counts across the connected databases"),
],
)
}
fn hint() -> Section<'static> {
(Tier::Always, vec![Line::from(""), plain(KEYBOARD_HINT)])
}
fn visible<'a>(mut kept: Vec<Section<'a>>, height: usize) -> Vec<Line<'a>> {
kept.retain(|(_, lines)| !lines.is_empty());
while kept.iter().map(|(_, lines)| lines.len()).sum::<usize>() > height {
let Some(least_kept) = kept.iter().map(|(tier, _)| *tier).max() else {
break;
};
if least_kept == Tier::Always {
break; }
let drop_at = kept
.iter()
.rposition(|(tier, _)| *tier == least_kept)
.expect("the tier is present");
kept.remove(drop_at);
}
kept.into_iter().flat_map(|(_, lines)| lines).collect()
}
pub(in crate::interactive::tui) fn draw_empty_state(
frame: &mut Frame<'_>,
app: &App,
area: Rect,
workspace_bound: Option<bool>,
) {
let height = area.height as usize;
let sections = vec![
identity(),
state(app),
workspace(workspace_bound),
concept(),
examples(),
hint(),
];
let full_len = sections.iter().map(|(_, lines)| lines.len()).sum();
let mut content: Vec<Line<'_>> = visible(sections, height);
if let Some(with_art) = splash_art(height, full_len) {
let mut art_and_content: Vec<Line<'_>> = with_art;
art_and_content.push(Line::from(""));
art_and_content.extend(content);
content = art_and_content;
}
let content_len = content.len();
let pad = height.saturating_sub(content_len) / 2;
let mut lines = Vec::with_capacity(pad + content_len);
for _ in 0..pad {
lines.push(Line::from(""));
}
lines.extend(content);
frame.render_widget(
Paragraph::new(Text::from(lines)).alignment(Alignment::Center),
area,
);
}
#[cfg(test)]
#[path = "empty_state_tests.rs"]
mod tests;