Complete TUI Phase 3 implementation with all 5 power feature screens: - Who screen: 5 modes (expert/workload/reviews/active/overlap) with mode tabs, input bar, result rendering, and hint bar - Search screen: full-text search with result list and scoring display - Timeline screen: chronological event feed with time-relative display - Trace screen: file provenance chains with expand/collapse, rename tracking, and linked issues/discussions - File History screen: per-file MR timeline with rename chain display and discussion snippets Also includes: - Command palette overlay (fuzzy search) - Bootstrap screen (initial sync flow) - Action layer split from monolithic action.rs to per-screen modules - Entity and render cache infrastructure - Shared who_types module in core crate - All screens wired into view/mod.rs dispatch - 597 tests passing, clippy clean (pedantic + nursery), fmt clean
135 lines
3.9 KiB
Rust
135 lines
3.9 KiB
Rust
#![allow(dead_code)] // Phase 2.5: consumed by render_screen dispatch
|
|
|
|
//! Bootstrap screen view.
|
|
//!
|
|
//! Shown when the database has no entity data. Guides users to run
|
|
//! a sync to populate the database.
|
|
|
|
use ftui::core::geometry::Rect;
|
|
use ftui::render::cell::{Cell, PackedRgba};
|
|
use ftui::render::drawing::Draw;
|
|
use ftui::render::frame::Frame;
|
|
|
|
use crate::state::bootstrap::BootstrapState;
|
|
|
|
// Colors (Flexoki palette).
|
|
const TEXT: PackedRgba = PackedRgba::rgb(0xCE, 0xCD, 0xC3); // tx
|
|
const MUTED: PackedRgba = PackedRgba::rgb(0x87, 0x87, 0x80); // tx-2
|
|
const ACCENT: PackedRgba = PackedRgba::rgb(0xDA, 0x70, 0x2C); // orange
|
|
|
|
/// Render the bootstrap screen.
|
|
///
|
|
/// Centers a message in the content area, guiding the user to start a sync.
|
|
/// When a sync is in progress, shows a "syncing" message instead.
|
|
pub fn render_bootstrap(frame: &mut Frame<'_>, state: &BootstrapState, area: Rect) {
|
|
if area.width < 10 || area.height < 5 {
|
|
return;
|
|
}
|
|
|
|
let center_y = area.y + area.height / 2;
|
|
let max_x = area.x.saturating_add(area.width);
|
|
|
|
// Title.
|
|
let title = "No data found";
|
|
let title_x = area.x + area.width.saturating_sub(title.len() as u16) / 2;
|
|
frame.print_text_clipped(
|
|
title_x,
|
|
center_y.saturating_sub(2),
|
|
title,
|
|
Cell {
|
|
fg: ACCENT,
|
|
..Cell::default()
|
|
},
|
|
max_x,
|
|
);
|
|
|
|
if state.sync_started {
|
|
// Sync in progress.
|
|
let msg = "Syncing data from GitLab...";
|
|
let msg_x = area.x + area.width.saturating_sub(msg.len() as u16) / 2;
|
|
frame.print_text_clipped(
|
|
msg_x,
|
|
center_y,
|
|
msg,
|
|
Cell {
|
|
fg: TEXT,
|
|
..Cell::default()
|
|
},
|
|
max_x,
|
|
);
|
|
} else {
|
|
// Prompt user to start sync.
|
|
let msg = "Run sync to get started.";
|
|
let msg_x = area.x + area.width.saturating_sub(msg.len() as u16) / 2;
|
|
frame.print_text_clipped(
|
|
msg_x,
|
|
center_y,
|
|
msg,
|
|
Cell {
|
|
fg: TEXT,
|
|
..Cell::default()
|
|
},
|
|
max_x,
|
|
);
|
|
|
|
let hint = "Press 'g' then 's' to start sync, or 'q' to quit.";
|
|
let hint_x = area.x + area.width.saturating_sub(hint.len() as u16) / 2;
|
|
frame.print_text_clipped(
|
|
hint_x,
|
|
center_y + 2,
|
|
hint,
|
|
Cell {
|
|
fg: MUTED,
|
|
..Cell::default()
|
|
},
|
|
max_x,
|
|
);
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use ftui::render::grapheme_pool::GraphemePool;
|
|
|
|
macro_rules! with_frame {
|
|
($width:expr, $height:expr, |$frame:ident| $body:block) => {{
|
|
let mut pool = GraphemePool::new();
|
|
let mut $frame = Frame::new($width, $height, &mut pool);
|
|
$body
|
|
}};
|
|
}
|
|
|
|
#[test]
|
|
fn test_render_bootstrap_no_panic() {
|
|
with_frame!(80, 24, |frame| {
|
|
let state = BootstrapState::default();
|
|
render_bootstrap(&mut frame, &state, Rect::new(0, 1, 80, 22));
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn test_render_bootstrap_sync_started() {
|
|
with_frame!(80, 24, |frame| {
|
|
let state = BootstrapState {
|
|
sync_started: true,
|
|
..Default::default()
|
|
};
|
|
render_bootstrap(&mut frame, &state, Rect::new(0, 1, 80, 22));
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn test_render_bootstrap_tiny_area_noop() {
|
|
with_frame!(8, 3, |frame| {
|
|
let state = BootstrapState::default();
|
|
render_bootstrap(&mut frame, &state, Rect::new(0, 0, 8, 3));
|
|
// Should not panic — early return for tiny areas.
|
|
});
|
|
}
|
|
}
|