sergeant 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 31da9bd15ab6238af30e32ee9af80598fa98c2621a1d9d61c37bff4b59ba8e4f
4
- data.tar.gz: ee04423f976b128d354565d4a3a7e262b40934c0c02f49af55bce6089408b16c
3
+ metadata.gz: 41b7c3b966daedbb73dc65a9ceb67bb5653791757713bc33dd838f3327d9c33b
4
+ data.tar.gz: 636ed748a5e8efbe0291ccacb65031c814524e85e731ce076c9918c8adcfddc0
5
5
  SHA512:
6
- metadata.gz: 18dc7522e127b540a93b6c1383008e53bc5ae74b4ddcfdadda9d61f5df754b228637cd7e02a0f50df63f3ab3cc253cd2f868224e824c1b94b4bf958d4f71cae1
7
- data.tar.gz: c0d31805d279d936c8d9575636a07e350cae4f0e5240469a87183deacd38544c11fd4ff7d797930b2f85a86b5ccd2dd21e683268c3610fcb9d9920508da3df01
6
+ metadata.gz: d7e9b24e7f865efadec5dfe8729945bbf4c24bfa49df8c175e85d5de45c63457aba733fe09156950293e1ef3af4addd3831dd4e5a32f353e507b09667749eb23
7
+ data.tar.gz: 2b24694b491e148932545f6a727ca966a4803c489ca9d623098cd5e36ecc53cbcaf5761cf5fb1c6e259390555f6341551a1d55a1d765069a582275fc8d4c811a
data/CHANGELOG.md CHANGED
@@ -5,6 +5,56 @@ All notable changes to Sergeant will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.0.3] - 2024-12-26
9
+
10
+ ### Added
11
+ - **Total size display for marked items**
12
+ - Status bar now shows total size of all marked files
13
+ - Helps users understand the size of operations before copying/cutting
14
+ - Automatically formatted with appropriate units (B, K, M, G)
15
+ - **Quick filter feature** (f key)
16
+ - Filter current directory view without changing directories
17
+ - Case-insensitive real-time filtering as you type
18
+ - Status bar shows active filter and filtered item count
19
+ - Press ESC to clear filter, Enter to apply
20
+ - **Archive peek support** (v key on archives)
21
+ - Preview contents of archive files without extracting
22
+ - Supports: .zip, .tar, .tar.gz/.tgz, .tar.bz2/.tbz, .tar.xz/.txz, .7z, .rar
23
+ - Uses native tools (unzip, tar, 7z, unrar) for listing contents
24
+ - Falls back gracefully if archive tools not installed
25
+
26
+ ### Changed
27
+ - Updated help modal to reflect new features
28
+ - Reorganized help modal with "View & Search" section for better clarity
29
+
30
+ ### Performance
31
+ - **Optimized directory refresh**
32
+ - Only fetch owner info and permissions when ownership display is enabled
33
+ - Use `stat.directory?` instead of `File.directory?()` to avoid duplicate syscalls
34
+ - Track ownership toggle changes to refresh only when needed
35
+ - **Added comprehensive test coverage** for performance optimizations (14 test cases)
36
+
37
+ ## [1.0.2] - 2024-12-26
38
+
39
+ ### Fixed
40
+ - **Display issue on Arch Linux**: Added terminal color support checking
41
+ - Prevents crashes on terminals without color support
42
+ - Gracefully degrades when `start_color` is unavailable
43
+ - Fixes blank screen issue with Ruby version managers (mise, rbenv, asdf)
44
+
45
+ ### Added
46
+ - **Installation troubleshooting**
47
+ - Comprehensive troubleshooting documentation in README
48
+ - Simple alias solution for Arch Linux display issues: `alias sgt='ruby "$(which sgt)"'`
49
+ - **Better error handling**
50
+ - Terminal initialization now shows helpful error messages on failure
51
+ - Displays environment information (TERM, TTY status) to aid debugging
52
+
53
+ ### Technical
54
+ - Improved terminal initialization with `has_colors?` checks before calling `start_color`
55
+ - Added error recovery for curses screen initialization failures
56
+ - Better compatibility with different ncurses implementations
57
+
8
58
  ## [1.0.1] - 2024-12-24
9
59
 
10
60
  ### Fixed
data/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # 🎖️ Sergeant (sgt)
2
2
 
3
3
  ![Sergeant Logo](./logo.svg)
4
+ ![highlight](./highlight.gif)
4
5
 
5
6
  **Interactive TUI Directory Navigator for Terminal - "Leave it to the Sarge!"**
6
7
 
@@ -19,14 +20,17 @@ Simple, fast, and elegant.
19
20
  - 🔍 **Git Branch Display** - Shows current git branch in header
20
21
  - 👤 **Ownership Toggle** - View file permissions and ownership (press 'o')
21
22
  - 📑 **Bookmarks** - Save and quickly navigate to favorite directories
23
+ - 🔎 **Quick Filter** - Filter current directory view in real-time (press 'f')
22
24
 
23
25
  ### File Operations
24
26
  - 📋 **Copy/Cut/Paste** - Mark files with spacebar, copy (c), cut (x), and paste (p)
25
27
  - ✂️ **Multi-file Selection** - Mark multiple files/folders for batch operations
28
+ - 📏 **Size Display** - See total size of marked items in status bar
26
29
  - 🗑️ **Delete with Confirmation** - Safe deletion with confirmation dialog
27
30
  - ✏️ **Rename** - Rename files and folders with pre-filled input
28
31
  - 🔄 **Conflict Resolution** - Smart handling of file conflicts (skip/overwrite/rename)
29
- - 📄 **File Preview** - View markdown files with glow, code files with vim/nano
32
+ - 📄 **File Preview** - View markdown with glow, code with vim/nano, peek inside archives
33
+ - 📦 **Archive Peek** - Preview contents of .zip, .tar.gz, .7z, .rar files without extracting
30
34
 
31
35
  ### Search & Productivity
32
36
  - 🔎 **Fuzzy Search** - Integrate with fzf for fast file finding
@@ -65,6 +69,7 @@ sudo dnf install ncurses-devel ruby-devel
65
69
  ### Optional Tools
66
70
  - **glow** - For beautiful markdown preview (`brew install glow` or `go install github.com/charmbracelet/glow@latest`)
67
71
  - **fzf** - For fuzzy file search (`brew install fzf` or `sudo apt-get install fzf`)
72
+ - **Archive tools** - For archive preview: `unzip`, `tar`, `7z`, `unrar` (usually pre-installed on most systems)
68
73
 
69
74
  ## 🚀 Installation
70
75
 
@@ -90,6 +95,29 @@ gem install ./sergeant-1.0.0.gem
90
95
 
91
96
  That's it! The `sgt` command will automatically be added to your PATH.
92
97
 
98
+ ### Troubleshooting Installation
99
+
100
+ **If sgt doesn't display anything (shows blank screen):**
101
+
102
+ This can happen on Arch Linux or other systems using Ruby version managers (mise, rbenv, asdf).
103
+
104
+ **Recommended fix - Add an alias (simplest):**
105
+ ```bash
106
+ # Add to your ~/.bashrc or ~/.zshrc:
107
+ echo 'alias sgt='"'"'ruby "$(which sgt)"'"'"'' >> ~/.bashrc
108
+
109
+ # Reload your shell:
110
+ source ~/.bashrc # or: source ~/.zshrc
111
+
112
+ # Now sgt works!
113
+ sgt
114
+ ```
115
+
116
+ **Alternative - Quick test:**
117
+ ```bash
118
+ # Run with explicit ruby (temporary fix)
119
+ ruby $(which sgt)
120
+ ```
93
121
  ### Development Installation
94
122
 
95
123
  If you want to work on the gem:
@@ -132,7 +160,9 @@ sgt
132
160
  | `d` | Delete marked items (with confirmation) |
133
161
  | `r` | Rename current item |
134
162
  | `u` | Unmark all items |
135
- | `v` | Preview file (markdown/code) |
163
+ | `n` | Create new file or directory |
164
+ | `e` | Edit file with $EDITOR (or nano/nvim/vim) |
165
+ | `v` | Preview file or archive contents |
136
166
 
137
167
  ### Other Commands
138
168
 
@@ -142,9 +172,11 @@ sgt
142
172
  | `↓/j` | Move down |
143
173
  | `Enter/→/l` | Open directory or preview file |
144
174
  | `←/h` | Go to parent directory |
175
+ | `f` | Filter current directory view |
176
+ | `/` | Search files (requires fzf) |
177
+ | `:` | Execute terminal command in current directory |
145
178
  | `o` | Toggle ownership/permissions display |
146
179
  | `b` | Go to bookmark |
147
- | `/` | Search files (requires fzf) |
148
180
  | `m` | Show help modal with all key mappings |
149
181
  | `q/ESC` | Quit and cd to current directory |
150
182
 
data/highlight.gif ADDED
Binary file
@@ -49,12 +49,13 @@ module Sergeant
49
49
 
50
50
  # Restore curses screen
51
51
  init_screen
52
- start_color
52
+ if has_colors?
53
+ start_color
54
+ apply_color_theme
55
+ end
53
56
  curs_set(0)
54
57
  noecho
55
58
  stdscr.keypad(true)
56
- apply_color_theme
57
- refresh # Ensure display updates on all platforms
58
59
  end
59
60
 
60
61
  def preview_file
@@ -64,6 +65,13 @@ module Sergeant
64
65
  return unless item && item[:type] == :file
65
66
 
66
67
  file_path = item[:path]
68
+ file_ext = File.extname(file_path).downcase
69
+
70
+ # Check if it's an archive file
71
+ if archive_file?(file_ext)
72
+ preview_archive(file_path, file_ext)
73
+ return
74
+ end
67
75
 
68
76
  # Check if it's a text file
69
77
  unless text_file?(file_path)
@@ -75,8 +83,6 @@ module Sergeant
75
83
  close_screen
76
84
 
77
85
  begin
78
- file_ext = File.extname(file_path).downcase
79
-
80
86
  # Use glow for markdown files if available, otherwise fall back to less
81
87
  if file_ext == '.md' && glow_available?
82
88
  system("glow -p \"#{file_path}\"")
@@ -103,12 +109,13 @@ module Sergeant
103
109
 
104
110
  # Restore curses screen
105
111
  init_screen
106
- start_color
112
+ if has_colors?
113
+ start_color
114
+ apply_color_theme
115
+ end
107
116
  curs_set(0)
108
117
  noecho
109
118
  stdscr.keypad(true)
110
- apply_color_theme
111
- refresh # Ensure display updates on all platforms
112
119
  end
113
120
 
114
121
  def paste_with_modal
@@ -638,16 +645,91 @@ module Sergeant
638
645
 
639
646
  # Restore curses
640
647
  init_screen
641
- start_color
648
+ if has_colors?
649
+ start_color
650
+ apply_color_theme
651
+ end
642
652
  curs_set(0)
643
653
  noecho
644
654
  stdscr.keypad(true)
645
- apply_color_theme
646
- refresh # Ensure display updates on all platforms
647
655
 
648
656
  # Force refresh to show any changes from the command
649
657
  force_refresh
650
658
  end
659
+
660
+ private
661
+
662
+ def archive_file?(ext)
663
+ %w[.zip .tar .gz .bz2 .xz .7z .rar .tar.gz .tar.bz2 .tar.xz .tgz .tbz .txz].include?(ext) ||
664
+ ext.end_with?('.tar.gz', '.tar.bz2', '.tar.xz')
665
+ end
666
+
667
+ def preview_archive(file_path, file_ext)
668
+ close_screen
669
+
670
+ puts "Archive Preview: #{File.basename(file_path)}"
671
+ puts '─' * 80
672
+ puts
673
+
674
+ begin
675
+ # Detect archive type and use appropriate command
676
+ case file_ext
677
+ when '.zip'
678
+ system("unzip -l \"#{file_path}\" | less -R -F -X")
679
+ when '.tar'
680
+ system("tar -tvf \"#{file_path}\" | less -R -F -X")
681
+ when '.tar.gz', '.tgz', '.gz'
682
+ if file_path.end_with?('.tar.gz') || file_path.end_with?('.tgz')
683
+ system("tar -tzf \"#{file_path}\" | less -R -F -X")
684
+ else
685
+ system("gzip -l \"#{file_path}\"")
686
+ puts
687
+ puts 'Press Enter to continue...'
688
+ gets
689
+ end
690
+ when '.tar.bz2', '.tbz', '.bz2'
691
+ if file_path.end_with?('.tar.bz2') || file_path.end_with?('.tbz')
692
+ system("tar -tjf \"#{file_path}\" | less -R -F -X")
693
+ else
694
+ system("bzip2 -l \"#{file_path}\" 2>/dev/null || echo 'bzip2 does not support -l flag'")
695
+ puts
696
+ puts 'Press Enter to continue...'
697
+ gets
698
+ end
699
+ when '.tar.xz', '.txz', '.xz'
700
+ if file_path.end_with?('.tar.xz') || file_path.end_with?('.txz')
701
+ system("tar -tJf \"#{file_path}\" | less -R -F -X")
702
+ else
703
+ system("xz -l \"#{file_path}\"")
704
+ puts
705
+ puts 'Press Enter to continue...'
706
+ gets
707
+ end
708
+ when '.7z'
709
+ system("7z l \"#{file_path}\" | less -R -F -X")
710
+ when '.rar'
711
+ system("unrar l \"#{file_path}\" | less -R -F -X")
712
+ else
713
+ puts 'Archive type detected but no preview command available'
714
+ puts 'Press Enter to continue...'
715
+ gets
716
+ end
717
+ rescue StandardError => e
718
+ puts "Error previewing archive: #{e.message}"
719
+ puts 'Press Enter to continue...'
720
+ gets
721
+ end
722
+
723
+ # Restore curses screen
724
+ init_screen
725
+ if has_colors?
726
+ start_color
727
+ apply_color_theme
728
+ end
729
+ curs_set(0)
730
+ noecho
731
+ stdscr.keypad(true)
732
+ end
651
733
  end
652
734
  end
653
735
  end
@@ -59,13 +59,16 @@ module Sergeant
59
59
  ' u - Unmark all items',
60
60
  ' n - Create new file or directory',
61
61
  '',
62
+ 'View & Search:',
63
+ ' e - Edit file ($EDITOR, nano, nvim, vim)',
64
+ ' v - Preview file or archive contents',
65
+ ' f - Filter current directory view',
66
+ ' / - Search files (with fzf if available)',
67
+ '',
62
68
  'Other:',
63
69
  ' : - Execute terminal command',
64
- ' e - Edit file ($EDITOR, nano, nvim, vim)',
65
- ' v - Preview file (read-only)',
66
70
  ' o - Toggle ownership display',
67
71
  ' b - Go to bookmark',
68
- ' / - Search files (with fzf if available)',
69
72
  ' q / ESC - Quit and cd to current directory'
70
73
  ]
71
74
 
@@ -240,6 +240,130 @@ module Sergeant
240
240
  getch
241
241
  true
242
242
  end
243
+
244
+ def filter_current_view
245
+ max_y = lines
246
+ max_x = cols
247
+
248
+ modal_height = 8
249
+ modal_width = [70, max_x - 4].min
250
+ modal_y = (max_y - modal_height) / 2
251
+ modal_x = (max_x - modal_width) / 2
252
+
253
+ # Draw modal
254
+ (modal_y..(modal_y + modal_height)).each do |y|
255
+ setpos(y, modal_x)
256
+ attron(color_pair(3)) do
257
+ addstr(' ' * modal_width)
258
+ end
259
+ end
260
+
261
+ setpos(modal_y, modal_x)
262
+ attron(color_pair(4) | Curses::A_BOLD) do
263
+ addstr("\u250C#{'─' * (modal_width - 2)}\u2510")
264
+ end
265
+
266
+ setpos(modal_y + 1, modal_x)
267
+ attron(color_pair(4) | Curses::A_BOLD) do
268
+ addstr('│')
269
+ end
270
+ attron(color_pair(5) | Curses::A_BOLD) do
271
+ addstr(' Filter Current View '.center(modal_width - 2))
272
+ end
273
+ attron(color_pair(4) | Curses::A_BOLD) do
274
+ addstr('│')
275
+ end
276
+
277
+ setpos(modal_y + 2, modal_x)
278
+ attron(color_pair(4)) do
279
+ addstr("\u251C#{'─' * (modal_width - 2)}\u2524")
280
+ end
281
+
282
+ setpos(modal_y + 3, modal_x)
283
+ attron(color_pair(4)) do
284
+ addstr('│ ')
285
+ end
286
+ addstr('Enter text to filter files/folders (ESC to clear):'.ljust(modal_width - 4))
287
+ attron(color_pair(4)) do
288
+ addstr(' │')
289
+ end
290
+
291
+ setpos(modal_y + 4, modal_x)
292
+ attron(color_pair(4)) do
293
+ addstr('│ ')
294
+ end
295
+ prompt = 'Filter: '
296
+ attron(color_pair(5)) do
297
+ addstr(prompt)
298
+ end
299
+ addstr(' ' * (modal_width - 4 - prompt.length))
300
+ attron(color_pair(4)) do
301
+ addstr(' │')
302
+ end
303
+
304
+ setpos(modal_y + 6, modal_x)
305
+ attron(color_pair(4)) do
306
+ addstr('│')
307
+ end
308
+ attron(color_pair(4) | Curses::A_DIM) do
309
+ count = @all_items.length - (@all_items.any? { |i| i[:name] == '..' } ? 1 : 0)
310
+ addstr(" #{count} items in current directory ".center(modal_width - 2))
311
+ end
312
+ attron(color_pair(4)) do
313
+ addstr('│')
314
+ end
315
+
316
+ setpos(modal_y + modal_height - 1, modal_x)
317
+ attron(color_pair(4) | Curses::A_BOLD) do
318
+ addstr("\u2514#{'─' * (modal_width - 2)}\u2518")
319
+ end
320
+
321
+ # Input handling
322
+ curs_set(1)
323
+ echo
324
+ input_width = modal_width - 6 - prompt.length
325
+ filter_input = @filter_text.dup
326
+
327
+ setpos(modal_y + 4, modal_x + 2 + prompt.length)
328
+ addstr(filter_input.ljust(input_width))
329
+ setpos(modal_y + 4, modal_x + 2 + prompt.length + filter_input.length)
330
+
331
+ loop do
332
+ refresh
333
+ ch = getch
334
+
335
+ case ch
336
+ when 10, 13 # Enter
337
+ break
338
+ when 27 # ESC - clear filter
339
+ filter_input = ''
340
+ break
341
+ when 127, Curses::Key::BACKSPACE
342
+ if filter_input.length.positive?
343
+ filter_input = filter_input[0...-1]
344
+ setpos(modal_y + 4, modal_x + 2 + prompt.length)
345
+ addstr(filter_input.ljust(input_width))
346
+ setpos(modal_y + 4, modal_x + 2 + prompt.length + filter_input.length)
347
+ end
348
+ else
349
+ if ch.is_a?(String) && filter_input.length < input_width
350
+ filter_input += ch
351
+ setpos(modal_y + 4, modal_x + 2 + prompt.length)
352
+ addstr(filter_input.ljust(input_width))
353
+ setpos(modal_y + 4, modal_x + 2 + prompt.length + filter_input.length)
354
+ end
355
+ end
356
+ end
357
+
358
+ noecho
359
+ curs_set(0)
360
+
361
+ # Apply filter
362
+ @filter_text = filter_input.strip
363
+ @selected_index = 0
364
+ @scroll_offset = 0
365
+ force_refresh # Force refresh to apply filter
366
+ end
243
367
  end
244
368
  end
245
369
  end
@@ -20,11 +20,19 @@ module Sergeant
20
20
 
21
21
  # Build status info
22
22
  status_parts = []
23
- status_parts << "Marked: #{@marked_items.length}" unless @marked_items.empty?
23
+ unless @marked_items.empty?
24
+ total_size = @marked_items.sum { |path| File.size(path) rescue 0 }
25
+ size_str = format_size(total_size)
26
+ status_parts << "Marked: #{@marked_items.length} (#{size_str.strip})"
27
+ end
24
28
  unless @copied_items.empty?
25
29
  mode_text = @cut_mode ? 'Cut' : 'Copied'
26
30
  status_parts << "#{mode_text}: #{@copied_items.length}"
27
31
  end
32
+ unless @filter_text.empty?
33
+ filtered_count = @items.length - (@items.any? { |i| i[:name] == '..' } ? 1 : 0)
34
+ status_parts << "Filter: '#{@filter_text}' (#{filtered_count})"
35
+ end
28
36
  status_text = status_parts.empty? ? '' : " | #{status_parts.join(' | ')}"
29
37
 
30
38
  if branch
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sergeant
4
- VERSION = '1.0.1'
4
+ VERSION = '1.0.3'
5
5
  end
data/lib/sergeant.rb CHANGED
@@ -26,28 +26,36 @@ class SergeantApp
26
26
  @selected_index = 0
27
27
  @scroll_offset = 0
28
28
  @show_ownership = false
29
+ @last_show_ownership = false
29
30
  @config = Sergeant::Config.load_config
30
31
  @bookmarks = Sergeant::Config.load_bookmarks
31
32
  @marked_items = []
32
33
  @copied_items = []
33
34
  @cut_mode = false
35
+ @last_refreshed_dir = nil
36
+ @items = []
37
+ @filter_text = ''
38
+ @all_items = []
34
39
  end
35
40
 
36
41
  def run
37
42
  init_screen
38
- start_color
43
+
44
+ # Only initialize colors if terminal supports them
45
+ if has_colors?
46
+ start_color
47
+ apply_color_theme
48
+ end
49
+
39
50
  curs_set(0)
40
51
  noecho
41
52
  stdscr.keypad(true)
42
53
 
43
- apply_color_theme
44
-
45
54
  begin
46
- needs_refresh = true
47
55
  loop do
48
- refresh_items if needs_refresh
56
+ # Only refresh items when directory changes, not on every keystroke
57
+ refresh_items_if_needed
49
58
  draw_screen
50
- needs_refresh = false
51
59
 
52
60
  key = getch
53
61
  case key
@@ -61,22 +69,17 @@ class SergeantApp
61
69
  @current_dir = item[:path]
62
70
  @selected_index = 0
63
71
  @scroll_offset = 0
64
- needs_refresh = true
65
72
  elsif item && item[:type] == :file
66
73
  preview_file
67
- needs_refresh = true
68
74
  end
69
75
  when 'b'
70
76
  goto_bookmark
71
- needs_refresh = true
72
77
  when 'o'
73
78
  @show_ownership = !@show_ownership
74
79
  when 'e'
75
80
  edit_file
76
- needs_refresh = true
77
81
  when 'v'
78
82
  preview_file
79
- needs_refresh = true
80
83
  when 32, ' '
81
84
  toggle_mark
82
85
  when 'c'
@@ -85,26 +88,22 @@ class SergeantApp
85
88
  cut_marked_items
86
89
  when 'd'
87
90
  delete_marked_items
88
- needs_refresh = true
89
91
  when 'r'
90
92
  rename_item
91
- needs_refresh = true
92
93
  when 'p'
93
94
  paste_items
94
- needs_refresh = true
95
95
  when 'u'
96
96
  unmark_all
97
97
  when 'm'
98
98
  show_help_modal
99
99
  when 'n'
100
100
  create_new_with_modal
101
- needs_refresh = true
102
101
  when ':'
103
102
  execute_terminal_command
104
- needs_refresh = true
105
103
  when '/'
106
104
  search_files
107
- needs_refresh = true
105
+ when 'f'
106
+ filter_current_view
108
107
  when 'q', 27
109
108
  close_screen
110
109
  puts @current_dir
@@ -115,7 +114,6 @@ class SergeantApp
115
114
  @current_dir = parent
116
115
  @selected_index = 0
117
116
  @scroll_offset = 0
118
- needs_refresh = true
119
117
  end
120
118
  end
121
119
  end
@@ -179,11 +177,13 @@ class SergeantApp
179
177
  end
180
178
 
181
179
  init_screen
182
- start_color
180
+ if has_colors?
181
+ start_color
182
+ apply_color_theme
183
+ end
183
184
  curs_set(0)
184
185
  noecho
185
186
  stdscr.keypad(true)
186
- apply_color_theme
187
187
 
188
188
  return unless selected && !selected.empty?
189
189
 
@@ -196,9 +196,23 @@ class SergeantApp
196
196
  @scroll_offset = 0
197
197
  end
198
198
 
199
- def refresh_items
200
- entries = Dir.entries(@current_dir).reject { |e| e == '.' }
199
+ def refresh_items_if_needed
200
+ # Only refresh if directory has changed, or if showing ownership toggle changed
201
+ # This prevents expensive file system operations on every keystroke
202
+ if @current_dir != @last_refreshed_dir || @show_ownership != @last_show_ownership
203
+ refresh_items
204
+ @last_refreshed_dir = @current_dir
205
+ @last_show_ownership = @show_ownership
206
+ end
207
+ end
201
208
 
209
+ def force_refresh
210
+ # Force a refresh even if directory hasn't changed (e.g., after file operations)
211
+ @last_refreshed_dir = nil
212
+ end
213
+
214
+ def refresh_items
215
+ entries = Dir.entries(@current_dir).reject { |e| e == '.' || e == '..' }
202
216
  @items = []
203
217
 
204
218
  unless @current_dir == '/'
@@ -220,9 +234,9 @@ class SergeantApp
220
234
  full_path = File.join(@current_dir, entry)
221
235
  begin
222
236
  stat = File.stat(full_path)
223
- owner_info = get_owner_info(stat)
224
- is_dir = File.directory?(full_path)
225
- perms = format_permissions(stat.mode, is_dir)
237
+ is_dir = stat.directory? # Use stat instead of File.directory? (saves syscall)
238
+ owner_info = @show_ownership ? get_owner_info(stat) : nil # Only fetch if needed
239
+ perms = @show_ownership ? format_permissions(stat.mode, is_dir) : nil
226
240
 
227
241
  if is_dir
228
242
  directories << {
@@ -253,15 +267,32 @@ class SergeantApp
253
267
  files.sort_by! { |f| f[:name].downcase }
254
268
 
255
269
  @items += directories + files
270
+ @all_items = @items.dup # Store all items for filtering
271
+
272
+ # Apply filter if active
273
+ apply_filter if @filter_text && !@filter_text.empty?
256
274
 
257
275
  @selected_index = [@selected_index, @items.length - 1].min
258
276
  @selected_index = 0 if @selected_index.negative?
259
277
  end
260
278
 
279
+ def apply_filter
280
+ return if @filter_text.empty?
281
+
282
+ # Filter items by name (case-insensitive), keep '..' entry
283
+ @items = @all_items.select do |item|
284
+ item[:name] == '..' || item[:name].downcase.include?(@filter_text.downcase)
285
+ end
286
+ end
287
+
261
288
  def move_selection(delta)
262
289
  return if @items.empty?
263
290
 
264
291
  @selected_index = (@selected_index + delta).clamp(0, @items.length - 1)
292
+
293
+ # Flush input buffer to prevent lag on Windows when holding arrow keys
294
+ # This clears any queued key-repeat events that accumulated during processing
295
+ Curses.flushinp
265
296
  end
266
297
 
267
298
  def toggle_mark
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sergeant
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mateusz Grotha
@@ -66,6 +66,7 @@ files:
66
66
  - LICENSE
67
67
  - README.md
68
68
  - bin/sgt
69
+ - highlight.gif
69
70
  - lib/.DS_Store
70
71
  - lib/sergeant.rb
71
72
  - lib/sergeant/.DS_Store