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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +95 -236
  3. data/docs/.nojekyll +0 -0
  4. data/docs/index.html +205 -0
  5. data/docs/scripts.js +85 -0
  6. data/docs/styles.css +507 -0
  7. data/examples/simple.yml +3 -3
  8. data/lib/shellfie/animation_frame_builder.rb +178 -0
  9. data/lib/shellfie/animation_scroll_easing.rb +77 -0
  10. data/lib/shellfie/animation_timeline.rb +27 -0
  11. data/lib/shellfie/ansi_colors.rb +94 -0
  12. data/lib/shellfie/ansi_line_buffer.rb +87 -0
  13. data/lib/shellfie/ansi_normalizer.rb +51 -0
  14. data/lib/shellfie/ansi_parser.rb +50 -84
  15. data/lib/shellfie/cli.rb +22 -173
  16. data/lib/shellfie/cli_generate.rb +197 -0
  17. data/lib/shellfie/cli_info.rb +139 -0
  18. data/lib/shellfie/config.rb +108 -25
  19. data/lib/shellfie/config_defaults.rb +64 -0
  20. data/lib/shellfie/config_validation.rb +200 -0
  21. data/lib/shellfie/dependency_checker.rb +76 -0
  22. data/lib/shellfie/errors.rb +11 -1
  23. data/lib/shellfie/font_resolver.rb +58 -0
  24. data/lib/shellfie/format_resolver.rb +15 -0
  25. data/lib/shellfie/gif_generator.rb +83 -87
  26. data/lib/shellfie/gif_palette.rb +101 -0
  27. data/lib/shellfie/headless_theme_registry.rb +42 -0
  28. data/lib/shellfie/image_magick_command_builder.rb +75 -0
  29. data/lib/shellfie/line_layout.rb +137 -0
  30. data/lib/shellfie/output_writer.rb +41 -0
  31. data/lib/shellfie/parser.rb +113 -23
  32. data/lib/shellfie/parser_validation.rb +145 -0
  33. data/lib/shellfie/raster_painter.rb +157 -0
  34. data/lib/shellfie/render_chrome_cache.rb +40 -0
  35. data/lib/shellfie/render_geometry.rb +114 -0
  36. data/lib/shellfie/render_segment.rb +59 -0
  37. data/lib/shellfie/renderer.rb +79 -149
  38. data/lib/shellfie/rendering/shape_helpers.rb +42 -0
  39. data/lib/shellfie/rendering/text_painter.rb +187 -0
  40. data/lib/shellfie/rendering/window_chrome.rb +196 -0
  41. data/lib/shellfie/svg_raster_wrapper.rb +35 -0
  42. data/lib/shellfie/text_metrics.rb +96 -0
  43. data/lib/shellfie/theme_data.rb +80 -0
  44. data/lib/shellfie/theme_registry.rb +131 -0
  45. data/lib/shellfie/themes/base.rb +10 -1
  46. data/lib/shellfie/themes/configured.rb +61 -0
  47. data/lib/shellfie/themes/macos.rb +3 -1
  48. data/lib/shellfie/themes/ubuntu.rb +2 -1
  49. data/lib/shellfie/themes/windows_terminal.rb +7 -1
  50. data/lib/shellfie/version.rb +1 -1
  51. data/lib/shellfie.rb +37 -3
  52. 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
@@ -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
@@ -43,7 +43,9 @@ module Shellfie
43
43
  {
44
44
  family: "SF Mono",
45
45
  size: 14,
46
- line_height: 1.4
46
+ line_height: 1.4,
47
+ fallback_family: "Menlo",
48
+ emoji_family: "Apple Color Emoji"
47
49
  }
48
50
  end
49
51
  end
@@ -49,7 +49,8 @@ module Shellfie
49
49
  {
50
50
  family: "Ubuntu Mono",
51
51
  size: 14,
52
- line_height: 1.4
52
+ line_height: 1.4,
53
+ fallback_family: "DejaVu Sans Mono"
53
54
  }
54
55
  end
55
56
  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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Shellfie
4
- VERSION = "0.1.1"
4
+ VERSION = "1.0.0"
5
5
  end