Rust · 12836 bytes Raw Blame History
1 mod app;
2 mod cli;
3 mod error;
4 mod git;
5 mod tree;
6 mod types;
7 mod ui;
8
9 use crate::app::App;
10 use crate::cli::Args;
11 use crate::error::Result;
12 use crate::types::{AppMode, InputMode};
13
14 use crossterm::{
15 event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers},
16 execute,
17 terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
18 };
19 use ratatui::{backend::CrosstermBackend, Terminal};
20 use std::io;
21
22 fn main() -> Result<()> {
23 let args = Args::parse_args();
24
25 // Create app
26 let mut app = App::new(args.show_all_files())?;
27
28 if app.items.is_empty() {
29 println!("No files to display");
30 return Ok(());
31 }
32
33 if args.is_interactive() {
34 run_interactive(&mut app)?;
35 } else {
36 ui::print_tree(&app);
37 }
38
39 Ok(())
40 }
41
42 /// Run the interactive TUI
43 fn run_interactive(app: &mut App) -> Result<()> {
44 // Setup terminal
45 enable_raw_mode()?;
46 let mut stdout = io::stdout();
47 execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
48 let backend = CrosstermBackend::new(stdout);
49 let mut terminal = Terminal::new(backend)?;
50
51 // Run event loop
52 let result = run_event_loop(&mut terminal, app);
53
54 // Restore terminal
55 disable_raw_mode()?;
56 execute!(
57 terminal.backend_mut(),
58 LeaveAlternateScreen,
59 DisableMouseCapture
60 )?;
61 terminal.show_cursor()?;
62
63 // Print final tree state
64 println!(".");
65 ui::print_tree(app);
66
67 result
68 }
69
70 /// Main event loop
71 fn run_event_loop(
72 terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
73 app: &mut App,
74 ) -> Result<()> {
75 loop {
76 // Draw UI
77 terminal.draw(|frame| ui::draw(frame, app))?;
78
79 // Handle events
80 if event::poll(std::time::Duration::from_millis(100))? {
81 if let Event::Key(key) = event::read()? {
82 // Handle Ctrl+Q to quit from any mode
83 if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('q')
84 {
85 app.should_quit = true;
86 }
87
88 // Handle key based on current input mode
89 match &app.input_mode {
90 InputMode::Navigation => handle_navigation_key(app, key.code, key.modifiers)?,
91 InputMode::Rename { .. } => handle_rename_key(app, key.code)?,
92 InputMode::Commit { .. } => handle_commit_key(app, key.code)?,
93 InputMode::Search { .. } => handle_search_key(app, key.code)?,
94 InputMode::Push { .. } => handle_push_key(app, key.code)?,
95 InputMode::Confirm { .. } => handle_confirm_key(app, key.code)?,
96 }
97
98 if app.should_quit {
99 break;
100 }
101 }
102 }
103 }
104
105 Ok(())
106 }
107
108 /// Handle keys in navigation mode
109 fn handle_navigation_key(app: &mut App, code: KeyCode, modifiers: KeyModifiers) -> Result<()> {
110 // Check search timeout on every key
111 app.check_search_timeout();
112
113 // Check for Alt+key combinations
114 if modifiers.contains(KeyModifiers::ALT) {
115 match code {
116 KeyCode::Char('g') => app.toggle_mode(),
117 KeyCode::Char('n') => app.enter_rename_mode(),
118 _ => {}
119 }
120 return Ok(());
121 }
122
123 // In normal mode, handle fuzzy search for printable characters
124 if app.mode == AppMode::Normal {
125 match code {
126 // Fuzzy search - letters, numbers, and common filename chars
127 KeyCode::Char(c) if c.is_ascii_alphanumeric() || c == '_' || c == '-' => {
128 app.search_add_char(c);
129 return Ok(());
130 }
131 // Backspace removes from search buffer
132 KeyCode::Backspace => {
133 if !app.search_buffer.is_empty() {
134 app.search_backspace();
135 return Ok(());
136 }
137 }
138 // ESC clears search buffer in normal mode
139 KeyCode::Esc => {
140 if !app.search_buffer.is_empty() {
141 app.clear_search();
142 return Ok(());
143 }
144 }
145 _ => {}
146 }
147 }
148
149 match code {
150 // Navigation (clears search buffer)
151 KeyCode::Char('j') | KeyCode::Down => {
152 app.clear_search();
153 app.navigate_down();
154 }
155 KeyCode::Char('k') | KeyCode::Up => {
156 app.clear_search();
157 app.navigate_up();
158 }
159 KeyCode::Left => {
160 app.clear_search();
161 app.navigate_left();
162 }
163 KeyCode::Right => {
164 app.clear_search();
165 app.navigate_right();
166 }
167 KeyCode::Char(' ') => {
168 app.clear_search();
169 app.toggle_selected();
170 }
171 KeyCode::Char('.') => app.toggle_dotfiles(),
172
173 // Mode switching
174 KeyCode::Char('q') | KeyCode::Char('Q') if app.mode == AppMode::Git => {
175 app.mode = AppMode::Normal;
176 }
177 KeyCode::Esc if app.mode == AppMode::Git => {
178 app.mode = AppMode::Normal;
179 }
180
181 // Git mode commands
182 KeyCode::Char('a') if app.mode == AppMode::Git => {
183 app.stage_selected()?;
184 }
185 KeyCode::Char('u') if app.mode == AppMode::Git => {
186 app.unstage_selected()?;
187 }
188 KeyCode::Char('S') if app.mode == AppMode::Git => {
189 app.stage_all()?;
190 }
191 KeyCode::Char('U') if app.mode == AppMode::Git => {
192 app.unstage_all()?;
193 }
194 KeyCode::Char('x') | KeyCode::Char('X') if app.mode == AppMode::Git => {
195 app.discard_selected()?;
196 }
197 KeyCode::Char('r') if app.mode == AppMode::Git => {
198 app.delete_selected()?;
199 }
200 KeyCode::Char('f') if app.mode == AppMode::Git => {
201 match app.fetch() {
202 Ok(()) => {}
203 Err(e) => app.set_status(format!("Fetch failed: {}", e)),
204 }
205 }
206 KeyCode::Char('l') if app.mode == AppMode::Git => {
207 match app.pull() {
208 Ok(()) => {}
209 Err(e) => app.set_status(format!("Pull failed: {}", e)),
210 }
211 }
212 KeyCode::Char('p') if app.mode == AppMode::Git => {
213 match app.push() {
214 Ok(()) => {}
215 Err(e) => app.set_status(format!("Push failed: {}", e)),
216 }
217 }
218 KeyCode::Char('m') if app.mode == AppMode::Git => {
219 app.enter_commit_mode(false);
220 }
221 KeyCode::Char('M') if app.mode == AppMode::Git => {
222 app.enter_commit_mode(true); // amend
223 }
224
225 _ => {}
226 }
227
228 Ok(())
229 }
230
231 /// Handle keys in rename mode
232 fn handle_rename_key(app: &mut App, code: KeyCode) -> Result<()> {
233 match code {
234 KeyCode::Esc => app.cancel_rename(),
235 KeyCode::Tab | KeyCode::Enter => app.apply_rename()?,
236 KeyCode::Backspace => {
237 if let InputMode::Rename { buffer, cursor } = &mut app.input_mode {
238 if *cursor > 0 {
239 buffer.remove(*cursor - 1);
240 *cursor -= 1;
241 }
242 }
243 }
244 KeyCode::Left => {
245 if let InputMode::Rename { cursor, .. } = &mut app.input_mode {
246 if *cursor > 0 {
247 *cursor -= 1;
248 }
249 }
250 }
251 KeyCode::Right => {
252 if let InputMode::Rename { buffer, cursor } = &mut app.input_mode {
253 if *cursor < buffer.len() {
254 *cursor += 1;
255 }
256 }
257 }
258 KeyCode::Char(c) => {
259 if let InputMode::Rename { buffer, cursor } = &mut app.input_mode {
260 buffer.insert(*cursor, c);
261 *cursor += 1;
262 }
263 }
264 _ => {}
265 }
266 Ok(())
267 }
268
269 /// Handle keys in commit mode
270 fn handle_commit_key(app: &mut App, code: KeyCode) -> Result<()> {
271 use crate::types::CommitStatus;
272
273 // Get current status
274 let status = if let InputMode::Commit { status, .. } = &app.input_mode {
275 status.clone()
276 } else {
277 return Ok(());
278 };
279
280 match status {
281 CommitStatus::Editing => {
282 // Normal editing mode
283 match code {
284 KeyCode::Esc => app.cancel_commit(),
285 KeyCode::Enter => app.apply_commit()?,
286 KeyCode::Backspace => {
287 if let InputMode::Commit { buffer, cursor, .. } = &mut app.input_mode {
288 if *cursor > 0 {
289 buffer.remove(*cursor - 1);
290 *cursor -= 1;
291 }
292 }
293 }
294 KeyCode::Left => {
295 if let InputMode::Commit { cursor, .. } = &mut app.input_mode {
296 if *cursor > 0 {
297 *cursor -= 1;
298 }
299 }
300 }
301 KeyCode::Right => {
302 if let InputMode::Commit { buffer, cursor, .. } = &mut app.input_mode {
303 if *cursor < buffer.len() {
304 *cursor += 1;
305 }
306 }
307 }
308 KeyCode::Char(c) => {
309 if let InputMode::Commit { buffer, cursor, .. } = &mut app.input_mode {
310 buffer.insert(*cursor, c);
311 *cursor += 1;
312 }
313 }
314 _ => {}
315 }
316 }
317 CommitStatus::Committing => {
318 // Don't respond to keys while committing
319 }
320 CommitStatus::Success | CommitStatus::Failed => {
321 // Any key closes the modal
322 app.close_commit();
323 }
324 }
325 Ok(())
326 }
327
328 /// Handle keys in push mode
329 fn handle_push_key(app: &mut App, code: KeyCode) -> Result<()> {
330 use crate::types::PushStatus;
331
332 // Get current status
333 let status = if let InputMode::Push { status, .. } = &app.input_mode {
334 status.clone()
335 } else {
336 return Ok(());
337 };
338
339 match status {
340 PushStatus::SelectRemote => {
341 match code {
342 KeyCode::Esc => app.close_push(),
343 KeyCode::Char('j') | KeyCode::Down => {
344 if let InputMode::Push { remotes, selected, .. } = &mut app.input_mode {
345 if *selected + 1 < remotes.len() {
346 *selected += 1;
347 }
348 }
349 }
350 KeyCode::Char('k') | KeyCode::Up => {
351 if let InputMode::Push { selected, .. } = &mut app.input_mode {
352 if *selected > 0 {
353 *selected -= 1;
354 }
355 }
356 }
357 KeyCode::Enter => {
358 // Get the selected remote and push
359 let remote = if let InputMode::Push { remotes, selected, .. } = &app.input_mode {
360 remotes.get(*selected).cloned()
361 } else {
362 None
363 };
364
365 if let Some(remote) = remote {
366 app.push_to_remote(&remote)?;
367 }
368 }
369 _ => {}
370 }
371 }
372 PushStatus::Pushing => {
373 // Don't respond to keys while pushing
374 }
375 PushStatus::Success | PushStatus::Failed(_) => {
376 // Any key closes the modal
377 app.close_push();
378 }
379 }
380
381 Ok(())
382 }
383
384 /// Handle keys in search mode (legacy - not currently used)
385 fn handle_search_key(app: &mut App, code: KeyCode) -> Result<()> {
386 match code {
387 KeyCode::Esc => {
388 app.input_mode = InputMode::Navigation;
389 }
390 KeyCode::Backspace => {
391 if let InputMode::Search { buffer } = &mut app.input_mode {
392 buffer.pop();
393 }
394 }
395 KeyCode::Char(c) => {
396 if let InputMode::Search { buffer } = &mut app.input_mode {
397 buffer.push(c);
398 }
399 }
400 _ => {}
401 }
402 Ok(())
403 }
404
405 /// Handle keys in confirm mode
406 fn handle_confirm_key(app: &mut App, code: KeyCode) -> Result<()> {
407 match code {
408 KeyCode::Char('y') | KeyCode::Char('Y') => {
409 // Execute the confirmed action
410 // TODO: Implement confirmation actions
411 app.input_mode = InputMode::Navigation;
412 }
413 KeyCode::Esc | KeyCode::Char('n') | KeyCode::Char('N') => {
414 app.input_mode = InputMode::Navigation;
415 }
416 _ => {}
417 }
418 Ok(())
419 }
420