pub mod components;
pub mod dialogs;
pub mod rendering;
use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph, Wrap},
Frame,
};
use crate::app::App;
use crate::models::*;
pub use dialogs::*;
pub use rendering::*;
fn should_show_messages(app: &App) -> bool {
let has_messages = app.ui_state.message_state.has_messages();
let in_dialog = matches!(
app.app_state(),
AppState::CreatingDocument
| AppState::CreatingChildDocument
| AppState::CreatingAdr
| AppState::Confirming
| AppState::SelectingBacklogCategory
);
has_messages && !in_dialog
}
fn calculate_message_height(app: &App) -> u16 {
if should_show_messages(app) {
3
} else {
0
}
}
fn draw_main_content(f: &mut Frame, app: &App, area: Rect) {
match app.app_state() {
AppState::EditingContent => {
draw_content_edit_form(f, app, area);
}
_ => {
if !app.is_ready() {
draw_loading(f, app, area);
} else {
draw_kanban_board(f, app, area);
}
draw_modal_dialogs(f, app, f.area());
}
}
}
fn draw_modal_dialogs(f: &mut Frame, app: &App, area: Rect) {
match app.app_state() {
AppState::CreatingDocument => draw_creation_dialog(f, app, area),
AppState::CreatingChildDocument => draw_child_creation_dialog(f, app, area),
AppState::CreatingAdr => draw_adr_creation_dialog(f, app, area),
AppState::SelectingBacklogCategory => draw_backlog_category_selection_dialog(f, app, area),
AppState::Confirming => draw_confirmation_dialog(f, app, area),
_ => {} }
}
fn get_footer_text(app: &App) -> &'static str {
match app.app_state() {
AppState::CreatingDocument => "Enter: Create | Escape: Cancel | Type to enter title",
AppState::CreatingChildDocument => "Enter: Create | Escape: Cancel | Type to enter title",
AppState::CreatingAdr => "Enter: Create ADR | Escape: Cancel | Type to enter title",
AppState::SelectingBacklogCategory => "↑↓: Navigate | Enter: Select | Escape: Cancel",
AppState::EditingContent => "Ctrl+S: Save | Esc: Cancel | Type to edit document content",
AppState::Confirming => get_confirmation_footer_text(app),
_ => get_normal_state_footer_text(app),
}
}
fn get_confirmation_footer_text(app: &App) -> &'static str {
match app.ui_state.confirmation_type {
Some(crate::app::state::ConfirmationType::Delete) => "Y: Yes, delete | N: Cancel",
Some(crate::app::state::ConfirmationType::Transition) => "Y: Yes, transition | N: Cancel",
None => "Y: Yes | N: Cancel",
}
}
fn get_normal_state_footer_text(app: &App) -> &'static str {
if !app.is_ready() {
return "q: Quit";
}
use crate::models::BoardType;
match app.ui_state.current_board {
BoardType::Strategy => "↑↓←→: Navigate | 1-5: Jump to board | v: Vision | Tab: Switch | Enter: Edit | Ctrl+n: Create Initiative | d: Del | r: Archive | t: Trans | y: Sync | q: Quit",
BoardType::Initiative => "↑↓←→: Navigate | 1-5: Jump to board | v: Vision | Tab: Switch | Enter: Edit | Ctrl+n: Create Task | d: Del | r: Archive | t: Trans | y: Sync | q: Quit",
BoardType::Task => "↑↓←→: Navigate | 1-5: Jump to board | v: Vision | Tab: Switch | Enter: Edit | d: Del | r: Archive | t: Trans | y: Sync | q: Quit",
BoardType::Adr => "↑↓←→: Navigate | 1-5: Jump to board | v: Vision | Tab: Switch | Enter: Edit | Ctrl+n: Create ADR | d: Del | r: Archive | t: Trans | y: Sync | q: Quit",
BoardType::Backlog => "↑↓←→: Navigate | 1-5: Jump to board | v: Vision | Tab: Switch | Enter: Edit | Ctrl+n: Create Backlog Item | d: Del | r: Archive | t: Trans | y: Sync | q: Quit",
}
}
fn get_board_title(app: &App) -> String {
if app.is_ready() {
format!(
"{} Board",
match app.ui_state.current_board {
BoardType::Strategy => "Strategy",
BoardType::Initiative => "Initiative",
BoardType::Task => "Task",
BoardType::Adr => "ADR",
BoardType::Backlog => "Backlog",
}
)
} else {
"Initializing...".to_string()
}
}
pub fn draw(f: &mut Frame, app: &App) {
let message_height = calculate_message_height(app);
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), Constraint::Min(0), Constraint::Length(message_height), Constraint::Length(3), ])
.split(f.area());
draw_header(f, app, chunks[0]);
draw_main_content(f, app, chunks[1]);
if should_show_messages(app) {
draw_message_area(f, &app.ui_state.message_state, chunks[2]);
}
draw_footer(f, app, chunks[3]);
}
fn draw_header(f: &mut Frame, app: &App, area: Rect) {
let title = get_board_title(app);
let header = Paragraph::new(title)
.style(
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)
.block(Block::default().borders(Borders::ALL))
.alignment(Alignment::Center);
f.render_widget(header, area);
}
fn draw_loading(f: &mut Frame, app: &App, area: Rect) {
let mut status_lines = vec![];
if app.core_state.workspace_valid {
status_lines.push(Line::from(vec![
Span::styled("✓ ", Style::default().fg(Color::Green)),
Span::raw("Workspace found"),
]));
} else {
status_lines.push(Line::from(vec![
Span::styled("○ ", Style::default().fg(Color::Yellow)),
Span::raw("Checking workspace..."),
]));
}
if app.core_state.sync_in_progress {
status_lines.push(Line::from(vec![
Span::styled("⟳ ", Style::default().fg(Color::Cyan)),
Span::raw("Syncing database..."),
]));
} else if app.core_state.sync_complete {
status_lines.push(Line::from(vec![
Span::styled("✓ ", Style::default().fg(Color::Green)),
Span::raw("Database synchronized"),
]));
} else if app.core_state.workspace_valid {
status_lines.push(Line::from(vec![
Span::styled("○ ", Style::default().fg(Color::Yellow)),
Span::raw("Synchronizing database..."),
]));
}
let loading = Paragraph::new(status_lines)
.style(Style::default().fg(Color::White))
.block(Block::default().title("Loading").borders(Borders::ALL))
.wrap(Wrap { trim: true });
f.render_widget(loading, area);
}
fn draw_footer(f: &mut Frame, app: &App, area: Rect) {
let footer_text = get_footer_text(app);
let footer = Paragraph::new(footer_text)
.style(Style::default().fg(Color::Gray))
.block(Block::default().borders(Borders::ALL));
f.render_widget(footer, area);
}
fn draw_content_edit_form(f: &mut Frame, app: &App, area: Rect) {
if let Some(ref textarea) = app.ui_state.strategy_editor {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), Constraint::Min(1), ])
.split(area);
let title = if app.ui_state.editing_vision_path.is_some() {
"Editing Vision Document".to_string()
} else if let Some(selected_item) = app.get_viewed_ticket() {
format!(
"Editing {}: {}",
selected_item.doc_type(),
selected_item.title()
)
} else {
"Editing Document".to_string()
};
let title_widget = Paragraph::new(title)
.style(
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)
.block(Block::default().borders(Borders::ALL));
f.render_widget(title_widget, chunks[0]);
f.render_widget(textarea, chunks[1]);
}
}