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:
claude@clouddev1
2026-05-07 20:21:52 +00:00
parent 4fca862c6c
commit 601d3b6c51
20 changed files with 1883 additions and 18 deletions
+15 -6
View File
@@ -26,11 +26,13 @@ use tracing::{debug, error, info, warn};
use crate::action::Action;
use crate::app::App;
use crate::cli::Args;
use crate::db::{
DataResult, Database, DbError, DeleteResult, InsertResult, TableDescription, UpdateResult,
};
use crate::dsl::Command;
use crate::event::AppEvent;
use crate::project::open_or_create;
use crate::theme::Theme;
use crate::ui;
@@ -39,17 +41,22 @@ const SHUTDOWN_GRACE: Duration = Duration::from_millis(100);
/// Run the application until a `Quit` action is enacted or the
/// terminal closes.
pub async fn run(theme: Theme) -> Result<()> {
// For this iteration, every session uses a fresh in-memory
// database. Track 2 (project storage) wires up file-backed
// databases with proper lifecycle management.
let database = Database::open(":memory:").context("open database")?;
pub async fn run(args: Args) -> Result<()> {
let project = open_or_create(args.project_path.as_deref(), args.data_dir.as_deref())
.context("open or create project")?;
let db_path = project.db_path();
let display_name = project.display_name().to_string();
let database = Database::open(db_path.as_path()).context("open database")?;
let mut terminal = setup_terminal().context("setup terminal")?;
let result = run_loop(&mut terminal, theme, database).await;
let result = run_loop(&mut terminal, args.theme, database, display_name).await;
if let Err(e) = teardown_terminal(&mut terminal) {
// Teardown failures should not mask the primary error.
warn!(error = %e, "terminal teardown failed");
}
// `project` (and the lock it holds) is dropped here, releasing
// the lock file *after* the terminal has been restored.
drop(project);
result
}
@@ -57,11 +64,13 @@ async fn run_loop(
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
theme: Theme,
database: Database,
project_display_name: String,
) -> Result<()> {
let (event_tx, mut event_rx) = mpsc::channel::<AppEvent>(EVENT_CHANNEL_CAPACITY);
let reader_handle = spawn_event_reader(event_tx.clone());
let mut app = App::new();
app.project_name = Some(project_display_name);
// Seed the table list with whatever the database currently
// shows. For a fresh in-memory DB this is empty, but doing