tenseleyflow/fackr / 99366a9

Browse files

feat: fuss fuzzy filter and references panel visual fix

- Add fuzzy filter to fuss mode: type to jump to matching files
- Filter auto-resets after 500ms of inactivity
- Move git operations behind Alt+G prefix (git sub-mode)
- Move toggle hidden behind Alt+.
- Fix references panel row padding calculation
- Add ResetColor after panel rows to prevent color bleed
- Change code completion keybinding from Ctrl+Space to Ctrl+N
Authored by espadonne
SHA
99366a9c2b9a6e8f3404b07ed13eb728ed26c70a
Parents
422bf81
Tree
996558d

3 changed files

StatusFile+-
M src/editor/state.rs 63 26
M src/fuss/state.rs 100 0
M src/render/screen.rs 37 6
src/editor/state.rsmodified
@@ -485,8 +485,8 @@ impl Editor {
485
             }
485
             }
486
         }
486
         }
487
 
487
 
488
-        // Update diagnostics for current file
488
+        // Update diagnostics for current file (need full path to match LSP URIs)
489
-        if let Some(path) = self.filename() {
489
+        if let Some(path) = self.current_file_path() {
490
             let path_str = path.to_string_lossy();
490
             let path_str = path.to_string_lossy();
491
             self.lsp_state.diagnostics = self.workspace.lsp.get_diagnostics(&path_str);
491
             self.lsp_state.diagnostics = self.workspace.lsp.get_diagnostics(&path_str);
492
         }
492
         }
@@ -1049,6 +1049,7 @@ impl Editor {
1049
                     self.workspace.fuss.hints_expanded,
1049
                     self.workspace.fuss.hints_expanded,
1050
                     &repo_name,
1050
                     &repo_name,
1051
                     branch.as_deref(),
1051
                     branch.as_deref(),
1052
+                    self.workspace.fuss.git_mode,
1052
                 )?;
1053
                 )?;
1053
             }
1054
             }
1054
         }
1055
         }
@@ -1466,8 +1467,8 @@ impl Editor {
1466
             (Key::F(12), Modifiers { shift: true, .. }) => self.lsp_find_references(),
1467
             (Key::F(12), Modifiers { shift: true, .. }) => self.lsp_find_references(),
1467
             // Hover info: F1
1468
             // Hover info: F1
1468
             (Key::F(1), _) => self.lsp_hover(),
1469
             (Key::F(1), _) => self.lsp_hover(),
1469
-            // Code completion: Ctrl+Space
1470
+            // Code completion: Ctrl+N (vim-style)
1470
-            (Key::Char(' '), Modifiers { ctrl: true, .. }) => self.lsp_complete(),
1471
+            (Key::Char('n'), Modifiers { ctrl: true, .. }) => self.lsp_complete(),
1471
             // Rename: F2
1472
             // Rename: F2
1472
             (Key::F(2), _) => self.lsp_rename(),
1473
             (Key::F(2), _) => self.lsp_rename(),
1473
             // Server manager: Alt+M
1474
             // Server manager: Alt+M
@@ -3138,6 +3139,11 @@ impl Editor {
3138
     }
3139
     }
3139
 
3140
 
3140
     fn handle_fuss_key(&mut self, key: Key, mods: Modifiers) -> Result<()> {
3141
     fn handle_fuss_key(&mut self, key: Key, mods: Modifiers) -> Result<()> {
3142
+        // Handle git mode separately
3143
+        if self.workspace.fuss.git_mode {
3144
+            return self.handle_fuss_git_key(key, mods);
3145
+        }
3146
+
3141
         match (&key, &mods) {
3147
         match (&key, &mods) {
3142
             // Quit: Ctrl+Q (still works in fuss mode)
3148
             // Quit: Ctrl+Q (still works in fuss mode)
3143
             (Key::Char('q'), Modifiers { ctrl: true, .. }) => {
3149
             (Key::Char('q'), Modifiers { ctrl: true, .. }) => {
@@ -3146,19 +3152,23 @@ impl Editor {
3146
 
3152
 
3147
             // Exit fuss mode (Escape or F3)
3153
             // Exit fuss mode (Escape or F3)
3148
             (Key::Escape, _) | (Key::F(3), _) => {
3154
             (Key::Escape, _) | (Key::F(3), _) => {
3155
+                self.workspace.fuss.filter_clear();
3149
                 self.workspace.fuss.deactivate();
3156
                 self.workspace.fuss.deactivate();
3150
             }
3157
             }
3151
 
3158
 
3152
             // Navigation
3159
             // Navigation
3153
-            (Key::Up, _) | (Key::Char('k'), Modifiers { ctrl: false, alt: false, .. }) => {
3160
+            (Key::Up, _) => {
3161
+                self.workspace.fuss.filter_clear();
3154
                 self.workspace.fuss.move_up();
3162
                 self.workspace.fuss.move_up();
3155
             }
3163
             }
3156
-            (Key::Down, _) | (Key::Char('j'), Modifiers { ctrl: false, alt: false, .. }) => {
3164
+            (Key::Down, _) => {
3165
+                self.workspace.fuss.filter_clear();
3157
                 self.workspace.fuss.move_down();
3166
                 self.workspace.fuss.move_down();
3158
             }
3167
             }
3159
 
3168
 
3160
             // Toggle expand/collapse directory, or collapse parent if on a file/collapsed dir
3169
             // Toggle expand/collapse directory, or collapse parent if on a file/collapsed dir
3161
             (Key::Char(' '), _) => {
3170
             (Key::Char(' '), _) => {
3171
+                self.workspace.fuss.filter_clear();
3162
                 if self.workspace.fuss.is_dir_selected() {
3172
                 if self.workspace.fuss.is_dir_selected() {
3163
                     // If on a directory, toggle its expand state
3173
                     // If on a directory, toggle its expand state
3164
                     self.workspace.fuss.toggle_expand();
3174
                     self.workspace.fuss.toggle_expand();
@@ -3170,6 +3180,7 @@ impl Editor {
3170
 
3180
 
3171
             // Expand directory (right arrow)
3181
             // Expand directory (right arrow)
3172
             (Key::Right, _) => {
3182
             (Key::Right, _) => {
3183
+                self.workspace.fuss.filter_clear();
3173
                 if self.workspace.fuss.is_dir_selected() {
3184
                 if self.workspace.fuss.is_dir_selected() {
3174
                     // Only expand if not already expanded
3185
                     // Only expand if not already expanded
3175
                     if let Some(ref tree) = self.workspace.fuss.tree {
3186
                     if let Some(ref tree) = self.workspace.fuss.tree {
@@ -3185,6 +3196,7 @@ impl Editor {
3185
 
3196
 
3186
             // Collapse directory or go to parent (left arrow)
3197
             // Collapse directory or go to parent (left arrow)
3187
             (Key::Left, _) => {
3198
             (Key::Left, _) => {
3199
+                self.workspace.fuss.filter_clear();
3188
                 let mut collapsed = false;
3200
                 let mut collapsed = false;
3189
                 if self.workspace.fuss.is_dir_selected() {
3201
                 if self.workspace.fuss.is_dir_selected() {
3190
                     // If on an expanded directory, collapse it
3202
                     // If on an expanded directory, collapse it
@@ -3205,7 +3217,8 @@ impl Editor {
3205
             }
3217
             }
3206
 
3218
 
3207
             // Open file or toggle directory
3219
             // Open file or toggle directory
3208
-            (Key::Enter, _) | (Key::Char('o'), Modifiers { ctrl: false, alt: false, .. }) => {
3220
+            (Key::Enter, _) => {
3221
+                self.workspace.fuss.filter_clear();
3209
                 if self.workspace.fuss.is_dir_selected() {
3222
                 if self.workspace.fuss.is_dir_selected() {
3210
                     self.workspace.fuss.toggle_expand();
3223
                     self.workspace.fuss.toggle_expand();
3211
                 } else if let Some(path) = self.workspace.fuss.selected_file() {
3224
                 } else if let Some(path) = self.workspace.fuss.selected_file() {
@@ -3214,8 +3227,8 @@ impl Editor {
3214
                 }
3227
                 }
3215
             }
3228
             }
3216
 
3229
 
3217
-            // Toggle hidden files
3230
+            // Toggle hidden files: Alt+.
3218
-            (Key::Char('.'), _) => {
3231
+            (Key::Char('.'), Modifiers { alt: true, .. }) => {
3219
                 self.workspace.fuss.toggle_hidden();
3232
                 self.workspace.fuss.toggle_hidden();
3220
             }
3233
             }
3221
 
3234
 
@@ -3228,13 +3241,8 @@ impl Editor {
3228
                 self.workspace.fuss.toggle_hints();
3241
                 self.workspace.fuss.toggle_hints();
3229
             }
3242
             }
3230
 
3243
 
3231
-            // Also allow 'h' for hints toggle as fallback
3244
+            // Open file in vertical split: Ctrl+V
3232
-            (Key::Char('h'), Modifiers { ctrl: false, alt: false, .. }) => {
3245
+            (Key::Char('v'), Modifiers { ctrl: true, .. }) => {
3233
-                self.workspace.fuss.toggle_hints();
3234
-            }
3235
-
3236
-            // Open file in vertical split (v)
3237
-            (Key::Char('v'), Modifiers { ctrl: false, alt: false, .. }) => {
3238
                 if !self.workspace.fuss.is_dir_selected() {
3246
                 if !self.workspace.fuss.is_dir_selected() {
3239
                     if let Some(path) = self.workspace.fuss.selected_file() {
3247
                     if let Some(path) = self.workspace.fuss.selected_file() {
3240
                         self.open_file_in_vsplit(&path)?;
3248
                         self.open_file_in_vsplit(&path)?;
@@ -3243,8 +3251,8 @@ impl Editor {
3243
                 }
3251
                 }
3244
             }
3252
             }
3245
 
3253
 
3246
-            // Open file in horizontal split (s)
3254
+            // Open file in horizontal split: Ctrl+S
3247
-            (Key::Char('s'), Modifiers { ctrl: false, alt: false, .. }) => {
3255
+            (Key::Char('s'), Modifiers { ctrl: true, .. }) => {
3248
                 if !self.workspace.fuss.is_dir_selected() {
3256
                 if !self.workspace.fuss.is_dir_selected() {
3249
                     if let Some(path) = self.workspace.fuss.selected_file() {
3257
                     if let Some(path) = self.workspace.fuss.selected_file() {
3250
                         self.open_file_in_hsplit(&path)?;
3258
                         self.open_file_in_hsplit(&path)?;
@@ -3253,8 +3261,36 @@ impl Editor {
3253
                 }
3261
                 }
3254
             }
3262
             }
3255
 
3263
 
3264
+            // Enter git mode: Alt+G
3265
+            (Key::Char('g'), Modifiers { alt: true, .. }) => {
3266
+                self.workspace.fuss.enter_git_mode();
3267
+                self.message = Some("Git: [a]dd [u]nstage [d]iff [m]sg [p]ush pu[l]l [f]etch [t]ag".to_string());
3268
+            }
3269
+
3270
+            // Backspace: remove last filter character
3271
+            (Key::Backspace, _) => {
3272
+                self.workspace.fuss.filter_pop();
3273
+            }
3274
+
3275
+            // Regular characters: add to filter for fuzzy jump
3276
+            (Key::Char(c), Modifiers { ctrl: false, alt: false, .. }) => {
3277
+                self.workspace.fuss.filter_push(*c);
3278
+            }
3279
+
3280
+            _ => {}
3281
+        }
3282
+        Ok(())
3283
+    }
3284
+
3285
+    /// Handle keys when in git sub-mode within fuss
3286
+    fn handle_fuss_git_key(&mut self, key: Key, mods: Modifiers) -> Result<()> {
3287
+        // Any key exits git mode (after potentially doing an action)
3288
+        self.workspace.fuss.exit_git_mode();
3289
+        self.message = None;
3290
+
3291
+        match (&key, &mods) {
3256
             // Git: Stage file (a)
3292
             // Git: Stage file (a)
3257
-            (Key::Char('a'), Modifiers { ctrl: false, alt: false, .. }) => {
3293
+            (Key::Char('a'), _) => {
3258
                 if self.workspace.fuss.stage_selected() {
3294
                 if self.workspace.fuss.stage_selected() {
3259
                     self.message = Some("Staged".to_string());
3295
                     self.message = Some("Staged".to_string());
3260
                 } else {
3296
                 } else {
@@ -3263,7 +3299,7 @@ impl Editor {
3263
             }
3299
             }
3264
 
3300
 
3265
             // Git: Unstage file (u)
3301
             // Git: Unstage file (u)
3266
-            (Key::Char('u'), Modifiers { ctrl: false, alt: false, .. }) => {
3302
+            (Key::Char('u'), _) => {
3267
                 if self.workspace.fuss.unstage_selected() {
3303
                 if self.workspace.fuss.unstage_selected() {
3268
                     self.message = Some("Unstaged".to_string());
3304
                     self.message = Some("Unstaged".to_string());
3269
                 } else {
3305
                 } else {
@@ -3272,7 +3308,7 @@ impl Editor {
3272
             }
3308
             }
3273
 
3309
 
3274
             // Git: Show diff (d)
3310
             // Git: Show diff (d)
3275
-            (Key::Char('d'), Modifiers { ctrl: false, alt: false, .. }) => {
3311
+            (Key::Char('d'), _) => {
3276
                 if let Some((filename, diff)) = self.workspace.fuss.get_diff_for_selected() {
3312
                 if let Some((filename, diff)) = self.workspace.fuss.get_diff_for_selected() {
3277
                     let display_name = format!("[diff] {}", filename);
3313
                     let display_name = format!("[diff] {}", filename);
3278
                     self.workspace.open_content_tab(&diff, &display_name);
3314
                     self.workspace.open_content_tab(&diff, &display_name);
@@ -3283,7 +3319,7 @@ impl Editor {
3283
             }
3319
             }
3284
 
3320
 
3285
             // Git: Commit (m) - opens prompt for commit message
3321
             // Git: Commit (m) - opens prompt for commit message
3286
-            (Key::Char('m'), Modifiers { ctrl: false, alt: false, .. }) => {
3322
+            (Key::Char('m'), _) => {
3287
                 self.prompt = PromptState::TextInput {
3323
                 self.prompt = PromptState::TextInput {
3288
                     label: "Commit message: ".to_string(),
3324
                     label: "Commit message: ".to_string(),
3289
                     buffer: String::new(),
3325
                     buffer: String::new(),
@@ -3293,25 +3329,25 @@ impl Editor {
3293
             }
3329
             }
3294
 
3330
 
3295
             // Git: Push (p)
3331
             // Git: Push (p)
3296
-            (Key::Char('p'), Modifiers { ctrl: false, alt: false, .. }) => {
3332
+            (Key::Char('p'), _) => {
3297
                 let (_, msg) = self.workspace.fuss.git_push();
3333
                 let (_, msg) = self.workspace.fuss.git_push();
3298
                 self.message = Some(msg);
3334
                 self.message = Some(msg);
3299
             }
3335
             }
3300
 
3336
 
3301
             // Git: Pull (l)
3337
             // Git: Pull (l)
3302
-            (Key::Char('l'), Modifiers { ctrl: false, alt: false, .. }) => {
3338
+            (Key::Char('l'), _) => {
3303
                 let (_, msg) = self.workspace.fuss.git_pull();
3339
                 let (_, msg) = self.workspace.fuss.git_pull();
3304
                 self.message = Some(msg);
3340
                 self.message = Some(msg);
3305
             }
3341
             }
3306
 
3342
 
3307
             // Git: Fetch (f)
3343
             // Git: Fetch (f)
3308
-            (Key::Char('f'), Modifiers { ctrl: false, alt: false, .. }) => {
3344
+            (Key::Char('f'), _) => {
3309
                 let (_, msg) = self.workspace.fuss.git_fetch();
3345
                 let (_, msg) = self.workspace.fuss.git_fetch();
3310
                 self.message = Some(msg);
3346
                 self.message = Some(msg);
3311
             }
3347
             }
3312
 
3348
 
3313
             // Git: Tag (t) - opens prompt for tag name
3349
             // Git: Tag (t) - opens prompt for tag name
3314
-            (Key::Char('t'), Modifiers { ctrl: false, alt: false, .. }) => {
3350
+            (Key::Char('t'), _) => {
3315
                 self.prompt = PromptState::TextInput {
3351
                 self.prompt = PromptState::TextInput {
3316
                     label: "Tag name: ".to_string(),
3352
                     label: "Tag name: ".to_string(),
3317
                     buffer: String::new(),
3353
                     buffer: String::new(),
@@ -3320,6 +3356,7 @@ impl Editor {
3320
                 self.message = Some("Enter tag name (Enter to create, Esc to cancel)".to_string());
3356
                 self.message = Some("Enter tag name (Enter to create, Esc to cancel)".to_string());
3321
             }
3357
             }
3322
 
3358
 
3359
+            // Escape or any other key just cancels git mode
3323
             _ => {}
3360
             _ => {}
3324
         }
3361
         }
3325
         Ok(())
3362
         Ok(())
src/fuss/state.rsmodified
@@ -4,8 +4,12 @@
4
 
4
 
5
 use std::path::{Path, PathBuf};
5
 use std::path::{Path, PathBuf};
6
 use std::process::Command;
6
 use std::process::Command;
7
+use std::time::Instant;
7
 use super::tree::FileTree;
8
 use super::tree::FileTree;
8
 
9
 
10
+/// Timeout for filter reset (in milliseconds)
11
+const FILTER_TIMEOUT_MS: u128 = 500;
12
+
9
 /// Fuss mode state
13
 /// Fuss mode state
10
 #[derive(Debug)]
14
 #[derive(Debug)]
11
 pub struct FussMode {
15
 pub struct FussMode {
@@ -23,6 +27,12 @@ pub struct FussMode {
23
     pub hints_expanded: bool,
27
     pub hints_expanded: bool,
24
     /// Workspace root path
28
     /// Workspace root path
25
     root_path: Option<PathBuf>,
29
     root_path: Option<PathBuf>,
30
+    /// Current fuzzy filter query
31
+    pub filter: String,
32
+    /// Last time a filter character was typed
33
+    filter_last_input: Option<Instant>,
34
+    /// Whether git mode is active (after pressing Alt+G)
35
+    pub git_mode: bool,
26
 }
36
 }
27
 
37
 
28
 impl Default for FussMode {
38
 impl Default for FussMode {
@@ -35,6 +45,9 @@ impl Default for FussMode {
35
             width_percent: 30,
45
             width_percent: 30,
36
             hints_expanded: false,
46
             hints_expanded: false,
37
             root_path: None,
47
             root_path: None,
48
+            filter: String::new(),
49
+            filter_last_input: None,
50
+            git_mode: false,
38
         }
51
         }
39
     }
52
     }
40
 }
53
 }
@@ -476,4 +489,91 @@ impl FussMode {
476
             None
489
             None
477
         }
490
         }
478
     }
491
     }
492
+
493
+    /// Add a character to the filter and jump to first match
494
+    /// Resets the filter if too much time has passed since last input
495
+    pub fn filter_push(&mut self, c: char) {
496
+        let now = Instant::now();
497
+
498
+        // Check if we should reset the filter due to timeout
499
+        if let Some(last) = self.filter_last_input {
500
+            if now.duration_since(last).as_millis() > FILTER_TIMEOUT_MS {
501
+                self.filter.clear();
502
+            }
503
+        }
504
+
505
+        self.filter.push(c);
506
+        self.filter_last_input = Some(now);
507
+        self.jump_to_filter_match();
508
+    }
509
+
510
+    /// Remove last character from filter
511
+    pub fn filter_pop(&mut self) {
512
+        self.filter.pop();
513
+        if !self.filter.is_empty() {
514
+            self.jump_to_filter_match();
515
+        }
516
+    }
517
+
518
+    /// Clear the filter
519
+    pub fn filter_clear(&mut self) {
520
+        self.filter.clear();
521
+        self.filter_last_input = None;
522
+    }
523
+
524
+    /// Jump to the first item matching the current filter (fuzzy match)
525
+    fn jump_to_filter_match(&mut self) {
526
+        if self.filter.is_empty() {
527
+            return;
528
+        }
529
+
530
+        let tree = match &self.tree {
531
+            Some(t) => t,
532
+            None => return,
533
+        };
534
+
535
+        let items = tree.visible_items();
536
+        let query = self.filter.to_lowercase();
537
+
538
+        // Find best matching item starting from current position + 1
539
+        // This allows pressing the same keys repeatedly to cycle through matches
540
+        let start = (self.selected + 1) % items.len().max(1);
541
+
542
+        // First try: find match starting from current position
543
+        for offset in 0..items.len() {
544
+            let idx = (start + offset) % items.len();
545
+            let name = items[idx].name.to_lowercase();
546
+
547
+            if fuzzy_match(&name, &query) {
548
+                self.selected = idx;
549
+                return;
550
+            }
551
+        }
552
+    }
553
+
554
+    /// Enter git mode (after Alt+G)
555
+    pub fn enter_git_mode(&mut self) {
556
+        self.git_mode = true;
557
+    }
558
+
559
+    /// Exit git mode
560
+    pub fn exit_git_mode(&mut self) {
561
+        self.git_mode = false;
562
+    }
563
+}
564
+
565
+/// Simple fuzzy matching: checks if query characters appear in order in the target
566
+fn fuzzy_match(target: &str, query: &str) -> bool {
567
+    let mut query_chars = query.chars().peekable();
568
+
569
+    for c in target.chars() {
570
+        if query_chars.peek() == Some(&c) {
571
+            query_chars.next();
572
+        }
573
+        if query_chars.peek().is_none() {
574
+            return true;
575
+        }
576
+    }
577
+
578
+    query_chars.peek().is_none()
479
 }
579
 }
src/render/screen.rsmodified
@@ -841,11 +841,13 @@ impl Screen {
841
         hints_expanded: bool,
841
         hints_expanded: bool,
842
         repo_name: &str,
842
         repo_name: &str,
843
         branch: Option<&str>,
843
         branch: Option<&str>,
844
+        git_mode: bool,
844
     ) -> Result<()> {
845
     ) -> Result<()> {
845
         let width = width as usize;
846
         let width = width as usize;
846
         let text_rows = self.rows.saturating_sub(1) as usize;
847
         let text_rows = self.rows.saturating_sub(1) as usize;
847
         let hint_rows = if hints_expanded { 4 } else { 1 };
848
         let hint_rows = if hints_expanded { 4 } else { 1 };
848
-        let header_rows = 2; // Header line + separator
849
+        // Header line + separator + optional git mode line
850
+        let header_rows = if git_mode { 3 } else { 2 };
849
         let tree_rows = text_rows.saturating_sub(hint_rows + header_rows);
851
         let tree_rows = text_rows.saturating_sub(hint_rows + header_rows);
850
 
852
 
851
         // Draw header: repo_name:branch
853
         // Draw header: repo_name:branch
@@ -893,6 +895,21 @@ impl Screen {
893
             ResetColor,
895
             ResetColor,
894
         )?;
896
         )?;
895
 
897
 
898
+        // Draw git mode indicator line
899
+        if git_mode {
900
+            let git_row = 2u16;
901
+            execute!(self.stdout, MoveTo(0, git_row))?;
902
+            let git_hint = "Git: a/u/d/m/p/l/f/t";
903
+            let padded = format!("{:<width$}", git_hint, width = width);
904
+            execute!(
905
+                self.stdout,
906
+                SetBackgroundColor(Color::AnsiValue(235)),
907
+                SetForegroundColor(Color::Yellow),
908
+                Print(&padded),
909
+                ResetColor,
910
+            )?;
911
+        }
912
+
896
         // Draw file tree (starting after header)
913
         // Draw file tree (starting after header)
897
         for row in 0..tree_rows {
914
         for row in 0..tree_rows {
898
             let screen_row = (row + header_rows) as u16;
915
             let screen_row = (row + header_rows) as u16;
@@ -1015,10 +1032,10 @@ impl Screen {
1015
         let hint_start = header_rows + tree_rows;
1032
         let hint_start = header_rows + tree_rows;
1016
         if hints_expanded {
1033
         if hints_expanded {
1017
             let hints = [
1034
             let hints = [
1018
-                "j/k:nav spc:toggle o:open .:hidden",
1035
+                "type:jump  spc:toggle  enter:open",
1019
-                "a:stage u:unstage d:diff m:commit",
1036
+                "alt-.:hidden  alt-g:git  ctrl-v/s:split",
1020
-                "p:push l:pull f:fetch t:tag",
1037
+                "ctrl-b:close  ctrl-/:hints",
1021
-                "ctrl-b:close ctrl-/:hints",
1038
+                "",
1022
             ];
1039
             ];
1023
             for (i, hint) in hints.iter().enumerate() {
1040
             for (i, hint) in hints.iter().enumerate() {
1024
                 if hint_start + i < text_rows {
1041
                 if hint_start + i < text_rows {
@@ -1994,6 +2011,7 @@ impl Screen {
1994
             Print(&title),
2011
             Print(&title),
1995
             SetForegroundColor(border_color),
2012
             SetForegroundColor(border_color),
1996
             Print(format!("{:─<width$}┐", "", width = panel_width.saturating_sub(title.len() + 2))),
2013
             Print(format!("{:─<width$}┐", "", width = panel_width.saturating_sub(title.len() + 2))),
2014
+            ResetColor,
1997
         )?;
2015
         )?;
1998
 
2016
 
1999
         // Draw filter input row
2017
         // Draw filter input row
@@ -2011,6 +2029,7 @@ impl Screen {
2011
             SetBackgroundColor(bg),
2029
             SetBackgroundColor(bg),
2012
             SetForegroundColor(border_color),
2030
             SetForegroundColor(border_color),
2013
             Print("│"),
2031
             Print("│"),
2032
+            ResetColor,
2014
         )?;
2033
         )?;
2015
 
2034
 
2016
         // Draw separator
2035
         // Draw separator
@@ -2020,6 +2039,7 @@ impl Screen {
2020
             SetBackgroundColor(bg),
2039
             SetBackgroundColor(bg),
2021
             SetForegroundColor(border_color),
2040
             SetForegroundColor(border_color),
2022
             Print(format!("├{:─<width$}┤", "", width = panel_width.saturating_sub(2))),
2041
             Print(format!("├{:─<width$}┤", "", width = panel_width.saturating_sub(2))),
2042
+            ResetColor,
2023
         )?;
2043
         )?;
2024
 
2044
 
2025
         // Calculate visible range with scrolling
2045
         // Calculate visible range with scrolling
@@ -2063,6 +2083,12 @@ impl Screen {
2063
 
2083
 
2064
             let item_bg = if is_selected { selected_bg } else { bg };
2084
             let item_bg = if is_selected { selected_bg } else { bg };
2065
 
2085
 
2086
+            // Build a fixed-width line: "│ " + path (padded to max_path_width) + line_info + " │"
2087
+            // Total: 2 + max_path_width + line_info.len() + 2 = panel_width
2088
+            // So we need: max_path_width = panel_width - line_info.len() - 4
2089
+            // The remaining padding goes after line_info
2090
+            let remaining = panel_width.saturating_sub(max_path_width + line_info.len() + 4);
2091
+
2066
             execute!(
2092
             execute!(
2067
                 self.stdout,
2093
                 self.stdout,
2068
                 MoveTo(start_col as u16, row),
2094
                 MoveTo(start_col as u16, row),
@@ -2073,8 +2099,10 @@ impl Screen {
2073
                 Print(format!("{:<width$}", truncated_path, width = max_path_width)),
2099
                 Print(format!("{:<width$}", truncated_path, width = max_path_width)),
2074
                 SetForegroundColor(line_num_color),
2100
                 SetForegroundColor(line_num_color),
2075
                 Print(&line_info),
2101
                 Print(&line_info),
2102
+                Print(format!("{:width$}", "", width = remaining)),
2076
                 SetForegroundColor(border_color),
2103
                 SetForegroundColor(border_color),
2077
-                Print(format!("{:>width$}│", "", width = panel_width.saturating_sub(truncated_path.len() + line_info.len() + 3))),
2104
+                Print(" │"),
2105
+                ResetColor,
2078
             )?;
2106
             )?;
2079
         }
2107
         }
2080
 
2108
 
@@ -2088,6 +2116,7 @@ impl Screen {
2088
                 SetBackgroundColor(bg),
2116
                 SetBackgroundColor(bg),
2089
                 SetForegroundColor(border_color),
2117
                 SetForegroundColor(border_color),
2090
                 Print(format!("│{:width$}│", "", width = panel_width.saturating_sub(2))),
2118
                 Print(format!("│{:width$}│", "", width = panel_width.saturating_sub(2))),
2119
+                ResetColor,
2091
             )?;
2120
             )?;
2092
         }
2121
         }
2093
 
2122
 
@@ -2104,6 +2133,7 @@ impl Screen {
2104
             Print(format!(" {:<width$}", help_text, width = panel_width.saturating_sub(3))),
2133
             Print(format!(" {:<width$}", help_text, width = panel_width.saturating_sub(3))),
2105
             SetForegroundColor(border_color),
2134
             SetForegroundColor(border_color),
2106
             Print("┤"),
2135
             Print("┤"),
2136
+            ResetColor,
2107
         )?;
2137
         )?;
2108
 
2138
 
2109
         // Draw bottom border
2139
         // Draw bottom border
@@ -2119,6 +2149,7 @@ impl Screen {
2119
         // Hide cursor when in references panel
2149
         // Hide cursor when in references panel
2120
         execute!(self.stdout, Hide)?;
2150
         execute!(self.stdout, Hide)?;
2121
 
2151
 
2152
+        self.stdout.flush()?;
2122
         Ok(())
2153
         Ok(())
2123
     }
2154
     }
2124
 
2155