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
@@ -0,0 +1,153 @@
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 実装のコンテンツ検索(G mode 用)。
9
+ # Ruby 実装(GlobalSearch#scan_content)と同じ結果を、並列走査と SIMD 検索で返す。
10
+ class NativeContentSearch
11
+ HEADER_SIZE = 25
12
+ HEADER_FORMAT = 'CL<Q<L<L<L<'
13
+ STATE_SCANNING = 1
14
+ KINDS = { 1 => :match, 2 => :document, 3 => :error }.freeze
15
+ MAX_QUERY_BYTES = 1024
16
+ POLL_INTERVAL = 0.001
17
+
18
+ # 検索をまたいでファイル内容を保持するキャッシュ(mtime/inode/サイズで毎回検証する)
19
+ class Cache
20
+ DEFAULT_MAX_BYTES = 256 * 1024 * 1024
21
+
22
+ attr_reader :handle
23
+
24
+ def self.finalizer(handle)
25
+ proc { NativeScannerZigFFI.core_cache_destroy(handle) }
26
+ end
27
+
28
+ def initialize(max_bytes: DEFAULT_MAX_BYTES)
29
+ @handle = NativeScannerZigFFI.core_cache_create(max_bytes)
30
+ raise StandardError, 'Failed to create search cache' if @handle.zero?
31
+
32
+ ObjectSpace.define_finalizer(self, self.class.finalizer(@handle))
33
+ end
34
+
35
+ def cached_bytes
36
+ @handle.zero? ? 0 : NativeScannerZigFFI.core_cache_bytes(@handle)
37
+ end
38
+
39
+ # このキャッシュを使う検索をすべて close してから呼ぶこと
40
+ def close
41
+ return if @handle.zero?
42
+
43
+ ObjectSpace.undefine_finalizer(self)
44
+ NativeScannerZigFFI.core_cache_destroy(@handle)
45
+ @handle = 0
46
+ end
47
+ end
48
+
49
+ def self.available?
50
+ NativeScannerZigFFI.search_available?
51
+ end
52
+
53
+ # Zig 側は ASCII のみ大文字小文字を畳み込むため、大文字小文字を持つ非 ASCII 文字は Ruby 実装に任せる
54
+ def self.supports_query?(query)
55
+ return false if query.empty? || query.bytesize > MAX_QUERY_BYTES
56
+
57
+ query.each_char.all? { |char| char.ascii_only? || (char.upcase == char && char.downcase == char) }
58
+ end
59
+
60
+ # マッチ周辺のバイト列から Ruby 実装と同じスニペット(30 文字前から 200 文字)を作る
61
+ def self.snippet(window, offset)
62
+ line = window.dup.force_encoding(Encoding::UTF_8).scrub
63
+ match_index = window.byteslice(0, offset).force_encoding(Encoding::UTF_8).scrub.length
64
+ line[[match_index - 30, 0].max, 200].to_s.strip
65
+ end
66
+
67
+ def initialize(roots:, query:, excluded_extensions: [], document_extensions: [], cache: nil, threads: 0)
68
+ raise StandardError, 'Zig content search is not available' unless self.class.available?
69
+
70
+ @cache = cache
71
+ @path_encoding = Encoding.find('filesystem')
72
+ roots_buf = pack(roots)
73
+ query_buf = query.b
74
+ excluded_buf = pack(excluded_extensions)
75
+ documents_buf = pack(document_extensions)
76
+ @handle = NativeScannerZigFFI.core_search_create(
77
+ roots_buf, roots_buf.bytesize, query_buf, query_buf.bytesize,
78
+ excluded_buf, excluded_buf.bytesize, documents_buf, documents_buf.bytesize,
79
+ threads, cache ? cache.handle : 0
80
+ )
81
+ raise StandardError, 'Failed to start native search' if @handle.zero?
82
+
83
+ @length = Fiddle::Pointer.malloc(8, Fiddle::RUBY_FREE)
84
+ @skipped = 0
85
+ end
86
+
87
+ # [:match, root_index, path, line_number, snippet] / [:document, root_index, path] /
88
+ # [:error, root_index, message] / [:skipped, count] を検索終了まで順に返す
89
+ def each_event(&block)
90
+ return enum_for(:each_event) unless block_given?
91
+
92
+ each_batch { |batch| batch.each(&block) }
93
+ end
94
+
95
+ # 1 回の取り出しごとにイベント配列を返す(UI へまとめて渡すため)
96
+ def each_batch
97
+ return enum_for(:each_batch) unless block_given?
98
+
99
+ loop do
100
+ state = NativeScannerZigFFI.core_search_state(@handle)
101
+ data = drain
102
+ batch = []
103
+ each_record(data) { |event| batch << event }
104
+ skipped = NativeScannerZigFFI.core_search_skipped(@handle)
105
+ if skipped > @skipped
106
+ batch << [:skipped, skipped - @skipped]
107
+ @skipped = skipped
108
+ end
109
+ yield batch unless batch.empty?
110
+ break if data.empty? && state != STATE_SCANNING
111
+
112
+ sleep POLL_INTERVAL if data.empty?
113
+ end
114
+ end
115
+
116
+ def close
117
+ return if @handle.nil? || @handle.zero?
118
+
119
+ NativeScannerZigFFI.core_search_destroy(@handle)
120
+ @handle = 0
121
+ end
122
+
123
+ private
124
+
125
+ def pack(values)
126
+ values.map { |value| value.to_s.b }.join("\0")
127
+ end
128
+
129
+ def drain
130
+ pointer = NativeScannerZigFFI.core_search_drain(@handle, @length)
131
+ length = @length[0, 8].unpack1('Q')
132
+ length.zero? ? ''.b : pointer.to_str(length)
133
+ end
134
+
135
+ def each_record(data)
136
+ position = 0
137
+ while position + HEADER_SIZE <= data.bytesize
138
+ kind, root, line, offset, path_length, extra_length = data.byteslice(position, HEADER_SIZE).unpack(HEADER_FORMAT)
139
+ position += HEADER_SIZE
140
+ path = data.byteslice(position, path_length).force_encoding(@path_encoding)
141
+ position += path_length
142
+ extra = data.byteslice(position, extra_length)
143
+ position += extra_length
144
+
145
+ case KINDS[kind]
146
+ when :match then yield [:match, root, path, line, self.class.snippet(extra, offset)]
147
+ when :document then yield [:document, root, path]
148
+ when :error then yield [:error, root, "#{path}: #{extra.force_encoding(Encoding::UTF_8).scrub}"]
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
@@ -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
@@ -262,5 +249,16 @@ module Rufio
262
249
  navigate_enter(entry, in_help_mode, in_log_viewer_mode)
263
250
  end
264
251
  end
252
+
253
+ private
254
+
255
+ # 移動に成功したときだけ、移動後の選択・フィルタ状態を更新する。
256
+ def perform_navigation
257
+ return false unless @directory_listing
258
+
259
+ result = yield
260
+ clear_filter_mode if result
261
+ result
262
+ end
265
263
  end
266
264
  end