rufio 1.1.0 → 1.3.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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -0
  3. data/README.md +74 -3
  4. data/README_ja.md +71 -2
  5. data/Rakefile +14 -1
  6. data/bin/rufio +31 -2
  7. data/docs/fast_zip_design_ja.md +167 -0
  8. data/info/help.md +2 -0
  9. data/info/keybindings.md +3 -1
  10. data/info/welcome.md +1 -1
  11. data/lib/rufio/application.rb +12 -1
  12. data/lib/rufio/archive_controller.rb +211 -0
  13. data/lib/rufio/background_command_executor.rb +9 -0
  14. data/lib/rufio/background_status.rb +49 -0
  15. data/lib/rufio/bookmark_controller.rb +38 -121
  16. data/lib/rufio/bookmark_manager.rb +77 -331
  17. data/lib/rufio/command_completion.rb +6 -1
  18. data/lib/rufio/command_history.rb +5 -2
  19. data/lib/rufio/command_mode.rb +40 -134
  20. data/lib/rufio/command_mode_ui.rb +4 -42
  21. data/lib/rufio/config.rb +88 -136
  22. data/lib/rufio/config_loader.rb +3 -11
  23. data/lib/rufio/dialog_renderer.rb +64 -152
  24. data/lib/rufio/dialog_support.rb +69 -0
  25. data/lib/rufio/document_text_extractor.rb +167 -0
  26. data/lib/rufio/file_operation_controller.rb +49 -222
  27. data/lib/rufio/file_preview.rb +46 -2
  28. data/lib/rufio/global_search.rb +121 -9
  29. data/lib/rufio/help_menu.rb +8 -2
  30. data/lib/rufio/interpreter_resolver.rb +6 -11
  31. data/lib/rufio/job_manager.rb +2 -5
  32. data/lib/rufio/key_input.rb +77 -0
  33. data/lib/rufio/keybind_handler.rb +81 -115
  34. data/lib/rufio/local_script_scanner.rb +6 -18
  35. data/lib/rufio/native/aarch64-linux-gnu/librufio_zig.so +0 -0
  36. data/lib/rufio/native/aarch64-linux-musl/librufio_zig.so +0 -0
  37. data/lib/rufio/native/aarch64-macos/librufio_zig.dylib +0 -0
  38. data/lib/rufio/native/aarch64-windows/rufio_zig.dll +0 -0
  39. data/lib/rufio/native/x86_64-linux-gnu/librufio_zig.so +0 -0
  40. data/lib/rufio/native/x86_64-linux-musl/librufio_zig.so +0 -0
  41. data/lib/rufio/native/x86_64-macos/librufio_zig.dylib +0 -0
  42. data/lib/rufio/native/x86_64-windows/rufio_zig.dll +0 -0
  43. data/lib/rufio/native_content_search.rb +153 -0
  44. data/lib/rufio/native_scanner.rb +13 -68
  45. data/lib/rufio/native_scanner_zig.rb +106 -56
  46. data/lib/rufio/native_zip.rb +121 -0
  47. data/lib/rufio/navigation_controller.rb +18 -20
  48. data/lib/rufio/screen.rb +2 -12
  49. data/lib/rufio/script_executor.rb +6 -73
  50. data/lib/rufio/script_file.rb +44 -0
  51. data/lib/rufio/script_path_manager.rb +9 -43
  52. data/lib/rufio/script_runner.rb +13 -50
  53. data/lib/rufio/search_controller.rb +1 -1
  54. data/lib/rufio/shell_command_completion.rb +1 -5
  55. data/lib/rufio/shell_init.rb +90 -0
  56. data/lib/rufio/task_status.rb +5 -1
  57. data/lib/rufio/terminal_ui.rb +55 -294
  58. data/lib/rufio/text_utils.rb +26 -3
  59. data/lib/rufio/ui_renderer.rb +63 -40
  60. data/lib/rufio/version.rb +1 -1
  61. data/lib/rufio/zip_archive.rb +314 -0
  62. data/lib/rufio/zip_extractor.rb +346 -0
  63. data/lib/rufio/zip_writer.rb +268 -0
  64. data/lib/rufio/zoxide_integration.rb +5 -43
  65. data/lib/rufio.rb +5 -0
  66. data/lib_zig/rufio_native/Makefile +54 -17
  67. data/lib_zig/rufio_native/bench.sh +36 -0
  68. data/lib_zig/rufio_native/build.zig +32 -35
  69. data/lib_zig/rufio_native/src/cli.zig +99 -0
  70. data/lib_zig/rufio_native/src/main.zig +187 -83
  71. data/lib_zig/rufio_native/src/os.zig +889 -0
  72. data/lib_zig/rufio_native/src/search.zig +919 -0
  73. data/lib_zig/rufio_native/src/search_test.zig +88 -0
  74. data/lib_zig/rufio_native/src/zip.zig +482 -0
  75. metadata +125 -3
  76. 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
+ require_relative 'text_utils'
3
5
  require_relative 'bookmark'
4
6
  require_relative 'bookmark_storage'
5
7
  require_relative 'config_loader'
@@ -7,6 +9,8 @@ require_relative 'config_loader'
7
9
  module Rufio
8
10
  # Manages bookmark operations with interactive UI
9
11
  class BookmarkManager
12
+ include DialogSupport
13
+
10
14
  def initialize(bookmark = nil, dialog_renderer = nil)
11
15
  @bookmark = bookmark || create_default_bookmark
12
16
  @dialog_renderer = dialog_renderer
@@ -31,159 +35,29 @@ module Rufio
31
35
 
32
36
  public
33
37
 
34
- # Show bookmark menu and handle user selection
35
- # @param current_path [String] Current directory path
36
- # @return [Symbol, nil] Action to perform (:navigate, :add, :list, :remove, :cancel)
37
- def show_menu(current_path)
38
- return :cancel unless @dialog_renderer
39
-
40
- title = 'Bookmark Menu'
41
- content_lines = [
42
- '',
43
- '[A]dd current directory to bookmarks',
44
- '[L]ist bookmarks',
45
- '[R]ename bookmark',
46
- '[D]elete bookmark',
47
- '',
48
- 'Press 1-9 to go to bookmark directly',
49
- '',
50
- 'Press any other key to cancel'
51
- ]
52
-
53
- dialog_width = 45
54
- dialog_height = 4 + content_lines.length
55
-
56
- result = { action: :cancel }
57
- show_overlay_dialog(title, content_lines, {
58
- width: dialog_width,
59
- height: dialog_height,
60
- border_color: "\e[34m", # Blue
61
- title_color: "\e[1;34m", # Bold blue
62
- content_color: "\e[37m" # White
63
- }) do
64
- # Wait for key input
65
- loop do
66
- input = STDIN.getch.downcase
67
-
68
- case input
69
- when 'a'
70
- result = { action: :add, path: current_path }
71
- break
72
- when 'l'
73
- result = { action: :list }
74
- break
75
- when 'r'
76
- result = { action: :rename }
77
- break
78
- when 'd'
79
- result = { action: :remove }
80
- break
81
- when '1', '2', '3', '4', '5', '6', '7', '8', '9'
82
- result = { action: :navigate, number: input.to_i }
83
- break
84
- else
85
- # Cancel
86
- result = { action: :cancel }
87
- break
88
- end
89
- end
90
- nil
91
- end
92
-
93
- result
94
- end
95
-
96
- # Add a bookmark interactively
97
- # @param path [String] Path to bookmark
98
- # @return [Boolean] Success status
99
- def add_interactive(path)
100
- return false unless @dialog_renderer
101
-
102
- # Show floating input dialog
103
- title = 'Add Bookmark'
104
- prompt = 'Enter bookmark name:'
105
- name = @dialog_renderer.show_input_dialog(title, prompt, {
106
- border_color: "\e[34m", # Blue
107
- title_color: "\e[1;34m", # Bold blue
108
- content_color: "\e[37m" # White
109
- })
110
-
111
- return false if name.nil? || name.empty?
112
-
113
- # Add bookmark (name is already trimmed in show_input_dialog)
114
- if @bookmark.add(path, name)
115
- show_result_dialog('Bookmark Added', "Added: #{name}", :success)
116
- true
117
- else
118
- show_result_dialog('Add Failed', 'Failed to add bookmark', :error)
119
- false
120
- end
38
+ # List all bookmarks interactively
39
+ # @return [Hash, nil] Selected bookmark path or nil
40
+ def list_interactive
41
+ select_bookmark_from_dialog(
42
+ title: 'Bookmarks',
43
+ header: 'Select a bookmark:',
44
+ border_color: "\e[34m",
45
+ title_color: "\e[1;34m"
46
+ )
121
47
  end
122
48
 
123
49
  # Rename a bookmark interactively
124
50
  # @return [Boolean] Success status
125
51
  def rename_interactive
126
- bookmarks = @bookmark.list
127
-
128
- if bookmarks.empty?
129
- show_result_dialog('No Bookmarks', 'No bookmarks found', :error)
130
- return false
131
- end
132
-
133
- return false unless @dialog_renderer
134
-
135
- # Build content lines for bookmark selection
136
- content_lines = ['', 'Select bookmark to rename:', '']
137
- bookmarks.each_with_index do |bookmark, index|
138
- # Truncate path if too long
139
- display_path = bookmark[:path]
140
- if display_path.start_with?(Dir.home)
141
- display_path = display_path.sub(Dir.home, '~')
142
- end
143
- if display_path.length > 35
144
- display_path = "...#{display_path[-32..]}"
145
- end
146
- content_lines << " #{index + 1}. #{bookmark[:name]}"
147
- content_lines << " #{display_path}"
148
- end
149
- content_lines << ''
150
- content_lines << 'Press 1-9 to select, ESC to cancel'
151
-
152
- title = 'Rename Bookmark'
153
- width = 60
154
- height = [content_lines.length + 4, 20].min
155
-
156
- # Wait for selection
157
- selected_number = nil
158
- show_overlay_dialog(title, content_lines, {
159
- width: width,
160
- height: height,
161
- border_color: "\e[33m", # Yellow
162
- title_color: "\e[1;33m", # Bold yellow
163
- content_color: "\e[37m" # White
164
- }) do
165
- loop do
166
- input = STDIN.getch.downcase
167
-
168
- if input >= '1' && input <= '9'
169
- number = input.to_i
170
- if number > 0 && number <= bookmarks.length
171
- selected_number = number
172
- break
173
- end
174
- elsif input == "\e" # ESC
175
- break
176
- end
177
- end
178
- nil
179
- end
180
-
181
- return false unless selected_number
182
-
183
- # Get new name
184
- bookmark_to_rename = bookmarks[selected_number - 1]
185
- old_name = bookmark_to_rename[:name]
186
-
52
+ selected = select_bookmark_from_dialog(
53
+ title: 'Rename Bookmark',
54
+ header: 'Select bookmark to rename:',
55
+ border_color: "\e[33m",
56
+ title_color: "\e[1;33m"
57
+ )
58
+ return false unless selected
59
+
60
+ old_name = selected[:name]
187
61
  new_name = @dialog_renderer.show_input_dialog("Rename: #{old_name}", "Enter new name:", {
188
62
  border_color: "\e[33m", # Yellow
189
63
  title_color: "\e[1;33m", # Bold yellow
@@ -192,140 +66,27 @@ module Rufio
192
66
 
193
67
  return false if new_name.nil? || new_name.empty?
194
68
 
195
- # Rename bookmark
196
69
  @bookmark.rename(old_name, new_name)
197
70
  end
198
71
 
199
72
  # Remove a bookmark interactively
200
73
  # @return [Boolean] Success status
201
74
  def remove_interactive
202
- bookmarks = @bookmark.list
203
-
204
- if bookmarks.empty?
205
- show_result_dialog('No Bookmarks', 'No bookmarks found', :error)
206
- return false
207
- end
208
-
209
- return false unless @dialog_renderer
210
-
211
- # Build content lines for bookmark selection
212
- content_lines = ['', 'Select bookmark to delete:', '']
213
- bookmarks.each_with_index do |bookmark, index|
214
- # Truncate path if too long
215
- display_path = bookmark[:path]
216
- if display_path.start_with?(Dir.home)
217
- display_path = display_path.sub(Dir.home, '~')
218
- end
219
- if display_path.length > 35
220
- display_path = "...#{display_path[-32..]}"
221
- end
222
- content_lines << " #{index + 1}. #{bookmark[:name]}"
223
- content_lines << " #{display_path}"
224
- end
225
- content_lines << ''
226
- content_lines << 'Press 1-9 to select, ESC to cancel'
227
-
228
- title = 'Delete Bookmark'
229
- width = 60
230
- height = [content_lines.length + 4, 20].min
231
-
232
- # Wait for selection
233
- selected_number = nil
234
- show_overlay_dialog(title, content_lines, {
235
- width: width,
236
- height: height,
237
- border_color: "\e[31m", # Red (warning)
238
- title_color: "\e[1;31m", # Bold red
239
- content_color: "\e[37m" # White
240
- }) do
241
- loop do
242
- input = STDIN.getch.downcase
243
-
244
- if input >= '1' && input <= '9'
245
- number = input.to_i
246
- if number > 0 && number <= bookmarks.length
247
- selected_number = number
248
- break
249
- end
250
- elsif input == "\e" # ESC
251
- break
252
- end
253
- end
254
- nil
255
- end
256
-
257
- return false unless selected_number
258
-
259
- # Confirm deletion
260
- bookmark_to_remove = bookmarks[selected_number - 1]
261
- if show_remove_confirmation(bookmark_to_remove[:name])
262
- @bookmark.remove(bookmark_to_remove[:name])
75
+ selected = select_bookmark_from_dialog(
76
+ title: 'Delete Bookmark',
77
+ header: 'Select bookmark to delete:',
78
+ border_color: "\e[31m",
79
+ title_color: "\e[1;31m"
80
+ )
81
+ return false unless selected
82
+
83
+ if show_remove_confirmation(selected[:name])
84
+ @bookmark.remove(selected[:name])
263
85
  else
264
86
  false
265
87
  end
266
88
  end
267
89
 
268
- # List all bookmarks interactively
269
- # @return [Hash, nil] Selected bookmark path or nil
270
- def list_interactive
271
- bookmarks = @bookmark.list
272
-
273
- if bookmarks.empty?
274
- show_result_dialog('No Bookmarks', 'No bookmarks found', :error)
275
- return nil
276
- end
277
-
278
- return nil unless @dialog_renderer
279
-
280
- # Build content lines
281
- content_lines = ['', 'Select a bookmark:','']
282
- bookmarks.each_with_index do |bookmark, index|
283
- # Truncate path if too long
284
- display_path = bookmark[:path]
285
- if display_path.start_with?(Dir.home)
286
- display_path = display_path.sub(Dir.home, '~')
287
- end
288
- if display_path.length > 35
289
- display_path = "...#{display_path[-32..]}"
290
- end
291
- content_lines << " #{index + 1}. #{bookmark[:name]}"
292
- content_lines << " #{display_path}"
293
- end
294
- content_lines << ''
295
- content_lines << 'Press 1-9 to select, ESC to cancel'
296
-
297
- title = 'Bookmarks'
298
- width = 60
299
- height = [content_lines.length + 4, 20].min
300
-
301
- # Wait for selection
302
- selected_bookmark = nil
303
- show_overlay_dialog(title, content_lines, {
304
- width: width,
305
- height: height,
306
- border_color: "\e[34m", # Blue
307
- title_color: "\e[1;34m", # Bold blue
308
- content_color: "\e[37m" # White
309
- }) do
310
- loop do
311
- input = STDIN.getch.downcase
312
-
313
- if input >= '1' && input <= '9'
314
- number = input.to_i
315
- if number > 0 && number <= bookmarks.length
316
- selected_bookmark = bookmarks[number - 1]
317
- break
318
- end
319
- elsif input == "\e" # ESC
320
- break
321
- end
322
- end
323
- nil
324
- end
325
-
326
- selected_bookmark
327
- end
328
-
329
90
  # Get bookmark by number
330
91
  # @param number [Integer] Bookmark number (1-9)
331
92
  # @return [Hash, nil] Bookmark hash with :path and :name
@@ -390,46 +151,53 @@ module Rufio
390
151
 
391
152
  private
392
153
 
393
- # オーバーレイダイアログを表示してキー入力を待つヘルパーメソッド
394
- def show_overlay_dialog(title, content_lines, options = {}, &block)
395
- # terminal_ui が利用可能で、screen と renderer が存在する場合のみオーバーレイを使用
396
- use_overlay = @terminal_ui &&
397
- @terminal_ui.respond_to?(:screen) &&
398
- @terminal_ui.respond_to?(:renderer) &&
399
- @terminal_ui.screen &&
400
- @terminal_ui.renderer
401
-
402
- if use_overlay
403
- # オーバーレイを使用
404
- @terminal_ui.show_overlay_dialog(title, content_lines, options, &block)
405
- else
406
- # フォールバック: 従来の方法
407
- width = options[:width]
408
- height = options[:height]
409
-
410
- unless width && height
411
- width, height = @dialog_renderer.calculate_dimensions(content_lines, {
412
- title: title,
413
- min_width: options[:min_width] || 40,
414
- max_width: options[:max_width] || 80
415
- })
416
- end
154
+ def select_bookmark_from_dialog(title:, header:, border_color:, title_color:)
155
+ bookmarks = @bookmark.list
156
+
157
+ if bookmarks.empty?
158
+ show_result_dialog('No Bookmarks', 'No bookmarks found', :error)
159
+ return nil
160
+ end
417
161
 
418
- x, y = @dialog_renderer.calculate_center(width, height)
162
+ return nil unless @dialog_renderer
419
163
 
420
- @dialog_renderer.draw_floating_window(x, y, width, height, title, content_lines, {
421
- border_color: options[:border_color] || "\e[37m",
422
- title_color: options[:title_color] || "\e[1;33m",
423
- content_color: options[:content_color] || "\e[37m"
424
- })
164
+ content_lines = ['', header, '']
165
+ bookmarks.each_with_index do |bookmark, index|
166
+ display_path = TextUtils.shorten_path_from_end(bookmark[:path], 35, tilde: true)
167
+ content_lines << " #{index + 1}. #{bookmark[:name]}"
168
+ content_lines << " #{display_path}"
169
+ end
170
+ content_lines << ''
171
+ content_lines << 'Press 1-9 to select, ESC to cancel'
425
172
 
426
- key = block_given? ? yield : STDIN.getch
173
+ width = 60
174
+ height = [content_lines.length + 4, 20].min
427
175
 
428
- @dialog_renderer.clear_area(x, y, width, height)
429
- @terminal_ui&.refresh_display
176
+ selected_bookmark = nil
177
+ show_overlay_dialog(title, content_lines, {
178
+ width: width,
179
+ height: height,
180
+ border_color: border_color,
181
+ title_color: title_color,
182
+ content_color: "\e[37m"
183
+ }) do
184
+ loop do
185
+ input = KeyInput.getch.downcase
430
186
 
431
- key
187
+ if input >= '1' && input <= '9'
188
+ number = input.to_i
189
+ if number > 0 && number <= bookmarks.length
190
+ selected_bookmark = bookmarks[number - 1]
191
+ break
192
+ end
193
+ elsif input == "\e"
194
+ break
195
+ end
196
+ end
197
+ nil
432
198
  end
199
+
200
+ selected_bookmark
433
201
  end
434
202
 
435
203
  # Show result dialog with success or error styling
@@ -483,35 +251,13 @@ module Rufio
483
251
  ''
484
252
  ]
485
253
 
486
- title = 'Confirm Delete'
487
- width = 50
488
- height = content_lines.length + 4
489
-
490
- # Wait for confirmation
491
- confirmed = false
492
- show_overlay_dialog(title, content_lines, {
493
- width: width,
494
- height: height,
254
+ confirm_dialog('Confirm Delete', content_lines, {
255
+ width: 50,
256
+ height: content_lines.length + 4,
495
257
  border_color: "\e[33m", # Yellow (warning)
496
258
  title_color: "\e[1;33m", # Bold yellow
497
259
  content_color: "\e[37m" # White
498
- }) do
499
- loop do
500
- input = STDIN.getch.downcase
501
-
502
- case input
503
- when 'y'
504
- confirmed = true
505
- break
506
- when 'n', "\e" # n or ESC
507
- confirmed = false
508
- break
509
- end
510
- end
511
- nil
512
- end
513
-
514
- confirmed
260
+ })
515
261
  end
516
262
  end
517
263
  end
@@ -4,9 +4,14 @@ module Rufio
4
4
  # コマンド補完機能を提供するクラス
5
5
  class CommandCompletion
6
6
  # 初期化
7
- # @param history [CommandHistory, nil] コマンド履歴(オプション)
7
+ # @param history [CommandHistory, CommandMode, nil] 履歴、または単独で渡すコマンドモード
8
8
  # @param command_mode [CommandMode, nil] コマンドモード(スクリプト補完に使用)
9
9
  def initialize(history = nil, command_mode = nil)
10
+ # CommandMode だけを渡す従来の呼び方も受け付ける。
11
+ if history.is_a?(CommandMode) && command_mode.nil?
12
+ command_mode = history
13
+ history = nil
14
+ end
10
15
  @command_mode = command_mode || CommandMode.new
11
16
  @shell_completion = ShellCommandCompletion.new
12
17
  @history = history
@@ -5,8 +5,6 @@ module Rufio
5
5
  class CommandHistory
6
6
  DEFAULT_MAX_SIZE = 1000
7
7
 
8
- attr_reader :size
9
-
10
8
  # 初期化
11
9
  # @param history_file [String] 履歴ファイルのパス
12
10
  # @param max_size [Integer] 履歴の最大保存数
@@ -37,6 +35,11 @@ module Rufio
37
35
  reset_position
38
36
  end
39
37
 
38
+ # 呼び出し側の変更が履歴本体や上下移動の位置に影響しないスナップショット。
39
+ def entries
40
+ @history.map(&:dup)
41
+ end
42
+
40
43
  # 前のコマンドを取得(上矢印キー相当)
41
44
  # @return [String, nil] 前のコマンド、存在しない場合はnil
42
45
  def previous