rufio 1.1.0 → 1.4.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.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +50 -0
  3. data/README.md +26 -297
  4. data/README_ja.md +24 -261
  5. data/Rakefile +14 -1
  6. data/bin/rufio +31 -2
  7. data/docs/fast_zip_design_ja.md +167 -0
  8. data/docs/guide.md +410 -0
  9. data/docs/guide_ja.md +410 -0
  10. data/info/help.md +54 -17
  11. data/info/keybindings.md +50 -22
  12. data/info/welcome.md +14 -5
  13. data/lib/rufio/application.rb +12 -1
  14. data/lib/rufio/archive_controller.rb +211 -0
  15. data/lib/rufio/background_command_executor.rb +9 -0
  16. data/lib/rufio/background_status.rb +49 -0
  17. data/lib/rufio/bookmark_controller.rb +38 -121
  18. data/lib/rufio/bookmark_manager.rb +77 -331
  19. data/lib/rufio/command_completion.rb +28 -2
  20. data/lib/rufio/command_history.rb +5 -2
  21. data/lib/rufio/command_mode.rb +60 -134
  22. data/lib/rufio/command_mode_ui.rb +4 -42
  23. data/lib/rufio/config.rb +88 -136
  24. data/lib/rufio/config_loader.rb +4 -11
  25. data/lib/rufio/dialog_renderer.rb +90 -152
  26. data/lib/rufio/dialog_support.rb +69 -0
  27. data/lib/rufio/document_text_extractor.rb +167 -0
  28. data/lib/rufio/file_operation_controller.rb +49 -222
  29. data/lib/rufio/file_operations.rb +28 -6
  30. data/lib/rufio/file_preview.rb +46 -2
  31. data/lib/rufio/global_search.rb +125 -13
  32. data/lib/rufio/help_menu.rb +19 -7
  33. data/lib/rufio/interpreter_resolver.rb +6 -11
  34. data/lib/rufio/job_manager.rb +2 -5
  35. data/lib/rufio/key_input.rb +77 -0
  36. data/lib/rufio/keybind_handler.rb +83 -116
  37. data/lib/rufio/local_script_scanner.rb +6 -18
  38. data/lib/rufio/native/aarch64-linux-gnu/librufio_zig.so +0 -0
  39. data/lib/rufio/native/aarch64-linux-musl/librufio_zig.so +0 -0
  40. data/lib/rufio/native/aarch64-macos/librufio_zig.dylib +0 -0
  41. data/lib/rufio/native/aarch64-windows/rufio_zig.dll +0 -0
  42. data/lib/rufio/native/x86_64-linux-gnu/librufio_zig.so +0 -0
  43. data/lib/rufio/native/x86_64-linux-musl/librufio_zig.so +0 -0
  44. data/lib/rufio/native/x86_64-macos/librufio_zig.dylib +0 -0
  45. data/lib/rufio/native/x86_64-windows/rufio_zig.dll +0 -0
  46. data/lib/rufio/native_content_search.rb +153 -0
  47. data/lib/rufio/native_scanner.rb +13 -68
  48. data/lib/rufio/native_scanner_zig.rb +106 -56
  49. data/lib/rufio/native_zip.rb +121 -0
  50. data/lib/rufio/navigation_controller.rb +20 -26
  51. data/lib/rufio/path_jump.rb +55 -0
  52. data/lib/rufio/screen.rb +2 -12
  53. data/lib/rufio/script_executor.rb +6 -73
  54. data/lib/rufio/script_file.rb +44 -0
  55. data/lib/rufio/script_path_manager.rb +9 -43
  56. data/lib/rufio/script_runner.rb +13 -50
  57. data/lib/rufio/search_controller.rb +1 -1
  58. data/lib/rufio/shell_command_completion.rb +1 -5
  59. data/lib/rufio/shell_init.rb +90 -0
  60. data/lib/rufio/task_status.rb +5 -1
  61. data/lib/rufio/terminal_ui.rb +84 -298
  62. data/lib/rufio/text_utils.rb +26 -3
  63. data/lib/rufio/ui_renderer.rb +73 -45
  64. data/lib/rufio/version.rb +1 -1
  65. data/lib/rufio/zip_archive.rb +314 -0
  66. data/lib/rufio/zip_extractor.rb +346 -0
  67. data/lib/rufio/zip_writer.rb +268 -0
  68. data/lib/rufio/zoxide_integration.rb +5 -43
  69. data/lib/rufio.rb +6 -0
  70. data/lib_zig/rufio_native/Makefile +54 -17
  71. data/lib_zig/rufio_native/bench.sh +36 -0
  72. data/lib_zig/rufio_native/build.zig +32 -35
  73. data/lib_zig/rufio_native/src/cli.zig +99 -0
  74. data/lib_zig/rufio_native/src/main.zig +187 -83
  75. data/lib_zig/rufio_native/src/os.zig +889 -0
  76. data/lib_zig/rufio_native/src/search.zig +919 -0
  77. data/lib_zig/rufio_native/src/search_test.zig +88 -0
  78. data/lib_zig/rufio_native/src/zip.zig +482 -0
  79. metadata +128 -3
  80. data/lib/rufio/native/rufio_zig.bundle +0 -0
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'dialog_support'
4
+
3
5
  require_relative 'file_operations'
4
6
  require_relative 'dialog_renderer'
5
7
  require_relative 'logger'
@@ -8,6 +10,8 @@ module Rufio
8
10
  # ファイル操作専用コントローラ
9
11
  # KeybindHandler からファイル作成・削除・リネーム・移動・コピー系メソッドを分離し、単一責任原則に準拠
10
12
  class FileOperationController
13
+ include DialogSupport
14
+
11
15
  # Dialog size constants
12
16
  CONFIRMATION_DIALOG_WIDTH = 45
13
17
  DIALOG_BORDER_HEIGHT = 4
@@ -35,51 +39,15 @@ module Rufio
35
39
  # ============================
36
40
 
37
41
  def create_file
38
- current_path = @directory_listing&.current_path || Dir.pwd
39
-
40
- filename = @dialog_renderer.show_input_dialog("Create File", "Enter file name:", {
41
- border_color: "\e[32m",
42
- title_color: "\e[1;32m",
43
- content_color: "\e[37m"
44
- })
45
-
46
- return false if filename.nil? || filename.empty?
47
-
48
- result = @file_operations.create_file(current_path, filename)
49
-
50
- if result.success
51
- @directory_listing.refresh
52
- entries = @directory_listing.list_entries
53
- new_file_index = entries.find_index { |entry| entry[:name] == filename }
54
- @nav_controller.select_index(new_file_index) if new_file_index
42
+ prompt_and_select_entry("Create File", "Enter file name:", "\e[32m", "\e[1;32m") do |path, name|
43
+ @file_operations.create_file(path, name)
55
44
  end
56
-
57
- @terminal_ui&.refresh_display
58
- result.success
59
45
  end
60
46
 
61
47
  def create_directory
62
- current_path = @directory_listing&.current_path || Dir.pwd
63
-
64
- dirname = @dialog_renderer.show_input_dialog("Create Directory", "Enter directory name:", {
65
- border_color: "\e[34m",
66
- title_color: "\e[1;34m",
67
- content_color: "\e[37m"
68
- })
69
-
70
- return false if dirname.nil? || dirname.empty?
71
-
72
- result = @file_operations.create_directory(current_path, dirname)
73
-
74
- if result.success
75
- @directory_listing.refresh
76
- entries = @directory_listing.list_entries
77
- new_dir_index = entries.find_index { |entry| entry[:name] == dirname }
78
- @nav_controller.select_index(new_dir_index) if new_dir_index
48
+ prompt_and_select_entry("Create Directory", "Enter directory name:", "\e[34m", "\e[1;34m") do |path, name|
49
+ @file_operations.create_directory(path, name)
79
50
  end
80
-
81
- @terminal_ui&.refresh_display
82
- result.success
83
51
  end
84
52
 
85
53
  # ============================
@@ -91,27 +59,9 @@ module Rufio
91
59
  return false unless entry
92
60
 
93
61
  current_name = entry[:name]
94
- current_path = @directory_listing&.current_path || Dir.pwd
95
-
96
- new_name = @dialog_renderer.show_input_dialog("Rename: #{current_name}", "Enter new name:", {
97
- border_color: "\e[33m",
98
- title_color: "\e[1;33m",
99
- content_color: "\e[37m"
100
- })
101
-
102
- return false if new_name.nil? || new_name.empty?
103
-
104
- result = @file_operations.rename(current_path, current_name, new_name)
105
-
106
- if result.success
107
- @directory_listing.refresh
108
- entries = @directory_listing.list_entries
109
- new_index = entries.find_index { |e| e[:name] == new_name }
110
- @nav_controller.select_index(new_index) if new_index
62
+ prompt_and_select_entry("Rename: #{current_name}", "Enter new name:", "\e[33m", "\e[1;33m") do |path, new_name|
63
+ @file_operations.rename(path, current_name, new_name)
111
64
  end
112
-
113
- @terminal_ui&.refresh_display
114
- result.success
115
65
  end
116
66
 
117
67
  # ============================
@@ -154,31 +104,13 @@ module Rufio
154
104
  ''
155
105
  ]
156
106
 
157
- title = 'Confirm Delete'
158
- width = [50, current_name.length + 10].max
159
- height = content_lines.length + 4
160
-
161
- confirmed = false
162
- show_overlay_dialog(title, content_lines, {
163
- width: width,
164
- height: height,
107
+ confirmed = confirm_dialog('Confirm Delete', content_lines, {
108
+ width: [50, current_name.length + 10].max,
109
+ height: content_lines.length + 4,
165
110
  border_color: "\e[31m",
166
111
  title_color: "\e[1;31m",
167
112
  content_color: "\e[37m"
168
- }) do
169
- loop do
170
- input = STDIN.getch.downcase
171
- case input
172
- when 'y'
173
- confirmed = true
174
- break
175
- when 'n', "\e"
176
- confirmed = false
177
- break
178
- end
179
- end
180
- nil
181
- end
113
+ })
182
114
 
183
115
  return false unless confirmed
184
116
 
@@ -260,69 +192,39 @@ module Rufio
260
192
  ''
261
193
  ]
262
194
 
263
- confirmed = false
264
- show_overlay_dialog('Exit Confirmation', content_lines, {
195
+ confirm_dialog('Exit Confirmation', content_lines, {
265
196
  width: CONFIRMATION_DIALOG_WIDTH,
266
197
  height: DIALOG_BORDER_HEIGHT + content_lines.length,
267
198
  border_color: "\e[33m",
268
199
  title_color: "\e[1;33m",
269
200
  content_color: "\e[37m"
270
- }) do
271
- loop do
272
- input = STDIN.getch.downcase
273
- case input
274
- when 'y'
275
- confirmed = true
276
- break
277
- when 'n', "\e", "\x03"
278
- confirmed = false
279
- break
280
- end
281
- end
282
- nil
283
- end
284
-
285
- confirmed
201
+ }, cancel_on_quit: false)
286
202
  end
287
203
 
288
204
  private
289
205
 
290
- def show_overlay_dialog(title, content_lines, options = {}, &block)
291
- use_overlay = @terminal_ui &&
292
- @terminal_ui.respond_to?(:screen) &&
293
- @terminal_ui.respond_to?(:renderer) &&
294
- @terminal_ui.screen &&
295
- @terminal_ui.renderer
296
-
297
- if use_overlay
298
- @terminal_ui.show_overlay_dialog(title, content_lines, options, &block)
299
- else
300
- width = options[:width]
301
- height = options[:height]
302
-
303
- unless width && height
304
- width, height = @dialog_renderer.calculate_dimensions(content_lines, {
305
- title: title,
306
- min_width: options[:min_width] || 40,
307
- max_width: options[:max_width] || 80
308
- })
309
- end
310
-
311
- x, y = @dialog_renderer.calculate_center(width, height)
206
+ def prompt_and_select_entry(title, prompt, border_color, title_color)
207
+ current_path = @directory_listing&.current_path || Dir.pwd
312
208
 
313
- @dialog_renderer.draw_floating_window(x, y, width, height, title, content_lines, {
314
- border_color: options[:border_color] || "\e[37m",
315
- title_color: options[:title_color] || "\e[1;33m",
316
- content_color: options[:content_color] || "\e[37m"
317
- })
209
+ name = @dialog_renderer.show_input_dialog(title, prompt, {
210
+ border_color: border_color,
211
+ title_color: title_color,
212
+ content_color: "\e[37m"
213
+ })
318
214
 
319
- key = block_given? ? yield : STDIN.getch
215
+ return false if name.nil? || name.empty?
320
216
 
321
- @dialog_renderer.clear_area(x, y, width, height)
322
- @terminal_ui&.refresh_display
217
+ result = yield(current_path, name)
323
218
 
324
- key
219
+ if result.success
220
+ @directory_listing.refresh
221
+ entries = @directory_listing.list_entries
222
+ new_index = entries.find_index { |e| e[:name] == name }
223
+ @nav_controller.select_index(new_index) if new_index
325
224
  end
225
+
226
+ @terminal_ui&.refresh_display
227
+ result.success
326
228
  end
327
229
 
328
230
  def show_delete_confirmation(count, source_path = nil)
@@ -342,111 +244,37 @@ module Rufio
342
244
 
343
245
  print "\a"
344
246
 
345
- confirmed = false
346
- show_overlay_dialog(title, content_lines, {
247
+ confirm_dialog(title, content_lines, {
347
248
  width: dialog_width,
348
249
  height: dialog_height,
349
250
  border_color: "\e[31m",
350
251
  title_color: "\e[1;31m",
351
252
  content_color: "\e[37m"
352
- }) do
353
- loop do
354
- input = STDIN.getch.downcase
355
- case input
356
- when 'y'
357
- confirmed = true
358
- break
359
- when 'n', "\e", "\x03", 'q'
360
- confirmed = false
361
- break
362
- end
363
- end
364
- nil
365
- end
366
-
367
- confirmed
253
+ })
368
254
  end
369
255
 
370
256
  def show_move_confirmation(count, source_path, dest_path)
371
- source_display = shorten_path(source_path, 35)
372
- dest_display = shorten_path(dest_path, 35)
373
-
374
- content_lines = [
375
- '',
376
- "Move #{count} item(s)?",
377
- '',
378
- "From: #{source_display}",
379
- "To: #{dest_display}",
380
- '',
381
- ' [Y]es - Move',
382
- ' [N]o - Cancel',
383
- ''
384
- ]
385
-
386
- confirmed = false
387
- show_overlay_dialog('Move Confirmation', content_lines, {
388
- width: CONFIRMATION_DIALOG_WIDTH,
389
- height: DIALOG_BORDER_HEIGHT + content_lines.length,
390
- border_color: "\e[34m",
391
- title_color: "\e[1;34m",
392
- content_color: "\e[37m"
393
- }) do
394
- loop do
395
- input = STDIN.getch.downcase
396
- case input
397
- when 'y'
398
- confirmed = true
399
- break
400
- when 'n', "\e", "\x03", 'q'
401
- confirmed = false
402
- break
403
- end
404
- end
405
- nil
406
- end
407
-
408
- confirmed
257
+ show_transfer_confirmation('Move', count, source_path, dest_path, '34')
409
258
  end
410
259
 
411
260
  def show_copy_confirmation(count, source_path, dest_path)
412
- source_display = shorten_path(source_path, 35)
413
- dest_display = shorten_path(dest_path, 35)
261
+ show_transfer_confirmation('Copy', count, source_path, dest_path, '32')
262
+ end
414
263
 
264
+ def show_transfer_confirmation(action, count, source_path, dest_path, color)
415
265
  content_lines = [
416
- '',
417
- "Copy #{count} item(s)?",
418
- '',
419
- "From: #{source_display}",
420
- "To: #{dest_display}",
421
- '',
422
- ' [Y]es - Copy',
423
- ' [N]o - Cancel',
424
- ''
266
+ '', "#{action} #{count} item(s)?", '',
267
+ "From: #{shorten_path(source_path, 35)}",
268
+ "To: #{shorten_path(dest_path, 35)}", '',
269
+ " [Y]es - #{action}", ' [N]o - Cancel', ''
425
270
  ]
426
-
427
- confirmed = false
428
- show_overlay_dialog('Copy Confirmation', content_lines, {
271
+ confirm_dialog("#{action} Confirmation", content_lines, {
429
272
  width: CONFIRMATION_DIALOG_WIDTH,
430
273
  height: DIALOG_BORDER_HEIGHT + content_lines.length,
431
- border_color: "\e[32m",
432
- title_color: "\e[1;32m",
274
+ border_color: "\e[#{color}m",
275
+ title_color: "\e[1;#{color}m",
433
276
  content_color: "\e[37m"
434
- }) do
435
- loop do
436
- input = STDIN.getch.downcase
437
- case input
438
- when 'y'
439
- confirmed = true
440
- break
441
- when 'n', "\e", "\x03", 'q'
442
- confirmed = false
443
- break
444
- end
445
- end
446
- nil
447
- end
448
-
449
- confirmed
277
+ })
450
278
  end
451
279
 
452
280
  def show_deletion_result(success_count, total_count, error_messages = [])
@@ -484,14 +312,13 @@ module Rufio
484
312
  title_color: title_color,
485
313
  content_color: "\e[37m"
486
314
  }) do
487
- STDIN.getch
315
+ KeyInput.getch
488
316
  nil
489
317
  end
490
318
  end
491
319
 
492
320
  def shorten_path(path, max_length)
493
- return path if path.length <= max_length
494
- "...#{path[-(max_length - 3)..-1]}"
321
+ TextUtils.shorten_path_from_end(path, max_length)
495
322
  end
496
323
  end
497
324
  end
@@ -246,16 +246,17 @@ module Rufio
246
246
  dest_path = File.join(destination, item_name)
247
247
 
248
248
  begin
249
- if File.exist?(dest_path)
250
- error_messages << "#{item_name}: Already exists in destination"
251
- next
252
- end
253
-
254
249
  case operation
255
250
  when :move
251
+ if File.exist?(dest_path)
252
+ error_messages << "#{item_name}: Already exists in destination"
253
+ next
254
+ end
256
255
  FileUtils.mv(source_path, dest_path)
257
256
  when :copy
258
- if File.directory?(source_path)
257
+ is_directory = File.directory?(source_path)
258
+ dest_path = resolve_copy_destination(destination, item_name, is_directory)
259
+ if is_directory
259
260
  FileUtils.cp_r(source_path, dest_path)
260
261
  else
261
262
  FileUtils.cp(source_path, dest_path)
@@ -281,6 +282,27 @@ module Rufio
281
282
  )
282
283
  end
283
284
 
285
+ def resolve_copy_destination(destination, item_name, is_directory)
286
+ dest_path = File.join(destination, item_name)
287
+ return dest_path unless File.exist?(dest_path)
288
+
289
+ if is_directory
290
+ base = item_name
291
+ ext = ''
292
+ else
293
+ ext = File.extname(item_name)
294
+ base = File.basename(item_name, ext)
295
+ end
296
+
297
+ index = 1
298
+ loop do
299
+ candidate = File.join(destination, "#{base}_#{index}#{ext}")
300
+ return candidate unless File.exist?(candidate)
301
+
302
+ index += 1
303
+ end
304
+ end
305
+
284
306
  # Build delete operation message
285
307
  # @param success_count [Integer] Number of successfully deleted items
286
308
  # @param total_count [Integer] Total number of items
@@ -13,6 +13,9 @@ module Rufio
13
13
  CONTROL_CHAR_NEWLINE = 10
14
14
  CONTROL_CHAR_CARRIAGE_RETURN = 13
15
15
 
16
+ # ZIPプレビューで一覧表示するエントリ数の上限
17
+ ARCHIVE_PREVIEW_ENTRIES = 200
18
+
16
19
  def initialize
17
20
  # future: hold syntax highlight settings etc.
18
21
  end
@@ -25,6 +28,8 @@ module Rufio
25
28
  return empty_response if file_size == 0
26
29
 
27
30
  begin
31
+ return archive_response(file_path, max_lines) if zip_archive?(file_path)
32
+
28
33
  # binary file detection
29
34
  sample = File.binread(file_path, [file_size, BINARY_SAMPLE_SIZE].min)
30
35
  return binary_response(file_path) if binary_file?(sample)
@@ -49,6 +54,45 @@ module Rufio
49
54
 
50
55
  private
51
56
 
57
+ def zip_archive?(file_path)
58
+ File.extname(file_path).casecmp?('.zip') || ZipArchive.zip_signature?(file_path)
59
+ end
60
+
61
+ # unzip -l 相当のエントリ一覧をプレビュー行として返す
62
+ def archive_response(file_path, max_lines)
63
+ limit = [max_lines, ARCHIVE_PREVIEW_ENTRIES].max
64
+ listing = ZipArchive.list(file_path, limit: limit)
65
+ total_size = listing.entries.sum(&:size)
66
+
67
+ lines = ["ZIP archive: #{listing.total_entries} entries#{' (ZIP64)' if listing.zip64?}", '']
68
+ lines << format('%12s %-8s %-16s %s', 'Length', 'Method', 'Modified', 'Name')
69
+ lines << format('%12s %-8s %-16s %s', '-' * 10, '-' * 8, '-' * 16, '-' * 4)
70
+ listing.entries.each do |entry|
71
+ modified = entry.mtime ? entry.mtime.strftime('%Y-%m-%d %H:%M') : '-'
72
+ method = entry.encrypted? ? "#{ZipArchive.method_name(entry.method)}*" : ZipArchive.method_name(entry.method)
73
+ lines << format('%12d %-8s %-16s %s', entry.size, method, modified, entry.name)
74
+ end
75
+ lines << format('%12s', '-' * 10)
76
+ if listing.truncated?
77
+ lines << "... #{listing.total_entries - listing.entries.length} more entries"
78
+ else
79
+ lines << format('%12d %d files', total_size, listing.entries.length)
80
+ end
81
+ lines << "Comment: #{listing.comment}" unless listing.comment.empty?
82
+
83
+ {
84
+ type: "archive",
85
+ language: nil,
86
+ lines: lines,
87
+ truncated: listing.truncated?,
88
+ size: File.size(file_path),
89
+ modified: File.mtime(file_path),
90
+ encoding: "UTF-8"
91
+ }
92
+ rescue ZipArchive::FormatError => e
93
+ error_response("#{ConfigLoader.message('file.invalid_archive')}: #{e.message}")
94
+ end
95
+
52
96
  def binary_file?(sample)
53
97
  return false if sample.empty?
54
98
 
@@ -77,7 +121,7 @@ module Rufio
77
121
  end
78
122
 
79
123
  lines << line.chomp
80
- rescue EncodingError, ArgumentError => e
124
+ rescue EncodingError, ArgumentError
81
125
  # If encoding fails, add placeholder
82
126
  lines << "[encoding error in line #{index + 1}]"
83
127
  end
@@ -104,7 +148,7 @@ module Rufio
104
148
  # Ensure line is properly encoded as UTF-8
105
149
  line = line.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?')
106
150
  lines << line.chomp
107
- rescue EncodingError, ArgumentError => e
151
+ rescue EncodingError, ArgumentError
108
152
  # If encoding fails, add placeholder
109
153
  lines << "[encoding error in line #{index + 1}]"
110
154
  end