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,178 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "animation_scroll_easing"
|
|
4
|
+
require_relative "animation_timeline"
|
|
5
|
+
|
|
6
|
+
module Shellfie
|
|
7
|
+
class AnimationFrameBuilder
|
|
8
|
+
def initialize(config)
|
|
9
|
+
@config = config
|
|
10
|
+
@random = Random.new(0)
|
|
11
|
+
@scroll_easing = AnimationScrollEasing.new(config)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def build
|
|
15
|
+
return [{ lines: @config.lines, delay: @config.animation[:final_delay] }] if @config.frames.empty?
|
|
16
|
+
|
|
17
|
+
frames = []
|
|
18
|
+
current_lines = []
|
|
19
|
+
AnimationTimeline.new(@config).each do |event|
|
|
20
|
+
case event.kind
|
|
21
|
+
when :command
|
|
22
|
+
frames.concat(command_frames(current_lines, event.frame))
|
|
23
|
+
when :output
|
|
24
|
+
frames.concat(output_frames(current_lines, event.frame))
|
|
25
|
+
when :pause
|
|
26
|
+
frames << { lines: build_display_lines(current_lines), delay: event.frame.delay }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
if @config.animation[:final_delay].positive?
|
|
31
|
+
frames << { lines: build_display_lines(current_lines), delay: @config.animation[:final_delay] }
|
|
32
|
+
end
|
|
33
|
+
frames
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def cursor_text
|
|
37
|
+
glyph = case @config.cursor[:style]
|
|
38
|
+
when "bar"
|
|
39
|
+
"|"
|
|
40
|
+
when "underline"
|
|
41
|
+
"_"
|
|
42
|
+
else
|
|
43
|
+
"█"
|
|
44
|
+
end
|
|
45
|
+
color = @config.cursor[:color]
|
|
46
|
+
return glyph unless color
|
|
47
|
+
|
|
48
|
+
"#{ansi_color(color)}#{glyph}\e[0m"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def command_frames(current_lines, frame)
|
|
54
|
+
prompt = frame.prompt || ""
|
|
55
|
+
frames = build_typing_frames(current_lines.dup, frame)
|
|
56
|
+
current_lines << command_line(prompt, frame.type, prompt_color: frame.prompt_color, command_color: frame.command_color)
|
|
57
|
+
frames.concat(command_pause_frames(current_lines, frame))
|
|
58
|
+
frames
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def build_typing_frames(base_lines, frame)
|
|
62
|
+
frames = []
|
|
63
|
+
prompt = frame.prompt || ""
|
|
64
|
+
command = frame.type
|
|
65
|
+
chars = command.chars
|
|
66
|
+
chunk_size = @config.animation[:typing_chunk_size]
|
|
67
|
+
|
|
68
|
+
(chunk_size..chars.length).step(chunk_size).each do |index|
|
|
69
|
+
typed = chars.first(index).join
|
|
70
|
+
frames << typing_frame(base_lines, frame, typed)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
frames << typing_frame(base_lines, frame, command) if chars.length % chunk_size != 0 || frames.empty?
|
|
74
|
+
final_lines = base_lines.dup
|
|
75
|
+
final_lines << command_line(prompt, command, prompt_color: frame.prompt_color, command_color: frame.command_color)
|
|
76
|
+
frames << { lines: build_display_lines(final_lines), delay: @config.animation[:typing_speed] }
|
|
77
|
+
frames
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def typing_frame(base_lines, frame, typed)
|
|
81
|
+
lines = base_lines.dup
|
|
82
|
+
lines << command_line(
|
|
83
|
+
frame.prompt || "",
|
|
84
|
+
typed,
|
|
85
|
+
cursor: true,
|
|
86
|
+
prompt_color: frame.prompt_color,
|
|
87
|
+
command_color: frame.command_color
|
|
88
|
+
)
|
|
89
|
+
{
|
|
90
|
+
lines: build_display_lines(lines),
|
|
91
|
+
delay: jittered_delay(@config.animation[:typing_speed], @config.animation[:typing_jitter])
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def command_pause_frames(current_lines, frame)
|
|
96
|
+
delay = @config.animation[:command_delay]
|
|
97
|
+
return [] unless delay.positive?
|
|
98
|
+
return [{ lines: build_display_lines(current_lines), delay: delay }] unless @config.animation[:cursor_blink]
|
|
99
|
+
|
|
100
|
+
half_delay = [delay / 2, 1].max
|
|
101
|
+
[
|
|
102
|
+
{ lines: build_display_lines(current_lines[0...-1] + [cursor_command_line(frame)]), delay: half_delay },
|
|
103
|
+
{ lines: build_display_lines(current_lines), delay: delay - half_delay }
|
|
104
|
+
]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def output_frames(current_lines, frame)
|
|
108
|
+
output_lines = frame.output.to_s.split("\n", -1)
|
|
109
|
+
output_delay = @config.animation[:output_delay]
|
|
110
|
+
|
|
111
|
+
if output_delay.positive?
|
|
112
|
+
output_lines.each_with_index.each_with_object([]) do |(line, index), frames|
|
|
113
|
+
previous_count = current_lines.size
|
|
114
|
+
current_lines << { output: line, output_color: frame.output_color }
|
|
115
|
+
delay = @scroll_easing.output_delay(output_delay, index, output_lines.size)
|
|
116
|
+
frames.concat(
|
|
117
|
+
@scroll_easing.transition_frames(
|
|
118
|
+
build_display_lines(current_lines),
|
|
119
|
+
delay: delay,
|
|
120
|
+
previous_count: previous_count
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
else
|
|
125
|
+
output_lines.each { |line| current_lines << { output: line, output_color: frame.output_color } }
|
|
126
|
+
[{ lines: build_display_lines(current_lines), delay: frame.delay || 100 }]
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def command_line(prompt, command, cursor: false, prompt_color: nil, command_color: nil)
|
|
131
|
+
{ prompt: prompt, command: command, cursor: cursor, prompt_color: prompt_color, command_color: command_color }
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def cursor_command_line(frame)
|
|
135
|
+
command_line(
|
|
136
|
+
frame.prompt || "",
|
|
137
|
+
frame.type,
|
|
138
|
+
cursor: true,
|
|
139
|
+
prompt_color: frame.prompt_color,
|
|
140
|
+
command_color: frame.command_color
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def build_display_lines(lines_data)
|
|
145
|
+
lines_data.map do |line_data|
|
|
146
|
+
if line_data[:prompt]
|
|
147
|
+
command = line_data[:command].to_s
|
|
148
|
+
command += cursor_text if line_data[:cursor]
|
|
149
|
+
Line.new(
|
|
150
|
+
prompt: line_data[:prompt],
|
|
151
|
+
command: command,
|
|
152
|
+
output: nil,
|
|
153
|
+
prompt_color: line_data[:prompt_color],
|
|
154
|
+
command_color: line_data[:command_color]
|
|
155
|
+
)
|
|
156
|
+
else
|
|
157
|
+
Line.new(prompt: nil, command: nil, output: line_data[:output], output_color: line_data[:output_color])
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def ansi_color(color)
|
|
163
|
+
return "" unless color.to_s.match?(/\A#[0-9a-fA-F]{6}\z/)
|
|
164
|
+
|
|
165
|
+
r = color[1, 2].to_i(16)
|
|
166
|
+
g = color[3, 2].to_i(16)
|
|
167
|
+
b = color[5, 2].to_i(16)
|
|
168
|
+
"\e[38;2;#{r};#{g};#{b}m"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def jittered_delay(base_delay, jitter)
|
|
172
|
+
return base_delay unless jitter.positive?
|
|
173
|
+
|
|
174
|
+
factor = 1.0 + @random.rand(-jitter..jitter)
|
|
175
|
+
[(base_delay * factor).round, 1].max
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shellfie
|
|
4
|
+
class AnimationScrollEasing
|
|
5
|
+
def initialize(config)
|
|
6
|
+
@config = config
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def output_delay(base_delay, index, total)
|
|
10
|
+
return base_delay if total <= 1 || easing == "linear"
|
|
11
|
+
|
|
12
|
+
progress = index.to_f / (total - 1)
|
|
13
|
+
[(base_delay * delay_factor(progress)).round, 1].max
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def transition_frames(lines, delay:, previous_count:)
|
|
17
|
+
return [{ lines: lines, delay: delay }] unless scroll_transition?(delay, previous_count)
|
|
18
|
+
|
|
19
|
+
delays = split_delay(delay, scroll_frame_count(delay))
|
|
20
|
+
delays.each_with_index.map do |frame_delay, index|
|
|
21
|
+
progress = (index + 1).to_f / delays.size
|
|
22
|
+
{
|
|
23
|
+
lines: lines,
|
|
24
|
+
delay: frame_delay,
|
|
25
|
+
window: { scroll_offset: scroll_progress(progress) }
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def easing
|
|
33
|
+
@config.animation[:scroll_easing]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def scroll_transition?(delay, previous_count)
|
|
37
|
+
visible_lines = @config.window[:visible_lines]
|
|
38
|
+
visible_lines && previous_count >= visible_lines && delay.positive?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def scroll_frame_count(delay)
|
|
42
|
+
[[[(delay / 40.0).ceil, 1].max, 4].min, delay].min
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def split_delay(delay, count)
|
|
46
|
+
base_delay = delay / count
|
|
47
|
+
remainder = delay % count
|
|
48
|
+
Array.new(count) { |index| base_delay + (index < remainder ? 1 : 0) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def delay_factor(progress)
|
|
52
|
+
case easing
|
|
53
|
+
when "ease_in"
|
|
54
|
+
0.75 + progress * 0.5
|
|
55
|
+
when "ease_out"
|
|
56
|
+
1.25 - progress * 0.5
|
|
57
|
+
when "ease_in_out"
|
|
58
|
+
0.75 + (0.5 - (progress - 0.5).abs) * 1.0
|
|
59
|
+
else
|
|
60
|
+
1.0
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def scroll_progress(progress)
|
|
65
|
+
case easing
|
|
66
|
+
when "ease_in"
|
|
67
|
+
progress**2
|
|
68
|
+
when "ease_out"
|
|
69
|
+
1.0 - ((1.0 - progress)**2)
|
|
70
|
+
when "ease_in_out"
|
|
71
|
+
0.5 - Math.cos(progress * Math::PI) / 2.0
|
|
72
|
+
else
|
|
73
|
+
progress
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shellfie
|
|
4
|
+
class AnimationTimeline
|
|
5
|
+
Event = Struct.new(:kind, :frame, keyword_init: true)
|
|
6
|
+
|
|
7
|
+
def initialize(config)
|
|
8
|
+
@config = config
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def each
|
|
12
|
+
return enum_for(:each) unless block_given?
|
|
13
|
+
|
|
14
|
+
@config.frames.each do |frame|
|
|
15
|
+
yield Event.new(kind: :command, frame: frame) if frame.type
|
|
16
|
+
yield Event.new(kind: :output, frame: frame) if frame.output
|
|
17
|
+
yield Event.new(kind: :pause, frame: frame) if pause_frame?(frame)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def pause_frame?(frame)
|
|
24
|
+
frame.delay&.positive? && frame.output.nil? && frame.type.nil?
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shellfie
|
|
4
|
+
module AnsiColors
|
|
5
|
+
COLORS = {
|
|
6
|
+
30 => :black,
|
|
7
|
+
31 => :red,
|
|
8
|
+
32 => :green,
|
|
9
|
+
33 => :yellow,
|
|
10
|
+
34 => :blue,
|
|
11
|
+
35 => :magenta,
|
|
12
|
+
36 => :cyan,
|
|
13
|
+
37 => :white,
|
|
14
|
+
90 => :bright_black,
|
|
15
|
+
91 => :bright_red,
|
|
16
|
+
92 => :bright_green,
|
|
17
|
+
93 => :bright_yellow,
|
|
18
|
+
94 => :bright_blue,
|
|
19
|
+
95 => :bright_magenta,
|
|
20
|
+
96 => :bright_cyan,
|
|
21
|
+
97 => :bright_white
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
BG_COLORS = {
|
|
25
|
+
40 => :black,
|
|
26
|
+
41 => :red,
|
|
27
|
+
42 => :green,
|
|
28
|
+
43 => :yellow,
|
|
29
|
+
44 => :blue,
|
|
30
|
+
45 => :magenta,
|
|
31
|
+
46 => :cyan,
|
|
32
|
+
47 => :white,
|
|
33
|
+
100 => :bright_black,
|
|
34
|
+
101 => :bright_red,
|
|
35
|
+
102 => :bright_green,
|
|
36
|
+
103 => :bright_yellow,
|
|
37
|
+
104 => :bright_blue,
|
|
38
|
+
105 => :bright_magenta,
|
|
39
|
+
106 => :bright_cyan,
|
|
40
|
+
107 => :bright_white
|
|
41
|
+
}.freeze
|
|
42
|
+
|
|
43
|
+
XTERM_COLOR_STEPS = [0, 95, 135, 175, 215, 255].freeze
|
|
44
|
+
|
|
45
|
+
module_function
|
|
46
|
+
|
|
47
|
+
def parse_extended_color(codes, index)
|
|
48
|
+
return [index, nil] if codes[index + 1].nil?
|
|
49
|
+
|
|
50
|
+
case codes[index + 1]
|
|
51
|
+
when 5
|
|
52
|
+
color_index = codes[index + 2]
|
|
53
|
+
return [index, nil] unless color_index
|
|
54
|
+
|
|
55
|
+
[index + 2, color_256(color_index)]
|
|
56
|
+
when 2
|
|
57
|
+
rgb = codes.values_at(index + 2, index + 3, index + 4)
|
|
58
|
+
return [index, nil] unless rgb.all? { |value| value.is_a?(Integer) && value.between?(0, 255) }
|
|
59
|
+
|
|
60
|
+
[index + 4, format("#%02x%02x%02x", *rgb)]
|
|
61
|
+
else
|
|
62
|
+
[index, nil]
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def color_256(index)
|
|
67
|
+
return nil unless index.is_a?(Integer) && index.between?(0, 255)
|
|
68
|
+
|
|
69
|
+
if index < 16
|
|
70
|
+
standard_colors[index]
|
|
71
|
+
elsif index < 232
|
|
72
|
+
color_cube(index - 16)
|
|
73
|
+
else
|
|
74
|
+
gray = (index - 232) * 10 + 8
|
|
75
|
+
format("#%02x%02x%02x", gray, gray, gray)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def standard_colors
|
|
80
|
+
%i[
|
|
81
|
+
black red green yellow blue magenta cyan white
|
|
82
|
+
bright_black bright_red bright_green bright_yellow
|
|
83
|
+
bright_blue bright_magenta bright_cyan bright_white
|
|
84
|
+
]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def color_cube(index)
|
|
88
|
+
r = XTERM_COLOR_STEPS[index / 36]
|
|
89
|
+
g = XTERM_COLOR_STEPS[(index % 36) / 6]
|
|
90
|
+
b = XTERM_COLOR_STEPS[index % 6]
|
|
91
|
+
format("#%02x%02x%02x", r, g, b)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shellfie
|
|
4
|
+
class AnsiLineBuffer
|
|
5
|
+
def initialize
|
|
6
|
+
@cells = []
|
|
7
|
+
@column = 0
|
|
8
|
+
@pending_escape = +""
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def write_escape(sequence)
|
|
12
|
+
@pending_escape << sequence
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def write_character(char)
|
|
16
|
+
case char
|
|
17
|
+
when "\r"
|
|
18
|
+
@column = 0
|
|
19
|
+
when "\b"
|
|
20
|
+
@column = [@column - 1, 0].max
|
|
21
|
+
@cells[@column] = nil
|
|
22
|
+
when "\a"
|
|
23
|
+
nil
|
|
24
|
+
else
|
|
25
|
+
@cells[@column] = "#{@pending_escape}#{char}"
|
|
26
|
+
@pending_escape = +""
|
|
27
|
+
@column += 1
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def move(command, params)
|
|
32
|
+
amount = first_param(params, default: 1)
|
|
33
|
+
case command
|
|
34
|
+
when "C"
|
|
35
|
+
@column += amount
|
|
36
|
+
when "D"
|
|
37
|
+
@column = [@column - amount, 0].max
|
|
38
|
+
when "G"
|
|
39
|
+
@column = [amount - 1, 0].max
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def position(params)
|
|
44
|
+
values = params.to_s.split(";")
|
|
45
|
+
column = values.length >= 2 ? Integer(values[1], exception: false) : 1
|
|
46
|
+
@column = [[column || 1, 1].max - 1, 0].max
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def clear(command, params)
|
|
50
|
+
mode = first_param(params, default: 0)
|
|
51
|
+
command == "K" ? clear_line(mode) : clear_screen(mode)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def to_s
|
|
55
|
+
last = @cells.rindex { |cell| !cell.nil? }
|
|
56
|
+
return @pending_escape unless last
|
|
57
|
+
|
|
58
|
+
@cells[0..last].map { |cell| cell || " " }.join + @pending_escape
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def first_param(params, default:)
|
|
64
|
+
value = Integer(params.to_s.split(";").first, exception: false)
|
|
65
|
+
value && value.positive? ? value : default
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def clear_line(mode)
|
|
69
|
+
case mode
|
|
70
|
+
when 1
|
|
71
|
+
clear_range(0..@column)
|
|
72
|
+
when 2, 3
|
|
73
|
+
@cells.clear
|
|
74
|
+
else
|
|
75
|
+
@cells.slice!(@column..)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def clear_screen(mode)
|
|
80
|
+
mode.zero? ? clear_line(0) : clear_line(2)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def clear_range(range)
|
|
84
|
+
range.each { |index| @cells[index] = nil if index < @cells.length }
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "strscan"
|
|
4
|
+
require_relative "ansi_line_buffer"
|
|
5
|
+
|
|
6
|
+
module Shellfie
|
|
7
|
+
module AnsiNormalizer
|
|
8
|
+
ANSI_REGEX = /\e\[([0-9;]*)m/
|
|
9
|
+
OSC_REGEX = /\e\].*?(?:\a|\e\\)/
|
|
10
|
+
CSI_CONTROL_REGEX = /\e\[[0-9;?]*[A-Za-ln-z]/
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def normalize(text)
|
|
15
|
+
text = text.gsub(OSC_REGEX, "")
|
|
16
|
+
text = apply_line_controls(text)
|
|
17
|
+
text.gsub(CSI_CONTROL_REGEX, "")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def apply_line_controls(text)
|
|
21
|
+
buffer = AnsiLineBuffer.new
|
|
22
|
+
scanner = StringScanner.new(text)
|
|
23
|
+
|
|
24
|
+
until scanner.eos?
|
|
25
|
+
if scanner.scan(ANSI_REGEX)
|
|
26
|
+
buffer.write_escape(scanner.matched)
|
|
27
|
+
next
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
if scanner.scan(/\e\[([0-9;?]*)[JK]/)
|
|
31
|
+
buffer.clear(scanner.matched[-1], scanner[1])
|
|
32
|
+
next
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
if scanner.scan(/\e\[([0-9;?]*)[CDG]/)
|
|
36
|
+
buffer.move(scanner.matched[-1], scanner[1])
|
|
37
|
+
next
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
if scanner.scan(/\e\[([0-9;?]*)[Hf]/)
|
|
41
|
+
buffer.position(scanner[1])
|
|
42
|
+
next
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
buffer.write_character(scanner.getch)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
buffer.to_s
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|