sergeant 1.0.0
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 +7 -0
- data/CHANGELOG.md +49 -0
- data/Gemfile +18 -0
- data/LICENSE +21 -0
- data/README.md +245 -0
- data/bin/sgt +13 -0
- data/lib/.DS_Store +0 -0
- data/lib/sergeant/.DS_Store +0 -0
- data/lib/sergeant/config.rb +120 -0
- data/lib/sergeant/modals/.DS_Store +0 -0
- data/lib/sergeant/modals/dialogs.rb +352 -0
- data/lib/sergeant/modals/file_operations.rb +635 -0
- data/lib/sergeant/modals/help.rb +109 -0
- data/lib/sergeant/modals/navigation.rb +245 -0
- data/lib/sergeant/modals.rb +17 -0
- data/lib/sergeant/rendering.rb +160 -0
- data/lib/sergeant/utils.rb +121 -0
- data/lib/sergeant/version.rb +5 -0
- data/lib/sergeant.rb +305 -0
- data/logo.svg +7831 -0
- data/sergeant.gemspec +43 -0
- metadata +108 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Help modal
|
|
4
|
+
|
|
5
|
+
module Sergeant
|
|
6
|
+
module Modals
|
|
7
|
+
module Help
|
|
8
|
+
def show_help_modal
|
|
9
|
+
max_y = lines
|
|
10
|
+
max_x = cols
|
|
11
|
+
|
|
12
|
+
modal_height = [26, max_y - 4].min # Adaptive height
|
|
13
|
+
modal_width = [70, max_x - 4].min # Adaptive width
|
|
14
|
+
modal_y = (max_y - modal_height) / 2
|
|
15
|
+
modal_x = (max_x - modal_width) / 2
|
|
16
|
+
|
|
17
|
+
(modal_y..(modal_y + modal_height)).each do |y|
|
|
18
|
+
setpos(y, modal_x)
|
|
19
|
+
attron(color_pair(3)) do
|
|
20
|
+
addstr(' ' * modal_width)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
setpos(modal_y, modal_x)
|
|
25
|
+
attron(color_pair(4) | Curses::A_BOLD) do
|
|
26
|
+
addstr("\u250C#{'─' * (modal_width - 2)}\u2510")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
setpos(modal_y + 1, modal_x)
|
|
30
|
+
attron(color_pair(4) | Curses::A_BOLD) do
|
|
31
|
+
addstr('│')
|
|
32
|
+
end
|
|
33
|
+
attron(color_pair(5) | Curses::A_BOLD) do
|
|
34
|
+
addstr(' Key Mappings '.center(modal_width - 2))
|
|
35
|
+
end
|
|
36
|
+
attron(color_pair(4) | Curses::A_BOLD) do
|
|
37
|
+
addstr('│')
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
setpos(modal_y + 2, modal_x)
|
|
41
|
+
attron(color_pair(4)) do
|
|
42
|
+
addstr("\u251C#{'─' * (modal_width - 2)}\u2524")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
help_lines = [
|
|
46
|
+
'Navigation:',
|
|
47
|
+
' ↑/k - Move up',
|
|
48
|
+
' ↓/j - Move down',
|
|
49
|
+
' Enter / →/l - Open directory or preview file',
|
|
50
|
+
' ←/h - Go back to parent directory',
|
|
51
|
+
'',
|
|
52
|
+
'File Operations:',
|
|
53
|
+
' Space - Mark/unmark item',
|
|
54
|
+
' c - Copy marked items',
|
|
55
|
+
' x - Cut marked items',
|
|
56
|
+
' p - Paste copied/cut items',
|
|
57
|
+
' d - Delete marked items',
|
|
58
|
+
' r - Rename current item',
|
|
59
|
+
' u - Unmark all items',
|
|
60
|
+
' n - Create new file or directory',
|
|
61
|
+
'',
|
|
62
|
+
'Other:',
|
|
63
|
+
' : - Execute terminal command',
|
|
64
|
+
' e - Edit file ($EDITOR, nano, nvim, vim)',
|
|
65
|
+
' v - Preview file (read-only)',
|
|
66
|
+
' o - Toggle ownership display',
|
|
67
|
+
' b - Go to bookmark',
|
|
68
|
+
' / - Search files (with fzf if available)',
|
|
69
|
+
' q / ESC - Quit and cd to current directory'
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
help_lines.each_with_index do |line, idx|
|
|
73
|
+
break if idx >= modal_height - 4 # Stop if we run out of vertical space
|
|
74
|
+
|
|
75
|
+
setpos(modal_y + 3 + idx, modal_x)
|
|
76
|
+
attron(color_pair(4)) do
|
|
77
|
+
addstr('│ ')
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Truncate line if it's too long for the modal width
|
|
81
|
+
display_line = if line.length > modal_width - 4
|
|
82
|
+
"#{line[0...(modal_width - 7)]}..."
|
|
83
|
+
else
|
|
84
|
+
line.ljust(modal_width - 4)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
if line.start_with?('Navigation:', 'File Operations:', 'Other:')
|
|
88
|
+
attron(color_pair(1) | Curses::A_BOLD) do
|
|
89
|
+
addstr(display_line)
|
|
90
|
+
end
|
|
91
|
+
else
|
|
92
|
+
addstr(display_line)
|
|
93
|
+
end
|
|
94
|
+
attron(color_pair(4)) do
|
|
95
|
+
addstr(' │')
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
setpos(modal_y + modal_height - 1, modal_x)
|
|
100
|
+
attron(color_pair(4) | Curses::A_BOLD) do
|
|
101
|
+
addstr("\u2514#{'─' * (modal_width - 2)}\u2518")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
refresh
|
|
105
|
+
getch
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Navigation and bookmark modals
|
|
4
|
+
|
|
5
|
+
module Sergeant
|
|
6
|
+
module Modals
|
|
7
|
+
module Navigation
|
|
8
|
+
def goto_bookmark
|
|
9
|
+
return if @bookmarks.empty? && show_no_bookmarks_modal
|
|
10
|
+
|
|
11
|
+
max_y = lines
|
|
12
|
+
max_x = cols
|
|
13
|
+
|
|
14
|
+
modal_height = [@bookmarks.length + 8, max_y - 4].min
|
|
15
|
+
modal_width = [60, max_x - 4].min
|
|
16
|
+
modal_y = (max_y - modal_height) / 2
|
|
17
|
+
modal_x = (max_x - modal_width) / 2
|
|
18
|
+
|
|
19
|
+
(modal_y..(modal_y + modal_height)).each do |y|
|
|
20
|
+
setpos(y, modal_x)
|
|
21
|
+
attron(color_pair(3)) do
|
|
22
|
+
addstr(' ' * modal_width)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
setpos(modal_y, modal_x)
|
|
27
|
+
attron(color_pair(4) | Curses::A_BOLD) do
|
|
28
|
+
addstr("\u250C#{'─' * (modal_width - 2)}\u2510")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
setpos(modal_y + 1, modal_x)
|
|
32
|
+
attron(color_pair(4) | Curses::A_BOLD) do
|
|
33
|
+
addstr('│')
|
|
34
|
+
end
|
|
35
|
+
attron(color_pair(5) | Curses::A_BOLD) do
|
|
36
|
+
title = ' Bookmarks '.center(modal_width - 2)
|
|
37
|
+
addstr(title)
|
|
38
|
+
end
|
|
39
|
+
attron(color_pair(4) | Curses::A_BOLD) do
|
|
40
|
+
addstr('│')
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
setpos(modal_y + 2, modal_x)
|
|
44
|
+
attron(color_pair(4)) do
|
|
45
|
+
addstr("\u251C#{'─' * (modal_width - 2)}\u2524")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
visible_bookmarks = @bookmarks.to_a.first(modal_height - 7)
|
|
49
|
+
visible_bookmarks.each_with_index do |(name, path), idx|
|
|
50
|
+
setpos(modal_y + 3 + idx, modal_x)
|
|
51
|
+
attron(color_pair(4)) do
|
|
52
|
+
addstr('│')
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
attron(color_pair(1) | Curses::A_BOLD) do
|
|
56
|
+
addstr(" #{name}".ljust(20))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
path_space = modal_width - 23
|
|
60
|
+
display_path = path.length > path_space ? "...#{path[-(path_space - 3)..]}" : path
|
|
61
|
+
addstr(display_path.ljust(path_space - 1))
|
|
62
|
+
|
|
63
|
+
attron(color_pair(4)) do
|
|
64
|
+
addstr('│')
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
(visible_bookmarks.length...(modal_height - 7)).each do |idx|
|
|
69
|
+
setpos(modal_y + 3 + idx, modal_x)
|
|
70
|
+
attron(color_pair(4)) do
|
|
71
|
+
addstr("\u2502#{' ' * (modal_width - 2)}\u2502")
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
input_line = modal_y + modal_height - 4
|
|
76
|
+
setpos(input_line, modal_x)
|
|
77
|
+
attron(color_pair(4)) do
|
|
78
|
+
addstr("\u251C#{'─' * (modal_width - 2)}\u2524")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
setpos(input_line + 1, modal_x)
|
|
82
|
+
attron(color_pair(4)) do
|
|
83
|
+
addstr('│')
|
|
84
|
+
end
|
|
85
|
+
prompt = ' Enter bookmark name: '
|
|
86
|
+
attron(color_pair(5)) do
|
|
87
|
+
addstr(prompt)
|
|
88
|
+
end
|
|
89
|
+
addstr(' ' * (modal_width - 2 - prompt.length))
|
|
90
|
+
attron(color_pair(4)) do
|
|
91
|
+
addstr('│')
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
setpos(input_line + 2, modal_x)
|
|
95
|
+
attron(color_pair(4)) do
|
|
96
|
+
addstr('│ ')
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
curs_set(1)
|
|
100
|
+
echo
|
|
101
|
+
setpos(input_line + 2, modal_x + 2)
|
|
102
|
+
|
|
103
|
+
input_width = modal_width - 5
|
|
104
|
+
bookmark_name = ''
|
|
105
|
+
|
|
106
|
+
loop do
|
|
107
|
+
ch = getch
|
|
108
|
+
|
|
109
|
+
case ch
|
|
110
|
+
when 10, 13
|
|
111
|
+
break
|
|
112
|
+
when 27
|
|
113
|
+
bookmark_name = ''
|
|
114
|
+
break
|
|
115
|
+
when 127, Curses::Key::BACKSPACE
|
|
116
|
+
if bookmark_name.length.positive?
|
|
117
|
+
bookmark_name = bookmark_name[0...-1]
|
|
118
|
+
setpos(input_line + 2, modal_x + 2)
|
|
119
|
+
addstr(bookmark_name.ljust(input_width))
|
|
120
|
+
setpos(input_line + 2, modal_x + 2 + bookmark_name.length)
|
|
121
|
+
end
|
|
122
|
+
else
|
|
123
|
+
if ch.is_a?(String) && bookmark_name.length < input_width
|
|
124
|
+
bookmark_name += ch
|
|
125
|
+
setpos(input_line + 2, modal_x + 2)
|
|
126
|
+
addstr(bookmark_name.ljust(input_width))
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
refresh
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
noecho
|
|
134
|
+
curs_set(0)
|
|
135
|
+
|
|
136
|
+
setpos(modal_y + modal_height - 1, modal_x)
|
|
137
|
+
attron(color_pair(4) | Curses::A_BOLD) do
|
|
138
|
+
addstr("\u2514#{'─' * (modal_width - 2)}\u2518")
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
refresh
|
|
142
|
+
|
|
143
|
+
bookmark_name = bookmark_name.strip
|
|
144
|
+
|
|
145
|
+
return if bookmark_name.empty?
|
|
146
|
+
|
|
147
|
+
if @bookmarks.key?(bookmark_name)
|
|
148
|
+
target_path = @bookmarks[bookmark_name]
|
|
149
|
+
if Dir.exist?(target_path)
|
|
150
|
+
@current_dir = target_path
|
|
151
|
+
@selected_index = 0
|
|
152
|
+
@scroll_offset = 0
|
|
153
|
+
else
|
|
154
|
+
show_error_modal("Bookmark path doesn't exist")
|
|
155
|
+
end
|
|
156
|
+
else
|
|
157
|
+
show_error_modal("Bookmark '#{bookmark_name}' not found")
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def show_no_bookmarks_modal
|
|
162
|
+
max_y = lines
|
|
163
|
+
max_x = cols
|
|
164
|
+
|
|
165
|
+
modal_height = 10
|
|
166
|
+
modal_width = 60
|
|
167
|
+
modal_y = (max_y - modal_height) / 2
|
|
168
|
+
modal_x = (max_x - modal_width) / 2
|
|
169
|
+
|
|
170
|
+
(modal_y..(modal_y + modal_height)).each do |y|
|
|
171
|
+
setpos(y, modal_x)
|
|
172
|
+
attron(color_pair(3)) do
|
|
173
|
+
addstr(' ' * modal_width)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
setpos(modal_y, modal_x)
|
|
178
|
+
attron(color_pair(4) | Curses::A_BOLD) do
|
|
179
|
+
addstr("\u250C#{'─' * (modal_width - 2)}\u2510")
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
setpos(modal_y + 1, modal_x)
|
|
183
|
+
attron(color_pair(4) | Curses::A_BOLD) do
|
|
184
|
+
addstr('│')
|
|
185
|
+
end
|
|
186
|
+
attron(color_pair(5) | Curses::A_BOLD) do
|
|
187
|
+
addstr(' No Bookmarks Defined '.center(modal_width - 2))
|
|
188
|
+
end
|
|
189
|
+
attron(color_pair(4) | Curses::A_BOLD) do
|
|
190
|
+
addstr('│')
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
setpos(modal_y + 2, modal_x)
|
|
194
|
+
attron(color_pair(4)) do
|
|
195
|
+
addstr("\u251C#{'─' * (modal_width - 2)}\u2524")
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
messages = [
|
|
199
|
+
'Add bookmarks to ~/.sgtrc:',
|
|
200
|
+
'',
|
|
201
|
+
'[bookmarks]',
|
|
202
|
+
'home=/home/user',
|
|
203
|
+
'projects=~/projects'
|
|
204
|
+
]
|
|
205
|
+
|
|
206
|
+
messages.each_with_index do |msg, idx|
|
|
207
|
+
setpos(modal_y + 3 + idx, modal_x)
|
|
208
|
+
attron(color_pair(4)) do
|
|
209
|
+
addstr('│ ')
|
|
210
|
+
end
|
|
211
|
+
if idx > 1
|
|
212
|
+
attron(color_pair(1)) do
|
|
213
|
+
addstr(msg.ljust(modal_width - 4))
|
|
214
|
+
end
|
|
215
|
+
else
|
|
216
|
+
addstr(msg.ljust(modal_width - 4))
|
|
217
|
+
end
|
|
218
|
+
attron(color_pair(4)) do
|
|
219
|
+
addstr(' │')
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
setpos(modal_y + 9, modal_x)
|
|
224
|
+
attron(color_pair(4)) do
|
|
225
|
+
addstr('│')
|
|
226
|
+
end
|
|
227
|
+
attron(color_pair(4) | Curses::A_DIM) do
|
|
228
|
+
addstr(' Press any key to continue '.center(modal_width - 2))
|
|
229
|
+
end
|
|
230
|
+
attron(color_pair(4)) do
|
|
231
|
+
addstr('│')
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
setpos(modal_y + modal_height - 1, modal_x)
|
|
235
|
+
attron(color_pair(4) | Curses::A_BOLD) do
|
|
236
|
+
addstr("\u2514#{'─' * (modal_width - 2)}\u2518")
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
refresh
|
|
240
|
+
getch
|
|
241
|
+
true
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Modal dialog windows - organized by context
|
|
4
|
+
|
|
5
|
+
require_relative 'modals/navigation'
|
|
6
|
+
require_relative 'modals/dialogs'
|
|
7
|
+
require_relative 'modals/file_operations'
|
|
8
|
+
require_relative 'modals/help'
|
|
9
|
+
|
|
10
|
+
module Sergeant
|
|
11
|
+
module Modals
|
|
12
|
+
include Navigation
|
|
13
|
+
include Dialogs
|
|
14
|
+
include FileOperations
|
|
15
|
+
include Help
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# UI rendering methods
|
|
4
|
+
|
|
5
|
+
module Sergeant
|
|
6
|
+
module Rendering
|
|
7
|
+
def draw_screen
|
|
8
|
+
clear
|
|
9
|
+
|
|
10
|
+
max_y = lines - 1
|
|
11
|
+
max_x = cols
|
|
12
|
+
|
|
13
|
+
setpos(0, 0)
|
|
14
|
+
attron(color_pair(4) | Curses::A_BOLD) do
|
|
15
|
+
addstr("┌─ Sergeant - 'Leave it to the Sarge!' ".ljust(max_x, '─'))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
setpos(1, 0)
|
|
19
|
+
branch = get_git_branch
|
|
20
|
+
|
|
21
|
+
# Build status info
|
|
22
|
+
status_parts = []
|
|
23
|
+
status_parts << "Marked: #{@marked_items.length}" unless @marked_items.empty?
|
|
24
|
+
unless @copied_items.empty?
|
|
25
|
+
mode_text = @cut_mode ? 'Cut' : 'Copied'
|
|
26
|
+
status_parts << "#{mode_text}: #{@copied_items.length}"
|
|
27
|
+
end
|
|
28
|
+
status_text = status_parts.empty? ? '' : " | #{status_parts.join(' | ')}"
|
|
29
|
+
|
|
30
|
+
if branch
|
|
31
|
+
branch_text = " [#{branch}]"
|
|
32
|
+
path_max_length = max_x - 4 - branch_text.length - status_text.length
|
|
33
|
+
path_display = @current_dir.length > path_max_length ? "...#{@current_dir[(-path_max_length + 3)..]}" : @current_dir
|
|
34
|
+
|
|
35
|
+
attron(color_pair(5)) do
|
|
36
|
+
addstr("│ #{path_display}")
|
|
37
|
+
end
|
|
38
|
+
attron(color_pair(6) | Curses::A_BOLD) do
|
|
39
|
+
addstr(branch_text)
|
|
40
|
+
end
|
|
41
|
+
unless status_text.empty?
|
|
42
|
+
attron(color_pair(1) | Curses::A_BOLD) do
|
|
43
|
+
addstr(status_text)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
remaining = max_x - 2 - path_display.length - branch_text.length - status_text.length
|
|
47
|
+
else
|
|
48
|
+
path_max_length = max_x - 4 - status_text.length
|
|
49
|
+
path_display = @current_dir.length > path_max_length ? "...#{@current_dir[(-path_max_length + 3)..]}" : @current_dir
|
|
50
|
+
|
|
51
|
+
attron(color_pair(5)) do
|
|
52
|
+
addstr("│ #{path_display}")
|
|
53
|
+
end
|
|
54
|
+
unless status_text.empty?
|
|
55
|
+
attron(color_pair(1) | Curses::A_BOLD) do
|
|
56
|
+
addstr(status_text)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
remaining = max_x - 2 - path_display.length - status_text.length
|
|
60
|
+
end
|
|
61
|
+
addstr(''.ljust(remaining)) if remaining.positive?
|
|
62
|
+
|
|
63
|
+
setpos(2, 0)
|
|
64
|
+
attron(color_pair(4)) do
|
|
65
|
+
addstr('├'.ljust(max_x, '─'))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
setpos(max_y, 0)
|
|
69
|
+
attron(color_pair(4)) do
|
|
70
|
+
help = '↑↓/jk:Move Enter:Open ←h:Back Space:Mark c:Copy x:Cut p:Paste d:Del e:Edit m:Help q:Quit'
|
|
71
|
+
addstr("└─ #{help}".ljust(max_x, ' '))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
visible_lines = max_y - 4
|
|
75
|
+
|
|
76
|
+
if @selected_index < @scroll_offset
|
|
77
|
+
@scroll_offset = @selected_index
|
|
78
|
+
elsif @selected_index >= @scroll_offset + visible_lines
|
|
79
|
+
@scroll_offset = @selected_index - visible_lines + 1
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
visible_items = @items[@scroll_offset, visible_lines] || []
|
|
83
|
+
visible_items.each_with_index do |item, idx|
|
|
84
|
+
line_num = idx + 3
|
|
85
|
+
actual_index = @scroll_offset + idx
|
|
86
|
+
|
|
87
|
+
setpos(line_num, 0)
|
|
88
|
+
|
|
89
|
+
is_selected = actual_index == @selected_index
|
|
90
|
+
|
|
91
|
+
if is_selected
|
|
92
|
+
attron(color_pair(3) | Curses::A_BOLD) do
|
|
93
|
+
draw_item(item, max_x, true)
|
|
94
|
+
end
|
|
95
|
+
elsif item[:type] == :directory
|
|
96
|
+
attron(color_pair(1)) do
|
|
97
|
+
draw_item(item, max_x, false)
|
|
98
|
+
end
|
|
99
|
+
else
|
|
100
|
+
attron(color_pair(2) | Curses::A_DIM) do
|
|
101
|
+
draw_item(item, max_x, false)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
if @items.length > visible_lines
|
|
107
|
+
total = @items.length
|
|
108
|
+
visible = visible_lines
|
|
109
|
+
scroll_pos = (@scroll_offset.to_f / (total - visible)) * (visible - 1)
|
|
110
|
+
scroll_pos = scroll_pos.round.clamp(0, visible - 1)
|
|
111
|
+
|
|
112
|
+
setpos(3 + scroll_pos, max_x - 1)
|
|
113
|
+
attron(color_pair(4) | Curses::A_BOLD) do
|
|
114
|
+
addstr('█')
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
refresh
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def draw_item(item, max_x, is_selected)
|
|
122
|
+
icon = item[:type] == :directory ? '📁 ' : '📄 '
|
|
123
|
+
|
|
124
|
+
# Check if item is marked
|
|
125
|
+
is_marked = @marked_items.include?(item[:path])
|
|
126
|
+
mark_indicator = is_marked ? '✓ ' : ' '
|
|
127
|
+
|
|
128
|
+
prefix = is_selected ? '▶ ' : ' '
|
|
129
|
+
|
|
130
|
+
size_str = format_size(item[:size])
|
|
131
|
+
date_str = format_date(item[:mtime])
|
|
132
|
+
|
|
133
|
+
if @show_ownership && item[:owner] && item[:perms]
|
|
134
|
+
perms_str = item[:perms]
|
|
135
|
+
owner_str = item[:owner].ljust(16)
|
|
136
|
+
metadata_space = perms_str.length + owner_str.length + size_str.length + date_str.length + 8
|
|
137
|
+
else
|
|
138
|
+
perms_str = ''
|
|
139
|
+
owner_str = ''
|
|
140
|
+
metadata_space = size_str.length + date_str.length + 4
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
available = max_x - prefix.length - mark_indicator.length - icon.length - metadata_space - 1
|
|
144
|
+
|
|
145
|
+
name = if item[:name].length > available
|
|
146
|
+
"#{item[:name][0...(available - 3)]}..."
|
|
147
|
+
else
|
|
148
|
+
item[:name].ljust(available)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
display = if @show_ownership && item[:owner] && item[:perms]
|
|
152
|
+
"#{prefix}#{mark_indicator}#{icon}#{name} #{perms_str} #{owner_str} #{size_str} #{date_str}".ljust(max_x)
|
|
153
|
+
else
|
|
154
|
+
"#{prefix}#{mark_indicator}#{icon}#{name} #{size_str} #{date_str}".ljust(max_x)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
addstr(display)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Utility functions for formatting and file operations
|
|
4
|
+
|
|
5
|
+
module Sergeant
|
|
6
|
+
module Utils
|
|
7
|
+
def get_git_branch
|
|
8
|
+
return nil unless Dir.exist?(File.join(@current_dir, '.git'))
|
|
9
|
+
|
|
10
|
+
head_file = File.join(@current_dir, '.git', 'HEAD')
|
|
11
|
+
return nil unless File.exist?(head_file)
|
|
12
|
+
|
|
13
|
+
head_content = File.read(head_file).strip
|
|
14
|
+
if head_content.start_with?('ref: refs/heads/')
|
|
15
|
+
head_content.sub('ref: refs/heads/', '')
|
|
16
|
+
else
|
|
17
|
+
head_content[0..7]
|
|
18
|
+
end
|
|
19
|
+
rescue StandardError
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def get_owner_info(stat)
|
|
24
|
+
begin
|
|
25
|
+
user = Etc.getpwuid(stat.uid).name
|
|
26
|
+
rescue StandardError
|
|
27
|
+
user = stat.uid.to_s
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
begin
|
|
31
|
+
group = Etc.getgrgid(stat.gid).name
|
|
32
|
+
rescue StandardError
|
|
33
|
+
group = stat.gid.to_s
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
"#{user}:#{group}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def format_permissions(mode, is_directory)
|
|
40
|
+
type = is_directory ? 'd' : '-'
|
|
41
|
+
|
|
42
|
+
owner = ''
|
|
43
|
+
owner += mode.anybits?(0o400) ? 'r' : '-'
|
|
44
|
+
owner += mode.anybits?(0o200) ? 'w' : '-'
|
|
45
|
+
owner += mode.anybits?(0o100) ? 'x' : '-'
|
|
46
|
+
|
|
47
|
+
group = ''
|
|
48
|
+
group += mode.anybits?(0o040) ? 'r' : '-'
|
|
49
|
+
group += mode.anybits?(0o020) ? 'w' : '-'
|
|
50
|
+
group += mode.anybits?(0o010) ? 'x' : '-'
|
|
51
|
+
|
|
52
|
+
other = ''
|
|
53
|
+
other += mode.anybits?(0o004) ? 'r' : '-'
|
|
54
|
+
other += mode.anybits?(0o002) ? 'w' : '-'
|
|
55
|
+
other += mode.anybits?(0o001) ? 'x' : '-'
|
|
56
|
+
|
|
57
|
+
"#{type}#{owner}#{group}#{other}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def format_size(bytes)
|
|
61
|
+
return ' ' if bytes.nil?
|
|
62
|
+
|
|
63
|
+
if bytes < 1024
|
|
64
|
+
"#{bytes}B".rjust(7)
|
|
65
|
+
elsif bytes < 1024 * 1024
|
|
66
|
+
"#{(bytes / 1024.0).round(1)}K".rjust(7)
|
|
67
|
+
elsif bytes < 1024 * 1024 * 1024
|
|
68
|
+
"#{(bytes / (1024.0 * 1024)).round(1)}M".rjust(7)
|
|
69
|
+
else
|
|
70
|
+
"#{(bytes / (1024.0 * 1024 * 1024)).round(1)}G".rjust(7)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def format_date(time)
|
|
75
|
+
return '' if time.nil?
|
|
76
|
+
|
|
77
|
+
time.strftime('%b %d %H:%M')
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def fzf_available?
|
|
81
|
+
system('command -v fzf > /dev/null 2>&1')
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def glow_available?
|
|
85
|
+
system('command -v glow > /dev/null 2>&1')
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def nvim_available?
|
|
89
|
+
system('command -v nvim > /dev/null 2>&1')
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def vim_available?
|
|
93
|
+
system('command -v vim > /dev/null 2>&1')
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def vi_available?
|
|
97
|
+
system('command -v vi > /dev/null 2>&1')
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def nano_available?
|
|
101
|
+
system('command -v nano > /dev/null 2>&1')
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def text_file?(file_path)
|
|
105
|
+
return false unless File.file?(file_path)
|
|
106
|
+
return false unless File.readable?(file_path)
|
|
107
|
+
|
|
108
|
+
# Check file size - limit to 50MB for safety
|
|
109
|
+
return false if File.size(file_path) > 50 * 1024 * 1024
|
|
110
|
+
|
|
111
|
+
# Check if it's a binary file by reading first 8KB
|
|
112
|
+
File.open(file_path, 'rb') do |f|
|
|
113
|
+
sample = f.read(8192) || ''
|
|
114
|
+
# Check for null bytes (common in binary files)
|
|
115
|
+
!sample.bytes.include?(0)
|
|
116
|
+
end
|
|
117
|
+
rescue StandardError
|
|
118
|
+
false
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|