shellfie 0.1.1 → 1.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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +56 -2
  3. data/README.md +248 -228
  4. data/lib/shellfie/animation_frame_builder.rb +202 -0
  5. data/lib/shellfie/animation_scroll_easing.rb +77 -0
  6. data/lib/shellfie/animation_timeline.rb +28 -0
  7. data/lib/shellfie/ansi_colors.rb +94 -0
  8. data/lib/shellfie/ansi_line_buffer.rb +103 -0
  9. data/lib/shellfie/ansi_normalizer.rb +61 -0
  10. data/lib/shellfie/ansi_parser.rb +162 -83
  11. data/lib/shellfie/cassette.rb +76 -0
  12. data/lib/shellfie/cli.rb +75 -163
  13. data/lib/shellfie/cli_authoring.rb +233 -0
  14. data/lib/shellfie/cli_generate.rb +401 -0
  15. data/lib/shellfie/cli_info.rb +239 -0
  16. data/lib/shellfie/cli_run.rb +167 -0
  17. data/lib/shellfie/config.rb +112 -25
  18. data/lib/shellfie/config_defaults.rb +83 -0
  19. data/lib/shellfie/config_validation.rb +289 -0
  20. data/lib/shellfie/dependency_checker.rb +147 -0
  21. data/lib/shellfie/errors.rb +12 -1
  22. data/lib/shellfie/ffmpeg_encoder.rb +46 -0
  23. data/lib/shellfie/font_resolver.rb +68 -0
  24. data/lib/shellfie/format_resolver.rb +15 -0
  25. data/lib/shellfie/gif_generator.rb +231 -89
  26. data/lib/shellfie/gif_palette.rb +105 -0
  27. data/lib/shellfie/headless_theme_registry.rb +42 -0
  28. data/lib/shellfie/html_renderer.rb +54 -0
  29. data/lib/shellfie/image_magick_command_builder.rb +75 -0
  30. data/lib/shellfie/line_layout.rb +146 -0
  31. data/lib/shellfie/output_writer.rb +46 -0
  32. data/lib/shellfie/parser.rb +183 -30
  33. data/lib/shellfie/parser_validation.rb +178 -0
  34. data/lib/shellfie/raster_painter.rb +157 -0
  35. data/lib/shellfie/render_chrome_cache.rb +40 -0
  36. data/lib/shellfie/render_geometry.rb +115 -0
  37. data/lib/shellfie/render_segment.rb +71 -0
  38. data/lib/shellfie/renderer.rb +96 -149
  39. data/lib/shellfie/rendering/shape_helpers.rb +42 -0
  40. data/lib/shellfie/rendering/text_painter.rb +195 -0
  41. data/lib/shellfie/rendering/window_chrome.rb +196 -0
  42. data/lib/shellfie/reproducibility_manifest.rb +41 -0
  43. data/lib/shellfie/session.rb +111 -0
  44. data/lib/shellfie/session_config.rb +562 -0
  45. data/lib/shellfie/session_runner.rb +689 -0
  46. data/lib/shellfie/svg_raster_wrapper.rb +35 -0
  47. data/lib/shellfie/svg_renderer.rb +222 -0
  48. data/lib/shellfie/terminal_screen.rb +389 -0
  49. data/lib/shellfie/text_metrics.rb +155 -0
  50. data/lib/shellfie/theme_data.rb +80 -0
  51. data/lib/shellfie/theme_registry.rb +131 -0
  52. data/lib/shellfie/themes/base.rb +10 -1
  53. data/lib/shellfie/themes/configured.rb +61 -0
  54. data/lib/shellfie/themes/macos.rb +3 -1
  55. data/lib/shellfie/themes/ubuntu.rb +2 -1
  56. data/lib/shellfie/themes/windows_terminal.rb +7 -1
  57. data/lib/shellfie/transcript_renderer.rb +92 -0
  58. data/lib/shellfie/version.rb +1 -1
  59. data/lib/shellfie/yaml_safety.rb +147 -0
  60. data/lib/shellfie.rb +44 -3
  61. data/schema/shellfie-v1.schema.json +153 -0
  62. data/schema/shellfie-v2.schema.json +262 -0
  63. metadata +58 -20
  64. data/.rspec +0 -3
  65. data/Rakefile +0 -8
  66. data/examples/animation.yml +0 -33
  67. data/examples/colored.yml +0 -20
  68. data/examples/demo.gif +0 -0
  69. data/examples/demo.png +0 -0
  70. data/examples/demo_animation.yml +0 -31
  71. data/examples/headless.png +0 -0
  72. data/examples/headless.yml +0 -16
  73. data/examples/scrolling.yml +0 -48
  74. data/examples/simple.yml +0 -21
  75. data/examples/theme_macos.png +0 -0
  76. data/examples/theme_ubuntu.png +0 -0
  77. data/examples/theme_windows.png +0 -0
  78. data/shellfie.gemspec +0 -32
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+ require "yaml"
5
+
6
+ module Shellfie
7
+ module YamlSafety
8
+ MAX_DEPTH = 100
9
+ MAX_NODES = 100_000
10
+
11
+ module_function
12
+
13
+ def annotate_validation_error(error, documents, provenance: {})
14
+ if (target = validation_path(error.message)) && (origin = provenance[target])
15
+ path, local_path = origin
16
+ content = documents.assoc(path)&.last
17
+ if content && (location = location_for_path(content, local_path))
18
+ return error.class.new("#{path}:#{location[0]}:#{location[1]}: #{error.message}")
19
+ end
20
+ end
21
+
22
+ documents.each do |path, content|
23
+ next unless path && content
24
+
25
+ location = validation_location(content, error.message)
26
+ return error.class.new("#{path}:#{location[0]}:#{location[1]}: #{error.message}") if location
27
+ end
28
+
29
+ path = documents.first&.first
30
+ error.class.new(path ? "#{path}: #{error.message}" : error.message)
31
+ end
32
+
33
+ def read_file(path, max_bytes:, label: "Configuration")
34
+ raise ParseError, "#{label} file not found: #{path}" unless File.file?(path)
35
+
36
+ content = File.open(path, "rb") { |file| file.read(max_bytes + 1) }
37
+ raise ParseError, "#{label} file is too large: #{path} (max #{max_bytes} bytes)" if content.bytesize > max_bytes
38
+
39
+ content
40
+ end
41
+
42
+ def load_file(path, max_bytes:, label: "Configuration", symbolize_names: true)
43
+ value = YAML.safe_load(
44
+ read_file(path, max_bytes: max_bytes, label: label), symbolize_names: symbolize_names, aliases: true
45
+ )
46
+ validate_tree!(value)
47
+ rescue Psych::Exception => e
48
+ raise ParseError, "Invalid #{label.downcase} YAML syntax: #{e.message}"
49
+ end
50
+
51
+ def validate_tree!(value)
52
+ active = {}
53
+ nodes = 0
54
+ stack = [[value, 0, false]]
55
+
56
+ until stack.empty?
57
+ node, depth, leaving = stack.pop
58
+ next unless node.is_a?(Hash) || node.is_a?(Array)
59
+ if leaving
60
+ active.delete(node.object_id)
61
+ next
62
+ end
63
+
64
+ raise ParseError, "YAML aliases must not contain cycles" if active[node.object_id]
65
+ raise ParseError, "YAML nesting is too deep (max #{MAX_DEPTH})" if depth > MAX_DEPTH
66
+ nodes += 1
67
+ raise ParseError, "YAML structure is too large (max #{MAX_NODES} collections)" if nodes > MAX_NODES
68
+
69
+ active[node.object_id] = true
70
+ stack << [node, depth, true]
71
+ children = node.is_a?(Hash) ? node.flat_map { |key, nested| [key, nested] } : node
72
+ children.reverse_each { |child| stack << [child, depth + 1, false] }
73
+ end
74
+
75
+ value
76
+ end
77
+
78
+ def validation_location(content, message)
79
+ locations = {}
80
+ collect_locations(Psych.parse(content).root, [], locations)
81
+ target = validation_path(message)
82
+ return locations[target] if target && locations[target]
83
+
84
+ locations.sort_by { |path, _location| -path.size }.each do |path, location|
85
+ return location if message.include?(format_path(path))
86
+ end
87
+ nil
88
+ rescue Psych::Exception
89
+ nil
90
+ end
91
+
92
+ def location_for_path(content, path)
93
+ locations = {}
94
+ collect_locations(Psych.parse(content).root, [], locations)
95
+ locations[path] || locations[path[0...-1]]
96
+ rescue Psych::Exception
97
+ nil
98
+ end
99
+
100
+ def collect_locations(node, path, locations)
101
+ case node
102
+ when Psych::Nodes::Mapping
103
+ node.children.each_slice(2) do |key, value|
104
+ next unless key.respond_to?(:value)
105
+
106
+ child_path = path + [key.value.to_sym]
107
+ locations[child_path] = [key.start_line + 1, key.start_column + 1]
108
+ collect_locations(value, child_path, locations)
109
+ end
110
+ when Psych::Nodes::Sequence
111
+ node.children.each_with_index { |child, index| collect_locations(child, path + [index], locations) }
112
+ end
113
+ end
114
+
115
+ def validation_path(message)
116
+ if (match = /Unknown (.+?) key\(s\):\s*([^,\s]+)/.match(message))
117
+ prefix = match[1] == "configuration" ? [] : parse_path(match[1])
118
+ return prefix + [match[2].to_sym]
119
+ end
120
+
121
+ explicit = message[/\b(?:steps|outputs|frames|lines)\[\d+\](?:\.[a-z_]+)*|\b(?:terminal|render|window|font|animation|cursor|limits)\.[a-z_]+/]
122
+ return parse_path(explicit) if explicit
123
+
124
+ {
125
+ "Session config version" => [:version], "mode must" => [:mode], "Invalid theme" => [:theme],
126
+ "redaction" => [:redact], "requires" => [:requires], "terminal.env" => %i[terminal env],
127
+ "title must" => [:title], "headless must" => [:headless]
128
+ }.each { |fragment, path| return path if message.include?(fragment) }
129
+ nil
130
+ end
131
+
132
+ def parse_path(value)
133
+ value.to_s.scan(/[A-Za-z_][A-Za-z0-9_]*|\d+/).map { |part| part.match?(/\A\d+\z/) ? part.to_i : part.to_sym }
134
+ end
135
+
136
+ def format_path(path)
137
+ path.each_with_object(+"") do |part, result|
138
+ if part.is_a?(Integer)
139
+ result << "[#{part}]"
140
+ else
141
+ result << "." unless result.empty?
142
+ result << part.to_s
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
data/lib/shellfie.rb CHANGED
@@ -5,9 +5,50 @@ require_relative "shellfie/errors"
5
5
  require_relative "shellfie/config"
6
6
  require_relative "shellfie/parser"
7
7
  require_relative "shellfie/ansi_parser"
8
- require_relative "shellfie/renderer"
9
- require_relative "shellfie/gif_generator"
10
- require_relative "shellfie/cli"
11
8
 
12
9
  module Shellfie
10
+ autoload :AnimationFrameBuilder, File.expand_path("shellfie/animation_frame_builder", __dir__)
11
+ autoload :AnimationScrollEasing, File.expand_path("shellfie/animation_scroll_easing", __dir__)
12
+ autoload :AnimationTimeline, File.expand_path("shellfie/animation_timeline", __dir__)
13
+ autoload :CLI, File.expand_path("shellfie/cli", __dir__)
14
+ autoload :GifGenerator, File.expand_path("shellfie/gif_generator", __dir__)
15
+ autoload :GifPalette, File.expand_path("shellfie/gif_palette", __dir__)
16
+ autoload :ImageMagickCommandBuilder, File.expand_path("shellfie/image_magick_command_builder", __dir__)
17
+ autoload :RenderChromeCache, File.expand_path("shellfie/render_chrome_cache", __dir__)
18
+ autoload :Renderer, File.expand_path("shellfie/renderer", __dir__)
19
+ autoload :TerminalScreen, File.expand_path("shellfie/terminal_screen", __dir__)
20
+ autoload :TranscriptRenderer, File.expand_path("shellfie/transcript_renderer", __dir__)
21
+ autoload :Cassette, File.expand_path("shellfie/cassette", __dir__)
22
+ autoload :Session, File.expand_path("shellfie/session", __dir__)
23
+ autoload :SessionConfig, File.expand_path("shellfie/session_config", __dir__)
24
+ autoload :SessionRunner, File.expand_path("shellfie/session_runner", __dir__)
25
+ autoload :ReproducibilityManifest, File.expand_path("shellfie/reproducibility_manifest", __dir__)
26
+
27
+ class << self
28
+ def parse(source)
29
+ Parser.parse(source)
30
+ end
31
+
32
+ def validate(source)
33
+ parse(source)
34
+ true
35
+ end
36
+
37
+ def render(config_or_source, output:, animate: nil, scale: 1, shadow: true, transparent: false, format: nil)
38
+ config = config_or_source.is_a?(Config) ? config_or_source : parse(config_or_source)
39
+ animated = animate.nil? ? config.animated? : animate
40
+
41
+ if animated
42
+ GifGenerator.new(config).generate(output, scale: scale, shadow: shadow, transparent: transparent, format: format)
43
+ else
44
+ Renderer.new(config).render(output, scale: scale, shadow: shadow, transparent: transparent, format: format)
45
+ end
46
+ end
47
+
48
+ def inspect_config(source, scale: 1, shadow: true)
49
+ config = parse(source)
50
+ geometry = Renderer.new(config).estimate(scale: scale, shadow: shadow)
51
+ { config: config.to_h, theme: config.theme, geometry: geometry, fonts: Renderer.new(config).font_info }
52
+ end
53
+ end
13
54
  end
@@ -0,0 +1,153 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://raw.githubusercontent.com/ydah/shellfie/main/schema/shellfie-v1.schema.json",
4
+ "title": "Shellfie compose configuration",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "properties": {
8
+ "version": { "const": 1 },
9
+ "include": { "oneOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }] },
10
+ "include_policy": { "enum": ["allow", "root"] },
11
+ "theme": { "enum": ["macos", "ubuntu", "windows", "custom"] },
12
+ "window_theme": { "enum": ["macos", "ubuntu", "windows"] },
13
+ "color_scheme": { "enum": ["dracula", "one_dark", "solarized_dark", "catppuccin_mocha", null] },
14
+ "colors": {
15
+ "type": "object",
16
+ "propertyNames": { "enum": ["background", "foreground", "title_bar", "title_text", "title_bar_border", "border", "selection", "black", "red", "green", "yellow", "blue", "magenta", "cyan", "white", "bright_black", "bright_red", "bright_green", "bright_yellow", "bright_blue", "bright_magenta", "bright_cyan", "bright_white"] },
17
+ "additionalProperties": { "type": "string" }
18
+ },
19
+ "window_decoration": {
20
+ "type": "object",
21
+ "additionalProperties": false,
22
+ "properties": {
23
+ "title_bar_height": { "type": "number", "minimum": 0 },
24
+ "button_size": { "type": "number", "minimum": 0 },
25
+ "button_spacing": { "type": "number", "minimum": 0 },
26
+ "button_width": { "type": "number", "minimum": 0 },
27
+ "corner_radius": { "type": "number", "minimum": 0 },
28
+ "shadow": {
29
+ "type": "object", "additionalProperties": false,
30
+ "properties": {
31
+ "blur": { "type": "number", "minimum": 0 }, "offset_x": { "type": "number" },
32
+ "offset_y": { "type": "number" }, "color": { "type": "string" }
33
+ }
34
+ }
35
+ }
36
+ },
37
+ "title": { "type": "string" },
38
+ "headless": { "type": "boolean" },
39
+ "window": {
40
+ "type": "object",
41
+ "additionalProperties": false,
42
+ "properties": {
43
+ "width": { "type": "integer", "minimum": 120 },
44
+ "height": { "type": ["integer", "null"], "minimum": 1 },
45
+ "padding": { "type": "integer", "minimum": 0, "maximum": 40 },
46
+ "opacity": { "type": "number", "minimum": 0, "maximum": 1 },
47
+ "visible_lines": { "type": ["integer", "null"], "minimum": 1 },
48
+ "max_lines": { "type": ["integer", "null"], "minimum": 1 },
49
+ "max_height": { "type": ["integer", "null"], "minimum": 1 },
50
+ "wrap": { "type": "boolean" },
51
+ "overflow": { "enum": ["clip", "wrap", "scroll"] },
52
+ "margin": { "type": ["integer", "null"], "minimum": 0 },
53
+ "exact_size": { "type": "boolean" },
54
+ "trim": { "type": "boolean" },
55
+ "tab_width": { "type": "integer", "minimum": 1 },
56
+ "ambiguous_width": { "enum": [1, 2] },
57
+ "osc_policy": { "enum": ["ignore", "preserve", "apply"] },
58
+ "graphics_policy": { "enum": ["ignore", "error"] },
59
+ "ansi_state": { "enum": ["persistent", "line"] },
60
+ "background_gradient": { "type": ["array", "null"], "minItems": 2, "maxItems": 2, "items": { "type": "string" } },
61
+ "scroll_offset": { "type": "number", "minimum": 0, "maximum": 1 }
62
+ }
63
+ },
64
+ "font": {
65
+ "type": "object",
66
+ "additionalProperties": false,
67
+ "properties": {
68
+ "family": { "type": ["string", "null"] },
69
+ "size": { "type": "number", "exclusiveMinimum": 0 },
70
+ "line_height": { "type": "number", "exclusiveMinimum": 0 },
71
+ "fallback_family": { "type": ["string", "null"] },
72
+ "italic_family": { "type": ["string", "null"] },
73
+ "emoji_family": { "type": ["string", "null"] }
74
+ }
75
+ },
76
+ "animation": {
77
+ "type": "object",
78
+ "additionalProperties": false,
79
+ "properties": {
80
+ "typing_speed": { "type": "integer", "minimum": 0, "maximum": 86400000 },
81
+ "command_delay": { "type": "integer", "minimum": 0, "maximum": 86400000 },
82
+ "cursor_blink": { "type": "boolean" },
83
+ "loop": { "type": "boolean" },
84
+ "typing_jitter": { "type": "number", "minimum": 0, "maximum": 1 },
85
+ "seed": { "type": "integer", "minimum": 0, "maximum": 2147483647 },
86
+ "typing_chunk_size": { "type": "integer", "minimum": 1 },
87
+ "output_delay": { "type": "integer", "minimum": 0, "maximum": 86400000 },
88
+ "final_delay": { "type": "integer", "minimum": 0, "maximum": 86400000 },
89
+ "max_frames": { "type": ["integer", "null"], "minimum": 1 },
90
+ "dither": { "type": "boolean" },
91
+ "palette": { "enum": ["global", "adaptive", "theme"] },
92
+ "gif_colors": { "type": "integer", "minimum": 2, "maximum": 256 },
93
+ "gif_optimize": { "type": "boolean" },
94
+ "webp_lossless": { "type": "boolean" },
95
+ "webp_quality": { "type": "integer", "minimum": 0, "maximum": 100 },
96
+ "webp_method": { "type": "integer", "minimum": 0, "maximum": 6 },
97
+ "webp_near_lossless": { "type": "integer", "minimum": 0, "maximum": 100 },
98
+ "apng_prediction": { "enum": ["none", "sub", "up", "avg", "paeth", "mixed"] },
99
+ "loop_count": { "type": ["integer", "null"], "minimum": 0, "maximum": 65535 },
100
+ "direction": { "enum": ["forward", "reverse", "ping_pong"] },
101
+ "loop_offset": { "type": "integer", "minimum": 0 },
102
+ "scroll_easing": { "enum": ["linear", "ease_in", "ease_out", "ease_in_out"] },
103
+ "framerate": { "type": "integer", "minimum": 1, "maximum": 120 },
104
+ "playback_speed": { "type": "number", "exclusiveMinimum": 0, "maximum": 100 }
105
+ }
106
+ },
107
+ "cursor": {
108
+ "type": "object", "additionalProperties": false,
109
+ "properties": { "style": { "enum": ["block", "bar", "underline"] }, "color": { "type": ["string", "null"] } }
110
+ },
111
+ "limits": {
112
+ "type": "object", "additionalProperties": false,
113
+ "properties": {
114
+ "max_lines": { "type": "integer", "minimum": 1, "maximum": 10000 },
115
+ "max_frames": { "type": "integer", "minimum": 1, "maximum": 500 },
116
+ "max_render_frames": { "type": "integer", "minimum": 1, "maximum": 2000 },
117
+ "max_characters": { "type": "integer", "minimum": 1, "maximum": 200000 },
118
+ "max_pixels": { "type": "integer", "minimum": 1, "maximum": 50000000 },
119
+ "max_total_pixels": { "type": "integer", "minimum": 1, "maximum": 2000000000 },
120
+ "max_temp_bytes": { "type": "integer", "minimum": 1, "maximum": 8000000000 }
121
+ }
122
+ },
123
+ "lines": { "type": "array", "maxItems": 10000, "items": { "$ref": "#/$defs/line" } },
124
+ "frames": { "type": "array", "maxItems": 500, "items": { "$ref": "#/$defs/frame" } }
125
+ },
126
+ "anyOf": [{ "required": ["lines"] }, { "required": ["frames"] }],
127
+ "$defs": {
128
+ "line": {
129
+ "type": "object",
130
+ "additionalProperties": false,
131
+ "properties": {
132
+ "prompt": { "type": "string" }, "command": { "type": "string" }, "output": { "type": "string" },
133
+ "prompt_color": { "type": "string" }, "command_color": { "type": "string" }, "output_color": { "type": "string" },
134
+ "selected": { "type": "boolean" }
135
+ },
136
+ "anyOf": [{ "required": ["prompt"] }, { "required": ["command"] }, { "required": ["output"] }]
137
+ },
138
+ "frame": {
139
+ "type": "object",
140
+ "additionalProperties": false,
141
+ "properties": {
142
+ "prompt": { "type": "string" }, "type": { "type": "string" }, "output": { "type": "string" },
143
+ "screen": { "type": "array", "items": { "type": "string" } },
144
+ "delay": { "type": "integer", "minimum": 0, "maximum": 86400000 }, "prompt_color": { "type": "string" },
145
+ "command_color": { "type": "string" }, "output_color": { "type": "string" }
146
+ },
147
+ "allOf": [
148
+ { "anyOf": [{ "required": ["type"] }, { "required": ["output"] }, { "required": ["screen"] }, { "required": ["delay"] }] },
149
+ { "if": { "required": ["prompt"] }, "then": { "required": ["type"] } }
150
+ ]
151
+ }
152
+ }
153
+ }
@@ -0,0 +1,262 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://raw.githubusercontent.com/ydah/shellfie/main/schema/shellfie-v2.schema.json",
4
+ "title": "Shellfie executable session",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["version"],
8
+ "anyOf": [{ "required": ["steps"] }, { "required": ["include"] }],
9
+ "dependentRequired": { "include_policy": ["include"] },
10
+ "properties": {
11
+ "version": { "const": 2 },
12
+ "include": { "oneOf": [{ "type": "string" }, { "type": "array", "minItems": 1, "items": { "type": "string" } }] },
13
+ "include_policy": { "enum": ["allow", "root"] },
14
+ "mode": { "enum": ["run", "replay"] },
15
+ "title": { "type": "string" },
16
+ "theme": { "enum": ["macos", "ubuntu", "windows", "custom"] },
17
+ "vars": {
18
+ "type": "object", "maxProperties": 100,
19
+ "propertyNames": { "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" },
20
+ "additionalProperties": { "type": ["string", "number", "boolean", "null"] }
21
+ },
22
+ "step_sets": {
23
+ "type": "object", "maxProperties": 100,
24
+ "propertyNames": { "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" },
25
+ "additionalProperties": {
26
+ "type": "array", "items": { "oneOf": [{ "$ref": "#/$defs/step" }, { "$ref": "#/$defs/step_use" }] }
27
+ }
28
+ },
29
+ "terminal": {
30
+ "type": "object",
31
+ "additionalProperties": false,
32
+ "properties": {
33
+ "shell": { "type": "string" }, "columns": { "type": "integer", "minimum": 1, "maximum": 500 },
34
+ "rows": { "type": "integer", "minimum": 1, "maximum": 200 }, "cwd": { "type": "string" },
35
+ "cwd_policy": { "enum": ["allow", "root"] },
36
+ "env": {
37
+ "type": "object",
38
+ "propertyNames": { "allOf": [{ "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" }, { "not": { "const": "PS1" } }] },
39
+ "additionalProperties": { "type": ["string", "null"] }
40
+ },
41
+ "env_allowlist": {
42
+ "type": ["array", "null"], "uniqueItems": true,
43
+ "items": { "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" }
44
+ },
45
+ "timeout": { "$ref": "#/$defs/duration" },
46
+ "total_timeout": { "oneOf": [{ "$ref": "#/$defs/duration" }, { "type": "null" }] },
47
+ "prompt": { "type": "string", "pattern": "\\S" }
48
+ }
49
+ },
50
+ "requires": { "type": "array", "items": { "type": "string", "pattern": "^[A-Za-z0-9_.+-]+$" } },
51
+ "steps": {
52
+ "type": "array", "maxItems": 10000,
53
+ "items": { "oneOf": [{ "$ref": "#/$defs/step" }, { "$ref": "#/$defs/step_use" }] },
54
+ "contains": { "type": "object", "required": ["capture"] }, "minContains": 0, "maxContains": 100
55
+ },
56
+ "outputs": { "type": "array", "items": { "$ref": "#/$defs/output" } },
57
+ "render": {
58
+ "type": "object", "additionalProperties": false,
59
+ "properties": {
60
+ "window": {
61
+ "$ref": "#/$defs/window"
62
+ },
63
+ "font": {
64
+ "$ref": "#/$defs/font"
65
+ },
66
+ "animation": {
67
+ "$ref": "#/$defs/animation"
68
+ },
69
+ "headless": { "type": "boolean" }
70
+ }
71
+ },
72
+ "redact": { "type": "array", "maxItems": 100, "items": { "type": "string", "maxLength": 512 } }
73
+ },
74
+ "$defs": {
75
+ "duration": {
76
+ "oneOf": [
77
+ { "type": "number", "exclusiveMinimum": 0, "maximum": 86400 },
78
+ {
79
+ "type": "string",
80
+ "pattern": "^(?=.{1,32}s?$)0*(?:0\\.[0-9]*[1-9][0-9]*|(?:[1-9][0-9]{0,3}|[1-7][0-9]{4}|8[0-5][0-9]{3}|86[0-3][0-9]{2})(?:\\.[0-9]+)?|86400(?:\\.0+)?)s?$"
81
+ },
82
+ {
83
+ "type": "string",
84
+ "pattern": "^(?=.{1,32}ms$)0*(?:0\\.[0-9]*[1-9][0-9]*|(?:[1-9][0-9]{0,6}|[1-7][0-9]{7}|8[0-5][0-9]{6}|86[0-3][0-9]{5})(?:\\.[0-9]+)?|86400000(?:\\.0+)?)ms$"
85
+ }
86
+ ]
87
+ },
88
+ "window": {
89
+ "type": "object",
90
+ "additionalProperties": false,
91
+ "properties": {
92
+ "width": { "type": "integer", "minimum": 120 },
93
+ "height": { "type": ["integer", "null"], "minimum": 1 },
94
+ "padding": { "type": "integer", "minimum": 0, "maximum": 40 },
95
+ "opacity": { "type": "number", "minimum": 0, "maximum": 1 },
96
+ "visible_lines": { "type": ["integer", "null"], "minimum": 1 },
97
+ "max_lines": { "type": ["integer", "null"], "minimum": 1 },
98
+ "max_height": { "type": ["integer", "null"], "minimum": 1 },
99
+ "wrap": { "type": "boolean" },
100
+ "overflow": { "enum": ["clip", "wrap", "scroll"] },
101
+ "margin": { "type": ["integer", "null"], "minimum": 0 },
102
+ "exact_size": { "type": "boolean" },
103
+ "trim": { "type": "boolean" },
104
+ "tab_width": { "type": "integer", "minimum": 1 },
105
+ "ambiguous_width": { "enum": [1, 2] },
106
+ "osc_policy": { "enum": ["ignore", "preserve", "apply"] },
107
+ "graphics_policy": { "enum": ["ignore", "error"] },
108
+ "ansi_state": { "enum": ["persistent", "line"] },
109
+ "background_gradient": { "type": ["array", "null"], "minItems": 2, "maxItems": 2, "items": { "type": "string" } },
110
+ "scroll_offset": { "type": "number", "minimum": 0, "maximum": 1 }
111
+ }
112
+ },
113
+ "font": {
114
+ "type": "object",
115
+ "additionalProperties": false,
116
+ "properties": {
117
+ "family": { "type": ["string", "null"] },
118
+ "size": { "type": "number", "exclusiveMinimum": 0 },
119
+ "line_height": { "type": "number", "exclusiveMinimum": 0 },
120
+ "fallback_family": { "type": ["string", "null"] },
121
+ "italic_family": { "type": ["string", "null"] },
122
+ "emoji_family": { "type": ["string", "null"] }
123
+ }
124
+ },
125
+ "animation": {
126
+ "type": "object",
127
+ "additionalProperties": false,
128
+ "properties": {
129
+ "typing_speed": { "type": "integer", "minimum": 0, "maximum": 86400000 },
130
+ "command_delay": { "type": "integer", "minimum": 0, "maximum": 86400000 },
131
+ "cursor_blink": { "type": "boolean" },
132
+ "loop": { "type": "boolean" },
133
+ "typing_jitter": { "type": "number", "minimum": 0, "maximum": 1 },
134
+ "seed": { "type": "integer", "minimum": 0, "maximum": 2147483647 },
135
+ "typing_chunk_size": { "type": "integer", "minimum": 1 },
136
+ "output_delay": { "type": "integer", "minimum": 0, "maximum": 86400000 },
137
+ "final_delay": { "type": "integer", "minimum": 0, "maximum": 86400000 },
138
+ "max_frames": { "type": ["integer", "null"], "minimum": 1 },
139
+ "dither": { "type": "boolean" },
140
+ "palette": { "enum": ["global", "adaptive", "theme"] },
141
+ "gif_colors": { "type": "integer", "minimum": 2, "maximum": 256 },
142
+ "gif_optimize": { "type": "boolean" },
143
+ "webp_lossless": { "type": "boolean" },
144
+ "webp_quality": { "type": "integer", "minimum": 0, "maximum": 100 },
145
+ "webp_method": { "type": "integer", "minimum": 0, "maximum": 6 },
146
+ "webp_near_lossless": { "type": "integer", "minimum": 0, "maximum": 100 },
147
+ "apng_prediction": { "enum": ["none", "sub", "up", "avg", "paeth", "mixed"] },
148
+ "loop_count": { "type": ["integer", "null"], "minimum": 0, "maximum": 65535 },
149
+ "direction": { "enum": ["forward", "reverse", "ping_pong"] },
150
+ "loop_offset": { "type": "integer", "minimum": 0 },
151
+ "scroll_easing": { "enum": ["linear", "ease_in", "ease_out", "ease_in_out"] },
152
+ "framerate": { "type": "integer", "minimum": 1, "maximum": 120 },
153
+ "playback_speed": { "type": "number", "exclusiveMinimum": 0, "maximum": 100 }
154
+ }
155
+ },
156
+ "step": {
157
+ "oneOf": [
158
+ { "type": "string", "enum": ["hide", "show"] },
159
+ {
160
+ "type": "object",
161
+ "additionalProperties": false,
162
+ "properties": {
163
+ "run": { "type": "string" }, "type": { "type": "string" }, "key": { "type": "string" },
164
+ "sleep": { "$ref": "#/$defs/duration" }, "wait": { "$ref": "#/$defs/wait" },
165
+ "expect": { "$ref": "#/$defs/expect" }, "capture": { "type": "string" },
166
+ "hide": { "const": true }, "show": { "const": true }, "visibility": { "enum": ["visible", "hidden"] },
167
+ "timeout": { "$ref": "#/$defs/duration" },
168
+ "speed": { "type": "string", "maxLength": 32, "pattern": "^0*(?:[1-9][0-9]{0,2}(?:\\.[0-9]+)?|1000(?:\\.0+)?)cps$" },
169
+ "count": { "type": "integer", "minimum": 1, "maximum": 10000 },
170
+ "async": { "type": "boolean" }, "cwd": { "type": "string" },
171
+ "delay": { "$ref": "#/$defs/duration" },
172
+ "repeat": { "type": "integer", "minimum": 1, "maximum": 10000 },
173
+ "if": { "$ref": "#/$defs/condition" }
174
+ },
175
+ "oneOf": [
176
+ { "required": ["run"] }, { "required": ["type"] }, { "required": ["key"] },
177
+ { "required": ["sleep"] }, { "required": ["wait"] }, { "required": ["expect"] },
178
+ { "required": ["capture"] }, { "required": ["hide"] }, { "required": ["show"] }
179
+ ],
180
+ "dependentRequired": { "visibility": ["run"], "speed": ["type"], "count": ["key"], "cwd": ["run"], "delay": ["key"] },
181
+ "allOf": [
182
+ { "if": { "required": ["async"] }, "then": { "anyOf": [{ "required": ["run"] }, { "required": ["key"] }] } },
183
+ { "if": { "required": ["timeout"] }, "then": { "anyOf": [{ "required": ["run"] }, { "required": ["key"] }, { "required": ["wait"] }] } },
184
+ { "not": { "required": ["run", "async", "visibility"], "properties": { "async": { "const": true }, "visibility": { "const": "hidden" } } } }
185
+ ]
186
+ }
187
+ ]
188
+ },
189
+ "step_use": {
190
+ "type": "object", "required": ["use"], "additionalProperties": false,
191
+ "properties": {
192
+ "use": { "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" },
193
+ "repeat": { "type": "integer", "minimum": 1, "maximum": 10000 },
194
+ "if": { "$ref": "#/$defs/condition" }
195
+ }
196
+ },
197
+ "condition": {
198
+ "type": "object", "minProperties": 1, "additionalProperties": false,
199
+ "properties": {
200
+ "os": {
201
+ "oneOf": [
202
+ { "enum": ["macos", "linux", "windows"] },
203
+ { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "enum": ["macos", "linux", "windows"] } }
204
+ ]
205
+ },
206
+ "shell": { "type": "string" },
207
+ "ruby": { "type": "string" },
208
+ "env": { "type": "object", "additionalProperties": { "type": ["string", "null"] } }
209
+ }
210
+ },
211
+ "wait": {
212
+ "oneOf": [
213
+ { "type": "string", "maxLength": 512 },
214
+ {
215
+ "type": "object",
216
+ "additionalProperties": false,
217
+ "properties": {
218
+ "screen": { "type": "string", "maxLength": 512 }, "line": { "type": "string", "maxLength": 512 },
219
+ "prompt": { "const": true },
220
+ "stable": { "$ref": "#/$defs/duration" }, "exit": { "const": true },
221
+ "timeout": { "$ref": "#/$defs/duration" }
222
+ },
223
+ "oneOf": [
224
+ { "required": ["screen"] }, { "required": ["line"] }, { "required": ["prompt"] },
225
+ { "required": ["stable"] }, { "required": ["exit"] }
226
+ ]
227
+ }
228
+ ]
229
+ },
230
+ "expect": {
231
+ "oneOf": [
232
+ { "type": "string" },
233
+ {
234
+ "type": "object",
235
+ "additionalProperties": false,
236
+ "properties": {
237
+ "screen_contains": { "type": "string" }, "screen": { "type": "string", "maxLength": 512 },
238
+ "line": { "type": "string", "maxLength": 512 },
239
+ "exit_status": { "type": "integer", "minimum": 0, "maximum": 255 },
240
+ "cursor_row": { "type": "integer", "minimum": 0 },
241
+ "cursor_column": { "type": "integer", "minimum": 0 },
242
+ "golden": { "type": "string" },
243
+ "elapsed_under": { "$ref": "#/$defs/duration" }, "elapsed_over": { "$ref": "#/$defs/duration" }
244
+ },
245
+ "minProperties": 1
246
+ }
247
+ ]
248
+ },
249
+ "output": {
250
+ "type": "object",
251
+ "required": ["path"],
252
+ "additionalProperties": false,
253
+ "properties": {
254
+ "path": { "type": "string" },
255
+ "format": { "enum": ["png", "gif", "svg", "svg-raster", "webp", "apng", "mp4", "webm", "png-sequence", "html", "txt", "ansi", "json", "asciicast", "cast"] },
256
+ "animate": { "type": "boolean" }, "scale": { "type": "integer", "minimum": 1, "maximum": 3 },
257
+ "shadow": { "type": "boolean" }, "transparent": { "type": "boolean" },
258
+ "capture": { "type": "string" }
259
+ }
260
+ }
261
+ }
262
+ }