use ratatui::{
layout::Rect,
style::Style,
text::{Line, Span},
widgets::{Clear, Paragraph},
Frame,
};
use super::{
exclusive_screen::ExclusiveOccupant,
first_run::SetupEntry,
render::{display_width, truncate_one_line},
theme::Theme,
App, ComposerMode,
};
const CONTENT_WIDTH: u16 = 88;
const TOP_PADDING: u16 = 2;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum SetupStep {
SignIn,
ChooseModel,
}
impl SetupStep {
fn index(self) -> usize {
match self {
Self::SignIn => 0,
Self::ChooseModel => 1,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum StepState {
Done,
Current,
Pending,
}
impl StepState {
fn marker(self) -> &'static str {
match self {
Self::Done => "✓",
Self::Current => "â–¸",
Self::Pending => " ",
}
}
fn style(self) -> Style {
match self {
Self::Done => Theme::success(),
Self::Current => Theme::accent(),
Self::Pending => Theme::dim(),
}
}
}
const STEP_LABELS: [&str; 2] = ["Sign in to a provider", "Choose a model"];
impl App {
pub(super) fn setup_step(&self) -> Option<SetupStep> {
self.exclusive.setup_step()
}
fn enter_setup(&mut self, step: SetupStep) {
self.exclusive = ExclusiveOccupant::Setup(step);
}
fn leave_setup(&mut self) {
if matches!(self.exclusive, ExclusiveOccupant::Setup(_)) {
self.exclusive = ExclusiveOccupant::Session;
}
}
pub(super) fn start_setup_screen(&mut self, terminal: &mut super::DefaultTerminal) {
let Some(entry) = self.info.services.first_run else {
return;
};
let step = match entry {
SetupEntry::SignIn => SetupStep::SignIn,
SetupEntry::ChooseModel => SetupStep::ChooseModel,
SetupEntry::Auto if self.setup_model_picker().is_some() => SetupStep::ChooseModel,
SetupEntry::Auto => SetupStep::SignIn,
};
self.enter_setup(step);
match step {
SetupStep::SignIn => self.open_login_picker(),
SetupStep::ChooseModel => self.open_setup_model_picker(terminal),
}
}
pub(super) fn advance_setup_screen_after_login(
&mut self,
terminal: &mut super::DefaultTerminal,
) {
match self.setup_step() {
Some(SetupStep::SignIn) => {
self.enter_setup(SetupStep::ChooseModel);
self.open_setup_model_picker(terminal);
}
Some(SetupStep::ChooseModel) | None => {}
}
}
pub(super) fn finish_setup_screen(&mut self) {
match self.setup_step() {
Some(SetupStep::ChooseModel) => self.leave_setup(),
Some(SetupStep::SignIn) | None => {}
}
}
pub(super) fn dismiss_setup_screen(&mut self) {
self.leave_setup();
}
fn setup_model_picker(&mut self) -> Option<super::UiPicker> {
self.refresh_available_auths();
let picker = self.conversation_model_picker();
(!picker.items.is_empty()).then_some(picker)
}
fn open_setup_model_picker(&mut self, terminal: &mut super::DefaultTerminal) {
let Some(picker) = self.setup_model_picker() else {
self.leave_setup();
self.set_status("ready");
return;
};
self.input_ui.set_composer(ComposerMode::Picker(picker));
self.set_status("select model");
let _ = terminal.draw(|frame| self.draw(frame));
}
pub(super) fn draw_setup_screen(&mut self, frame: &mut Frame<'_>, area: Rect, step: SetupStep) {
frame.render_widget(Clear, area);
frame.render_widget(
ratatui::widgets::Paragraph::new("").style(Theme::surface()),
area,
);
let column = content_column(area);
if column.height == 0 {
return;
}
let width = column.width as usize;
let mut lines = welcome_lines(width);
lines.extend(step_lines(step, width));
lines.push(Line::raw(""));
let body_row = lines.len() as u16;
lines.extend(self.setup_body_lines(width, column.height.saturating_sub(body_row)));
lines.push(Line::raw(""));
lines.push(Line::from(Span::styled(
truncate_one_line("Esc to skip setup", width),
Theme::dim(),
)));
frame.render_widget(Paragraph::new(lines).style(Theme::surface()), column);
if let Some(position) = self.setup_filter_cursor(column, body_row) {
frame.set_cursor_position(position);
}
}
fn setup_body_lines(&mut self, width: usize, height: u16) -> Vec<Line<'static>> {
match self.input_ui.composer() {
ComposerMode::Input => vec![Line::from(Span::styled(
truncate_one_line(self.status(), width),
Theme::dim(),
))],
_ => self.composer_frame(width, height as usize).lines,
}
}
fn setup_filter_cursor(
&self,
column: Rect,
body_row: u16,
) -> Option<ratatui::layout::Position> {
let ComposerMode::Picker(picker) = self.input_ui.composer() else {
return None;
};
let offset = display_width(&picker.filter).saturating_add(2);
Some(ratatui::layout::Position {
x: column
.x
.saturating_add(offset.min(column.width.saturating_sub(1) as usize) as u16),
y: column.y.saturating_add(body_row),
})
}
}
fn content_column(area: Rect) -> Rect {
let width = area.width.min(CONTENT_WIDTH);
Rect {
x: area.x.saturating_add(area.width.saturating_sub(width) / 2),
y: area.y.saturating_add(TOP_PADDING),
width,
height: area.height.saturating_sub(TOP_PADDING),
}
}
fn welcome_lines(width: usize) -> Vec<Line<'static>> {
vec![
Line::from(vec![
Span::styled("rho", Theme::brand()),
Span::styled(" v", Theme::dim()),
Span::styled(super::smoke_injection::display_version(), Theme::success()),
]),
Line::raw(""),
Line::from(Span::styled(
truncate_one_line("Welcome. Two steps and you are ready to work.", width),
Theme::text_strong(),
)),
Line::raw(""),
]
}
fn step_lines(step: SetupStep, width: usize) -> Vec<Line<'static>> {
STEP_LABELS
.iter()
.enumerate()
.map(|(index, label)| {
let state = match index.cmp(&step.index()) {
std::cmp::Ordering::Less => StepState::Done,
std::cmp::Ordering::Equal => StepState::Current,
std::cmp::Ordering::Greater => StepState::Pending,
};
Line::from(Span::styled(
truncate_one_line(&format!("{} {label}", state.marker()), width),
state.style(),
))
})
.collect()
}
#[cfg(test)]
#[path = "setup_screen_tests.rs"]
mod tests;