Skip to main content

mneme/tui/
app.rs

1use std::sync::Arc;
2
3use chrono::{DateTime, Utc};
4
5use crate::config::settings::Settings;
6use crate::store::db::Database;
7use crate::store::entities::{EntitySearchResult, EntityType};
8use crate::store::memory::{GraphData, Memory, MemoryStats, ProjectSummary, SearchQuery, Session};
9
10/// Tabs del panel de detalle.
11#[derive(Debug, Clone, Copy, PartialEq)]
12pub enum DetailTab {
13    Content,
14    Structured,
15    Entities,
16    Temporal,
17    Relations,
18}
19impl DetailTab {
20    pub fn label(&self) -> &'static str {
21        match self {
22            Self::Content => "Content",
23            Self::Structured => "Fields",
24            Self::Entities => "Entities",
25            Self::Temporal => "Temporal",
26            Self::Relations => "Graph",
27        }
28    }
29    pub fn next(&self) -> Self {
30        match self {
31            Self::Content => Self::Structured,
32            Self::Structured => Self::Entities,
33            Self::Entities => Self::Temporal,
34            Self::Temporal => Self::Relations,
35            Self::Relations => Self::Content,
36        }
37    }
38    pub fn prev(&self) -> Self {
39        match self {
40            Self::Content => Self::Relations,
41            Self::Structured => Self::Content,
42            Self::Entities => Self::Structured,
43            Self::Temporal => Self::Entities,
44            Self::Relations => Self::Temporal,
45        }
46    }
47}
48
49/// Estado global de la app — estilo lazygit/lazydocker.
50pub struct App {
51    pub project: String,
52    pub memories: Vec<Memory>,
53    pub sessions: Vec<Session>,
54    pub selected: usize,
55    pub scroll: usize,
56    pub search: String,
57    pub status_msg: Option<String>,
58    pub quit: bool,
59    pub show_help: bool,
60    pub detail_tab: DetailTab,
61    pub detail_scroll: usize,
62    pub stats: Option<MemoryStats>,
63    pub graph: Option<GraphData>,
64    pub graph_sel: usize,
65    pub entity_data: Option<Vec<(String, EntityType, u32)>>,
66    pub temporal_data: Option<(Vec<Memory>, u8)>,
67    pub active_panel: usize, // 0=lista, 1=detalle, 2=search
68    pub total_mems: u32,
69    pub show_projects: bool,
70    pub projects_list: Vec<crate::store::memory::ProjectSummary>,
71    pub db: Arc<Database>,
72}
73
74impl App {
75    pub fn new(db: Arc<Database>, _settings: Arc<Settings>) -> Self {
76        // Show all projects by default, or infer if none
77        let project = Settings::infer_project();
78        Self {
79            project,
80            memories: Vec::new(),
81            sessions: Vec::new(),
82            selected: 0,
83            scroll: 0,
84            search: String::new(),
85            status_msg: None,
86            quit: false,
87            show_help: false,
88            detail_tab: DetailTab::Content,
89            detail_scroll: 0,
90            stats: None,
91            graph: None,
92            graph_sel: 0,
93            entity_data: None,
94            temporal_data: None,
95            active_panel: 0,
96            total_mems: 0,
97            show_projects: false,
98            projects_list: vec![],
99            db,
100        }
101    }
102
103    // ── CARGA ──
104    pub fn load(&mut self) {
105        let s = self.db.memories();
106        if self.search.is_empty() {
107            // Show memories from all projects
108            let mut all = Vec::new();
109            if let Ok(projects) = s.list_projects() {
110                for p in &projects {
111                    if let Ok(pmems) = s.list(&p.name, None, None, None, 100, 0) {
112                        all.extend(pmems);
113                    }
114                }
115            }
116            all.sort_by(|a, b| b.created_at.cmp(&a.created_at));
117            all.truncate(500);
118            self.memories = all;
119        } else {
120            let q = SearchQuery {
121                text: self.search.clone(),
122                project: None,
123                scope: None,
124                memory_type: None,
125                importance: None,
126                tags: vec![],
127                limit: 200,
128                include_snippet: false,
129                all_projects: true,
130            };
131            self.memories = s
132                .search(&q, &crate::store::search::SearchWeights::default(), None)
133                .unwrap_or_default()
134                .into_iter()
135                .map(|r| r.memory)
136                .collect();
137        }
138        self.selected = self.selected.min(self.memories.len().saturating_sub(1));
139        self.scroll = 0;
140        self.detail_scroll = 0;
141        // Show total stats across all projects
142        let mut total_mems = self.memories.len() as u32;
143        if let Ok(projects) = s.list_projects() {
144            total_mems = projects.iter().map(|p| p.memory_count).sum();
145        }
146        self.projects_list = s.list_projects().unwrap_or_default();
147        self.total_mems = total_mems;
148        self.sessions = self
149            .db
150            .sessions()
151            .list(&self.project, 50)
152            .unwrap_or_default();
153    }
154
155    // ── NAV ──
156    pub fn down(&mut self) {
157        if !self.memories.is_empty() {
158            self.selected = (self.selected + 1).min(self.memories.len() - 1);
159            if self.selected >= self.scroll + 20 {
160                self.scroll += 1;
161            }
162        }
163    }
164    pub fn up(&mut self) {
165        self.selected = self.selected.saturating_sub(1);
166        if self.selected < self.scroll {
167            self.scroll = self.scroll.saturating_sub(1);
168        }
169    }
170    pub fn first(&mut self) {
171        self.selected = 0;
172        self.scroll = 0;
173    }
174    pub fn last(&mut self) {
175        if !self.memories.is_empty() {
176            self.selected = self.memories.len() - 1;
177            self.scroll = self.selected.saturating_sub(19);
178        }
179    }
180    pub fn pgdn(&mut self) {
181        if !self.memories.is_empty() {
182            self.selected = (self.selected + 20).min(self.memories.len() - 1);
183            if self.selected >= self.scroll + 20 {
184                self.scroll += 20;
185            }
186        }
187    }
188    pub fn pgup(&mut self) {
189        self.selected = self.selected.saturating_sub(20);
190        if self.selected < self.scroll {
191            self.scroll = self.scroll.saturating_sub(20);
192        }
193    }
194    pub fn sel(&self) -> Option<&Memory> {
195        self.memories.get(self.selected)
196    }
197
198    // ── DETAIL ──
199    pub fn tab_next(&mut self) {
200        self.detail_tab = self.detail_tab.next();
201        self.detail_scroll = 0;
202    }
203    pub fn tab_prev(&mut self) {
204        self.detail_tab = self.detail_tab.prev();
205        self.detail_scroll = 0;
206    }
207    pub fn dscroll_down(&mut self) {
208        self.detail_scroll += 3;
209    }
210    pub fn dscroll_up(&mut self) {
211        self.detail_scroll = self.detail_scroll.saturating_sub(3);
212    }
213
214    // ── ACTIONS ──
215    pub fn delete_sel(&mut self) {
216        if let Some(m) = self.sel() {
217            self.db.memories().delete(m.id, false).ok();
218            self.status_msg = Some("🗑 Deleted".into());
219            self.load();
220        }
221    }
222    pub fn load_graph(&mut self) {
223        self.graph = self.db.memories().get_graph(&self.project).ok();
224        self.graph_sel = 0;
225    }
226    pub fn graph_next(&mut self) {
227        if let Some(ref d) = self.graph {
228            if !d.nodes.is_empty() {
229                self.graph_sel = (self.graph_sel + 1) % d.nodes.len();
230            }
231        }
232    }
233    pub fn graph_prev(&mut self) {
234        if let Some(ref d) = self.graph {
235            if !d.nodes.is_empty() {
236                self.graph_sel = self.graph_sel.checked_sub(1).unwrap_or(d.nodes.len() - 1);
237            }
238        }
239    }
240    pub fn load_entity(&mut self) {
241        self.entity_data = self.db.entities().frequent_entities(&self.project, 30).ok();
242    }
243    pub fn load_temporal(&mut self) {
244        self.temporal_data = Some((
245            self.db
246                .memories()
247                .list(&self.project, None, None, None, 500, 0)
248                .unwrap_or_default(),
249            0,
250        ));
251    }
252    pub fn temporal_cycle(&mut self) {
253        if let Some(ref mut td) = self.temporal_data {
254            td.1 = (td.1 + 1) % 3;
255        }
256    }
257}