Skip to main content

seshat_cli/tui/
mod.rs

1pub mod app;
2pub mod review_wizard;
3pub mod widgets;
4
5use std::sync::{Arc, Mutex};
6
7use crate::error::CliError;
8
9/// Launch the interactive convention review TUI using a shared connection.
10///
11/// The connection is also passed to `apply_review_actions` so that all
12/// reads and writes happen on the same snapshot inside a transaction.
13pub fn run_review_tui_with_conn(
14    branch_id: &str,
15    conn: &Arc<Mutex<rusqlite::Connection>>,
16) -> Result<Vec<app::ReviewAction>, CliError> {
17    let (conventions, _queried_branch_id) = app::query_conventions_for_review(conn, branch_id)?;
18
19    if conventions.is_empty() {
20        eprintln!("No conventions found to review.");
21        return Ok(Vec::new());
22    }
23
24    let convention_count = conventions.len();
25    let mut terminal = ratatui::init();
26    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
27        review_wizard::run_app(&mut terminal, conventions, conn, branch_id)
28    }));
29    ratatui::restore();
30
31    let already_confirmed = app::count_confirmed_conventions(conn);
32
33    match result {
34        Ok(Ok(r)) => {
35            app::show_summary(
36                &r,
37                &app::SummaryContext {
38                    total_in_scope: convention_count,
39                    already_confirmed,
40                },
41            );
42            Ok(r)
43        }
44        Ok(Err(e)) => Err(e),
45        Err(_) => Err(CliError::TuiError(
46            "TUI panicked; terminal state has been restored".to_owned(),
47        )),
48    }
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54    use rusqlite::Connection;
55
56    fn setup_empty_db() -> Arc<Mutex<Connection>> {
57        let conn = Connection::open_in_memory().unwrap();
58        // The decisions table mirrors V12's primary-key column so the LEFT
59        // JOIN in `query_conventions_for_review` resolves cleanly.
60        conn.execute_batch(
61            "CREATE TABLE nodes (
62                id INTEGER PRIMARY KEY,
63                description TEXT,
64                nature TEXT,
65                weight TEXT,
66                confidence REAL,
67                adoption_count INTEGER,
68                total_count INTEGER,
69                ext_data TEXT,
70                description_hash TEXT,
71                branch_id TEXT,
72                removed INTEGER DEFAULT 0
73            );
74            CREATE TABLE edges (
75                id INTEGER PRIMARY KEY,
76                source_node_id INTEGER,
77                target_node_id INTEGER,
78                edge_type TEXT,
79                ext_data TEXT,
80                branch_id TEXT,
81                removed INTEGER DEFAULT 0
82            );
83            CREATE TABLE fts_index (
84                content TEXT,
85                node_id INTEGER,
86                branch_id TEXT
87            );
88            CREATE TABLE decisions (
89                description_hash TEXT NOT NULL PRIMARY KEY,
90                description TEXT NOT NULL,
91                state TEXT NOT NULL,
92                nature TEXT NOT NULL,
93                weight TEXT NOT NULL,
94                category TEXT,
95                reason TEXT,
96                examples TEXT,
97                decided_on_branch TEXT NOT NULL,
98                decided_at INTEGER NOT NULL,
99                updated_at INTEGER NOT NULL
100            );",
101        )
102        .unwrap();
103        Arc::new(Mutex::new(conn))
104    }
105
106    #[test]
107    fn empty_conventions_returns_empty_vec() {
108        let conn = setup_empty_db();
109        let result = run_review_tui_with_conn("main", &conn);
110        assert!(result.is_ok());
111        assert!(result.unwrap().is_empty());
112    }
113
114    #[test]
115    fn error_from_query_is_propagated() {
116        let conn = setup_empty_db();
117        // Drop the table so the query fails
118        {
119            let guard = conn.lock().unwrap();
120            guard.execute_batch("DROP TABLE nodes").unwrap();
121        }
122        let result = run_review_tui_with_conn("main", &conn);
123        assert!(result.is_err());
124    }
125}