tenseleyflow/fussr / 5212888

Browse files

implement fuzzy jumping for faster staging. fix commit modalss

Authored by espadonne
SHA
52128880944c5d54ce5ee3f45c010314637d4029
Parents
f782186
Tree
d18cf37

5 changed files

StatusFile+-
M src/app.rs 203 34
M src/git.rs 30 20
M src/main.rs 110 50
M src/types.rs 10 1
M src/ui.rs 66 18
src/app.rsmodified
@@ -3,6 +3,7 @@ use crate::git::GitRepo;
33
 use crate::tree::{build_tree, flatten_tree, toggle_expanded};
44
 use crate::types::{AppMode, FileEntry, InputMode, SelectableItem, TreeNode};
55
 use std::path::PathBuf;
6
+use std::time::Instant;
67
 
78
 /// Main application state
89
 pub struct App {
@@ -32,6 +33,10 @@ pub struct App {
3233
     pub should_quit: bool,
3334
     /// Status message to display
3435
     pub status_message: Option<String>,
36
+    /// Fuzzy search buffer
37
+    pub search_buffer: String,
38
+    /// Last search keystroke time (for timeout)
39
+    pub last_search_time: Option<Instant>,
3540
 }
3641
 
3742
 impl App {
@@ -55,6 +60,8 @@ impl App {
5560
             input_mode: InputMode::Navigation,
5661
             should_quit: false,
5762
             status_message: None,
63
+            search_buffer: String::new(),
64
+            last_search_time: None,
5865
         };
5966
 
6067
         app.refresh_files()?;
@@ -411,84 +418,246 @@ impl App {
411418
             buffer: initial_buffer,
412419
             cursor,
413420
             amend,
421
+            status: crate::types::CommitStatus::Editing,
414422
         };
415423
     }
416424
 
417
-    /// Apply commit
425
+    /// Start the commit process (show "Committing..." state)
426
+    pub fn start_commit(&mut self) {
427
+        if let InputMode::Commit { status, .. } = &mut self.input_mode {
428
+            *status = crate::types::CommitStatus::Committing;
429
+        }
430
+    }
431
+
432
+    /// Apply commit and update status
418433
     pub fn apply_commit(&mut self) -> Result<()> {
419
-        if let InputMode::Commit { buffer, amend, .. } = &self.input_mode {
420
-            let message = buffer.trim();
421
-            if !message.is_empty() {
422
-                if *amend {
423
-                    self.repo.commit_amend(message)?;
424
-                    self.set_status("Commit amended".to_string());
425
-                } else {
426
-                    self.repo.commit(message)?;
427
-                    self.set_status("Committed".to_string());
434
+        // Extract values we need before modifying
435
+        let (message, amend) = if let InputMode::Commit { buffer, amend, .. } = &self.input_mode {
436
+            (buffer.trim().to_string(), *amend)
437
+        } else {
438
+            return Ok(());
439
+        };
440
+
441
+        if message.is_empty() {
442
+            self.input_mode = InputMode::Navigation;
443
+            return Ok(());
444
+        }
445
+
446
+        // Perform the commit
447
+        let result = if amend {
448
+            self.repo.commit_amend(&message)
449
+        } else {
450
+            self.repo.commit(&message)
451
+        };
452
+
453
+        // Update status based on result
454
+        match result {
455
+            Ok(()) => {
456
+                if let InputMode::Commit { status, .. } = &mut self.input_mode {
457
+                    *status = crate::types::CommitStatus::Success;
428458
                 }
459
+                self.set_status("Committed".to_string());
429460
                 self.refresh_files()?;
430461
             }
462
+            Err(_) => {
463
+                if let InputMode::Commit { status, .. } = &mut self.input_mode {
464
+                    *status = crate::types::CommitStatus::Failed;
465
+                }
466
+            }
431467
         }
432
-        self.input_mode = InputMode::Navigation;
468
+
433469
         Ok(())
434470
     }
435471
 
472
+    /// Close commit modal and return to navigation
473
+    pub fn close_commit(&mut self) {
474
+        self.input_mode = InputMode::Navigation;
475
+    }
476
+
436477
     /// Cancel commit mode
437478
     pub fn cancel_commit(&mut self) {
438479
         self.input_mode = InputMode::Navigation;
439480
     }
440481
 
441
-    /// Fuzzy search and jump to match
442
-    pub fn fuzzy_jump(&mut self, pattern: &str) {
443
-        if pattern.is_empty() {
482
+    /// Check if search timeout has elapsed (0.5 seconds) and reset if needed
483
+    pub fn check_search_timeout(&mut self) {
484
+        if let Some(last_time) = self.last_search_time {
485
+            if last_time.elapsed().as_millis() > 500 {
486
+                self.search_buffer.clear();
487
+                self.last_search_time = None;
488
+            }
489
+        }
490
+    }
491
+
492
+    /// Add a character to the search buffer and jump to match
493
+    pub fn search_add_char(&mut self, c: char) {
494
+        // Check timeout first - if elapsed, start fresh
495
+        self.check_search_timeout();
496
+
497
+        // Add character to buffer
498
+        if self.search_buffer.len() < 32 {
499
+            self.search_buffer.push(c);
500
+            self.last_search_time = Some(Instant::now());
501
+
502
+            // Jump to best match
503
+            self.fuzzy_jump_to_match();
504
+        }
505
+    }
506
+
507
+    /// Remove last character from search buffer
508
+    pub fn search_backspace(&mut self) {
509
+        if !self.search_buffer.is_empty() {
510
+            self.search_buffer.pop();
511
+            self.last_search_time = Some(Instant::now());
512
+
513
+            if !self.search_buffer.is_empty() {
514
+                self.fuzzy_jump_to_match();
515
+            }
516
+        }
517
+    }
518
+
519
+    /// Clear the search buffer
520
+    pub fn clear_search(&mut self) {
521
+        self.search_buffer.clear();
522
+        self.last_search_time = None;
523
+    }
524
+
525
+    /// Jump to the best matching item using fzf-style scoring
526
+    fn fuzzy_jump_to_match(&mut self) {
527
+        if self.search_buffer.is_empty() || self.items.is_empty() {
444528
             return;
445529
         }
446530
 
447
-        let pattern_lower = pattern.to_lowercase();
531
+        let pattern = self.search_buffer.to_lowercase();
448532
         let mut best_idx = self.selected;
449
-        let mut best_score = 0;
533
+        let mut best_score: i32 = 0;
534
+
535
+        // Check current item first - if exact match, stay on it
536
+        if let Some(item) = self.items.get(self.selected) {
537
+            let current_score = fuzzy_match_score(&pattern, &item.name.to_lowercase());
538
+            if current_score >= 10000 {
539
+                return; // Exact match - stay here
540
+            }
541
+            best_score = current_score;
542
+        }
543
+
544
+        // PASS 1: Search for basename matches (file/folder names)
545
+        for (i, item) in self.items.iter().enumerate() {
546
+            if i == self.selected {
547
+                continue;
548
+            }
549
+            let score = fuzzy_match_score(&pattern, &item.name.to_lowercase());
550
+            if score > best_score {
551
+                best_score = score;
552
+                best_idx = i;
553
+            }
554
+        }
555
+
556
+        // If we found a good basename match (prefix or exact), use it
557
+        if best_score >= 5000 {
558
+            self.selected = best_idx;
559
+            return;
560
+        }
450561
 
562
+        // PASS 2: Search full paths if no good basename match
451563
         for (i, item) in self.items.iter().enumerate() {
452
-            let score = fuzzy_score(&pattern_lower, &item.name.to_lowercase());
564
+            if i == self.selected {
565
+                continue;
566
+            }
567
+            let path_str = item.path.to_string_lossy().to_lowercase();
568
+            let score = fuzzy_match_score(&pattern, &path_str);
453569
             if score > best_score {
454570
                 best_score = score;
455571
                 best_idx = i;
456572
             }
457573
         }
458574
 
575
+        // Jump to best match if any was found
459576
         if best_score > 0 {
460577
             self.selected = best_idx;
461578
         }
462579
     }
463580
 }
464581
 
465
-/// Simple fuzzy matching score
466
-fn fuzzy_score(pattern: &str, text: &str) -> usize {
582
+/// Fuzzy matching with fzf-style scoring
583
+/// Returns a score (higher is better), 0 means no match
584
+fn fuzzy_match_score(pattern: &str, text: &str) -> i32 {
585
+    if pattern.is_empty() {
586
+        return 1;
587
+    }
588
+
589
+    // Exact match (highest score)
590
+    if pattern == text {
591
+        return 10000;
592
+    }
593
+
594
+    // Prefix match (very high score)
467595
     if text.starts_with(pattern) {
468
-        return 1000 + (100 - text.len()); // Prefix match bonus
596
+        return 5000;
469597
     }
470598
 
471
-    let mut score = 0;
472
-    let mut pattern_idx = 0;
599
+    // Fuzzy match with scoring
473600
     let pattern_chars: Vec<char> = pattern.chars().collect();
474
-    let mut consecutive = 0;
601
+    let text_chars: Vec<char> = text.chars().collect();
602
+
603
+    let mut score: i32 = 0;
604
+    let mut pattern_idx = 0;
605
+    let mut consecutive_bonus: i32 = 0;
606
+    let mut is_consecutive = false;
607
+    let mut match_started = false;
608
+
609
+    for (text_idx, &c) in text_chars.iter().enumerate() {
610
+        if pattern_idx >= pattern_chars.len() {
611
+            break;
612
+        }
613
+
614
+        if c == pattern_chars[pattern_idx] {
615
+            match_started = true;
616
+
617
+            // Base score for each matched character
618
+            score += 100;
619
+
620
+            // Bonus for consecutive characters
621
+            if is_consecutive {
622
+                consecutive_bonus += 1;
623
+                score += consecutive_bonus * 50;
624
+            } else {
625
+                consecutive_bonus = 1;
626
+                is_consecutive = true;
627
+            }
475628
 
476
-    for (i, c) in text.chars().enumerate() {
477
-        if pattern_idx < pattern_chars.len() && c == pattern_chars[pattern_idx] {
478
-            score += 10 + consecutive * 5;
479
-            if i == 0 {
480
-                score += 20; // Start match bonus
629
+            // Bonus for matching at start of text
630
+            if text_idx == 0 {
631
+                score += 200;
632
+            }
633
+
634
+            // Bonus for matching after separator (word boundary)
635
+            if text_idx > 0 {
636
+                let prev = text_chars[text_idx - 1];
637
+                if prev == '/' || prev == '_' || prev == '-' || prev == '.' {
638
+                    score += 150;
639
+                }
481640
             }
482
-            consecutive += 1;
641
+
483642
             pattern_idx += 1;
484643
         } else {
485
-            consecutive = 0;
644
+            // Reset consecutive bonus
645
+            is_consecutive = false;
646
+            consecutive_bonus = 0;
647
+            // Small penalty for gaps
648
+            if match_started {
649
+                score -= 1;
650
+            }
486651
         }
487652
     }
488653
 
489
-    if pattern_idx == pattern_chars.len() {
490
-        score
491
-    } else {
492
-        0 // Not all chars matched
654
+    // No match if we didn't find all pattern characters
655
+    if pattern_idx < pattern_chars.len() {
656
+        return 0;
493657
     }
658
+
659
+    // Penalty for longer strings (prefer concise matches)
660
+    score -= text.len() as i32;
661
+
662
+    score.max(1) // Ensure positive score if we matched
494663
 }
src/git.rsmodified
@@ -276,26 +276,30 @@ impl GitRepo {
276276
         Ok(())
277277
     }
278278
 
279
-    /// Create a commit with the given message
279
+    /// Create a commit with the given message (captures output to not corrupt TUI)
280280
     pub fn commit(&self, message: &str) -> Result<()> {
281
-        let status = Command::new("git")
281
+        let output = Command::new("git")
282282
             .args(["commit", "-m", message])
283
-            .status()?;
283
+            .stdout(std::process::Stdio::piped())
284
+            .stderr(std::process::Stdio::piped())
285
+            .output()?;
284286
 
285
-        if status.success() {
287
+        if output.status.success() {
286288
             Ok(())
287289
         } else {
288290
             Err(FussrError::Git(git2::Error::from_str("Failed to commit")))
289291
         }
290292
     }
291293
 
292
-    /// Amend the last commit
294
+    /// Amend the last commit (captures output to not corrupt TUI)
293295
     pub fn commit_amend(&self, message: &str) -> Result<()> {
294
-        let status = Command::new("git")
296
+        let output = Command::new("git")
295297
             .args(["commit", "--amend", "-m", message])
296
-            .status()?;
298
+            .stdout(std::process::Stdio::piped())
299
+            .stderr(std::process::Stdio::piped())
300
+            .output()?;
297301
 
298
-        if status.success() {
302
+        if output.status.success() {
299303
             Ok(())
300304
         } else {
301305
             Err(FussrError::Git(git2::Error::from_str("Failed to amend commit")))
@@ -316,39 +320,45 @@ impl GitRepo {
316320
         }
317321
     }
318322
 
319
-    /// Fetch from remote
323
+    /// Fetch from remote (captures output to not corrupt TUI)
320324
     pub fn fetch(&self) -> Result<()> {
321
-        let status = Command::new("git")
325
+        let output = Command::new("git")
322326
             .args(["fetch"])
323
-            .status()?;
327
+            .stdout(std::process::Stdio::piped())
328
+            .stderr(std::process::Stdio::piped())
329
+            .output()?;
324330
 
325
-        if status.success() {
331
+        if output.status.success() {
326332
             Ok(())
327333
         } else {
328334
             Err(FussrError::Git(git2::Error::from_str("Failed to fetch")))
329335
         }
330336
     }
331337
 
332
-    /// Pull from remote
338
+    /// Pull from remote (captures output to not corrupt TUI)
333339
     pub fn pull(&self) -> Result<()> {
334
-        let status = Command::new("git")
340
+        let output = Command::new("git")
335341
             .args(["pull"])
336
-            .status()?;
342
+            .stdout(std::process::Stdio::piped())
343
+            .stderr(std::process::Stdio::piped())
344
+            .output()?;
337345
 
338
-        if status.success() {
346
+        if output.status.success() {
339347
             Ok(())
340348
         } else {
341349
             Err(FussrError::Git(git2::Error::from_str("Failed to pull")))
342350
         }
343351
     }
344352
 
345
-    /// Push to remote
353
+    /// Push to remote (captures output to not corrupt TUI)
346354
     pub fn push(&self) -> Result<()> {
347
-        let status = Command::new("git")
355
+        let output = Command::new("git")
348356
             .args(["push"])
349
-            .status()?;
357
+            .stdout(std::process::Stdio::piped())
358
+            .stderr(std::process::Stdio::piped())
359
+            .output()?;
350360
 
351
-        if status.success() {
361
+        if output.status.success() {
352362
             Ok(())
353363
         } else {
354364
             Err(FussrError::Git(git2::Error::from_str("Failed to push")))
src/main.rsmodified
@@ -106,6 +106,9 @@ fn run_event_loop(
106106
 
107107
 /// Handle keys in navigation mode
108108
 fn handle_navigation_key(app: &mut App, code: KeyCode, modifiers: KeyModifiers) -> Result<()> {
109
+    // Check search timeout on every key
110
+    app.check_search_timeout();
111
+
109112
     // Check for Alt+key combinations
110113
     if modifiers.contains(KeyModifiers::ALT) {
111114
         match code {
@@ -116,13 +119,54 @@ fn handle_navigation_key(app: &mut App, code: KeyCode, modifiers: KeyModifiers)
116119
         return Ok(());
117120
     }
118121
 
122
+    // In normal mode, handle fuzzy search for printable characters
123
+    if app.mode == AppMode::Normal {
124
+        match code {
125
+            // Fuzzy search - letters, numbers, and common filename chars
126
+            KeyCode::Char(c) if c.is_ascii_alphanumeric() || c == '_' || c == '-' => {
127
+                app.search_add_char(c);
128
+                return Ok(());
129
+            }
130
+            // Backspace removes from search buffer
131
+            KeyCode::Backspace => {
132
+                if !app.search_buffer.is_empty() {
133
+                    app.search_backspace();
134
+                    return Ok(());
135
+                }
136
+            }
137
+            // ESC clears search buffer in normal mode
138
+            KeyCode::Esc => {
139
+                if !app.search_buffer.is_empty() {
140
+                    app.clear_search();
141
+                    return Ok(());
142
+                }
143
+            }
144
+            _ => {}
145
+        }
146
+    }
147
+
119148
     match code {
120
-        // Navigation
121
-        KeyCode::Char('j') | KeyCode::Down => app.navigate_down(),
122
-        KeyCode::Char('k') | KeyCode::Up => app.navigate_up(),
123
-        KeyCode::Left => app.navigate_left(),
124
-        KeyCode::Right => app.navigate_right(),
125
-        KeyCode::Char(' ') => app.toggle_selected(),
149
+        // Navigation (clears search buffer)
150
+        KeyCode::Char('j') | KeyCode::Down => {
151
+            app.clear_search();
152
+            app.navigate_down();
153
+        }
154
+        KeyCode::Char('k') | KeyCode::Up => {
155
+            app.clear_search();
156
+            app.navigate_up();
157
+        }
158
+        KeyCode::Left => {
159
+            app.clear_search();
160
+            app.navigate_left();
161
+        }
162
+        KeyCode::Right => {
163
+            app.clear_search();
164
+            app.navigate_right();
165
+        }
166
+        KeyCode::Char(' ') => {
167
+            app.clear_search();
168
+            app.toggle_selected();
169
+        }
126170
         KeyCode::Char('.') => app.toggle_dotfiles(),
127171
 
128172
         // Mode switching
@@ -153,13 +197,22 @@ fn handle_navigation_key(app: &mut App, code: KeyCode, modifiers: KeyModifiers)
153197
             app.delete_selected()?;
154198
         }
155199
         KeyCode::Char('f') if app.mode == AppMode::Git => {
156
-            app.fetch()?;
200
+            match app.fetch() {
201
+                Ok(()) => {}
202
+                Err(e) => app.set_status(format!("Fetch failed: {}", e)),
203
+            }
157204
         }
158205
         KeyCode::Char('l') if app.mode == AppMode::Git => {
159
-            app.pull()?;
206
+            match app.pull() {
207
+                Ok(()) => {}
208
+                Err(e) => app.set_status(format!("Pull failed: {}", e)),
209
+            }
160210
         }
161211
         KeyCode::Char('p') if app.mode == AppMode::Git => {
162
-            app.push()?;
212
+            match app.push() {
213
+                Ok(()) => {}
214
+                Err(e) => app.set_status(format!("Push failed: {}", e)),
215
+            }
163216
         }
164217
         KeyCode::Char('m') if app.mode == AppMode::Git => {
165218
             app.enter_commit_mode(false);
@@ -214,71 +267,78 @@ fn handle_rename_key(app: &mut App, code: KeyCode) -> Result<()> {
214267
 
215268
 /// Handle keys in commit mode
216269
 fn handle_commit_key(app: &mut App, code: KeyCode) -> Result<()> {
217
-    match code {
218
-        KeyCode::Esc => app.cancel_commit(),
219
-        KeyCode::Enter => app.apply_commit()?,
220
-        KeyCode::Backspace => {
221
-            if let InputMode::Commit { buffer, cursor, .. } = &mut app.input_mode {
222
-                if *cursor > 0 {
223
-                    buffer.remove(*cursor - 1);
224
-                    *cursor -= 1;
270
+    use crate::types::CommitStatus;
271
+
272
+    // Get current status
273
+    let status = if let InputMode::Commit { status, .. } = &app.input_mode {
274
+        status.clone()
275
+    } else {
276
+        return Ok(());
277
+    };
278
+
279
+    match status {
280
+        CommitStatus::Editing => {
281
+            // Normal editing mode
282
+            match code {
283
+                KeyCode::Esc => app.cancel_commit(),
284
+                KeyCode::Enter => app.apply_commit()?,
285
+                KeyCode::Backspace => {
286
+                    if let InputMode::Commit { buffer, cursor, .. } = &mut app.input_mode {
287
+                        if *cursor > 0 {
288
+                            buffer.remove(*cursor - 1);
289
+                            *cursor -= 1;
290
+                        }
291
+                    }
225292
                 }
226
-            }
227
-        }
228
-        KeyCode::Left => {
229
-            if let InputMode::Commit { cursor, .. } = &mut app.input_mode {
230
-                if *cursor > 0 {
231
-                    *cursor -= 1;
293
+                KeyCode::Left => {
294
+                    if let InputMode::Commit { cursor, .. } = &mut app.input_mode {
295
+                        if *cursor > 0 {
296
+                            *cursor -= 1;
297
+                        }
298
+                    }
232299
                 }
233
-            }
234
-        }
235
-        KeyCode::Right => {
236
-            if let InputMode::Commit { buffer, cursor, .. } = &mut app.input_mode {
237
-                if *cursor < buffer.len() {
238
-                    *cursor += 1;
300
+                KeyCode::Right => {
301
+                    if let InputMode::Commit { buffer, cursor, .. } = &mut app.input_mode {
302
+                        if *cursor < buffer.len() {
303
+                            *cursor += 1;
304
+                        }
305
+                    }
306
+                }
307
+                KeyCode::Char(c) => {
308
+                    if let InputMode::Commit { buffer, cursor, .. } = &mut app.input_mode {
309
+                        buffer.insert(*cursor, c);
310
+                        *cursor += 1;
311
+                    }
239312
                 }
313
+                _ => {}
240314
             }
241315
         }
242
-        KeyCode::Char(c) => {
243
-            if let InputMode::Commit { buffer, cursor, .. } = &mut app.input_mode {
244
-                buffer.insert(*cursor, c);
245
-                *cursor += 1;
246
-            }
316
+        CommitStatus::Committing => {
317
+            // Don't respond to keys while committing
318
+        }
319
+        CommitStatus::Success | CommitStatus::Failed => {
320
+            // Any key closes the modal
321
+            app.close_commit();
247322
         }
248
-        _ => {}
249323
     }
250324
     Ok(())
251325
 }
252326
 
253
-/// Handle keys in search mode
327
+/// Handle keys in search mode (legacy - not currently used)
254328
 fn handle_search_key(app: &mut App, code: KeyCode) -> Result<()> {
255329
     match code {
256330
         KeyCode::Esc => {
257331
             app.input_mode = InputMode::Navigation;
258332
         }
259333
         KeyCode::Backspace => {
260
-            // Extract, modify, and reassign to avoid borrow conflicts
261334
             if let InputMode::Search { buffer } = &mut app.input_mode {
262335
                 buffer.pop();
263336
             }
264
-            // Now do fuzzy jump with cloned buffer
265
-            if let InputMode::Search { buffer } = &app.input_mode {
266
-                if !buffer.is_empty() {
267
-                    let pattern = buffer.clone();
268
-                    app.fuzzy_jump(&pattern);
269
-                }
270
-            }
271337
         }
272338
         KeyCode::Char(c) => {
273
-            // Extract, modify, and reassign to avoid borrow conflicts
274339
             if let InputMode::Search { buffer } = &mut app.input_mode {
275340
                 buffer.push(c);
276341
             }
277
-            // Now do fuzzy jump with cloned buffer
278
-            if let InputMode::Search { buffer } = &app.input_mode {
279
-                let pattern = buffer.clone();
280
-                app.fuzzy_jump(&pattern);
281
-            }
282342
         }
283343
         _ => {}
284344
     }
src/types.rsmodified
@@ -171,13 +171,22 @@ impl AppMode {
171171
     }
172172
 }
173173
 
174
+/// Status of commit operation
175
+#[derive(Debug, Clone, PartialEq, Eq)]
176
+pub enum CommitStatus {
177
+    Editing,
178
+    Committing,
179
+    Success,
180
+    Failed,
181
+}
182
+
174183
 /// Input mode for special states
175184
 #[derive(Debug, Clone, PartialEq, Eq)]
176185
 pub enum InputMode {
177186
     Navigation,
178187
     Rename { buffer: String, cursor: usize },
179188
     Search { buffer: String },
180
-    Commit { buffer: String, cursor: usize, amend: bool },
189
+    Commit { buffer: String, cursor: usize, amend: bool, status: CommitStatus },
181190
     Confirm { message: String, action: ConfirmAction },
182191
 }
183192
 
src/ui.rsmodified
@@ -1,5 +1,5 @@
11
 use crate::app::App;
2
-use crate::types::{AppMode, InputMode, SelectableItem};
2
+use crate::types::{AppMode, CommitStatus, InputMode, SelectableItem};
33
 use ratatui::{
44
     layout::{Constraint, Direction, Layout, Rect},
55
     style::{Color, Modifier, Style},
@@ -24,13 +24,13 @@ pub fn draw(frame: &mut Frame, app: &App) {
2424
     draw_help(frame, app, chunks[2]);
2525
 
2626
     // Draw modal overlay if in commit mode
27
-    if let InputMode::Commit { buffer, cursor, amend } = &app.input_mode {
28
-        draw_commit_modal(frame, buffer, *cursor, *amend);
27
+    if let InputMode::Commit { buffer, cursor, amend, status } = &app.input_mode {
28
+        draw_commit_modal(frame, buffer, *cursor, *amend, status);
2929
     }
3030
 }
3131
 
3232
 /// Draw commit message modal
33
-fn draw_commit_modal(frame: &mut Frame, buffer: &str, cursor: usize, amend: bool) {
33
+fn draw_commit_modal(frame: &mut Frame, buffer: &str, cursor: usize, amend: bool, status: &CommitStatus) {
3434
     let area = frame.area();
3535
 
3636
     // Center the modal
@@ -44,27 +44,66 @@ fn draw_commit_modal(frame: &mut Frame, buffer: &str, cursor: usize, amend: bool
4444
     // Clear area behind modal
4545
     frame.render_widget(Clear, modal_area);
4646
 
47
-    // Title based on amend
48
-    let title = if amend { " Amend Commit " } else { " Commit " };
47
+    // Title and border color based on status
48
+    let (title, border_color) = match status {
49
+        CommitStatus::Editing => {
50
+            let t = if amend { " Amend Commit " } else { " Commit " };
51
+            (t, Color::Green)
52
+        }
53
+        CommitStatus::Committing => (" Committing... ", Color::Yellow),
54
+        CommitStatus::Success => (" ✓ Committed ", Color::Green),
55
+        CommitStatus::Failed => (" ✗ Failed ", Color::Red),
56
+    };
4957
 
5058
     let block = Block::default()
5159
         .title(title)
5260
         .borders(Borders::ALL)
53
-        .border_style(Style::default().fg(Color::Green));
61
+        .border_style(Style::default().fg(border_color));
5462
 
55
-    // Build input line with cursor
56
-    let display_text = if cursor >= buffer.len() {
57
-        format!("{}█", buffer)
58
-    } else {
59
-        format!("{}█{}", &buffer[..cursor], &buffer[cursor..])
63
+    // Content based on status
64
+    let content = match status {
65
+        CommitStatus::Editing => {
66
+            // Build input line with cursor
67
+            let display_text = if cursor >= buffer.len() {
68
+                format!("{}█", buffer)
69
+            } else {
70
+                format!("{}█{}", &buffer[..cursor], &buffer[cursor..])
71
+            };
72
+            vec![
73
+                Line::from(""),
74
+                Line::from(Span::raw(display_text)),
75
+            ]
76
+        }
77
+        CommitStatus::Committing => {
78
+            vec![
79
+                Line::from(""),
80
+                Line::from(Span::styled(
81
+                    "  Committing changes...",
82
+                    Style::default().fg(Color::Yellow),
83
+                )),
84
+            ]
85
+        }
86
+        CommitStatus::Success => {
87
+            vec![
88
+                Line::from(""),
89
+                Line::from(Span::styled(
90
+                    "  ✓ Changes committed successfully!",
91
+                    Style::default().fg(Color::Green),
92
+                )),
93
+            ]
94
+        }
95
+        CommitStatus::Failed => {
96
+            vec![
97
+                Line::from(""),
98
+                Line::from(Span::styled(
99
+                    "  ✗ Commit failed (nothing staged?)",
100
+                    Style::default().fg(Color::Red),
101
+                )),
102
+            ]
103
+        }
60104
     };
61105
 
62
-    let input = Paragraph::new(vec![
63
-        Line::from(""),
64
-        Line::from(Span::raw(display_text)),
65
-    ])
66
-    .block(block);
67
-
106
+    let input = Paragraph::new(content).block(block);
68107
     frame.render_widget(input, modal_area);
69108
 }
70109
 
@@ -85,6 +124,15 @@ fn draw_header(frame: &mut Frame, app: &App, area: Rect) {
85124
         ));
86125
     }
87126
 
127
+    // Show search buffer if active
128
+    if !app.search_buffer.is_empty() {
129
+        spans.push(Span::raw(" "));
130
+        spans.push(Span::styled(
131
+            format!("/{}", &app.search_buffer),
132
+            Style::default().fg(Color::Magenta).add_modifier(Modifier::BOLD),
133
+        ));
134
+    }
135
+
88136
     // Show status message if any
89137
     if let Some(ref msg) = app.status_message {
90138
         spans.push(Span::raw(" - "));