use super::theme::{
BG, BG_SURFACE, BORDER_ACTIVE, BORDER_IDLE, BRAND, BRAND_DARK, BRAND_MID, TEXT_MUTED,
TEXT_PRIMARY,
};
use crossterm::{
event::{self, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::CrosstermBackend,
layout::{Alignment, Constraint, Direction, Layout},
style::{Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph},
Terminal,
};
use smbcloud_model::project::Project;
use std::io;
pub fn show_project_detail_tui(project: &Project) -> io::Result<()> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let result = run_loop(&mut terminal, project);
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
result
}
fn run_loop<B: ratatui::backend::Backend>(
terminal: &mut Terminal<B>,
project: &Project,
) -> io::Result<()> {
loop {
terminal.draw(|frame| render(frame, project))?;
if let Event::Key(key) = event::read()? {
if key.kind != KeyEventKind::Press {
continue;
}
if matches!(key.code, KeyCode::Char('q') | KeyCode::Esc) {
return Ok(());
}
}
}
}
fn render(frame: &mut ratatui::Frame, project: &Project) {
let area = frame.area();
frame.render_widget(Block::default().style(Style::default().bg(BG)), area);
let zones = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), Constraint::Min(0), Constraint::Length(2), ])
.split(area);
render_title(frame, zones[0]);
render_card(frame, project, zones[1]);
render_footer(frame, zones[2]);
}
fn render_title(frame: &mut ratatui::Frame, area: ratatui::layout::Rect) {
let block = Block::default()
.style(Style::default().bg(BG_SURFACE))
.borders(Borders::BOTTOM)
.border_style(Style::default().fg(BORDER_IDLE));
let line = Line::from(vec![
Span::styled(
"smb",
Style::default().fg(BRAND).add_modifier(Modifier::BOLD),
),
Span::styled(
"Cloud",
Style::default()
.fg(TEXT_PRIMARY)
.add_modifier(Modifier::BOLD),
),
Span::styled(" · ", Style::default().fg(BORDER_ACTIVE)),
Span::styled("Project", Style::default().fg(TEXT_MUTED)),
]);
frame.render_widget(
Paragraph::new(line)
.block(block)
.alignment(Alignment::Center),
area,
);
}
fn render_card(frame: &mut ratatui::Frame, project: &Project, area: ratatui::layout::Rect) {
let h_zones = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Fill(1),
Constraint::Max(72),
Constraint::Fill(1),
])
.split(area);
let card_col = h_zones[1];
let v_zones = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Fill(1),
Constraint::Length(17),
Constraint::Fill(1),
])
.split(card_col);
let card_area = v_zones[1];
let sep_len = (card_area.width.saturating_sub(5)) as usize;
let separator = "─".repeat(sep_len);
let created_at = project.created_at.format("%Y-%m-%d").to_string();
let updated_at = project.updated_at.format("%Y-%m-%d").to_string();
let content: Vec<Line<'static>> = vec![
Line::from(""),
Line::from(vec![
Span::raw(" "),
Span::styled(
"◈ ",
Style::default().fg(BRAND).add_modifier(Modifier::BOLD),
),
Span::styled(
project.name.clone(),
Style::default()
.fg(TEXT_PRIMARY)
.add_modifier(Modifier::BOLD),
),
]),
Line::from(""),
Line::from(vec![
Span::raw(" "),
Span::styled(separator, Style::default().fg(BRAND_MID)),
]),
Line::from(""),
field_line("ID", &project.id.to_string()),
field_line("Runner", &project.runner.to_string()),
field_line("Deploy method", &project.deployment_method.to_string()),
Line::from(""),
field_line(
"Repository",
&project.repository.clone().unwrap_or_else(|| "—".into()),
),
field_line(
"Description",
&project.description.clone().unwrap_or_else(|| "—".into()),
),
Line::from(""),
field_line("Created at", &created_at),
field_line("Updated at", &updated_at),
Line::from(""),
];
let block = Block::default()
.title(Span::styled(
" Project ",
Style::default().fg(BRAND).add_modifier(Modifier::BOLD),
))
.borders(Borders::ALL)
.border_style(Style::default().fg(BORDER_ACTIVE))
.style(Style::default().bg(BG_SURFACE));
frame.render_widget(Paragraph::new(content).block(block), card_area);
}
fn field_line(label: &str, value: &str) -> Line<'static> {
Line::from(vec![
Span::raw(" "),
Span::styled(format!("{:<16}", label), Style::default().fg(TEXT_MUTED)),
Span::styled(value.to_string(), Style::default().fg(TEXT_PRIMARY)),
])
}
fn render_footer(frame: &mut ratatui::Frame, area: ratatui::layout::Rect) {
let footer = Paragraph::new(Line::from(vec![
Span::styled(
" q / Esc ",
Style::default()
.fg(BRAND)
.bg(BRAND_DARK)
.add_modifier(Modifier::BOLD),
),
Span::styled(" Quit", Style::default().fg(TEXT_MUTED)),
]))
.alignment(Alignment::Center)
.style(Style::default().bg(BG_SURFACE));
frame.render_widget(footer, area);
}