kolor 1.0.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/LICENSE +8 -0
- data/bin/kolor +6 -0
- data/lib/kolor/cli.rb +310 -0
- data/lib/kolor/enum/background.rb +31 -0
- data/lib/kolor/enum/foreground.rb +31 -0
- data/lib/kolor/enum/style.rb +19 -0
- data/lib/kolor/enum/theme.rb +32 -0
- data/lib/kolor/extra.rb +302 -0
- data/lib/kolor/internal/config.rb +177 -0
- data/lib/kolor/internal/enum.rb +160 -0
- data/lib/kolor/internal/logger.rb +151 -0
- data/lib/kolor/internal/version.rb +5 -0
- data/lib/kolor.rb +247 -0
- metadata +103 -0
data/lib/kolor/extra.rb
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# @!parse
|
|
3
|
+
# class String
|
|
4
|
+
# include Kolor::Extra
|
|
5
|
+
# end
|
|
6
|
+
require_relative '../kolor'
|
|
7
|
+
require_relative 'internal/enum'
|
|
8
|
+
require_relative 'internal/logger'
|
|
9
|
+
require_relative 'enum/foreground'
|
|
10
|
+
require_relative 'enum/theme'
|
|
11
|
+
# Kolor::Extra provides advanced terminal styling features.
|
|
12
|
+
#
|
|
13
|
+
# Features:
|
|
14
|
+
# - 256 color palette
|
|
15
|
+
# - RGB/True color support
|
|
16
|
+
# - Color gradients
|
|
17
|
+
# - Named themes
|
|
18
|
+
#
|
|
19
|
+
# @example 256 colors
|
|
20
|
+
# "text".color(196) # foreground
|
|
21
|
+
# "text".on_color(196) # background
|
|
22
|
+
#
|
|
23
|
+
# @example RGB colors
|
|
24
|
+
# "text".rgb(255, 0, 0) # foreground
|
|
25
|
+
# "text".on_rgb(255, 0, 0) # background
|
|
26
|
+
#
|
|
27
|
+
# @example Gradients
|
|
28
|
+
# "Hello World".gradient(:red, :blue)
|
|
29
|
+
# "Rainbow".rainbow
|
|
30
|
+
#
|
|
31
|
+
# @example Themes
|
|
32
|
+
# Kolor::Extra.theme(:success, :green, :bold)
|
|
33
|
+
# "Done!".success
|
|
34
|
+
module Kolor
|
|
35
|
+
# Extended functionality for Kolor including RGB colors, gradients, and themes
|
|
36
|
+
module Extra
|
|
37
|
+
# 256-color palette support
|
|
38
|
+
# @param code [Integer] color code (0-255)
|
|
39
|
+
# @return [String, ColorizedString] colorized string
|
|
40
|
+
def color(code)
|
|
41
|
+
create_colorized_string("\e[38;5;#{code}m")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# 256-color background palette support
|
|
45
|
+
# @param code [Integer] color code (0-255)
|
|
46
|
+
# @return [String, ColorizedString] colorized string
|
|
47
|
+
def on_color(code)
|
|
48
|
+
create_colorized_string("\e[48;5;#{code}m")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# RGB / True color support (foreground)
|
|
52
|
+
# @param red [Integer] red component (0-255)
|
|
53
|
+
# @param green [Integer] green component (0-255)
|
|
54
|
+
# @param blue [Integer] blue component (0-255)
|
|
55
|
+
# @return [String, ColorizedString] colorized string
|
|
56
|
+
def rgb(red, green, blue)
|
|
57
|
+
return to_s unless Kolor.enabled?
|
|
58
|
+
return to_s unless [red, green, blue].all? { |v| v.between?(0, 255) }
|
|
59
|
+
create_colorized_string("\e[38;2;#{red};#{green};#{blue}m")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# RGB / True color support (background)
|
|
63
|
+
# @param red [Integer] red component (0-255)
|
|
64
|
+
# @param green [Integer] green component (0-255)
|
|
65
|
+
# @param blue [Integer] blue component (0-255)
|
|
66
|
+
# @return [String, ColorizedString] colorized string
|
|
67
|
+
def on_rgb(red, green, blue)
|
|
68
|
+
return to_s unless Kolor.enabled?
|
|
69
|
+
return to_s unless [red, green, blue].all? { |v| v.between?(0, 255) }
|
|
70
|
+
create_colorized_string("\e[48;2;#{red};#{green};#{blue}m")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Hex color support (foreground)
|
|
74
|
+
# @param hex_color [String] with_hex color code (with or without #)
|
|
75
|
+
# @return [String, ColorizedString] colorized string
|
|
76
|
+
def with_hex(hex_color)
|
|
77
|
+
return to_s unless Kolor.enabled?
|
|
78
|
+
|
|
79
|
+
hex_color = hex_color.delete('#')
|
|
80
|
+
return to_s unless hex_color.match?(/^[0-9A-Fa-f]{6}$/)
|
|
81
|
+
|
|
82
|
+
r = hex_color[0..1].to_i(16)
|
|
83
|
+
g = hex_color[2..3].to_i(16)
|
|
84
|
+
b = hex_color[4..5].to_i(16)
|
|
85
|
+
rgb(r, g, b)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Hex color support (background)
|
|
89
|
+
# @param hex_color [String] with_hex color code (with or without #)
|
|
90
|
+
# @return [String, ColorizedString] colorized string
|
|
91
|
+
def on_hex(hex_color)
|
|
92
|
+
return to_s unless Kolor.enabled?
|
|
93
|
+
|
|
94
|
+
hex_color = hex_color.delete('#')
|
|
95
|
+
return to_s unless hex_color.match?(/^[0-9A-Fa-f]{6}$/)
|
|
96
|
+
|
|
97
|
+
r = hex_color[0..1].to_i(16)
|
|
98
|
+
g = hex_color[2..3].to_i(16)
|
|
99
|
+
b = hex_color[4..5].to_i(16)
|
|
100
|
+
on_rgb(r, g, b)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Gradient between two colors
|
|
104
|
+
# @param start_color [Symbol] starting color name
|
|
105
|
+
# @param end_color [Symbol] ending color name
|
|
106
|
+
# @return [String] string with gradient effect
|
|
107
|
+
def gradient(start_color, end_color)
|
|
108
|
+
return to_s unless Kolor.enabled?
|
|
109
|
+
|
|
110
|
+
start_enum = Kolor::Enum::Foreground[start_color]
|
|
111
|
+
end_enum = Kolor::Enum::Foreground[end_color]
|
|
112
|
+
|
|
113
|
+
return to_s unless start_enum && end_enum
|
|
114
|
+
|
|
115
|
+
start_code = start_enum.value
|
|
116
|
+
end_code = end_enum.value
|
|
117
|
+
|
|
118
|
+
chars = to_s.chars
|
|
119
|
+
return Kolor.clear_code if chars.empty?
|
|
120
|
+
|
|
121
|
+
result = chars.map.with_index do |char, i|
|
|
122
|
+
progress = chars.length > 1 ? i.to_f / (chars.length - 1) : 0
|
|
123
|
+
color_code = start_code + ((end_code - start_code) * progress).round
|
|
124
|
+
"\e[#{color_code}m#{char}"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
result.join + Kolor.clear_code
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Rainbow colors
|
|
131
|
+
# @return [String] string with rainbow effect
|
|
132
|
+
def rainbow
|
|
133
|
+
return to_s unless Kolor.enabled?
|
|
134
|
+
|
|
135
|
+
colors = %i[red yellow green cyan blue magenta]
|
|
136
|
+
chars = to_s.chars
|
|
137
|
+
|
|
138
|
+
return Kolor.clear_code if chars.empty?
|
|
139
|
+
|
|
140
|
+
result = chars.map.with_index do |char, i|
|
|
141
|
+
color = colors[i % colors.length]
|
|
142
|
+
enum = Kolor::Enum::Foreground[color]
|
|
143
|
+
code = enum.value
|
|
144
|
+
"\e[#{code}m#{char}"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
result.join + Kolor.clear_code
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Define a custom theme
|
|
151
|
+
# @param name [Symbol] theme name
|
|
152
|
+
# @param styles [Array<Symbol>] list of color and style names to apply
|
|
153
|
+
# @example
|
|
154
|
+
# Kolor::Extra.theme(:error, :white, :on_red, :bold)
|
|
155
|
+
# "Error!".error # => white text on red background, bold
|
|
156
|
+
def self.theme(name, *styles)
|
|
157
|
+
begin
|
|
158
|
+
Kolor::Enum::Theme.entry(name, normalize_styles(styles))
|
|
159
|
+
define_theme_method(name, styles)
|
|
160
|
+
rescue ArgumentError => e
|
|
161
|
+
if e.message =~ /already assigned to (\w+)/
|
|
162
|
+
existing = Regexp.last_match(1)
|
|
163
|
+
Kolor::Logger.warn("Theme value already registered as :#{existing}. Skipping :#{name}.")
|
|
164
|
+
else
|
|
165
|
+
Kolor::Logger.error("Theme registration failed for #{name}: #{e.message}")
|
|
166
|
+
end
|
|
167
|
+
rescue TypeError => e
|
|
168
|
+
Kolor::Logger.error("Theme registration failed for #{name}: #{e.message}")
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# Defines a theme method on String and ColorizedString
|
|
175
|
+
# @param name [Symbol] theme name
|
|
176
|
+
# @param styles [Array<Symbol>] list of style methods to apply
|
|
177
|
+
# @return [void]
|
|
178
|
+
def self.define_theme_method(name, styles)
|
|
179
|
+
# Define the method on both String and ColorizedString
|
|
180
|
+
[String, Kolor::ColorizedString].each do |klass|
|
|
181
|
+
klass.class_eval do
|
|
182
|
+
# Remove existing method if present to avoid warnings
|
|
183
|
+
remove_method(name) if method_defined?(name)
|
|
184
|
+
|
|
185
|
+
# Define the theme method
|
|
186
|
+
define_method(name) do
|
|
187
|
+
# Return plain string if colors are disabled
|
|
188
|
+
return to_s unless Kolor.enabled?
|
|
189
|
+
|
|
190
|
+
# Apply each style in sequence
|
|
191
|
+
result = self
|
|
192
|
+
styles.each do |style|
|
|
193
|
+
# Verify the style method exists before calling it
|
|
194
|
+
if result.respond_to?(style)
|
|
195
|
+
result = result.public_send(style)
|
|
196
|
+
else
|
|
197
|
+
# Log warning but continue with other styles
|
|
198
|
+
Kolor::Logger.debug "Style '#{style}' not found for theme '#{name}'"
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
result
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
Kolor::Logger.debug "Defined theme method '#{name}' with styles #{styles.inspect}"
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Normalizes a list of style symbols into a structured theme hash.
|
|
211
|
+
# Extracts foreground, background, and remaining styles.
|
|
212
|
+
#
|
|
213
|
+
# @param styles [Array<Symbol>] list of style names (e.g., :green, :on_red, :bold)
|
|
214
|
+
# @return [Hash{Symbol=>Symbol, Array<Symbol>}] normalized theme structure
|
|
215
|
+
# - :foreground → foreground color (Symbol or nil)
|
|
216
|
+
# - :background → background color (Symbol or nil)
|
|
217
|
+
# - :styles → remaining style modifiers (Array<Symbol>)
|
|
218
|
+
def self.normalize_styles(styles)
|
|
219
|
+
fg = styles.find { |s| Kolor::Enum::Foreground.keys.include?(s) }
|
|
220
|
+
bg = styles.find { |s| s.to_s.start_with?('on_') }
|
|
221
|
+
rest = styles - [fg, bg].compact
|
|
222
|
+
{
|
|
223
|
+
foreground: fg,
|
|
224
|
+
background: bg&.to_s&.sub(/^on_/, '')&.to_sym,
|
|
225
|
+
styles: rest
|
|
226
|
+
}
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Returns the list of all registered theme names.
|
|
230
|
+
#
|
|
231
|
+
# @return [Array<Symbol>] list of theme keys
|
|
232
|
+
def self.themes
|
|
233
|
+
Kolor::Enum::Theme.keys
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Retrieves the configuration object for a given theme.
|
|
237
|
+
#
|
|
238
|
+
# @param name [Symbol] theme name
|
|
239
|
+
# @return [Object, nil] theme configuration or nil if not found or invalid
|
|
240
|
+
# Expected keys:
|
|
241
|
+
# - :foreground → foreground color (Symbol or nil)
|
|
242
|
+
# - :background → background color (Symbol or nil)
|
|
243
|
+
# - :styles → style modifiers (Array<Symbol>)
|
|
244
|
+
def self.get_theme(name)
|
|
245
|
+
Kolor::Enum::Theme[name]&.value.then { |v| v.is_a?(Hash) ? v : nil }
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def self.remove_theme(name)
|
|
249
|
+
theme = Kolor::Enum::Theme[name]
|
|
250
|
+
raise ArgumentError, "Theme #{name} not found" unless theme
|
|
251
|
+
|
|
252
|
+
built_in = %i[success error warning info debug]
|
|
253
|
+
raise ArgumentError, "Cannot remove built-in theme #{name}" if built_in.include?(name)
|
|
254
|
+
|
|
255
|
+
Kolor::Logger.info("Removing theme #{name}")
|
|
256
|
+
Kolor::Enum::Theme.remove(name)
|
|
257
|
+
|
|
258
|
+
[String, Kolor::ColorizedString].each do |klass|
|
|
259
|
+
klass.class_eval do
|
|
260
|
+
remove_method(name) if method_defined?(name)
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Defines all built-in themes from Kolor::Enum::Theme
|
|
266
|
+
# @return [void]
|
|
267
|
+
def self.define_all_themes
|
|
268
|
+
return if @themes_initialized
|
|
269
|
+
Kolor::Logger.info('Built-in themes initialized') unless @themes_initialized
|
|
270
|
+
@themes_initialized = true
|
|
271
|
+
|
|
272
|
+
Kolor::Enum::Theme.keys.each do |name|
|
|
273
|
+
config = Kolor::Enum::Theme[name].value
|
|
274
|
+
next unless config.is_a?(Hash)
|
|
275
|
+
|
|
276
|
+
styles = []
|
|
277
|
+
styles << config[:foreground] if config[:foreground]
|
|
278
|
+
styles << "on_#{config[:background]}".to_sym if config[:background]
|
|
279
|
+
styles += config[:styles] if config[:styles].is_a?(Array)
|
|
280
|
+
|
|
281
|
+
define_theme_method(name, styles)
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Check if a theme method is defined
|
|
286
|
+
# @param name [Symbol] theme name
|
|
287
|
+
# @return [Boolean] true if the method exists on String
|
|
288
|
+
def self.theme_defined?(name)
|
|
289
|
+
String.method_defined?(name.to_sym)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Init built-in themes
|
|
293
|
+
self.define_all_themes
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Extend String class with Extra methods
|
|
298
|
+
String.include(Kolor::Extra)
|
|
299
|
+
Kolor::ColorizedString.include(Kolor::Extra)
|
|
300
|
+
|
|
301
|
+
# Init built-in themes
|
|
302
|
+
Kolor::Extra.define_all_themes
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'fileutils'
|
|
3
|
+
require_relative 'logger'
|
|
4
|
+
|
|
5
|
+
module Kolor
|
|
6
|
+
# Kolor::Config handles configuration loading and initialization for Kolor.
|
|
7
|
+
#
|
|
8
|
+
# It supports Ruby-based config files located in the user's home directory:
|
|
9
|
+
# - `.kolorrc.rb` — preferred format
|
|
10
|
+
# - `.kolorrc` — optional alias
|
|
11
|
+
#
|
|
12
|
+
# The config file can define themes using `Kolor::Extra.theme`.
|
|
13
|
+
# If no config is found, a default file is created automatically.
|
|
14
|
+
#
|
|
15
|
+
# @example Initialize and load config
|
|
16
|
+
# Kolor::Config.init
|
|
17
|
+
#
|
|
18
|
+
# @example Reload config manually
|
|
19
|
+
# Kolor::Config.reload!
|
|
20
|
+
#
|
|
21
|
+
# @example Access loaded themes
|
|
22
|
+
# Kolor::Config.themes_from_config
|
|
23
|
+
#
|
|
24
|
+
# @example Describe a theme
|
|
25
|
+
# Kolor::Config.describe_theme(:success)
|
|
26
|
+
module Config
|
|
27
|
+
HOME_PATH = ENV['HOME'] || ENV['USERPROFILE']
|
|
28
|
+
CONFIG_FILE_RB = HOME_PATH ? File.expand_path("#{HOME_PATH}/.kolorrc.rb") : nil
|
|
29
|
+
CONFIG_FILE_ALIAS = HOME_PATH ? File.expand_path("#{HOME_PATH}/.kolorrc") : nil
|
|
30
|
+
|
|
31
|
+
class << self
|
|
32
|
+
# Initializes configuration.
|
|
33
|
+
# Creates a default config file if none exists, then loads it.
|
|
34
|
+
#
|
|
35
|
+
# @return [void]
|
|
36
|
+
def init
|
|
37
|
+
create_default_config unless config_exists?
|
|
38
|
+
load_config
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Checks whether any supported config file exists.
|
|
42
|
+
#
|
|
43
|
+
# @return [Boolean] true if a config file is found, false otherwise
|
|
44
|
+
def config_exists?
|
|
45
|
+
!config_file_path.nil?
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Creates a default Ruby config file in the user's home directory.
|
|
49
|
+
# Skips creation if a config already exists.
|
|
50
|
+
# Logs warnings if HOME is missing or creation fails.
|
|
51
|
+
#
|
|
52
|
+
# @return [void]
|
|
53
|
+
def create_default_config
|
|
54
|
+
unless HOME_PATH
|
|
55
|
+
Kolor::Logger.warn 'No home directory found'
|
|
56
|
+
return
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
return unless find_existing_config_file.nil?
|
|
60
|
+
|
|
61
|
+
begin
|
|
62
|
+
if CONFIG_FILE_RB.nil?
|
|
63
|
+
raise LoadError, 'Config path is invalid'
|
|
64
|
+
end
|
|
65
|
+
FileUtils.cp(default_config_path, CONFIG_FILE_RB)
|
|
66
|
+
Kolor::Logger.warn "Created default configuration file at #{CONFIG_FILE_RB}"
|
|
67
|
+
rescue StandardError, LoadError => e
|
|
68
|
+
Kolor::Logger.warn "Failed to create default config file: #{e.message}" if e.is_a?(StandardError)
|
|
69
|
+
Kolor::Logger.warn e.message if e.is_a?(LoadError)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Loads configuration from disk.
|
|
74
|
+
# Supports only Ruby-based config files (.rb or .kolorrc).
|
|
75
|
+
# Logs info and warnings during the process.
|
|
76
|
+
#
|
|
77
|
+
# @return [void]
|
|
78
|
+
def load_config
|
|
79
|
+
config_path = config_file_path
|
|
80
|
+
|
|
81
|
+
Kolor::Logger.info "load_config called, config_path: #{config_path.inspect}"
|
|
82
|
+
|
|
83
|
+
return unless config_path
|
|
84
|
+
|
|
85
|
+
Kolor::Logger.info "Loading config from: #{config_path}"
|
|
86
|
+
|
|
87
|
+
if config_path.end_with?('.rb') || config_path.end_with?('.kolorrc')
|
|
88
|
+
Kolor::Logger.info "Detected Ruby config"
|
|
89
|
+
load_ruby_config(config_path)
|
|
90
|
+
else
|
|
91
|
+
Kolor::Logger.warn "Unknown config file type: #{config_path}"
|
|
92
|
+
end
|
|
93
|
+
rescue StandardError => e
|
|
94
|
+
Kolor::Logger.warn "Error loading config file #{config_path}: #{e.message}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Reloads configuration from disk.
|
|
98
|
+
#
|
|
99
|
+
# @return [void]
|
|
100
|
+
def reload!
|
|
101
|
+
load_config
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Returns the theme configuration as a hash.
|
|
105
|
+
#
|
|
106
|
+
# @param name [Symbol, String] theme name
|
|
107
|
+
# @return [Hash{Symbol => Object}, nil] theme config or nil if not found
|
|
108
|
+
def describe_theme(name)
|
|
109
|
+
Kolor::Extra.get_theme(name.to_sym)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Returns all theme names loaded from config.
|
|
113
|
+
#
|
|
114
|
+
# @return [Array<Symbol>] list of theme keys
|
|
115
|
+
def themes_from_config
|
|
116
|
+
Kolor::Extra.themes
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
# Returns the absolute path to the default config template.
|
|
122
|
+
#
|
|
123
|
+
# @return [String] path to default .kolorrc.rb file
|
|
124
|
+
def default_config_path
|
|
125
|
+
File.expand_path('../../../default_config/.kolorrc.rb', __dir__)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Returns the path to the first existing config file.
|
|
129
|
+
#
|
|
130
|
+
# @return [String, nil] path to config file or nil if none found
|
|
131
|
+
def config_file_path
|
|
132
|
+
find_existing_config_file
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Finds the first existing config file among supported formats.
|
|
136
|
+
#
|
|
137
|
+
# @return [String, nil] path to config file or nil
|
|
138
|
+
def find_existing_config_file
|
|
139
|
+
return CONFIG_FILE_RB if !CONFIG_FILE_RB.nil? && File.exist?(CONFIG_FILE_RB)
|
|
140
|
+
return CONFIG_FILE_ALIAS if !CONFIG_FILE_ALIAS.nil? && File.exist?(CONFIG_FILE_ALIAS)
|
|
141
|
+
|
|
142
|
+
nil
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Loads and executes a Ruby config file.
|
|
146
|
+
# Ensures Kolor::Extra is loaded before execution.
|
|
147
|
+
#
|
|
148
|
+
# @param path [String] absolute path to config file
|
|
149
|
+
# @return [void]
|
|
150
|
+
def load_ruby_config(path)
|
|
151
|
+
ensure_extra_loaded
|
|
152
|
+
load path
|
|
153
|
+
rescue SyntaxError, LoadError => e
|
|
154
|
+
raise StandardError, e.message
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Ensures Kolor::Extra is loaded and included in String and ColorizedString.
|
|
158
|
+
# This method is idempotent and safe to call multiple times.
|
|
159
|
+
#
|
|
160
|
+
# @return [void]
|
|
161
|
+
def ensure_extra_loaded
|
|
162
|
+
unless defined?(Kolor::Extra)
|
|
163
|
+
require 'kolor/extra'
|
|
164
|
+
return
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
String.include(Kolor::Extra) unless String.included_modules.include?(Kolor::Extra)
|
|
168
|
+
|
|
169
|
+
if defined?(Kolor::ColorizedString)
|
|
170
|
+
unless Kolor::ColorizedString.included_modules.include?(Kolor::Extra)
|
|
171
|
+
Kolor::ColorizedString.include(Kolor::Extra)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Kolor::Enum provides a declarative, type-safe registry for named values.
|
|
4
|
+
# Each entry is unique by both name and value. Values can be of any type,
|
|
5
|
+
# but you may optionally declare a type constraint using `type`.
|
|
6
|
+
#
|
|
7
|
+
# Enum entries are registered via `.entry(name, value)` and accessed via `.name` or `[]`.
|
|
8
|
+
# Each entry becomes a singleton method of the class and returns an instance of the enum.
|
|
9
|
+
#
|
|
10
|
+
# @example Define an enum
|
|
11
|
+
# class MyEnum < Kolor::Enum
|
|
12
|
+
# type Integer
|
|
13
|
+
# entry :low, 1
|
|
14
|
+
# entry :high, 2
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# @example Access values
|
|
18
|
+
# MyEnum[:low].value # => 1
|
|
19
|
+
# MyEnum.high.to_sym # => :high
|
|
20
|
+
# MyEnum.low == MyEnum[:low] # => true
|
|
21
|
+
module Kolor
|
|
22
|
+
class Enum
|
|
23
|
+
# @return [Object] the value associated with the enum entry
|
|
24
|
+
attr_reader :value
|
|
25
|
+
|
|
26
|
+
# Initializes a new enum instance with a given value
|
|
27
|
+
#
|
|
28
|
+
# @param value [Object] the raw value of the enum
|
|
29
|
+
def initialize(value)
|
|
30
|
+
@value = value
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Returns the symbolic name of the value as a string
|
|
34
|
+
#
|
|
35
|
+
# @return [String]
|
|
36
|
+
def to_s = self.class.name_for(value).to_s
|
|
37
|
+
|
|
38
|
+
# Returns the symbolic name of the value as a symbol
|
|
39
|
+
#
|
|
40
|
+
# @return [Symbol]
|
|
41
|
+
def to_sym = self.class.name_for(value)
|
|
42
|
+
|
|
43
|
+
# Returns a debug-friendly string representation
|
|
44
|
+
#
|
|
45
|
+
# @return [String]
|
|
46
|
+
def inspect = "#<#{self.class.name} #{to_sym.inspect}:#{value.inspect}>"
|
|
47
|
+
|
|
48
|
+
# Equality based on class and value
|
|
49
|
+
#
|
|
50
|
+
# @param other [Object]
|
|
51
|
+
# @return [Boolean]
|
|
52
|
+
def ==(other)
|
|
53
|
+
other.is_a?(self.class) && other.value == value
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Alias for `==`
|
|
57
|
+
#
|
|
58
|
+
# @param other [Object]
|
|
59
|
+
# @return [Boolean]
|
|
60
|
+
alias eql? ==
|
|
61
|
+
|
|
62
|
+
# Hash code based on value
|
|
63
|
+
#
|
|
64
|
+
# @return [Integer]
|
|
65
|
+
def hash = value.hash
|
|
66
|
+
|
|
67
|
+
class << self
|
|
68
|
+
# Declares the expected type of all enum values
|
|
69
|
+
#
|
|
70
|
+
# @param klass [Class] the type constraint for values
|
|
71
|
+
# @return [void]
|
|
72
|
+
def type(klass)
|
|
73
|
+
@value_type = klass
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Registers a new enum entry with a unique name and value
|
|
77
|
+
#
|
|
78
|
+
# @param name [Symbol, String] symbolic name of the entry
|
|
79
|
+
# @param value [Object] value of the entry
|
|
80
|
+
# @raise [ArgumentError] if name or value is already registered
|
|
81
|
+
# @raise [TypeError] if value does not match declared type
|
|
82
|
+
# @return [void]
|
|
83
|
+
def entry(name, value)
|
|
84
|
+
name = name.to_sym
|
|
85
|
+
@registry ||= {}
|
|
86
|
+
@values ||= {}
|
|
87
|
+
|
|
88
|
+
if defined?(@value_type) && !value.is_a?(@value_type)
|
|
89
|
+
raise TypeError, "Invalid value type for #{name}: expected #{@value_type}, got #{value.class}"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
if @values.key?(value)
|
|
93
|
+
existing = @values[value]
|
|
94
|
+
raise ArgumentError, "Duplicate value #{value.inspect} for #{name}; already assigned to #{existing}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
if @registry.key?(name)
|
|
98
|
+
raise ArgumentError, "Duplicate name #{name}; already registered with value #{@registry[name].value.inspect}"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
instance = new(value)
|
|
102
|
+
@registry[name] = instance
|
|
103
|
+
@values[value] = name
|
|
104
|
+
|
|
105
|
+
define_singleton_method(name) { instance }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Retrieves an enum instance by name
|
|
109
|
+
#
|
|
110
|
+
# @param name [Symbol, String]
|
|
111
|
+
# @return [Kolor::Enum, nil]
|
|
112
|
+
def [](name)
|
|
113
|
+
@registry[name.to_sym]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Resolves the symbolic name for a given value
|
|
117
|
+
#
|
|
118
|
+
# @param value [Object]
|
|
119
|
+
# @return [Symbol] name or :unknown
|
|
120
|
+
def name_for(value)
|
|
121
|
+
@values[value] || :unknown
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Returns all registered enum instances
|
|
125
|
+
#
|
|
126
|
+
# @return [Array<Kolor::Enum>]
|
|
127
|
+
def all
|
|
128
|
+
@registry.values
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Returns all registered names
|
|
132
|
+
#
|
|
133
|
+
# @return [Array<Symbol>]
|
|
134
|
+
def keys
|
|
135
|
+
@registry.keys
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Returns all raw values
|
|
139
|
+
#
|
|
140
|
+
# @return [Array<Object>]
|
|
141
|
+
def values
|
|
142
|
+
@registry.values.map(&:value)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Removes an enum entry by name
|
|
146
|
+
#
|
|
147
|
+
# @param name [Symbol, String]
|
|
148
|
+
# @return [Kolor::Enum, nil] the removed entry or nil
|
|
149
|
+
def remove(name)
|
|
150
|
+
name = name.to_sym
|
|
151
|
+
entry = @registry.delete(name)
|
|
152
|
+
if entry
|
|
153
|
+
@values.delete_if { |_, v| v == name }
|
|
154
|
+
singleton_class.undef_method(name) if respond_to?(name)
|
|
155
|
+
end
|
|
156
|
+
entry
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|