beniya 0.1.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/README.md +288 -0
- data/README_EN.md +289 -0
- data/beniya.gemspec +40 -0
- data/bin/beniya +31 -0
- data/config_example.rb +88 -0
- data/lib/beniya/application.rb +28 -0
- data/lib/beniya/color_helper.rb +150 -0
- data/lib/beniya/config.rb +179 -0
- data/lib/beniya/config_loader.rb +110 -0
- data/lib/beniya/directory_listing.rb +99 -0
- data/lib/beniya/file_opener.rb +114 -0
- data/lib/beniya/file_preview.rb +172 -0
- data/lib/beniya/health_checker.rb +224 -0
- data/lib/beniya/keybind_handler.rb +332 -0
- data/lib/beniya/terminal_ui.rb +469 -0
- data/lib/beniya/version.rb +5 -0
- data/lib/beniya.rb +17 -0
- metadata +161 -0
@@ -0,0 +1,114 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Beniya
|
4
|
+
class FileOpener
|
5
|
+
def initialize
|
6
|
+
@config_loader = ConfigLoader
|
7
|
+
end
|
8
|
+
|
9
|
+
def open_file(file_path)
|
10
|
+
return false unless File.exist?(file_path)
|
11
|
+
return false if File.directory?(file_path)
|
12
|
+
|
13
|
+
application = find_application_for_file(file_path)
|
14
|
+
execute_command(application, file_path)
|
15
|
+
end
|
16
|
+
|
17
|
+
def open_file_with_line(file_path, line_number)
|
18
|
+
return false unless File.exist?(file_path)
|
19
|
+
return false if File.directory?(file_path)
|
20
|
+
|
21
|
+
application = find_application_for_file(file_path)
|
22
|
+
execute_command_with_line(application, file_path, line_number)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def find_application_for_file(file_path)
|
28
|
+
extension = File.extname(file_path).downcase.sub('.', '')
|
29
|
+
applications = @config_loader.applications
|
30
|
+
|
31
|
+
applications.each do |extensions, app|
|
32
|
+
return app if extensions.is_a?(Array) && extensions.include?(extension)
|
33
|
+
end
|
34
|
+
|
35
|
+
applications[:default] || 'open'
|
36
|
+
end
|
37
|
+
|
38
|
+
def execute_command(application, file_path)
|
39
|
+
quoted_path = quote_shell_argument(file_path)
|
40
|
+
|
41
|
+
case RbConfig::CONFIG['host_os']
|
42
|
+
when /mswin|mingw|cygwin/
|
43
|
+
# Windows
|
44
|
+
system("start \"\" \"#{file_path}\"")
|
45
|
+
when /darwin/
|
46
|
+
# macOS
|
47
|
+
if application == 'open'
|
48
|
+
system("open #{quoted_path}")
|
49
|
+
else
|
50
|
+
# VSCodeなど特定のアプリケーション
|
51
|
+
system("#{application} #{quoted_path}")
|
52
|
+
end
|
53
|
+
else
|
54
|
+
# Linux/Unix
|
55
|
+
if application == 'open'
|
56
|
+
system("xdg-open #{quoted_path}")
|
57
|
+
else
|
58
|
+
system("#{application} #{quoted_path}")
|
59
|
+
end
|
60
|
+
end
|
61
|
+
rescue StandardError => e
|
62
|
+
warn "ファイルを開けませんでした: #{e.message}"
|
63
|
+
false
|
64
|
+
end
|
65
|
+
|
66
|
+
def execute_command_with_line(application, file_path, line_number)
|
67
|
+
quoted_path = quote_shell_argument(file_path)
|
68
|
+
|
69
|
+
case RbConfig::CONFIG['host_os']
|
70
|
+
when /mswin|mingw|cygwin/
|
71
|
+
# Windows
|
72
|
+
if application.include?('code')
|
73
|
+
system("#{application} --goto #{quoted_path}:#{line_number}")
|
74
|
+
else
|
75
|
+
system("start \"\" \"#{file_path}\"")
|
76
|
+
end
|
77
|
+
when /darwin/
|
78
|
+
# macOS
|
79
|
+
if application == 'open'
|
80
|
+
system("open #{quoted_path}")
|
81
|
+
elsif application.include?('code')
|
82
|
+
system("#{application} --goto #{quoted_path}:#{line_number}")
|
83
|
+
elsif application.include?('vim') || application.include?('nvim')
|
84
|
+
system("#{application} +#{line_number} #{quoted_path}")
|
85
|
+
else
|
86
|
+
system("#{application} #{quoted_path}")
|
87
|
+
end
|
88
|
+
else
|
89
|
+
# Linux/Unix
|
90
|
+
if application == 'open'
|
91
|
+
system("xdg-open #{quoted_path}")
|
92
|
+
elsif application.include?('code')
|
93
|
+
system("#{application} --goto #{quoted_path}:#{line_number}")
|
94
|
+
elsif application.include?('vim') || application.include?('nvim')
|
95
|
+
system("#{application} +#{line_number} #{quoted_path}")
|
96
|
+
else
|
97
|
+
system("#{application} #{quoted_path}")
|
98
|
+
end
|
99
|
+
end
|
100
|
+
rescue StandardError => e
|
101
|
+
warn "ファイルを開けませんでした: #{e.message}"
|
102
|
+
false
|
103
|
+
end
|
104
|
+
|
105
|
+
def quote_shell_argument(argument)
|
106
|
+
if argument.include?(' ') || argument.include?("'") || argument.include?('"')
|
107
|
+
'"' + argument.gsub('"', '\"') + '"'
|
108
|
+
else
|
109
|
+
argument
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
@@ -0,0 +1,172 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Beniya
|
4
|
+
class FilePreview
|
5
|
+
BINARY_THRESHOLD = 0.3 # treat as binary if 30% or more binary characters
|
6
|
+
DEFAULT_MAX_LINES = 50
|
7
|
+
MAX_LINE_LENGTH = 500
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
# future: hold syntax highlight settings etc.
|
11
|
+
end
|
12
|
+
|
13
|
+
def preview_file(file_path, max_lines: DEFAULT_MAX_LINES)
|
14
|
+
return error_response(ConfigLoader.message('file.not_found')) unless File.exist?(file_path)
|
15
|
+
return error_response(ConfigLoader.message('file.not_readable')) unless File.readable?(file_path)
|
16
|
+
|
17
|
+
file_size = File.size(file_path)
|
18
|
+
return empty_response if file_size == 0
|
19
|
+
|
20
|
+
begin
|
21
|
+
# binary file detection
|
22
|
+
sample = File.read(file_path, [file_size, 512].min)
|
23
|
+
return binary_response if binary_file?(sample)
|
24
|
+
|
25
|
+
# process as text file
|
26
|
+
lines = read_text_file(file_path, max_lines)
|
27
|
+
file_type = determine_file_type(file_path)
|
28
|
+
|
29
|
+
{
|
30
|
+
type: file_type[:type],
|
31
|
+
language: file_type[:language],
|
32
|
+
lines: lines[:content],
|
33
|
+
truncated: lines[:truncated],
|
34
|
+
size: file_size,
|
35
|
+
modified: File.mtime(file_path),
|
36
|
+
encoding: lines[:encoding]
|
37
|
+
}
|
38
|
+
rescue => e
|
39
|
+
error_response("#{ConfigLoader.message('file.read_error')}: #{e.message}")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def binary_file?(sample)
|
46
|
+
return false if sample.empty?
|
47
|
+
|
48
|
+
binary_chars = sample.bytes.count { |byte| byte < 32 && ![9, 10, 13].include?(byte) }
|
49
|
+
(binary_chars.to_f / sample.bytes.length) > BINARY_THRESHOLD
|
50
|
+
end
|
51
|
+
|
52
|
+
def read_text_file(file_path, max_lines)
|
53
|
+
lines = []
|
54
|
+
truncated = false
|
55
|
+
encoding = "UTF-8"
|
56
|
+
|
57
|
+
File.open(file_path, "r:UTF-8") do |file|
|
58
|
+
file.each_line.with_index do |line, index|
|
59
|
+
break if index >= max_lines
|
60
|
+
|
61
|
+
# truncate too long lines
|
62
|
+
if line.length > MAX_LINE_LENGTH
|
63
|
+
line = line[0...MAX_LINE_LENGTH] + "..."
|
64
|
+
end
|
65
|
+
|
66
|
+
lines << line.chomp
|
67
|
+
end
|
68
|
+
|
69
|
+
# check if there are more lines to read
|
70
|
+
truncated = !file.eof?
|
71
|
+
end
|
72
|
+
|
73
|
+
{
|
74
|
+
content: lines,
|
75
|
+
truncated: truncated,
|
76
|
+
encoding: encoding
|
77
|
+
}
|
78
|
+
rescue Encoding::InvalidByteSequenceError
|
79
|
+
# try Shift_JIS if UTF-8 fails
|
80
|
+
begin
|
81
|
+
lines = []
|
82
|
+
File.open(file_path, "r:Shift_JIS:UTF-8") do |file|
|
83
|
+
file.each_line.with_index do |line, index|
|
84
|
+
break if index >= max_lines
|
85
|
+
lines << line.chomp
|
86
|
+
end
|
87
|
+
truncated = !file.eof?
|
88
|
+
end
|
89
|
+
{
|
90
|
+
content: lines,
|
91
|
+
truncated: truncated,
|
92
|
+
encoding: "Shift_JIS"
|
93
|
+
}
|
94
|
+
rescue
|
95
|
+
{
|
96
|
+
content: ["(#{ConfigLoader.message('file.encoding_error')})"],
|
97
|
+
truncated: false,
|
98
|
+
encoding: "unknown"
|
99
|
+
}
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def determine_file_type(file_path)
|
104
|
+
extension = File.extname(file_path).downcase
|
105
|
+
|
106
|
+
case extension
|
107
|
+
when ".rb"
|
108
|
+
{ type: "code", language: "ruby" }
|
109
|
+
when ".py"
|
110
|
+
{ type: "code", language: "python" }
|
111
|
+
when ".js", ".mjs"
|
112
|
+
{ type: "code", language: "javascript" }
|
113
|
+
when ".ts"
|
114
|
+
{ type: "code", language: "typescript" }
|
115
|
+
when ".html", ".htm"
|
116
|
+
{ type: "code", language: "html" }
|
117
|
+
when ".css"
|
118
|
+
{ type: "code", language: "css" }
|
119
|
+
when ".json"
|
120
|
+
{ type: "code", language: "json" }
|
121
|
+
when ".yml", ".yaml"
|
122
|
+
{ type: "code", language: "yaml" }
|
123
|
+
when ".md", ".markdown"
|
124
|
+
{ type: "code", language: "markdown" }
|
125
|
+
when ".txt", ".log"
|
126
|
+
{ type: "text", language: nil }
|
127
|
+
else
|
128
|
+
{ type: "text", language: nil }
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def empty_response
|
133
|
+
{
|
134
|
+
type: "empty",
|
135
|
+
lines: [],
|
136
|
+
size: 0,
|
137
|
+
modified: File.mtime(""),
|
138
|
+
encoding: "UTF-8"
|
139
|
+
}
|
140
|
+
rescue
|
141
|
+
{
|
142
|
+
type: "empty",
|
143
|
+
lines: [],
|
144
|
+
size: 0,
|
145
|
+
modified: Time.now,
|
146
|
+
encoding: "UTF-8"
|
147
|
+
}
|
148
|
+
end
|
149
|
+
|
150
|
+
def binary_response
|
151
|
+
{
|
152
|
+
type: "binary",
|
153
|
+
message: "#{ConfigLoader.message('file.binary_file')} - #{ConfigLoader.message('file.cannot_preview')}",
|
154
|
+
lines: ["(#{ConfigLoader.message('file.binary_file')})"],
|
155
|
+
size: 0,
|
156
|
+
modified: Time.now,
|
157
|
+
encoding: "binary"
|
158
|
+
}
|
159
|
+
end
|
160
|
+
|
161
|
+
def error_response(message)
|
162
|
+
{
|
163
|
+
type: "error",
|
164
|
+
message: message,
|
165
|
+
lines: ["#{ConfigLoader.message('file.error_prefix')}: #{message}"],
|
166
|
+
size: 0,
|
167
|
+
modified: Time.now,
|
168
|
+
encoding: "UTF-8"
|
169
|
+
}
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
@@ -0,0 +1,224 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pastel'
|
4
|
+
|
5
|
+
module Beniya
|
6
|
+
class HealthChecker
|
7
|
+
def initialize
|
8
|
+
@pastel = Pastel.new
|
9
|
+
# Load configuration including language settings
|
10
|
+
ConfigLoader.load_config
|
11
|
+
end
|
12
|
+
|
13
|
+
def run_check
|
14
|
+
puts @pastel.bold(ConfigLoader.message('health.title'))
|
15
|
+
puts "=" * 40
|
16
|
+
puts
|
17
|
+
|
18
|
+
checks = [
|
19
|
+
{ name: ConfigLoader.message('health.ruby_version'), method: :check_ruby_version },
|
20
|
+
{ name: ConfigLoader.message('health.required_gems'), method: :check_required_gems },
|
21
|
+
{ name: ConfigLoader.message('health.fzf'), method: :check_fzf },
|
22
|
+
{ name: ConfigLoader.message('health.rga'), method: :check_rga },
|
23
|
+
{ name: ConfigLoader.message('health.file_opener'), method: :check_file_opener }
|
24
|
+
]
|
25
|
+
|
26
|
+
results = []
|
27
|
+
checks.each do |check|
|
28
|
+
result = send(check[:method])
|
29
|
+
results << result
|
30
|
+
print_check_result(check[:name], result)
|
31
|
+
end
|
32
|
+
|
33
|
+
puts
|
34
|
+
print_summary(results)
|
35
|
+
|
36
|
+
results.all? { |r| r[:status] == :ok }
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def check_ruby_version
|
42
|
+
version = RUBY_VERSION
|
43
|
+
major, minor = version.split('.').map(&:to_i)
|
44
|
+
|
45
|
+
if major > 2 || (major == 2 && minor >= 7)
|
46
|
+
{
|
47
|
+
status: :ok,
|
48
|
+
message: "Ruby #{version}",
|
49
|
+
details: nil
|
50
|
+
}
|
51
|
+
else
|
52
|
+
{
|
53
|
+
status: :error,
|
54
|
+
message: "Ruby #{version} (requires >= 2.7.0)",
|
55
|
+
details: ConfigLoader.message('health.ruby_upgrade_needed')
|
56
|
+
}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def check_required_gems
|
61
|
+
required_gems = %w[io-console pastel tty-cursor tty-screen]
|
62
|
+
missing_gems = []
|
63
|
+
|
64
|
+
required_gems.each do |gem_name|
|
65
|
+
begin
|
66
|
+
require gem_name.gsub('-', '/')
|
67
|
+
rescue LoadError
|
68
|
+
missing_gems << gem_name
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
if missing_gems.empty?
|
73
|
+
{
|
74
|
+
status: :ok,
|
75
|
+
message: ConfigLoader.message('health.all_gems_installed'),
|
76
|
+
details: nil
|
77
|
+
}
|
78
|
+
else
|
79
|
+
{
|
80
|
+
status: :error,
|
81
|
+
message: "#{ConfigLoader.message('health.missing_gems')}: #{missing_gems.join(', ')}",
|
82
|
+
details: "#{ConfigLoader.message('health.gem_install_instruction')} #{missing_gems.join(' ')}"
|
83
|
+
}
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def check_fzf
|
88
|
+
if system("which fzf > /dev/null 2>&1")
|
89
|
+
version = `fzf --version 2>/dev/null`.strip
|
90
|
+
{
|
91
|
+
status: :ok,
|
92
|
+
message: "fzf #{version}",
|
93
|
+
details: nil
|
94
|
+
}
|
95
|
+
else
|
96
|
+
{
|
97
|
+
status: :warning,
|
98
|
+
message: "fzf #{ConfigLoader.message('health.tool_not_found')}",
|
99
|
+
details: install_instruction_for('fzf')
|
100
|
+
}
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def check_rga
|
105
|
+
if system("which rga > /dev/null 2>&1")
|
106
|
+
version = `rga --version 2>/dev/null | head -1`.strip
|
107
|
+
{
|
108
|
+
status: :ok,
|
109
|
+
message: version,
|
110
|
+
details: nil
|
111
|
+
}
|
112
|
+
else
|
113
|
+
{
|
114
|
+
status: :warning,
|
115
|
+
message: "rga #{ConfigLoader.message('health.tool_not_found')}",
|
116
|
+
details: install_instruction_for('rga')
|
117
|
+
}
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def check_file_opener
|
122
|
+
case RUBY_PLATFORM
|
123
|
+
when /darwin/
|
124
|
+
opener = "open"
|
125
|
+
description = ConfigLoader.message('health.macos_opener')
|
126
|
+
when /linux/
|
127
|
+
opener = "xdg-open"
|
128
|
+
description = ConfigLoader.message('health.linux_opener')
|
129
|
+
when /mswin|mingw|cygwin/
|
130
|
+
opener = "explorer"
|
131
|
+
description = ConfigLoader.message('health.windows_opener')
|
132
|
+
else
|
133
|
+
return {
|
134
|
+
status: :warning,
|
135
|
+
message: "#{ConfigLoader.message('health.unknown_platform')}: #{RUBY_PLATFORM}",
|
136
|
+
details: ConfigLoader.message('health.file_open_may_not_work')
|
137
|
+
}
|
138
|
+
end
|
139
|
+
|
140
|
+
if system("which #{opener} > /dev/null 2>&1") || RUBY_PLATFORM =~ /mswin|mingw|cygwin/
|
141
|
+
{
|
142
|
+
status: :ok,
|
143
|
+
message: description,
|
144
|
+
details: nil
|
145
|
+
}
|
146
|
+
else
|
147
|
+
{
|
148
|
+
status: :warning,
|
149
|
+
message: "#{opener} #{ConfigLoader.message('health.tool_not_found')}",
|
150
|
+
details: ConfigLoader.message('health.file_open_may_not_work')
|
151
|
+
}
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def install_instruction_for(tool)
|
156
|
+
case RUBY_PLATFORM
|
157
|
+
when /darwin/
|
158
|
+
case tool
|
159
|
+
when 'fzf'
|
160
|
+
"#{ConfigLoader.message('health.install_brew')} fzf"
|
161
|
+
when 'rga'
|
162
|
+
"#{ConfigLoader.message('health.install_brew')} rga"
|
163
|
+
end
|
164
|
+
when /linux/
|
165
|
+
case tool
|
166
|
+
when 'fzf'
|
167
|
+
"#{ConfigLoader.message('health.install_apt')} fzf (Ubuntu/Debian) or check your package manager"
|
168
|
+
when 'rga'
|
169
|
+
ConfigLoader.message('health.rga_releases')
|
170
|
+
end
|
171
|
+
else
|
172
|
+
ConfigLoader.message('health.install_guide')
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def print_check_result(name, result)
|
177
|
+
status_icon = case result[:status]
|
178
|
+
when :ok
|
179
|
+
@pastel.green("✓")
|
180
|
+
when :warning
|
181
|
+
@pastel.yellow("⚠")
|
182
|
+
when :error
|
183
|
+
@pastel.red("✗")
|
184
|
+
end
|
185
|
+
|
186
|
+
status_color = case result[:status]
|
187
|
+
when :ok
|
188
|
+
:green
|
189
|
+
when :warning
|
190
|
+
:yellow
|
191
|
+
when :error
|
192
|
+
:red
|
193
|
+
end
|
194
|
+
|
195
|
+
puts "#{status_icon} #{name.ljust(20)} #{@pastel.decorate(result[:message], status_color)}"
|
196
|
+
|
197
|
+
if result[:details]
|
198
|
+
puts " #{@pastel.dim(result[:details])}"
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def print_summary(results)
|
203
|
+
ok_count = results.count { |r| r[:status] == :ok }
|
204
|
+
warning_count = results.count { |r| r[:status] == :warning }
|
205
|
+
error_count = results.count { |r| r[:status] == :error }
|
206
|
+
|
207
|
+
puts "#{ConfigLoader.message('health.summary')}"
|
208
|
+
puts " #{@pastel.green("✓ #{ok_count} #{ConfigLoader.message('health.ok')}")}"
|
209
|
+
puts " #{@pastel.yellow("⚠ #{warning_count} #{ConfigLoader.message('health.warnings')}")}}" if warning_count > 0
|
210
|
+
puts " #{@pastel.red("✗ #{error_count} #{ConfigLoader.message('health.errors')}")}}" if error_count > 0
|
211
|
+
|
212
|
+
if error_count > 0
|
213
|
+
puts
|
214
|
+
puts @pastel.red(ConfigLoader.message('health.critical_missing'))
|
215
|
+
elsif warning_count > 0
|
216
|
+
puts
|
217
|
+
puts @pastel.yellow(ConfigLoader.message('health.optional_missing'))
|
218
|
+
else
|
219
|
+
puts
|
220
|
+
puts @pastel.green(ConfigLoader.message('health.all_passed'))
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|