tawasuyu

tawasuyu/brahman

Rust 582TOML 222Markdown 73JSON 198629 Sutichaykuna

⎇ 53dbdf0f1deea10d6fc567ea2b43e1e75d6d60cd crates/apps/database_explorer/src/lib.rs Rust · 284 siq'ikuna · 9.9 KB Ñawpa willay Pi imata llamkarqan ch'uya

chore: monorepo inicial con arje + minga + yahweh absorbidos 53dbdf0f1 · Sergio ·

1//! `yahweh_database_explorer` — explorer de SQLite.
2//!
3//! Mismo patrón que `yahweh_file_explorer` pero con `SqliteProvider`. La
4//! UX es idéntica (TreeView con lazy load por chevron); cambia solo el
5//! origen de los datos: filas de una tabla `items(id, parent_id, name,
6//! display_type, content)` en lugar del filesystem.
7
8use std::collections::{HashMap, HashSet};
9use std::sync::Arc;
10
11use gpui::{
12    Context, Entity, EventEmitter, IntoElement, Render, SharedString, Window, div, prelude::*,
13    px,
14};
15
16use yahweh_core::{DataProvider, DisplayType, EntityNode};
17use yahweh_provider_sqlite::SqliteDataProvider;
18use yahweh_theme::Theme;
19use yahweh_widget_tree::{RowId, RowKind, TreeEvent, TreeRow, TreeView};
20
21#[derive(Clone, Debug)]
22#[allow(dead_code)] // Consumido por el AppBus en Fase 4+.
23pub enum DatabaseExplorerEvent {
24    EntitySelected { id: String },
25    EntityOpened { id: String },
26}
27
28pub struct DatabaseExplorer {
29    tree_view: Entity<TreeView>,
30    provider: Arc<SqliteDataProvider>,
31    db_path: String,
32
33    expanded: HashSet<String>,
34    children: HashMap<String, Vec<EntityNode>>,
35    pending: HashSet<String>,
36    /// Mensaje de error si la DB no abrió. Se muestra en el header.
37    open_error: Option<String>,
38}
39
40const ROOT_KEY: &str = "__db_root__";
41
42impl EventEmitter<DatabaseExplorerEvent> for DatabaseExplorer {}
43
44impl DatabaseExplorer {
45    /// `db_path` es la ruta al .sqlite. Si no existe se crea con la tabla
46    /// `items` y un seed mínimo (ver `SqliteDataProvider::new`).
47    pub fn new(db_path: String, cx: &mut Context<Self>) -> Self {
48        cx.observe_global::<Theme>(|_, cx| cx.notify()).detach();
49
50        let tree_view = cx.new(|cx| TreeView::new("db-explorer-tree", cx));
51        cx.subscribe(&tree_view, |this: &mut DatabaseExplorer, _, ev, cx| {
52            this.on_tree_event(ev, cx);
53        })
54        .detach();
55
56        let (provider, open_error) = match SqliteDataProvider::new(&db_path) {
57            Ok(p) => (Some(Arc::new(p)), None),
58            Err(e) => (None, Some(e)),
59        };
60
61        let mut expanded = HashSet::new();
62        expanded.insert(ROOT_KEY.to_string());
63
64        let mut me = Self {
65            tree_view,
66            // Usamos un dummy provider si la DB no abrió. La UI mostrará el
67            // error en el header; cualquier load_children retornará vacío.
68            provider: provider.unwrap_or_else(|| {
69                Arc::new(SqliteDataProvider::new(":memory:").expect("memory db"))
70            }),
71            db_path,
72            expanded,
73            children: HashMap::new(),
74            pending: HashSet::new(),
75            open_error,
76        };
77        // Cargar el root (parent_id NULL en SQLite, mapped to None acá).
78        me.load_children(ROOT_KEY.to_string(), cx);
79        me
80    }
81
82    pub fn db_path(&self) -> &str {
83        &self.db_path
84    }
85
86    fn load_children(&mut self, parent_key: String, cx: &mut Context<Self>) {
87        if self.pending.contains(&parent_key) || self.children.contains_key(&parent_key) {
88            return;
89        }
90        self.pending.insert(parent_key.clone());
91
92        let provider = self.provider.clone();
93        let parent_for_task = parent_key.clone();
94        cx.spawn(async move |this, cx| {
95            // ROOT_KEY → None; cualquier otro → Some(actual id).
96            let arg: Option<String> = if parent_for_task == ROOT_KEY {
97                None
98            } else {
99                Some(parent_for_task.clone())
100            };
101            let result = provider.list_children(arg.as_deref()).await;
102            let _ = this.update(cx, |this, cx| {
103                this.on_children_loaded(parent_for_task, result, cx);
104            });
105        })
106        .detach();
107    }
108
109    fn on_children_loaded(
110        &mut self,
111        parent_key: String,
112        result: Result<Vec<EntityNode>, String>,
113        cx: &mut Context<Self>,
114    ) {
115        self.pending.remove(&parent_key);
116        let mut entries = result.unwrap_or_default();
117        sort_entries(&mut entries);
118        self.children.insert(parent_key, entries);
119        self.push_rows(cx);
120    }
121
122    fn push_rows(&self, cx: &mut Context<Self>) {
123        let mut rows = Vec::new();
124        // Una row "raíz virtual" para que el árbol tenga un anchor visible.
125        rows.push(TreeRow {
126            id: RowId::new(ROOT_KEY),
127            label: format!("(db) {}", self.db_path),
128            depth: 0,
129            kind: RowKind::Branch,
130            expanded: self.expanded.contains(ROOT_KEY),
131            icon: Some("🗄️".to_string()),
132        });
133        if self.expanded.contains(ROOT_KEY) {
134            self.append_children(ROOT_KEY, 1, &mut rows);
135        }
136
137        self.tree_view
138            .update(&mut *cx, |tree, cx| tree.set_rows(rows, cx));
139    }
140
141    fn append_children(&self, parent: &str, depth: u32, out: &mut Vec<TreeRow>) {
142        let Some(children) = self.children.get(parent) else { return };
143        for entry in children {
144            let kind = match entry.display_type {
145                DisplayType::Folder => RowKind::Branch,
146                _ => RowKind::Leaf,
147            };
148            let icon = match entry.display_type {
149                DisplayType::Folder => "📂",
150                DisplayType::File => "📄",
151                DisplayType::Stream => "📡",
152            };
153            let is_expanded = self.expanded.contains(&entry.id);
154            out.push(TreeRow {
155                id: RowId::new(entry.id.clone()),
156                label: entry.name.clone(),
157                depth,
158                kind,
159                expanded: is_expanded,
160                icon: Some(icon.to_string()),
161            });
162            if is_expanded {
163                self.append_children(&entry.id, depth + 1, out);
164            }
165        }
166    }
167
168    fn on_tree_event(&mut self, event: &TreeEvent, cx: &mut Context<Self>) {
169        match event {
170            TreeEvent::ChevronToggled(id) => {
171                let key = id.as_str().to_string();
172                if !self.expanded.remove(&key) {
173                    self.expanded.insert(key.clone());
174                    self.load_children(key, cx);
175                }
176                self.push_rows(cx);
177            }
178            TreeEvent::RowClicked(id) => {
179                let key = id.as_str();
180                if key == ROOT_KEY {
181                    return;
182                }
183                if let Some(entry) = self.find_entry(key) {
184                    if !matches!(entry.display_type, DisplayType::Folder) {
185                        cx.emit(DatabaseExplorerEvent::EntitySelected {
186                            id: key.to_string(),
187                        });
188                    }
189                }
190            }
191            TreeEvent::RowDoubleClicked(id) => {
192                let key = id.as_str();
193                if key == ROOT_KEY {
194                    return;
195                }
196                if let Some(entry) = self.find_entry(key) {
197                    if !matches!(entry.display_type, DisplayType::Folder) {
198                        cx.emit(DatabaseExplorerEvent::EntityOpened {
199                            id: key.to_string(),
200                        });
201                    }
202                }
203            }
204            TreeEvent::ContextMenuRequested { .. } | TreeEvent::ActiveChanged(_) => {}
205        }
206    }
207
208    fn find_entry(&self, id: &str) -> Option<&EntityNode> {
209        for entries in self.children.values() {
210            if let Some(e) = entries.iter().find(|e| e.id == id) {
211                return Some(e);
212            }
213        }
214        None
215    }
216}
217
218impl Render for DatabaseExplorer {
219    fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
220        let theme = Theme::global(cx).clone();
221        let pending_count = self.pending.len();
222
223        div()
224            .size_full()
225            .bg(theme.bg_panel.clone())
226            .flex()
227            .flex_col()
228            .child(
229                div()
230                    .h(px(28.0))
231                    .px(px(8.0))
232                    .border_b_1()
233                    .border_color(theme.border)
234                    .flex()
235                    .flex_row()
236                    .items_center()
237                    .gap(px(6.0))
238                    .child(
239                        div()
240                            .text_size(px(11.0))
241                            .text_color(theme.fg_muted)
242                            .child("🗄️"),
243                    )
244                    .child(
245                        div()
246                            .text_size(px(11.0))
247                            .text_color(theme.fg_text)
248                            .child(SharedString::from(self.db_path.clone())),
249                    )
250                    .child(
251                        div()
252                            .ml_auto()
253                            .text_size(px(10.0))
254                            .text_color(theme.fg_muted)
255                            .child(SharedString::from(if pending_count > 0 {
256                                format!("⏳ {}", pending_count)
257                            } else {
258                                String::new()
259                            })),
260                    ),
261            )
262            .child(if let Some(err) = self.open_error.clone() {
263                // Si la DB no abrió, mostramos el error y no pintamos el
264                // árbol vacío — sería confuso.
265                div()
266                    .p(px(12.0))
267                    .text_size(px(11.0))
268                    .text_color(theme.accent_strong)
269                    .child(SharedString::from(format!("error abriendo DB: {}", err)))
270            } else {
271                div().flex_grow().min_h(px(0.0)).child(self.tree_view.clone())
272            })
273    }
274}
275
276fn sort_entries(entries: &mut Vec<EntityNode>) {
277    entries.sort_by(|a, b| {
278        let a_dir = matches!(a.display_type, DisplayType::Folder);
279        let b_dir = matches!(b.display_type, DisplayType::Folder);
280        b_dir
281            .cmp(&a_dir)
282            .then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase()))
283    });
284}