rufio 0.32.0 → 0.34.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.
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'async'
5
+ ASYNC_GEM_AVAILABLE = true
6
+ rescue LoadError
7
+ ASYNC_GEM_AVAILABLE = false
8
+ end
9
+
10
+ module Rufio
11
+ # Fiber(Asyncライブラリ)統合用ラッパークラス
12
+ #
13
+ # Asyncライブラリと統合し、ノンブロッキングで非同期スキャンを実行します。
14
+ #
15
+ # 使用例:
16
+ # Async do
17
+ # scanner = NativeScannerRubyCore.new
18
+ # wrapper = AsyncScannerFiberWrapper.new(scanner)
19
+ # entries = wrapper.scan_async('/path')
20
+ # puts "Found #{entries.length} entries"
21
+ # end
22
+ #
23
+ class AsyncScannerFiberWrapper
24
+ def initialize(scanner)
25
+ @scanner = scanner
26
+ end
27
+
28
+ # 非同期スキャンを開始し、Fiberで完了を待つ
29
+ #
30
+ # @param path [String] スキャンするディレクトリのパス
31
+ # @param timeout [Integer, nil] タイムアウト秒数(オプション)
32
+ # @return [Array<Hash>] スキャン結果
33
+ def scan_async(path, timeout: nil)
34
+ # スキャンを開始
35
+ @scanner.scan_async(path)
36
+
37
+ # Fiberでポーリング
38
+ poll_until_complete(timeout: timeout)
39
+ end
40
+
41
+ # 高速スキャン(エントリ数制限付き)
42
+ #
43
+ # @param path [String] スキャンするディレクトリのパス
44
+ # @param max_entries [Integer] 最大エントリ数
45
+ # @param timeout [Integer, nil] タイムアウト秒数(オプション)
46
+ # @return [Array<Hash>] スキャン結果
47
+ def scan_fast_async(path, max_entries, timeout: nil)
48
+ # スキャンを開始
49
+ @scanner.scan_fast_async(path, max_entries)
50
+
51
+ # Fiberでポーリング
52
+ poll_until_complete(timeout: timeout)
53
+ end
54
+
55
+ # 進捗報告付きスキャン
56
+ #
57
+ # @param path [String] スキャンするディレクトリのパス
58
+ # @param timeout [Integer, nil] タイムアウト秒数(オプション)
59
+ # @yield [current, total] 進捗情報を受け取るブロック
60
+ # @return [Array<Hash>] スキャン結果
61
+ def scan_async_with_progress(path, timeout: nil, &block)
62
+ # スキャンを開始
63
+ @scanner.scan_async(path)
64
+
65
+ # 進捗付きでポーリング
66
+ poll_until_complete_with_progress(timeout: timeout, &block)
67
+ end
68
+
69
+ # スキャンをキャンセル
70
+ def cancel
71
+ @scanner.cancel
72
+ end
73
+
74
+ # 状態を取得
75
+ #
76
+ # @return [Symbol] 現在の状態
77
+ def get_state
78
+ @scanner.get_state
79
+ end
80
+
81
+ # 進捗を取得
82
+ #
83
+ # @return [Hash] 進捗情報 {current:, total:}
84
+ def get_progress
85
+ @scanner.get_progress
86
+ end
87
+
88
+ private
89
+
90
+ # 完了までポーリング(Fiberでスリープ)
91
+ def poll_until_complete(timeout: nil)
92
+ start_time = Time.now
93
+
94
+ loop do
95
+ state = @scanner.get_state
96
+
97
+ case state
98
+ when :done
99
+ result = @scanner.get_results
100
+ @scanner.close
101
+ return result
102
+ when :failed
103
+ @scanner.close
104
+ raise StandardError, "Scan failed"
105
+ when :cancelled
106
+ @scanner.close
107
+ raise StandardError, "Scan cancelled"
108
+ end
109
+
110
+ if timeout && (Time.now - start_time) > timeout
111
+ @scanner.close
112
+ raise StandardError, "Timeout"
113
+ end
114
+
115
+ # Fiberでスリープ(ノンブロッキング)
116
+ sleep 0.01
117
+ end
118
+ end
119
+
120
+ # 進捗報告付きでポーリング
121
+ def poll_until_complete_with_progress(timeout: nil, &block)
122
+ start_time = Time.now
123
+
124
+ loop do
125
+ state = @scanner.get_state
126
+ progress = @scanner.get_progress
127
+
128
+ # 進捗コールバック実行
129
+ yield(progress[:current], progress[:total]) if block_given?
130
+
131
+ case state
132
+ when :done
133
+ result = @scanner.get_results
134
+ @scanner.close
135
+ return result
136
+ when :failed
137
+ @scanner.close
138
+ raise StandardError, "Scan failed"
139
+ when :cancelled
140
+ @scanner.close
141
+ raise StandardError, "Scan cancelled"
142
+ end
143
+
144
+ if timeout && (Time.now - start_time) > timeout
145
+ @scanner.close
146
+ raise StandardError, "Timeout"
147
+ end
148
+
149
+ # Fiberでスリープ(ノンブロッキング)
150
+ sleep 0.01
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rufio
4
+ # Promise風インターフェースで非同期スキャンを扱うクラス
5
+ #
6
+ # 使用例:
7
+ # scanner = NativeScannerRubyCore.new
8
+ # AsyncScannerPromise.new(scanner)
9
+ # .scan_async('/path')
10
+ # .then { |entries| entries.select { |e| e[:type] == 'file' } }
11
+ # .then { |files| files.map { |f| f[:name] } }
12
+ # .wait
13
+ #
14
+ class AsyncScannerPromise
15
+ def initialize(scanner)
16
+ @scanner = scanner
17
+ @callbacks = []
18
+ end
19
+
20
+ # 非同期スキャンを開始
21
+ #
22
+ # @param path [String] スキャンするディレクトリのパス
23
+ # @return [AsyncScannerPromise] self(メソッドチェーン用)
24
+ def scan_async(path)
25
+ @scanner.scan_async(path)
26
+ self
27
+ end
28
+
29
+ # 高速スキャンを開始(エントリ数制限付き)
30
+ #
31
+ # @param path [String] スキャンするディレクトリのパス
32
+ # @param max_entries [Integer] 最大エントリ数
33
+ # @return [AsyncScannerPromise] self(メソッドチェーン用)
34
+ def scan_fast_async(path, max_entries)
35
+ @scanner.scan_fast_async(path, max_entries)
36
+ self
37
+ end
38
+
39
+ # コールバックを登録
40
+ #
41
+ # @yield [result] 前のステップの結果を受け取るブロック
42
+ # @return [AsyncScannerPromise] self(メソッドチェーン用)
43
+ def then(&block)
44
+ @callbacks << block if block_given?
45
+ self
46
+ end
47
+
48
+ # スキャン完了を待ち、コールバックを順次実行
49
+ #
50
+ # @param timeout [Integer, nil] タイムアウト秒数(オプション)
51
+ # @return [Object] 最後のコールバックの戻り値、またはスキャン結果
52
+ def wait(timeout: nil)
53
+ result = @scanner.wait(timeout: timeout)
54
+
55
+ # 登録されたコールバックを順次実行
56
+ @callbacks.each do |callback|
57
+ result = callback.call(result)
58
+ end
59
+
60
+ result
61
+ ensure
62
+ # 完了後はスキャナーを自動的にクローズ
63
+ @scanner.close
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+
5
+ module Rufio
6
+ # バックグラウンドでシェルコマンドを実行するクラス
7
+ class BackgroundCommandExecutor
8
+ attr_reader :command_logger
9
+
10
+ # 初期化
11
+ # @param command_logger [CommandLogger] コマンドロガー
12
+ def initialize(command_logger)
13
+ @command_logger = command_logger
14
+ @thread = nil
15
+ @command = nil
16
+ @completed = false
17
+ @completion_message = nil
18
+ end
19
+
20
+ # コマンドを非同期で実行
21
+ # @param command [String] 実行するコマンド
22
+ # @return [Boolean] 実行を開始した場合はtrue、既に実行中の場合はfalse
23
+ def execute_async(command)
24
+ # 既に実行中の場合は新しいコマンドを開始しない
25
+ return false if running?
26
+
27
+ @command = command
28
+ @completed = false
29
+ @completion_message = nil
30
+
31
+ @thread = Thread.new do
32
+ begin
33
+ # コマンドを実行
34
+ stdout, stderr, status = Open3.capture3(command)
35
+
36
+ # 結果をログに保存
37
+ output = stdout + stderr
38
+ success = status.success?
39
+
40
+ error_message = success ? nil : stderr
41
+
42
+ @command_logger.log(
43
+ command,
44
+ output,
45
+ success: success,
46
+ error: error_message
47
+ )
48
+
49
+ # 完了メッセージを生成
50
+ command_name = extract_command_name(command)
51
+ if success
52
+ @completion_message = "✓ #{command_name} 完了"
53
+ else
54
+ @completion_message = "✗ #{command_name} 失敗"
55
+ end
56
+
57
+ @completed = true
58
+ rescue StandardError => e
59
+ # エラーが発生した場合もログに記録
60
+ @command_logger.log(
61
+ command,
62
+ "",
63
+ success: false,
64
+ error: e.message
65
+ )
66
+
67
+ command_name = extract_command_name(command)
68
+ @completion_message = "✗ #{command_name} エラー"
69
+ @completed = true
70
+ end
71
+ end
72
+
73
+ true
74
+ end
75
+
76
+ # コマンドが実行中かどうか
77
+ # @return [Boolean] 実行中の場合はtrue
78
+ def running?
79
+ @thread&.alive? || false
80
+ end
81
+
82
+ # 完了メッセージを取得
83
+ # @return [String, nil] 完了メッセージ(完了していない場合はnil)
84
+ def get_completion_message
85
+ @completion_message
86
+ end
87
+
88
+ private
89
+
90
+ # コマンド文字列からコマンド名を抽出
91
+ # @param command [String] コマンド
92
+ # @return [String] コマンド名
93
+ def extract_command_name(command)
94
+ # 最初の単語を取得
95
+ command.strip.split.first || 'command'
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'time'
5
+
6
+ module Rufio
7
+ # コマンド実行ログを保存・管理するクラス
8
+ class CommandLogger
9
+ attr_reader :log_dir
10
+
11
+ # 初期化
12
+ # @param log_dir [String] ログディレクトリのパス
13
+ def initialize(log_dir)
14
+ @log_dir = log_dir
15
+ FileUtils.mkdir_p(@log_dir) unless Dir.exist?(@log_dir)
16
+ end
17
+
18
+ # コマンド実行ログを保存
19
+ # @param command [String] 実行したコマンド
20
+ # @param output [String] コマンドの出力
21
+ # @param success [Boolean] 実行が成功したかどうか
22
+ # @param error [String, nil] エラーメッセージ(失敗時)
23
+ def log(command, output, success:, error: nil)
24
+ timestamp = Time.now
25
+ filename = generate_filename(command, timestamp)
26
+ filepath = File.join(@log_dir, filename)
27
+
28
+ content = format_log_content(command, output, timestamp, success, error)
29
+
30
+ # ディレクトリが存在しない場合は作成(バックグラウンドスレッドでの実行時の競合を防ぐ)
31
+ FileUtils.mkdir_p(@log_dir) unless Dir.exist?(@log_dir)
32
+
33
+ File.write(filepath, content)
34
+ end
35
+
36
+ # ログファイル一覧を取得(新しい順)
37
+ # @return [Array<String>] ログファイルのパス一覧
38
+ def list_logs
39
+ Dir.glob(File.join(@log_dir, "*.log")).sort.reverse
40
+ end
41
+
42
+ # 古いログを削除
43
+ # @param max_logs [Integer] 保管する最大ログ数
44
+ def cleanup_old_logs(max_logs:)
45
+ logs = list_logs
46
+ return if logs.size <= max_logs
47
+
48
+ logs_to_delete = logs[max_logs..-1]
49
+ logs_to_delete.each do |log_file|
50
+ File.delete(log_file)
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ # ログファイル名を生成
57
+ # @param command [String] コマンド
58
+ # @param timestamp [Time] タイムスタンプ
59
+ # @return [String] ファイル名
60
+ def generate_filename(command, timestamp)
61
+ # ミリ秒を含めて一意性を確保
62
+ timestamp_str = timestamp.strftime("%Y%m%d%H%M%S") + sprintf("%03d", (timestamp.usec / 1000).to_i)
63
+ command_part = sanitize_command(command)
64
+ "#{timestamp_str}-#{command_part}.log"
65
+ end
66
+
67
+ # コマンド文字列をファイル名用にサニタイズ
68
+ # @param command [String] コマンド
69
+ # @return [String] サニタイズされたコマンド
70
+ def sanitize_command(command)
71
+ # Remove ! prefix if exists
72
+ cmd = command.start_with?('!') ? command[1..-1] : command
73
+
74
+ # Take first word (command name)
75
+ cmd = cmd.split.first || 'command'
76
+
77
+ # Remove unsafe characters
78
+ cmd = cmd.gsub(/[^\w\-]/, '_')
79
+
80
+ # Limit length
81
+ cmd[0...50]
82
+ end
83
+
84
+ # ログ内容をフォーマット
85
+ # @param command [String] コマンド
86
+ # @param output [String] 出力
87
+ # @param timestamp [Time] タイムスタンプ
88
+ # @param success [Boolean] 成功フラグ
89
+ # @param error [String, nil] エラーメッセージ
90
+ # @return [String] フォーマットされたログ内容
91
+ def format_log_content(command, output, timestamp, success, error)
92
+ lines = []
93
+ lines << "=" * 80
94
+ lines << "Command Execution Log"
95
+ lines << "=" * 80
96
+ lines << ""
97
+ lines << "Timestamp: #{timestamp.strftime('%Y-%m-%d %H:%M:%S')}"
98
+ lines << "Command: #{command}"
99
+ lines << "Status: #{success ? 'Success' : 'Failed'}"
100
+ lines << ""
101
+
102
+ if error
103
+ lines << "Error:"
104
+ lines << error
105
+ lines << ""
106
+ end
107
+
108
+ if output && !output.empty?
109
+ lines << "Output:"
110
+ lines << "-" * 80
111
+ lines << output
112
+ lines << "-" * 80
113
+ end
114
+
115
+ lines << ""
116
+ lines << "=" * 80
117
+
118
+ lines.join("\n")
119
+ end
120
+ end
121
+ end
@@ -5,8 +5,11 @@ require 'open3'
5
5
  module Rufio
6
6
  # コマンドモード - プラグインコマンドを実行するためのインターフェース
7
7
  class CommandMode
8
- def initialize
8
+ attr_accessor :background_executor
9
+
10
+ def initialize(background_executor = nil)
9
11
  @commands = {}
12
+ @background_executor = background_executor
10
13
  load_plugin_commands
11
14
  end
12
15
 
@@ -17,7 +20,19 @@ module Rufio
17
20
 
18
21
  # シェルコマンドの実行 (! で始まる場合)
19
22
  if command_string.strip.start_with?('!')
20
- return execute_shell_command(command_string.strip[1..-1])
23
+ shell_command = command_string.strip[1..-1]
24
+
25
+ # バックグラウンドエグゼキュータが利用可能な場合は非同期実行
26
+ if @background_executor
27
+ if @background_executor.execute_async(shell_command)
28
+ return "🔄 バックグラウンドで実行中: #{shell_command.split.first}"
29
+ else
30
+ return "⚠️ 既にコマンドが実行中です"
31
+ end
32
+ else
33
+ # バックグラウンドエグゼキュータがない場合は同期実行
34
+ return execute_shell_command(shell_command)
35
+ end
21
36
  end
22
37
 
23
38
  # コマンド名を取得 (前後の空白を削除)
@@ -22,18 +22,11 @@ module Rufio
22
22
 
23
23
  @entries = []
24
24
 
25
- Dir.entries(@current_path).each do |name|
26
- next if name == '.'
27
-
28
- full_path = File.join(@current_path, name)
29
- entry = {
30
- name: name,
31
- path: full_path,
32
- type: determine_file_type(full_path),
33
- size: safe_file_size(full_path),
34
- modified: safe_file_mtime(full_path)
35
- }
36
- @entries << entry
25
+ # NativeScannerが利用可能な場合は使用
26
+ if use_native_scanner?
27
+ scan_with_native_scanner
28
+ else
29
+ scan_with_ruby
37
30
  end
38
31
 
39
32
  sort_entries!
@@ -84,6 +77,61 @@ module Rufio
84
77
 
85
78
  private
86
79
 
80
+ # NativeScannerを使用するかどうか
81
+ def use_native_scanner?
82
+ defined?(Rufio::NativeScanner) && Rufio::NativeScanner.mode != 'ruby'
83
+ end
84
+
85
+ # NativeScannerでスキャン
86
+ def scan_with_native_scanner
87
+ entries = Rufio::NativeScanner.scan_directory(@current_path)
88
+
89
+ entries.each do |entry|
90
+ next if entry[:name] == '.'
91
+
92
+ full_path = File.join(@current_path, entry[:name])
93
+
94
+ # NativeScannerのエントリをDirectoryListingの形式に変換
95
+ converted_entry = {
96
+ name: entry[:name],
97
+ path: full_path,
98
+ type: convert_type(entry[:type], entry[:executable]),
99
+ size: entry[:size] || 0,
100
+ modified: entry[:mtime] ? Time.at(entry[:mtime]) : Time.now
101
+ }
102
+ @entries << converted_entry
103
+ end
104
+ rescue StandardError => e
105
+ # エラーが発生した場合はRubyでスキャン
106
+ Logger.debug "NativeScanner failed: #{e.message}, falling back to Ruby" if defined?(Logger)
107
+ scan_with_ruby
108
+ end
109
+
110
+ # Rubyの標準機能でスキャン
111
+ def scan_with_ruby
112
+ Dir.entries(@current_path).each do |name|
113
+ next if name == '.'
114
+
115
+ full_path = File.join(@current_path, name)
116
+ entry = {
117
+ name: name,
118
+ path: full_path,
119
+ type: determine_file_type(full_path),
120
+ size: safe_file_size(full_path),
121
+ modified: safe_file_mtime(full_path)
122
+ }
123
+ @entries << entry
124
+ end
125
+ end
126
+
127
+ # NativeScannerのタイプをDirectoryListingのタイプに変換
128
+ def convert_type(type, executable)
129
+ return type if type == 'directory'
130
+ return 'executable' if executable
131
+
132
+ 'file'
133
+ end
134
+
87
135
  def determine_file_type(path)
88
136
  return 'directory' if File.directory?(path)
89
137
  return 'executable' if File.executable?(path) && !File.directory?(path)
@@ -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