Rust · 22007 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::Pull { .. } => handle_pull_key(app, key.code)?,
96 InputMode::Fetch { .. } => handle_fetch_key(app, key.code)?,
97 InputMode::Tag { .. } => handle_tag_key(app, key.code)?,
98 InputMode::Confirm { .. } => handle_confirm_key(app, key.code)?,
99 }
100
101 if app.should_quit {
102 break;
103 }
104 }
105 }
106 }
107
108 Ok(())
109 }
110
111 /// Handle keys in navigation mode
112 fn handle_navigation_key(app: &mut App, code: KeyCode, modifiers: KeyModifiers) -> Result<()> {
113 // Check search timeout on every key
114 app.check_search_timeout();
115
116 // Check for Alt+key combinations
117 if modifiers.contains(KeyModifiers::ALT) {
118 match code {
119 KeyCode::Char('g') => app.toggle_mode(),
120 KeyCode::Char('n') => app.enter_rename_mode(),
121 _ => {}
122 }
123 return Ok(());
124 }
125
126 // In normal mode, handle fuzzy search for printable characters
127 if app.mode == AppMode::Normal {
128 match code {
129 // Fuzzy search - letters, numbers, and common filename chars
130 KeyCode::Char(c) if c.is_ascii_alphanumeric() || c == '_' || c == '-' => {
131 app.search_add_char(c);
132 return Ok(());
133 }
134 // Backspace removes from search buffer
135 KeyCode::Backspace => {
136 if !app.search_buffer.is_empty() {
137 app.search_backspace();
138 return Ok(());
139 }
140 }
141 // ESC clears search buffer in normal mode
142 KeyCode::Esc => {
143 if !app.search_buffer.is_empty() {
144 app.clear_search();
145 return Ok(());
146 }
147 }
148 _ => {}
149 }
150 }
151
152 match code {
153 // Navigation (clears search buffer)
154 KeyCode::Char('j') | KeyCode::Down => {
155 app.clear_search();
156 app.navigate_down();
157 }
158 KeyCode::Char('k') | KeyCode::Up => {
159 app.clear_search();
160 app.navigate_up();
161 }
162 KeyCode::Left => {
163 app.clear_search();
164 app.navigate_left();
165 }
166 KeyCode::Right => {
167 app.clear_search();
168 app.navigate_right();
169 }
170 KeyCode::Char(' ') => {
171 app.clear_search();
172 app.toggle_selected();
173 }
174 KeyCode::Char('.') => app.toggle_dotfiles(),
175
176 // Mode switching
177 KeyCode::Char('q') | KeyCode::Char('Q') if app.mode == AppMode::Git => {
178 app.mode = AppMode::Normal;
179 }
180 KeyCode::Esc if app.mode == AppMode::Git => {
181 app.mode = AppMode::Normal;
182 }
183
184 // Git mode commands
185 KeyCode::Char('a') if app.mode == AppMode::Git => {
186 app.stage_selected()?;
187 }
188 KeyCode::Char('u') if app.mode == AppMode::Git => {
189 app.unstage_selected()?;
190 }
191 KeyCode::Char('S') if app.mode == AppMode::Git => {
192 app.stage_all()?;
193 }
194 KeyCode::Char('U') if app.mode == AppMode::Git => {
195 app.unstage_all()?;
196 }
197 KeyCode::Char('x') | KeyCode::Char('X') if app.mode == AppMode::Git => {
198 app.confirm_discard();
199 }
200 KeyCode::Char('r') if app.mode == AppMode::Git => {
201 app.delete_selected()?;
202 }
203 KeyCode::Char('f') if app.mode == AppMode::Git => {
204 match app.fetch() {
205 Ok(()) => {}
206 Err(e) => app.set_status(format!("Fetch failed: {}", e)),
207 }
208 }
209 KeyCode::Char('F') if app.mode == AppMode::Git => {
210 // Debug: Force show fetch modal regardless of remote count
211 app.show_fetch_modal();
212 }
213 KeyCode::Char('l') if app.mode == AppMode::Git => {
214 match app.pull() {
215 Ok(()) => {}
216 Err(e) => app.set_status(format!("Pull failed: {}", e)),
217 }
218 }
219 KeyCode::Char('L') if app.mode == AppMode::Git => {
220 // Debug: Force show pull modal regardless of upstream status
221 app.show_pull_modal();
222 }
223 KeyCode::Char('p') if app.mode == AppMode::Git => {
224 match app.push() {
225 Ok(()) => {}
226 Err(e) => app.set_status(format!("Push failed: {}", e)),
227 }
228 }
229 KeyCode::Char('P') if app.mode == AppMode::Git => {
230 // Debug: Force show push modal regardless of upstream status
231 app.show_push_modal();
232 }
233 KeyCode::Char('m') if app.mode == AppMode::Git => {
234 app.enter_commit_mode(false);
235 }
236 KeyCode::Char('M') if app.mode == AppMode::Git => {
237 app.enter_commit_mode(true); // amend
238 }
239 KeyCode::Char('t') if app.mode == AppMode::Git => {
240 app.enter_tag_mode();
241 }
242
243 _ => {}
244 }
245
246 Ok(())
247 }
248
249 /// Handle keys in rename mode
250 fn handle_rename_key(app: &mut App, code: KeyCode) -> Result<()> {
251 match code {
252 KeyCode::Esc => app.cancel_rename(),
253 KeyCode::Tab | KeyCode::Enter => app.apply_rename()?,
254 KeyCode::Backspace => {
255 if let InputMode::Rename { buffer, cursor } = &mut app.input_mode {
256 if *cursor > 0 {
257 buffer.remove(*cursor - 1);
258 *cursor -= 1;
259 }
260 }
261 }
262 KeyCode::Left => {
263 if let InputMode::Rename { cursor, .. } = &mut app.input_mode {
264 if *cursor > 0 {
265 *cursor -= 1;
266 }
267 }
268 }
269 KeyCode::Right => {
270 if let InputMode::Rename { buffer, cursor } = &mut app.input_mode {
271 if *cursor < buffer.len() {
272 *cursor += 1;
273 }
274 }
275 }
276 KeyCode::Char(c) => {
277 if let InputMode::Rename { buffer, cursor } = &mut app.input_mode {
278 buffer.insert(*cursor, c);
279 *cursor += 1;
280 }
281 }
282 _ => {}
283 }
284 Ok(())
285 }
286
287 /// Handle keys in commit mode
288 fn handle_commit_key(app: &mut App, code: KeyCode) -> Result<()> {
289 use crate::types::CommitStatus;
290
291 // Get current status
292 let status = if let InputMode::Commit { status, .. } = &app.input_mode {
293 status.clone()
294 } else {
295 return Ok(());
296 };
297
298 match status {
299 CommitStatus::Editing => {
300 // Normal editing mode
301 match code {
302 KeyCode::Esc => app.cancel_commit(),
303 KeyCode::Enter => app.apply_commit()?,
304 KeyCode::Backspace => {
305 if let InputMode::Commit { buffer, cursor, .. } = &mut app.input_mode {
306 if *cursor > 0 {
307 buffer.remove(*cursor - 1);
308 *cursor -= 1;
309 }
310 }
311 }
312 KeyCode::Left => {
313 if let InputMode::Commit { cursor, .. } = &mut app.input_mode {
314 if *cursor > 0 {
315 *cursor -= 1;
316 }
317 }
318 }
319 KeyCode::Right => {
320 if let InputMode::Commit { buffer, cursor, .. } = &mut app.input_mode {
321 if *cursor < buffer.len() {
322 *cursor += 1;
323 }
324 }
325 }
326 KeyCode::Char(c) => {
327 if let InputMode::Commit { buffer, cursor, .. } = &mut app.input_mode {
328 buffer.insert(*cursor, c);
329 *cursor += 1;
330 }
331 }
332 _ => {}
333 }
334 }
335 CommitStatus::Committing => {
336 // Don't respond to keys while committing
337 }
338 CommitStatus::Success | CommitStatus::Failed => {
339 // Any key closes the modal
340 app.close_commit();
341 }
342 }
343 Ok(())
344 }
345
346 /// Handle keys in push mode
347 fn handle_push_key(app: &mut App, code: KeyCode) -> Result<()> {
348 use crate::types::PushStatus;
349
350 // Get current status
351 let status = if let InputMode::Push { status, .. } = &app.input_mode {
352 status.clone()
353 } else {
354 return Ok(());
355 };
356
357 match status {
358 PushStatus::SelectRemote => {
359 match code {
360 KeyCode::Esc => app.close_push(),
361 KeyCode::Char('j') | KeyCode::Down => {
362 if let InputMode::Push { remotes, selected, .. } = &mut app.input_mode {
363 if *selected + 1 < remotes.len() {
364 *selected += 1;
365 }
366 }
367 }
368 KeyCode::Char('k') | KeyCode::Up => {
369 if let InputMode::Push { selected, .. } = &mut app.input_mode {
370 if *selected > 0 {
371 *selected -= 1;
372 }
373 }
374 }
375 KeyCode::Enter => {
376 // Get the selected remote and push
377 let remote = if let InputMode::Push { remotes, selected, .. } = &app.input_mode {
378 remotes.get(*selected).cloned()
379 } else {
380 None
381 };
382
383 if let Some(remote) = remote {
384 app.push_to_remote(&remote)?;
385 }
386 }
387 _ => {}
388 }
389 }
390 PushStatus::Pushing => {
391 // Don't respond to keys while pushing
392 }
393 PushStatus::Success | PushStatus::Failed(_) => {
394 // Any key closes the modal
395 app.close_push();
396 }
397 }
398
399 Ok(())
400 }
401
402 /// Handle keys in pull mode
403 fn handle_pull_key(app: &mut App, code: KeyCode) -> Result<()> {
404 use crate::types::PullStatus;
405
406 let status = if let InputMode::Pull { status, .. } = &app.input_mode {
407 status.clone()
408 } else {
409 return Ok(());
410 };
411
412 match status {
413 PullStatus::SelectRemote => {
414 match code {
415 KeyCode::Esc => app.close_pull(),
416 KeyCode::Char('j') | KeyCode::Down => {
417 if let InputMode::Pull { remotes, selected, .. } = &mut app.input_mode {
418 if *selected + 1 < remotes.len() {
419 *selected += 1;
420 }
421 }
422 }
423 KeyCode::Char('k') | KeyCode::Up => {
424 if let InputMode::Pull { selected, .. } = &mut app.input_mode {
425 if *selected > 0 {
426 *selected -= 1;
427 }
428 }
429 }
430 KeyCode::Enter => {
431 let remote = if let InputMode::Pull { remotes, selected, .. } = &app.input_mode {
432 remotes.get(*selected).cloned()
433 } else {
434 None
435 };
436
437 if let Some(remote) = remote {
438 app.pull_from_remote(&remote)?;
439 }
440 }
441 _ => {}
442 }
443 }
444 PullStatus::Pulling => {
445 // Don't respond to keys while pulling
446 }
447 PullStatus::Success | PullStatus::Failed(_) => {
448 // Any key closes the modal
449 app.close_pull();
450 }
451 }
452
453 Ok(())
454 }
455
456 /// Handle keys in fetch mode
457 fn handle_fetch_key(app: &mut App, code: KeyCode) -> Result<()> {
458 use crate::types::FetchStatus;
459
460 let status = if let InputMode::Fetch { status, .. } = &app.input_mode {
461 status.clone()
462 } else {
463 return Ok(());
464 };
465
466 match status {
467 FetchStatus::SelectRemote => {
468 match code {
469 KeyCode::Esc => app.close_fetch(),
470 KeyCode::Char('j') | KeyCode::Down => {
471 if let InputMode::Fetch { remotes, selected, .. } = &mut app.input_mode {
472 if *selected + 1 < remotes.len() {
473 *selected += 1;
474 }
475 }
476 }
477 KeyCode::Char('k') | KeyCode::Up => {
478 if let InputMode::Fetch { selected, .. } = &mut app.input_mode {
479 if *selected > 0 {
480 *selected -= 1;
481 }
482 }
483 }
484 KeyCode::Enter => {
485 let remote = if let InputMode::Fetch { remotes, selected, .. } = &app.input_mode {
486 remotes.get(*selected).cloned()
487 } else {
488 None
489 };
490
491 if let Some(remote) = remote {
492 app.fetch_from_remote(&remote)?;
493 }
494 }
495 _ => {}
496 }
497 }
498 FetchStatus::Fetching => {
499 // Don't respond to keys while fetching
500 }
501 FetchStatus::Success | FetchStatus::Failed(_) => {
502 // Any key closes the modal
503 app.close_fetch();
504 }
505 }
506
507 Ok(())
508 }
509
510 /// Handle keys in tag mode
511 fn handle_tag_key(app: &mut App, code: KeyCode) -> Result<()> {
512 use crate::types::TagStep;
513
514 let step = if let InputMode::Tag { step, .. } = &app.input_mode {
515 step.clone()
516 } else {
517 return Ok(());
518 };
519
520 match step {
521 TagStep::EnterName => {
522 match code {
523 KeyCode::Esc => {
524 app.close_tag(false);
525 }
526 KeyCode::Enter => {
527 // Check if name is non-empty before proceeding
528 let has_name = if let InputMode::Tag { name, .. } = &app.input_mode {
529 !name.trim().is_empty()
530 } else {
531 false
532 };
533
534 if has_name {
535 // Move cursor to message field
536 if let InputMode::Tag { step, cursor, .. } = &mut app.input_mode {
537 *step = TagStep::EnterMessage;
538 *cursor = 0;
539 }
540 }
541 }
542 KeyCode::Backspace => {
543 if let InputMode::Tag { name, cursor, .. } = &mut app.input_mode {
544 if *cursor > 0 {
545 name.remove(*cursor - 1);
546 *cursor -= 1;
547 }
548 }
549 }
550 KeyCode::Left => {
551 if let InputMode::Tag { cursor, .. } = &mut app.input_mode {
552 if *cursor > 0 {
553 *cursor -= 1;
554 }
555 }
556 }
557 KeyCode::Right => {
558 if let InputMode::Tag { name, cursor, .. } = &mut app.input_mode {
559 if *cursor < name.len() {
560 *cursor += 1;
561 }
562 }
563 }
564 KeyCode::Char(c) => {
565 if let InputMode::Tag { name, cursor, .. } = &mut app.input_mode {
566 name.insert(*cursor, c);
567 *cursor += 1;
568 }
569 }
570 _ => {}
571 }
572 }
573 TagStep::EnterMessage => {
574 match code {
575 KeyCode::Esc => {
576 // Go back to name entry
577 if let InputMode::Tag { step, name, cursor, .. } = &mut app.input_mode {
578 *step = TagStep::EnterName;
579 *cursor = name.len();
580 }
581 }
582 KeyCode::Enter => {
583 // Create the tag
584 app.create_tag()?;
585 }
586 KeyCode::Backspace => {
587 if let InputMode::Tag { message, cursor, .. } = &mut app.input_mode {
588 if *cursor > 0 {
589 message.remove(*cursor - 1);
590 *cursor -= 1;
591 }
592 }
593 }
594 KeyCode::Left => {
595 if let InputMode::Tag { cursor, .. } = &mut app.input_mode {
596 if *cursor > 0 {
597 *cursor -= 1;
598 }
599 }
600 }
601 KeyCode::Right => {
602 if let InputMode::Tag { message, cursor, .. } = &mut app.input_mode {
603 if *cursor < message.len() {
604 *cursor += 1;
605 }
606 }
607 }
608 KeyCode::Char(c) => {
609 if let InputMode::Tag { message, cursor, .. } = &mut app.input_mode {
610 message.insert(*cursor, c);
611 *cursor += 1;
612 }
613 }
614 _ => {}
615 }
616 }
617 TagStep::Creating | TagStep::Pushing => {
618 // Don't respond to keys while creating/pushing
619 }
620 TagStep::AskPush => {
621 match code {
622 KeyCode::Char('y') | KeyCode::Char('Y') => {
623 app.push_tag()?;
624 }
625 KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
626 app.close_tag(true);
627 }
628 _ => {}
629 }
630 }
631 TagStep::Success => {
632 // Any key closes
633 app.close_tag(true);
634 }
635 TagStep::Failed(_) => {
636 // Any key closes
637 app.close_tag(false);
638 }
639 }
640
641 Ok(())
642 }
643
644 /// Handle keys in search mode (legacy - not currently used)
645 fn handle_search_key(app: &mut App, code: KeyCode) -> Result<()> {
646 match code {
647 KeyCode::Esc => {
648 app.input_mode = InputMode::Navigation;
649 }
650 KeyCode::Backspace => {
651 if let InputMode::Search { buffer } = &mut app.input_mode {
652 buffer.pop();
653 }
654 }
655 KeyCode::Char(c) => {
656 if let InputMode::Search { buffer } = &mut app.input_mode {
657 buffer.push(c);
658 }
659 }
660 _ => {}
661 }
662 Ok(())
663 }
664
665 /// Handle keys in confirm mode
666 fn handle_confirm_key(app: &mut App, code: KeyCode) -> Result<()> {
667 match code {
668 KeyCode::Char('y') | KeyCode::Char('Y') => {
669 // Extract action and execute
670 if let InputMode::Confirm { action, .. } = &app.input_mode {
671 let action = action.clone();
672 app.input_mode = InputMode::Navigation;
673 app.execute_confirm_action(&action)?;
674 }
675 }
676 KeyCode::Esc | KeyCode::Char('n') | KeyCode::Char('N') => {
677 app.input_mode = InputMode::Navigation;
678 }
679 _ => {}
680 }
681 Ok(())
682 }
683