Module: Sergeant::Modals::FileOperations

Included in:
Sergeant::Modals
Defined in:
lib/sergeant/modals/file_operations.rb

Instance Method Summary collapse

Instance Method Details

#create_new_with_modalObject



409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
# File 'lib/sergeant/modals/file_operations.rb', line 409

def create_new_with_modal
  max_y = lines
  max_x = cols

  modal_width = [60, max_x - 4].min
  modal_height = [10, max_y - 8].min  # Adaptive height (more conservative margin)
  modal_x = (max_x - modal_width) / 2
  modal_y = (max_y - modal_height) / 2

  # Draw modal box
  setpos(modal_y, modal_x)
  attron(color_pair(4) | Curses::A_BOLD) do
    addstr("\u250C#{'' * (modal_width - 2)}\u2510")
  end

  (1...modal_height - 1).each do |i|
    setpos(modal_y + i, modal_x)
    attron(color_pair(4) | Curses::A_BOLD) do
      addstr("\u2502#{' ' * (modal_width - 2)}\u2502")
    end
  end

  # Bottom border
  setpos(modal_y + modal_height - 1, modal_x)
  attron(color_pair(4) | Curses::A_BOLD) do
    addstr("\u2514#{'' * (modal_width - 2)}\u2518")
  end

  # Title
  setpos(modal_y + 1, modal_x + 2)
  attron(color_pair(4) | Curses::A_BOLD) do
    addstr('Create New')
  end

  # Prompt
  setpos(modal_y + 3, modal_x + 2)
  addstr('What do you want to create?')

  setpos(modal_y + 5, modal_x + 2)
  attron(color_pair(1) | Curses::A_BOLD) do
    addstr('[f] File    [d] Directory    [ESC] Cancel')
  end

  refresh

  # Get choice
  choice = getch
  return if choice == 27 # ESC

  create_type = case choice
                when 'f', 'F'
                  :file
                when 'd', 'D'
                  :directory
                else
                  return
                end

  # Clear and ask for name
  setpos(modal_y + 3, modal_x + 2)
  addstr(' ' * (modal_width - 4))
  setpos(modal_y + 5, modal_x + 2)
  addstr(' ' * (modal_width - 4))

  setpos(modal_y + 3, modal_x + 2)
  type_text = create_type == :file ? 'file' : 'directory'
  addstr("Enter #{type_text} name:")

  setpos(modal_y + 5, modal_x + 2)
  addstr('(ESC to cancel)')

  # Input field
  input_width = modal_width - 6
  setpos(modal_y + 6, modal_x + 2)
  attron(color_pair(3)) do
    addstr(' ' * input_width)
  end

  # Get input
  echo
  curs_set(1)
  new_name = ''

  loop do
    setpos(modal_y + 6, modal_x + 2 + new_name.length)
    refresh

    ch = getch

    case ch
    when 10, 13
      break
    when 27
      new_name = ''
      break
    when 127, Curses::Key::BACKSPACE
      if new_name.length.positive?
        new_name = new_name[0...-1]
        setpos(modal_y + 6, modal_x + 2)
        addstr(new_name.ljust(input_width))
        setpos(modal_y + 6, modal_x + 2 + new_name.length)
      end
    else
      if ch.is_a?(String) && new_name.length < input_width && ch != '/'
        new_name += ch
        setpos(modal_y + 6, modal_x + 2)
        addstr(new_name.ljust(input_width))
        setpos(modal_y + 6, modal_x + 2 + new_name.length)
      end
    end
  end

  noecho
  curs_set(0)

  new_name = new_name.strip

  return if new_name.empty?

  new_path = File.join(@current_dir, new_name)

  if File.exist?(new_path)
    show_error_modal('File or directory already exists!')
  else
    begin
      if create_type == :file
        FileUtils.touch(new_path)
        show_info_modal('File created successfully!')
      else
        FileUtils.mkdir_p(new_path)
        show_info_modal('Directory created successfully!')
      end

      # Force refresh to show new item
      force_refresh
    rescue StandardError => e
      show_error_modal("Error: #{e.message}")
    end
  end
end

#delete_with_modalObject



209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/sergeant/modals/file_operations.rb', line 209

def delete_with_modal
  require 'fileutils'

  success_count = 0
  error_count = 0
  errors = []

  total = @marked_items.count { |p| File.exist?(p) }
  draw_progress_modal('Deleting Files', total)
  current = 0

  @marked_items.each do |item_path|
    next unless File.exist?(item_path)

    filename = File.basename(item_path)
    current += 1
    update_progress_modal(current, total, filename)

    begin
      FileUtils.rm_rf(item_path)
      success_count += 1
    rescue StandardError => e
      error_count += 1
      errors << "#{filename}: #{e.message}"
    end
  end

  @progress_modal = nil

  # Clear marked items after deletion
  @marked_items.clear

  # Show result
  if error_count.positive?
    show_error_modal("Deleted #{success_count}, #{error_count} error(s)")
  else
    show_info_modal("Successfully deleted #{success_count} item(s)")
  end

  # Force refresh to show changes
  force_refresh
end

#edit_fileObject



8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/sergeant/modals/file_operations.rb', line 8

def edit_file
  item = @items[@selected_index]

  # Only edit files, not directories
  return unless item && item[:type] == :file

  file_path = item[:path]

  # Close curses screen temporarily
  close_screen

  begin
    # Respect user's preferred editor
    editor = ENV['EDITOR'] || ENV['VISUAL']

    if editor
      # Use user's preferred editor
      system("#{editor} \"#{file_path}\"")
    elsif Gem.win_platform?
      # Windows: use notepad (always available)
      system("notepad \"#{file_path}\"")
    elsif nvim_available?
      # Second fallback: nvim (modern vim)
      system("nvim \"#{file_path}\"")
    elsif nano_available?
      # First fallback: nano (user-friendly)
      system("nano \"#{file_path}\"")
    elsif vim_available?
      # Third fallback: vim
      system("vim \"#{file_path}\"")
    elsif vi_available?
      # Fourth fallback: vi (always available on POSIX)
      system("vi \"#{file_path}\"")
    else
      # This should never happen on POSIX systems
      puts 'No editor found. Please set $EDITOR environment variable.'
      puts 'Press Enter to continue...'
      gets
    end
  rescue StandardError => e
    puts "Error opening editor: #{e.message}"
    puts 'Press Enter to continue...'
    gets
  end

  # Restore curses screen
  init_screen
  if has_colors?
    start_color
    apply_color_theme
  end
  curs_set(0)
  noecho
  stdscr.keypad(true)
end

#execute_terminal_commandObject



550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
# File 'lib/sergeant/modals/file_operations.rb', line 550

def execute_terminal_command
  max_y = lines
  max_x = cols

  modal_width = [80, max_x - 4].min
  modal_height = [8, max_y - 8].min  # Adaptive height (more conservative margin)
  modal_x = (max_x - modal_width) / 2
  modal_y = (max_y - modal_height) / 2

  # Draw modal box
  setpos(modal_y, modal_x)
  attron(color_pair(4) | Curses::A_BOLD) do
    addstr("\u250C#{'' * (modal_width - 2)}\u2510")
  end

  (1...modal_height - 1).each do |i|
    setpos(modal_y + i, modal_x)
    attron(color_pair(4) | Curses::A_BOLD) do
      addstr("\u2502#{' ' * (modal_width - 2)}\u2502")
    end
  end

  # Bottom border
  setpos(modal_y + modal_height - 1, modal_x)
  attron(color_pair(4) | Curses::A_BOLD) do
    addstr("\u2514#{'' * (modal_width - 2)}\u2518")
  end

  # Title
  setpos(modal_y + 1, modal_x + 2)
  attron(color_pair(4) | Curses::A_BOLD) do
    addstr('Execute Terminal Command')
  end

  # Show current directory
  setpos(modal_y + 2, modal_x + 2)
  attron(color_pair(5)) do
    dir_display = @current_dir
    max_dir_len = modal_width - 8
    dir_display = "...#{@current_dir[(-max_dir_len + 3)..]}" if @current_dir.length > max_dir_len
    addstr("in: #{dir_display}")
  end

  # Prompt
  setpos(modal_y + 4, modal_x + 2)
  addstr(':')

  # Input field
  input_width = modal_width - 6
  setpos(modal_y + 4, modal_x + 4)
  attron(color_pair(3)) do
    addstr(' ' * input_width)
  end

  setpos(modal_y + 6, modal_x + 2)
  addstr('(ESC to cancel)')

  # Get input
  echo
  curs_set(1)
  command = ''

  loop do
    setpos(modal_y + 4, modal_x + 4 + command.length)
    refresh

    ch = getch

    case ch
    when 10, 13
      break
    when 27
      command = ''
      break
    when 127, Curses::Key::BACKSPACE
      if command.length.positive?
        command = command[0...-1]
        setpos(modal_y + 4, modal_x + 4)
        addstr(command.ljust(input_width))
        setpos(modal_y + 4, modal_x + 4 + command.length)
      end
    else
      if ch.is_a?(String) && command.length < input_width
        command += ch
        setpos(modal_y + 4, modal_x + 4)
        addstr(command.ljust(input_width))
        setpos(modal_y + 4, modal_x + 4 + command.length)
      end
    end
  end

  noecho
  curs_set(0)

  command = command.strip

  return if command.empty?

  # Close curses and execute command
  close_screen

  puts "Executing: #{command}"
  puts '' * 80
  puts

  begin
    # Change to current directory and execute
    Dir.chdir(@current_dir) do
      system(command)
    end
  rescue StandardError => e
    puts
    puts "Error: #{e.message}"
  end

  puts
  puts '' * 80
  puts 'Press Enter to continue...'
  gets

  # Restore curses
  init_screen
  if has_colors?
    start_color
    apply_color_theme
  end
  curs_set(0)
  noecho
  stdscr.keypad(true)

  # Force refresh to show any changes from the command
  force_refresh
end

#get_unique_filename(path) ⇒ Object



195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/sergeant/modals/file_operations.rb', line 195

def get_unique_filename(path)
  dir = File.dirname(path)
  basename = File.basename(path, '.*')
  ext = File.extname(path)
  counter = 1

  loop do
    new_path = File.join(dir, "#{basename}_#{counter}#{ext}")
    return new_path unless File.exist?(new_path)

    counter += 1
  end
end

#paste_with_modalObject



127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/sergeant/modals/file_operations.rb', line 127

def paste_with_modal
  require 'fileutils'

  success_count = 0
  error_count = 0
  errors = []

  total = @copied_items.count { |p| File.exist?(p) }
  operation = @cut_mode ? 'Moving' : 'Copying'
  draw_progress_modal("#{operation} Files", total)
  current = 0

  @copied_items.each do |source_path|
    next unless File.exist?(source_path)

    filename = File.basename(source_path)
    dest_path = File.join(@current_dir, filename)
    current += 1
    update_progress_modal(current, total, filename)

    begin
      if File.exist?(dest_path)
        # Handle conflict
        action = ask_conflict_resolution(filename)
        case action
        when :skip
          next
        when :overwrite
          FileUtils.rm_rf(dest_path)
        when :rename
          dest_path = get_unique_filename(dest_path)
        end
      end

      # Perform copy or move
      if @cut_mode
        FileUtils.mv(source_path, dest_path)
      elsif File.directory?(source_path)
        FileUtils.cp_r(source_path, dest_path)
      else
        FileUtils.cp(source_path, dest_path)
      end

      success_count += 1
    rescue StandardError => e
      error_count += 1
      errors << "#{filename}: #{e.message}"
    end
  end

  @progress_modal = nil

  # Clean up after operation
  @marked_items.clear
  @copied_items.clear
  @cut_mode = false if @cut_mode

  # Show result
  if error_count.positive?
    show_error_modal("Pasted #{success_count}, #{error_count} error(s)")
  else
    show_info_modal("Successfully pasted #{success_count} item(s)")
  end

  # Force refresh to show new files
  force_refresh
end

#preview_fileObject



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/sergeant/modals/file_operations.rb', line 64

def preview_file
  item = @items[@selected_index]

  # Only preview files, not directories
  return unless item && item[:type] == :file

  file_path = item[:path]
  file_ext = File.extname(file_path).downcase

  # Check if it's an archive file
  if archive_file?(file_ext)
    preview_archive(file_path, file_ext)
    return
  end

  # Check if it's a text file
  unless text_file?(file_path)
    show_error_modal('Cannot preview: Not a text file or too large (>50MB)')
    return
  end

  # Close curses screen temporarily
  close_screen

  begin
    if Gem.win_platform?
      # Windows: use notepad for preview (simpler and always works)
      system("notepad \"#{file_path}\"")
    # Use glow for markdown files if available, otherwise fall back to less
    elsif file_ext == '.md' && glow_available?
      system("glow -p \"#{file_path}\"")
    elsif file_ext == '.md'
      system("less -R -F -X \"#{file_path}\"")
    elsif nvim_available?
      # For all other text files, prefer nvim for read-only
      system("nvim -R \"#{file_path}\"")
    elsif vim_available?
      system("vim -R \"#{file_path}\"")
    elsif vi_available?
      system("vi -R \"#{file_path}\"")
    elsif nano_available?
      system("nano -v \"#{file_path}\"")
    else
      # Ultimate fallback to less
      system("less -R -F -X \"#{file_path}\"")
    end
  rescue StandardError => e
    puts "Error previewing file: #{e.message}"
    puts 'Press Enter to continue...'
    gets
  end

  # Restore curses screen
  init_screen
  if has_colors?
    start_color
    apply_color_theme
  end
  curs_set(0)
  noecho
  stdscr.keypad(true)
end

#rename_with_modal(item) ⇒ Object



252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
# File 'lib/sergeant/modals/file_operations.rb', line 252

def rename_with_modal(item)
  require 'fileutils'

  max_y = lines
  max_x = cols

  modal_height = 8
  modal_width = 70
  modal_y = (max_y - modal_height) / 2
  modal_x = (max_x - modal_width) / 2

  (modal_y..(modal_y + modal_height)).each do |y|
    setpos(y, modal_x)
    attron(color_pair(3)) do
      addstr(' ' * modal_width)
    end
  end

  setpos(modal_y, modal_x)
  attron(color_pair(4) | Curses::A_BOLD) do
    addstr("\u250C#{'' * (modal_width - 2)}\u2510")
  end

  setpos(modal_y + 1, modal_x)
  attron(color_pair(4) | Curses::A_BOLD) do
    addstr('')
  end
  attron(color_pair(5) | Curses::A_BOLD) do
    addstr(' Rename '.center(modal_width - 2))
  end
  attron(color_pair(4) | Curses::A_BOLD) do
    addstr('')
  end

  setpos(modal_y + 2, modal_x)
  attron(color_pair(4)) do
    addstr("\u251C#{'' * (modal_width - 2)}\u2524")
  end

  msg = "Current: #{item[:name]}"
  setpos(modal_y + 3, modal_x)
  attron(color_pair(4)) do
    addstr('')
  end
  display_msg = msg.length > modal_width - 4 ? "#{msg[0..(modal_width - 8)]}..." : msg
  addstr(display_msg.ljust(modal_width - 4))
  attron(color_pair(4)) do
    addstr('')
  end

  setpos(modal_y + 4, modal_x)
  attron(color_pair(4)) do
    addstr("\u2502#{' ' * (modal_width - 2)}\u2502")
  end

  setpos(modal_y + 5, modal_x)
  attron(color_pair(4)) do
    addstr('')
  end
  prompt = 'New name: '
  attron(color_pair(5)) do
    addstr(prompt)
  end
  addstr(' ' * (modal_width - 4 - prompt.length))
  attron(color_pair(4)) do
    addstr('')
  end

  setpos(modal_y + 6, modal_x)
  attron(color_pair(4)) do
    addstr('')
  end

  curs_set(1)
  echo
  setpos(modal_y + 6, modal_x + 2)

  input_width = modal_width - 5
  new_name = item[:name].dup

  # Position cursor at end of name
  setpos(modal_y + 6, modal_x + 2)
  addstr(new_name.ljust(input_width))
  setpos(modal_y + 6, modal_x + 2 + new_name.length)

  loop do
    ch = getch

    case ch
    when 10, 13
      break
    when 27
      new_name = ''
      break
    when 127, Curses::Key::BACKSPACE
      if new_name.length.positive?
        new_name = new_name[0...-1]
        setpos(modal_y + 6, modal_x + 2)
        addstr(new_name.ljust(input_width))
        setpos(modal_y + 6, modal_x + 2 + new_name.length)
      end
    else
      if ch.is_a?(String) && new_name.length < input_width && ch != '/'
        new_name += ch
        setpos(modal_y + 6, modal_x + 2)
        addstr(new_name.ljust(input_width))
        setpos(modal_y + 6, modal_x + 2 + new_name.length)
      end
    end

    refresh
  end

  noecho
  curs_set(0)

  setpos(modal_y + modal_height - 1, modal_x)
  attron(color_pair(4) | Curses::A_BOLD) do
    addstr("\u2514#{'' * (modal_width - 2)}\u2518")
  end

  refresh

  new_name = new_name.strip

  return if new_name.empty? || new_name == item[:name]

  old_path = item[:path]
  new_path = File.join(File.dirname(old_path), new_name)

  if File.exist?(new_path)
    show_error_modal('File or directory already exists!')
  else
    begin
      FileUtils.mv(old_path, new_path)
      show_info_modal('Renamed successfully!')

      # Update marked items if this item was marked
      if @marked_items.include?(old_path)
        @marked_items.delete(old_path)
        @marked_items << new_path
      end

      # Update copied items if this item was copied
      if @copied_items.include?(old_path)
        @copied_items.delete(old_path)
        @copied_items << new_path
      end

      # Force refresh to show renamed item
      force_refresh
    rescue StandardError => e
      show_error_modal("Error: #{e.message}")
    end
  end
end