rufio 1.0.4 → 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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -0
  3. data/README.md +105 -2
  4. data/README_ja.md +103 -2
  5. data/Rakefile +14 -1
  6. data/bin/rufio +31 -2
  7. data/docs/fast_zip_design_ja.md +167 -0
  8. data/examples/config.rb +8 -0
  9. data/info/help.md +2 -0
  10. data/info/keybindings.md +3 -1
  11. data/info/welcome.md +1 -1
  12. data/lib/rufio/application.rb +12 -1
  13. data/lib/rufio/archive_controller.rb +211 -0
  14. data/lib/rufio/background_command_executor.rb +9 -0
  15. data/lib/rufio/background_status.rb +49 -0
  16. data/lib/rufio/bookmark_controller.rb +38 -121
  17. data/lib/rufio/bookmark_manager.rb +77 -331
  18. data/lib/rufio/command_completion.rb +6 -1
  19. data/lib/rufio/command_history.rb +5 -2
  20. data/lib/rufio/command_mode.rb +40 -134
  21. data/lib/rufio/command_mode_ui.rb +4 -42
  22. data/lib/rufio/config.rb +89 -137
  23. data/lib/rufio/config_loader.rb +21 -13
  24. data/lib/rufio/dialog_renderer.rb +64 -152
  25. data/lib/rufio/dialog_support.rb +69 -0
  26. data/lib/rufio/document_text_extractor.rb +167 -0
  27. data/lib/rufio/file_operation_controller.rb +49 -222
  28. data/lib/rufio/file_preview.rb +46 -2
  29. data/lib/rufio/global_search.rb +373 -0
  30. data/lib/rufio/help_menu.rb +27 -2
  31. data/lib/rufio/interpreter_resolver.rb +6 -11
  32. data/lib/rufio/job_manager.rb +2 -5
  33. data/lib/rufio/key_input.rb +77 -0
  34. data/lib/rufio/keybind_handler.rb +120 -112
  35. data/lib/rufio/local_script_scanner.rb +6 -18
  36. data/lib/rufio/native/aarch64-linux-gnu/librufio_zig.so +0 -0
  37. data/lib/rufio/native/aarch64-linux-musl/librufio_zig.so +0 -0
  38. data/lib/rufio/native/aarch64-macos/librufio_zig.dylib +0 -0
  39. data/lib/rufio/native/aarch64-windows/rufio_zig.dll +0 -0
  40. data/lib/rufio/native/x86_64-linux-gnu/librufio_zig.so +0 -0
  41. data/lib/rufio/native/x86_64-linux-musl/librufio_zig.so +0 -0
  42. data/lib/rufio/native/x86_64-macos/librufio_zig.dylib +0 -0
  43. data/lib/rufio/native/x86_64-windows/rufio_zig.dll +0 -0
  44. data/lib/rufio/native_content_search.rb +153 -0
  45. data/lib/rufio/native_scanner.rb +13 -68
  46. data/lib/rufio/native_scanner_zig.rb +106 -56
  47. data/lib/rufio/native_zip.rb +121 -0
  48. data/lib/rufio/navigation_controller.rb +18 -20
  49. data/lib/rufio/screen.rb +2 -12
  50. data/lib/rufio/script_executor.rb +6 -73
  51. data/lib/rufio/script_file.rb +44 -0
  52. data/lib/rufio/script_path_manager.rb +9 -43
  53. data/lib/rufio/script_runner.rb +13 -50
  54. data/lib/rufio/search_controller.rb +1 -1
  55. data/lib/rufio/shell_command_completion.rb +1 -5
  56. data/lib/rufio/shell_init.rb +90 -0
  57. data/lib/rufio/tab_mode_manager.rb +5 -1
  58. data/lib/rufio/task_status.rb +5 -1
  59. data/lib/rufio/terminal_ui.rb +116 -293
  60. data/lib/rufio/text_utils.rb +26 -3
  61. data/lib/rufio/ui_renderer.rb +68 -41
  62. data/lib/rufio/version.rb +1 -1
  63. data/lib/rufio/zip_archive.rb +314 -0
  64. data/lib/rufio/zip_extractor.rb +346 -0
  65. data/lib/rufio/zip_writer.rb +268 -0
  66. data/lib/rufio/zoxide_integration.rb +5 -43
  67. data/lib/rufio.rb +5 -0
  68. data/lib_zig/rufio_native/Makefile +54 -17
  69. data/lib_zig/rufio_native/bench.sh +36 -0
  70. data/lib_zig/rufio_native/build.zig +32 -35
  71. data/lib_zig/rufio_native/src/cli.zig +99 -0
  72. data/lib_zig/rufio_native/src/main.zig +187 -83
  73. data/lib_zig/rufio_native/src/os.zig +889 -0
  74. data/lib_zig/rufio_native/src/search.zig +919 -0
  75. data/lib_zig/rufio_native/src/search_test.zig +88 -0
  76. data/lib_zig/rufio_native/src/zip.zig +482 -0
  77. metadata +126 -3
  78. 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
@@ -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
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
@@ -83,11 +83,7 @@ module Rufio
83
83
  options[:chdir] = chdir if chdir
84
84
  spawn_env = env || {}
85
85
 
86
- if timeout
87
- execute_shell_with_timeout(dsl_command.shell_command, spawn_env, options, timeout)
88
- else
89
- execute_shell_without_timeout(dsl_command.shell_command, spawn_env, options)
90
- end
86
+ execute_with_options([dsl_command.shell_command], spawn_env, options, timeout)
91
87
  rescue StandardError => e
92
88
  build_error_result(e)
93
89
  end
@@ -133,8 +129,11 @@ module Rufio
133
129
  # プロセスを終了
134
130
  if pid
135
131
  begin
136
- Process.kill("TERM", pid)
137
- sleep 0.1
132
+ # Windows は TERM を送れない(Errno::EINVAL)ため KILL だけで終了させる
133
+ unless Gem.win_platform?
134
+ Process.kill("TERM", pid)
135
+ sleep 0.1
136
+ end
138
137
  Process.kill("KILL", pid)
139
138
  rescue Errno::ESRCH, Errno::EPERM
140
139
  # プロセスが既に終了している、または権限がない
@@ -176,72 +175,6 @@ module Rufio
176
175
  }
177
176
  end
178
177
 
179
- # シェルコマンドをタイムアウト付きで実行
180
- def execute_shell_with_timeout(shell_command, env, options, timeout_sec)
181
- stdout = ""
182
- stderr = ""
183
- status = nil
184
- timed_out = false
185
- pid = nil
186
-
187
- begin
188
- Timeout.timeout(timeout_sec) do
189
- args = env.empty? ? [shell_command] : [env, shell_command]
190
- stdin, stdout_io, stderr_io, wait_thread = Open3.popen3(*args, **options)
191
- pid = wait_thread.pid
192
- stdin.close
193
- stdout = stdout_io.read
194
- stderr = stderr_io.read
195
- stdout_io.close
196
- stderr_io.close
197
- status = wait_thread.value
198
- end
199
- rescue Timeout::Error
200
- timed_out = true
201
- if pid
202
- begin
203
- Process.kill("TERM", pid)
204
- sleep 0.1
205
- Process.kill("KILL", pid)
206
- rescue Errno::ESRCH, Errno::EPERM
207
- # プロセスが既に終了している、または権限がない
208
- end
209
- end
210
- end
211
-
212
- if timed_out
213
- {
214
- success: false,
215
- exit_code: nil,
216
- stdout: stdout,
217
- stderr: stderr,
218
- timeout: true
219
- }
220
- else
221
- {
222
- success: status&.success? || false,
223
- exit_code: status&.exitstatus || 1,
224
- stdout: stdout,
225
- stderr: stderr,
226
- timeout: false
227
- }
228
- end
229
- end
230
-
231
- # シェルコマンドをタイムアウトなしで実行
232
- def execute_shell_without_timeout(shell_command, env, options)
233
- args = env.empty? ? [shell_command] : [env, shell_command]
234
- stdout, stderr, status = Open3.capture3(*args, **options)
235
-
236
- {
237
- success: status.success?,
238
- exit_code: status.exitstatus,
239
- stdout: stdout,
240
- stderr: stderr,
241
- timeout: false
242
- }
243
- end
244
-
245
178
  # エラー結果を構築
246
179
  def build_error_result(error)
247
180
  {