shellfie 0.1.1 → 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 +4 -4
- data/README.md +95 -236
- data/docs/.nojekyll +0 -0
- data/docs/index.html +205 -0
- data/docs/scripts.js +85 -0
- data/docs/styles.css +507 -0
- data/examples/simple.yml +3 -3
- data/lib/shellfie/animation_frame_builder.rb +178 -0
- data/lib/shellfie/animation_scroll_easing.rb +77 -0
- data/lib/shellfie/animation_timeline.rb +27 -0
- data/lib/shellfie/ansi_colors.rb +94 -0
- data/lib/shellfie/ansi_line_buffer.rb +87 -0
- data/lib/shellfie/ansi_normalizer.rb +51 -0
- data/lib/shellfie/ansi_parser.rb +50 -84
- data/lib/shellfie/cli.rb +22 -173
- data/lib/shellfie/cli_generate.rb +197 -0
- data/lib/shellfie/cli_info.rb +139 -0
- data/lib/shellfie/config.rb +108 -25
- data/lib/shellfie/config_defaults.rb +64 -0
- data/lib/shellfie/config_validation.rb +200 -0
- data/lib/shellfie/dependency_checker.rb +76 -0
- data/lib/shellfie/errors.rb +11 -1
- data/lib/shellfie/font_resolver.rb +58 -0
- data/lib/shellfie/format_resolver.rb +15 -0
- data/lib/shellfie/gif_generator.rb +83 -87
- data/lib/shellfie/gif_palette.rb +101 -0
- data/lib/shellfie/headless_theme_registry.rb +42 -0
- data/lib/shellfie/image_magick_command_builder.rb +75 -0
- data/lib/shellfie/line_layout.rb +137 -0
- data/lib/shellfie/output_writer.rb +41 -0
- data/lib/shellfie/parser.rb +113 -23
- data/lib/shellfie/parser_validation.rb +145 -0
- data/lib/shellfie/raster_painter.rb +157 -0
- data/lib/shellfie/render_chrome_cache.rb +40 -0
- data/lib/shellfie/render_geometry.rb +114 -0
- data/lib/shellfie/render_segment.rb +59 -0
- data/lib/shellfie/renderer.rb +79 -149
- data/lib/shellfie/rendering/shape_helpers.rb +42 -0
- data/lib/shellfie/rendering/text_painter.rb +187 -0
- data/lib/shellfie/rendering/window_chrome.rb +196 -0
- data/lib/shellfie/svg_raster_wrapper.rb +35 -0
- data/lib/shellfie/text_metrics.rb +96 -0
- data/lib/shellfie/theme_data.rb +80 -0
- data/lib/shellfie/theme_registry.rb +131 -0
- data/lib/shellfie/themes/base.rb +10 -1
- data/lib/shellfie/themes/configured.rb +61 -0
- data/lib/shellfie/themes/macos.rb +3 -1
- data/lib/shellfie/themes/ubuntu.rb +2 -1
- data/lib/shellfie/themes/windows_terminal.rb +7 -1
- data/lib/shellfie/version.rb +1 -1
- data/lib/shellfie.rb +37 -3
- metadata +37 -2
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "shape_helpers"
|
|
4
|
+
require_relative "../image_magick_command_builder"
|
|
5
|
+
require_relative "../text_metrics"
|
|
6
|
+
|
|
7
|
+
module Shellfie
|
|
8
|
+
module Rendering
|
|
9
|
+
module WindowChrome
|
|
10
|
+
include ShapeHelpers
|
|
11
|
+
|
|
12
|
+
def draw_shadow(convert, geometry)
|
|
13
|
+
shadow = theme.window_decoration[:shadow]
|
|
14
|
+
scale = geometry[:scale]
|
|
15
|
+
offset_x = (shadow[:offset_x] * scale).to_i
|
|
16
|
+
offset_y = (shadow[:offset_y] * scale).to_i
|
|
17
|
+
radius = geometry[:scaled_radius]
|
|
18
|
+
margin = geometry[:margin]
|
|
19
|
+
|
|
20
|
+
convert.fill shadow[:color]
|
|
21
|
+
draw_roundrect(
|
|
22
|
+
convert,
|
|
23
|
+
margin + offset_x,
|
|
24
|
+
margin + offset_y,
|
|
25
|
+
margin + geometry[:scaled_width] - 1 + offset_x,
|
|
26
|
+
margin + geometry[:scaled_height] - 1 + offset_y,
|
|
27
|
+
radius
|
|
28
|
+
)
|
|
29
|
+
convert.blur "0x#{(shadow[:blur] * scale).to_i}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def draw_window(convert, geometry, transparent:)
|
|
33
|
+
margin = geometry[:margin]
|
|
34
|
+
background = color_with_opacity(theme.colors[:background], config.window[:opacity], true)
|
|
35
|
+
|
|
36
|
+
convert.fill background
|
|
37
|
+
draw_roundrect(
|
|
38
|
+
convert,
|
|
39
|
+
margin,
|
|
40
|
+
margin,
|
|
41
|
+
margin + geometry[:scaled_width] - 1,
|
|
42
|
+
margin + geometry[:scaled_height] - 1,
|
|
43
|
+
geometry[:scaled_radius]
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
draw_border(convert, geometry)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def draw_title_bar(convert, geometry)
|
|
50
|
+
margin = geometry[:margin]
|
|
51
|
+
title_y2 = margin + geometry[:scaled_title_bar] - 1
|
|
52
|
+
|
|
53
|
+
convert.fill theme.colors[:title_bar]
|
|
54
|
+
draw_roundrect(
|
|
55
|
+
convert,
|
|
56
|
+
margin,
|
|
57
|
+
margin,
|
|
58
|
+
margin + geometry[:scaled_width] - 1,
|
|
59
|
+
title_y2,
|
|
60
|
+
geometry[:scaled_radius]
|
|
61
|
+
)
|
|
62
|
+
convert.fill theme.colors[:title_bar]
|
|
63
|
+
ImageMagickCommandBuilder.rectangle(
|
|
64
|
+
convert,
|
|
65
|
+
margin,
|
|
66
|
+
margin + geometry[:scaled_radius],
|
|
67
|
+
margin + geometry[:scaled_width] - 1,
|
|
68
|
+
title_y2
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
draw_title_separator(convert, geometry, title_y2)
|
|
72
|
+
draw_buttons(convert, geometry)
|
|
73
|
+
draw_title(convert, geometry)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def draw_title_separator(convert, geometry, y)
|
|
77
|
+
color = theme.colors[:title_bar_border]
|
|
78
|
+
return unless color
|
|
79
|
+
|
|
80
|
+
convert.stroke color
|
|
81
|
+
convert.strokewidth 1
|
|
82
|
+
ImageMagickCommandBuilder.line(convert, geometry[:margin], y, geometry[:margin] + geometry[:scaled_width] - 1, y)
|
|
83
|
+
convert.stroke "none"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def draw_border(convert, geometry)
|
|
87
|
+
color = theme.colors[:border]
|
|
88
|
+
return unless color
|
|
89
|
+
|
|
90
|
+
convert.fill "none"
|
|
91
|
+
convert.stroke color
|
|
92
|
+
convert.strokewidth [geometry[:scale].to_i, 1].max
|
|
93
|
+
draw_roundrect(
|
|
94
|
+
convert,
|
|
95
|
+
geometry[:margin],
|
|
96
|
+
geometry[:margin],
|
|
97
|
+
geometry[:margin] + geometry[:scaled_width] - 1,
|
|
98
|
+
geometry[:margin] + geometry[:scaled_height] - 1,
|
|
99
|
+
geometry[:scaled_radius]
|
|
100
|
+
)
|
|
101
|
+
convert.stroke "none"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def draw_buttons(convert, geometry)
|
|
105
|
+
return draw_windows_buttons(convert, geometry) if theme.button_style == :icons
|
|
106
|
+
|
|
107
|
+
button_radius = ((theme.window_decoration[:button_size] / 2.0) * geometry[:scale]).to_i
|
|
108
|
+
centers = circle_button_centers(geometry)
|
|
109
|
+
|
|
110
|
+
centers.each_with_index do |(x, y), index|
|
|
111
|
+
convert.fill theme.button_colors[index]
|
|
112
|
+
convert.stroke "rgba(0,0,0,0.18)"
|
|
113
|
+
convert.strokewidth [geometry[:scale].to_i, 1].max
|
|
114
|
+
ImageMagickCommandBuilder.circle(convert, x, y, button_radius)
|
|
115
|
+
end
|
|
116
|
+
convert.stroke "none"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def circle_button_centers(geometry)
|
|
120
|
+
scale = geometry[:scale]
|
|
121
|
+
size = (theme.window_decoration[:button_size] * scale).to_i
|
|
122
|
+
spacing = ((theme.window_decoration[:button_spacing] + theme.window_decoration[:button_size]) * scale).to_i
|
|
123
|
+
y = geometry[:margin] + geometry[:scaled_title_bar] / 2
|
|
124
|
+
|
|
125
|
+
start_x = if theme.buttons_position == :left
|
|
126
|
+
geometry[:margin] + (16 * scale).to_i
|
|
127
|
+
else
|
|
128
|
+
group_width = size * 3 + (theme.window_decoration[:button_spacing] * scale).to_i * 2
|
|
129
|
+
geometry[:margin] + geometry[:scaled_width] - (16 * scale).to_i - group_width + size / 2
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
3.times.map { |index| [start_x + index * spacing, y] }
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def draw_windows_buttons(convert, geometry)
|
|
136
|
+
scale = geometry[:scale]
|
|
137
|
+
button_width = ((theme.window_decoration[:button_width] || 46) * scale).to_i
|
|
138
|
+
icon_size = (10 * scale).to_i
|
|
139
|
+
start_x = geometry[:margin] + geometry[:scaled_width] - button_width * 3
|
|
140
|
+
center_y = geometry[:margin] + geometry[:scaled_title_bar] / 2
|
|
141
|
+
color = theme.colors[:title_text]
|
|
142
|
+
|
|
143
|
+
convert.stroke color
|
|
144
|
+
convert.strokewidth [scale.to_i, 1].max
|
|
145
|
+
convert.fill "none"
|
|
146
|
+
|
|
147
|
+
3.times do |index|
|
|
148
|
+
center_x = start_x + button_width * index + button_width / 2
|
|
149
|
+
draw_windows_icon(convert, index, center_x, center_y, icon_size)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
convert.stroke "none"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def draw_title(convert, geometry)
|
|
156
|
+
scaled_font_size = geometry[:scaled_font_size]
|
|
157
|
+
group_width = button_group_width(geometry)
|
|
158
|
+
reserve_left = theme.buttons_position == :left ? group_width + (32 * geometry[:scale]).to_i : (12 * geometry[:scale]).to_i
|
|
159
|
+
reserve_right = theme.buttons_position == :right ? group_width + (12 * geometry[:scale]).to_i : (12 * geometry[:scale]).to_i
|
|
160
|
+
available_width = geometry[:scaled_width] - reserve_left - reserve_right
|
|
161
|
+
title = fit_text(config.title.to_s, available_width, scaled_font_size)
|
|
162
|
+
title_width = TextMetrics.pixel_width(title, scaled_font_size)
|
|
163
|
+
min_x = geometry[:margin] + reserve_left
|
|
164
|
+
max_x = geometry[:margin] + geometry[:scaled_width] - reserve_right - title_width
|
|
165
|
+
centered_x = geometry[:margin] + (geometry[:scaled_width] - title_width) / 2
|
|
166
|
+
x = title_x(min_x, max_x, centered_x)
|
|
167
|
+
y = geometry[:margin] + geometry[:scaled_title_bar] / 2 + scaled_font_size / 3
|
|
168
|
+
|
|
169
|
+
draw_text(convert, title, x, y - scaled_font_size, theme.colors[:title_text], scaled_font_size, geometry[:font_config])
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def title_x(min_x, max_x, centered_x)
|
|
173
|
+
case theme.title_alignment
|
|
174
|
+
when :left
|
|
175
|
+
min_x
|
|
176
|
+
when :right
|
|
177
|
+
max_x
|
|
178
|
+
else
|
|
179
|
+
[[centered_x, min_x].max, max_x].min
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def button_group_width(geometry)
|
|
184
|
+
scale = geometry[:scale]
|
|
185
|
+
if theme.button_style == :icons
|
|
186
|
+
((theme.window_decoration[:button_width] || 46) * 3 * scale).to_i
|
|
187
|
+
else
|
|
188
|
+
size = theme.window_decoration[:button_size]
|
|
189
|
+
spacing = theme.window_decoration[:button_spacing]
|
|
190
|
+
((size * 3 + spacing * 2) * scale).to_i
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tempfile"
|
|
4
|
+
require "mini_magick"
|
|
5
|
+
|
|
6
|
+
module Shellfie
|
|
7
|
+
class SvgRasterWrapper
|
|
8
|
+
class << self
|
|
9
|
+
def write(output_path)
|
|
10
|
+
temp = Tempfile.new(["shellfie-svg", ".png"], binmode: true)
|
|
11
|
+
temp.close
|
|
12
|
+
yield temp.path
|
|
13
|
+
image = MiniMagick::Image.open(temp.path)
|
|
14
|
+
File.binwrite(output_path, svg_document(image.width, image.height, File.binread(temp.path)))
|
|
15
|
+
ensure
|
|
16
|
+
if temp
|
|
17
|
+
temp.close unless temp.closed?
|
|
18
|
+
File.delete(temp.path) if File.exist?(temp.path)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def svg_document(width, height, png_data)
|
|
25
|
+
encoded = [png_data].pack("m0")
|
|
26
|
+
<<~SVG
|
|
27
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
28
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="#{width}" height="#{height}" viewBox="0 0 #{width} #{height}">
|
|
29
|
+
<image width="#{width}" height="#{height}" href="data:image/png;base64,#{encoded}"/>
|
|
30
|
+
</svg>
|
|
31
|
+
SVG
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shellfie
|
|
4
|
+
module TextMetrics
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def cell_width(text)
|
|
8
|
+
text.to_s.each_char.sum { |char| char_width(char) }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def pixel_width(text, font_size)
|
|
12
|
+
(cell_width(text) * font_size * 0.6).ceil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def take_cells(text, max_cells)
|
|
16
|
+
result = +""
|
|
17
|
+
used_cells = 0
|
|
18
|
+
|
|
19
|
+
text.to_s.each_char do |char|
|
|
20
|
+
width = char_width(char)
|
|
21
|
+
break if used_cells + width > max_cells
|
|
22
|
+
|
|
23
|
+
result << char
|
|
24
|
+
used_cells += width
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
result
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def drop_cells(text, cells_to_drop)
|
|
31
|
+
used_cells = 0
|
|
32
|
+
text.to_s.each_char.with_object(+"") do |char, result|
|
|
33
|
+
width = char_width(char)
|
|
34
|
+
if used_cells < cells_to_drop
|
|
35
|
+
used_cells += width
|
|
36
|
+
next
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
result << char
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def split_cells(text, max_cells)
|
|
44
|
+
return [text.to_s] if max_cells <= 0
|
|
45
|
+
|
|
46
|
+
chunks = []
|
|
47
|
+
current = +""
|
|
48
|
+
used_cells = 0
|
|
49
|
+
|
|
50
|
+
text.to_s.each_char do |char|
|
|
51
|
+
width = char_width(char)
|
|
52
|
+
if used_cells.positive? && used_cells + width > max_cells
|
|
53
|
+
chunks << current
|
|
54
|
+
current = +""
|
|
55
|
+
used_cells = 0
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
current << char
|
|
59
|
+
used_cells += width
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
chunks << current unless current.empty?
|
|
63
|
+
chunks
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def char_width(char)
|
|
67
|
+
codepoint = char.ord
|
|
68
|
+
return 0 if combining?(codepoint)
|
|
69
|
+
return 0 if codepoint < 32 || codepoint == 0x7f
|
|
70
|
+
return 2 if wide?(codepoint)
|
|
71
|
+
|
|
72
|
+
1
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def combining?(codepoint)
|
|
76
|
+
(0x0300..0x036f).cover?(codepoint) ||
|
|
77
|
+
(0x1ab0..0x1aff).cover?(codepoint) ||
|
|
78
|
+
(0x1dc0..0x1dff).cover?(codepoint) ||
|
|
79
|
+
(0x20d0..0x20ff).cover?(codepoint) ||
|
|
80
|
+
(0xfe20..0xfe2f).cover?(codepoint)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def wide?(codepoint)
|
|
84
|
+
(0x1100..0x115f).cover?(codepoint) ||
|
|
85
|
+
(0x2329..0x232a).cover?(codepoint) ||
|
|
86
|
+
(0x2e80..0xa4cf).cover?(codepoint) ||
|
|
87
|
+
(0xac00..0xd7a3).cover?(codepoint) ||
|
|
88
|
+
(0xf900..0xfaff).cover?(codepoint) ||
|
|
89
|
+
(0xfe10..0xfe19).cover?(codepoint) ||
|
|
90
|
+
(0xfe30..0xfe6f).cover?(codepoint) ||
|
|
91
|
+
(0xff00..0xff60).cover?(codepoint) ||
|
|
92
|
+
(0xffe0..0xffe6).cover?(codepoint) ||
|
|
93
|
+
(0x1f300..0x1faff).cover?(codepoint)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shellfie
|
|
4
|
+
class ThemeData
|
|
5
|
+
attr_reader :name, :colors, :window_decoration, :button_colors, :buttons_position, :button_style, :font,
|
|
6
|
+
:title_alignment
|
|
7
|
+
|
|
8
|
+
def self.from_theme(theme, name:, colors: {}, window_decoration: {}, font: {}, headless: false)
|
|
9
|
+
data = new(
|
|
10
|
+
name: name,
|
|
11
|
+
colors: theme.colors.merge(colors || {}),
|
|
12
|
+
window_decoration: deep_merge(theme.window_decoration, window_decoration || {}),
|
|
13
|
+
button_colors: theme.button_colors,
|
|
14
|
+
buttons_position: theme.buttons_position,
|
|
15
|
+
button_style: theme.button_style,
|
|
16
|
+
font: theme.font.merge(font || {}),
|
|
17
|
+
title_alignment: theme.title_alignment
|
|
18
|
+
)
|
|
19
|
+
headless ? data.headless : data
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.deep_merge(base, overrides)
|
|
23
|
+
base.merge(overrides) do |_key, left, right|
|
|
24
|
+
left.is_a?(Hash) && right.is_a?(Hash) ? deep_merge(left, right) : right
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def initialize(name:, colors:, window_decoration:, button_colors:, buttons_position:, button_style:, font:,
|
|
29
|
+
title_alignment:)
|
|
30
|
+
@name = name
|
|
31
|
+
@colors = deep_freeze_copy(colors)
|
|
32
|
+
@window_decoration = deep_freeze_copy(window_decoration)
|
|
33
|
+
@button_colors = deep_freeze_copy(button_colors)
|
|
34
|
+
@buttons_position = buttons_position
|
|
35
|
+
@button_style = button_style
|
|
36
|
+
@font = deep_freeze_copy(font)
|
|
37
|
+
@title_alignment = title_alignment
|
|
38
|
+
freeze
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def color_for(name)
|
|
42
|
+
return name if name.is_a?(String) && name.start_with?("#")
|
|
43
|
+
|
|
44
|
+
colors[name.to_sym] || colors[:foreground]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def headless
|
|
48
|
+
self.class.new(
|
|
49
|
+
name: name,
|
|
50
|
+
colors: colors,
|
|
51
|
+
window_decoration: self.class.deep_merge(
|
|
52
|
+
window_decoration,
|
|
53
|
+
title_bar_height: 0,
|
|
54
|
+
corner_radius: 0,
|
|
55
|
+
button_size: 0,
|
|
56
|
+
button_spacing: 0
|
|
57
|
+
),
|
|
58
|
+
button_colors: [],
|
|
59
|
+
buttons_position: :left,
|
|
60
|
+
button_style: :none,
|
|
61
|
+
font: font,
|
|
62
|
+
title_alignment: :left
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def deep_freeze_copy(value)
|
|
69
|
+
copy = case value
|
|
70
|
+
when Hash
|
|
71
|
+
value.each_with_object({}) { |(key, nested), result| result[key] = deep_freeze_copy(nested) }
|
|
72
|
+
when Array
|
|
73
|
+
value.map { |nested| deep_freeze_copy(nested) }
|
|
74
|
+
else
|
|
75
|
+
value
|
|
76
|
+
end
|
|
77
|
+
copy.freeze
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "themes/base"
|
|
4
|
+
require_relative "themes/configured"
|
|
5
|
+
require_relative "themes/macos"
|
|
6
|
+
require_relative "themes/ubuntu"
|
|
7
|
+
require_relative "themes/windows_terminal"
|
|
8
|
+
require_relative "headless_theme_registry"
|
|
9
|
+
require_relative "theme_data"
|
|
10
|
+
|
|
11
|
+
module Shellfie
|
|
12
|
+
class ThemeRegistry
|
|
13
|
+
WINDOW_THEMES = {
|
|
14
|
+
"macos" => Themes::MacOS,
|
|
15
|
+
"ubuntu" => Themes::Ubuntu,
|
|
16
|
+
"windows" => Themes::WindowsTerminal
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
COLOR_SCHEMES = {
|
|
20
|
+
"dracula" => {
|
|
21
|
+
background: "#282a36",
|
|
22
|
+
foreground: "#f8f8f2",
|
|
23
|
+
black: "#21222c",
|
|
24
|
+
red: "#ff5555",
|
|
25
|
+
green: "#50fa7b",
|
|
26
|
+
yellow: "#f1fa8c",
|
|
27
|
+
blue: "#6272a4",
|
|
28
|
+
magenta: "#ff79c6",
|
|
29
|
+
cyan: "#8be9fd",
|
|
30
|
+
white: "#f8f8f2",
|
|
31
|
+
bright_black: "#6272a4",
|
|
32
|
+
bright_red: "#ff6e6e",
|
|
33
|
+
bright_green: "#69ff94",
|
|
34
|
+
bright_yellow: "#ffffa5",
|
|
35
|
+
bright_blue: "#d6acff",
|
|
36
|
+
bright_magenta: "#ff92df",
|
|
37
|
+
bright_cyan: "#a4ffff",
|
|
38
|
+
bright_white: "#ffffff"
|
|
39
|
+
},
|
|
40
|
+
"one_dark" => {
|
|
41
|
+
background: "#282c34",
|
|
42
|
+
foreground: "#abb2bf",
|
|
43
|
+
red: "#e06c75",
|
|
44
|
+
green: "#98c379",
|
|
45
|
+
yellow: "#e5c07b",
|
|
46
|
+
blue: "#61afef",
|
|
47
|
+
magenta: "#c678dd",
|
|
48
|
+
cyan: "#56b6c2",
|
|
49
|
+
white: "#abb2bf"
|
|
50
|
+
},
|
|
51
|
+
"solarized_dark" => {
|
|
52
|
+
background: "#002b36",
|
|
53
|
+
foreground: "#839496",
|
|
54
|
+
red: "#dc322f",
|
|
55
|
+
green: "#859900",
|
|
56
|
+
yellow: "#b58900",
|
|
57
|
+
blue: "#268bd2",
|
|
58
|
+
magenta: "#d33682",
|
|
59
|
+
cyan: "#2aa198",
|
|
60
|
+
white: "#eee8d5"
|
|
61
|
+
},
|
|
62
|
+
"catppuccin_mocha" => {
|
|
63
|
+
background: "#1e1e2e",
|
|
64
|
+
foreground: "#cdd6f4",
|
|
65
|
+
red: "#f38ba8",
|
|
66
|
+
green: "#a6e3a1",
|
|
67
|
+
yellow: "#f9e2af",
|
|
68
|
+
blue: "#89b4fa",
|
|
69
|
+
magenta: "#cba6f7",
|
|
70
|
+
cyan: "#94e2d5",
|
|
71
|
+
white: "#bac2de"
|
|
72
|
+
}
|
|
73
|
+
}.freeze
|
|
74
|
+
|
|
75
|
+
class << self
|
|
76
|
+
def build(config)
|
|
77
|
+
base_name = config.theme == "custom" ? (config.window_theme || "macos") : (config.window_theme || config.theme)
|
|
78
|
+
base_theme = fetch_window_theme(base_name).new
|
|
79
|
+
colors = color_scheme(config.color_scheme).merge(config.colors)
|
|
80
|
+
|
|
81
|
+
theme = Themes::Configured.new(
|
|
82
|
+
base_theme,
|
|
83
|
+
name: config.theme,
|
|
84
|
+
colors: colors,
|
|
85
|
+
window_decoration: config.window_decoration,
|
|
86
|
+
font: font_overrides(config)
|
|
87
|
+
)
|
|
88
|
+
theme_data = ThemeData.from_theme(theme, name: config.theme)
|
|
89
|
+
config.headless ? HeadlessThemeRegistry.build(theme_data) : theme_data
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def valid_theme?(name)
|
|
93
|
+
name == "custom" || WINDOW_THEMES.key?(name)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def valid_window_theme?(name)
|
|
97
|
+
WINDOW_THEMES.key?(name)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def valid_color_scheme?(name)
|
|
101
|
+
name.nil? || COLOR_SCHEMES.key?(name)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def available_themes
|
|
105
|
+
(WINDOW_THEMES.keys + ["custom"]).sort
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def available_color_schemes
|
|
109
|
+
COLOR_SCHEMES.keys.sort
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
def fetch_window_theme(name)
|
|
115
|
+
WINDOW_THEMES.fetch(name) do
|
|
116
|
+
raise ValidationError, "Invalid theme '#{name}'\n → Available themes: #{available_themes.join(", ")}"
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def color_scheme(name)
|
|
121
|
+
return {} unless name
|
|
122
|
+
|
|
123
|
+
COLOR_SCHEMES.fetch(name)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def font_overrides(config)
|
|
127
|
+
config.font == Config::DEFAULTS[:font] ? {} : config.font
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
data/lib/shellfie/themes/base.rb
CHANGED
|
@@ -23,6 +23,9 @@ module Shellfie
|
|
|
23
23
|
foreground: "#ffffff",
|
|
24
24
|
title_bar: "#3c3c3c",
|
|
25
25
|
title_text: "#ffffff",
|
|
26
|
+
title_bar_border: "rgba(255,255,255,0.08)",
|
|
27
|
+
border: "rgba(255,255,255,0.10)",
|
|
28
|
+
selection: "rgba(80,140,255,0.28)",
|
|
26
29
|
black: "#000000",
|
|
27
30
|
red: "#ff5555",
|
|
28
31
|
green: "#50fa7b",
|
|
@@ -54,11 +57,17 @@ module Shellfie
|
|
|
54
57
|
:left
|
|
55
58
|
end
|
|
56
59
|
|
|
60
|
+
def title_alignment
|
|
61
|
+
:center
|
|
62
|
+
end
|
|
63
|
+
|
|
57
64
|
def font
|
|
58
65
|
{
|
|
59
66
|
family: "Monaco",
|
|
60
67
|
size: 14,
|
|
61
|
-
line_height: 1.4
|
|
68
|
+
line_height: 1.4,
|
|
69
|
+
fallback_family: "Menlo",
|
|
70
|
+
emoji_family: "Apple Color Emoji"
|
|
62
71
|
}
|
|
63
72
|
end
|
|
64
73
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shellfie
|
|
4
|
+
module Themes
|
|
5
|
+
class Configured
|
|
6
|
+
def initialize(base_theme, name:, colors: {}, window_decoration: {}, font: {})
|
|
7
|
+
@base_theme = base_theme
|
|
8
|
+
@name = name
|
|
9
|
+
@colors = colors || {}
|
|
10
|
+
@window_decoration = window_decoration || {}
|
|
11
|
+
@font = font || {}
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def name
|
|
15
|
+
@name
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def window_decoration
|
|
19
|
+
deep_merge(@base_theme.window_decoration, @window_decoration)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def colors
|
|
23
|
+
@base_theme.colors.merge(@colors)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def font
|
|
27
|
+
@base_theme.font.merge(@font)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def button_colors
|
|
31
|
+
@base_theme.button_colors
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def button_style
|
|
35
|
+
@base_theme.button_style
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def buttons_position
|
|
39
|
+
@base_theme.buttons_position
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def title_alignment
|
|
43
|
+
@base_theme.title_alignment
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def color_for(name)
|
|
47
|
+
return name if name.is_a?(String) && name.start_with?("#")
|
|
48
|
+
|
|
49
|
+
colors[name.to_sym] || colors[:foreground]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def deep_merge(base, overrides)
|
|
55
|
+
base.merge(overrides) do |_key, left, right|
|
|
56
|
+
left.is_a?(Hash) && right.is_a?(Hash) ? deep_merge(left, right) : right
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -13,6 +13,7 @@ module Shellfie
|
|
|
13
13
|
{
|
|
14
14
|
title_bar_height: 32,
|
|
15
15
|
button_size: 10,
|
|
16
|
+
button_width: 46,
|
|
16
17
|
button_spacing: 0,
|
|
17
18
|
corner_radius: 0,
|
|
18
19
|
shadow: { blur: 15, offset_x: 0, offset_y: 5, color: "rgba(0,0,0,0.25)" }
|
|
@@ -52,11 +53,16 @@ module Shellfie
|
|
|
52
53
|
:right
|
|
53
54
|
end
|
|
54
55
|
|
|
56
|
+
def title_alignment
|
|
57
|
+
:left
|
|
58
|
+
end
|
|
59
|
+
|
|
55
60
|
def font
|
|
56
61
|
{
|
|
57
62
|
family: "Cascadia Mono",
|
|
58
63
|
size: 14,
|
|
59
|
-
line_height: 1.3
|
|
64
|
+
line_height: 1.3,
|
|
65
|
+
fallback_family: "Consolas"
|
|
60
66
|
}
|
|
61
67
|
end
|
|
62
68
|
end
|
data/lib/shellfie/version.rb
CHANGED