runa_tui/
app.rs

1pub mod actions;
2mod handlers;
3mod nav;
4mod parent;
5mod preview;
6
7pub use nav::NavState;
8pub use parent::ParentState;
9pub use preview::{PreviewData, PreviewState};
10
11use crate::app::actions::ActionContext;
12use crate::config::Config;
13use crate::keymap::{Action, Keymap, SystemAction};
14use crate::worker::{WorkerResponse, WorkerTask, start_worker};
15use crossbeam_channel::{Receiver, Sender, unbounded};
16use crossterm::event::KeyEvent;
17use std::sync::Arc;
18use std::time::Instant;
19
20pub enum KeypressResult {
21    Continue,
22    Consumed,
23    Quit,
24    OpenedEditor,
25}
26
27#[derive(Debug, Clone, Copy)]
28pub struct LayoutMetrics {
29    pub parent_width: usize,
30    pub main_width: usize,
31    pub preview_width: usize,
32    pub preview_height: usize,
33}
34
35impl Default for LayoutMetrics {
36    fn default() -> Self {
37        Self {
38            parent_width: 20,
39            main_width: 40,
40            preview_width: 40,
41            preview_height: 50,
42        }
43    }
44}
45
46pub struct AppState<'a> {
47    config: &'a Config,
48    keymap: Keymap,
49
50    metrics: LayoutMetrics,
51
52    nav: NavState,
53    actions: ActionContext,
54    preview: PreviewState,
55    parent: ParentState,
56
57    worker_tx: Sender<WorkerTask>,
58    response_rx: Receiver<WorkerResponse>,
59    is_loading: bool,
60
61    notification_time: Option<Instant>,
62}
63
64impl<'a> AppState<'a> {
65    pub fn new(config: &'a Config) -> std::io::Result<Self> {
66        let (worker_tx, task_rx) = unbounded::<WorkerTask>();
67        let (res_tx, response_rx) = unbounded::<WorkerResponse>();
68        let current_dir = std::env::current_dir()?;
69
70        start_worker(task_rx, res_tx);
71
72        let mut app = Self {
73            config,
74            keymap: Keymap::from_config(config),
75            metrics: LayoutMetrics::default(),
76            nav: NavState::new(current_dir),
77            actions: ActionContext::default(),
78            preview: PreviewState::default(),
79            parent: ParentState::default(),
80            worker_tx,
81            response_rx,
82            is_loading: false,
83            notification_time: None,
84        };
85
86        app.request_dir_load(None);
87        app.request_parent_content();
88        Ok(app)
89    }
90
91    // Getters/ accessors
92    pub fn config(&self) -> &Config {
93        self.config
94    }
95
96    pub fn metrics_mut(&mut self) -> &mut LayoutMetrics {
97        &mut self.metrics
98    }
99
100    pub fn nav(&self) -> &NavState {
101        &self.nav
102    }
103
104    pub fn actions(&self) -> &ActionContext {
105        &self.actions
106    }
107
108    pub fn preview(&self) -> &PreviewState {
109        &self.preview
110    }
111
112    pub fn parent(&self) -> &ParentState {
113        &self.parent
114    }
115
116    pub fn notification_time(&self) -> &Option<Instant> {
117        &self.notification_time
118    }
119
120    // mutators
121
122    pub fn visible_selected(&self) -> Option<usize> {
123        if self.nav.entries().is_empty() {
124            None
125        } else {
126            Some(self.nav.selected_idx())
127        }
128    }
129    pub fn has_visible_entries(&self) -> bool {
130        !self.nav.entries().is_empty()
131    }
132
133    /// The heart of the app: updates state and handles worker messages
134    pub fn tick(&mut self) -> bool {
135        let mut changed = false;
136
137        // Handle preview debounc
138        if self.preview.should_trigger() {
139            self.request_preview();
140            changed = true;
141        }
142
143        // Process worker response
144        while let Ok(response) = self.response_rx.try_recv() {
145            changed = true;
146            match response {
147                WorkerResponse::DirectoryLoaded {
148                    path,
149                    entries,
150                    focus,
151                    request_id,
152                } => {
153                    // only update nav if BOTH the ID and path match.
154                    if request_id == self.nav.request_id() && path == self.nav.current_dir() {
155                        self.nav.update_from_worker(path, entries, focus);
156                        self.is_loading = false;
157                        self.request_preview();
158                        self.request_parent_content();
159                    }
160                    // PREVIEW CHECK: Must match the current preview request
161                    else if request_id == self.preview.request_id() {
162                        self.preview.update_from_entries(entries, request_id);
163                        if let Some(entry) = self.nav.selected_entry() {
164                            let path = self.nav.current_dir().join(entry.name());
165                            if let Some(&cached_pos) = self.nav.get_position().get(&path) {
166                                self.preview.set_selected_idx(cached_pos);
167                            } else {
168                                self.preview.set_selected_idx(0);
169                            }
170                        }
171                    }
172                    // PARENT CHECK: Must match the current parent request
173                    else if request_id == self.parent.request_id() {
174                        let current_name = self
175                            .nav
176                            .current_dir()
177                            .file_name()
178                            .map(|n| n.to_string_lossy().to_string())
179                            .unwrap_or_default();
180
181                        self.parent
182                            .update_from_entries(entries, &current_name, request_id);
183                    }
184                }
185                WorkerResponse::PreviewLoaded { lines, request_id } => {
186                    self.preview.update_content(lines, request_id);
187                }
188
189                WorkerResponse::OperationComplete {
190                    message: _,
191                    request_id: _,
192                    need_reload,
193                    focus,
194                } => {
195                    if need_reload {
196                        self.request_dir_load(focus);
197                        self.request_parent_content();
198                    }
199                }
200
201                WorkerResponse::Error(e) => {
202                    self.preview.set_error(e);
203                }
204            }
205        }
206        changed
207    }
208
209    // Central key handlers
210    pub fn handle_keypress(&mut self, key: KeyEvent) -> KeypressResult {
211        if self.actions.is_input_mode() {
212            return self.handle_input_mode(key);
213        }
214
215        if let Some(action) = self.keymap.lookup(key) {
216            match action {
217                Action::System(SystemAction::Quit) => return KeypressResult::Quit,
218                Action::Nav(nav_act) => return self.handle_nav_action(nav_act),
219                Action::File(file_act) => return self.handle_file_action(file_act),
220            }
221        }
222
223        KeypressResult::Continue
224    }
225
226    // Worker requests functions
227    pub fn request_dir_load(&mut self, focus: Option<std::ffi::OsString>) {
228        self.is_loading = true;
229        let request_id = self.nav.prepare_new_request();
230        let _ = self.worker_tx.send(WorkerTask::LoadDirectory {
231            path: self.nav.current_dir().to_path_buf(),
232            focus,
233            dirs_first: self.config.dirs_first(),
234            show_hidden: self.config.show_hidden(),
235            show_system: self.config.show_system(),
236            case_insensitive: self.config.case_insensitive(),
237            always_show: Arc::clone(self.config.always_show()),
238            pane_width: self.metrics.main_width,
239            request_id,
240        });
241    }
242
243    pub fn request_preview(&mut self) {
244        if let Some(entry) = self.nav.selected_shown_entry() {
245            let path = self.nav.current_dir().join(entry.name());
246            let req_id = self.preview.prepare_new_request(path.clone());
247
248            if entry.is_dir() {
249                let _ = self.worker_tx.send(WorkerTask::LoadDirectory {
250                    path,
251                    focus: None,
252                    dirs_first: self.config.dirs_first(),
253                    show_hidden: self.config.show_hidden(),
254                    show_system: self.config.show_system(),
255                    case_insensitive: self.config.case_insensitive(),
256                    always_show: Arc::clone(self.config.always_show()),
257                    pane_width: self.metrics.preview_width,
258                    request_id: req_id,
259                });
260            } else {
261                let _ = self.worker_tx.send(WorkerTask::LoadPreview {
262                    path,
263                    max_lines: self.metrics.preview_height,
264                    pane_width: self.metrics.preview_width,
265                    request_id: req_id,
266                });
267            }
268        }
269    }
270
271    pub fn request_parent_content(&mut self) {
272        if let Some(parent_path) = self.nav.current_dir().parent() {
273            let parent_path_buf = parent_path.to_path_buf();
274
275            if self.parent.should_request(&parent_path_buf) {
276                let req_id = self.parent.prepare_new_request(parent_path_buf.clone());
277
278                let _ = self.worker_tx.send(WorkerTask::LoadDirectory {
279                    path: parent_path_buf,
280                    focus: None,
281                    dirs_first: self.config.dirs_first(),
282                    show_hidden: self.config.show_hidden(),
283                    show_system: self.config.show_system(),
284                    case_insensitive: self.config.case_insensitive(),
285                    always_show: Arc::clone(self.config.always_show()),
286                    pane_width: self.metrics.parent_width,
287                    request_id: req_id,
288                });
289            }
290        } else {
291            // at root.
292            self.parent.clear();
293        }
294    }
295}