sergeant 1.0.4 → 1.0.6

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: 9a87751fe5e98dee814b470aa90818bd0f798cbdb4143f55522ffcf610d8de0c
4
- data.tar.gz: d1c47033ac959cc1bbd50af0fe96e526df4d2f72077efc914d0570b8df6e0e7b
3
+ metadata.gz: 8121e483349ea026f5d4abec53dea0fc9a08b9201d43904c3a31e2a6667b457a
4
+ data.tar.gz: e3067dbdafba6fe79d606662ea3ee27d85dcba40a290413a9c43c1c3195e2976
5
5
  SHA512:
6
- metadata.gz: 77dac789c948ee83d3bfeda13cdda4fa0c3f03779d2e6cfc3ec85126dd97465c309d55cfa2500008656be3b65367e961c7af8b639c4d237a0716572d2cfa82be
7
- data.tar.gz: e0e1222dba1be26d9a3da7611aa8c5289ea422f58f9c1f5d3813d7f7581c05f860e61144e7623c1f66ccbb127ba37d27eaded1b9c5633f6e9e5cfa7422df8c0b
6
+ metadata.gz: 52c9ede72d02a8d22f6ad98d5d7b8590c16adbfe4c5e3d907ea6e4dc52e90b6abf609a2a01de11d3c9d3556bc537f7adad40206ee0823f105a98bf53fc3418f0
7
+ data.tar.gz: 8cf1a0e2467cd455513385bef1beceea416b0d347129eff30ef61162c4370623034031e8b623ce966d078db4075d79beac7003c0aae2586b3da1b0e93682b6e0
data/CHANGELOG.md CHANGED
@@ -5,7 +5,58 @@ 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
8
+ ## [1.0.6] - 2026-01-16
9
+
10
+ ### Added
11
+ - **Session persistence** (`--restore` flag)
12
+ - Automatically saves current directory on exit to `~/.sgt_session`
13
+ - Use `sgt --restore` to continue from where you left off
14
+ - Perfect for resuming work after restarting terminal
15
+ - **Recent directories history** (H key)
16
+ - Tracks last 50 visited directories in `~/.sgt_history`
17
+ - Press 'H' to show history modal with quick navigation
18
+ - Navigate with ↑/↓, press Enter to jump to directory
19
+ - **Enhanced error handling**
20
+ - New error dialog with file path and detailed error message
21
+ - Options: [S]kip, [R]etry, [A]bort for better error recovery
22
+ - Shows specific file/path that caused the error
23
+
24
+ ### Performance
25
+ - **Stat caching with 5-second TTL**
26
+ - Cache file stat results to avoid redundant system calls
27
+ - 90%+ faster navigation when browsing back/forth between directories
28
+ - Automatic cache cleanup and memory management (max 5000 entries)
29
+ - Press 'R' to force refresh and clear cache manually
30
+ - Especially beneficial on network filesystems (NFS, SMB)
31
+
32
+ ## [1.0.5] - 2025-01-15
33
+
34
+ ### Added
35
+ - **Command-line interface improvements**
36
+ - Added `--help` / `-h` flag to show usage and features
37
+ - Added `--version` / `-v` flag to show version number
38
+ - Support starting in specific directory: `sgt ~/Documents`
39
+ - Added `-b` / `--bookmark [name]` to start at a saved bookmark location
40
+ - Added `--list-bookmarks` to display all saved bookmarks with status
41
+ - Added `--pwd` flag to print final directory on exit for shell integration
42
+ - Added `--debug` flag to show environment and configuration details
43
+ - Added `--no-color` flag for terminals without color support
44
+ - Post-install message with quick start guide and tips
45
+ - Shell integration examples for quick directory jumping
46
+
47
+ ## [1.0.4] - 2025-12-27
48
+
49
+ ### Fixed
50
+ - **Windows compatibility improvements**
51
+ - Use ASCII icons ([D], [F], *, >) on Windows for better terminal compatibility (PR #15)
52
+ - Windows terminals often don't render emoji properly - now uses ASCII fallback
53
+ - Add notepad fallback for file preview and edit on Windows (PR #16)
54
+ - POSIX tools (vim, nano, less) replaced with notepad when not available
55
+
56
+ ### Changed
57
+ - Reduced gem package size from 4.8MB to ~115KB by excluding media files and .DS_Store
58
+
59
+ ## [1.0.3] - 2025-12-26
9
60
 
10
61
  ### Added
11
62
  - **Total size display for marked items**
@@ -34,7 +85,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
34
85
  - Track ownership toggle changes to refresh only when needed
35
86
  - **Added comprehensive test coverage** for performance optimizations (14 test cases)
36
87
 
37
- ## [1.0.2] - 2024-12-26
88
+ ## [1.0.2] - 2025-12-26
38
89
 
39
90
  ### Fixed
40
91
  - **Display issue on Arch Linux**: Added terminal color support checking
@@ -55,7 +106,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
55
106
  - Added error recovery for curses screen initialization failures
56
107
  - Better compatibility with different ncurses implementations
57
108
 
58
- ## [1.0.1] - 2024-12-24
109
+ ## [1.0.1] - 2025-12-24
59
110
 
60
111
  ### Fixed
61
112
  - **Major performance improvement**: Fixed severe input lag with large directories
@@ -66,7 +117,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66
117
  - Shows load path and helpful reinstall instructions if gem fails to load
67
118
  - Displays full error details instead of silently failing
68
119
 
69
- ## [1.0.0] - 2024-12-23
120
+ ## [1.0.0] - 2025-12-23
70
121
 
71
122
  ### Added
72
123
  - **Interactive TUI navigation** - Navigate directories with arrow keys or vim bindings (hjkl)
data/README.md CHANGED
@@ -37,6 +37,12 @@ Simple, fast, and elegant.
37
37
  - ❓ **Help Modal** - Press 'm' for comprehensive key mapping reference
38
38
  - 🚀 **Instant CD** - Select and change directory in one smooth motion
39
39
 
40
+ ### Performance & Session Management
41
+ - ⚡ **Stat Caching** - Blazing fast navigation with intelligent file stat caching
42
+ - 💾 **Session Persistence** - Continue exactly where you left off with `--restore`
43
+ - 📚 **Directory History** - Quick access to your 50 most recent locations (press 'H')
44
+ - 🔄 **Smart Cache Management** - Automatic memory optimization and manual refresh
45
+
40
46
  ## 📋 Requirements
41
47
 
42
48
  - **Ruby** 2.7 or higher (Ruby 3.x recommended)
@@ -73,7 +79,7 @@ sudo dnf install ncurses-devel ruby-devel
73
79
 
74
80
  ## 🚀 Installation
75
81
 
76
- ### Install from RubyGems (Coming Soon)
82
+ ### Install from RubyGems
77
83
 
78
84
  Once published to RubyGems:
79
85
 
@@ -142,6 +148,12 @@ bundle exec bin/sgt
142
148
  # Start sergeant in current directory
143
149
  sgt
144
150
 
151
+ # Start in specific directory
152
+ sgt ~/Documents
153
+
154
+ # Start at a bookmark
155
+ sgt -b projects
156
+
145
157
  # Navigate and select
146
158
  # Arrow keys or j/k to move up/down
147
159
  # Enter or l to enter directory
@@ -149,6 +161,62 @@ sgt
149
161
  # q to quit and cd to selected directory
150
162
  ```
151
163
 
164
+ ### Command-Line Options
165
+
166
+ ```bash
167
+ # View help and all options
168
+ sgt --help
169
+
170
+ # Show version
171
+ sgt --version
172
+
173
+ # List all bookmarks
174
+ sgt --list-bookmarks
175
+
176
+ # Start at bookmark location
177
+ sgt -b [bookmark_name]
178
+
179
+ # Restore last session (continue from where you left off)
180
+ sgt --restore
181
+
182
+ # Debug mode (show environment info)
183
+ sgt --debug
184
+
185
+ # Disable colors
186
+ sgt --no-color
187
+ ```
188
+
189
+ ### Shell Integration (cd to final directory)
190
+
191
+ The `--pwd` flag enables powerful shell integration, allowing you to navigate visually in sergeant and have your shell automatically cd to the final location:
192
+
193
+ ```bash
194
+ # Quick navigation function
195
+ # Add this to your ~/.bashrc or ~/.zshrc:
196
+ s() {
197
+ local dir=$(sgt --pwd "$@")
198
+ [[ -n "$dir" ]] && cd "$dir"
199
+ }
200
+
201
+ # Usage examples:
202
+ s # Navigate from current dir, cd to final location
203
+ s ~/projects # Start in projects, cd to where you end up
204
+ s -b work # Start at work bookmark, cd to final location
205
+
206
+ # Alternative one-liner (no function needed):
207
+ cd $(sgt --pwd ~/projects)
208
+
209
+ # Jump to deeply nested directory visually:
210
+ cd $(sgt --pwd /usr/local)
211
+ ```
212
+
213
+ **How it works:**
214
+ 1. Start sergeant with `--pwd` flag
215
+ 2. Navigate to your desired directory using arrow keys
216
+ 3. Press `q` to quit
217
+ 4. Sergeant outputs the final directory path
218
+ 5. Shell captures it with `$()` and cd's there
219
+
152
220
  ### File Operations
153
221
 
154
222
  | Key | Action |
@@ -177,6 +245,8 @@ sgt
177
245
  | `:` | Execute terminal command in current directory |
178
246
  | `o` | Toggle ownership/permissions display |
179
247
  | `b` | Go to bookmark |
248
+ | `H` | Show recent directories history |
249
+ | `R` | Force refresh and clear cache |
180
250
  | `m` | Show help modal with all key mappings |
181
251
  | `q/ESC` | Quit and cd to current directory |
182
252
 
data/bin/sgt CHANGED
@@ -2,7 +2,6 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  # Sergeant (sgt) - Interactive TUI directory navigator
5
- # Version: 1.0.0
6
5
 
7
6
  begin
8
7
  require 'sergeant'
@@ -13,11 +12,200 @@ rescue LoadError => e
13
12
  exit 1
14
13
  end
15
14
 
15
+ def show_help
16
+ puts <<~HELP
17
+ Sergeant (sgt) v#{Sergeant::VERSION} - Interactive TUI Directory Navigator
18
+
19
+ USAGE:
20
+ sgt [OPTIONS] [DIRECTORY]
21
+
22
+ OPTIONS:
23
+ -h, --help Show this help message
24
+ -v, --version Show version number
25
+ -b, --bookmark NAME Start at bookmark location
26
+ --list-bookmarks List all saved bookmarks
27
+ --restore Restore last session (start in last directory)
28
+ --pwd Print final directory on exit (for shell integration)
29
+ --debug Show debug information and environment details
30
+ --no-color Disable colors (for terminals without color support)
31
+
32
+ ARGUMENTS:
33
+ DIRECTORY Start in specified directory (default: current directory)
34
+
35
+ EXAMPLES:
36
+ sgt Start in current directory
37
+ sgt ~/Documents Start in Documents folder
38
+ sgt -b projects Start at 'projects' bookmark
39
+ sgt --restore Continue from last session
40
+ sgt --list-bookmarks Show all bookmarks
41
+ cd $(sgt --pwd) Navigate and cd to final directory
42
+ sgt --debug Show debug info before starting
43
+ sgt --help Show this help
44
+
45
+ SHELL INTEGRATION:
46
+ Add this to your ~/.bashrc or ~/.zshrc for quick directory jumping:
47
+
48
+ s() {
49
+ local dir=$(sgt --pwd "$@")
50
+ [[ -n "$dir" ]] && cd "$dir"
51
+ }
52
+
53
+ Then use: s ~/projects
54
+
55
+ FEATURES:
56
+ • Navigate with arrow keys or vim bindings (hjkl)
57
+ • Mark files with Space, copy (c), cut (x), paste (p)
58
+ • Quick filter (f), fuzzy search (/), file preview (v)
59
+ • Archive peek for .zip, .tar.gz, and more
60
+ • Press 'm' in-app for complete key mappings
61
+
62
+ For more information, visit: https://github.com/biscoitinho/Sergeant
63
+ HELP
64
+ exit 0
65
+ end
66
+
67
+ def show_version
68
+ puts "Sergeant (sgt) version #{Sergeant::VERSION}"
69
+ exit 0
70
+ end
71
+
72
+ def show_debug_info
73
+ puts "Sergeant Debug Information"
74
+ puts "=" * 50
75
+ puts "Version: #{Sergeant::VERSION}"
76
+ puts "Ruby Version: #{RUBY_VERSION}"
77
+ puts "Ruby Platform: #{RUBY_PLATFORM}"
78
+ puts "Current Directory: #{Dir.pwd}"
79
+ puts "Home Directory: #{ENV['HOME']}"
80
+ puts "TERM: #{ENV['TERM'] || 'not set'}"
81
+ puts "COLORTERM: #{ENV['COLORTERM'] || 'not set'}"
82
+ puts "EDITOR: #{ENV['EDITOR'] || 'not set'}"
83
+ puts "VISUAL: #{ENV['VISUAL'] || 'not set'}"
84
+ puts "PATH: #{ENV['PATH']}"
85
+ puts "\nLoad Path:"
86
+ $LOAD_PATH.each { |path| puts " #{path}" }
87
+ puts "\nConfig File: #{File.expand_path('~/.sgtrc')}"
88
+ puts "Config Exists: #{File.exist?(File.expand_path('~/.sgtrc'))}"
89
+ puts "=" * 50
90
+ puts "\nPress Enter to continue or Ctrl+C to exit..."
91
+ gets
92
+ end
93
+
94
+ def list_bookmarks
95
+ require_relative '../lib/sergeant/config'
96
+ bookmarks = Sergeant::Config.load_bookmarks
97
+
98
+ if bookmarks.empty?
99
+ puts "No bookmarks found."
100
+ puts "Add bookmarks to ~/.sgtrc under [bookmarks] section:"
101
+ puts "\n[bookmarks]"
102
+ puts "home=/home/user"
103
+ puts "projects=~/projects"
104
+ exit 0
105
+ end
106
+
107
+ puts "Saved Bookmarks:"
108
+ puts "=" * 50
109
+ bookmarks.each do |name, path|
110
+ expanded = File.expand_path(path)
111
+ exists = File.directory?(expanded) ? "✓" : "✗"
112
+ puts " #{exists} #{name.ljust(15)} → #{path}"
113
+ end
114
+ puts "=" * 50
115
+ puts "\nUsage: sgt -b [bookmark_name]"
116
+ exit 0
117
+ end
118
+
119
+ def get_bookmark_path(bookmark_name)
120
+ require_relative '../lib/sergeant/config'
121
+ bookmarks = Sergeant::Config.load_bookmarks
122
+
123
+ unless bookmarks.key?(bookmark_name)
124
+ warn "Error: Bookmark '#{bookmark_name}' not found"
125
+ warn "Available bookmarks: #{bookmarks.keys.join(', ')}"
126
+ warn "Use 'sgt --list-bookmarks' to see all bookmarks"
127
+ exit 1
128
+ end
129
+
130
+ path = File.expand_path(bookmarks[bookmark_name])
131
+ unless File.directory?(path)
132
+ warn "Error: Bookmark path '#{path}' does not exist"
133
+ exit 1
134
+ end
135
+
136
+ path
137
+ end
138
+
139
+ # Process arguments
140
+ start_dir = nil
141
+ no_color = false
142
+ show_debug = false
143
+ pwd_mode = false
144
+ restore_session = false
145
+ bookmark_name = nil
146
+
147
+ i = 0
148
+ while i < ARGV.length
149
+ arg = ARGV[i]
150
+ case arg
151
+ when '-h', '--help'
152
+ show_help
153
+ when '-v', '--version'
154
+ show_version
155
+ when '--debug'
156
+ show_debug = true
157
+ when '--pwd'
158
+ pwd_mode = true
159
+ when '--restore'
160
+ restore_session = true
161
+ when '--list-bookmarks'
162
+ list_bookmarks
163
+ when '-b', '--bookmark'
164
+ i += 1
165
+ if i >= ARGV.length
166
+ warn "Error: --bookmark requires a bookmark name"
167
+ warn "Use 'sgt --list-bookmarks' to see available bookmarks"
168
+ exit 1
169
+ end
170
+ bookmark_name = ARGV[i]
171
+ when '--no-color'
172
+ no_color = true
173
+ when /^-/
174
+ warn "Unknown option: #{arg}"
175
+ warn "Try 'sgt --help' for more information."
176
+ exit 1
177
+ else
178
+ start_dir = arg
179
+ end
180
+ i += 1
181
+ end
182
+
183
+ # Show debug info if requested
184
+ show_debug_info if show_debug
185
+
186
+ # Handle bookmark
187
+ start_dir = get_bookmark_path(bookmark_name) if bookmark_name
188
+
189
+ # Validate and expand directory path
190
+ if start_dir
191
+ start_dir = File.expand_path(start_dir)
192
+ unless File.directory?(start_dir)
193
+ warn "Error: '#{start_dir}' is not a valid directory"
194
+ exit 1
195
+ end
196
+ end
197
+
16
198
  # Run the navigator
17
199
  begin
18
- SergeantApp.new.run
200
+ SergeantApp.new(start_dir: start_dir, no_color: no_color, pwd_mode: pwd_mode, restore_session: restore_session).run
19
201
  rescue StandardError => e
20
- warn "\nSergeant error: #{e.message}"
21
- warn e.backtrace.join("\n")
22
- exit 1
202
+ # In pwd mode, don't show error details, just exit silently with current dir
203
+ if pwd_mode
204
+ puts Dir.pwd
205
+ exit 1
206
+ else
207
+ warn "\nSergeant error: #{e.message}"
208
+ warn e.backtrace.join("\n")
209
+ exit 1
210
+ end
23
211
  end
@@ -347,6 +347,112 @@ module Sergeant
347
347
  end
348
348
  end
349
349
  end
350
+
351
+ def show_error_with_retry(filepath, error_message)
352
+ max_y = lines
353
+ max_x = cols
354
+
355
+ # Calculate modal dimensions based on message length
356
+ message_lines = [
357
+ "Error with:",
358
+ filepath.length > 60 ? "...#{filepath[-60..]}" : filepath,
359
+ "",
360
+ error_message.length > 60 ? error_message[0...60] : error_message
361
+ ]
362
+
363
+ modal_height = 11
364
+ modal_width = [70, max_x - 4].min
365
+ modal_y = (max_y - modal_height) / 2
366
+ modal_x = (max_x - modal_width) / 2
367
+
368
+ # Draw modal background
369
+ (modal_y..(modal_y + modal_height)).each do |y|
370
+ setpos(y, modal_x)
371
+ attron(color_pair(3)) do
372
+ addstr(' ' * modal_width)
373
+ end
374
+ end
375
+
376
+ # Draw border and title
377
+ setpos(modal_y, modal_x)
378
+ attron(color_pair(4) | Curses::A_BOLD) do
379
+ addstr("\u250C#{'─' * (modal_width - 2)}\u2510")
380
+ end
381
+
382
+ setpos(modal_y + 1, modal_x)
383
+ attron(color_pair(4) | Curses::A_BOLD) do
384
+ addstr('│')
385
+ end
386
+ attron(color_pair(4) | Curses::A_BOLD) do
387
+ addstr(' Error '.center(modal_width - 2))
388
+ end
389
+ attron(color_pair(4) | Curses::A_BOLD) do
390
+ addstr('│')
391
+ end
392
+
393
+ setpos(modal_y + 2, modal_x)
394
+ attron(color_pair(4)) do
395
+ addstr("\u251C#{'─' * (modal_width - 2)}\u2524")
396
+ end
397
+
398
+ # Draw message lines
399
+ message_lines.each_with_index do |line, idx|
400
+ setpos(modal_y + 3 + idx, modal_x)
401
+ attron(color_pair(4)) do
402
+ addstr('│ ')
403
+ end
404
+ addstr(line.ljust(modal_width - 4))
405
+ attron(color_pair(4)) do
406
+ addstr(' │')
407
+ end
408
+ end
409
+
410
+ # Draw separator
411
+ setpos(modal_y + 7, modal_x)
412
+ attron(color_pair(4)) do
413
+ addstr("\u251C#{'─' * (modal_width - 2)}\u2524")
414
+ end
415
+
416
+ # Draw options
417
+ setpos(modal_y + 8, modal_x)
418
+ attron(color_pair(4)) do
419
+ addstr('│ ')
420
+ end
421
+ addstr('[S]kip [R]etry [A]bort'.ljust(modal_width - 4))
422
+ attron(color_pair(4)) do
423
+ addstr(' │')
424
+ end
425
+
426
+ setpos(modal_y + 9, modal_x)
427
+ attron(color_pair(4)) do
428
+ addstr('│ ')
429
+ end
430
+ addstr('Choose action:'.ljust(modal_width - 4))
431
+ attron(color_pair(4)) do
432
+ addstr(' │')
433
+ end
434
+
435
+ # Draw bottom border
436
+ setpos(modal_y + modal_height - 1, modal_x)
437
+ attron(color_pair(4) | Curses::A_BOLD) do
438
+ addstr("\u2514#{'─' * (modal_width - 2)}\u2518")
439
+ end
440
+
441
+ refresh
442
+
443
+ # Handle input
444
+ loop do
445
+ ch = getch
446
+ case ch
447
+ when 's', 'S'
448
+ return :skip
449
+ when 'r', 'R'
450
+ return :retry
451
+ when 'a', 'A', 27 # A or ESC = abort
452
+ return :abort
453
+ end
454
+ end
455
+ end
350
456
  end
351
457
  end
352
458
  end
@@ -9,7 +9,7 @@ module Sergeant
9
9
  max_y = lines
10
10
  max_x = cols
11
11
 
12
- modal_height = [26, max_y - 4].min # Adaptive height
12
+ modal_height = [28, max_y - 4].min # Adaptive height
13
13
  modal_width = [70, max_x - 4].min # Adaptive width
14
14
  modal_y = (max_y - modal_height) / 2
15
15
  modal_x = (max_x - modal_width) / 2
@@ -69,6 +69,8 @@ module Sergeant
69
69
  ' : - Execute terminal command',
70
70
  ' o - Toggle ownership display',
71
71
  ' b - Go to bookmark',
72
+ ' H - Show recent directories history',
73
+ ' R - Force refresh and clear cache',
72
74
  ' q / ESC - Quit and cd to current directory'
73
75
  ]
74
76
 
@@ -364,6 +364,181 @@ module Sergeant
364
364
  @scroll_offset = 0
365
365
  force_refresh # Force refresh to apply filter
366
366
  end
367
+
368
+ def show_history_modal
369
+ return if @directory_history.empty? && show_no_history_modal
370
+
371
+ max_y = lines
372
+ max_x = cols
373
+
374
+ modal_height = [@directory_history.length + 8, max_y - 4].min
375
+ modal_width = [70, max_x - 4].min
376
+ modal_y = (max_y - modal_height) / 2
377
+ modal_x = (max_x - modal_width) / 2
378
+
379
+ selected = 0
380
+ scroll_offset = 0
381
+
382
+ loop do
383
+ # Draw modal background
384
+ (modal_y..(modal_y + modal_height)).each do |y|
385
+ setpos(y, modal_x)
386
+ attron(color_pair(3)) do
387
+ addstr(' ' * modal_width)
388
+ end
389
+ end
390
+
391
+ # Draw border
392
+ setpos(modal_y, modal_x)
393
+ attron(color_pair(4) | Curses::A_BOLD) do
394
+ addstr("\u250C#{'─' * (modal_width - 2)}\u2510")
395
+ end
396
+
397
+ setpos(modal_y + 1, modal_x)
398
+ attron(color_pair(4) | Curses::A_BOLD) do
399
+ addstr('│')
400
+ end
401
+ attron(color_pair(5) | Curses::A_BOLD) do
402
+ title = ' Recent Directories '.center(modal_width - 2)
403
+ addstr(title)
404
+ end
405
+ attron(color_pair(4) | Curses::A_BOLD) do
406
+ addstr('│')
407
+ end
408
+
409
+ setpos(modal_y + 2, modal_x)
410
+ attron(color_pair(4)) do
411
+ addstr("\u251C#{'─' * (modal_width - 2)}\u2524")
412
+ end
413
+
414
+ # Draw history entries
415
+ visible_height = modal_height - 7
416
+ visible_history = @directory_history[scroll_offset...(scroll_offset + visible_height)]
417
+
418
+ visible_history.each_with_index do |dir, idx|
419
+ actual_idx = scroll_offset + idx
420
+ setpos(modal_y + 3 + idx, modal_x)
421
+ attron(color_pair(4)) do
422
+ addstr('│ ')
423
+ end
424
+
425
+ # Highlight selected
426
+ if actual_idx == selected
427
+ attron(color_pair(3) | Curses::A_BOLD) do
428
+ display_dir = dir.length > modal_width - 6 ? "...#{dir[-(modal_width - 9)..]}" : dir
429
+ addstr(display_dir.ljust(modal_width - 4))
430
+ end
431
+ else
432
+ display_dir = dir.length > modal_width - 6 ? "...#{dir[-(modal_width - 9)..]}" : dir
433
+ addstr(display_dir.ljust(modal_width - 4))
434
+ end
435
+
436
+ attron(color_pair(4)) do
437
+ addstr(' │')
438
+ end
439
+ end
440
+
441
+ # Fill remaining lines
442
+ (visible_history.length...visible_height).each do |idx|
443
+ setpos(modal_y + 3 + idx, modal_x)
444
+ attron(color_pair(4)) do
445
+ addstr("\u2502#{' ' * (modal_width - 2)}\u2502")
446
+ end
447
+ end
448
+
449
+ # Draw footer
450
+ setpos(modal_y + modal_height - 4, modal_x)
451
+ attron(color_pair(4)) do
452
+ addstr("\u251C#{'─' * (modal_width - 2)}\u2524")
453
+ end
454
+
455
+ setpos(modal_y + modal_height - 3, modal_x)
456
+ attron(color_pair(4)) do
457
+ addstr('│ ')
458
+ end
459
+ addstr('↑/↓: Navigate Enter: Go ESC: Cancel'.ljust(modal_width - 4))
460
+ attron(color_pair(4)) do
461
+ addstr(' │')
462
+ end
463
+
464
+ setpos(modal_y + modal_height - 2, modal_x)
465
+ attron(color_pair(4)) do
466
+ addstr('│ ')
467
+ end
468
+ addstr("#{selected + 1}/#{@directory_history.length}".ljust(modal_width - 4))
469
+ attron(color_pair(4)) do
470
+ addstr(' │')
471
+ end
472
+
473
+ setpos(modal_y + modal_height - 1, modal_x)
474
+ attron(color_pair(4)) do
475
+ addstr("\u2514#{'─' * (modal_width - 2)}\u2518")
476
+ end
477
+
478
+ refresh
479
+
480
+ # Handle input
481
+ ch = getch
482
+ case ch
483
+ when Curses::Key::UP, 'k'
484
+ selected = [selected - 1, 0].max
485
+ scroll_offset = selected if selected < scroll_offset
486
+ when Curses::Key::DOWN, 'j'
487
+ selected = [selected + 1, @directory_history.length - 1].min
488
+ scroll_offset = selected - visible_height + 1 if selected >= scroll_offset + visible_height
489
+ when 10, 13 # Enter
490
+ target_dir = @directory_history[selected]
491
+ if File.directory?(target_dir)
492
+ @current_dir = target_dir
493
+ @selected_index = 0
494
+ @scroll_offset = 0
495
+ force_refresh
496
+ end
497
+ break
498
+ when 27, 'q' # ESC
499
+ break
500
+ end
501
+ end
502
+ end
503
+
504
+ def show_no_history_modal
505
+ max_y = lines
506
+ max_x = cols
507
+
508
+ modal_height = 8
509
+ modal_width = 50
510
+ modal_y = (max_y - modal_height) / 2
511
+ modal_x = (max_x - modal_width) / 2
512
+
513
+ (modal_y..(modal_y + modal_height)).each do |y|
514
+ setpos(y, modal_x)
515
+ attron(color_pair(3)) do
516
+ addstr(' ' * modal_width)
517
+ end
518
+ end
519
+
520
+ setpos(modal_y + 3, modal_x)
521
+ attron(color_pair(4)) do
522
+ addstr('│ ')
523
+ end
524
+ addstr('No directory history yet.'.ljust(modal_width - 4))
525
+ attron(color_pair(4)) do
526
+ addstr(' │')
527
+ end
528
+
529
+ setpos(modal_y + 5, modal_x)
530
+ attron(color_pair(4)) do
531
+ addstr('│ ')
532
+ end
533
+ addstr('Press any key to continue'.ljust(modal_width - 4))
534
+ attron(color_pair(4)) do
535
+ addstr(' │')
536
+ end
537
+
538
+ refresh
539
+ getch
540
+ true
541
+ end
367
542
  end
368
543
  end
369
544
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sergeant
4
- VERSION = '1.0.4'
4
+ VERSION = '1.0.6'
5
5
  end
data/lib/sergeant.rb CHANGED
@@ -21,12 +21,14 @@ class SergeantApp
21
21
  include Sergeant::Modals
22
22
  include Sergeant::Rendering
23
23
 
24
- def initialize
25
- @current_dir = Dir.pwd
24
+ def initialize(start_dir: nil, no_color: false, pwd_mode: false, restore_session: false)
25
+ @current_dir = start_dir || Dir.pwd
26
26
  @selected_index = 0
27
27
  @scroll_offset = 0
28
28
  @show_ownership = false
29
29
  @last_show_ownership = false
30
+ @no_color = no_color
31
+ @pwd_mode = pwd_mode
30
32
  @config = Sergeant::Config.load_config
31
33
  @bookmarks = Sergeant::Config.load_bookmarks
32
34
  @marked_items = []
@@ -36,13 +38,30 @@ class SergeantApp
36
38
  @items = []
37
39
  @filter_text = ''
38
40
  @all_items = []
41
+
42
+ # Stat caching for performance
43
+ @stat_cache = {}
44
+ @cache_ttl = 5 # seconds
45
+ @max_cache_entries = 5000
46
+
47
+ # Session persistence
48
+ @session_file = File.expand_path('~/.sgt_session')
49
+ if restore_session && File.exist?(@session_file)
50
+ saved_dir = File.read(@session_file).strip
51
+ @current_dir = saved_dir if File.directory?(saved_dir)
52
+ end
53
+
54
+ # Recent directories history
55
+ @history_file = File.expand_path('~/.sgt_history')
56
+ @directory_history = load_history
57
+ @history_max_size = 50
39
58
  end
40
59
 
41
60
  def run
42
61
  init_screen
43
62
 
44
- # Only initialize colors if terminal supports them
45
- if has_colors?
63
+ # Only initialize colors if terminal supports them and not disabled
64
+ if !@no_color && has_colors?
46
65
  start_color
47
66
  apply_color_theme
48
67
  end
@@ -104,8 +123,15 @@ class SergeantApp
104
123
  search_files
105
124
  when 'f'
106
125
  filter_current_view
126
+ when 'H'
127
+ show_history_modal
128
+ when 'R'
129
+ # Force refresh and clear cache
130
+ @stat_cache.clear
131
+ force_refresh
107
132
  when 'q', 27
108
133
  close_screen
134
+ save_session # Save current directory for --restore
109
135
  puts @current_dir
110
136
  exit 0
111
137
  when Curses::Key::LEFT, 'h'
@@ -119,6 +145,7 @@ class SergeantApp
119
145
  end
120
146
  rescue Interrupt
121
147
  close_screen
148
+ save_session
122
149
  exit 0
123
150
  rescue StandardError => e
124
151
  close_screen
@@ -196,6 +223,99 @@ class SergeantApp
196
223
  @scroll_offset = 0
197
224
  end
198
225
 
226
+ # Stat caching for performance
227
+ def cached_stat(path)
228
+ now = Time.now
229
+
230
+ # Check if we have a cached stat for this path
231
+ if @stat_cache[path]
232
+ cached_entry = @stat_cache[path]
233
+ age = now - cached_entry[:time]
234
+
235
+ # If cache is still fresh (less than TTL), return it
236
+ return cached_entry[:stat] if age < @cache_ttl
237
+ end
238
+
239
+ # Cache miss or expired - fetch fresh stat
240
+ stat = File.stat(path)
241
+
242
+ # Store in cache with timestamp
243
+ @stat_cache[path] = {
244
+ stat: stat,
245
+ time: now
246
+ }
247
+
248
+ # Cleanup cache if it's too large
249
+ cleanup_cache if @stat_cache.size > @max_cache_entries
250
+
251
+ stat
252
+ rescue Errno::ENOENT, Errno::EACCES
253
+ # File was deleted or no permission - remove from cache
254
+ @stat_cache.delete(path)
255
+ nil
256
+ end
257
+
258
+ def clear_cache_for_directory(dir)
259
+ # Remove all cached stats for files in this directory
260
+ @stat_cache.delete_if { |path, _| path.start_with?(dir) }
261
+ end
262
+
263
+ def cleanup_cache
264
+ now = Time.now
265
+
266
+ # Remove entries older than TTL
267
+ @stat_cache.delete_if do |_, entry|
268
+ (now - entry[:time]) > @cache_ttl
269
+ end
270
+
271
+ # If still too large, remove oldest entries
272
+ if @stat_cache.size > @max_cache_entries
273
+ sorted = @stat_cache.sort_by { |_, entry| entry[:time] }
274
+ to_remove = @stat_cache.size - @max_cache_entries
275
+ sorted.first(to_remove).each do |path, _|
276
+ @stat_cache.delete(path)
277
+ end
278
+ end
279
+ end
280
+
281
+ # Session persistence
282
+ def save_session
283
+ File.write(@session_file, @current_dir)
284
+ rescue StandardError
285
+ # Silently ignore session save errors
286
+ end
287
+
288
+ # Directory history
289
+ def load_history
290
+ return [] unless File.exist?(@history_file)
291
+
292
+ File.readlines(@history_file).map(&:strip).reject(&:empty?)
293
+ rescue StandardError
294
+ []
295
+ end
296
+
297
+ def save_history
298
+ File.write(@history_file, @directory_history.join("\n"))
299
+ rescue StandardError
300
+ # Silently ignore history save errors
301
+ end
302
+
303
+ def add_to_history(dir)
304
+ # Don't add duplicates or current dir if it's already at the top
305
+ return if @directory_history.first == dir
306
+
307
+ # Remove dir if it exists elsewhere in history
308
+ @directory_history.delete(dir)
309
+
310
+ # Add to front
311
+ @directory_history.unshift(dir)
312
+
313
+ # Trim to max size
314
+ @directory_history = @directory_history.first(@history_max_size)
315
+
316
+ save_history
317
+ end
318
+
199
319
  def refresh_items_if_needed
200
320
  # Only refresh if directory has changed, or if showing ownership toggle changed
201
321
  # This prevents expensive file system operations on every keystroke
@@ -203,12 +323,18 @@ class SergeantApp
203
323
  refresh_items
204
324
  @last_refreshed_dir = @current_dir
205
325
  @last_show_ownership = @show_ownership
326
+
327
+ # Add to history when directory changes
328
+ add_to_history(@current_dir) if @current_dir != @last_refreshed_dir
206
329
  end
207
330
  end
208
331
 
209
332
  def force_refresh
210
333
  # Force a refresh even if directory hasn't changed (e.g., after file operations)
211
334
  @last_refreshed_dir = nil
335
+
336
+ # Also clear cache for current directory
337
+ clear_cache_for_directory(@current_dir)
212
338
  end
213
339
 
214
340
  def refresh_items
@@ -233,7 +359,9 @@ class SergeantApp
233
359
  entries.each do |entry|
234
360
  full_path = File.join(@current_dir, entry)
235
361
  begin
236
- stat = File.stat(full_path)
362
+ stat = cached_stat(full_path) # Use cached stat for performance
363
+ next unless stat # Skip if file was deleted or no permission
364
+
237
365
  is_dir = stat.directory? # Use stat instead of File.directory? (saves syscall)
238
366
  owner_info = @show_ownership ? get_owner_info(stat) : nil # Only fetch if needed
239
367
  perms = @show_ownership ? format_permissions(stat.mode, is_dir) : nil
data/sergeant.gemspec CHANGED
@@ -36,6 +36,25 @@ Gem::Specification.new do |spec|
36
36
  spec.executables = ['sgt']
37
37
  spec.require_paths = ['lib']
38
38
 
39
+ # Post-install message
40
+ spec.post_install_message = <<~MSG
41
+ ╔═══════════════════════════════════════════════════════════════╗
42
+ ║ Sergeant (sgt) installed successfully! 🎖️ ║
43
+ ╚═══════════════════════════════════════════════════════════════╝
44
+
45
+ Get started:
46
+ sgt # Start in current directory
47
+ sgt ~/Documents # Start in specific directory
48
+ sgt --help # View all options
49
+
50
+ Quick tips:
51
+ • Use arrow keys or vim bindings (hjkl) to navigate
52
+ • Press 'm' for help modal with all key mappings
53
+ • Press 'f' to filter, 'v' to preview, Space to mark files
54
+
55
+ For documentation: https://github.com/biscoitinho/Sergeant
56
+ MSG
57
+
39
58
  # Runtime dependencies
40
59
  spec.add_dependency 'curses', '~> 1.4'
41
60
 
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
4
+ version: 1.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mateusz Grotha
@@ -85,6 +85,13 @@ metadata:
85
85
  homepage_uri: https://github.com/biscoitinho/Sergeant
86
86
  source_code_uri: https://github.com/biscoitinho/Sergeant
87
87
  changelog_uri: https://github.com/biscoitinho/Sergeant/blob/main/CHANGELOG.md
88
+ post_install_message: "╔═══════════════════════════════════════════════════════════════╗\n║
89
+ \ Sergeant (sgt) installed successfully! \U0001F396️ ║\n╚═══════════════════════════════════════════════════════════════╝\n\nGet
90
+ started:\n sgt # Start in current directory\n sgt ~/Documents #
91
+ Start in specific directory\n sgt --help # View all options\n\nQuick tips:\n
92
+ \ • Use arrow keys or vim bindings (hjkl) to navigate\n • Press 'm' for help modal
93
+ with all key mappings\n • Press 'f' to filter, 'v' to preview, Space to mark files\n\nFor
94
+ documentation: https://github.com/biscoitinho/Sergeant\n"
88
95
  rdoc_options: []
89
96
  require_paths:
90
97
  - lib