rufio 0.9.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 (47) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +188 -0
  3. data/CHANGELOG_v0.4.0.md +146 -0
  4. data/CHANGELOG_v0.5.0.md +26 -0
  5. data/CHANGELOG_v0.6.0.md +182 -0
  6. data/CHANGELOG_v0.7.0.md +280 -0
  7. data/CHANGELOG_v0.8.0.md +267 -0
  8. data/CHANGELOG_v0.9.0.md +279 -0
  9. data/README.md +631 -0
  10. data/README_EN.md +561 -0
  11. data/Rakefile +156 -0
  12. data/bin/rufio +34 -0
  13. data/config_example.rb +88 -0
  14. data/docs/PLUGIN_GUIDE.md +431 -0
  15. data/docs/plugin_example.rb +119 -0
  16. data/lib/rufio/application.rb +32 -0
  17. data/lib/rufio/bookmark.rb +115 -0
  18. data/lib/rufio/bookmark_manager.rb +173 -0
  19. data/lib/rufio/color_helper.rb +150 -0
  20. data/lib/rufio/command_mode.rb +72 -0
  21. data/lib/rufio/command_mode_ui.rb +168 -0
  22. data/lib/rufio/config.rb +199 -0
  23. data/lib/rufio/config_loader.rb +110 -0
  24. data/lib/rufio/dialog_renderer.rb +127 -0
  25. data/lib/rufio/directory_listing.rb +113 -0
  26. data/lib/rufio/file_opener.rb +140 -0
  27. data/lib/rufio/file_operations.rb +231 -0
  28. data/lib/rufio/file_preview.rb +200 -0
  29. data/lib/rufio/filter_manager.rb +114 -0
  30. data/lib/rufio/health_checker.rb +246 -0
  31. data/lib/rufio/keybind_handler.rb +828 -0
  32. data/lib/rufio/logger.rb +103 -0
  33. data/lib/rufio/plugin.rb +89 -0
  34. data/lib/rufio/plugin_config.rb +59 -0
  35. data/lib/rufio/plugin_manager.rb +84 -0
  36. data/lib/rufio/plugins/file_operations.rb +44 -0
  37. data/lib/rufio/selection_manager.rb +79 -0
  38. data/lib/rufio/terminal_ui.rb +630 -0
  39. data/lib/rufio/text_utils.rb +108 -0
  40. data/lib/rufio/version.rb +5 -0
  41. data/lib/rufio/zoxide_integration.rb +188 -0
  42. data/lib/rufio.rb +33 -0
  43. data/publish_gem.zsh +131 -0
  44. data/rufio.gemspec +40 -0
  45. data/test_delete/test1.txt +1 -0
  46. data/test_delete/test2.txt +1 -0
  47. metadata +189 -0
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'fileutils'
5
+
6
+ module Rufio
7
+ class Bookmark
8
+ MAX_BOOKMARKS = 9
9
+
10
+ def initialize(config_file = nil)
11
+ @config_file = config_file || default_config_file
12
+ @bookmarks = []
13
+ ensure_config_directory
14
+ load
15
+ end
16
+
17
+ def add(path, name)
18
+ return false if @bookmarks.length >= MAX_BOOKMARKS
19
+ return false if exists_by_name?(name)
20
+ return false if exists_by_path?(path)
21
+ return false unless Dir.exist?(path)
22
+
23
+ @bookmarks << { path: File.expand_path(path), name: name }
24
+ save
25
+ true
26
+ end
27
+
28
+ def remove(name)
29
+ initial_length = @bookmarks.length
30
+ @bookmarks.reject! { |bookmark| bookmark[:name] == name }
31
+
32
+ if @bookmarks.length < initial_length
33
+ save
34
+ true
35
+ else
36
+ false
37
+ end
38
+ end
39
+
40
+ def get_path(name)
41
+ bookmark = @bookmarks.find { |b| b[:name] == name }
42
+ bookmark&.[](:path)
43
+ end
44
+
45
+ def find_by_number(number)
46
+ return nil unless number.is_a?(Integer)
47
+ return nil if number < 1 || number > @bookmarks.length
48
+
49
+ sorted_bookmarks[number - 1]
50
+ end
51
+
52
+ def list
53
+ sorted_bookmarks
54
+ end
55
+
56
+ def save
57
+ begin
58
+ File.write(@config_file, JSON.pretty_generate(@bookmarks))
59
+ true
60
+ rescue StandardError => e
61
+ warn "Failed to save bookmarks: #{e.message}"
62
+ false
63
+ end
64
+ end
65
+
66
+ def load
67
+ return true unless File.exist?(@config_file)
68
+
69
+ begin
70
+ content = File.read(@config_file)
71
+ @bookmarks = JSON.parse(content, symbolize_names: true)
72
+ @bookmarks = [] unless @bookmarks.is_a?(Array)
73
+
74
+ # 無効なブックマークを除去
75
+ @bookmarks = @bookmarks.select do |bookmark|
76
+ bookmark.is_a?(Hash) &&
77
+ bookmark.key?(:path) &&
78
+ bookmark.key?(:name) &&
79
+ bookmark[:path].is_a?(String) &&
80
+ bookmark[:name].is_a?(String)
81
+ end
82
+
83
+ true
84
+ rescue JSON::ParserError, StandardError => e
85
+ warn "Failed to load bookmarks: #{e.message}"
86
+ @bookmarks = []
87
+ true
88
+ end
89
+ end
90
+
91
+ private
92
+
93
+ def default_config_file
94
+ File.expand_path('~/.config/rufio/bookmarks.json')
95
+ end
96
+
97
+ def ensure_config_directory
98
+ config_dir = File.dirname(@config_file)
99
+ FileUtils.mkdir_p(config_dir) unless Dir.exist?(config_dir)
100
+ end
101
+
102
+ def exists_by_name?(name)
103
+ @bookmarks.any? { |bookmark| bookmark[:name] == name }
104
+ end
105
+
106
+ def exists_by_path?(path)
107
+ expanded_path = File.expand_path(path)
108
+ @bookmarks.any? { |bookmark| bookmark[:path] == expanded_path }
109
+ end
110
+
111
+ def sorted_bookmarks
112
+ @bookmarks.sort_by { |bookmark| bookmark[:name] }
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'bookmark'
4
+ require_relative 'config_loader'
5
+
6
+ module Rufio
7
+ # Manages bookmark operations with interactive UI
8
+ class BookmarkManager
9
+ def initialize(bookmark = nil, dialog_renderer = nil)
10
+ @bookmark = bookmark || Bookmark.new
11
+ @dialog_renderer = dialog_renderer
12
+ end
13
+
14
+ # Show bookmark menu and handle user selection
15
+ # @param current_path [String] Current directory path
16
+ # @return [Symbol, nil] Action to perform (:navigate, :add, :list, :remove, :cancel)
17
+ def show_menu(current_path)
18
+ return :cancel unless @dialog_renderer
19
+
20
+ title = 'Bookmark Menu'
21
+ content_lines = [
22
+ '',
23
+ '[A]dd current directory to bookmarks',
24
+ '[L]ist bookmarks',
25
+ '[R]emove bookmark',
26
+ '',
27
+ 'Press 1-9 to go to bookmark directly',
28
+ '',
29
+ 'Press any other key to cancel'
30
+ ]
31
+
32
+ dialog_width = 45
33
+ dialog_height = 4 + content_lines.length
34
+ x, y = @dialog_renderer.calculate_center(dialog_width, dialog_height)
35
+
36
+ @dialog_renderer.draw_floating_window(x, y, dialog_width, dialog_height, title, content_lines, {
37
+ border_color: "\e[34m", # Blue
38
+ title_color: "\e[1;34m", # Bold blue
39
+ content_color: "\e[37m" # White
40
+ })
41
+
42
+ # Wait for key input
43
+ loop do
44
+ input = STDIN.getch.downcase
45
+
46
+ case input
47
+ when 'a'
48
+ @dialog_renderer.clear_area(x, y, dialog_width, dialog_height)
49
+ return { action: :add, path: current_path }
50
+ when 'l'
51
+ @dialog_renderer.clear_area(x, y, dialog_width, dialog_height)
52
+ return { action: :list }
53
+ when 'r'
54
+ @dialog_renderer.clear_area(x, y, dialog_width, dialog_height)
55
+ return { action: :remove }
56
+ when '1', '2', '3', '4', '5', '6', '7', '8', '9'
57
+ @dialog_renderer.clear_area(x, y, dialog_width, dialog_height)
58
+ return { action: :navigate, number: input.to_i }
59
+ else
60
+ # Cancel
61
+ @dialog_renderer.clear_area(x, y, dialog_width, dialog_height)
62
+ return { action: :cancel }
63
+ end
64
+ end
65
+ end
66
+
67
+ # Add a bookmark interactively
68
+ # @param path [String] Path to bookmark
69
+ # @return [Boolean] Success status
70
+ def add_interactive(path)
71
+ print ConfigLoader.message('bookmark.input_name') || 'Enter bookmark name: '
72
+ name = STDIN.gets.chomp
73
+ return false if name.empty?
74
+
75
+ if @bookmark.add(path, name)
76
+ puts "\n#{ConfigLoader.message('bookmark.added') || 'Bookmark added'}: #{name}"
77
+ true
78
+ else
79
+ puts "\n#{ConfigLoader.message('bookmark.add_failed') || 'Failed to add bookmark'}"
80
+ false
81
+ end
82
+ end
83
+
84
+ # Remove a bookmark interactively
85
+ # @return [Boolean] Success status
86
+ def remove_interactive
87
+ bookmarks = @bookmark.list
88
+
89
+ if bookmarks.empty?
90
+ puts "\n#{ConfigLoader.message('bookmark.no_bookmarks') || 'No bookmarks found'}"
91
+ return false
92
+ end
93
+
94
+ puts "\nBookmarks:"
95
+ bookmarks.each_with_index do |bookmark, index|
96
+ puts " #{index + 1}. #{bookmark[:name]} (#{bookmark[:path]})"
97
+ end
98
+
99
+ print ConfigLoader.message('bookmark.input_number') || 'Enter number to remove: '
100
+ input = STDIN.gets.chomp
101
+ number = input.to_i
102
+
103
+ if number > 0 && number <= bookmarks.length
104
+ bookmark_to_remove = bookmarks[number - 1]
105
+ if @bookmark.remove(bookmark_to_remove[:name])
106
+ puts "\n#{ConfigLoader.message('bookmark.removed') || 'Bookmark removed'}: #{bookmark_to_remove[:name]}"
107
+ true
108
+ else
109
+ puts "\n#{ConfigLoader.message('bookmark.remove_failed') || 'Failed to remove bookmark'}"
110
+ false
111
+ end
112
+ else
113
+ puts "\n#{ConfigLoader.message('bookmark.invalid_number') || 'Invalid number'}"
114
+ false
115
+ end
116
+ end
117
+
118
+ # List all bookmarks interactively
119
+ # @return [Boolean] Success status
120
+ def list_interactive
121
+ bookmarks = @bookmark.list
122
+
123
+ if bookmarks.empty?
124
+ puts "\n#{ConfigLoader.message('bookmark.no_bookmarks') || 'No bookmarks found'}"
125
+ return false
126
+ end
127
+
128
+ puts "\nBookmarks:"
129
+ bookmarks.each_with_index do |bookmark, index|
130
+ puts " #{index + 1}. #{bookmark[:name]} (#{bookmark[:path]})"
131
+ end
132
+
133
+ true
134
+ end
135
+
136
+ # Get bookmark by number
137
+ # @param number [Integer] Bookmark number (1-9)
138
+ # @return [Hash, nil] Bookmark hash with :path and :name
139
+ def find_by_number(number)
140
+ @bookmark.find_by_number(number)
141
+ end
142
+
143
+ # Validate bookmark path exists
144
+ # @param bookmark [Hash] Bookmark hash
145
+ # @return [Boolean]
146
+ def path_exists?(bookmark)
147
+ return false unless bookmark
148
+
149
+ Dir.exist?(bookmark[:path])
150
+ end
151
+
152
+ # Get all bookmarks
153
+ # @return [Array<Hash>]
154
+ def list
155
+ @bookmark.list
156
+ end
157
+
158
+ # Add bookmark
159
+ # @param path [String] Path to bookmark
160
+ # @param name [String] Bookmark name
161
+ # @return [Boolean] Success status
162
+ def add(path, name)
163
+ @bookmark.add(path, name)
164
+ end
165
+
166
+ # Remove bookmark
167
+ # @param name [String] Bookmark name
168
+ # @return [Boolean] Success status
169
+ def remove(name)
170
+ @bookmark.remove(name)
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rufio
4
+ class ColorHelper
5
+ # HSLからRGBへの変換
6
+ def self.hsl_to_rgb(hue, saturation, lightness)
7
+ h = hue.to_f / 360.0
8
+ s = saturation.to_f / 100.0
9
+ l = lightness.to_f / 100.0
10
+
11
+ if s == 0
12
+ # 彩度が0の場合(グレースケール)
13
+ r = g = b = l
14
+ else
15
+ hue2rgb = lambda do |p, q, t|
16
+ t += 1 if t < 0
17
+ t -= 1 if t > 1
18
+ return p + (q - p) * 6 * t if t < 1.0/6
19
+ return q if t < 1.0/2
20
+ return p + (q - p) * (2.0/3 - t) * 6 if t < 2.0/3
21
+ p
22
+ end
23
+
24
+ q = l < 0.5 ? l * (1 + s) : l + s - l * s
25
+ p = 2 * l - q
26
+
27
+ r = hue2rgb.call(p, q, h + 1.0/3)
28
+ g = hue2rgb.call(p, q, h)
29
+ b = hue2rgb.call(p, q, h - 1.0/3)
30
+ end
31
+
32
+ [(r * 255).round, (g * 255).round, (b * 255).round]
33
+ end
34
+
35
+ # 色設定をANSIエスケープコードに変換
36
+ def self.color_to_ansi(color_config)
37
+ case color_config
38
+ when Hash
39
+ if color_config[:hsl]
40
+ # HSL形式: {hsl: [240, 100, 50]}
41
+ hue, saturation, lightness = color_config[:hsl]
42
+ r, g, b = hsl_to_rgb(hue, saturation, lightness)
43
+ "\e[38;2;#{r};#{g};#{b}m"
44
+ elsif color_config[:rgb]
45
+ # RGB形式: {rgb: [100, 150, 200]}
46
+ r, g, b = color_config[:rgb]
47
+ "\e[38;2;#{r};#{g};#{b}m"
48
+ elsif color_config[:hex]
49
+ # HEX形式: {hex: "#ff0000"}
50
+ hex = color_config[:hex].gsub('#', '')
51
+ r = hex[0..1].to_i(16)
52
+ g = hex[2..3].to_i(16)
53
+ b = hex[4..5].to_i(16)
54
+ "\e[38;2;#{r};#{g};#{b}m"
55
+ else
56
+ # デフォルト(白)
57
+ "\e[37m"
58
+ end
59
+ when Symbol
60
+ # 従来のシンボル形式をANSIコードに変換
61
+ symbol_to_ansi(color_config)
62
+ when String
63
+ # 直接ANSIコードまたは名前が指定された場合
64
+ if color_config.match?(/^\d+$/)
65
+ "\e[#{color_config}m"
66
+ else
67
+ name_to_ansi(color_config)
68
+ end
69
+ when Integer
70
+ # 数値が直接指定された場合
71
+ "\e[#{color_config}m"
72
+ else
73
+ # デフォルト(白)
74
+ "\e[37m"
75
+ end
76
+ end
77
+
78
+ # シンボルをANSIコードに変換
79
+ def self.symbol_to_ansi(symbol)
80
+ case symbol
81
+ when :black then "\e[30m"
82
+ when :red then "\e[31m"
83
+ when :green then "\e[32m"
84
+ when :yellow then "\e[33m"
85
+ when :blue then "\e[34m"
86
+ when :magenta then "\e[35m"
87
+ when :cyan then "\e[36m"
88
+ when :white then "\e[37m"
89
+ when :bright_black then "\e[90m"
90
+ when :bright_red then "\e[91m"
91
+ when :bright_green then "\e[92m"
92
+ when :bright_yellow then "\e[93m"
93
+ when :bright_blue then "\e[94m"
94
+ when :bright_magenta then "\e[95m"
95
+ when :bright_cyan then "\e[96m"
96
+ when :bright_white then "\e[97m"
97
+ else "\e[37m" # デフォルト(白)
98
+ end
99
+ end
100
+
101
+ # 色名をANSIコードに変換
102
+ def self.name_to_ansi(name)
103
+ symbol_to_ansi(name.to_sym)
104
+ end
105
+
106
+ # 背景色用のANSIコードを生成
107
+ def self.color_to_bg_ansi(color_config)
108
+ ansi_code = color_to_ansi(color_config)
109
+ # 前景色(38)を背景色(48)に変換
110
+ ansi_code.gsub('38;', '48;')
111
+ end
112
+
113
+ # リセットコード
114
+ def self.reset
115
+ "\e[0m"
116
+ end
117
+
118
+ # 選択状態(反転表示)用のANSIコードを生成
119
+ def self.color_to_selected_ansi(color_config)
120
+ color_code = color_to_ansi(color_config)
121
+ # 反転表示を追加
122
+ color_code.gsub("\e[", "\e[7;").gsub("m", ";7m")
123
+ end
124
+
125
+ # プリセットHSLカラー
126
+ def self.preset_hsl_colors
127
+ {
128
+ # ディレクトリ用の青系
129
+ directory_blue: { hsl: [220, 80, 60] },
130
+ directory_cyan: { hsl: [180, 70, 55] },
131
+
132
+ # 実行ファイル用の緑系
133
+ executable_green: { hsl: [120, 70, 50] },
134
+ executable_lime: { hsl: [90, 80, 55] },
135
+
136
+ # テキストファイル用
137
+ text_white: { hsl: [0, 0, 90] },
138
+ text_gray: { hsl: [0, 0, 70] },
139
+
140
+ # 選択状態用
141
+ selected_yellow: { hsl: [50, 90, 70] },
142
+ selected_orange: { hsl: [30, 85, 65] },
143
+
144
+ # プレビュー用
145
+ preview_cyan: { hsl: [180, 60, 65] },
146
+ preview_purple: { hsl: [270, 50, 70] }
147
+ }
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rufio
4
+ # コマンドモード - プラグインコマンドを実行するためのインターフェース
5
+ class CommandMode
6
+ def initialize
7
+ @commands = {}
8
+ load_plugin_commands
9
+ end
10
+
11
+ # コマンドを実行する
12
+ def execute(command_string)
13
+ # 空のコマンドは無視
14
+ return nil if command_string.nil? || command_string.strip.empty?
15
+
16
+ # コマンド名を取得 (前後の空白を削除)
17
+ command_name = command_string.strip.to_sym
18
+
19
+ # コマンドが存在するかチェック
20
+ unless @commands.key?(command_name)
21
+ return "⚠️ コマンドが見つかりません: #{command_name}"
22
+ end
23
+
24
+ # コマンドを実行
25
+ begin
26
+ command_method = @commands[command_name][:method]
27
+ command_method.call
28
+ rescue StandardError => e
29
+ "⚠️ コマンド実行エラー: #{e.message}"
30
+ end
31
+ end
32
+
33
+ # 利用可能なコマンドのリストを取得
34
+ def available_commands
35
+ @commands.keys
36
+ end
37
+
38
+ # コマンドの情報を取得
39
+ def command_info(command_name)
40
+ return nil unless @commands.key?(command_name)
41
+
42
+ {
43
+ name: command_name,
44
+ plugin: @commands[command_name][:plugin],
45
+ description: @commands[command_name][:description]
46
+ }
47
+ end
48
+
49
+ private
50
+
51
+ # プラグインからコマンドを読み込む
52
+ def load_plugin_commands
53
+ # 有効なプラグインを取得
54
+ enabled_plugins = PluginManager.enabled_plugins
55
+
56
+ # 各プラグインからコマンドを取得
57
+ enabled_plugins.each do |plugin|
58
+ plugin_name = plugin.name
59
+ plugin_commands = plugin.commands
60
+
61
+ # 各コマンドを登録
62
+ plugin_commands.each do |command_name, command_method|
63
+ @commands[command_name] = {
64
+ method: command_method,
65
+ plugin: plugin_name,
66
+ description: plugin.description
67
+ }
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'io/console'
4
+
5
+ module Rufio
6
+ # コマンドモードのUI - Tab補完とフローティングウィンドウでの結果表示
7
+ class CommandModeUI
8
+ def initialize(command_mode, dialog_renderer)
9
+ @command_mode = command_mode
10
+ @dialog_renderer = dialog_renderer
11
+ end
12
+
13
+ # 入力文字列に対する補完候補を取得
14
+ # @param input [String] 現在の入力文字列
15
+ # @return [Array<String>] 補完候補の配列
16
+ def autocomplete(input)
17
+ # 利用可能なコマンド一覧を取得
18
+ available = @command_mode.available_commands.map(&:to_s)
19
+
20
+ # 入力が空の場合は全てのコマンドを返す
21
+ return available if input.empty?
22
+
23
+ # 入力に一致するコマンドをフィルタリング
24
+ available.select { |cmd| cmd.start_with?(input) }
25
+ end
26
+
27
+ # コマンドを補完する
28
+ # @param input [String] 現在の入力文字列
29
+ # @return [String] 補完後の文字列
30
+ def complete_command(input)
31
+ suggestions = autocomplete(input)
32
+
33
+ # マッチするものがない場合は元の入力を返す
34
+ return input if suggestions.empty?
35
+
36
+ # 一つだけマッチする場合はそれを返す
37
+ return suggestions.first if suggestions.length == 1
38
+
39
+ # 複数マッチする場合は共通プレフィックスを返す
40
+ find_common_prefix(suggestions)
41
+ end
42
+
43
+ # コマンド入力プロンプトをフローティングウィンドウで表示
44
+ # @param input [String] 現在の入力文字列
45
+ # @param suggestions [Array<String>] 補完候補(オプション)
46
+ def show_input_prompt(input, suggestions = [])
47
+ # タイトル
48
+ title = "コマンドモード"
49
+
50
+ # コンテンツ行を構築
51
+ content_lines = [""]
52
+ content_lines << "#{input}_" # カーソルを_で表現
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
+ content_lines << "Tab: 補完 | Enter: 実行 | ESC: キャンセル"
65
+
66
+ # ウィンドウの色設定(青)
67
+ border_color = "\e[34m" # Blue
68
+ title_color = "\e[1;34m" # Bold blue
69
+ content_color = "\e[37m" # White
70
+
71
+ # ウィンドウサイズを計算
72
+ width, height = @dialog_renderer.calculate_dimensions(content_lines, {
73
+ title: title,
74
+ min_width: 50,
75
+ max_width: 80
76
+ })
77
+
78
+ # 中央位置を計算
79
+ x, y = @dialog_renderer.calculate_center(width, height)
80
+
81
+ # フローティングウィンドウを描画
82
+ @dialog_renderer.draw_floating_window(x, y, width, height, title, content_lines, {
83
+ border_color: border_color,
84
+ title_color: title_color,
85
+ content_color: content_color
86
+ })
87
+ end
88
+
89
+ # コマンド実行結果をフローティングウィンドウで表示
90
+ # @param result [String, nil] コマンド実行結果
91
+ def show_result(result)
92
+ # nil または空文字列の場合は何も表示しない
93
+ return if result.nil? || result.empty?
94
+
95
+ # 結果を行に分割
96
+ result_lines = result.split("\n")
97
+
98
+ # エラーメッセージかどうかを判定
99
+ is_error = result.include?("⚠️") || result.include?("エラー")
100
+
101
+ # ウィンドウの色設定
102
+ if is_error
103
+ border_color = "\e[31m" # Red
104
+ title_color = "\e[1;31m" # Bold red
105
+ content_color = "\e[37m" # White
106
+ else
107
+ border_color = "\e[32m" # Green
108
+ title_color = "\e[1;32m" # Bold green
109
+ content_color = "\e[37m" # White
110
+ end
111
+
112
+ # ウィンドウタイトル
113
+ title = "コマンド実行結果"
114
+
115
+ # コンテンツ行を構築
116
+ content_lines = [""] + result_lines + ["", "Press any key to close"]
117
+
118
+ # ウィンドウサイズを計算
119
+ width, height = @dialog_renderer.calculate_dimensions(content_lines, {
120
+ title: title,
121
+ min_width: 40,
122
+ max_width: 100
123
+ })
124
+
125
+ # 中央位置を計算
126
+ x, y = @dialog_renderer.calculate_center(width, height)
127
+
128
+ # フローティングウィンドウを描画
129
+ @dialog_renderer.draw_floating_window(x, y, width, height, title, content_lines, {
130
+ border_color: border_color,
131
+ title_color: title_color,
132
+ content_color: content_color
133
+ })
134
+
135
+ # キー入力を待つ
136
+ STDIN.getch
137
+
138
+ # ウィンドウをクリア
139
+ @dialog_renderer.clear_area(x, y, width, height)
140
+ end
141
+
142
+ private
143
+
144
+ # 文字列配列の共通プレフィックスを見つける
145
+ # @param strings [Array<String>] 文字列配列
146
+ # @return [String] 共通プレフィックス
147
+ def find_common_prefix(strings)
148
+ return "" if strings.empty?
149
+ return strings.first if strings.length == 1
150
+
151
+ # 最短の文字列の長さを取得
152
+ min_length = strings.map(&:length).min
153
+
154
+ # 各文字位置で全ての文字列が同じ文字を持っているかチェック
155
+ common_length = 0
156
+ min_length.times do |i|
157
+ char = strings.first[i]
158
+ if strings.all? { |s| s[i] == char }
159
+ common_length = i + 1
160
+ else
161
+ break
162
+ end
163
+ end
164
+
165
+ strings.first[0...common_length]
166
+ end
167
+ end
168
+ end