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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +188 -0
- data/CHANGELOG_v0.4.0.md +146 -0
- data/CHANGELOG_v0.5.0.md +26 -0
- data/CHANGELOG_v0.6.0.md +182 -0
- data/CHANGELOG_v0.7.0.md +280 -0
- data/CHANGELOG_v0.8.0.md +267 -0
- data/CHANGELOG_v0.9.0.md +279 -0
- data/README.md +631 -0
- data/README_EN.md +561 -0
- data/Rakefile +156 -0
- data/bin/rufio +34 -0
- data/config_example.rb +88 -0
- data/docs/PLUGIN_GUIDE.md +431 -0
- data/docs/plugin_example.rb +119 -0
- data/lib/rufio/application.rb +32 -0
- data/lib/rufio/bookmark.rb +115 -0
- data/lib/rufio/bookmark_manager.rb +173 -0
- data/lib/rufio/color_helper.rb +150 -0
- data/lib/rufio/command_mode.rb +72 -0
- data/lib/rufio/command_mode_ui.rb +168 -0
- data/lib/rufio/config.rb +199 -0
- data/lib/rufio/config_loader.rb +110 -0
- data/lib/rufio/dialog_renderer.rb +127 -0
- data/lib/rufio/directory_listing.rb +113 -0
- data/lib/rufio/file_opener.rb +140 -0
- data/lib/rufio/file_operations.rb +231 -0
- data/lib/rufio/file_preview.rb +200 -0
- data/lib/rufio/filter_manager.rb +114 -0
- data/lib/rufio/health_checker.rb +246 -0
- data/lib/rufio/keybind_handler.rb +828 -0
- data/lib/rufio/logger.rb +103 -0
- data/lib/rufio/plugin.rb +89 -0
- data/lib/rufio/plugin_config.rb +59 -0
- data/lib/rufio/plugin_manager.rb +84 -0
- data/lib/rufio/plugins/file_operations.rb +44 -0
- data/lib/rufio/selection_manager.rb +79 -0
- data/lib/rufio/terminal_ui.rb +630 -0
- data/lib/rufio/text_utils.rb +108 -0
- data/lib/rufio/version.rb +5 -0
- data/lib/rufio/zoxide_integration.rb +188 -0
- data/lib/rufio.rb +33 -0
- data/publish_gem.zsh +131 -0
- data/rufio.gemspec +40 -0
- data/test_delete/test1.txt +1 -0
- data/test_delete/test2.txt +1 -0
- 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
|