sergeant 1.0.2 → 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: 6f8e44aa17efd4cddd56d88d910edc0082e94488cd3d25cf6def3b832a579c3d
4
- data.tar.gz: c66899a4748192ded5481c0497a81ebb81edad956a932d6253beab34ab834a67
3
+ metadata.gz: 41b7c3b966daedbb73dc65a9ceb67bb5653791757713bc33dd838f3327d9c33b
4
+ data.tar.gz: 636ed748a5e8efbe0291ccacb65031c814524e85e731ce076c9918c8adcfddc0
5
5
  SHA512:
6
- metadata.gz: 1590f36daa589471edc9f65f150b24b9764ef192b77d46cc51ffc1884e01dafa2fd9a4832a5fef635dfef8bca12f3d16f49b305a295a4fd8e6159e50529d9127
7
- data.tar.gz: 8ee3a3f6eacd45f3a91c42b7a96e94ccc0961c12cbd3b363ee8a92553efe4db4db73be8499d5304d54702b82333c50fa29b27ad0f87a6f2e141a0a9ca4dad3f2
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
 
@@ -113,16 +118,6 @@ sgt
113
118
  # Run with explicit ruby (temporary fix)
114
119
  ruby $(which sgt)
115
120
  ```
116
-
117
- **Alternative - Automated fix script:**
118
- ```bash
119
- # Creates a wrapper script (requires cloning repo)
120
- cd Sergeant
121
- bash arch_fix.sh
122
- ```
123
-
124
- **For detailed troubleshooting**, see [INSTALL_TROUBLESHOOTING.md](./INSTALL_TROUBLESHOOTING.md)
125
-
126
121
  ### Development Installation
127
122
 
128
123
  If you want to work on the gem:
@@ -165,7 +160,9 @@ sgt
165
160
  | `d` | Delete marked items (with confirmation) |
166
161
  | `r` | Rename current item |
167
162
  | `u` | Unmark all items |
168
- | `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 |
169
166
 
170
167
  ### Other Commands
171
168
 
@@ -175,9 +172,11 @@ sgt
175
172
  | `↓/j` | Move down |
176
173
  | `Enter/→/l` | Open directory or preview file |
177
174
  | `←/h` | Go to parent directory |
175
+ | `f` | Filter current directory view |
176
+ | `/` | Search files (requires fzf) |
177
+ | `:` | Execute terminal command in current directory |
178
178
  | `o` | Toggle ownership/permissions display |
179
179
  | `b` | Go to bookmark |
180
- | `/` | Search files (requires fzf) |
181
180
  | `m` | Show help modal with all key mappings |
182
181
  | `q/ESC` | Quit and cd to current directory |
183
182
 
data/highlight.gif ADDED
Binary file
@@ -65,6 +65,13 @@ module Sergeant
65
65
  return unless item && item[:type] == :file
66
66
 
67
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
68
75
 
69
76
  # Check if it's a text file
70
77
  unless text_file?(file_path)
@@ -76,8 +83,6 @@ module Sergeant
76
83
  close_screen
77
84
 
78
85
  begin
79
- file_ext = File.extname(file_path).downcase
80
-
81
86
  # Use glow for markdown files if available, otherwise fall back to less
82
87
  if file_ext == '.md' && glow_available?
83
88
  system("glow -p \"#{file_path}\"")
@@ -651,6 +656,80 @@ module Sergeant
651
656
  # Force refresh to show any changes from the command
652
657
  force_refresh
653
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
654
733
  end
655
734
  end
656
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.2'
4
+ VERSION = '1.0.3'
5
5
  end
data/lib/sergeant.rb CHANGED
@@ -26,6 +26,7 @@ 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 = []
@@ -33,6 +34,8 @@ class SergeantApp
33
34
  @cut_mode = false
34
35
  @last_refreshed_dir = nil
35
36
  @items = []
37
+ @filter_text = ''
38
+ @all_items = []
36
39
  end
37
40
 
38
41
  def run
@@ -99,6 +102,8 @@ class SergeantApp
99
102
  execute_terminal_command
100
103
  when '/'
101
104
  search_files
105
+ when 'f'
106
+ filter_current_view
102
107
  when 'q', 27
103
108
  close_screen
104
109
  puts @current_dir
@@ -194,9 +199,10 @@ class SergeantApp
194
199
  def refresh_items_if_needed
195
200
  # Only refresh if directory has changed, or if showing ownership toggle changed
196
201
  # This prevents expensive file system operations on every keystroke
197
- if @current_dir != @last_refreshed_dir
202
+ if @current_dir != @last_refreshed_dir || @show_ownership != @last_show_ownership
198
203
  refresh_items
199
204
  @last_refreshed_dir = @current_dir
205
+ @last_show_ownership = @show_ownership
200
206
  end
201
207
  end
202
208
 
@@ -206,8 +212,7 @@ class SergeantApp
206
212
  end
207
213
 
208
214
  def refresh_items
209
- entries = Dir.entries(@current_dir).reject { |e| e == '.' }
210
-
215
+ entries = Dir.entries(@current_dir).reject { |e| e == '.' || e == '..' }
211
216
  @items = []
212
217
 
213
218
  unless @current_dir == '/'
@@ -229,9 +234,9 @@ class SergeantApp
229
234
  full_path = File.join(@current_dir, entry)
230
235
  begin
231
236
  stat = File.stat(full_path)
232
- owner_info = get_owner_info(stat)
233
- is_dir = File.directory?(full_path)
234
- 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
235
240
 
236
241
  if is_dir
237
242
  directories << {
@@ -262,15 +267,32 @@ class SergeantApp
262
267
  files.sort_by! { |f| f[:name].downcase }
263
268
 
264
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?
265
274
 
266
275
  @selected_index = [@selected_index, @items.length - 1].min
267
276
  @selected_index = 0 if @selected_index.negative?
268
277
  end
269
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
+
270
288
  def move_selection(delta)
271
289
  return if @items.empty?
272
290
 
273
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
274
296
  end
275
297
 
276
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.2
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