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.
@@ -0,0 +1,33 @@
1
+ # animation.yml - Animated GIF example
2
+ theme: macos
3
+ title: "Terminal — zsh"
4
+
5
+ window:
6
+ width: 600
7
+ padding: 20
8
+
9
+ animation:
10
+ typing_speed: 50
11
+ command_delay: 500
12
+ cursor_blink: true
13
+ loop: true
14
+
15
+ frames:
16
+ - prompt: "$ "
17
+ type: "echo Hello, World!"
18
+ delay: 500
19
+
20
+ - output: "Hello, World!"
21
+ delay: 1000
22
+
23
+ - prompt: "$ "
24
+ type: "ls -la"
25
+ delay: 500
26
+
27
+ - output: |
28
+ total 24
29
+ drwxr-xr-x 5 user staff 160 Jan 10 10:00 .
30
+ drwxr-xr-x 3 user staff 96 Jan 10 09:00 ..
31
+ -rw-r--r-- 1 user staff 1024 Jan 10 10:00 README.md
32
+ -rw-r--r-- 1 user staff 512 Jan 10 10:00 Gemfile
33
+ delay: 2000
@@ -0,0 +1,20 @@
1
+ # colored.yml - Example with ANSI color codes
2
+ theme: ubuntu
3
+ title: "user@ubuntu: ~/project"
4
+
5
+ window:
6
+ width: 700
7
+ padding: 20
8
+
9
+ lines:
10
+ - prompt: "\e[32muser@ubuntu\e[0m:\e[34m~/project\e[0m$ "
11
+ command: "bundle exec rspec"
12
+
13
+ - output: |
14
+ \e[32m.\e[0m\e[32m.\e[0m\e[32m.\e[0m\e[32m.\e[0m\e[32m.\e[0m\e[32m.\e[0m\e[32m.\e[0m\e[32m.\e[0m\e[32m.\e[0m\e[32m.\e[0m
15
+
16
+ - output: "Finished in 0.12 seconds (files took 0.5 seconds to load)"
17
+ - output: "\e[32m10 examples, 0 failures\e[0m"
18
+
19
+ - prompt: "\e[32muser@ubuntu\e[0m:\e[34m~/project\e[0m$ "
20
+ command: ""
data/examples/demo.gif ADDED
Binary file
data/examples/demo.png ADDED
Binary file
@@ -0,0 +1,31 @@
1
+ theme: macos
2
+ title: "Terminal — zsh"
3
+
4
+ window:
5
+ width: 550
6
+ padding: 18
7
+ visible_lines: 7
8
+
9
+ animation:
10
+ typing_speed: 35
11
+ command_delay: 400
12
+ loop: true
13
+
14
+ frames:
15
+ - prompt: "$ "
16
+ type: "shellfie generate config.yml -o demo.png"
17
+ delay: 600
18
+
19
+ - output: "Generated: demo.png"
20
+ delay: 1200
21
+
22
+ - prompt: "$ "
23
+ type: "shellfie themes"
24
+ delay: 500
25
+
26
+ - output: |
27
+ Available themes:
28
+ macos - macOS Terminal style
29
+ ubuntu - Ubuntu Terminal style
30
+ windows - Windows Terminal style
31
+ delay: 2000
Binary file
@@ -0,0 +1,16 @@
1
+ # headless.yml - Terminal output without window decoration
2
+ theme: macos
3
+ headless: true
4
+
5
+ window:
6
+ width: 500
7
+ padding: 15
8
+
9
+ lines:
10
+ - prompt: "$ "
11
+ command: "ruby -v"
12
+
13
+ - output: "ruby 3.3.0 (2024-12-25 revision 5124f9ac75) [arm64-darwin24]"
14
+
15
+ - prompt: "$ "
16
+ command: ""
@@ -0,0 +1,48 @@
1
+ # scrolling.yml - Animation with fixed visible lines and scrolling
2
+ theme: macos
3
+ title: "Terminal — zsh"
4
+
5
+ window:
6
+ width: 600
7
+ padding: 20
8
+ visible_lines: 6 # Number of lines visible in the terminal window
9
+
10
+ animation:
11
+ typing_speed: 40
12
+ command_delay: 300
13
+ loop: true
14
+
15
+ frames:
16
+ - prompt: "$ "
17
+ type: "for i in {1..10}; do echo \"Line $i\"; done"
18
+ delay: 500
19
+
20
+ - output: "Line 1"
21
+ delay: 200
22
+
23
+ - output: "Line 2"
24
+ delay: 200
25
+
26
+ - output: "Line 3"
27
+ delay: 200
28
+
29
+ - output: "Line 4"
30
+ delay: 200
31
+
32
+ - output: "Line 5"
33
+ delay: 200
34
+
35
+ - output: "Line 6"
36
+ delay: 200
37
+
38
+ - output: "Line 7"
39
+ delay: 200
40
+
41
+ - output: "Line 8"
42
+ delay: 200
43
+
44
+ - output: "Line 9"
45
+ delay: 200
46
+
47
+ - output: "Line 10"
48
+ delay: 1500
@@ -0,0 +1,21 @@
1
+ # simple.yml - Basic terminal screenshot example
2
+ theme: macos
3
+ title: "Terminal — zsh"
4
+
5
+ window:
6
+ width: 600
7
+ padding: 20
8
+
9
+ lines:
10
+ - prompt: "$ "
11
+ command: "gem install shellfie"
12
+
13
+ - output: |
14
+ Fetching shellfie-0.1.0.gem
15
+ Successfully installed shellfie-0.1.0
16
+ 1 gem installed
17
+
18
+ - prompt: "$ "
19
+ command: "shellfie --version"
20
+
21
+ - output: "shellfie 0.1.0"
Binary file
Binary file
Binary file
data/exe/shellfie ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/shellfie"
5
+
6
+ Shellfie::CLI.new(ARGV).run
data/exe/shf ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # shf - Short alias for shellfie
5
+ load File.expand_path("shellfie", __dir__)
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "strscan"
4
+
5
+ module Shellfie
6
+ Segment = Struct.new(:text, :foreground, :background, :bold, :italic, :underline, keyword_init: true)
7
+
8
+ class AnsiParser
9
+ ANSI_REGEX = /\e\[([0-9;]*)m/
10
+
11
+ COLORS = {
12
+ 30 => :black,
13
+ 31 => :red,
14
+ 32 => :green,
15
+ 33 => :yellow,
16
+ 34 => :blue,
17
+ 35 => :magenta,
18
+ 36 => :cyan,
19
+ 37 => :white,
20
+ 90 => :bright_black,
21
+ 91 => :bright_red,
22
+ 92 => :bright_green,
23
+ 93 => :bright_yellow,
24
+ 94 => :bright_blue,
25
+ 95 => :bright_magenta,
26
+ 96 => :bright_cyan,
27
+ 97 => :bright_white
28
+ }.freeze
29
+
30
+ BG_COLORS = {
31
+ 40 => :black,
32
+ 41 => :red,
33
+ 42 => :green,
34
+ 43 => :yellow,
35
+ 44 => :blue,
36
+ 45 => :magenta,
37
+ 46 => :cyan,
38
+ 47 => :white,
39
+ 100 => :bright_black,
40
+ 101 => :bright_red,
41
+ 102 => :bright_green,
42
+ 103 => :bright_yellow,
43
+ 104 => :bright_blue,
44
+ 105 => :bright_magenta,
45
+ 106 => :bright_cyan,
46
+ 107 => :bright_white
47
+ }.freeze
48
+
49
+ def initialize
50
+ reset_state
51
+ end
52
+
53
+ def parse(text)
54
+ segments = []
55
+ scanner = StringScanner.new(text)
56
+ current_text = +""
57
+
58
+ while !scanner.eos?
59
+ if scanner.scan(ANSI_REGEX)
60
+ unless current_text.empty?
61
+ segments << create_segment(current_text)
62
+ current_text = +""
63
+ end
64
+ process_codes(scanner[1])
65
+ else
66
+ current_text << scanner.getch
67
+ end
68
+ end
69
+
70
+ segments << create_segment(current_text) unless current_text.empty?
71
+ segments
72
+ end
73
+
74
+ private
75
+
76
+ def reset_state
77
+ @foreground = nil
78
+ @background = nil
79
+ @bold = false
80
+ @italic = false
81
+ @underline = false
82
+ end
83
+
84
+ def create_segment(text)
85
+ Segment.new(
86
+ text: text,
87
+ foreground: @foreground,
88
+ background: @background,
89
+ bold: @bold,
90
+ italic: @italic,
91
+ underline: @underline
92
+ )
93
+ end
94
+
95
+ def process_codes(codes_str)
96
+ return reset_state if codes_str.empty?
97
+
98
+ codes = codes_str.split(";").map(&:to_i)
99
+ i = 0
100
+
101
+ while i < codes.length
102
+ code = codes[i]
103
+
104
+ case code
105
+ when 0
106
+ reset_state
107
+ when 1
108
+ @bold = true
109
+ when 3
110
+ @italic = true
111
+ when 4
112
+ @underline = true
113
+ when 22
114
+ @bold = false
115
+ when 23
116
+ @italic = false
117
+ when 24
118
+ @underline = false
119
+ when 30..37, 90..97
120
+ @foreground = COLORS[code]
121
+ when 38
122
+ i, @foreground = parse_extended_color(codes, i)
123
+ when 39
124
+ @foreground = nil
125
+ when 40..47, 100..107
126
+ @background = BG_COLORS[code]
127
+ when 48
128
+ i, @background = parse_extended_color(codes, i)
129
+ when 49
130
+ @background = nil
131
+ end
132
+
133
+ i += 1
134
+ end
135
+ end
136
+
137
+ def parse_extended_color(codes, i)
138
+ return [i, nil] if codes[i + 1].nil?
139
+
140
+ case codes[i + 1]
141
+ when 5
142
+ color_index = codes[i + 2]
143
+ [i + 2, color_256(color_index)]
144
+ when 2
145
+ r, g, b = codes[i + 2], codes[i + 3], codes[i + 4]
146
+ [i + 4, format("#%02x%02x%02x", r, g, b)]
147
+ else
148
+ [i, nil]
149
+ end
150
+ end
151
+
152
+ def color_256(index)
153
+ return nil unless index
154
+
155
+ if index < 16
156
+ standard_colors = %i[
157
+ black red green yellow blue magenta cyan white
158
+ bright_black bright_red bright_green bright_yellow
159
+ bright_blue bright_magenta bright_cyan bright_white
160
+ ]
161
+ standard_colors[index]
162
+ elsif index < 232
163
+ index -= 16
164
+ r = (index / 36) * 51
165
+ g = ((index % 36) / 6) * 51
166
+ b = (index % 6) * 51
167
+ format("#%02x%02x%02x", r, g, b)
168
+ else
169
+ gray = (index - 232) * 10 + 8
170
+ format("#%02x%02x%02x", gray, gray, gray)
171
+ end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,228 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+ require_relative "../shellfie"
5
+
6
+ module Shellfie
7
+ class CLI
8
+ COMMANDS = %w[generate init themes validate version help].freeze
9
+
10
+ def initialize(args)
11
+ @args = args
12
+ @options = {}
13
+ end
14
+
15
+ def run
16
+ return show_help if @args.empty?
17
+
18
+ command = @args.shift
19
+
20
+ case command
21
+ when "generate", "g"
22
+ run_generate
23
+ when "init"
24
+ run_init
25
+ when "themes"
26
+ run_themes
27
+ when "validate"
28
+ run_validate
29
+ when "version", "-v", "--version"
30
+ run_version
31
+ when "help", "-h", "--help"
32
+ show_help
33
+ else
34
+ puts "Unknown command: #{command}"
35
+ puts "Run 'shellfie help' for usage information."
36
+ exit 1
37
+ end
38
+ rescue Shellfie::Error => e
39
+ puts "Error: #{e.message}"
40
+ exit determine_exit_code(e)
41
+ end
42
+
43
+ private
44
+
45
+ def run_generate
46
+ parser = OptionParser.new do |opts|
47
+ opts.banner = "Usage: shellfie generate INPUT_FILE [options]"
48
+
49
+ opts.on("-o", "--output PATH", "Output file path (required)") do |path|
50
+ @options[:output] = path
51
+ end
52
+
53
+ opts.on("-t", "--theme NAME", "Override theme (macos, ubuntu, windows)") do |theme|
54
+ @options[:theme] = theme
55
+ end
56
+
57
+ opts.on("-a", "--animate", "Generate animated GIF") do
58
+ @options[:animate] = true
59
+ end
60
+
61
+ opts.on("-s", "--scale FACTOR", "Output scale (1, 2, 3)") do |scale|
62
+ @options[:scale] = scale.to_i
63
+ end
64
+
65
+ opts.on("-w", "--width PIXELS", Integer, "Override width") do |width|
66
+ @options[:width] = width
67
+ end
68
+
69
+ opts.on("--no-shadow", "Disable shadow effect") do
70
+ @options[:shadow] = false
71
+ end
72
+
73
+ opts.on("--transparent", "Transparent background") do
74
+ @options[:transparent] = true
75
+ end
76
+
77
+ opts.on("--no-header", "Disable window header (headless mode)") do
78
+ @options[:headless] = true
79
+ end
80
+ end
81
+
82
+ parser.parse!(@args)
83
+
84
+ input_file = @args.shift
85
+ raise ConfigError, "Input file is required" unless input_file
86
+ raise ConfigError, "Output file is required (use -o option)" unless @options[:output]
87
+
88
+ config = Parser.parse(input_file)
89
+
90
+ if @options[:theme] || @options[:width] || @options[:headless]
91
+ config = Config.new(
92
+ theme: @options[:theme] || config.theme,
93
+ title: config.title,
94
+ window: config.window.merge(@options[:width] ? { width: @options[:width] } : {}),
95
+ font: config.font,
96
+ lines: config.lines,
97
+ animation: config.animation,
98
+ frames: config.frames,
99
+ headless: @options[:headless] || config.headless
100
+ )
101
+ end
102
+
103
+ if @options[:animate] || config.animated?
104
+ generator = GifGenerator.new(config)
105
+ output = generator.generate(
106
+ @options[:output],
107
+ scale: @options[:scale] || 1,
108
+ shadow: @options[:shadow] != false
109
+ )
110
+ else
111
+ renderer = Renderer.new(config)
112
+ output = renderer.render(
113
+ @options[:output],
114
+ scale: @options[:scale] || 1,
115
+ shadow: @options[:shadow] != false,
116
+ transparent: @options[:transparent] || false
117
+ )
118
+ end
119
+
120
+ puts "Generated: #{output}"
121
+ end
122
+
123
+ def run_init
124
+ sample_config = <<~YAML
125
+ # Shellfie configuration file
126
+ theme: macos
127
+ title: "Terminal — zsh"
128
+
129
+ window:
130
+ width: 600
131
+ padding: 20
132
+
133
+ lines:
134
+ - prompt: "$ "
135
+ command: "gem install shellfie"
136
+
137
+ - output: |
138
+ Fetching shellfie-#{VERSION}.gem
139
+ Successfully installed shellfie-#{VERSION}
140
+ 1 gem installed
141
+
142
+ - prompt: "$ "
143
+ command: "shellfie --version"
144
+
145
+ - output: "shellfie #{VERSION}"
146
+ YAML
147
+
148
+ puts sample_config
149
+ end
150
+
151
+ def run_themes
152
+ puts "Available themes:"
153
+ puts
154
+ puts " macos - macOS Terminal style (red/yellow/green buttons, left side)"
155
+ puts " ubuntu - Ubuntu Terminal style (buttons on right side)"
156
+ puts " windows - Windows Terminal style (square corners, icons)"
157
+ puts
158
+ puts "Use: shellfie generate config.yml -o output.png -t THEME_NAME"
159
+ end
160
+
161
+ def run_validate
162
+ input_file = @args.shift
163
+ raise ConfigError, "Input file is required" unless input_file
164
+
165
+ config = Parser.parse(input_file)
166
+ puts "✓ Configuration is valid"
167
+ puts " Theme: #{config.theme}"
168
+ puts " Title: #{config.title}"
169
+ puts " Lines: #{config.lines.size}"
170
+ puts " Mode: #{config.animated? ? "animated" : "static"}"
171
+ end
172
+
173
+ def run_version
174
+ puts "shellfie #{VERSION}"
175
+ end
176
+
177
+ def show_help
178
+ puts <<~HELP
179
+ Shellfie - Terminal screenshot-style image generator
180
+
181
+ Usage: shellfie <command> [options]
182
+ shf <command> [options]
183
+
184
+ Commands:
185
+ generate Generate image from configuration file
186
+ init Output sample configuration
187
+ themes List available themes
188
+ validate Validate configuration file
189
+ version Show version
190
+ help Show this help
191
+
192
+ Generate Options:
193
+ -o, --output PATH Output file path (required)
194
+ -t, --theme NAME Override theme (macos, ubuntu, windows)
195
+ -a, --animate Generate animated GIF
196
+ -s, --scale FACTOR Output scale (1, 2, 3)
197
+ -w, --width PIXELS Override width
198
+ --no-shadow Disable shadow effect
199
+ --no-header Disable window header (headless mode)
200
+ --transparent Transparent background
201
+
202
+ Examples:
203
+ shellfie generate config.yml -o terminal.png
204
+ shellfie generate config.yml -o demo.gif --animate
205
+ shellfie generate config.yml -o retina.png --scale 2
206
+ shellfie init > my-config.yml
207
+ shellfie themes
208
+
209
+ # Short form
210
+ shf generate config.yml -o terminal.png
211
+ shf init > config.yml
212
+ HELP
213
+ end
214
+
215
+ def determine_exit_code(error)
216
+ case error
217
+ when ParseError, ValidationError
218
+ 2
219
+ when RenderError, ImageError
220
+ 3
221
+ when DependencyError
222
+ 4
223
+ else
224
+ 1
225
+ end
226
+ end
227
+ end
228
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shellfie
4
+ class Config
5
+ DEFAULTS = {
6
+ theme: "macos",
7
+ window: {
8
+ width: 600,
9
+ padding: 20,
10
+ opacity: 1.0,
11
+ visible_lines: nil
12
+ },
13
+ font: {
14
+ family: "Monaco",
15
+ size: 14,
16
+ line_height: 1.4
17
+ },
18
+ animation: {
19
+ typing_speed: 80,
20
+ command_delay: 500,
21
+ cursor_blink: true,
22
+ loop: false
23
+ }
24
+ }.freeze
25
+
26
+ attr_reader :theme, :title, :window, :font, :lines, :animation, :frames, :headless
27
+
28
+ def initialize(options = {})
29
+ merged = merge_defaults(options)
30
+ @theme = merged[:theme]
31
+ @title = merged[:title] || "Terminal"
32
+ @window = merged[:window]
33
+ @font = merged[:font]
34
+ @lines = merged[:lines] || []
35
+ @animation = merged[:animation]
36
+ @frames = merged[:frames] || []
37
+ @headless = options[:headless] || false
38
+ end
39
+
40
+ def static?
41
+ @frames.empty?
42
+ end
43
+
44
+ def animated?
45
+ !static?
46
+ end
47
+
48
+ private
49
+
50
+ def merge_defaults(options)
51
+ result = {}
52
+ DEFAULTS.each do |key, value|
53
+ result[key] = if value.is_a?(Hash) && options[key].is_a?(Hash)
54
+ value.merge(options[key])
55
+ else
56
+ options.key?(key) ? options[key] : value
57
+ end
58
+ end
59
+ result[:title] = options[:title]
60
+ result[:lines] = options[:lines]
61
+ result[:frames] = options[:frames]
62
+ result
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shellfie
4
+ class Error < StandardError; end
5
+
6
+ class ConfigError < Error; end
7
+ class ParseError < ConfigError; end
8
+ class ValidationError < ConfigError; end
9
+
10
+ class RenderError < Error; end
11
+ class FontError < RenderError; end
12
+ class ImageError < RenderError; end
13
+
14
+ class DependencyError < Error; end
15
+ end