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 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 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 pub fn tick(&mut self) -> bool {
135 let mut changed = false;
136
137 if self.preview.should_trigger() {
139 self.request_preview();
140 changed = true;
141 }
142
143 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 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 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 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, ¤t_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 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 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 self.parent.clear();
293 }
294 }
295}