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,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shellfie
|
|
4
|
+
class RenderSegment
|
|
5
|
+
ATTRIBUTES = %i[
|
|
6
|
+
foreground background bold italic underline dim reverse strikethrough overline
|
|
7
|
+
].freeze
|
|
8
|
+
|
|
9
|
+
attr_reader :text, :foreground, :background, :bold, :italic, :underline, :dim, :reverse, :strikethrough, :overline
|
|
10
|
+
|
|
11
|
+
def self.from_segment(segment, default_color:)
|
|
12
|
+
new(
|
|
13
|
+
text: segment.text.to_s,
|
|
14
|
+
foreground: segment.foreground || default_color,
|
|
15
|
+
background: segment.background,
|
|
16
|
+
bold: segment.bold,
|
|
17
|
+
italic: segment.italic,
|
|
18
|
+
underline: segment.underline,
|
|
19
|
+
dim: segment.dim,
|
|
20
|
+
reverse: segment.reverse,
|
|
21
|
+
strikethrough: segment.strikethrough,
|
|
22
|
+
overline: segment.overline
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.copy(segment, text)
|
|
27
|
+
new(**ATTRIBUTES.each_with_object(text: text.dup) { |attribute, values| values[attribute] = segment.public_send(attribute) })
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.coalesce(segments)
|
|
31
|
+
segments.each_with_object([]) do |segment, result|
|
|
32
|
+
if result.last&.same_style?(segment)
|
|
33
|
+
result[-1] = copy(result.last, result.last.text + segment.text)
|
|
34
|
+
else
|
|
35
|
+
result << segment
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def initialize(text:, foreground: nil, background: nil, bold: false, italic: false, underline: false, dim: false,
|
|
41
|
+
reverse: false, strikethrough: false, overline: false)
|
|
42
|
+
@text = text
|
|
43
|
+
@foreground = foreground
|
|
44
|
+
@background = background
|
|
45
|
+
@bold = bold
|
|
46
|
+
@italic = italic
|
|
47
|
+
@underline = underline
|
|
48
|
+
@dim = dim
|
|
49
|
+
@reverse = reverse
|
|
50
|
+
@strikethrough = strikethrough
|
|
51
|
+
@overline = overline
|
|
52
|
+
freeze
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def same_style?(other)
|
|
56
|
+
ATTRIBUTES.all? { |attribute| public_send(attribute) == other.public_send(attribute) }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
data/lib/shellfie/renderer.rb
CHANGED
|
@@ -2,192 +2,122 @@
|
|
|
2
2
|
|
|
3
3
|
require "mini_magick"
|
|
4
4
|
require_relative "ansi_parser"
|
|
5
|
-
require_relative "
|
|
6
|
-
require_relative "
|
|
7
|
-
require_relative "
|
|
8
|
-
require_relative "
|
|
5
|
+
require_relative "dependency_checker"
|
|
6
|
+
require_relative "font_resolver"
|
|
7
|
+
require_relative "format_resolver"
|
|
8
|
+
require_relative "output_writer"
|
|
9
|
+
require_relative "raster_painter"
|
|
10
|
+
require_relative "render_chrome_cache"
|
|
11
|
+
require_relative "render_geometry"
|
|
12
|
+
require_relative "render_segment"
|
|
13
|
+
require_relative "svg_raster_wrapper"
|
|
14
|
+
require_relative "theme_registry"
|
|
9
15
|
|
|
10
16
|
module Shellfie
|
|
11
17
|
class Renderer
|
|
12
|
-
|
|
13
|
-
"macos" => Themes::MacOS,
|
|
14
|
-
"ubuntu" => Themes::Ubuntu,
|
|
15
|
-
"windows" => Themes::WindowsTerminal
|
|
16
|
-
}.freeze
|
|
18
|
+
attr_reader :config, :theme, :font_resolver
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def initialize(config)
|
|
20
|
+
def initialize(config, chrome_cache: nil)
|
|
21
21
|
@config = config
|
|
22
|
-
@
|
|
23
|
-
@
|
|
22
|
+
@chrome_cache = chrome_cache
|
|
23
|
+
@theme = ThemeRegistry.build(config)
|
|
24
|
+
@ansi_parser = AnsiParser.new(state_mode: config.window[:ansi_state] || :persistent)
|
|
25
|
+
@font_resolver = FontResolver.new(-> { imagemagick_command })
|
|
24
26
|
end
|
|
25
27
|
|
|
26
|
-
def render(output_path, scale: 1, shadow: true, transparent: false)
|
|
28
|
+
def render(output_path, scale: 1, shadow: true, transparent: false, format: nil)
|
|
27
29
|
check_dependencies!
|
|
28
|
-
|
|
29
30
|
lines = build_lines
|
|
30
|
-
|
|
31
|
-
output_path
|
|
31
|
+
extension = FormatResolver.resolve(output_path, explicit: format, default: "png")
|
|
32
|
+
OutputWriter.write(output_path, extension: extension) do |temporary_path|
|
|
33
|
+
render_method = (extension == "svg") ? :create_svg_image : :create_image
|
|
34
|
+
send(render_method, lines, temporary_path, scale: scale, shadow: shadow, transparent: transparent)
|
|
35
|
+
end
|
|
36
|
+
rescue MiniMagick::Error => e
|
|
37
|
+
raise RenderError.new("ImageMagick render failed: #{e.message}", category: :render)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def estimate(scale: 1, shadow: true)
|
|
41
|
+
geometry = build_geometry(build_lines, scale: scale, shadow: shadow)
|
|
42
|
+
geometry.slice(:canvas_width, :canvas_height, :scaled_width, :scaled_height, :logical_width, :logical_height, :scale)
|
|
32
43
|
end
|
|
33
44
|
|
|
34
45
|
private
|
|
35
46
|
|
|
36
|
-
def
|
|
37
|
-
|
|
38
|
-
|
|
47
|
+
def check_dependencies!
|
|
48
|
+
DependencyChecker.configure_mini_magick!
|
|
49
|
+
DependencyChecker.ensure_imagemagick!
|
|
39
50
|
end
|
|
40
51
|
|
|
41
|
-
def
|
|
42
|
-
|
|
43
|
-
if result.empty?
|
|
44
|
-
raise DependencyError, <<~MSG
|
|
45
|
-
ImageMagick not found
|
|
46
|
-
→ Please install ImageMagick: brew install imagemagick
|
|
47
|
-
→ Or visit: https://imagemagick.org/script/download.php
|
|
48
|
-
MSG
|
|
49
|
-
end
|
|
52
|
+
def imagemagick_command
|
|
53
|
+
@imagemagick_command ||= DependencyChecker.imagemagick_path.to_s
|
|
50
54
|
end
|
|
51
55
|
|
|
52
56
|
def build_lines
|
|
53
57
|
config.lines.flat_map do |line|
|
|
54
|
-
|
|
55
|
-
if line.prompt
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
result
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def create_image(lines, output_path, scale:, shadow:, transparent:)
|
|
70
|
-
decoration = theme.window_decoration
|
|
71
|
-
font_config = config.font.is_a?(Hash) ? config.font : theme.font
|
|
72
|
-
padding = config.window[:padding] || 20
|
|
73
|
-
width = config.window[:width] || 600
|
|
74
|
-
line_height = (font_config[:size] || 14) * (font_config[:line_height] || 1.4)
|
|
75
|
-
title_bar_height = config.headless ? 0 : decoration[:title_bar_height]
|
|
76
|
-
visible_lines = config.window[:visible_lines]
|
|
77
|
-
|
|
78
|
-
display_line_count = visible_lines || lines.size
|
|
79
|
-
content_height = display_line_count * line_height + padding * 2
|
|
80
|
-
total_height = title_bar_height + content_height
|
|
81
|
-
corner_radius = decoration[:corner_radius]
|
|
82
|
-
|
|
83
|
-
if visible_lines && lines.size > visible_lines
|
|
84
|
-
lines = lines.last(visible_lines)
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
scaled_width = (width * scale).to_i
|
|
88
|
-
scaled_height = (total_height * scale).to_i
|
|
89
|
-
scaled_padding = (padding * scale).to_i
|
|
90
|
-
scaled_title_bar = (title_bar_height * scale).to_i
|
|
91
|
-
scaled_line_height = (line_height * scale).to_i
|
|
92
|
-
scaled_font_size = ((font_config[:size] || 14) * scale).to_i
|
|
93
|
-
scaled_radius = (corner_radius * scale).to_i
|
|
94
|
-
|
|
95
|
-
margin = shadow ? (50 * scale).to_i : (10 * scale).to_i
|
|
96
|
-
canvas_width = scaled_width + margin * 2
|
|
97
|
-
canvas_height = scaled_height + margin * 2
|
|
98
|
-
|
|
99
|
-
MiniMagick.convert do |convert|
|
|
100
|
-
convert.size "#{canvas_width}x#{canvas_height}"
|
|
101
|
-
convert << "xc:transparent"
|
|
102
|
-
|
|
103
|
-
if shadow
|
|
104
|
-
convert.fill "rgba(0,0,0,0.3)"
|
|
105
|
-
shadow_offset = (10 * scale).to_i
|
|
106
|
-
convert.draw "roundrectangle #{margin + shadow_offset},#{margin + shadow_offset} " \
|
|
107
|
-
"#{margin + scaled_width - 1 + shadow_offset},#{margin + scaled_height - 1 + shadow_offset} " \
|
|
108
|
-
"#{scaled_radius},#{scaled_radius}"
|
|
109
|
-
convert.blur "0x#{(15 * scale).to_i}"
|
|
58
|
+
rendered_lines = []
|
|
59
|
+
if line.prompt || line.command
|
|
60
|
+
rendered_lines << {
|
|
61
|
+
segments: coalesce_segments(
|
|
62
|
+
parse_with_default(line.prompt.to_s, line.prompt_color) +
|
|
63
|
+
parse_with_default(line.command.to_s, line.command_color)
|
|
64
|
+
),
|
|
65
|
+
selected: line.selected
|
|
66
|
+
}
|
|
110
67
|
end
|
|
111
68
|
|
|
112
|
-
|
|
113
|
-
convert.draw "roundrectangle #{margin},#{margin} " \
|
|
114
|
-
"#{margin + scaled_width - 1},#{margin + scaled_height - 1} " \
|
|
115
|
-
"#{scaled_radius},#{scaled_radius}"
|
|
116
|
-
|
|
117
|
-
unless config.headless
|
|
118
|
-
convert.fill theme.colors[:title_bar]
|
|
119
|
-
title_y2 = margin + scaled_title_bar - 1
|
|
120
|
-
convert.draw "roundrectangle #{margin},#{margin} " \
|
|
121
|
-
"#{margin + scaled_width - 1},#{title_y2} " \
|
|
122
|
-
"#{scaled_radius},#{scaled_radius}"
|
|
123
|
-
convert.fill theme.colors[:title_bar]
|
|
124
|
-
convert.draw "rectangle #{margin},#{margin + scaled_radius} " \
|
|
125
|
-
"#{margin + scaled_width - 1},#{title_y2}"
|
|
126
|
-
|
|
127
|
-
button_x, button_y = button_positions(margin, scaled_title_bar, scale)
|
|
128
|
-
button_radius = ((theme.window_decoration[:button_size] / 2) * scale).to_i
|
|
129
|
-
|
|
130
|
-
theme.button_colors.each_with_index do |color, i|
|
|
131
|
-
spacing = ((theme.window_decoration[:button_spacing] + theme.window_decoration[:button_size]) * scale).to_i
|
|
132
|
-
bx = button_x + i * spacing
|
|
133
|
-
convert.fill color
|
|
134
|
-
convert.draw "circle #{bx},#{button_y} #{bx + button_radius},#{button_y}"
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
convert.fill theme.colors[:title_text]
|
|
138
|
-
convert.pointsize scaled_font_size
|
|
139
|
-
title_y = margin + scaled_title_bar / 2 + scaled_font_size / 3
|
|
140
|
-
title_x = margin + scaled_width / 2
|
|
141
|
-
convert.gravity "NorthWest"
|
|
142
|
-
convert.draw "text #{title_x - config.title.to_s.length * scaled_font_size / 4},#{title_y - scaled_font_size} '#{escape_text(config.title.to_s)}'"
|
|
143
|
-
end
|
|
69
|
+
next rendered_lines unless line.output
|
|
144
70
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
y = content_y + idx * scaled_line_height + scaled_font_size
|
|
148
|
-
x = margin + scaled_padding
|
|
149
|
-
draw_line_segments(convert, line[:segments], x, y, scaled_font_size, font_config)
|
|
71
|
+
line.output.to_s.split("\n", -1).each do |output_line|
|
|
72
|
+
rendered_lines << { segments: coalesce_segments(parse_with_default(output_line, line.output_color)), selected: line.selected }
|
|
150
73
|
end
|
|
151
|
-
|
|
152
|
-
convert << output_path
|
|
74
|
+
rendered_lines
|
|
153
75
|
end
|
|
154
76
|
end
|
|
155
77
|
|
|
156
|
-
def
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if theme.buttons_position == :left
|
|
161
|
-
x = margin + (16 * scale).to_i
|
|
162
|
-
else
|
|
163
|
-
x = margin + scaled_width - (16 * scale).to_i - button_size * 3
|
|
78
|
+
def parse_with_default(text, default_color)
|
|
79
|
+
@ansi_parser.parse(expand_tabs(text)).map do |segment|
|
|
80
|
+
RenderSegment.from_segment(segment, default_color: default_color)
|
|
164
81
|
end
|
|
165
|
-
y = margin + title_bar_height / 2
|
|
166
|
-
|
|
167
|
-
[x, y]
|
|
168
82
|
end
|
|
169
83
|
|
|
170
|
-
def
|
|
171
|
-
|
|
84
|
+
def coalesce_segments(segments)
|
|
85
|
+
RenderSegment.coalesce(segments)
|
|
86
|
+
end
|
|
172
87
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
88
|
+
def expand_tabs(text)
|
|
89
|
+
text.to_s.gsub("\t", " " * config.window[:tab_width])
|
|
90
|
+
end
|
|
176
91
|
|
|
177
|
-
|
|
92
|
+
def create_image(lines, output_path, scale:, shadow:, transparent:)
|
|
93
|
+
geometry = build_geometry(lines, scale: scale, shadow: shadow)
|
|
178
94
|
|
|
179
|
-
|
|
180
|
-
|
|
95
|
+
raster_painter.paint(geometry, output_path, transparent: transparent)
|
|
96
|
+
end
|
|
181
97
|
|
|
182
|
-
|
|
98
|
+
def create_svg_image(lines, output_path, scale:, shadow:, transparent:)
|
|
99
|
+
SvgRasterWrapper.write(output_path) { |png_path| create_image(lines, png_path, scale: scale, shadow: shadow, transparent: transparent) }
|
|
100
|
+
end
|
|
183
101
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
end
|
|
102
|
+
def build_geometry(lines, scale:, shadow:)
|
|
103
|
+
geometry_builder.build(lines, scale: scale, shadow: shadow)
|
|
187
104
|
end
|
|
188
105
|
|
|
189
106
|
def escape_text(text)
|
|
190
|
-
|
|
107
|
+
raster_painter.send(:escape_text, text)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def geometry_builder
|
|
111
|
+
@geometry_builder ||= RenderGeometry.new(config: config, theme: theme)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def raster_painter
|
|
115
|
+
@raster_painter ||= RasterPainter.new(
|
|
116
|
+
config: config,
|
|
117
|
+
theme: theme,
|
|
118
|
+
font_resolver: font_resolver,
|
|
119
|
+
chrome_cache: @chrome_cache
|
|
120
|
+
)
|
|
191
121
|
end
|
|
192
122
|
end
|
|
193
123
|
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../image_magick_command_builder"
|
|
4
|
+
|
|
5
|
+
module Shellfie
|
|
6
|
+
module Rendering
|
|
7
|
+
module ShapeHelpers
|
|
8
|
+
def draw_roundrect(convert, x1, y1, x2, y2, radius)
|
|
9
|
+
if radius.positive?
|
|
10
|
+
ImageMagickCommandBuilder.round_rectangle(convert, x1, y1, x2, y2, radius)
|
|
11
|
+
else
|
|
12
|
+
ImageMagickCommandBuilder.rectangle(convert, x1, y1, x2, y2)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def draw_windows_icon(convert, index, center_x, center_y, icon_size)
|
|
17
|
+
case index
|
|
18
|
+
when 0
|
|
19
|
+
ImageMagickCommandBuilder.line(convert, center_x - icon_size / 2, center_y, center_x + icon_size / 2, center_y)
|
|
20
|
+
when 1
|
|
21
|
+
ImageMagickCommandBuilder.rectangle(
|
|
22
|
+
convert,
|
|
23
|
+
center_x - icon_size / 2,
|
|
24
|
+
center_y - icon_size / 2,
|
|
25
|
+
center_x + icon_size / 2,
|
|
26
|
+
center_y + icon_size / 2
|
|
27
|
+
)
|
|
28
|
+
when 2
|
|
29
|
+
ImageMagickCommandBuilder.lines(
|
|
30
|
+
convert,
|
|
31
|
+
[
|
|
32
|
+
{ x1: center_x - icon_size / 2, y1: center_y - icon_size / 2,
|
|
33
|
+
x2: center_x + icon_size / 2, y2: center_y + icon_size / 2 },
|
|
34
|
+
{ x1: center_x + icon_size / 2, y1: center_y - icon_size / 2,
|
|
35
|
+
x2: center_x - icon_size / 2, y2: center_y + icon_size / 2 }
|
|
36
|
+
]
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../image_magick_command_builder"
|
|
4
|
+
require_relative "../text_metrics"
|
|
5
|
+
|
|
6
|
+
module Shellfie
|
|
7
|
+
module Rendering
|
|
8
|
+
module TextPainter
|
|
9
|
+
def draw_content(convert, geometry)
|
|
10
|
+
content_y = content_origin_y(geometry)
|
|
11
|
+
draw_selected_backgrounds(convert, geometry, content_y)
|
|
12
|
+
|
|
13
|
+
geometry[:lines].each_with_index do |line, index|
|
|
14
|
+
top = content_y + index * geometry[:scaled_line_height]
|
|
15
|
+
baseline = top + geometry[:scaled_font_size]
|
|
16
|
+
x = geometry[:margin] + geometry[:scaled_padding]
|
|
17
|
+
|
|
18
|
+
draw_line_segments(convert, line[:segments], x, baseline, geometry)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def draw_line_segments(convert, segments, x, baseline, geometry)
|
|
23
|
+
positioned_segments = position_segments(segments, x, baseline, geometry)
|
|
24
|
+
draw_segment_backgrounds(convert, positioned_segments)
|
|
25
|
+
draw_positioned_segments(convert, positioned_segments, geometry)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def position_segments(segments, x, baseline, geometry)
|
|
29
|
+
current_x = x
|
|
30
|
+
segments.each_with_object([]) do |segment, result|
|
|
31
|
+
text = segment.text.to_s
|
|
32
|
+
next if text.empty?
|
|
33
|
+
|
|
34
|
+
width = TextMetrics.pixel_width(text, geometry[:scaled_font_size])
|
|
35
|
+
top = baseline - geometry[:scaled_font_size]
|
|
36
|
+
foreground, background = segment_colors(segment)
|
|
37
|
+
result << {
|
|
38
|
+
segment: segment,
|
|
39
|
+
text: text,
|
|
40
|
+
x: current_x,
|
|
41
|
+
width: width,
|
|
42
|
+
top: top,
|
|
43
|
+
bottom: top + geometry[:scaled_line_height],
|
|
44
|
+
foreground: foreground,
|
|
45
|
+
background: background
|
|
46
|
+
}
|
|
47
|
+
current_x += width
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def draw_segment_backgrounds(convert, positioned_segments)
|
|
52
|
+
positioned_segments
|
|
53
|
+
.select { |item| item[:background] }
|
|
54
|
+
.group_by { |item| item[:background] }
|
|
55
|
+
.each do |background, items|
|
|
56
|
+
convert.fill background
|
|
57
|
+
ImageMagickCommandBuilder.rectangles(
|
|
58
|
+
convert,
|
|
59
|
+
items.map { |item| { x1: item[:x], y1: item[:top], x2: item[:x] + item[:width], y2: item[:bottom] } }
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def draw_positioned_segments(convert, positioned_segments, geometry)
|
|
65
|
+
positioned_segments.each do |item|
|
|
66
|
+
draw_text(
|
|
67
|
+
convert,
|
|
68
|
+
item[:text],
|
|
69
|
+
item[:x],
|
|
70
|
+
item[:top],
|
|
71
|
+
item[:foreground],
|
|
72
|
+
geometry[:scaled_font_size],
|
|
73
|
+
geometry[:font_config],
|
|
74
|
+
bold: item[:segment].bold,
|
|
75
|
+
italic: item[:segment].italic
|
|
76
|
+
)
|
|
77
|
+
draw_text_decoration(
|
|
78
|
+
convert,
|
|
79
|
+
item[:segment],
|
|
80
|
+
item[:x],
|
|
81
|
+
item[:width],
|
|
82
|
+
item[:top] + geometry[:scaled_font_size],
|
|
83
|
+
geometry
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def segment_colors(segment)
|
|
89
|
+
foreground = segment.foreground ? theme.color_for(segment.foreground) : theme.colors[:foreground]
|
|
90
|
+
background = segment.background ? theme.color_for(segment.background) : nil
|
|
91
|
+
|
|
92
|
+
if segment.reverse
|
|
93
|
+
foreground, background = background || theme.colors[:background], foreground
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
foreground = color_with_opacity(foreground, 0.6, true) if segment.dim
|
|
97
|
+
[foreground, background]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def draw_text(convert, text, x, y, color, font_size, font_config, bold: false, italic: false)
|
|
101
|
+
convert.gravity "NorthWest"
|
|
102
|
+
convert.fill color
|
|
103
|
+
convert.stroke "none"
|
|
104
|
+
font = font_resolver.resolve(font_config, italic: italic)
|
|
105
|
+
convert.font font if font
|
|
106
|
+
convert.pointsize font_size
|
|
107
|
+
convert.weight(bold ? 700 : 400)
|
|
108
|
+
convert.style(italic ? "Italic" : "Normal")
|
|
109
|
+
convert.annotate "+#{x}+#{y}", escape_text(text)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def draw_text_decoration(convert, segment, x, width, baseline, geometry)
|
|
113
|
+
return unless segment.underline || segment.strikethrough || segment.overline
|
|
114
|
+
|
|
115
|
+
line_width = [(geometry[:scaled_font_size] / 12.0).ceil, 1].max
|
|
116
|
+
convert.stroke segment_colors(segment).first
|
|
117
|
+
convert.strokewidth line_width
|
|
118
|
+
|
|
119
|
+
if segment.underline
|
|
120
|
+
y = baseline + (geometry[:scaled_font_size] * 0.12).ceil
|
|
121
|
+
ImageMagickCommandBuilder.line(convert, x, y, x + width, y)
|
|
122
|
+
end
|
|
123
|
+
if segment.strikethrough
|
|
124
|
+
y = baseline - (geometry[:scaled_font_size] * 0.35).ceil
|
|
125
|
+
ImageMagickCommandBuilder.line(convert, x, y, x + width, y)
|
|
126
|
+
end
|
|
127
|
+
if segment.overline
|
|
128
|
+
y = baseline - geometry[:scaled_font_size]
|
|
129
|
+
ImageMagickCommandBuilder.line(convert, x, y, x + width, y)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
convert.stroke "none"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def draw_selected_backgrounds(convert, geometry, content_y)
|
|
136
|
+
rectangles = geometry[:lines].each_with_index.each_with_object([]) do |(line, index), result|
|
|
137
|
+
next unless line[:selected]
|
|
138
|
+
|
|
139
|
+
x = geometry[:margin] + geometry[:scaled_padding]
|
|
140
|
+
top = content_y + index * geometry[:scaled_line_height]
|
|
141
|
+
result << {
|
|
142
|
+
x1: x,
|
|
143
|
+
y1: top,
|
|
144
|
+
x2: x + geometry[:scaled_width] - geometry[:scaled_padding] * 2,
|
|
145
|
+
y2: top + geometry[:scaled_line_height]
|
|
146
|
+
}
|
|
147
|
+
end
|
|
148
|
+
return if rectangles.empty?
|
|
149
|
+
|
|
150
|
+
convert.fill theme.colors[:selection]
|
|
151
|
+
ImageMagickCommandBuilder.rectangles(convert, rectangles)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def content_origin_y(geometry)
|
|
155
|
+
base_y = geometry[:margin] + geometry[:scaled_title_bar] + geometry[:scaled_padding]
|
|
156
|
+
base_y - (geometry[:scroll_offset].to_f * geometry[:scaled_line_height]).round
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def fit_text(text, max_width, font_size)
|
|
160
|
+
return "" if max_width <= 0
|
|
161
|
+
return text if TextMetrics.pixel_width(text, font_size) <= max_width
|
|
162
|
+
|
|
163
|
+
max_cells = [(max_width / (font_size * 0.6)).floor - 3, 0].max
|
|
164
|
+
"#{TextMetrics.take_cells(text, max_cells)}..."
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def color_with_opacity(color, opacity, allow_rgba)
|
|
168
|
+
return color unless allow_rgba && opacity < 1.0
|
|
169
|
+
|
|
170
|
+
if color.to_s.match?(/\A#[0-9a-fA-F]{6}\z/)
|
|
171
|
+
r = color[1, 2].to_i(16)
|
|
172
|
+
g = color[3, 2].to_i(16)
|
|
173
|
+
b = color[5, 2].to_i(16)
|
|
174
|
+
"rgba(#{r},#{g},#{b},#{opacity})"
|
|
175
|
+
else
|
|
176
|
+
color
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def escape_text(text)
|
|
181
|
+
text.to_s
|
|
182
|
+
.encode("UTF-8", invalid: :replace, undef: :replace, replace: "?")
|
|
183
|
+
.gsub(/[\u0000-\u001f\u007f]/, " ")
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|