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
@@ -17,69 +17,17 @@ module Rufio
17
17
 
18
18
  # 非同期スキャン開始
19
19
  def scan_async(path)
20
- raise StandardError, "Directory does not exist: #{path}" unless Dir.exist?(path)
21
- raise StandardError, "Scanner is already running" unless @state == :idle
22
-
23
- @mutex.synchronize do
24
- @state = :scanning
25
- @results = []
26
- @error = nil
27
- @current_progress = 0
28
- @total_progress = 0
29
- end
30
-
31
- @thread = Thread.new do
32
- begin
33
- # ディレクトリをスキャン
34
- entries = []
35
- Dir.foreach(path) do |entry|
36
- next if entry == '.' || entry == '..'
37
-
38
- # キャンセルチェック
39
- break if @state == :cancelled
40
-
41
- full_path = File.join(path, entry)
42
- stat = File.lstat(full_path)
43
-
44
- entries << {
45
- name: entry,
46
- type: file_type(stat),
47
- size: stat.size,
48
- mtime: stat.mtime.to_i,
49
- mode: stat.mode,
50
- executable: stat.executable?,
51
- hidden: entry.start_with?('.')
52
- }
53
-
54
- # 進捗を更新
55
- @mutex.synchronize do
56
- @current_progress += 1
57
- @total_progress = entries.length + 1
58
- end
59
- end
60
-
61
- # 結果を保存
62
- @mutex.synchronize do
63
- if @state == :cancelled
64
- @state = :cancelled
65
- else
66
- @results = entries
67
- @state = :done
68
- end
69
- end
70
- rescue StandardError => e
71
- @mutex.synchronize do
72
- @error = e
73
- @state = :failed
74
- end
75
- end
76
- end
77
-
78
- self
20
+ start_scan_thread(path, nil)
79
21
  end
80
22
 
81
23
  # 高速スキャン(エントリ数制限付き)
82
24
  def scan_fast_async(path, max_entries)
25
+ start_scan_thread(path, max_entries)
26
+ end
27
+
28
+ private
29
+
30
+ def start_scan_thread(path, max_entries)
83
31
  raise StandardError, "Directory does not exist: #{path}" unless Dir.exist?(path)
84
32
  raise StandardError, "Scanner is already running" unless @state == :idle
85
33
 
@@ -88,7 +36,7 @@ module Rufio
88
36
  @results = []
89
37
  @error = nil
90
38
  @current_progress = 0
91
- @total_progress = max_entries
39
+ @total_progress = max_entries || 0
92
40
  end
93
41
 
94
42
  @thread = Thread.new do
@@ -98,9 +46,7 @@ module Rufio
98
46
 
99
47
  Dir.foreach(path) do |entry|
100
48
  next if entry == '.' || entry == '..'
101
- break if count >= max_entries
102
-
103
- # キャンセルチェック
49
+ break if max_entries && count >= max_entries
104
50
  break if @state == :cancelled
105
51
 
106
52
  full_path = File.join(path, entry)
@@ -117,17 +63,14 @@ module Rufio
117
63
  }
118
64
  count += 1
119
65
 
120
- # 進捗を更新
121
66
  @mutex.synchronize do
122
67
  @current_progress = count
68
+ @total_progress = entries.length + 1 unless max_entries
123
69
  end
124
70
  end
125
71
 
126
- # 結果を保存
127
72
  @mutex.synchronize do
128
- if @state == :cancelled
129
- @state = :cancelled
130
- else
73
+ if @state != :cancelled
131
74
  @results = entries
132
75
  @state = :done
133
76
  end
@@ -143,6 +86,8 @@ module Rufio
143
86
  self
144
87
  end
145
88
 
89
+ public
90
+
146
91
  # ポーリングして完了待ち
147
92
  def wait(timeout: nil)
148
93
  start_time = Time.now
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'fiddle'
4
4
  require 'fiddle/import'
5
+ require 'rbconfig'
5
6
 
6
7
  module Rufio
7
8
  # Zig拡張の非同期FFI層
@@ -9,21 +10,58 @@ module Rufio
9
10
  module NativeScannerZigFFI
10
11
  extend Fiddle::Importer
11
12
 
12
- LIB_PATH = File.expand_path('native/rufio_zig.bundle', __dir__)
13
+ NATIVE_DIR = File.expand_path('native', __dir__)
14
+
15
+ # Gem に同梱するプラットフォーム(lib/rufio/native/<platform>/<file>)
16
+ PLATFORMS = {
17
+ 'aarch64-macos' => 'librufio_zig.dylib',
18
+ 'x86_64-macos' => 'librufio_zig.dylib',
19
+ 'x86_64-linux-gnu' => 'librufio_zig.so',
20
+ 'aarch64-linux-gnu' => 'librufio_zig.so',
21
+ 'x86_64-linux-musl' => 'librufio_zig.so',
22
+ 'aarch64-linux-musl' => 'librufio_zig.so',
23
+ 'x86_64-windows' => 'rufio_zig.dll',
24
+ 'aarch64-windows' => 'rufio_zig.dll'
25
+ }.freeze
13
26
 
14
27
  @loaded = false
15
28
  @available = false
29
+ @loaded_path = nil
16
30
 
17
31
  class << self
32
+ attr_reader :loaded_path
33
+
34
+ # RbConfig の host_os / host_cpu から同梱バイナリのディレクトリ名を決める(非対応なら nil)
35
+ def platform_dir(host_os = RbConfig::CONFIG['host_os'], host_cpu = RbConfig::CONFIG['host_cpu'])
36
+ cpu = case host_cpu.to_s
37
+ when /\A(arm64|aarch64)/i then 'aarch64'
38
+ when /\A(x86_64|x64|amd64)\z/i then 'x86_64'
39
+ end
40
+ return nil unless cpu
41
+
42
+ case host_os.to_s
43
+ when /darwin/ then "#{cpu}-macos"
44
+ when /linux/ then "#{cpu}-linux-#{host_os.to_s.include?('musl') ? 'musl' : 'gnu'}"
45
+ when /mingw|mswin/ then "#{cpu}-windows"
46
+ end
47
+ end
48
+
49
+ def library_path(host_os = RbConfig::CONFIG['host_os'], host_cpu = RbConfig::CONFIG['host_cpu'])
50
+ dir = platform_dir(host_os, host_cpu)
51
+ dir && File.join(NATIVE_DIR, dir, PLATFORMS.fetch(dir))
52
+ end
53
+
18
54
  def load!
19
55
  return @available if @loaded
20
56
 
21
57
  @loaded = true
58
+ path = library_path
22
59
 
23
- if File.exist?(LIB_PATH)
60
+ if path && File.exist?(path)
24
61
  begin
25
62
  # 動的ライブラリをロード
26
- dlload LIB_PATH
63
+ dlload path
64
+ @loaded_path = path
27
65
 
28
66
  # ABI Boundary: Ruby ABI非依存のC関数(非同期版)
29
67
  extern 'uint64_t core_async_create()'
@@ -39,6 +77,8 @@ module Rufio
39
77
  extern 'char* core_async_version()'
40
78
 
41
79
  @available = true
80
+ load_search_functions!
81
+ load_zip_functions!
42
82
  rescue StandardError => e
43
83
  warn "Failed to load zig extension: #{e.message}" if ENV['RUFIO_DEBUG']
44
84
  @available = false
@@ -55,10 +95,55 @@ module Rufio
55
95
  @available
56
96
  end
57
97
 
98
+ # コンテンツ検索 API(古いライブラリには存在しない)
99
+ def search_available?
100
+ load! unless @loaded
101
+ @search_available == true
102
+ end
103
+
104
+ # ZIP データ処理 API(古いライブラリには存在しない)
105
+ def zip_available?
106
+ load! unless @loaded
107
+ @zip_available == true
108
+ end
109
+
58
110
  # バージョン取得
59
111
  def version
60
112
  core_async_version.to_s
61
113
  end
114
+
115
+ private
116
+
117
+ def load_search_functions!
118
+ extern 'uint64_t core_search_create(const char*, size_t, const char*, size_t, const char*, size_t, const char*, size_t, uint32_t, uint64_t)'
119
+ extern 'uint8_t core_search_state(uint64_t)'
120
+ extern 'void* core_search_drain(uint64_t, void*)'
121
+ extern 'uint64_t core_search_skipped(uint64_t)'
122
+ extern 'void core_search_cancel(uint64_t)'
123
+ extern 'void core_search_destroy(uint64_t)'
124
+ extern 'uint64_t core_cache_create(uint64_t)'
125
+ extern 'uint64_t core_cache_bytes(uint64_t)'
126
+ extern 'void core_cache_destroy(uint64_t)'
127
+ @search_available = true
128
+ rescue Fiddle::DLError => e
129
+ warn "Zig content search unavailable: #{e.message}" if ENV['RUFIO_DEBUG']
130
+ @search_available = false
131
+ end
132
+
133
+ def load_zip_functions!
134
+ extern 'uint64_t core_zip_extract_create(const char*, size_t, const char*, size_t, uint32_t)'
135
+ extern 'uint64_t core_zip_deflate_create(const char*, size_t, int32_t, uint32_t)'
136
+ extern 'uint8_t core_zip_state(uint64_t)'
137
+ extern 'void core_zip_progress(uint64_t, void*, void*)'
138
+ extern 'uint8_t core_zip_status(uint64_t, size_t)'
139
+ extern 'uint8_t core_zip_deflate_result(uint64_t, size_t, void*, void*, void*)'
140
+ extern 'void core_zip_cancel(uint64_t)'
141
+ extern 'void core_zip_destroy(uint64_t)'
142
+ @zip_available = true
143
+ rescue Fiddle::DLError => e
144
+ warn "Zig ZIP engine unavailable: #{e.message}" if ENV['RUFIO_DEBUG']
145
+ @zip_available = false
146
+ end
62
147
  end
63
148
  end
64
149
 
@@ -200,12 +285,12 @@ module Rufio
200
285
  if NativeScannerZigFFI.load!
201
286
  class NativeScanner
202
287
  class << self
203
- # zigモードを追加
288
+ # zigモードを追加(autoモードの優先順位: Zig > Ruby)
204
289
  alias_method :original_mode=, :mode= unless method_defined?(:original_mode=)
205
290
 
206
291
  def mode=(value)
207
292
  case value
208
- when 'zig'
293
+ when 'auto', 'zig'
209
294
  if NativeScannerZigFFI.available?
210
295
  @mode = 'zig'
211
296
  @current_library = nil
@@ -214,7 +299,7 @@ module Rufio
214
299
  @current_library = nil
215
300
  end
216
301
  else
217
- original_mode=(value)
302
+ send(:original_mode=, value)
218
303
  end
219
304
  end
220
305
 
@@ -222,24 +307,10 @@ module Rufio
222
307
  def scan_directory_with_zig(path)
223
308
  raise StandardError, "Directory does not exist: #{path}" unless Dir.exist?(path)
224
309
 
225
- # 非同期スキャナーを作成してスキャン
226
310
  scanner = NativeScannerZigCore.new
227
311
  begin
228
312
  scanner.scan_async(path)
229
- entries = scanner.wait(timeout: 60)
230
-
231
- # 結果の形式を統一(type フィールドを追加)
232
- entries.map do |entry|
233
- {
234
- name: entry[:name],
235
- type: entry[:is_dir] ? 'directory' : 'file',
236
- size: entry[:size],
237
- mtime: entry[:mtime],
238
- mode: 0,
239
- executable: entry[:executable],
240
- hidden: entry[:hidden]
241
- }
242
- end
313
+ normalize_zig_entries(scanner.wait(timeout: 60))
243
314
  ensure
244
315
  scanner.close
245
316
  end
@@ -254,19 +325,7 @@ module Rufio
254
325
  scanner = NativeScannerZigCore.new
255
326
  begin
256
327
  scanner.scan_fast_async(path, max_entries)
257
- entries = scanner.wait(timeout: 60)
258
-
259
- entries.map do |entry|
260
- {
261
- name: entry[:name],
262
- type: entry[:is_dir] ? 'directory' : 'file',
263
- size: entry[:size],
264
- mtime: entry[:mtime],
265
- mode: 0,
266
- executable: entry[:executable],
267
- hidden: entry[:hidden]
268
- }
269
- end
328
+ normalize_zig_entries(scanner.wait(timeout: 60))
270
329
  ensure
271
330
  scanner.close
272
331
  end
@@ -324,28 +383,19 @@ module Rufio
324
383
  original.merge(zig: NativeScannerZigFFI.available?)
325
384
  end
326
385
 
327
- # autoモードの優先順位を更新(zig > ruby)
328
- def mode=(value)
329
- case value
330
- when 'auto'
331
- # 優先順位: Zig > Ruby
332
- if NativeScannerZigFFI.available?
333
- @mode = 'zig'
334
- @current_library = nil
335
- else
336
- @mode = 'ruby'
337
- @current_library = nil
338
- end
339
- when 'zig'
340
- if NativeScannerZigFFI.available?
341
- @mode = 'zig'
342
- @current_library = nil
343
- else
344
- @mode = 'ruby'
345
- @current_library = nil
346
- end
347
- else
348
- send(:original_mode=, value)
386
+ private
387
+
388
+ def normalize_zig_entries(entries)
389
+ entries.map do |entry|
390
+ {
391
+ name: entry[:name],
392
+ type: entry[:is_dir] ? 'directory' : 'file',
393
+ size: entry[:size],
394
+ mtime: entry[:mtime],
395
+ mode: 0,
396
+ executable: entry[:executable],
397
+ hidden: entry[:hidden]
398
+ }
349
399
  end
350
400
  end
351
401
  end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fiddle'
4
+ require_relative 'native_scanner'
5
+ require_relative 'native_scanner_zig'
6
+
7
+ module Rufio
8
+ # Zig 実装の ZIP データ処理(並列・GVL 外)。
9
+ #
10
+ # 安全性の検証・パスの決定・ディレクトリ作成・rename・権限と日時の設定は Ruby 側
11
+ # (ZipExtractor / ZipWriter)が担い、ここではエントリ単位のデータ処理だけを行う。
12
+ # - 展開: ZIP 内の指定範囲を読み、伸長し、宣言サイズと CRC-32 を検証して一時ファイルに書く
13
+ # - 圧縮: ファイルを raw DEFLATE で一時ファイルに書き、CRC-32 とサイズを返す
14
+ module NativeZip
15
+ STATE_RUNNING = 1
16
+ STATE_DONE = 2
17
+ STATE_CANCELLED = 3
18
+ POLL_INTERVAL = 0.005
19
+
20
+ # タスクごとの結果コード(Zig 側 zip.zig の Status と一致させる)
21
+ STATUS_OK = 0
22
+ STATUS_CRC_MISMATCH = 1
23
+ STATUS_SIZE_MISMATCH = 2
24
+ STATUS_CORRUPT = 3
25
+ STATUS_IO_ERROR = 4
26
+ STATUS_CANCELLED = 5
27
+ STATUS_TOO_LARGE = 6
28
+ STATUS_PENDING = 255
29
+
30
+ class << self
31
+ def available?
32
+ return false if ENV['RUFIO_NATIVE_ZIP'] == '0'
33
+
34
+ NativeScannerZigFFI.zip_available?
35
+ end
36
+ end
37
+
38
+ # 実行中のジョブ(展開・圧縮共通)
39
+ class Job
40
+ def initialize(handle, count)
41
+ raise StandardError, 'Failed to start native ZIP job' if handle.zero?
42
+
43
+ @handle = handle
44
+ @count = count
45
+ @done = Fiddle::Pointer.malloc(8, Fiddle::RUBY_FREE)
46
+ @total = Fiddle::Pointer.malloc(8, Fiddle::RUBY_FREE)
47
+ end
48
+
49
+ # 完了まで待つ。progress には (処理済みバイト数, 合計バイト数) を渡す。
50
+ # cancelled が true を返したらキャンセルを要求し、終了を待ってから戻る
51
+ def wait(progress: nil, cancelled: nil)
52
+ loop do
53
+ state = NativeScannerZigFFI.core_zip_state(@handle)
54
+ report(progress)
55
+ return state unless state == STATE_RUNNING
56
+
57
+ NativeScannerZigFFI.core_zip_cancel(@handle) if cancelled&.call
58
+ sleep POLL_INTERVAL
59
+ end
60
+ end
61
+
62
+ def status(index)
63
+ NativeScannerZigFFI.core_zip_status(@handle, index)
64
+ end
65
+
66
+ def close
67
+ return if @handle.zero?
68
+
69
+ NativeScannerZigFFI.core_zip_destroy(@handle)
70
+ @handle = 0
71
+ end
72
+
73
+ protected
74
+
75
+ attr_reader :handle
76
+
77
+ private
78
+
79
+ def report(progress)
80
+ return unless progress
81
+
82
+ NativeScannerZigFFI.core_zip_progress(@handle, @done, @total)
83
+ progress.call(@done[0, 8].unpack1('Q<'), @total[0, 8].unpack1('Q<'))
84
+ end
85
+ end
86
+
87
+ # @param zip_path [String]
88
+ # @param tasks [Array<Hash>] data_offset:, compressed_size:, size:, crc32:, method:, output: (一時ファイルのパス)
89
+ def self.extract(zip_path, tasks, threads: 0)
90
+ buffer = tasks.map do |task|
91
+ output = task[:output].b
92
+ [task[:data_offset], task[:compressed_size], task[:size], task[:crc32], task[:method], output.bytesize]
93
+ .pack('Q<Q<Q<L<CL<') + output
94
+ end.join
95
+ path = zip_path.b
96
+ Job.new(NativeScannerZigFFI.core_zip_extract_create(path, path.bytesize, buffer, buffer.bytesize, threads), tasks.length)
97
+ end
98
+
99
+ # 圧縮ジョブ
100
+ class DeflateJob < Job
101
+ # @return [Array(Integer, Integer, Integer, Integer)] status, crc32, 圧縮後サイズ, 元サイズ
102
+ def result(index)
103
+ crc = Fiddle::Pointer.malloc(4, Fiddle::RUBY_FREE)
104
+ compressed = Fiddle::Pointer.malloc(8, Fiddle::RUBY_FREE)
105
+ size = Fiddle::Pointer.malloc(8, Fiddle::RUBY_FREE)
106
+ status = NativeScannerZigFFI.core_zip_deflate_result(handle, index, crc, compressed, size)
107
+ [status, crc[0, 4].unpack1('L<'), compressed[0, 8].unpack1('Q<'), size[0, 8].unpack1('Q<')]
108
+ end
109
+ end
110
+
111
+ # @param tasks [Array<Hash>] source:, output: (一時ファイルのパス)
112
+ def self.deflate(tasks, level:, threads: 0)
113
+ buffer = tasks.map do |task|
114
+ source = task[:source].b
115
+ output = task[:output].b
116
+ [source.bytesize].pack('L<') + source + [output.bytesize].pack('L<') + output
117
+ end.join
118
+ DeflateJob.new(NativeScannerZigFFI.core_zip_deflate_create(buffer, buffer.bytesize, level, threads), tasks.length)
119
+ end
120
+ end
121
+ end
@@ -67,31 +67,18 @@ module Rufio
67
67
  # @param in_help_mode [Boolean] ヘルプモード中かどうか
68
68
  # @param in_log_viewer_mode [Boolean] ログビューワモード中かどうか
69
69
  def navigate_enter(entry, in_help_mode, in_log_viewer_mode)
70
- return false unless entry
71
-
72
- if entry[:type] == 'directory'
73
- return false if in_help_mode || in_log_viewer_mode
70
+ return false unless entry && entry[:type] == 'directory'
71
+ return false if in_help_mode || in_log_viewer_mode
74
72
 
75
- result = @directory_listing.navigate_to(entry[:name])
76
- if result
77
- @current_index = 0
78
- clear_filter_mode
79
- end
80
- result
81
- else
82
- false
83
- end
73
+ perform_navigation { @directory_listing.navigate_to(entry[:name]) }
84
74
  end
85
75
 
86
76
  def navigate_parent
87
- return false unless @directory_listing
77
+ perform_navigation { @directory_listing.navigate_to_parent }
78
+ end
88
79
 
89
- result = @directory_listing.navigate_to_parent
90
- if result
91
- @current_index = 0
92
- clear_filter_mode
93
- end
94
- result
80
+ def navigate_to_directory(path)
81
+ perform_navigation { @directory_listing.navigate_to_path(path) }
95
82
  end
96
83
 
97
84
  def refresh
@@ -249,18 +236,25 @@ module Rufio
249
236
  end
250
237
  end
251
238
 
252
- # Enterキーの処理:ファイルならプレビューフォーカス、ディレクトリならナビゲート
239
+ # Enterキーの処理:ディレクトリへ移動する
253
240
  # @param entry [Hash] 現在選択中のエントリ
254
241
  # @param in_help_mode [Boolean]
255
242
  # @param in_log_viewer_mode [Boolean]
256
243
  def handle_enter_key(entry, in_help_mode, in_log_viewer_mode)
257
244
  return false unless entry
258
245
 
259
- if entry[:type] == 'file'
260
- focus_preview_pane(entry)
261
- else
262
- navigate_enter(entry, in_help_mode, in_log_viewer_mode)
263
- end
246
+ navigate_enter(entry, in_help_mode, in_log_viewer_mode)
247
+ end
248
+
249
+ private
250
+
251
+ # 移動に成功したときだけ、移動後の選択・フィルタ状態を更新する。
252
+ def perform_navigation
253
+ return false unless @directory_listing
254
+
255
+ result = yield
256
+ clear_filter_mode if result
257
+ result
264
258
  end
265
259
  end
266
260
  end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ module Rufio
6
+ # :cd に貼り付けられたパスを移動先に解決する。
7
+ # 引用符・末尾の改行・バックスラッシュエスケープされた空白・file:// URL を受け付け、
8
+ # ファイルを指す場合はその親ディレクトリとファイル自体を返す。
9
+ module PathJump
10
+ QUOTE_PAIRS = [%w[' '], %w[" "]].freeze
11
+
12
+ class << self
13
+ # @param input [String] 貼り付けられたパス(空なら ホーム)
14
+ # @param base_dir [String] 相対パスの基準ディレクトリ
15
+ # @return [Hash] { directory:, file: } または { directory: nil, error: }
16
+ def resolve(input, base_dir)
17
+ path = normalize(input.to_s)
18
+ path = '~' if path.empty?
19
+ expanded = File.expand_path(path, base_dir)
20
+
21
+ if File.directory?(expanded)
22
+ { directory: expanded, file: nil }
23
+ elsif File.file?(expanded)
24
+ { directory: File.dirname(expanded), file: expanded }
25
+ else
26
+ { directory: nil, error: "Path not found: #{path}" }
27
+ end
28
+ rescue ArgumentError => e
29
+ { directory: nil, error: "Invalid path: #{e.message}" }
30
+ end
31
+
32
+ private
33
+
34
+ def normalize(text)
35
+ path = text.strip
36
+ QUOTE_PAIRS.each do |open, close|
37
+ if path.length >= 2 && path.start_with?(open) && path.end_with?(close)
38
+ path = path[1..-2]
39
+ break
40
+ end
41
+ end
42
+ path = from_file_url(path) if path.start_with?('file://')
43
+ path = path.gsub(/\\(.)/, '\1') unless Gem.win_platform?
44
+ path
45
+ end
46
+
47
+ def from_file_url(url)
48
+ uri = URI.parse(url)
49
+ URI.decode_www_form_component(uri.path.to_s.gsub('+', '%2B'))
50
+ rescue URI::InvalidURIError
51
+ url.delete_prefix('file://')
52
+ end
53
+ end
54
+ end
55
+ end
data/lib/rufio/screen.rb CHANGED
@@ -229,12 +229,7 @@ module Rufio
229
229
  return if out_of_bounds?(x, y)
230
230
 
231
231
  char_width = width || TextUtils.display_width(char)
232
- @overlay_cells[y][x] = {
233
- char: char,
234
- fg: fg,
235
- bg: bg,
236
- width: char_width
237
- }
232
+ @overlay_cells[y][x] = Cell.new(char, fg, bg, char_width)
238
233
 
239
234
  @dirty_rows.add(y)
240
235
 
@@ -243,12 +238,7 @@ module Rufio
243
238
  (char_width - 1).times do |offset|
244
239
  next_x = x + 1 + offset
245
240
  break if next_x >= @width
246
- @overlay_cells[y][next_x] = {
247
- char: '',
248
- fg: nil,
249
- bg: nil,
250
- width: 0
251
- }
241
+ @overlay_cells[y][next_x] = Cell.new('', nil, nil, 0)
252
242
  end
253
243
  end
254
244
  end