rufio 0.31.0 → 0.32.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,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rufio
4
+ # コマンド補完機能を提供するクラス
5
+ class CommandCompletion
6
+ # 初期化
7
+ # @param history [CommandHistory, nil] コマンド履歴(オプション)
8
+ def initialize(history = nil)
9
+ @command_mode = CommandMode.new
10
+ @shell_completion = ShellCommandCompletion.new
11
+ @history = history
12
+ end
13
+
14
+ # コマンドの補完候補を取得
15
+ # @param input [String] 入力されたテキスト
16
+ # @return [Array<String>] 補完候補のリスト
17
+ def complete(input)
18
+ # 入力が空の場合は内部コマンドを返す
19
+ if input.nil? || input.strip.empty?
20
+ return @command_mode.available_commands.map(&:to_s)
21
+ end
22
+
23
+ # シェルコマンド補完(!で始まる場合)
24
+ if input.strip.start_with?('!')
25
+ return complete_shell_command(input.strip)
26
+ end
27
+
28
+ # 通常のコマンド補完(内部コマンド)
29
+ available_commands = @command_mode.available_commands.map(&:to_s)
30
+ input_lower = input.downcase
31
+ candidates = available_commands.select do |command|
32
+ command.downcase.start_with?(input_lower)
33
+ end
34
+
35
+ candidates
36
+ end
37
+
38
+ # 補完候補の共通プレフィックスを取得
39
+ # @param input [String] 入力されたテキスト
40
+ # @return [String] 共通プレフィックス
41
+ def common_prefix(input)
42
+ candidates = complete(input)
43
+
44
+ # 候補がない場合は元の入力を返す
45
+ return input if candidates.empty?
46
+
47
+ # 候補が1つの場合はそのコマンド名を返す
48
+ return candidates.first if candidates.size == 1
49
+
50
+ # 複数の候補がある場合、共通プレフィックスを計算
51
+ min_candidate = candidates.min
52
+ max_candidate = candidates.max
53
+
54
+ min_candidate.chars.zip(max_candidate.chars).each_with_index do |(char_min, char_max), index|
55
+ return min_candidate[0...index] if char_min != char_max
56
+ end
57
+
58
+ # すべての文字が一致した場合は最小の候補を返す
59
+ min_candidate
60
+ end
61
+
62
+ private
63
+
64
+ # シェルコマンドの補完
65
+ # @param input [String] ! で始まる入力
66
+ # @return [Array<String>] 補完候補のリスト
67
+ def complete_shell_command(input)
68
+ # ! を除去
69
+ command_part = input[1..-1]
70
+
71
+ # スペースが含まれる場合、コマンドと引数に分離
72
+ if command_part.include?(' ')
73
+ parts = command_part.split(' ', 2)
74
+ cmd = parts[0]
75
+ arg = parts[1] || ''
76
+
77
+ # 引数部分がパスっぽい場合、ファイルパス補完
78
+ if arg.include?('/') || arg.start_with?('~')
79
+ path_candidates = @shell_completion.complete_path(arg)
80
+ return path_candidates.map { |path| "!#{cmd} #{path}" }
81
+ else
82
+ # 引数部分のファイル補完(カレントディレクトリ)
83
+ path_candidates = @shell_completion.complete_path(arg)
84
+ return path_candidates.map { |path| "!#{cmd} #{path}" }
85
+ end
86
+ else
87
+ # コマンド名の補完
88
+ cmd_candidates = @shell_completion.complete_command(command_part)
89
+
90
+ # 履歴からの補完も追加
91
+ if @history
92
+ history_candidates = @shell_completion.complete_from_history(command_part, @history)
93
+ cmd_candidates += history_candidates
94
+ end
95
+
96
+ # ! を付けて返す
97
+ cmd_candidates.uniq.map { |cmd| "!#{cmd}" }
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rufio
4
+ # コマンド履歴管理クラス
5
+ class CommandHistory
6
+ DEFAULT_MAX_SIZE = 1000
7
+
8
+ attr_reader :size
9
+
10
+ # 初期化
11
+ # @param history_file [String] 履歴ファイルのパス
12
+ # @param max_size [Integer] 履歴の最大保存数
13
+ def initialize(history_file, max_size: DEFAULT_MAX_SIZE)
14
+ @history_file = history_file
15
+ @max_size = max_size
16
+ @history = []
17
+ @position = -1 # -1 = 最新の位置(履歴の外)
18
+
19
+ load_from_file if File.exist?(@history_file)
20
+ end
21
+
22
+ # コマンドを履歴に追加
23
+ # @param command [String] 追加するコマンド
24
+ def add(command)
25
+ # 空のコマンドは無視
26
+ return if command.nil? || command.strip.empty?
27
+
28
+ # 連続する重複は無視
29
+ return if !@history.empty? && @history.last == command
30
+
31
+ @history << command
32
+
33
+ # 最大サイズを超えたら古いものを削除
34
+ @history.shift if @history.size > @max_size
35
+
36
+ # 位置をリセット
37
+ reset_position
38
+ end
39
+
40
+ # 前のコマンドを取得(上矢印キー相当)
41
+ # @return [String, nil] 前のコマンド、存在しない場合はnil
42
+ def previous
43
+ return nil if @history.empty?
44
+
45
+ # 初回は最後のコマンドを返す
46
+ if @position == -1
47
+ @position = @history.size - 1
48
+ return @history[@position]
49
+ end
50
+
51
+ # これ以上前がない場合
52
+ return nil if @position <= 0
53
+
54
+ # 一つ前に移動
55
+ @position -= 1
56
+ @history[@position]
57
+ end
58
+
59
+ # 次のコマンドを取得(下矢印キー相当)
60
+ # @return [String] 次のコマンド、最新位置の場合は空文字列
61
+ def next
62
+ return "" if @history.empty? || @position == -1
63
+
64
+ # 一つ次に移動
65
+ @position += 1
66
+
67
+ # 最新位置を超えた場合は空文字列を返す
68
+ if @position >= @history.size
69
+ @position = -1
70
+ return ""
71
+ end
72
+
73
+ @history[@position]
74
+ end
75
+
76
+ # 履歴をファイルに保存
77
+ def save
78
+ File.write(@history_file, @history.join("\n") + "\n")
79
+ end
80
+
81
+ # 位置をリセット(新しいコマンド入力時などに使用)
82
+ def reset_position
83
+ @position = -1
84
+ end
85
+
86
+ # 履歴のサイズを取得
87
+ def size
88
+ @history.size
89
+ end
90
+
91
+ private
92
+
93
+ # ファイルから履歴を読み込む
94
+ def load_from_file
95
+ return unless File.exist?(@history_file)
96
+
97
+ File.readlines(@history_file, chomp: true).each do |line|
98
+ next if line.strip.empty?
99
+
100
+ @history << line
101
+ end
102
+
103
+ # 最大サイズを超えている場合は調整
104
+ while @history.size > @max_size
105
+ @history.shift
106
+ end
107
+ end
108
+ end
109
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'open3'
4
+
3
5
  module Rufio
4
6
  # コマンドモード - プラグインコマンドを実行するためのインターフェース
5
7
  class CommandMode
@@ -13,6 +15,11 @@ module Rufio
13
15
  # 空のコマンドは無視
14
16
  return nil if command_string.nil? || command_string.strip.empty?
15
17
 
18
+ # シェルコマンドの実行 (! で始まる場合)
19
+ if command_string.strip.start_with?('!')
20
+ return execute_shell_command(command_string.strip[1..-1])
21
+ end
22
+
16
23
  # コマンド名を取得 (前後の空白を削除)
17
24
  command_name = command_string.strip.to_sym
18
25
 
@@ -48,6 +55,34 @@ module Rufio
48
55
 
49
56
  private
50
57
 
58
+ # シェルコマンドを実行する
59
+ def execute_shell_command(shell_command)
60
+ # コマンドが空の場合
61
+ return { success: false, error: "コマンドが指定されていません" } if shell_command.strip.empty?
62
+
63
+ begin
64
+ # Open3を使って標準出力と標準エラーを分離して取得
65
+ stdout, stderr, status = Open3.capture3(shell_command)
66
+
67
+ result = {
68
+ success: status.success?,
69
+ output: stdout.strip,
70
+ stderr: stderr.strip
71
+ }
72
+
73
+ # コマンドが失敗した場合、エラーメッセージを追加
74
+ unless status.success?
75
+ result[:error] = "コマンドが失敗しました (終了コード: #{status.exitstatus})"
76
+ end
77
+
78
+ result
79
+ rescue Errno::ENOENT => e
80
+ { success: false, error: "コマンドが見つかりません: #{e.message}" }
81
+ rescue StandardError => e
82
+ { success: false, error: "コマンド実行エラー: #{e.message}" }
83
+ end
84
+ end
85
+
51
86
  # プラグインからコマンドを読み込む
52
87
  def load_plugin_commands
53
88
  # 有効なプラグインを取得
@@ -51,16 +51,6 @@ module Rufio
51
51
  content_lines = [""]
52
52
  content_lines << "#{input}_" # カーソルを_で表現
53
53
  content_lines << ""
54
-
55
- # 補完候補がある場合は表示
56
- unless suggestions.empty?
57
- content_lines << "補完候補:"
58
- suggestions.each do |suggestion|
59
- content_lines << " #{suggestion}"
60
- end
61
- content_lines << ""
62
- end
63
-
64
54
  content_lines << "Tab: 補完 | Enter: 実行 | ESC: キャンセル"
65
55
 
66
56
  # ウィンドウの色設定(青)
@@ -87,16 +77,23 @@ module Rufio
87
77
  end
88
78
 
89
79
  # コマンド実行結果をフローティングウィンドウで表示
90
- # @param result [String, nil] コマンド実行結果
80
+ # @param result [String, Hash, nil] コマンド実行結果
91
81
  def show_result(result)
92
82
  # nil または空文字列の場合は何も表示しない
93
83
  return if result.nil? || result.empty?
94
84
 
95
- # 結果を行に分割
96
- result_lines = result.split("\n")
85
+ # Hash形式の結果を処理
86
+ if result.is_a?(Hash)
87
+ result_text = format_hash_result(result)
88
+ is_error = !result[:success]
89
+ else
90
+ # 文字列形式の結果(従来の動作)
91
+ result_text = result
92
+ is_error = result.include?("⚠️") || result.include?("エラー")
93
+ end
97
94
 
98
- # エラーメッセージかどうかを判定
99
- is_error = result.include?("⚠️") || result.include?("エラー")
95
+ # 結果を行に分割
96
+ result_lines = result_text.split("\n")
100
97
 
101
98
  # ウィンドウの色設定
102
99
  if is_error
@@ -164,5 +161,41 @@ module Rufio
164
161
 
165
162
  strings.first[0...common_length]
166
163
  end
164
+
165
+ # Hash形式の結果を文字列に変換
166
+ # @param result [Hash] コマンド実行結果
167
+ # @return [String] フォーマットされた結果
168
+ def format_hash_result(result)
169
+ lines = []
170
+
171
+ # エラーメッセージがある場合
172
+ if result[:error]
173
+ lines << result[:error]
174
+ lines << ""
175
+ end
176
+
177
+ # 標準出力
178
+ if result[:output] && !result[:output].empty?
179
+ lines << result[:output]
180
+ end
181
+
182
+ # 標準エラー出力(空でない場合のみ)
183
+ if result[:stderr] && !result[:stderr].empty?
184
+ lines << "" if lines.any?
185
+ lines << "--- stderr ---"
186
+ lines << result[:stderr]
187
+ end
188
+
189
+ # 何も出力がない場合
190
+ if lines.empty?
191
+ if result[:success]
192
+ lines << "コマンドが正常に実行されました"
193
+ else
194
+ lines << "コマンドが失敗しました"
195
+ end
196
+ end
197
+
198
+ lines.join("\n")
199
+ end
167
200
  end
168
201
  end
@@ -56,6 +56,10 @@ module Rufio
56
56
  File.expand_path('~/.config/rufio/scripts')
57
57
  end
58
58
 
59
+ def command_history_size
60
+ load_config[:command_history_size] || 1000
61
+ end
62
+
59
63
  private
60
64
 
61
65
  def load_config_file
@@ -79,6 +83,11 @@ module Rufio
79
83
  config[:scripts_dir] = Object.const_get(:SCRIPTS_DIR)
80
84
  end
81
85
 
86
+ # Load command history size if defined
87
+ if Object.const_defined?(:COMMAND_HISTORY_SIZE)
88
+ config[:command_history_size] = Object.const_get(:COMMAND_HISTORY_SIZE)
89
+ end
90
+
82
91
  config
83
92
  rescue StandardError => e
84
93
  warn "Failed to load config file: #{e.message}"
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rufio
4
+ module Plugins
5
+ # Hello コマンドを提供するプラグイン
6
+ # Rubyコードで挨拶を返す簡単な例
7
+ class Hello < Plugin
8
+ def name
9
+ "Hello"
10
+ end
11
+
12
+ def description
13
+ "Rubyで実装された挨拶コマンドの例"
14
+ end
15
+
16
+ def commands
17
+ {
18
+ hello: method(:say_hello)
19
+ }
20
+ end
21
+
22
+ private
23
+
24
+ # 挨拶メッセージを返す
25
+ def say_hello
26
+ "Hello, World! 🌍\n\nこのコマンドはRubyで実装されています。"
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rufio
4
+ # シェルコマンド補完機能を提供するクラス
5
+ class ShellCommandCompletion
6
+ # PATHからコマンドを補完
7
+ # @param input [String] 入力されたコマンドの一部
8
+ # @return [Array<String>] 補完候補のリスト
9
+ def complete_command(input)
10
+ return [] if input.nil? || input.empty?
11
+
12
+ input_lower = input.downcase
13
+ path_commands.select { |cmd| cmd.downcase.start_with?(input_lower) }
14
+ end
15
+
16
+ # ファイルパスを補完
17
+ # @param input [String] 入力されたパスの一部
18
+ # @param options [Hash] オプション
19
+ # @option options [Boolean] :directories_only ディレクトリのみを補完
20
+ # @return [Array<String>] 補完候補のリスト
21
+ def complete_path(input, options = {})
22
+ return [] if input.nil?
23
+
24
+ # ~を展開
25
+ expanded_input = File.expand_path(input) rescue input
26
+
27
+ # 入力が/で終わる場合(ディレクトリ内を補完)
28
+ if input.end_with?("/")
29
+ dir = expanded_input
30
+ pattern = File.join(dir, "*")
31
+ else
32
+ # ディレクトリ部分とファイル名部分を分離
33
+ dir = File.dirname(expanded_input)
34
+ basename = File.basename(expanded_input)
35
+ pattern = File.join(dir, "#{basename}*")
36
+ end
37
+
38
+ # ディレクトリが存在しない場合は空の配列を返す
39
+ return [] unless Dir.exist?(dir)
40
+
41
+ # マッチするファイル/ディレクトリを取得
42
+ candidates = Dir.glob(pattern)
43
+
44
+ # ディレクトリのみのフィルタリング
45
+ if options[:directories_only]
46
+ candidates.select! { |path| File.directory?(path) }
47
+ end
48
+
49
+ # 元の入力が~で始まる場合、結果も~で始まるように変換
50
+ if input.start_with?("~")
51
+ home = ENV['HOME']
52
+ candidates.map! { |path| path.sub(home, "~") }
53
+ end
54
+
55
+ candidates.sort
56
+ rescue StandardError
57
+ []
58
+ end
59
+
60
+ # コマンド履歴から補完
61
+ # @param input [String] 入力されたコマンドの一部
62
+ # @param history [CommandHistory] コマンド履歴オブジェクト
63
+ # @return [Array<String>] 補完候補のリスト
64
+ def complete_from_history(input, history)
65
+ return [] if input.nil? || input.empty?
66
+ return [] unless history
67
+
68
+ # 履歴から全てのコマンドを取得
69
+ # CommandHistoryクラスから履歴を取得する方法が必要
70
+ # 現在の実装では、@historyインスタンス変数にアクセスできないため、
71
+ # 新しいメソッドを追加する必要がある
72
+ # 一旦、空の配列を返す実装にして、後でCommandHistoryを拡張する
73
+ commands = get_history_commands(history)
74
+
75
+ input_lower = input.downcase
76
+ commands.select { |cmd| cmd.downcase.start_with?(input_lower) }.uniq
77
+ end
78
+
79
+ private
80
+
81
+ # PATHから実行可能なコマンドのリストを取得
82
+ # @return [Array<String>] コマンドのリスト
83
+ def path_commands
84
+ @path_commands ||= begin
85
+ paths = ENV['PATH'].split(File::PATH_SEPARATOR)
86
+ commands = []
87
+
88
+ paths.each do |path|
89
+ next unless Dir.exist?(path)
90
+
91
+ Dir.foreach(path) do |file|
92
+ next if file == '.' || file == '..'
93
+
94
+ filepath = File.join(path, file)
95
+ # 実行可能なファイルのみを追加
96
+ commands << file if File.executable?(filepath) && !File.directory?(filepath)
97
+ end
98
+ end
99
+
100
+ commands.uniq.sort
101
+ rescue StandardError
102
+ []
103
+ end
104
+ end
105
+
106
+ # CommandHistoryオブジェクトからコマンドリストを取得
107
+ # @param history [CommandHistory] 履歴オブジェクト
108
+ # @return [Array<String>] コマンドのリスト
109
+ def get_history_commands(history)
110
+ # CommandHistoryの内部データにアクセスするため、
111
+ # instance_variable_getを使用(本来はpublicメソッドを追加すべき)
112
+ history_array = history.instance_variable_get(:@history) || []
113
+
114
+ # ! を除去したコマンドを返す
115
+ history_array.map do |cmd|
116
+ cmd.start_with?('!') ? cmd[1..-1] : cmd
117
+ end
118
+ end
119
+ end
120
+ end
@@ -48,6 +48,12 @@ module Rufio
48
48
  @dialog_renderer = DialogRenderer.new
49
49
  @command_mode_ui = CommandModeUI.new(@command_mode, @dialog_renderer)
50
50
 
51
+ # コマンド履歴と補完
52
+ history_file = File.join(Dir.home, '.rufio', 'command_history.txt')
53
+ FileUtils.mkdir_p(File.dirname(history_file))
54
+ @command_history = CommandHistory.new(history_file, max_size: ConfigLoader.command_history_size)
55
+ @command_completion = CommandCompletion.new(@command_history)
56
+
51
57
  # Project mode
52
58
  @project_mode = nil
53
59
  @project_command = nil
@@ -151,10 +157,8 @@ module Rufio
151
157
 
152
158
  # コマンドモードがアクティブな場合はコマンド入力ウィンドウを表示
153
159
  if @command_mode_active
154
- # 補完候補を取得
155
- suggestions = @command_mode_ui.autocomplete(@command_input)
156
160
  # フローティングウィンドウで表示
157
- @command_mode_ui.show_input_prompt(@command_input, suggestions)
161
+ @command_mode_ui.show_input_prompt(@command_input)
158
162
  else
159
163
  # move cursor to invisible position
160
164
  print "\e[#{@screen_height};#{@screen_width}H"
@@ -546,7 +550,7 @@ module Rufio
546
550
  deactivate_command_mode
547
551
  when "\t"
548
552
  # Tab キーで補完
549
- @command_input = @command_mode_ui.complete_command(@command_input)
553
+ handle_tab_completion
550
554
  when "\u007F", "\b"
551
555
  # Backspace
552
556
  @command_input.chop! unless @command_input.empty?
@@ -560,6 +564,9 @@ module Rufio
560
564
  def execute_command(command_string)
561
565
  return if command_string.nil? || command_string.empty?
562
566
 
567
+ # コマンド履歴に追加
568
+ @command_history.add(command_string)
569
+
563
570
  result = @command_mode.execute(command_string)
564
571
 
565
572
  # コマンド実行結果をフローティングウィンドウで表示
@@ -569,6 +576,84 @@ module Rufio
569
576
  draw_screen
570
577
  end
571
578
 
579
+ # Tab補完を処理
580
+ def handle_tab_completion
581
+ # 補完候補を取得
582
+ candidates = @command_completion.complete(@command_input)
583
+
584
+ # 候補がない場合は何もしない
585
+ return if candidates.empty?
586
+
587
+ # 候補が1つの場合はそれに補完
588
+ if candidates.size == 1
589
+ @command_input = candidates.first
590
+ return
591
+ end
592
+
593
+ # 複数の候補がある場合、共通プレフィックスまで補完
594
+ prefix = @command_completion.common_prefix(@command_input)
595
+
596
+ # 入力が変わる場合は補完して終了
597
+ if prefix != @command_input
598
+ @command_input = prefix
599
+ return
600
+ end
601
+
602
+ # 入力が変わらない場合は候補リストを表示
603
+ show_completion_candidates(candidates)
604
+ end
605
+
606
+ # 補完候補を一時的に表示
607
+ def show_completion_candidates(candidates)
608
+ title = "補完候補 (#{candidates.size}件)"
609
+
610
+ # 候補を表示用にフォーマット(最大20件)
611
+ display_candidates = candidates.first(20)
612
+ content_lines = [""]
613
+ display_candidates.each do |candidate|
614
+ content_lines << " #{candidate}"
615
+ end
616
+
617
+ if candidates.size > 20
618
+ content_lines << ""
619
+ content_lines << " ... 他 #{candidates.size - 20} 件"
620
+ end
621
+
622
+ content_lines << ""
623
+ content_lines << "Press any key to continue..."
624
+
625
+ # ウィンドウの色設定(黄色)
626
+ border_color = "\e[33m"
627
+ title_color = "\e[1;33m"
628
+ content_color = "\e[37m"
629
+
630
+ # ウィンドウサイズを計算
631
+ width, height = @dialog_renderer.calculate_dimensions(content_lines, {
632
+ title: title,
633
+ min_width: 40,
634
+ max_width: 80
635
+ })
636
+
637
+ # 中央位置を計算
638
+ x, y = @dialog_renderer.calculate_center(width, height)
639
+
640
+ # フローティングウィンドウを描画
641
+ @dialog_renderer.draw_floating_window(x, y, width, height, title, content_lines, {
642
+ border_color: border_color,
643
+ title_color: title_color,
644
+ content_color: content_color
645
+ })
646
+
647
+ # キー入力を待つ
648
+ STDIN.getch
649
+
650
+ # ウィンドウをクリア
651
+ @dialog_renderer.clear_area(x, y, width, height)
652
+
653
+ # 画面を再描画
654
+ draw_screen
655
+ end
656
+
572
657
  # Show info notices from the info directory if any are unread
573
658
  def show_info_notices
574
659
  require_relative 'info_notice'
data/lib/rufio/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rufio
4
- VERSION = '0.31.0'
4
+ VERSION = '0.32.0'
5
5
  end
data/lib/rufio.rb CHANGED
@@ -27,12 +27,18 @@ require_relative "rufio/plugin"
27
27
  require_relative "rufio/plugin_manager"
28
28
  require_relative "rufio/command_mode"
29
29
  require_relative "rufio/command_mode_ui"
30
+ require_relative "rufio/command_history"
31
+ require_relative "rufio/command_completion"
32
+ require_relative "rufio/shell_command_completion"
30
33
 
31
34
  # プロジェクトモード
32
35
  require_relative "rufio/project_mode"
33
36
  require_relative "rufio/project_command"
34
37
  require_relative "rufio/project_log"
35
38
 
39
+ # プラグインをロード
40
+ Rufio::PluginManager.load_all
41
+
36
42
  module Rufio
37
43
  class Error < StandardError; end
38
44
  end