rufio 0.31.0 → 0.33.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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +132 -12
  3. data/README.md +84 -10
  4. data/README_EN.md +55 -2
  5. data/docs/CHANGELOG_v0.32.0.md +288 -0
  6. data/docs/CHANGELOG_v0.33.0.md +444 -0
  7. data/{CHANGELOG_v0.4.0.md → docs/CHANGELOG_v0.4.0.md} +2 -2
  8. data/{CHANGELOG_v0.5.0.md → docs/CHANGELOG_v0.5.0.md} +3 -0
  9. data/{CHANGELOG_v0.7.0.md → docs/CHANGELOG_v0.7.0.md} +1 -1
  10. data/{CHANGELOG_v0.8.0.md → docs/CHANGELOG_v0.8.0.md} +1 -1
  11. data/{CHANGELOG_v0.9.0.md → docs/CHANGELOG_v0.9.0.md} +1 -1
  12. data/docs/file-preview-optimization-analysis.md +759 -0
  13. data/docs/file-preview-performance-issue-FIXED.md +547 -0
  14. data/lib/rufio/application.rb +9 -1
  15. data/lib/rufio/background_command_executor.rb +98 -0
  16. data/lib/rufio/command_completion.rb +101 -0
  17. data/lib/rufio/command_history.rb +109 -0
  18. data/lib/rufio/command_logger.rb +118 -0
  19. data/lib/rufio/command_mode.rb +51 -1
  20. data/lib/rufio/command_mode_ui.rb +48 -15
  21. data/lib/rufio/config_loader.rb +9 -0
  22. data/lib/rufio/directory_listing.rb +60 -12
  23. data/lib/rufio/keybind_handler.rb +73 -2
  24. data/lib/rufio/native/rufio_native.bundle +0 -0
  25. data/lib/rufio/native/rufio_zig.bundle +0 -0
  26. data/lib/rufio/native_scanner.rb +306 -0
  27. data/lib/rufio/native_scanner_magnus.rb +194 -0
  28. data/lib/rufio/native_scanner_zig.rb +221 -0
  29. data/lib/rufio/plugins/hello.rb +30 -0
  30. data/lib/rufio/shell_command_completion.rb +120 -0
  31. data/lib/rufio/terminal_ui.rb +155 -20
  32. data/lib/rufio/version.rb +1 -1
  33. data/lib/rufio.rb +11 -0
  34. data/lib_rust/rufio_native/.cargo/config.toml +2 -0
  35. data/lib_rust/rufio_native/Cargo.lock +346 -0
  36. data/lib_rust/rufio_native/Cargo.toml +18 -0
  37. data/lib_rust/rufio_native/build.rs +46 -0
  38. data/lib_rust/rufio_native/src/lib.rs +197 -0
  39. data/lib_zig/rufio_native/Makefile +33 -0
  40. data/lib_zig/rufio_native/build.zig +45 -0
  41. data/lib_zig/rufio_native/src/main.zig +167 -0
  42. metadata +36 -13
  43. /data/{CHANGELOG_v0.10.0.md → docs/CHANGELOG_v0.10.0.md} +0 -0
  44. /data/{CHANGELOG_v0.20.0.md → docs/CHANGELOG_v0.20.0.md} +0 -0
  45. /data/{CHANGELOG_v0.21.0.md → docs/CHANGELOG_v0.21.0.md} +0 -0
  46. /data/{CHANGELOG_v0.30.0.md → docs/CHANGELOG_v0.30.0.md} +0 -0
  47. /data/{CHANGELOG_v0.31.0.md → docs/CHANGELOG_v0.31.0.md} +0 -0
  48. /data/{CHANGELOG_v0.6.0.md → docs/CHANGELOG_v0.6.0.md} +0 -0
@@ -56,6 +56,11 @@ module Rufio
56
56
  @in_help_mode = false
57
57
  @pre_help_directory = nil
58
58
 
59
+ # Log viewer mode
60
+ @in_log_viewer_mode = false
61
+ @pre_log_viewer_directory = nil
62
+ @log_dir = File.join(Dir.home, '.config', 'rufio', 'log')
63
+
59
64
  # Preview pane focus and scroll
60
65
  @preview_focused = false
61
66
  @preview_scroll_offset = 0
@@ -103,6 +108,11 @@ module Rufio
103
108
  return exit_help_mode
104
109
  end
105
110
 
111
+ # ログビューワモード中のESCキー特別処理
112
+ if @in_log_viewer_mode && key == "\e"
113
+ return exit_log_viewer_mode
114
+ end
115
+
106
116
  # フィルターモード中は他のキーバインドを無効化
107
117
  return handle_filter_input(key) if @filter_manager.filter_mode
108
118
 
@@ -167,7 +177,7 @@ module Rufio
167
177
  copy_selected_to_current
168
178
  when 'x' # x - delete selected files
169
179
  delete_selected_files
170
- when 'p' # p - project mode
180
+ when 'P' # P - project mode
171
181
  enter_project_mode
172
182
  when 'b' # b - add bookmark
173
183
  add_bookmark
@@ -179,6 +189,8 @@ module Rufio
179
189
  goto_bookmark(key.to_i)
180
190
  when '?' # ? - enter help mode
181
191
  enter_help_mode
192
+ when 'L' # L - enter log viewer mode
193
+ enter_log_viewer_mode
182
194
  when ':' # : - command mode
183
195
  activate_command_mode
184
196
  else
@@ -255,6 +267,49 @@ module Rufio
255
267
  true
256
268
  end
257
269
 
270
+ # ログビューワモード関連メソッド
271
+
272
+ # ログビューワモード中かどうか
273
+ def log_viewer_mode?
274
+ @in_log_viewer_mode
275
+ end
276
+
277
+ # ログビューワモードに入る
278
+ def enter_log_viewer_mode
279
+ return false unless @directory_listing
280
+
281
+ # 現在のディレクトリを保存
282
+ @pre_log_viewer_directory = @directory_listing.current_path
283
+
284
+ # log ディレクトリを作成(存在しない場合)
285
+ FileUtils.mkdir_p(@log_dir) unless Dir.exist?(@log_dir)
286
+
287
+ # log ディレクトリに移動
288
+ navigate_to_directory(@log_dir)
289
+
290
+ # ログビューワモードを有効化
291
+ @in_log_viewer_mode = true
292
+
293
+ true
294
+ end
295
+
296
+ # ログビューワモードを終了
297
+ def exit_log_viewer_mode
298
+ return false unless @in_log_viewer_mode
299
+ return false unless @pre_log_viewer_directory
300
+
301
+ # ログビューワモードを無効化
302
+ @in_log_viewer_mode = false
303
+
304
+ # 元のディレクトリに戻る
305
+ navigate_to_directory(@pre_log_viewer_directory)
306
+
307
+ # 保存したディレクトリをクリア
308
+ @pre_log_viewer_directory = nil
309
+
310
+ true
311
+ end
312
+
258
313
  # ヘルプモード時の制限付き親ディレクトリナビゲーション
259
314
  def navigate_parent_with_restriction
260
315
  if @in_help_mode
@@ -276,8 +331,24 @@ module Rufio
276
331
 
277
332
  # info ディレクトリ配下であれば、通常のナビゲーションを実行
278
333
  navigate_parent
334
+ elsif @in_log_viewer_mode
335
+ # log ディレクトリより上には移動できない
336
+ current_path = @directory_listing.current_path
337
+
338
+ # 現在のパスが log ディレクトリ以下でない場合は移動を許可しない
339
+ unless current_path.start_with?(@log_dir)
340
+ return false
341
+ end
342
+
343
+ # 現在のパスが log ディレクトリそのものの場合は移動を許可しない
344
+ if current_path == @log_dir
345
+ return false
346
+ end
347
+
348
+ # log ディレクトリ配下であれば、通常のナビゲーションを実行
349
+ navigate_parent
279
350
  else
280
- # ヘルプモード外では通常のナビゲーション
351
+ # ヘルプモード・ログビューワモード外では通常のナビゲーション
281
352
  navigate_parent
282
353
  end
283
354
  end
Binary file
@@ -0,0 +1,306 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ffi'
4
+ require 'json'
5
+
6
+ module Rufio
7
+ # NativeScanner - Rust/Goのネイティブライブラリを使った高速ディレクトリスキャナー
8
+ class NativeScanner
9
+ # ライブラリパス
10
+ LIB_DIR = File.expand_path('native', __dir__)
11
+ RUST_LIB = File.join(LIB_DIR, 'librufio_scanner.dylib')
12
+ GO_LIB = File.join(LIB_DIR, 'libscanner.dylib')
13
+
14
+ @mode = nil
15
+ @current_library = nil
16
+
17
+ # Rustライブラリ用のFFIモジュール
18
+ module RustLib
19
+ extend FFI::Library
20
+
21
+ begin
22
+ ffi_lib RUST_LIB
23
+ attach_function :scan_directory, [:string], :pointer
24
+ attach_function :scan_directory_fast, [:string, :int], :pointer
25
+ attach_function :get_version, [], :pointer
26
+ @available = true
27
+ rescue LoadError, FFI::NotFoundError
28
+ @available = false
29
+ end
30
+
31
+ def self.available?
32
+ @available
33
+ end
34
+ end
35
+
36
+ # Goライブラリ用のFFIモジュール
37
+ module GoLib
38
+ extend FFI::Library
39
+
40
+ begin
41
+ ffi_lib GO_LIB
42
+ attach_function :ScanDirectory, [:string], :pointer
43
+ attach_function :ScanDirectoryFast, [:string, :int], :pointer
44
+ attach_function :GetVersion, [], :pointer
45
+ attach_function :FreeCString, [:pointer], :void
46
+ @available = true
47
+ rescue LoadError, FFI::NotFoundError
48
+ @available = false
49
+ end
50
+
51
+ def self.available?
52
+ @available
53
+ end
54
+ end
55
+
56
+ class << self
57
+ # モード設定
58
+ def mode=(value)
59
+ case value
60
+ when 'rust'
61
+ if RustLib.available?
62
+ @mode = 'rust'
63
+ @current_library = RustLib
64
+ else
65
+ @mode = 'ruby'
66
+ @current_library = nil
67
+ end
68
+ when 'go'
69
+ if GoLib.available?
70
+ @mode = 'go'
71
+ @current_library = GoLib
72
+ else
73
+ @mode = 'ruby'
74
+ @current_library = nil
75
+ end
76
+ when 'auto'
77
+ # 優先順位: Rust > Go > Ruby
78
+ if RustLib.available?
79
+ @mode = 'rust'
80
+ @current_library = RustLib
81
+ elsif GoLib.available?
82
+ @mode = 'go'
83
+ @current_library = GoLib
84
+ else
85
+ @mode = 'ruby'
86
+ @current_library = nil
87
+ end
88
+ when 'ruby'
89
+ @mode = 'ruby'
90
+ @current_library = nil
91
+ else
92
+ # 無効なモードはrubyにフォールバック
93
+ @mode = 'ruby'
94
+ @current_library = nil
95
+ end
96
+ end
97
+
98
+ # 現在のモード取得
99
+ def mode
100
+ # 初回アクセス時はautoモードに設定
101
+ self.mode = 'auto' if @mode.nil?
102
+ @mode
103
+ end
104
+
105
+ # 利用可能なライブラリをチェック
106
+ def available_libraries
107
+ {
108
+ rust: RustLib.available?,
109
+ go: GoLib.available?
110
+ }
111
+ end
112
+
113
+ # ディレクトリをスキャン
114
+ def scan_directory(path)
115
+ # モードが未設定の場合は自動設定
116
+ mode if @mode.nil?
117
+
118
+ case @mode
119
+ when 'rust'
120
+ scan_with_rust(path)
121
+ when 'go'
122
+ scan_with_go(path)
123
+ else
124
+ scan_with_ruby(path)
125
+ end
126
+ end
127
+
128
+ # 高速スキャン(エントリ数制限付き)
129
+ def scan_directory_fast(path, max_entries = 1000)
130
+ # モードが未設定の場合は自動設定
131
+ mode if @mode.nil?
132
+
133
+ case @mode
134
+ when 'rust'
135
+ scan_fast_with_rust(path, max_entries)
136
+ when 'go'
137
+ scan_fast_with_go(path, max_entries)
138
+ else
139
+ scan_fast_with_ruby(path, max_entries)
140
+ end
141
+ end
142
+
143
+ # バージョン情報取得
144
+ def version
145
+ # モードが未設定の場合は自動設定
146
+ mode if @mode.nil?
147
+
148
+ case @mode
149
+ when 'rust'
150
+ ptr = RustLib.get_version
151
+ ptr.read_string
152
+ when 'go'
153
+ ptr = GoLib.GetVersion
154
+ result = ptr.read_string
155
+ GoLib.FreeCString(ptr)
156
+ result
157
+ else
158
+ "Ruby #{RUBY_VERSION}"
159
+ end
160
+ end
161
+
162
+ private
163
+
164
+ # Rustライブラリでスキャン
165
+ def scan_with_rust(path)
166
+ raise StandardError, "Directory does not exist: #{path}" unless Dir.exist?(path)
167
+
168
+ ptr = RustLib.scan_directory(path)
169
+ json_str = ptr.read_string
170
+ parse_scan_result(json_str)
171
+ rescue StandardError => e
172
+ raise StandardError, "Rust scan failed: #{e.message}"
173
+ end
174
+
175
+ # Rustライブラリで高速スキャン
176
+ def scan_fast_with_rust(path, max_entries)
177
+ raise StandardError, "Directory does not exist: #{path}" unless Dir.exist?(path)
178
+
179
+ ptr = RustLib.scan_directory_fast(path, max_entries)
180
+ json_str = ptr.read_string
181
+ parse_scan_result(json_str)
182
+ rescue StandardError => e
183
+ raise StandardError, "Rust fast scan failed: #{e.message}"
184
+ end
185
+
186
+ # Goライブラリでスキャン
187
+ def scan_with_go(path)
188
+ raise StandardError, "Directory does not exist: #{path}" unless Dir.exist?(path)
189
+
190
+ ptr = GoLib.ScanDirectory(path)
191
+ json_str = ptr.read_string
192
+ GoLib.FreeCString(ptr)
193
+ parse_scan_result(json_str)
194
+ rescue StandardError => e
195
+ raise StandardError, "Go scan failed: #{e.message}"
196
+ end
197
+
198
+ # Goライブラリで高速スキャン
199
+ def scan_fast_with_go(path, max_entries)
200
+ raise StandardError, "Directory does not exist: #{path}" unless Dir.exist?(path)
201
+
202
+ ptr = GoLib.ScanDirectoryFast(path, max_entries)
203
+ json_str = ptr.read_string
204
+ GoLib.FreeCString(ptr)
205
+ parse_scan_result(json_str)
206
+ rescue StandardError => e
207
+ raise StandardError, "Go fast scan failed: #{e.message}"
208
+ end
209
+
210
+ # Rubyでスキャン(フォールバック実装)
211
+ def scan_with_ruby(path)
212
+ raise StandardError, "Directory does not exist: #{path}" unless Dir.exist?(path)
213
+
214
+ entries = []
215
+ Dir.foreach(path) do |entry|
216
+ next if entry == '.' || entry == '..'
217
+
218
+ full_path = File.join(path, entry)
219
+ stat = File.lstat(full_path)
220
+
221
+ entries << {
222
+ name: entry,
223
+ type: file_type(stat),
224
+ size: stat.size,
225
+ mtime: stat.mtime.to_i,
226
+ mode: stat.mode
227
+ }
228
+ end
229
+ entries
230
+ rescue StandardError => e
231
+ raise StandardError, "Ruby scan failed: #{e.message}"
232
+ end
233
+
234
+ # Ruby高速スキャン(エントリ数制限付き)
235
+ def scan_fast_with_ruby(path, max_entries)
236
+ raise StandardError, "Directory does not exist: #{path}" unless Dir.exist?(path)
237
+
238
+ entries = []
239
+ count = 0
240
+
241
+ Dir.foreach(path) do |entry|
242
+ next if entry == '.' || entry == '..'
243
+ break if count >= max_entries
244
+
245
+ full_path = File.join(path, entry)
246
+ stat = File.lstat(full_path)
247
+
248
+ entries << {
249
+ name: entry,
250
+ type: file_type(stat),
251
+ size: stat.size,
252
+ mtime: stat.mtime.to_i,
253
+ mode: stat.mode
254
+ }
255
+ count += 1
256
+ end
257
+ entries
258
+ rescue StandardError => e
259
+ raise StandardError, "Ruby fast scan failed: #{e.message}"
260
+ end
261
+
262
+ # ファイルタイプを判定
263
+ def file_type(stat)
264
+ if stat.directory?
265
+ 'directory'
266
+ elsif stat.symlink?
267
+ 'symlink'
268
+ elsif stat.file?
269
+ 'file'
270
+ else
271
+ 'other'
272
+ end
273
+ end
274
+
275
+ # JSONレスポンスをパース
276
+ def parse_scan_result(json_str)
277
+ entries = JSON.parse(json_str, symbolize_names: true)
278
+
279
+ # エラーチェック(配列ではなくハッシュが返された場合)
280
+ if entries.is_a?(Hash) && entries[:error]
281
+ raise StandardError, entries[:error]
282
+ end
283
+
284
+ # 配列が返された場合は各エントリを変換
285
+ if entries.is_a?(Array)
286
+ return entries.map do |entry|
287
+ {
288
+ name: entry[:name],
289
+ type: entry[:is_dir] ? 'directory' : 'file',
290
+ size: entry[:size],
291
+ mtime: entry[:mtime],
292
+ mode: 0, # Rustライブラリはmodeを返さない
293
+ executable: entry[:executable],
294
+ hidden: entry[:hidden]
295
+ }
296
+ end
297
+ end
298
+
299
+ # それ以外の場合は空配列
300
+ []
301
+ rescue JSON::ParserError => e
302
+ raise StandardError, "Failed to parse scan result: #{e.message}"
303
+ end
304
+ end
305
+ end
306
+ end
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rufio
4
+ # Magnus拡張のラッパー
5
+ # FFIを使わずに直接Rubyオブジェクトとして扱える
6
+ module NativeScannerMagnusLoader
7
+ LIB_PATH = File.expand_path('native/rufio_native.bundle', __dir__)
8
+
9
+ @loaded = false
10
+ @available = false
11
+
12
+ class << self
13
+ def load!
14
+ return @available if @loaded
15
+
16
+ @loaded = true
17
+
18
+ if File.exist?(LIB_PATH)
19
+ begin
20
+ # .bundleファイルを直接ロード
21
+ # 拡張子を外してrequireする
22
+ lib_path_without_ext = LIB_PATH.sub(/\.bundle$/, '')
23
+ require lib_path_without_ext
24
+ @available = defined?(Rufio::NativeScannerMagnus)
25
+ rescue LoadError => e
26
+ warn "Failed to load magnus extension: #{e.message}" if ENV['RUFIO_DEBUG']
27
+ @available = false
28
+ end
29
+ else
30
+ @available = false
31
+ end
32
+
33
+ @available
34
+ end
35
+
36
+ def available?
37
+ load! unless @loaded
38
+ @available
39
+ end
40
+ end
41
+ end
42
+
43
+ # Magnus拡張が利用可能な場合のみロード
44
+ if NativeScannerMagnusLoader.load!
45
+ # NativeScannerにmagnusモードを追加
46
+ class NativeScanner
47
+ class << self
48
+ # magnusモードを追加
49
+ alias_method :original_mode=, :mode=
50
+
51
+ def mode=(value)
52
+ case value
53
+ when 'magnus'
54
+ if NativeScannerMagnusLoader.available?
55
+ @mode = 'magnus'
56
+ @current_library = nil # magnus は FFI を使わない
57
+ else
58
+ @mode = 'ruby'
59
+ @current_library = nil
60
+ end
61
+ else
62
+ send(:original_mode=, value)
63
+ end
64
+ end
65
+
66
+ # magnusスキャン
67
+ def scan_directory_with_magnus(path)
68
+ raise StandardError, "Directory does not exist: #{path}" unless Dir.exist?(path)
69
+
70
+ entries = NativeScannerMagnus.scan_directory(path)
71
+
72
+ # 結果の形式を統一(すでに正しい形式だが念のため)
73
+ entries.map do |entry|
74
+ {
75
+ name: entry[:name],
76
+ type: entry[:is_dir] ? 'directory' : 'file',
77
+ size: entry[:size],
78
+ mtime: entry[:mtime],
79
+ mode: 0,
80
+ executable: entry[:executable],
81
+ hidden: entry[:hidden]
82
+ }
83
+ end
84
+ rescue StandardError => e
85
+ raise StandardError, "Magnus scan failed: #{e.message}"
86
+ end
87
+
88
+ # magnusで高速スキャン
89
+ def scan_directory_fast_with_magnus(path, max_entries)
90
+ raise StandardError, "Directory does not exist: #{path}" unless Dir.exist?(path)
91
+
92
+ entries = NativeScannerMagnus.scan_directory_fast(path, max_entries)
93
+
94
+ entries.map do |entry|
95
+ {
96
+ name: entry[:name],
97
+ type: entry[:is_dir] ? 'directory' : 'file',
98
+ size: entry[:size],
99
+ mtime: entry[:mtime],
100
+ mode: 0,
101
+ executable: entry[:executable],
102
+ hidden: entry[:hidden]
103
+ }
104
+ end
105
+ rescue StandardError => e
106
+ raise StandardError, "Magnus fast scan failed: #{e.message}"
107
+ end
108
+
109
+ # scan_directoryメソッドを拡張
110
+ alias_method :original_scan_directory, :scan_directory
111
+
112
+ def scan_directory(path)
113
+ mode if @mode.nil?
114
+
115
+ case @mode
116
+ when 'magnus'
117
+ scan_directory_with_magnus(path)
118
+ else
119
+ original_scan_directory(path)
120
+ end
121
+ end
122
+
123
+ # scan_directory_fastメソッドを拡張
124
+ alias_method :original_scan_directory_fast, :scan_directory_fast
125
+
126
+ def scan_directory_fast(path, max_entries = 1000)
127
+ mode if @mode.nil?
128
+
129
+ case @mode
130
+ when 'magnus'
131
+ scan_directory_fast_with_magnus(path, max_entries)
132
+ else
133
+ original_scan_directory_fast(path, max_entries)
134
+ end
135
+ end
136
+
137
+ # versionメソッドを拡張
138
+ alias_method :original_version, :version
139
+
140
+ def version
141
+ mode if @mode.nil?
142
+
143
+ case @mode
144
+ when 'magnus'
145
+ NativeScannerMagnus.version
146
+ else
147
+ original_version
148
+ end
149
+ end
150
+
151
+ # available_librariesを更新
152
+ alias_method :original_available_libraries, :available_libraries
153
+
154
+ def available_libraries
155
+ original = original_available_libraries
156
+ original.merge(magnus: NativeScannerMagnusLoader.available?)
157
+ end
158
+
159
+ # autoモードの優先順位を更新(magnus > rust > go > ruby)
160
+ alias_method :original_auto_mode, :mode=
161
+
162
+ def mode=(value)
163
+ case value
164
+ when 'auto'
165
+ # 優先順位: Magnus > Rust > Go > Ruby
166
+ if NativeScannerMagnusLoader.available?
167
+ @mode = 'magnus'
168
+ @current_library = nil
169
+ elsif RustLib.available?
170
+ @mode = 'rust'
171
+ @current_library = RustLib
172
+ elsif GoLib.available?
173
+ @mode = 'go'
174
+ @current_library = GoLib
175
+ else
176
+ @mode = 'ruby'
177
+ @current_library = nil
178
+ end
179
+ when 'magnus'
180
+ if NativeScannerMagnusLoader.available?
181
+ @mode = 'magnus'
182
+ @current_library = nil
183
+ else
184
+ @mode = 'ruby'
185
+ @current_library = nil
186
+ end
187
+ else
188
+ send(:original_auto_mode, value)
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end