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.
@@ -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