Skip to main content

rust_memex/tui/indexer/
state.rs

1//! Wizard state for the data setup step.
2
3use std::path::PathBuf;
4
5/// Data setup option selected by user.
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum DataSetupOption {
8    /// Import an existing LanceDB database.
9    ImportLanceDB,
10    /// Index a directory with files.
11    IndexDirectory,
12    /// Skip data setup for now.
13    Skip,
14}
15
16impl DataSetupOption {
17    pub fn label(&self) -> &'static str {
18        match self {
19            Self::ImportLanceDB => "[1] Import existing LanceDB",
20            Self::IndexDirectory => "[2] Index a directory now",
21            Self::Skip => "[3] Skip for now",
22        }
23    }
24
25    pub fn detail(&self) -> &'static str {
26        match self {
27            Self::ImportLanceDB => "Copy or link an existing LanceDB database",
28            Self::IndexDirectory => "Recursively index files with embeddings",
29            Self::Skip => "Configure data later via CLI",
30        }
31    }
32
33    pub fn all() -> Vec<Self> {
34        vec![Self::ImportLanceDB, Self::IndexDirectory, Self::Skip]
35    }
36}
37
38/// State for the data setup wizard step.
39#[derive(Debug, Clone)]
40pub struct DataSetupState {
41    pub option: DataSetupOption,
42    pub focus: usize,
43    pub input_mode: bool,
44    pub input_buffer: String,
45    pub source_path: Option<String>,
46    pub namespace: Option<String>,
47    pub sub_step: DataSetupSubStep,
48    pub import_mode: ImportMode,
49    pub validation_error: Option<String>,
50}
51
52/// Sub-steps within data setup.
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub enum DataSetupSubStep {
55    SelectOption,
56    EnterPath,
57    EnterNamespace,
58    SelectImportMode,
59    Indexing,
60    Complete,
61}
62
63/// Mode for importing LanceDB.
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub enum ImportMode {
66    Copy,
67    Symlink,
68    ConfigOnly,
69}
70
71impl ImportMode {
72    pub fn label(&self) -> &'static str {
73        match self {
74            Self::Copy => "[1] Copy database files",
75            Self::Symlink => "[2] Create symlink",
76            Self::ConfigOnly => "[3] Just update config path",
77        }
78    }
79
80    pub fn all() -> Vec<Self> {
81        vec![Self::Copy, Self::Symlink, Self::ConfigOnly]
82    }
83}
84
85impl Default for DataSetupState {
86    fn default() -> Self {
87        Self {
88            option: DataSetupOption::Skip,
89            focus: 0,
90            input_mode: false,
91            input_buffer: String::new(),
92            source_path: None,
93            namespace: None,
94            sub_step: DataSetupSubStep::SelectOption,
95            import_mode: ImportMode::ConfigOnly,
96            validation_error: None,
97        }
98    }
99}
100
101impl DataSetupState {
102    pub fn new() -> Self {
103        Self::default()
104    }
105
106    pub fn focused_option(&self) -> DataSetupOption {
107        let options = DataSetupOption::all();
108        options
109            .get(self.focus)
110            .cloned()
111            .unwrap_or(DataSetupOption::Skip)
112    }
113
114    pub fn select_focused(&mut self) {
115        self.option = self.focused_option();
116        self.validation_error = None;
117        self.sub_step = match self.option {
118            DataSetupOption::ImportLanceDB | DataSetupOption::IndexDirectory => {
119                DataSetupSubStep::EnterPath
120            }
121            DataSetupOption::Skip => DataSetupSubStep::Complete,
122        };
123        if self.sub_step == DataSetupSubStep::EnterPath {
124            self.input_mode = true;
125            self.input_buffer.clear();
126        }
127    }
128
129    pub fn confirm_path(&mut self) {
130        let path = self.input_buffer.trim().to_string();
131        if path.is_empty() {
132            return;
133        }
134        self.validation_error = None;
135        self.source_path = Some(path);
136        self.input_mode = false;
137
138        match self.option {
139            DataSetupOption::ImportLanceDB => {
140                self.sub_step = DataSetupSubStep::SelectImportMode;
141                self.focus = 0;
142            }
143            DataSetupOption::IndexDirectory => {
144                if let Some(ref source_path) = self.source_path {
145                    let folder_name = PathBuf::from(source_path)
146                        .file_name()
147                        .and_then(|name| name.to_str())
148                        .unwrap_or("indexed")
149                        .to_string();
150                    self.input_buffer = format!("kb:{folder_name}");
151                }
152                self.sub_step = DataSetupSubStep::EnterNamespace;
153                self.input_mode = true;
154            }
155            DataSetupOption::Skip => {
156                self.sub_step = DataSetupSubStep::Complete;
157            }
158        }
159    }
160
161    pub fn confirm_namespace(&mut self) {
162        let namespace = self.input_buffer.trim();
163        self.validation_error = None;
164        self.namespace = Some(if namespace.is_empty() {
165            "rag".to_string()
166        } else {
167            namespace.to_string()
168        });
169        self.input_mode = false;
170        self.sub_step = DataSetupSubStep::Indexing;
171    }
172
173    pub fn select_import_mode(&mut self, mode: ImportMode) {
174        self.import_mode = mode;
175        self.sub_step = DataSetupSubStep::Complete;
176    }
177
178    pub fn is_done(&self) -> bool {
179        self.sub_step == DataSetupSubStep::Complete
180    }
181
182    pub fn is_indexing(&self) -> bool {
183        self.sub_step == DataSetupSubStep::Indexing
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    #[test]
192    fn state_transitions_select_directory_flow() {
193        let mut state = DataSetupState::new();
194        state.focus = 1;
195        state.select_focused();
196        assert_eq!(state.option, DataSetupOption::IndexDirectory);
197        assert_eq!(state.sub_step, DataSetupSubStep::EnterPath);
198
199        state.input_buffer = "/tmp/docs".to_string();
200        state.confirm_path();
201        assert_eq!(state.sub_step, DataSetupSubStep::EnterNamespace);
202
203        state.input_buffer = "kb:docs".to_string();
204        state.confirm_namespace();
205        assert_eq!(state.namespace.as_deref(), Some("kb:docs"));
206        assert!(state.is_indexing());
207    }
208}