Iteration 1: file-backed projects with auto-named temps, lock file, and L1 CLI
Replaces the in-memory database with an on-disk project. Startup either opens a project at the positional CLI path (L1) or creates an auto-named temp project (<YYYYMMDD>-<word>-<word>-<word>) under the OS-standard data directory or a --data-dir override. The new project::Project type owns the directory skeleton and a PID+hostname lock file with stale-lock takeover via sysinfo. The status bar now shows "Project: <Display Name>", derived by a small kebab/snake/camel prettifier. Per-command persistence to YAML/CSV/history.log is NOT yet wired -- that's Iteration 2; for now playground.db carries the state across quits. Tests: 257 passing (231 lib + 9 new integration + 17 existing), 0 failing, 0 skipped. Clippy clean with nursery lints.
This commit is contained in:
@@ -27,10 +27,16 @@ pub fn render(app: &mut App, theme: &Theme, frame: &mut Frame<'_>) {
|
||||
let area = frame.area();
|
||||
paint_background(theme, frame, area);
|
||||
|
||||
// Reserve a single row at the bottom for the shortcut/status bar.
|
||||
// Reserve two rows at the bottom for status:
|
||||
// - top row: "Project: <Display Name>" (P-NAME-3, ADR-0015 §2).
|
||||
// - bottom row: mode-aware keyboard shortcuts.
|
||||
let outer = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Min(8), Constraint::Length(1)])
|
||||
.constraints([
|
||||
Constraint::Min(8),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
])
|
||||
.split(area);
|
||||
|
||||
let columns = Layout::default()
|
||||
@@ -40,7 +46,24 @@ pub fn render(app: &mut App, theme: &Theme, frame: &mut Frame<'_>) {
|
||||
|
||||
render_items_panel(app, theme, frame, columns[0]);
|
||||
render_right_column(app, theme, frame, columns[1]);
|
||||
render_status_bar(app, theme, frame, outer[1]);
|
||||
render_project_label(app, theme, frame, outer[1]);
|
||||
render_status_bar(app, theme, frame, outer[2]);
|
||||
}
|
||||
|
||||
fn render_project_label(app: &App, theme: &Theme, frame: &mut Frame<'_>, area: Rect) {
|
||||
let label_style = Style::default().fg(theme.muted);
|
||||
let value_style = Style::default()
|
||||
.fg(theme.fg)
|
||||
.add_modifier(Modifier::BOLD);
|
||||
let bar_style = Style::default().bg(theme.bg).fg(theme.muted);
|
||||
|
||||
let display = app.project_name.as_deref().unwrap_or("(no project)");
|
||||
let line = Line::from(vec![
|
||||
Span::styled("Project: ", label_style),
|
||||
Span::styled(display.to_string(), value_style),
|
||||
]);
|
||||
let paragraph = Paragraph::new(line).style(bar_style);
|
||||
frame.render_widget(paragraph, area);
|
||||
}
|
||||
|
||||
fn render_right_column(app: &mut App, theme: &Theme, frame: &mut Frame<'_>, area: Rect) {
|
||||
@@ -350,6 +373,13 @@ mod tests {
|
||||
use ratatui::backend::TestBackend;
|
||||
|
||||
fn render_to_string(app: &mut App, theme: &Theme, width: u16, height: u16) -> String {
|
||||
// Snapshot tests need realistic state, not the boot
|
||||
// fallback "(no project)" — every real session has a
|
||||
// project. Set a representative name unless the test
|
||||
// already set one.
|
||||
if app.project_name.is_none() {
|
||||
app.project_name = Some("Term Planner".to_string());
|
||||
}
|
||||
let backend = TestBackend::new(width, height);
|
||||
let mut terminal = Terminal::new(backend).expect("create terminal");
|
||||
terminal
|
||||
|
||||
Reference in New Issue
Block a user