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,635 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# File operation modals (preview, edit, paste, delete, rename)
|
|
4
|
+
|
|
5
|
+
module Sergeant
|
|
6
|
+
module Modals
|
|
7
|
+
module FileOperations
|
|
8
|
+
def edit_file
|
|
9
|
+
item = @items[@selected_index]
|
|
10
|
+
|
|
11
|
+
# Only edit files, not directories
|
|
12
|
+
return unless item && item[:type] == :file
|
|
13
|
+
|
|
14
|
+
file_path = item[:path]
|
|
15
|
+
|
|
16
|
+
# Close curses screen temporarily
|
|
17
|
+
close_screen
|
|
18
|
+
|
|
19
|
+
begin
|
|
20
|
+
# Respect user's preferred editor
|
|
21
|
+
editor = ENV['EDITOR'] || ENV['VISUAL']
|
|
22
|
+
|
|
23
|
+
if editor
|
|
24
|
+
# Use user's preferred editor
|
|
25
|
+
system("#{editor} \"#{file_path}\"")
|
|
26
|
+
elsif nvim_available?
|
|
27
|
+
# Second fallback: nvim (modern vim)
|
|
28
|
+
system("nvim \"#{file_path}\"")
|
|
29
|
+
elsif nano_available?
|
|
30
|
+
# First fallback: nano (user-friendly)
|
|
31
|
+
system("nano \"#{file_path}\"")
|
|
32
|
+
elsif vim_available?
|
|
33
|
+
# Third fallback: vim
|
|
34
|
+
system("vim \"#{file_path}\"")
|
|
35
|
+
elsif vi_available?
|
|
36
|
+
# Fourth fallback: vi (always available on POSIX)
|
|
37
|
+
system("vi \"#{file_path}\"")
|
|
38
|
+
else
|
|
39
|
+
# This should never happen on POSIX systems
|
|
40
|
+
puts 'No editor found. Please set $EDITOR environment variable.'
|
|
41
|
+
puts 'Press Enter to continue...'
|
|
42
|
+
gets
|
|
43
|
+
end
|
|
44
|
+
rescue StandardError => e
|
|
45
|
+
puts "Error opening editor: #{e.message}"
|
|
46
|
+
puts 'Press Enter to continue...'
|
|
47
|
+
gets
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Restore curses screen
|
|
51
|
+
init_screen
|
|
52
|
+
start_color
|
|
53
|
+
curs_set(0)
|
|
54
|
+
noecho
|
|
55
|
+
stdscr.keypad(true)
|
|
56
|
+
apply_color_theme
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def preview_file
|
|
60
|
+
item = @items[@selected_index]
|
|
61
|
+
|
|
62
|
+
# Only preview files, not directories
|
|
63
|
+
return unless item && item[:type] == :file
|
|
64
|
+
|
|
65
|
+
file_path = item[:path]
|
|
66
|
+
|
|
67
|
+
# Check if it's a text file
|
|
68
|
+
unless text_file?(file_path)
|
|
69
|
+
show_error_modal('Cannot preview: Not a text file or too large (>50MB)')
|
|
70
|
+
return
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Close curses screen temporarily
|
|
74
|
+
close_screen
|
|
75
|
+
|
|
76
|
+
begin
|
|
77
|
+
file_ext = File.extname(file_path).downcase
|
|
78
|
+
|
|
79
|
+
# Use glow for markdown files if available, otherwise fall back to less
|
|
80
|
+
if file_ext == '.md' && glow_available?
|
|
81
|
+
system("glow -p \"#{file_path}\"")
|
|
82
|
+
elsif file_ext == '.md'
|
|
83
|
+
system("less -R -F -X \"#{file_path}\"")
|
|
84
|
+
elsif nvim_available?
|
|
85
|
+
# For all other text files, prefer nvim for read-only
|
|
86
|
+
system("nvim -R \"#{file_path}\"")
|
|
87
|
+
elsif vim_available?
|
|
88
|
+
system("vim -R \"#{file_path}\"")
|
|
89
|
+
elsif vi_available?
|
|
90
|
+
system("vi -R \"#{file_path}\"")
|
|
91
|
+
elsif nano_available?
|
|
92
|
+
system("nano -v \"#{file_path}\"")
|
|
93
|
+
else
|
|
94
|
+
# Ultimate fallback to less
|
|
95
|
+
system("less -R -F -X \"#{file_path}\"")
|
|
96
|
+
end
|
|
97
|
+
rescue StandardError => e
|
|
98
|
+
puts "Error previewing file: #{e.message}"
|
|
99
|
+
puts 'Press Enter to continue...'
|
|
100
|
+
gets
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Restore curses screen
|
|
104
|
+
init_screen
|
|
105
|
+
start_color
|
|
106
|
+
curs_set(0)
|
|
107
|
+
noecho
|
|
108
|
+
stdscr.keypad(true)
|
|
109
|
+
apply_color_theme
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def paste_with_modal
|
|
113
|
+
require 'fileutils'
|
|
114
|
+
|
|
115
|
+
success_count = 0
|
|
116
|
+
error_count = 0
|
|
117
|
+
errors = []
|
|
118
|
+
|
|
119
|
+
@copied_items.each do |source_path|
|
|
120
|
+
next unless File.exist?(source_path)
|
|
121
|
+
|
|
122
|
+
filename = File.basename(source_path)
|
|
123
|
+
dest_path = File.join(@current_dir, filename)
|
|
124
|
+
|
|
125
|
+
begin
|
|
126
|
+
if File.exist?(dest_path)
|
|
127
|
+
# Handle conflict
|
|
128
|
+
action = ask_conflict_resolution(filename)
|
|
129
|
+
case action
|
|
130
|
+
when :skip
|
|
131
|
+
next
|
|
132
|
+
when :overwrite
|
|
133
|
+
FileUtils.rm_rf(dest_path)
|
|
134
|
+
when :rename
|
|
135
|
+
dest_path = get_unique_filename(dest_path)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Perform copy or move
|
|
140
|
+
if @cut_mode
|
|
141
|
+
FileUtils.mv(source_path, dest_path)
|
|
142
|
+
elsif File.directory?(source_path)
|
|
143
|
+
FileUtils.cp_r(source_path, dest_path)
|
|
144
|
+
else
|
|
145
|
+
FileUtils.cp(source_path, dest_path)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
success_count += 1
|
|
149
|
+
rescue StandardError => e
|
|
150
|
+
error_count += 1
|
|
151
|
+
errors << "#{filename}: #{e.message}"
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Clean up after operation
|
|
156
|
+
@marked_items.clear
|
|
157
|
+
@copied_items.clear
|
|
158
|
+
@cut_mode = false if @cut_mode
|
|
159
|
+
|
|
160
|
+
# Show result
|
|
161
|
+
if error_count.positive?
|
|
162
|
+
show_error_modal("Pasted #{success_count}, #{error_count} error(s)")
|
|
163
|
+
else
|
|
164
|
+
show_info_modal("Successfully pasted #{success_count} item(s)")
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def get_unique_filename(path)
|
|
169
|
+
dir = File.dirname(path)
|
|
170
|
+
basename = File.basename(path, '.*')
|
|
171
|
+
ext = File.extname(path)
|
|
172
|
+
counter = 1
|
|
173
|
+
|
|
174
|
+
loop do
|
|
175
|
+
new_path = File.join(dir, "#{basename}_#{counter}#{ext}")
|
|
176
|
+
return new_path unless File.exist?(new_path)
|
|
177
|
+
|
|
178
|
+
counter += 1
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def delete_with_modal
|
|
183
|
+
require 'fileutils'
|
|
184
|
+
|
|
185
|
+
success_count = 0
|
|
186
|
+
error_count = 0
|
|
187
|
+
errors = []
|
|
188
|
+
|
|
189
|
+
@marked_items.each do |item_path|
|
|
190
|
+
next unless File.exist?(item_path)
|
|
191
|
+
|
|
192
|
+
begin
|
|
193
|
+
FileUtils.rm_rf(item_path)
|
|
194
|
+
success_count += 1
|
|
195
|
+
rescue StandardError => e
|
|
196
|
+
error_count += 1
|
|
197
|
+
filename = File.basename(item_path)
|
|
198
|
+
errors << "#{filename}: #{e.message}"
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Clear marked items after deletion
|
|
203
|
+
@marked_items.clear
|
|
204
|
+
|
|
205
|
+
# Show result
|
|
206
|
+
if error_count.positive?
|
|
207
|
+
show_error_modal("Deleted #{success_count}, #{error_count} error(s)")
|
|
208
|
+
else
|
|
209
|
+
show_info_modal("Successfully deleted #{success_count} item(s)")
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def rename_with_modal(item)
|
|
214
|
+
require 'fileutils'
|
|
215
|
+
|
|
216
|
+
max_y = lines
|
|
217
|
+
max_x = cols
|
|
218
|
+
|
|
219
|
+
modal_height = 8
|
|
220
|
+
modal_width = 70
|
|
221
|
+
modal_y = (max_y - modal_height) / 2
|
|
222
|
+
modal_x = (max_x - modal_width) / 2
|
|
223
|
+
|
|
224
|
+
(modal_y..(modal_y + modal_height)).each do |y|
|
|
225
|
+
setpos(y, modal_x)
|
|
226
|
+
attron(color_pair(3)) do
|
|
227
|
+
addstr(' ' * modal_width)
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
setpos(modal_y, modal_x)
|
|
232
|
+
attron(color_pair(4) | Curses::A_BOLD) do
|
|
233
|
+
addstr("\u250C#{'─' * (modal_width - 2)}\u2510")
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
setpos(modal_y + 1, modal_x)
|
|
237
|
+
attron(color_pair(4) | Curses::A_BOLD) do
|
|
238
|
+
addstr('│')
|
|
239
|
+
end
|
|
240
|
+
attron(color_pair(5) | Curses::A_BOLD) do
|
|
241
|
+
addstr(' Rename '.center(modal_width - 2))
|
|
242
|
+
end
|
|
243
|
+
attron(color_pair(4) | Curses::A_BOLD) do
|
|
244
|
+
addstr('│')
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
setpos(modal_y + 2, modal_x)
|
|
248
|
+
attron(color_pair(4)) do
|
|
249
|
+
addstr("\u251C#{'─' * (modal_width - 2)}\u2524")
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
msg = "Current: #{item[:name]}"
|
|
253
|
+
setpos(modal_y + 3, modal_x)
|
|
254
|
+
attron(color_pair(4)) do
|
|
255
|
+
addstr('│ ')
|
|
256
|
+
end
|
|
257
|
+
display_msg = msg.length > modal_width - 4 ? "#{msg[0..(modal_width - 8)]}..." : msg
|
|
258
|
+
addstr(display_msg.ljust(modal_width - 4))
|
|
259
|
+
attron(color_pair(4)) do
|
|
260
|
+
addstr(' │')
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
setpos(modal_y + 4, modal_x)
|
|
264
|
+
attron(color_pair(4)) do
|
|
265
|
+
addstr("\u2502#{' ' * (modal_width - 2)}\u2502")
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
setpos(modal_y + 5, modal_x)
|
|
269
|
+
attron(color_pair(4)) do
|
|
270
|
+
addstr('│ ')
|
|
271
|
+
end
|
|
272
|
+
prompt = 'New name: '
|
|
273
|
+
attron(color_pair(5)) do
|
|
274
|
+
addstr(prompt)
|
|
275
|
+
end
|
|
276
|
+
addstr(' ' * (modal_width - 4 - prompt.length))
|
|
277
|
+
attron(color_pair(4)) do
|
|
278
|
+
addstr(' │')
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
setpos(modal_y + 6, modal_x)
|
|
282
|
+
attron(color_pair(4)) do
|
|
283
|
+
addstr('│ ')
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
curs_set(1)
|
|
287
|
+
echo
|
|
288
|
+
setpos(modal_y + 6, modal_x + 2)
|
|
289
|
+
|
|
290
|
+
input_width = modal_width - 5
|
|
291
|
+
new_name = item[:name].dup
|
|
292
|
+
|
|
293
|
+
# Position cursor at end of name
|
|
294
|
+
setpos(modal_y + 6, modal_x + 2)
|
|
295
|
+
addstr(new_name.ljust(input_width))
|
|
296
|
+
setpos(modal_y + 6, modal_x + 2 + new_name.length)
|
|
297
|
+
|
|
298
|
+
loop do
|
|
299
|
+
ch = getch
|
|
300
|
+
|
|
301
|
+
case ch
|
|
302
|
+
when 10, 13
|
|
303
|
+
break
|
|
304
|
+
when 27
|
|
305
|
+
new_name = ''
|
|
306
|
+
break
|
|
307
|
+
when 127, Curses::Key::BACKSPACE
|
|
308
|
+
if new_name.length.positive?
|
|
309
|
+
new_name = new_name[0...-1]
|
|
310
|
+
setpos(modal_y + 6, modal_x + 2)
|
|
311
|
+
addstr(new_name.ljust(input_width))
|
|
312
|
+
setpos(modal_y + 6, modal_x + 2 + new_name.length)
|
|
313
|
+
end
|
|
314
|
+
else
|
|
315
|
+
if ch.is_a?(String) && new_name.length < input_width && ch != '/'
|
|
316
|
+
new_name += ch
|
|
317
|
+
setpos(modal_y + 6, modal_x + 2)
|
|
318
|
+
addstr(new_name.ljust(input_width))
|
|
319
|
+
setpos(modal_y + 6, modal_x + 2 + new_name.length)
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
refresh
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
noecho
|
|
327
|
+
curs_set(0)
|
|
328
|
+
|
|
329
|
+
setpos(modal_y + modal_height - 1, modal_x)
|
|
330
|
+
attron(color_pair(4) | Curses::A_BOLD) do
|
|
331
|
+
addstr("\u2514#{'─' * (modal_width - 2)}\u2518")
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
refresh
|
|
335
|
+
|
|
336
|
+
new_name = new_name.strip
|
|
337
|
+
|
|
338
|
+
return if new_name.empty? || new_name == item[:name]
|
|
339
|
+
|
|
340
|
+
old_path = item[:path]
|
|
341
|
+
new_path = File.join(File.dirname(old_path), new_name)
|
|
342
|
+
|
|
343
|
+
if File.exist?(new_path)
|
|
344
|
+
show_error_modal('File or directory already exists!')
|
|
345
|
+
else
|
|
346
|
+
begin
|
|
347
|
+
FileUtils.mv(old_path, new_path)
|
|
348
|
+
show_info_modal('Renamed successfully!')
|
|
349
|
+
|
|
350
|
+
# Update marked items if this item was marked
|
|
351
|
+
if @marked_items.include?(old_path)
|
|
352
|
+
@marked_items.delete(old_path)
|
|
353
|
+
@marked_items << new_path
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Update copied items if this item was copied
|
|
357
|
+
if @copied_items.include?(old_path)
|
|
358
|
+
@copied_items.delete(old_path)
|
|
359
|
+
@copied_items << new_path
|
|
360
|
+
end
|
|
361
|
+
rescue StandardError => e
|
|
362
|
+
show_error_modal("Error: #{e.message}")
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def create_new_with_modal
|
|
368
|
+
max_y = lines
|
|
369
|
+
max_x = cols
|
|
370
|
+
|
|
371
|
+
modal_width = [60, max_x - 4].min
|
|
372
|
+
modal_height = [10, max_y - 8].min # Adaptive height (more conservative margin)
|
|
373
|
+
modal_x = (max_x - modal_width) / 2
|
|
374
|
+
modal_y = (max_y - modal_height) / 2
|
|
375
|
+
|
|
376
|
+
# Draw modal box
|
|
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
|
+
(1...modal_height - 1).each do |i|
|
|
383
|
+
setpos(modal_y + i, modal_x)
|
|
384
|
+
attron(color_pair(4) | Curses::A_BOLD) do
|
|
385
|
+
addstr("\u2502#{' ' * (modal_width - 2)}\u2502")
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# Bottom border
|
|
390
|
+
setpos(modal_y + modal_height - 1, modal_x)
|
|
391
|
+
attron(color_pair(4) | Curses::A_BOLD) do
|
|
392
|
+
addstr("\u2514#{'─' * (modal_width - 2)}\u2518")
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
# Title
|
|
396
|
+
setpos(modal_y + 1, modal_x + 2)
|
|
397
|
+
attron(color_pair(4) | Curses::A_BOLD) do
|
|
398
|
+
addstr('Create New')
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
# Prompt
|
|
402
|
+
setpos(modal_y + 3, modal_x + 2)
|
|
403
|
+
addstr('What do you want to create?')
|
|
404
|
+
|
|
405
|
+
setpos(modal_y + 5, modal_x + 2)
|
|
406
|
+
attron(color_pair(1) | Curses::A_BOLD) do
|
|
407
|
+
addstr('[f] File [d] Directory [ESC] Cancel')
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
refresh
|
|
411
|
+
|
|
412
|
+
# Get choice
|
|
413
|
+
choice = getch
|
|
414
|
+
return if choice == 27 # ESC
|
|
415
|
+
|
|
416
|
+
create_type = case choice
|
|
417
|
+
when 'f', 'F'
|
|
418
|
+
:file
|
|
419
|
+
when 'd', 'D'
|
|
420
|
+
:directory
|
|
421
|
+
else
|
|
422
|
+
return
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
# Clear and ask for name
|
|
426
|
+
setpos(modal_y + 3, modal_x + 2)
|
|
427
|
+
addstr(' ' * (modal_width - 4))
|
|
428
|
+
setpos(modal_y + 5, modal_x + 2)
|
|
429
|
+
addstr(' ' * (modal_width - 4))
|
|
430
|
+
|
|
431
|
+
setpos(modal_y + 3, modal_x + 2)
|
|
432
|
+
type_text = create_type == :file ? 'file' : 'directory'
|
|
433
|
+
addstr("Enter #{type_text} name:")
|
|
434
|
+
|
|
435
|
+
setpos(modal_y + 5, modal_x + 2)
|
|
436
|
+
addstr('(ESC to cancel)')
|
|
437
|
+
|
|
438
|
+
# Input field
|
|
439
|
+
input_width = modal_width - 6
|
|
440
|
+
setpos(modal_y + 6, modal_x + 2)
|
|
441
|
+
attron(color_pair(3)) do
|
|
442
|
+
addstr(' ' * input_width)
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
# Get input
|
|
446
|
+
echo
|
|
447
|
+
curs_set(1)
|
|
448
|
+
new_name = ''
|
|
449
|
+
|
|
450
|
+
loop do
|
|
451
|
+
setpos(modal_y + 6, modal_x + 2 + new_name.length)
|
|
452
|
+
refresh
|
|
453
|
+
|
|
454
|
+
ch = getch
|
|
455
|
+
|
|
456
|
+
case ch
|
|
457
|
+
when 10, 13
|
|
458
|
+
break
|
|
459
|
+
when 27
|
|
460
|
+
new_name = ''
|
|
461
|
+
break
|
|
462
|
+
when 127, Curses::Key::BACKSPACE
|
|
463
|
+
if new_name.length.positive?
|
|
464
|
+
new_name = new_name[0...-1]
|
|
465
|
+
setpos(modal_y + 6, modal_x + 2)
|
|
466
|
+
addstr(new_name.ljust(input_width))
|
|
467
|
+
setpos(modal_y + 6, modal_x + 2 + new_name.length)
|
|
468
|
+
end
|
|
469
|
+
else
|
|
470
|
+
if ch.is_a?(String) && new_name.length < input_width && ch != '/'
|
|
471
|
+
new_name += ch
|
|
472
|
+
setpos(modal_y + 6, modal_x + 2)
|
|
473
|
+
addstr(new_name.ljust(input_width))
|
|
474
|
+
setpos(modal_y + 6, modal_x + 2 + new_name.length)
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
noecho
|
|
480
|
+
curs_set(0)
|
|
481
|
+
|
|
482
|
+
new_name = new_name.strip
|
|
483
|
+
|
|
484
|
+
return if new_name.empty?
|
|
485
|
+
|
|
486
|
+
new_path = File.join(@current_dir, new_name)
|
|
487
|
+
|
|
488
|
+
if File.exist?(new_path)
|
|
489
|
+
show_error_modal('File or directory already exists!')
|
|
490
|
+
else
|
|
491
|
+
begin
|
|
492
|
+
if create_type == :file
|
|
493
|
+
FileUtils.touch(new_path)
|
|
494
|
+
show_info_modal('File created successfully!')
|
|
495
|
+
else
|
|
496
|
+
FileUtils.mkdir_p(new_path)
|
|
497
|
+
show_info_modal('Directory created successfully!')
|
|
498
|
+
end
|
|
499
|
+
rescue StandardError => e
|
|
500
|
+
show_error_modal("Error: #{e.message}")
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
def execute_terminal_command
|
|
506
|
+
max_y = lines
|
|
507
|
+
max_x = cols
|
|
508
|
+
|
|
509
|
+
modal_width = [80, max_x - 4].min
|
|
510
|
+
modal_height = [8, max_y - 8].min # Adaptive height (more conservative margin)
|
|
511
|
+
modal_x = (max_x - modal_width) / 2
|
|
512
|
+
modal_y = (max_y - modal_height) / 2
|
|
513
|
+
|
|
514
|
+
# Draw modal box
|
|
515
|
+
setpos(modal_y, modal_x)
|
|
516
|
+
attron(color_pair(4) | Curses::A_BOLD) do
|
|
517
|
+
addstr("\u250C#{'─' * (modal_width - 2)}\u2510")
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
(1...modal_height - 1).each do |i|
|
|
521
|
+
setpos(modal_y + i, modal_x)
|
|
522
|
+
attron(color_pair(4) | Curses::A_BOLD) do
|
|
523
|
+
addstr("\u2502#{' ' * (modal_width - 2)}\u2502")
|
|
524
|
+
end
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
# Bottom border
|
|
528
|
+
setpos(modal_y + modal_height - 1, modal_x)
|
|
529
|
+
attron(color_pair(4) | Curses::A_BOLD) do
|
|
530
|
+
addstr("\u2514#{'─' * (modal_width - 2)}\u2518")
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
# Title
|
|
534
|
+
setpos(modal_y + 1, modal_x + 2)
|
|
535
|
+
attron(color_pair(4) | Curses::A_BOLD) do
|
|
536
|
+
addstr('Execute Terminal Command')
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
# Show current directory
|
|
540
|
+
setpos(modal_y + 2, modal_x + 2)
|
|
541
|
+
attron(color_pair(5)) do
|
|
542
|
+
dir_display = @current_dir
|
|
543
|
+
max_dir_len = modal_width - 8
|
|
544
|
+
dir_display = "...#{@current_dir[(-max_dir_len + 3)..]}" if @current_dir.length > max_dir_len
|
|
545
|
+
addstr("in: #{dir_display}")
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
# Prompt
|
|
549
|
+
setpos(modal_y + 4, modal_x + 2)
|
|
550
|
+
addstr(':')
|
|
551
|
+
|
|
552
|
+
# Input field
|
|
553
|
+
input_width = modal_width - 6
|
|
554
|
+
setpos(modal_y + 4, modal_x + 4)
|
|
555
|
+
attron(color_pair(3)) do
|
|
556
|
+
addstr(' ' * input_width)
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
setpos(modal_y + 6, modal_x + 2)
|
|
560
|
+
addstr('(ESC to cancel)')
|
|
561
|
+
|
|
562
|
+
# Get input
|
|
563
|
+
echo
|
|
564
|
+
curs_set(1)
|
|
565
|
+
command = ''
|
|
566
|
+
|
|
567
|
+
loop do
|
|
568
|
+
setpos(modal_y + 4, modal_x + 4 + command.length)
|
|
569
|
+
refresh
|
|
570
|
+
|
|
571
|
+
ch = getch
|
|
572
|
+
|
|
573
|
+
case ch
|
|
574
|
+
when 10, 13
|
|
575
|
+
break
|
|
576
|
+
when 27
|
|
577
|
+
command = ''
|
|
578
|
+
break
|
|
579
|
+
when 127, Curses::Key::BACKSPACE
|
|
580
|
+
if command.length.positive?
|
|
581
|
+
command = command[0...-1]
|
|
582
|
+
setpos(modal_y + 4, modal_x + 4)
|
|
583
|
+
addstr(command.ljust(input_width))
|
|
584
|
+
setpos(modal_y + 4, modal_x + 4 + command.length)
|
|
585
|
+
end
|
|
586
|
+
else
|
|
587
|
+
if ch.is_a?(String) && command.length < input_width
|
|
588
|
+
command += ch
|
|
589
|
+
setpos(modal_y + 4, modal_x + 4)
|
|
590
|
+
addstr(command.ljust(input_width))
|
|
591
|
+
setpos(modal_y + 4, modal_x + 4 + command.length)
|
|
592
|
+
end
|
|
593
|
+
end
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
noecho
|
|
597
|
+
curs_set(0)
|
|
598
|
+
|
|
599
|
+
command = command.strip
|
|
600
|
+
|
|
601
|
+
return if command.empty?
|
|
602
|
+
|
|
603
|
+
# Close curses and execute command
|
|
604
|
+
close_screen
|
|
605
|
+
|
|
606
|
+
puts "Executing: #{command}"
|
|
607
|
+
puts '─' * 80
|
|
608
|
+
puts
|
|
609
|
+
|
|
610
|
+
begin
|
|
611
|
+
# Change to current directory and execute
|
|
612
|
+
Dir.chdir(@current_dir) do
|
|
613
|
+
system(command)
|
|
614
|
+
end
|
|
615
|
+
rescue StandardError => e
|
|
616
|
+
puts
|
|
617
|
+
puts "Error: #{e.message}"
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
puts
|
|
621
|
+
puts '─' * 80
|
|
622
|
+
puts 'Press Enter to continue...'
|
|
623
|
+
gets
|
|
624
|
+
|
|
625
|
+
# Restore curses
|
|
626
|
+
init_screen
|
|
627
|
+
start_color
|
|
628
|
+
curs_set(0)
|
|
629
|
+
noecho
|
|
630
|
+
stdscr.keypad(true)
|
|
631
|
+
apply_color_theme
|
|
632
|
+
end
|
|
633
|
+
end
|
|
634
|
+
end
|
|
635
|
+
end
|