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 +4 -4
- data/CHANGELOG.md +50 -0
- data/README.md +35 -3
- data/highlight.gif +0 -0
- data/lib/sergeant/modals/file_operations.rb +93 -11
- data/lib/sergeant/modals/help.rb +6 -3
- data/lib/sergeant/modals/navigation.rb +124 -0
- data/lib/sergeant/rendering.rb +9 -1
- data/lib/sergeant/version.rb +1 -1
- data/lib/sergeant.rb +56 -25
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 41b7c3b966daedbb73dc65a9ceb67bb5653791757713bc33dd838f3327d9c33b
|
|
4
|
+
data.tar.gz: 636ed748a5e8efbe0291ccacb65031c814524e85e731ce076c9918c8adcfddc0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|

|
|
4
|
+

|
|
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
|
|
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
|
-
| `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/lib/sergeant/modals/help.rb
CHANGED
|
@@ -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
|
data/lib/sergeant/rendering.rb
CHANGED
|
@@ -20,11 +20,19 @@ module Sergeant
|
|
|
20
20
|
|
|
21
21
|
# Build status info
|
|
22
22
|
status_parts = []
|
|
23
|
-
|
|
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
|
data/lib/sergeant/version.rb
CHANGED
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
200
|
-
|
|
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
|
-
|
|
224
|
-
|
|
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.
|
|
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
|