charming 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/LICENSE.txt +21 -0
- data/README.md +421 -0
- data/exe/charming +6 -0
- data/lib/charming/application.rb +90 -0
- data/lib/charming/application_model.rb +13 -0
- data/lib/charming/cli.rb +60 -0
- data/lib/charming/component.rb +8 -0
- data/lib/charming/components/activity_indicator.rb +158 -0
- data/lib/charming/components/command_palette.rb +118 -0
- data/lib/charming/components/keyboard_handler.rb +22 -0
- data/lib/charming/components/list.rb +105 -0
- data/lib/charming/components/modal.rb +48 -0
- data/lib/charming/components/progressbar.rb +55 -0
- data/lib/charming/components/spinner.rb +37 -0
- data/lib/charming/components/table.rb +115 -0
- data/lib/charming/components/text_input.rb +103 -0
- data/lib/charming/components/viewport.rb +191 -0
- data/lib/charming/controller.rb +523 -0
- data/lib/charming/focus.rb +65 -0
- data/lib/charming/generators/app_file_generator.rb +28 -0
- data/lib/charming/generators/app_generator/app_spec_templates.rb +86 -0
- data/lib/charming/generators/app_generator/basic_templates.rb +69 -0
- data/lib/charming/generators/app_generator/component_templates.rb +36 -0
- data/lib/charming/generators/app_generator/controller_template.rb +69 -0
- data/lib/charming/generators/app_generator/layout_template.rb +160 -0
- data/lib/charming/generators/app_generator/model_templates.rb +30 -0
- data/lib/charming/generators/app_generator/screen_spec_templates.rb +70 -0
- data/lib/charming/generators/app_generator/view_template.rb +90 -0
- data/lib/charming/generators/app_generator.rb +76 -0
- data/lib/charming/generators/base.rb +29 -0
- data/lib/charming/generators/component_generator.rb +30 -0
- data/lib/charming/generators/controller_generator.rb +50 -0
- data/lib/charming/generators/name.rb +32 -0
- data/lib/charming/generators/screen_generator.rb +154 -0
- data/lib/charming/generators/view_generator.rb +34 -0
- data/lib/charming/generators.rb +7 -0
- data/lib/charming/internal/renderer/differential.rb +53 -0
- data/lib/charming/internal/renderer/full_repaint.rb +19 -0
- data/lib/charming/internal/terminal/adapter.rb +52 -0
- data/lib/charming/internal/terminal/memory_backend.rb +91 -0
- data/lib/charming/internal/terminal/tty_backend.rb +250 -0
- data/lib/charming/key_event.rb +13 -0
- data/lib/charming/mouse_event.rb +40 -0
- data/lib/charming/resize_event.rb +7 -0
- data/lib/charming/response.rb +33 -0
- data/lib/charming/router.rb +137 -0
- data/lib/charming/runtime.rb +192 -0
- data/lib/charming/screen.rb +8 -0
- data/lib/charming/task.rb +7 -0
- data/lib/charming/task_event.rb +17 -0
- data/lib/charming/task_executor.rb +62 -0
- data/lib/charming/timer_event.rb +7 -0
- data/lib/charming/ui/border.rb +33 -0
- data/lib/charming/ui/style.rb +244 -0
- data/lib/charming/ui/theme.rb +178 -0
- data/lib/charming/ui/themes/phosphor.json +100 -0
- data/lib/charming/ui/width.rb +24 -0
- data/lib/charming/ui.rb +230 -0
- data/lib/charming/version.rb +5 -0
- data/lib/charming/view.rb +116 -0
- data/lib/charming.rb +24 -0
- data/sig/charming.rbs +3 -0
- metadata +225 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Charming
|
|
4
|
+
module UI
|
|
5
|
+
class Style
|
|
6
|
+
ATTRIBUTES = {
|
|
7
|
+
bold: 1,
|
|
8
|
+
faint: 2,
|
|
9
|
+
italic: 3,
|
|
10
|
+
underline: 4,
|
|
11
|
+
reverse: 7,
|
|
12
|
+
strikethrough: 9
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
COLORS = {
|
|
16
|
+
black: 30,
|
|
17
|
+
red: 31,
|
|
18
|
+
green: 32,
|
|
19
|
+
yellow: 33,
|
|
20
|
+
blue: 34,
|
|
21
|
+
magenta: 35,
|
|
22
|
+
cyan: 36,
|
|
23
|
+
white: 37,
|
|
24
|
+
bright_black: 90,
|
|
25
|
+
bright_red: 91,
|
|
26
|
+
bright_green: 92,
|
|
27
|
+
bright_yellow: 93,
|
|
28
|
+
bright_blue: 94,
|
|
29
|
+
bright_magenta: 95,
|
|
30
|
+
bright_cyan: 96,
|
|
31
|
+
bright_white: 97
|
|
32
|
+
}.freeze
|
|
33
|
+
|
|
34
|
+
def initialize(options = {})
|
|
35
|
+
@options = {
|
|
36
|
+
attributes: [],
|
|
37
|
+
padding: [0, 0, 0, 0],
|
|
38
|
+
align: :left
|
|
39
|
+
}.merge(options)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def foreground(color)
|
|
43
|
+
with(foreground: color)
|
|
44
|
+
end
|
|
45
|
+
alias_method :fg, :foreground
|
|
46
|
+
|
|
47
|
+
def background(color)
|
|
48
|
+
with(background: color)
|
|
49
|
+
end
|
|
50
|
+
alias_method :bg, :background
|
|
51
|
+
|
|
52
|
+
ATTRIBUTES.each_key do |attribute|
|
|
53
|
+
define_method(attribute) do
|
|
54
|
+
with(attributes: (@options.fetch(:attributes) + [attribute]).uniq)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def padding(*values)
|
|
59
|
+
with(padding: expand_box_values(values))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def border(style = :normal, sides: nil, foreground: nil)
|
|
63
|
+
with(border: style, border_sides: sides, border_foreground: foreground)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def width(value)
|
|
67
|
+
with(width: value)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def height(value)
|
|
71
|
+
with(height: value)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def align(value)
|
|
75
|
+
with(align: value)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def render(value)
|
|
79
|
+
lines = apply_dimensions(value.to_s.lines(chomp: true))
|
|
80
|
+
lines = apply_padding(lines)
|
|
81
|
+
lines = apply_border(lines)
|
|
82
|
+
apply_ansi(lines.join("\n"))
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def with(changes)
|
|
88
|
+
self.class.new(@options.merge(changes))
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def apply_dimensions(lines)
|
|
92
|
+
content_width = target_content_width(lines)
|
|
93
|
+
dimensioned = lines.map { |line| align_line(fit_line(line, content_width), content_width) }
|
|
94
|
+
apply_height(dimensioned, content_width)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def target_content_width(lines)
|
|
98
|
+
explicit_width = @options[:width]
|
|
99
|
+
natural_width = lines.map { |line| Width.measure(line) }.max || 0
|
|
100
|
+
explicit_width || natural_width
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def fit_line(line, width)
|
|
104
|
+
return line if Width.measure(line) <= width
|
|
105
|
+
|
|
106
|
+
UI.visible_slice(line, 0, width)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def apply_height(lines, width)
|
|
110
|
+
height = @options[:height]
|
|
111
|
+
return lines unless height
|
|
112
|
+
|
|
113
|
+
visible = lines.first(height)
|
|
114
|
+
visible + Array.new([height - visible.length, 0].max) { " " * width }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def apply_padding(lines)
|
|
118
|
+
top, right, bottom, left = @options.fetch(:padding)
|
|
119
|
+
inner_width = lines.map { |line| Width.measure(line) }.max || 0
|
|
120
|
+
empty = " " * (left + inner_width + right)
|
|
121
|
+
padded = lines.map do |line|
|
|
122
|
+
pad_line(line, inner_width, left, right)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
Array.new(top, empty) + padded + Array.new(bottom, empty)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def apply_border(lines)
|
|
129
|
+
border_name = @options[:border]
|
|
130
|
+
return lines unless border_name
|
|
131
|
+
|
|
132
|
+
border = Border.fetch(border_name)
|
|
133
|
+
sides = Array(@options[:border_sides] || %i[top right bottom left]).map(&:to_sym)
|
|
134
|
+
width = lines.map { |line| Width.measure(line) }.max || 0
|
|
135
|
+
horizontal = border.horizontal * width
|
|
136
|
+
body = lines.map { |line| border_line(line, width, border, sides) }
|
|
137
|
+
|
|
138
|
+
[top_border(border, horizontal, sides), *body, bottom_border(border, horizontal, sides)].compact
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def pad_line(line, inner_width, left, right)
|
|
142
|
+
(" " * left) + line + (" " * (inner_width - Width.measure(line) + right))
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def border_line(line, width, border, sides)
|
|
146
|
+
left = sides.include?(:left) ? render_border(border.vertical) : ""
|
|
147
|
+
right = sides.include?(:right) ? render_border(border.vertical) : ""
|
|
148
|
+
|
|
149
|
+
"#{left}#{line}#{" " * (width - Width.measure(line))}#{right}"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def top_border(border, horizontal, sides)
|
|
153
|
+
return unless sides.include?(:top)
|
|
154
|
+
return render_border(horizontal) unless full_horizontal_border?(sides)
|
|
155
|
+
|
|
156
|
+
render_border("#{border.top_left}#{horizontal}#{border.top_right}")
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def bottom_border(border, horizontal, sides)
|
|
160
|
+
return unless sides.include?(:bottom)
|
|
161
|
+
return render_border(horizontal) unless full_horizontal_border?(sides)
|
|
162
|
+
|
|
163
|
+
render_border("#{border.bottom_left}#{horizontal}#{border.bottom_right}")
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def full_horizontal_border?(sides)
|
|
167
|
+
sides.include?(:left) && sides.include?(:right)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def render_border(value)
|
|
171
|
+
border_foreground = @options[:border_foreground]
|
|
172
|
+
return value unless border_foreground
|
|
173
|
+
|
|
174
|
+
Style.new(foreground: border_foreground, background: @options[:background]).render(value)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def apply_ansi(value)
|
|
178
|
+
codes = ansi_codes
|
|
179
|
+
return value if codes.empty?
|
|
180
|
+
|
|
181
|
+
start = "\e[#{codes.join(";")}m"
|
|
182
|
+
value.split("\n", -1).map { |line| "#{start}#{line.gsub("\e[0m", "\e[0m#{start}")}\e[0m" }.join("\n")
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def ansi_codes
|
|
186
|
+
@options.fetch(:attributes).map { |attribute| ATTRIBUTES.fetch(attribute) } +
|
|
187
|
+
color_codes(@options[:foreground], foreground: true) +
|
|
188
|
+
color_codes(@options[:background], foreground: false)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def color_codes(color, foreground:)
|
|
192
|
+
return [] unless color
|
|
193
|
+
return indexed_color_code(color, foreground: foreground) if color.is_a?(Integer)
|
|
194
|
+
return named_color_code(color, foreground: foreground) if COLORS.key?(color.to_sym)
|
|
195
|
+
return truecolor_codes(color, foreground: foreground) if color.to_s.start_with?("#")
|
|
196
|
+
|
|
197
|
+
raise ArgumentError, "unknown color: #{color.inspect}"
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def named_color_code(color, foreground:)
|
|
201
|
+
code = COLORS.fetch(color.to_sym)
|
|
202
|
+
[foreground ? code : code + 10]
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def indexed_color_code(color, foreground:)
|
|
206
|
+
raise ArgumentError, "indexed color must be between 0 and 255" unless color.between?(0, 255)
|
|
207
|
+
|
|
208
|
+
[foreground ? 38 : 48, 5, color]
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def truecolor_codes(color, foreground:)
|
|
212
|
+
hex = color.to_s.delete_prefix("#")
|
|
213
|
+
raise ArgumentError, "truecolor must be #rrggbb" unless hex.match?(/\A[0-9a-fA-F]{6}\z/)
|
|
214
|
+
|
|
215
|
+
[foreground ? 38 : 48, 2, hex[0..1].to_i(16), hex[2..3].to_i(16), hex[4..5].to_i(16)]
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def align_line(line, width)
|
|
219
|
+
remaining = width - Width.measure(line)
|
|
220
|
+
return line if remaining <= 0
|
|
221
|
+
|
|
222
|
+
case @options.fetch(:align)
|
|
223
|
+
when :right
|
|
224
|
+
(" " * remaining) + line
|
|
225
|
+
when :center
|
|
226
|
+
left = remaining / 2
|
|
227
|
+
(" " * left) + line + (" " * (remaining - left))
|
|
228
|
+
else
|
|
229
|
+
line + (" " * remaining)
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def expand_box_values(values)
|
|
234
|
+
case values.length
|
|
235
|
+
when 1 then [values[0], values[0], values[0], values[0]]
|
|
236
|
+
when 2 then [values[0], values[1], values[0], values[1]]
|
|
237
|
+
when 4 then values
|
|
238
|
+
else
|
|
239
|
+
raise ArgumentError, "padding expects 1, 2, or 4 values"
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Charming
|
|
6
|
+
module UI
|
|
7
|
+
class Theme
|
|
8
|
+
BUILT_IN_ROOT = File.expand_path("themes", __dir__)
|
|
9
|
+
|
|
10
|
+
DEFAULT_TOKENS = {
|
|
11
|
+
text: {foreground: :bright_white},
|
|
12
|
+
title: {foreground: :bright_cyan, bold: true},
|
|
13
|
+
muted: {foreground: :bright_black},
|
|
14
|
+
border: {foreground: :bright_magenta},
|
|
15
|
+
selected: {reverse: true},
|
|
16
|
+
info: {foreground: :bright_cyan},
|
|
17
|
+
warn: {foreground: :yellow}
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
def self.default
|
|
21
|
+
@default ||= load_builtin("phosphor")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.load_file(path)
|
|
25
|
+
from_hash(JSON.parse(File.read(path)))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.load_builtin(name)
|
|
29
|
+
load_file(built_in_path(name))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.built_in_names
|
|
33
|
+
Dir.glob(File.join(BUILT_IN_ROOT, "*.json")).map { |path| File.basename(path, ".json") }.sort
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.from_hash(value)
|
|
37
|
+
raise ArgumentError, "theme file must contain an object" unless value.is_a?(Hash)
|
|
38
|
+
|
|
39
|
+
styles = value.fetch("styles") do
|
|
40
|
+
raise ArgumentError, "theme file must contain styles"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
palette = value.fetch("palette", {})
|
|
44
|
+
new(
|
|
45
|
+
resolve_palette_references(styles, palette),
|
|
46
|
+
background: resolve_background(value["background"], palette)
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.resolve_background(value, palette)
|
|
51
|
+
return unless value
|
|
52
|
+
|
|
53
|
+
deep_resolve_colors(value, normalize_colors(palette))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.built_in_path(name)
|
|
57
|
+
slug = name.to_s
|
|
58
|
+
raise ArgumentError, "unknown built-in theme: #{name.inspect}" unless built_in_names.include?(slug)
|
|
59
|
+
|
|
60
|
+
File.join(BUILT_IN_ROOT, "#{slug}.json")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.resolve_palette_references(styles, palette)
|
|
64
|
+
palette = normalize_colors(palette)
|
|
65
|
+
deep_resolve_colors(styles, palette)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.deep_resolve_colors(value, palette)
|
|
69
|
+
case value
|
|
70
|
+
when Hash
|
|
71
|
+
value.transform_values { |item| deep_resolve_colors(item, palette) }
|
|
72
|
+
when Array
|
|
73
|
+
value.map { |item| deep_resolve_colors(item, palette) }
|
|
74
|
+
when String
|
|
75
|
+
palette.fetch(value, normalize_color(value) || value)
|
|
76
|
+
else
|
|
77
|
+
value
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def self.normalize_colors(values)
|
|
82
|
+
values.transform_values { |value| normalize_color(value) }.compact
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def self.normalize_color(value)
|
|
86
|
+
return unless value.is_a?(String)
|
|
87
|
+
|
|
88
|
+
case value
|
|
89
|
+
when /\A#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])(?:[0-9a-fA-F])?\z/
|
|
90
|
+
"#{$1 * 2}#{$2 * 2}#{$3 * 2}".prepend("#")
|
|
91
|
+
when /\A#[0-9a-fA-F]{6}(?:[0-9a-fA-F]{2})?\z/
|
|
92
|
+
value[0, 7]
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
attr_reader :background
|
|
97
|
+
|
|
98
|
+
def initialize(tokens = {}, background: nil)
|
|
99
|
+
@tokens = symbolize_keys(tokens)
|
|
100
|
+
@background = background
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def style(name)
|
|
104
|
+
spec = @tokens.fetch(name.to_sym) do
|
|
105
|
+
raise ArgumentError, "unknown theme token: #{name.inspect}"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
build_style(spec)
|
|
109
|
+
end
|
|
110
|
+
alias_method :[], :style
|
|
111
|
+
|
|
112
|
+
def method_missing(name, ...)
|
|
113
|
+
return style(name) if @tokens.key?(name)
|
|
114
|
+
|
|
115
|
+
super
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def respond_to_missing?(name, include_private = false)
|
|
119
|
+
@tokens.key?(name) || super
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
def build_style(spec)
|
|
125
|
+
return spec if spec.is_a?(Style)
|
|
126
|
+
return UI.style.foreground(spec) unless spec.is_a?(Hash)
|
|
127
|
+
|
|
128
|
+
apply_options(UI.style, symbolize_keys(spec))
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def apply_options(base_style, spec)
|
|
132
|
+
styled = apply_colors(base_style, spec)
|
|
133
|
+
styled = apply_attributes(styled, spec)
|
|
134
|
+
apply_layout(styled, spec)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def apply_colors(base_style, spec)
|
|
138
|
+
styled = base_style
|
|
139
|
+
styled = styled.foreground(spec[:foreground] || spec[:fg]) if spec.key?(:foreground) || spec.key?(:fg)
|
|
140
|
+
styled = styled.background(spec[:background] || spec[:bg]) if spec.key?(:background) || spec.key?(:bg)
|
|
141
|
+
styled
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def apply_attributes(base_style, spec)
|
|
145
|
+
Style::ATTRIBUTES.each_key.reduce(base_style) do |styled, attribute|
|
|
146
|
+
spec[attribute] ? styled.public_send(attribute) : styled
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def apply_layout(base_style, spec)
|
|
151
|
+
styled = base_style
|
|
152
|
+
styled = styled.padding(*Array(spec[:padding])) if spec.key?(:padding)
|
|
153
|
+
styled = apply_border(styled, spec[:border]) if spec.key?(:border)
|
|
154
|
+
styled = styled.width(spec[:width]) if spec.key?(:width)
|
|
155
|
+
styled = styled.height(spec[:height]) if spec.key?(:height)
|
|
156
|
+
styled = styled.align(spec[:align].to_sym) if spec.key?(:align)
|
|
157
|
+
styled
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def apply_border(base_style, border_spec)
|
|
161
|
+
return base_style.border(border_spec) unless border_spec.is_a?(Hash)
|
|
162
|
+
|
|
163
|
+
border_spec = symbolize_keys(border_spec)
|
|
164
|
+
base_style.border(
|
|
165
|
+
border_spec.fetch(:style, :normal),
|
|
166
|
+
sides: border_spec[:sides],
|
|
167
|
+
foreground: border_spec[:foreground] || border_spec[:fg]
|
|
168
|
+
)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def symbolize_keys(value)
|
|
172
|
+
value.each_with_object({}) do |(key, item), result|
|
|
173
|
+
result[key.to_sym] = item.is_a?(Hash) ? symbolize_keys(item) : item
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Phosphor",
|
|
3
|
+
"id": "phosphor",
|
|
4
|
+
"background": "background",
|
|
5
|
+
"palette": {
|
|
6
|
+
"bright": "#9FE8B0",
|
|
7
|
+
"muted": "#5A8A68",
|
|
8
|
+
"subtle": "#788E80",
|
|
9
|
+
"background": "#111A2C",
|
|
10
|
+
"selected": "#18233D",
|
|
11
|
+
"divider": "#2A3752",
|
|
12
|
+
"amber": "#FFB347",
|
|
13
|
+
"cyan": "#6FD0E3"
|
|
14
|
+
},
|
|
15
|
+
"styles": {
|
|
16
|
+
"text": {
|
|
17
|
+
"foreground": "bright",
|
|
18
|
+
"background": "background"
|
|
19
|
+
},
|
|
20
|
+
"muted": {
|
|
21
|
+
"foreground": "muted",
|
|
22
|
+
"background": "background"
|
|
23
|
+
},
|
|
24
|
+
"title": {
|
|
25
|
+
"foreground": "amber",
|
|
26
|
+
"background": "background",
|
|
27
|
+
"bold": true
|
|
28
|
+
},
|
|
29
|
+
"selected": {
|
|
30
|
+
"foreground": "bright",
|
|
31
|
+
"background": "selected",
|
|
32
|
+
"bold": true
|
|
33
|
+
},
|
|
34
|
+
"header": {
|
|
35
|
+
"foreground": "bright",
|
|
36
|
+
"background": "background",
|
|
37
|
+
"border": {
|
|
38
|
+
"style": "normal",
|
|
39
|
+
"sides": ["bottom"],
|
|
40
|
+
"foreground": "divider"
|
|
41
|
+
},
|
|
42
|
+
"padding": [0, 1]
|
|
43
|
+
},
|
|
44
|
+
"header_accent": {
|
|
45
|
+
"foreground": "amber",
|
|
46
|
+
"background": "background",
|
|
47
|
+
"bold": true
|
|
48
|
+
},
|
|
49
|
+
"sidebar": {
|
|
50
|
+
"background": "background",
|
|
51
|
+
"border": {
|
|
52
|
+
"style": "normal",
|
|
53
|
+
"sides": ["right"],
|
|
54
|
+
"foreground": "divider"
|
|
55
|
+
},
|
|
56
|
+
"padding": [1, 1]
|
|
57
|
+
},
|
|
58
|
+
"main": {
|
|
59
|
+
"foreground": "bright",
|
|
60
|
+
"background": "background",
|
|
61
|
+
"padding": [1, 2]
|
|
62
|
+
},
|
|
63
|
+
"footer": {
|
|
64
|
+
"foreground": "subtle",
|
|
65
|
+
"background": "background",
|
|
66
|
+
"border": {
|
|
67
|
+
"style": "normal",
|
|
68
|
+
"sides": ["top"],
|
|
69
|
+
"foreground": "divider"
|
|
70
|
+
},
|
|
71
|
+
"padding": [0, 1]
|
|
72
|
+
},
|
|
73
|
+
"modal": {
|
|
74
|
+
"foreground": "bright",
|
|
75
|
+
"background": "background",
|
|
76
|
+
"border": {
|
|
77
|
+
"style": "rounded",
|
|
78
|
+
"foreground": "amber"
|
|
79
|
+
},
|
|
80
|
+
"padding": [1, 2]
|
|
81
|
+
},
|
|
82
|
+
"palette_accent": {
|
|
83
|
+
"foreground": "amber",
|
|
84
|
+
"background": "background",
|
|
85
|
+
"bold": true
|
|
86
|
+
},
|
|
87
|
+
"border": {
|
|
88
|
+
"foreground": "divider",
|
|
89
|
+
"background": "background"
|
|
90
|
+
},
|
|
91
|
+
"info": {
|
|
92
|
+
"foreground": "cyan",
|
|
93
|
+
"background": "background"
|
|
94
|
+
},
|
|
95
|
+
"warn": {
|
|
96
|
+
"foreground": "amber",
|
|
97
|
+
"background": "background"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "unicode/display_width"
|
|
4
|
+
|
|
5
|
+
module Charming
|
|
6
|
+
module UI
|
|
7
|
+
# Width is a namespace for measuring and normalising the visual width of strings that may contain
|
|
8
|
+
# ANSI escape sequences. It delegates to `Unicode::DisplayWidth` while automatically stripping
|
|
9
|
+
# formatting codes so layout primitives can calculate exact character positions.
|
|
10
|
+
module Width
|
|
11
|
+
ANSI_PATTERN = /\e\[[0-9;]*m/
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def measure(value)
|
|
16
|
+
Unicode::DisplayWidth.of(strip_ansi(value.to_s))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def strip_ansi(value)
|
|
20
|
+
value.to_s.gsub(ANSI_PATTERN, "")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|