rust_memex/tui/indexer/
state.rs1use std::path::PathBuf;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum DataSetupOption {
8 ImportLanceDB,
10 IndexDirectory,
12 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#[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#[derive(Debug, Clone, PartialEq, Eq)]
54pub enum DataSetupSubStep {
55 SelectOption,
56 EnterPath,
57 EnterNamespace,
58 SelectImportMode,
59 Indexing,
60 Complete,
61}
62
63#[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}