shellfie 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/.rspec +3 -0
- data/CHANGELOG.md +7 -0
- data/LICENSE +21 -0
- data/README.md +334 -0
- data/Rakefile +8 -0
- data/assets/logo-header.svg +28 -0
- data/examples/animation.yml +33 -0
- data/examples/colored.yml +20 -0
- data/examples/demo.gif +0 -0
- data/examples/demo.png +0 -0
- data/examples/demo_animation.yml +31 -0
- data/examples/headless.png +0 -0
- data/examples/headless.yml +16 -0
- data/examples/scrolling.yml +48 -0
- data/examples/simple.yml +21 -0
- data/examples/theme_macos.png +0 -0
- data/examples/theme_ubuntu.png +0 -0
- data/examples/theme_windows.png +0 -0
- data/exe/shellfie +6 -0
- data/exe/shf +5 -0
- data/lib/shellfie/ansi_parser.rb +174 -0
- data/lib/shellfie/cli.rb +228 -0
- data/lib/shellfie/config.rb +65 -0
- data/lib/shellfie/errors.rb +15 -0
- data/lib/shellfie/gif_generator.rb +151 -0
- data/lib/shellfie/parser.rb +90 -0
- data/lib/shellfie/renderer.rb +193 -0
- data/lib/shellfie/themes/base.rb +72 -0
- data/lib/shellfie/themes/macos.rb +51 -0
- data/lib/shellfie/themes/ubuntu.rb +57 -0
- data/lib/shellfie/themes/windows_terminal.rb +64 -0
- data/lib/shellfie/version.rb +5 -0
- data/lib/shellfie.rb +13 -0
- data/shellfie.gemspec +32 -0
- metadata +94 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mini_magick"
|
|
4
|
+
require_relative "renderer"
|
|
5
|
+
|
|
6
|
+
module Shellfie
|
|
7
|
+
class GifGenerator
|
|
8
|
+
attr_reader :config, :theme
|
|
9
|
+
|
|
10
|
+
def initialize(config)
|
|
11
|
+
@config = config
|
|
12
|
+
@renderer = Renderer.new(config)
|
|
13
|
+
@theme = @renderer.theme
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def generate(output_path, scale: 1, shadow: true)
|
|
17
|
+
check_dependencies!
|
|
18
|
+
|
|
19
|
+
frames = build_animation_frames
|
|
20
|
+
images = render_frames(frames, scale: scale, shadow: shadow)
|
|
21
|
+
combine_to_gif(images, output_path)
|
|
22
|
+
cleanup_temp_files(images)
|
|
23
|
+
output_path
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def check_dependencies!
|
|
29
|
+
result = `which magick 2>/dev/null || which convert 2>/dev/null`.strip
|
|
30
|
+
if result.empty?
|
|
31
|
+
raise DependencyError, <<~MSG
|
|
32
|
+
ImageMagick not found
|
|
33
|
+
→ Please install ImageMagick: brew install imagemagick
|
|
34
|
+
→ Or visit: https://imagemagick.org/script/download.php
|
|
35
|
+
MSG
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def build_animation_frames
|
|
40
|
+
frames = []
|
|
41
|
+
current_lines = []
|
|
42
|
+
animation_settings = config.animation
|
|
43
|
+
|
|
44
|
+
config.frames.each do |frame|
|
|
45
|
+
if frame.type
|
|
46
|
+
typing_frames = build_typing_frames(
|
|
47
|
+
current_lines.dup,
|
|
48
|
+
frame.prompt || "",
|
|
49
|
+
frame.type,
|
|
50
|
+
animation_settings[:typing_speed] || 80
|
|
51
|
+
)
|
|
52
|
+
frames.concat(typing_frames)
|
|
53
|
+
current_lines << { prompt: frame.prompt, command: frame.type }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
if frame.output
|
|
57
|
+
frame.output.split("\n").each do |line|
|
|
58
|
+
current_lines << { output: line }
|
|
59
|
+
end
|
|
60
|
+
frames << { lines: build_display_lines(current_lines), delay: frame.delay || 100 }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
if frame.delay && frame.delay > 0 && !frame.output
|
|
64
|
+
frames << { lines: build_display_lines(current_lines), delay: frame.delay }
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
frames
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def build_typing_frames(base_lines, prompt, command, typing_speed)
|
|
72
|
+
frames = []
|
|
73
|
+
chars = command.chars
|
|
74
|
+
|
|
75
|
+
chars.each_with_index do |_char, i|
|
|
76
|
+
typed = command[0..i]
|
|
77
|
+
lines = base_lines.dup
|
|
78
|
+
lines << { prompt: prompt, command: typed, cursor: true }
|
|
79
|
+
frames << { lines: build_display_lines(lines), delay: typing_speed }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
final_lines = base_lines.dup
|
|
83
|
+
final_lines << { prompt: prompt, command: command }
|
|
84
|
+
frames << { lines: build_display_lines(final_lines), delay: typing_speed }
|
|
85
|
+
|
|
86
|
+
frames
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def build_display_lines(lines_data)
|
|
90
|
+
lines_data.map do |line_data|
|
|
91
|
+
if line_data[:prompt]
|
|
92
|
+
text = "#{line_data[:prompt]}#{line_data[:command]}"
|
|
93
|
+
text += "█" if line_data[:cursor]
|
|
94
|
+
Line.new(prompt: text, command: nil, output: nil)
|
|
95
|
+
else
|
|
96
|
+
Line.new(prompt: nil, command: nil, output: line_data[:output])
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def render_frames(frames, scale:, shadow:)
|
|
102
|
+
temp_dir = Dir.mktmpdir("shellfie")
|
|
103
|
+
images = []
|
|
104
|
+
|
|
105
|
+
frames.each_with_index do |frame, idx|
|
|
106
|
+
frame_config = create_frame_config(frame[:lines])
|
|
107
|
+
renderer = Renderer.new(frame_config)
|
|
108
|
+
output_path = File.join(temp_dir, "frame_#{format("%04d", idx)}.png")
|
|
109
|
+
renderer.render(output_path, scale: scale, shadow: shadow)
|
|
110
|
+
images << { path: output_path, delay: frame[:delay] }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
images
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def create_frame_config(lines)
|
|
117
|
+
Config.new(
|
|
118
|
+
theme: config.theme,
|
|
119
|
+
title: config.title,
|
|
120
|
+
window: config.window,
|
|
121
|
+
font: config.font,
|
|
122
|
+
lines: lines,
|
|
123
|
+
headless: config.headless
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def combine_to_gif(images, output_path)
|
|
128
|
+
MiniMagick.convert do |convert|
|
|
129
|
+
convert.dispose "background"
|
|
130
|
+
convert.loop config.animation[:loop] ? 0 : 1
|
|
131
|
+
|
|
132
|
+
images.each do |img|
|
|
133
|
+
delay = (img[:delay] / 10.0).round
|
|
134
|
+
convert.delay delay
|
|
135
|
+
convert << img[:path]
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
convert.dither "FloydSteinberg"
|
|
139
|
+
convert.colors 256
|
|
140
|
+
convert.layers "optimize"
|
|
141
|
+
convert << output_path
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def cleanup_temp_files(images)
|
|
146
|
+
images.each { |img| File.delete(img[:path]) if File.exist?(img[:path]) }
|
|
147
|
+
temp_dir = File.dirname(images.first[:path]) if images.any?
|
|
148
|
+
Dir.rmdir(temp_dir) if temp_dir && Dir.exist?(temp_dir) && Dir.empty?(temp_dir)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require_relative "config"
|
|
5
|
+
require_relative "errors"
|
|
6
|
+
|
|
7
|
+
module Shellfie
|
|
8
|
+
class Parser
|
|
9
|
+
class << self
|
|
10
|
+
def parse(path)
|
|
11
|
+
raise ParseError, "Configuration file not found: #{path}" unless File.exist?(path)
|
|
12
|
+
|
|
13
|
+
content = File.read(path)
|
|
14
|
+
parse_string(content)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def parse_string(content)
|
|
18
|
+
raw = YAML.safe_load(content, symbolize_names: true)
|
|
19
|
+
validate_config(raw)
|
|
20
|
+
build_config(raw)
|
|
21
|
+
rescue Psych::SyntaxError => e
|
|
22
|
+
raise ParseError, "Invalid YAML syntax: #{e.message}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def validate_config(raw)
|
|
28
|
+
raise ValidationError, "Empty configuration" if raw.nil? || raw.empty?
|
|
29
|
+
|
|
30
|
+
valid_themes = %w[macos ubuntu windows]
|
|
31
|
+
if raw[:theme] && !valid_themes.include?(raw[:theme])
|
|
32
|
+
raise ValidationError, "Invalid theme '#{raw[:theme]}'\n → Available themes: #{valid_themes.join(", ")}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
if raw[:lines].nil? && raw[:frames].nil?
|
|
36
|
+
raise ValidationError, "Configuration must have either 'lines' or 'frames'"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def build_config(raw)
|
|
41
|
+
options = {
|
|
42
|
+
theme: raw[:theme],
|
|
43
|
+
title: raw[:title],
|
|
44
|
+
window: symbolize_hash(raw[:window]),
|
|
45
|
+
font: symbolize_hash(raw[:font]),
|
|
46
|
+
lines: parse_lines(raw[:lines]),
|
|
47
|
+
animation: symbolize_hash(raw[:animation]),
|
|
48
|
+
frames: parse_frames(raw[:frames]),
|
|
49
|
+
headless: raw[:headless] || false
|
|
50
|
+
}.compact
|
|
51
|
+
|
|
52
|
+
Config.new(options)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def symbolize_hash(hash)
|
|
56
|
+
return nil unless hash.is_a?(Hash)
|
|
57
|
+
|
|
58
|
+
hash.transform_keys(&:to_sym)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def parse_lines(lines)
|
|
62
|
+
return [] unless lines.is_a?(Array)
|
|
63
|
+
|
|
64
|
+
lines.map do |line|
|
|
65
|
+
Line.new(
|
|
66
|
+
prompt: line[:prompt],
|
|
67
|
+
command: line[:command],
|
|
68
|
+
output: line[:output]
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def parse_frames(frames)
|
|
74
|
+
return [] unless frames.is_a?(Array)
|
|
75
|
+
|
|
76
|
+
frames.map do |frame|
|
|
77
|
+
Frame.new(
|
|
78
|
+
prompt: frame[:prompt],
|
|
79
|
+
type: frame[:type],
|
|
80
|
+
output: frame[:output],
|
|
81
|
+
delay: frame[:delay] || 0
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
Line = Struct.new(:prompt, :command, :output, keyword_init: true)
|
|
89
|
+
Frame = Struct.new(:prompt, :type, :output, :delay, keyword_init: true)
|
|
90
|
+
end
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mini_magick"
|
|
4
|
+
require_relative "ansi_parser"
|
|
5
|
+
require_relative "themes/base"
|
|
6
|
+
require_relative "themes/macos"
|
|
7
|
+
require_relative "themes/ubuntu"
|
|
8
|
+
require_relative "themes/windows_terminal"
|
|
9
|
+
|
|
10
|
+
module Shellfie
|
|
11
|
+
class Renderer
|
|
12
|
+
THEMES = {
|
|
13
|
+
"macos" => Themes::MacOS,
|
|
14
|
+
"ubuntu" => Themes::Ubuntu,
|
|
15
|
+
"windows" => Themes::WindowsTerminal
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
attr_reader :config, :theme
|
|
19
|
+
|
|
20
|
+
def initialize(config)
|
|
21
|
+
@config = config
|
|
22
|
+
@theme = load_theme(config.theme)
|
|
23
|
+
@ansi_parser = AnsiParser.new
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def render(output_path, scale: 1, shadow: true, transparent: false)
|
|
27
|
+
check_dependencies!
|
|
28
|
+
|
|
29
|
+
lines = build_lines
|
|
30
|
+
create_image(lines, output_path, scale: scale, shadow: shadow, transparent: transparent)
|
|
31
|
+
output_path
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def load_theme(name)
|
|
37
|
+
klass = THEMES[name] || THEMES["macos"]
|
|
38
|
+
klass.new
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def check_dependencies!
|
|
42
|
+
result = `which magick 2>/dev/null || which convert 2>/dev/null`.strip
|
|
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
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def build_lines
|
|
53
|
+
config.lines.flat_map do |line|
|
|
54
|
+
result = []
|
|
55
|
+
if line.prompt
|
|
56
|
+
prompt_segments = @ansi_parser.parse(line.prompt)
|
|
57
|
+
command_segments = line.command ? @ansi_parser.parse(line.command) : []
|
|
58
|
+
result << { segments: prompt_segments + command_segments }
|
|
59
|
+
end
|
|
60
|
+
if line.output
|
|
61
|
+
line.output.split("\n").each do |output_line|
|
|
62
|
+
result << { segments: @ansi_parser.parse(output_line) }
|
|
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}"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
convert.fill theme.colors[:background]
|
|
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
|
|
144
|
+
|
|
145
|
+
content_y = margin + scaled_title_bar + scaled_padding
|
|
146
|
+
lines.each_with_index do |line, idx|
|
|
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)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
convert << output_path
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def button_positions(margin, title_bar_height, scale)
|
|
157
|
+
button_size = (theme.window_decoration[:button_size] * scale).to_i
|
|
158
|
+
scaled_width = ((config.window[:width] || 600) * scale).to_i
|
|
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
|
|
164
|
+
end
|
|
165
|
+
y = margin + title_bar_height / 2
|
|
166
|
+
|
|
167
|
+
[x, y]
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def draw_line_segments(convert, segments, x, y, font_size, font_config)
|
|
171
|
+
current_x = x
|
|
172
|
+
|
|
173
|
+
segments.each do |segment|
|
|
174
|
+
color = segment.foreground ? theme.color_for(segment.foreground) : theme.colors[:foreground]
|
|
175
|
+
text = escape_text(segment.text)
|
|
176
|
+
|
|
177
|
+
next if text.empty?
|
|
178
|
+
|
|
179
|
+
convert.fill color
|
|
180
|
+
convert.pointsize font_size
|
|
181
|
+
|
|
182
|
+
convert.draw "text #{current_x},#{y} '#{text}'"
|
|
183
|
+
|
|
184
|
+
char_width = font_size * 0.6
|
|
185
|
+
current_x += (segment.text.length * char_width).to_i
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def escape_text(text)
|
|
190
|
+
text.to_s.gsub("'", "\\\\'").gsub("\\", "\\\\\\\\")
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shellfie
|
|
4
|
+
module Themes
|
|
5
|
+
class Base
|
|
6
|
+
def name
|
|
7
|
+
"base"
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def window_decoration
|
|
11
|
+
{
|
|
12
|
+
title_bar_height: 28,
|
|
13
|
+
button_size: 12,
|
|
14
|
+
button_spacing: 8,
|
|
15
|
+
corner_radius: 10,
|
|
16
|
+
shadow: { blur: 20, offset_x: 0, offset_y: 10, color: "rgba(0,0,0,0.3)" }
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def colors
|
|
21
|
+
{
|
|
22
|
+
background: "#1e1e1e",
|
|
23
|
+
foreground: "#ffffff",
|
|
24
|
+
title_bar: "#3c3c3c",
|
|
25
|
+
title_text: "#ffffff",
|
|
26
|
+
black: "#000000",
|
|
27
|
+
red: "#ff5555",
|
|
28
|
+
green: "#50fa7b",
|
|
29
|
+
yellow: "#f1fa8c",
|
|
30
|
+
blue: "#6272a4",
|
|
31
|
+
magenta: "#ff79c6",
|
|
32
|
+
cyan: "#8be9fd",
|
|
33
|
+
white: "#f8f8f2",
|
|
34
|
+
bright_black: "#6272a4",
|
|
35
|
+
bright_red: "#ff6e6e",
|
|
36
|
+
bright_green: "#69ff94",
|
|
37
|
+
bright_yellow: "#ffffa5",
|
|
38
|
+
bright_blue: "#d6acff",
|
|
39
|
+
bright_magenta: "#ff92df",
|
|
40
|
+
bright_cyan: "#a4ffff",
|
|
41
|
+
bright_white: "#ffffff"
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def button_colors
|
|
46
|
+
%w[#ff5f56 #ffbd2e #27c93f]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def button_style
|
|
50
|
+
:circles
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def buttons_position
|
|
54
|
+
:left
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def font
|
|
58
|
+
{
|
|
59
|
+
family: "Monaco",
|
|
60
|
+
size: 14,
|
|
61
|
+
line_height: 1.4
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def color_for(name)
|
|
66
|
+
return name if name.is_a?(String) && name.start_with?("#")
|
|
67
|
+
|
|
68
|
+
colors[name.to_sym] || colors[:foreground]
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Shellfie
|
|
6
|
+
module Themes
|
|
7
|
+
class MacOS < Base
|
|
8
|
+
def name
|
|
9
|
+
"macos"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def window_decoration
|
|
13
|
+
{
|
|
14
|
+
title_bar_height: 28,
|
|
15
|
+
button_size: 12,
|
|
16
|
+
button_spacing: 8,
|
|
17
|
+
corner_radius: 10,
|
|
18
|
+
shadow: { blur: 50, offset_x: 0, offset_y: 25, color: "rgba(0,0,0,0.4)" }
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def colors
|
|
23
|
+
super.merge(
|
|
24
|
+
background: "#1e1e1e",
|
|
25
|
+
title_bar: "#3c3c3c",
|
|
26
|
+
title_text: "#ffffff"
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def button_colors
|
|
31
|
+
%w[#ff5f56 #ffbd2e #27c93f]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def button_style
|
|
35
|
+
:circles
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def buttons_position
|
|
39
|
+
:left
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def font
|
|
43
|
+
{
|
|
44
|
+
family: "SF Mono",
|
|
45
|
+
size: 14,
|
|
46
|
+
line_height: 1.4
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Shellfie
|
|
6
|
+
module Themes
|
|
7
|
+
class Ubuntu < Base
|
|
8
|
+
def name
|
|
9
|
+
"ubuntu"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def window_decoration
|
|
13
|
+
{
|
|
14
|
+
title_bar_height: 36,
|
|
15
|
+
button_size: 14,
|
|
16
|
+
button_spacing: 6,
|
|
17
|
+
corner_radius: 12,
|
|
18
|
+
shadow: { blur: 30, offset_x: 0, offset_y: 15, color: "rgba(0,0,0,0.35)" }
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def colors
|
|
23
|
+
super.merge(
|
|
24
|
+
background: "#300a24",
|
|
25
|
+
foreground: "#ffffff",
|
|
26
|
+
title_bar: "#2c2c2c",
|
|
27
|
+
title_text: "#ffffff",
|
|
28
|
+
green: "#4e9a06",
|
|
29
|
+
yellow: "#c4a000",
|
|
30
|
+
blue: "#3465a4",
|
|
31
|
+
magenta: "#75507b",
|
|
32
|
+
cyan: "#06989a"
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def button_colors
|
|
37
|
+
%w[#f46067 #f5bf55 #5fc454]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def button_style
|
|
41
|
+
:circles
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def buttons_position
|
|
45
|
+
:right
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def font
|
|
49
|
+
{
|
|
50
|
+
family: "Ubuntu Mono",
|
|
51
|
+
size: 14,
|
|
52
|
+
line_height: 1.4
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Shellfie
|
|
6
|
+
module Themes
|
|
7
|
+
class WindowsTerminal < Base
|
|
8
|
+
def name
|
|
9
|
+
"windows"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def window_decoration
|
|
13
|
+
{
|
|
14
|
+
title_bar_height: 32,
|
|
15
|
+
button_size: 10,
|
|
16
|
+
button_spacing: 0,
|
|
17
|
+
corner_radius: 0,
|
|
18
|
+
shadow: { blur: 15, offset_x: 0, offset_y: 5, color: "rgba(0,0,0,0.25)" }
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def colors
|
|
23
|
+
super.merge(
|
|
24
|
+
background: "#0c0c0c",
|
|
25
|
+
foreground: "#cccccc",
|
|
26
|
+
title_bar: "#1f1f1f",
|
|
27
|
+
title_text: "#ffffff",
|
|
28
|
+
black: "#0c0c0c",
|
|
29
|
+
red: "#c50f1f",
|
|
30
|
+
green: "#13a10e",
|
|
31
|
+
yellow: "#c19c00",
|
|
32
|
+
blue: "#0037da",
|
|
33
|
+
magenta: "#881798",
|
|
34
|
+
cyan: "#3a96dd",
|
|
35
|
+
white: "#cccccc",
|
|
36
|
+
bright_black: "#767676",
|
|
37
|
+
bright_red: "#e74856",
|
|
38
|
+
bright_green: "#16c60c",
|
|
39
|
+
bright_yellow: "#f9f1a5",
|
|
40
|
+
bright_blue: "#3b78ff",
|
|
41
|
+
bright_magenta: "#b4009e",
|
|
42
|
+
bright_cyan: "#61d6d6",
|
|
43
|
+
bright_white: "#f2f2f2"
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def button_style
|
|
48
|
+
:icons
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def buttons_position
|
|
52
|
+
:right
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def font
|
|
56
|
+
{
|
|
57
|
+
family: "Cascadia Mono",
|
|
58
|
+
size: 14,
|
|
59
|
+
line_height: 1.3
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
data/lib/shellfie.rb
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "shellfie/version"
|
|
4
|
+
require_relative "shellfie/errors"
|
|
5
|
+
require_relative "shellfie/config"
|
|
6
|
+
require_relative "shellfie/parser"
|
|
7
|
+
require_relative "shellfie/ansi_parser"
|
|
8
|
+
require_relative "shellfie/renderer"
|
|
9
|
+
require_relative "shellfie/gif_generator"
|
|
10
|
+
require_relative "shellfie/cli"
|
|
11
|
+
|
|
12
|
+
module Shellfie
|
|
13
|
+
end
|