sergeant 1.0.7 → 1.0.8
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 +12 -0
- data/Gemfile +0 -2
- data/README.md +7 -1
- data/lib/sergeant/config.rb +9 -1
- data/lib/sergeant/modals/help.rb +1 -0
- data/lib/sergeant/rendering.rb +145 -18
- data/lib/sergeant/utils.rb +22 -0
- data/lib/sergeant/version.rb +1 -1
- data/lib/sergeant.rb +84 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 36d4f8c54ef4d4f7168cb42c81abe472b8094bf67c648096e9513bdfcca9ad15
|
|
4
|
+
data.tar.gz: 820962c25da75cc167c88dfe3407fb1de3e294444bd5c737ec09779c46414aed
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1838e264508e70d0c4012560764396c7772e9fb2a50ea1517c52ad584ced2f5d2cecdc7d19efab344ae25d80bb036964f8aa11d39667d5f79afedac05c4c75e4
|
|
7
|
+
data.tar.gz: 8e00de4e37bb7e3d971f39eb1dbce3932131941b80af2549c0e778d23a3c5a6a90b0d57e5aa956582467e112d3e052e0f496c71d602c61ccebbd06a453bff2cd
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,18 @@ 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
8
|
|
|
9
|
+
## [1.0.8] - 2026-08-19
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- **Screen flicker on every keypress in fast terminals**
|
|
13
|
+
- `draw_screen` was calling curses' `clear`, forcing a full blank-then-redraw on each keystroke
|
|
14
|
+
- Barely visible on slower/buffered terminals but very noticeable on fast, immediate-mode terminals like Alacritty
|
|
15
|
+
- Switched to `erase`, which only rewrites the virtual buffer so `refresh` repaints just the changed cells
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **File-type colors** - files are now color-coded by category (archives, media, code, executables) in addition to the existing directory/file distinction, configurable via `~/.sgtrc`
|
|
19
|
+
- **Live side preview panel** (`P` key) - shows the first lines of a text file or a directory's contents in a panel next to the list, without leaving the UI or shelling out to a pager. Appears automatically once the terminal is wide enough
|
|
20
|
+
|
|
9
21
|
## [1.0.7] - 2026-03-09
|
|
10
22
|
|
|
11
23
|
### Added
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -15,7 +15,8 @@ Simple, fast, and elegant.
|
|
|
15
15
|
### Navigation & Display
|
|
16
16
|
- 🗂️ **Visual Directory Navigation** - See all directories and files at a glance
|
|
17
17
|
- ⌨️ **Keyboard Driven** - Arrow keys, vim bindings (hjkl), and shortcuts
|
|
18
|
-
- 🎨 **Color-Coded Display** - Directories in cyan
|
|
18
|
+
- 🎨 **Color-Coded Display** - Directories in cyan; files colored by type (code, archives, media, executables)
|
|
19
|
+
- 👀 **Live Preview Pane** - Text files and directory contents preview in a side panel as you move (press 'P' to toggle)
|
|
19
20
|
- 📊 **Smart Scrolling** - Handles directories with hundreds of items
|
|
20
21
|
- 🔍 **Git Branch Display** - Shows current git branch in header
|
|
21
22
|
- 👤 **Ownership Toggle** - View file permissions and ownership (press 'o')
|
|
@@ -244,6 +245,7 @@ cd $(sgt --pwd /usr/local)
|
|
|
244
245
|
| `/` | Search files (requires fzf) |
|
|
245
246
|
| `:` | Execute terminal command in current directory |
|
|
246
247
|
| `o` | Toggle ownership/permissions display |
|
|
248
|
+
| `P` | Toggle side preview panel |
|
|
247
249
|
| `b` | Go to bookmark |
|
|
248
250
|
| `H` | Show recent directories history |
|
|
249
251
|
| `R` | Force refresh and clear cache |
|
|
@@ -264,6 +266,10 @@ selected_fg=black
|
|
|
264
266
|
header=yellow
|
|
265
267
|
path=green
|
|
266
268
|
git_branch=magenta
|
|
269
|
+
archives=red
|
|
270
|
+
media=magenta
|
|
271
|
+
code=blue
|
|
272
|
+
executables=green
|
|
267
273
|
|
|
268
274
|
# Bookmarks
|
|
269
275
|
[bookmarks]
|
data/lib/sergeant/config.rb
CHANGED
|
@@ -22,7 +22,11 @@ module Sergeant
|
|
|
22
22
|
'selected_fg' => 'black',
|
|
23
23
|
'header' => 'yellow',
|
|
24
24
|
'path' => 'green',
|
|
25
|
-
'git_branch' => 'magenta'
|
|
25
|
+
'git_branch' => 'magenta',
|
|
26
|
+
'archives' => 'red',
|
|
27
|
+
'media' => 'magenta',
|
|
28
|
+
'code' => 'blue',
|
|
29
|
+
'executables' => 'green'
|
|
26
30
|
}.freeze
|
|
27
31
|
|
|
28
32
|
MINIMAL_CONFIG_TEMPLATE = <<~CONFIG.freeze
|
|
@@ -35,6 +39,10 @@ module Sergeant
|
|
|
35
39
|
header=yellow
|
|
36
40
|
path=green
|
|
37
41
|
git_branch=magenta
|
|
42
|
+
archives=red
|
|
43
|
+
media=magenta
|
|
44
|
+
code=blue
|
|
45
|
+
executables=green
|
|
38
46
|
|
|
39
47
|
# Bookmarks
|
|
40
48
|
[bookmarks]
|
data/lib/sergeant/modals/help.rb
CHANGED
data/lib/sergeant/rendering.rb
CHANGED
|
@@ -11,12 +11,40 @@ module Sergeant
|
|
|
11
11
|
ICON_MARK = WINDOWS ? '* ' : '✓ '
|
|
12
12
|
ICON_SELECT = WINDOWS ? '> ' : '▶ '
|
|
13
13
|
|
|
14
|
+
# Side preview panel only kicks in once the terminal is wide enough that
|
|
15
|
+
# both panes stay readable; below that it just doesn't fit.
|
|
16
|
+
MIN_PREVIEW_TOTAL_WIDTH = 100
|
|
17
|
+
MIN_PREVIEW_WIDTH = 24
|
|
18
|
+
PREVIEW_WIDTH_RATIO = 0.35
|
|
19
|
+
|
|
20
|
+
# Maps file_category (Sergeant::Utils) to the color pair set up in
|
|
21
|
+
# Sergeant#apply_color_theme (pairs 7-10).
|
|
22
|
+
FILE_CATEGORY_COLOR_PAIRS = {
|
|
23
|
+
archive: 7,
|
|
24
|
+
media: 8,
|
|
25
|
+
code: 9,
|
|
26
|
+
executable: 10
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
14
29
|
def draw_screen
|
|
15
|
-
|
|
30
|
+
# `erase` only rewrites the virtual buffer; `refresh` then diffs it
|
|
31
|
+
# against the physical screen and repaints just the changed cells.
|
|
32
|
+
# `clear` forces a full physical blank-then-redraw on every call,
|
|
33
|
+
# which flickers visibly on fast terminals like Alacritty.
|
|
34
|
+
erase
|
|
16
35
|
|
|
17
36
|
max_y = lines - 1
|
|
18
37
|
max_x = cols
|
|
19
38
|
|
|
39
|
+
preview_enabled = @show_preview && max_x >= MIN_PREVIEW_TOTAL_WIDTH
|
|
40
|
+
if preview_enabled
|
|
41
|
+
preview_width = [(max_x * PREVIEW_WIDTH_RATIO).to_i, MIN_PREVIEW_WIDTH].max
|
|
42
|
+
list_width = max_x - preview_width - 1
|
|
43
|
+
else
|
|
44
|
+
preview_width = 0
|
|
45
|
+
list_width = max_x
|
|
46
|
+
end
|
|
47
|
+
|
|
20
48
|
setpos(0, 0)
|
|
21
49
|
attron(color_pair(4) | Curses::A_BOLD) do
|
|
22
50
|
addstr("┌─ Sergeant - 'Leave it to the Sarge!' ".ljust(max_x, '─'))
|
|
@@ -42,9 +70,11 @@ module Sergeant
|
|
|
42
70
|
end
|
|
43
71
|
status_text = status_parts.empty? ? '' : " | #{status_parts.join(' | ')}"
|
|
44
72
|
|
|
73
|
+
status_width = list_width
|
|
74
|
+
|
|
45
75
|
if branch
|
|
46
76
|
branch_text = " [#{branch}]"
|
|
47
|
-
path_max_length =
|
|
77
|
+
path_max_length = status_width - 4 - branch_text.length - status_text.length
|
|
48
78
|
path_display = @current_dir.length > path_max_length ? "...#{@current_dir[(-path_max_length + 3)..]}" : @current_dir
|
|
49
79
|
|
|
50
80
|
attron(color_pair(5)) do
|
|
@@ -58,9 +88,9 @@ module Sergeant
|
|
|
58
88
|
addstr(status_text)
|
|
59
89
|
end
|
|
60
90
|
end
|
|
61
|
-
remaining =
|
|
91
|
+
remaining = status_width - 2 - path_display.length - branch_text.length - status_text.length
|
|
62
92
|
else
|
|
63
|
-
path_max_length =
|
|
93
|
+
path_max_length = status_width - 4 - status_text.length
|
|
64
94
|
path_display = @current_dir.length > path_max_length ? "...#{@current_dir[(-path_max_length + 3)..]}" : @current_dir
|
|
65
95
|
|
|
66
96
|
attron(color_pair(5)) do
|
|
@@ -71,7 +101,7 @@ module Sergeant
|
|
|
71
101
|
addstr(status_text)
|
|
72
102
|
end
|
|
73
103
|
end
|
|
74
|
-
remaining =
|
|
104
|
+
remaining = status_width - 2 - path_display.length - status_text.length
|
|
75
105
|
end
|
|
76
106
|
addstr(''.ljust(remaining)) if remaining.positive?
|
|
77
107
|
|
|
@@ -103,18 +133,8 @@ module Sergeant
|
|
|
103
133
|
|
|
104
134
|
is_selected = actual_index == @selected_index
|
|
105
135
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
draw_item(item, max_x, true)
|
|
109
|
-
end
|
|
110
|
-
elsif item[:type] == :directory
|
|
111
|
-
attron(color_pair(1)) do
|
|
112
|
-
draw_item(item, max_x, false)
|
|
113
|
-
end
|
|
114
|
-
else
|
|
115
|
-
attron(color_pair(2) | Curses::A_DIM) do
|
|
116
|
-
draw_item(item, max_x, false)
|
|
117
|
-
end
|
|
136
|
+
attron(item_attributes(item, is_selected)) do
|
|
137
|
+
draw_item(item, list_width, is_selected)
|
|
118
138
|
end
|
|
119
139
|
end
|
|
120
140
|
|
|
@@ -124,15 +144,28 @@ module Sergeant
|
|
|
124
144
|
scroll_pos = (@scroll_offset.to_f / (total - visible)) * (visible - 1)
|
|
125
145
|
scroll_pos = scroll_pos.round.clamp(0, visible - 1)
|
|
126
146
|
|
|
127
|
-
setpos(3 + scroll_pos,
|
|
147
|
+
setpos(3 + scroll_pos, list_width - 1)
|
|
128
148
|
attron(color_pair(4) | Curses::A_BOLD) do
|
|
129
149
|
addstr('█')
|
|
130
150
|
end
|
|
131
151
|
end
|
|
132
152
|
|
|
153
|
+
draw_preview_panel(list_width, preview_width, max_y, visible_lines) if preview_enabled
|
|
154
|
+
|
|
133
155
|
refresh
|
|
134
156
|
end
|
|
135
157
|
|
|
158
|
+
# Which curses attributes to draw a list row with: selection highlight
|
|
159
|
+
# wins, then directories, then a file's type-based color (falling back
|
|
160
|
+
# to the plain dimmed file color when its category has none).
|
|
161
|
+
def item_attributes(item, is_selected)
|
|
162
|
+
return color_pair(3) | Curses::A_BOLD if is_selected
|
|
163
|
+
return color_pair(1) if item[:type] == :directory
|
|
164
|
+
|
|
165
|
+
pair = FILE_CATEGORY_COLOR_PAIRS[file_category(item)]
|
|
166
|
+
pair ? color_pair(pair) : (color_pair(2) | Curses::A_DIM)
|
|
167
|
+
end
|
|
168
|
+
|
|
136
169
|
def draw_item(item, max_x, is_selected)
|
|
137
170
|
icon = item[:type] == :directory ? ICON_DIR : ICON_FILE
|
|
138
171
|
|
|
@@ -171,5 +204,99 @@ module Sergeant
|
|
|
171
204
|
|
|
172
205
|
addstr(display)
|
|
173
206
|
end
|
|
207
|
+
|
|
208
|
+
# Draws the divider, title row and content of the side preview panel.
|
|
209
|
+
# Content itself is precomputed by Sergeant#update_preview_if_needed -
|
|
210
|
+
# this method only ever paints what's already in @preview_lines, so it
|
|
211
|
+
# stays cheap even though draw_screen runs on every keystroke.
|
|
212
|
+
def draw_preview_panel(list_width, preview_width, max_y, visible_lines)
|
|
213
|
+
col = list_width + 1
|
|
214
|
+
width = preview_width - 1
|
|
215
|
+
return if width <= 1
|
|
216
|
+
|
|
217
|
+
draw_preview_divider(list_width, max_y)
|
|
218
|
+
draw_preview_title(col, width)
|
|
219
|
+
|
|
220
|
+
case @preview_kind
|
|
221
|
+
when :text
|
|
222
|
+
if @preview_lines.empty?
|
|
223
|
+
draw_preview_message(col, width, '(empty file)')
|
|
224
|
+
else
|
|
225
|
+
draw_preview_lines(col, width, visible_lines, @preview_lines, color_pair(2))
|
|
226
|
+
end
|
|
227
|
+
when :directory
|
|
228
|
+
if @preview_lines.empty?
|
|
229
|
+
draw_preview_message(col, width, '(empty directory)')
|
|
230
|
+
else
|
|
231
|
+
names = @preview_lines.map { |name| " #{name}" }
|
|
232
|
+
draw_preview_lines(col, width, visible_lines, names, color_pair(1))
|
|
233
|
+
end
|
|
234
|
+
else
|
|
235
|
+
draw_preview_message(col, width, preview_placeholder_message)
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def draw_preview_divider(list_width, max_y)
|
|
240
|
+
setpos(0, list_width)
|
|
241
|
+
attron(color_pair(4) | Curses::A_BOLD) { addstr('┬') }
|
|
242
|
+
|
|
243
|
+
(1...max_y).each do |row|
|
|
244
|
+
next if row == 2
|
|
245
|
+
|
|
246
|
+
setpos(row, list_width)
|
|
247
|
+
attron(color_pair(4)) { addstr('│') }
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
setpos(2, list_width)
|
|
251
|
+
attron(color_pair(4)) { addstr('┼') }
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def draw_preview_title(col, width)
|
|
255
|
+
label = case @preview_kind
|
|
256
|
+
when :directory then "#{ICON_DIR}#{@preview_item[:name]}"
|
|
257
|
+
when :text, :binary then "#{ICON_FILE}#{@preview_item[:name]}"
|
|
258
|
+
else ''
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
setpos(1, col)
|
|
262
|
+
attron(color_pair(5) | Curses::A_BOLD) do
|
|
263
|
+
addstr(truncate_for_preview(label, width).ljust(width))
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def draw_preview_lines(col, width, visible_lines, source_lines, pair)
|
|
268
|
+
visible_lines.times do |idx|
|
|
269
|
+
setpos(idx + 3, col)
|
|
270
|
+
line = source_lines[idx]
|
|
271
|
+
attron(pair) do
|
|
272
|
+
addstr((line ? truncate_for_preview(line, width) : '').ljust(width))
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def draw_preview_message(col, width, message)
|
|
278
|
+
setpos(3, col)
|
|
279
|
+
attron(color_pair(2) | Curses::A_DIM) do
|
|
280
|
+
addstr(truncate_for_preview(message, width).ljust(width))
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def truncate_for_preview(text, width)
|
|
285
|
+
return text if text.length <= width
|
|
286
|
+
return text[0, width].to_s if width <= 1
|
|
287
|
+
|
|
288
|
+
"#{text[0, width - 1]}…"
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def preview_placeholder_message
|
|
292
|
+
case @preview_kind
|
|
293
|
+
when :empty then '(empty directory)'
|
|
294
|
+
when :binary
|
|
295
|
+
size = @preview_item && @preview_item[:size]
|
|
296
|
+
size ? "(no preview - #{format_size(size).strip})" : '(no preview available)'
|
|
297
|
+
else
|
|
298
|
+
''
|
|
299
|
+
end
|
|
300
|
+
end
|
|
174
301
|
end
|
|
175
302
|
end
|
data/lib/sergeant/utils.rb
CHANGED
|
@@ -4,6 +4,28 @@
|
|
|
4
4
|
|
|
5
5
|
module Sergeant
|
|
6
6
|
module Utils
|
|
7
|
+
ARCHIVE_EXTENSIONS = %w[.zip .tar .gz .tgz .bz2 .tbz2 .xz .txz .7z .rar .zst .lz .lzma .cab .iso .deb .rpm .apk
|
|
8
|
+
.jar .war].freeze
|
|
9
|
+
MEDIA_EXTENSIONS = %w[.png .jpg .jpeg .gif .bmp .svg .webp .ico .tiff .tif .heic .avif
|
|
10
|
+
.mp4 .mkv .avi .mov .wmv .flv .webm .m4v .mpg .mpeg
|
|
11
|
+
.mp3 .wav .flac .ogg .aac .m4a .wma .opus].freeze
|
|
12
|
+
CODE_EXTENSIONS = %w[.rb .py .js .jsx .mjs .cjs .ts .tsx .go .rs .c .cpp .cc .h .hpp .java .kt .kts .swift .php
|
|
13
|
+
.sh .bash .zsh .fish .lua .pl .pm .r .scala .clj .cljs .ex .exs .erl .hs .ml .mli .sql
|
|
14
|
+
.html .htm .css .scss .sass .less .json .yaml .yml .toml .xml .vue .svelte].freeze
|
|
15
|
+
|
|
16
|
+
# Which color pair (see Sergeant#apply_color_theme) to use for a file,
|
|
17
|
+
# based on its extension, falling back to the executable bit. Directories
|
|
18
|
+
# are colored separately by draw_screen and never reach this method.
|
|
19
|
+
def file_category(item)
|
|
20
|
+
ext = File.extname(item[:name]).downcase
|
|
21
|
+
return :archive if ARCHIVE_EXTENSIONS.include?(ext)
|
|
22
|
+
return :media if MEDIA_EXTENSIONS.include?(ext)
|
|
23
|
+
return :code if CODE_EXTENSIONS.include?(ext)
|
|
24
|
+
return :executable if item[:mode] && (item[:mode] & 0o111).nonzero? && ext.empty?
|
|
25
|
+
|
|
26
|
+
:file
|
|
27
|
+
end
|
|
28
|
+
|
|
7
29
|
def get_git_branch
|
|
8
30
|
return nil unless Dir.exist?(File.join(@current_dir, '.git'))
|
|
9
31
|
|
data/lib/sergeant/version.rb
CHANGED
data/lib/sergeant.rb
CHANGED
|
@@ -21,6 +21,10 @@ class SergeantApp
|
|
|
21
21
|
include Sergeant::Modals
|
|
22
22
|
include Sergeant::Rendering
|
|
23
23
|
|
|
24
|
+
# Side preview panel content limits
|
|
25
|
+
PREVIEW_LINE_LIMIT = 500
|
|
26
|
+
PREVIEW_LINE_MAX_BYTES = 2000
|
|
27
|
+
|
|
24
28
|
def initialize(start_dir: nil, no_color: false, pwd_mode: false, restore_session: false)
|
|
25
29
|
@current_dir = start_dir || Dir.pwd
|
|
26
30
|
@selected_index = 0
|
|
@@ -39,6 +43,13 @@ class SergeantApp
|
|
|
39
43
|
@filter_text = ''
|
|
40
44
|
@all_items = []
|
|
41
45
|
|
|
46
|
+
# Side preview panel
|
|
47
|
+
@show_preview = true
|
|
48
|
+
@last_preview_path = :unset
|
|
49
|
+
@preview_kind = :empty
|
|
50
|
+
@preview_item = nil
|
|
51
|
+
@preview_lines = []
|
|
52
|
+
|
|
42
53
|
# Stat caching for performance
|
|
43
54
|
@stat_cache = {}
|
|
44
55
|
@cache_ttl = 5 # seconds
|
|
@@ -74,6 +85,7 @@ class SergeantApp
|
|
|
74
85
|
loop do
|
|
75
86
|
# Only refresh items when directory changes, not on every keystroke
|
|
76
87
|
refresh_items_if_needed
|
|
88
|
+
update_preview_if_needed
|
|
77
89
|
draw_screen
|
|
78
90
|
|
|
79
91
|
key = getch
|
|
@@ -95,6 +107,9 @@ class SergeantApp
|
|
|
95
107
|
goto_bookmark
|
|
96
108
|
when 'o'
|
|
97
109
|
@show_ownership = !@show_ownership
|
|
110
|
+
when 'P'
|
|
111
|
+
@show_preview = !@show_preview
|
|
112
|
+
@last_preview_path = :unset
|
|
98
113
|
when 'e'
|
|
99
114
|
edit_file
|
|
100
115
|
when 'v'
|
|
@@ -165,6 +180,10 @@ class SergeantApp
|
|
|
165
180
|
init_pair(4, Sergeant::Config.get_color(@config['header']), Curses::COLOR_BLACK)
|
|
166
181
|
init_pair(5, Sergeant::Config.get_color(@config['path']), Curses::COLOR_BLACK)
|
|
167
182
|
init_pair(6, Sergeant::Config.get_color(@config['git_branch']), Curses::COLOR_BLACK)
|
|
183
|
+
init_pair(7, Sergeant::Config.get_color(@config['archives']), Curses::COLOR_BLACK)
|
|
184
|
+
init_pair(8, Sergeant::Config.get_color(@config['media']), Curses::COLOR_BLACK)
|
|
185
|
+
init_pair(9, Sergeant::Config.get_color(@config['code']), Curses::COLOR_BLACK)
|
|
186
|
+
init_pair(10, Sergeant::Config.get_color(@config['executables']), Curses::COLOR_BLACK)
|
|
168
187
|
end
|
|
169
188
|
|
|
170
189
|
def search_files
|
|
@@ -335,6 +354,69 @@ class SergeantApp
|
|
|
335
354
|
|
|
336
355
|
# Also clear cache for current directory
|
|
337
356
|
clear_cache_for_directory(@current_dir)
|
|
357
|
+
|
|
358
|
+
# The selected item's content may have changed too
|
|
359
|
+
@last_preview_path = :unset
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# Side preview panel
|
|
363
|
+
|
|
364
|
+
def update_preview_if_needed
|
|
365
|
+
return unless @show_preview
|
|
366
|
+
|
|
367
|
+
item = @items[@selected_index]
|
|
368
|
+
path = item && item[:path]
|
|
369
|
+
|
|
370
|
+
# Only rebuild the preview when the selection actually points at a new
|
|
371
|
+
# path - never on every keystroke/redraw, to keep this cheap even in
|
|
372
|
+
# directories with many files.
|
|
373
|
+
return if path == @last_preview_path
|
|
374
|
+
|
|
375
|
+
@last_preview_path = path
|
|
376
|
+
@preview_item = item
|
|
377
|
+
@preview_kind, @preview_lines = build_preview(item)
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def build_preview(item)
|
|
381
|
+
return [:empty, []] unless item
|
|
382
|
+
return [:parent, []] if item[:name] == '..'
|
|
383
|
+
|
|
384
|
+
if item[:type] == :directory
|
|
385
|
+
[:directory, build_directory_preview(item[:path])]
|
|
386
|
+
elsif text_file?(item[:path])
|
|
387
|
+
[:text, build_text_preview(item[:path])]
|
|
388
|
+
else
|
|
389
|
+
[:binary, []]
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def build_directory_preview(path)
|
|
394
|
+
names = []
|
|
395
|
+
|
|
396
|
+
# each_child is lazy, so even a directory with huge numbers of entries
|
|
397
|
+
# only costs us PREVIEW_LINE_LIMIT reads, not a full scan.
|
|
398
|
+
Dir.each_child(path) do |name|
|
|
399
|
+
names << name
|
|
400
|
+
break if names.length >= PREVIEW_LINE_LIMIT
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
names.sort_by(&:downcase)
|
|
404
|
+
rescue Errno::EACCES, Errno::ENOENT, Errno::ENOTDIR
|
|
405
|
+
[]
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def build_text_preview(path)
|
|
409
|
+
lines = []
|
|
410
|
+
|
|
411
|
+
File.foreach(path, mode: 'rb') do |line|
|
|
412
|
+
text = line.byteslice(0, PREVIEW_LINE_MAX_BYTES).to_s.chomp
|
|
413
|
+
lines << text.force_encoding('UTF-8').scrub('?').gsub("\t", ' ')
|
|
414
|
+
break if lines.length >= PREVIEW_LINE_LIMIT
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
lines
|
|
418
|
+
rescue StandardError
|
|
419
|
+
[]
|
|
338
420
|
end
|
|
339
421
|
|
|
340
422
|
def refresh_items
|
|
@@ -384,7 +466,8 @@ class SergeantApp
|
|
|
384
466
|
size: stat.size,
|
|
385
467
|
mtime: stat.mtime,
|
|
386
468
|
owner: owner_info,
|
|
387
|
-
perms: perms
|
|
469
|
+
perms: perms,
|
|
470
|
+
mode: stat.mode
|
|
388
471
|
}
|
|
389
472
|
end
|
|
390
473
|
rescue Errno::EACCES, Errno::ENOENT
|