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 detail_tab: DetailTab,
60    pub detail_scroll: usize,
61    pub stats: Option<MemoryStats>,
62    pub graph: Option<GraphData>,
63    pub graph_sel: usize,
64    pub entity_data: Option<Vec<(String, EntityType, u32)>>,
65    pub temporal_data: Option<(Vec<Memory>, u8)>,
66    pub active_panel: usize, // 0=lista, 1=detalle, 2=search
67    pub total_mems: u32,
68    pub db: Arc<Database>,
69}
70
71impl App {
72    pub fn new(db: Arc<Database>, _settings: Arc<Settings>) -> Self {
73        let project = Settings::infer_project();
74        Self {
75            project,
76            memories: Vec::new(),
77            sessions: Vec::new(),
78            selected: 0,
79            scroll: 0,
80            search: String::new(),
81            status_msg: None,
82            quit: false,
83            detail_tab: DetailTab::Content,
84            detail_scroll: 0,
85            stats: None,
86            graph: None,
87            graph_sel: 0,
88            entity_data: None,
89            temporal_data: None,
90            active_panel: 0,
91            total_mems: 0,
92            db,
93        }
94    }
95
96    // ── CARGA ──
97    pub fn load(&mut self) {
98        let s = self.db.memories();
99        if self.search.is_empty() {
100            self.memories = s
101                .list(&self.project, None, None, None, 500, 0)
102                .unwrap_or_default();
103        } else {
104            let q = SearchQuery {
105                text: self.search.clone(),
106                project: Some(self.project.clone()),
107                scope: None,
108                memory_type: None,
109                importance: None,
110                tags: vec![],
111                limit: 200,
112                include_snippet: false,
113                all_projects: false,
114            };
115            self.memories = s
116                .search(&q, &crate::store::search::SearchWeights::default(), None)
117                .unwrap_or_default()
118                .into_iter()
119                .map(|r| r.memory)
120                .collect();
121        }
122        self.selected = self.selected.min(self.memories.len().saturating_sub(1));
123        self.scroll = 0;
124        self.detail_scroll = 0;
125        self.stats = s.stats(&self.project).ok();
126        self.total_mems = self.stats.as_ref().map(|s| s.total_memories).unwrap_or(0);
127        self.sessions = self
128            .db
129            .sessions()
130            .list(&self.project, 50)
131            .unwrap_or_default();
132    }
133
134    // ── NAV ──
135    pub fn down(&mut self) {
136        if !self.memories.is_empty() {
137            self.selected = (self.selected + 1).min(self.memories.len() - 1);
138            if self.selected >= self.scroll + 20 {
139                self.scroll += 1;
140            }
141        }
142    }
143    pub fn up(&mut self) {
144        self.selected = self.selected.saturating_sub(1);
145        if self.selected < self.scroll {
146            self.scroll = self.scroll.saturating_sub(1);
147        }
148    }
149    pub fn first(&mut self) {
150        self.selected = 0;
151        self.scroll = 0;
152    }
153    pub fn last(&mut self) {
154        if !self.memories.is_empty() {
155            self.selected = self.memories.len() - 1;
156            self.scroll = self.selected.saturating_sub(19);
157        }
158    }
159    pub fn pgdn(&mut self) {
160        if !self.memories.is_empty() {
161            self.selected = (self.selected + 20).min(self.memories.len() - 1);
162            if self.selected >= self.scroll + 20 {
163                self.scroll += 20;
164            }
165        }
166    }
167    pub fn pgup(&mut self) {
168        self.selected = self.selected.saturating_sub(20);
169        if self.selected < self.scroll {
170            self.scroll = self.scroll.saturating_sub(20);
171        }
172    }
173    pub fn sel(&self) -> Option<&Memory> {
174        self.memories.get(self.selected)
175    }
176
177    // ── DETAIL ──
178    pub fn tab_next(&mut self) {
179        self.detail_tab = self.detail_tab.next();
180        self.detail_scroll = 0;
181    }
182    pub fn tab_prev(&mut self) {
183        self.detail_tab = self.detail_tab.prev();
184        self.detail_scroll = 0;
185    }
186    pub fn dscroll_down(&mut self) {
187        self.detail_scroll += 3;
188    }
189    pub fn dscroll_up(&mut self) {
190        self.detail_scroll = self.detail_scroll.saturating_sub(3);
191    }
192
193    // ── ACTIONS ──
194    pub fn delete_sel(&mut self) {
195        if let Some(m) = self.sel() {
196            self.db.memories().delete(m.id, false).ok();
197            self.status_msg = Some("🗑 Deleted".into());
198            self.load();
199        }
200    }
201    pub fn load_graph(&mut self) {
202        self.graph = self.db.memories().get_graph(&self.project).ok();
203        self.graph_sel = 0;
204    }
205    pub fn graph_next(&mut self) {
206        if let Some(ref d) = self.graph {
207            if !d.nodes.is_empty() {
208                self.graph_sel = (self.graph_sel + 1) % d.nodes.len();
209            }
210        }
211    }
212    pub fn graph_prev(&mut self) {
213        if let Some(ref d) = self.graph {
214            if !d.nodes.is_empty() {
215                self.graph_sel = self.graph_sel.checked_sub(1).unwrap_or(d.nodes.len() - 1);
216            }
217        }
218    }
219    pub fn load_entity(&mut self) {
220        self.entity_data = self.db.entities().frequent_entities(&self.project, 30).ok();
221    }
222    pub fn load_temporal(&mut self) {
223        self.temporal_data = Some((
224            self.db
225                .memories()
226                .list(&self.project, None, None, None, 500, 0)
227                .unwrap_or_default(),
228            0,
229        ));
230    }
231    pub fn temporal_cycle(&mut self) {
232        if let Some(ref mut td) = self.temporal_data {
233            td.1 = (td.1 + 1) % 3;
234        }
235    }
236}