Skip to content

Instantly share code, notes, and snippets.

@tompng
Last active February 24, 2024 17:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tompng/f70dda073b8044112c7d1cc8ed6b4d53 to your computer and use it in GitHub Desktop.
Save tompng/f70dda073b8044112c7d1cc8ed6b4d53 to your computer and use it in GitHub Desktop.
reline line_editor readme svg
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
require 'irb'
require 'tempfile'
short_code = <<~RUBY
def foobar
puts 'hello' + 'world' + 'ruby' + 'happy' + 'hacking'
end
RUBY
long_code = <<~RUBY
class A
def foo
a = 'helloworldhelloworldhelloworldhelloworld'
puts a * 10
end
def bar
puts 'ruby'+'ruby'+'ruby'+'ruby'+'ruby'+'ruby'+'ruby'+'ruby'+'ruby'+'ruby'+'ruby'+'ruby'+'ruby'
end
def baz
puts 'ruby'
end
end
RUBY
def escape_text(text)
text.gsub('<', '&lt').gsub('>', '&gt;')
end
def calculate_prompt(code, offset = 0)
Tempfile.open do |f|
ENV['IRBRC'] = f.path
IRB.setup 'irb'
irb = IRB::Irb.new
io = Object.new
irb.context.io = io
dynamic_prompt = nil
irb.instance_variable_set(:@line_no, offset + 1)
def io.auto_indent; end
def (irb.context).prompting?() = true
io.define_singleton_method(:dynamic_prompt) do |&block|
dynamic_prompt = block
end
irb.configure_io
dynamic_prompt.(code.lines)
end
end
def colorize_concat_prompt(code, offset = 0)
prompts = calculate_prompt(code, offset)
IRB::Color.colorize_code(code).lines.zip(prompts).map { |line, prompt| (prompt + line).chomp }
end
COLORS = {
30 => 'black', 31 => 'red', 32 => 'green', 33 => 'yellow', 34 => 'blue', 35 => 'magenta', 36 => 'cyan', 37 => 'silver'
}
def lines_to_svg(lines, id: nil, width: nil, size: 20)
seq = []
colored_lines = lines.map do |line|
line = line.chomp
colored_chars = []
line.split(/(\e\[[\d;]*m)/).each_with_index do |s, i|
if i % 2 == 0
s.chars.each do |c|
colored_chars << [seq, c]
end
else
numbers = s.scan(/\d+/)
seq = [] if numbers.empty?
seq += numbers.map(&:to_i)
zero = seq.rindex(0)
seq = seq[zero + 1..] if zero
end
end
colored_chars
end
colored_lines = colored_lines.map { _1.each_slice(width).to_a }.flatten(1) if width
svg = []
colored_lines.each_with_index do |line, y|
x = 0
line.chunk { _2 == ' ' ? :space : _1 }.each do |col, colored_chars|
text = colored_chars.map(&:last).join
w = text.size
if col != :space
color = 'black'
col.each do |n|
color = COLORS[n] if COLORS[n]
end
svg << %(<text x="#{x * size / 2}" y="#{size * 0.75 + y * size}" fill="#{color}" textLength="#{size * w / 2}">#{escape_text(text)}</text>)
end
x += w
end
end
[
%(<g #{%(id="#{id}" ) if id}font-size="14px">),
*svg.map{ " #{_1}" },
%(</g>)
].join("\n")
end
short_irb_template = lines_to_svg(colorize_concat_prompt(short_code, 2), width: 40, id: :short_code)
long_irb_template = lines_to_svg(colorize_concat_prompt(long_code, 0), width: 40, id: :long_code)
short_irb_output_template = self.then do
evals = [['1 + 2', "=> \e[34m3\e[m"], ['puts :hello', 'hello', "=> \e[36mnil\e[m"]]
output = evals.flat_map.with_index do |(code, *output), idx|
colorize_concat_prompt(code, idx) + output
end
lines_to_svg(output, width: 40, id: :irb_output)
end
template = [short_irb_template, long_irb_template, short_irb_output_template].join("\n")
def line_with_text((x1, y1), (x2, y2), text:, color: 'black', font_size: 16, line_width: 2, edge: [4, 4], edge_left: edge, edge_right: edge)
len = Math.hypot(x2 - x1, y2 - y1)
angle = 180 * Math.atan2(y2 - y1, x2 - x1) / Math::PI
down = edge[0] + line_width / 2
up = edge[1] + line_width / 2
<<~SVG
<g transform="translate(#{x1} #{y1}) rotate(#{angle} 0 0)">
<line x1="0" y1="#{-(edge_left[1] + line_width / 2)}" x2="0" y2="#{edge_left[0] + line_width / 2}" stroke="#{color}" stroke-width="#{line_width}"/>
<line x1="0" y1="0" x2="#{len}" y2="0" stroke="#{color}" stroke-width="#{line_width}"/>
<line x1="#{len}" y1="#{-(edge_right[1] + line_width / 2)}" x2="#{len}" y2="#{edge_right[0] + line_width / 2}" stroke="#{color}" stroke-width="#{line_width}"/>
<text x="#{len / 2}" y="-#{2 * line_width + font_size / 8}" fill="#{color}" font-size="#{font_size}px" text-anchor="middle">#{text}</text>
</g>
SVG
end
def cursor(x, y)
%(<rect x="#{10 * x}" y="#{20 * y}" width="10" height="20" fill="#666" />)
end
FONT = "ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', Courier, Monaco, monospace"
def memo_to_svg(memo)
memo.lines.map.with_index do |line, idx|
%(<text y="#{12 * idx}">#{escape_text(line)}</text>)
end.join("\n")
end
def short_irb_view(label: false, uncontrolable_highlight: false, controlable_highlight: true, code_opacity: 0.4, background: 'silver')
<<~SVG
<rect x="-4" y="-2" width="408" height="204" fill="#{background}" stroke="black" stroke-width="4"/>
<use href="#irb_output" x="0" y="0" opacity="#{[0.4, code_opacity].min}"/>
#{cursor(23, 7)}
<use href="#short_code" x="0" y="100" opacity="#{code_opacity}"/>
#{%(<rect x="-1" y="1" width="402" height="98" stroke-width="2" fill="none" stroke="#F88"/>) if uncontrolable_highlight}
#{%(<text x="400" y="12" text-anchor="end" fill="black" font-size="10px">Uncontrolable area</text>) if label && uncontrolable_highlight}
#{%(<rect x="-1" y="101" width="402" height="98" fill="none" stroke-width="2" stroke="#88F"/>) if controlable_highlight}
#{%(<text x="400" y="198" text-anchor="end" fill="black" font-size="10px">Controlable area</text>) if label && controlable_highlight}
SVG
end
def long_irb_view(label: false)
<<~SVG
<rect x="-2" y="-80" width="404" height="340" fill="silver"/>
#{cursor(26, 5)}
<use href="#long_code" x="0" y="-80" opacity="0.4"/>
<rect x="-4" y="-2" width="408" height="204" fill="none" stroke="black" stroke-width="4"/>
<rect x="-1" y="1" width="402" height="198" fill="none" stroke-width="2" stroke="#88F"/>
#{%(<text x="400" y="198" text-anchor="end" fill="black" font-size="10px">Controlable area</text>) if label}
SVG
end
def svg_header(width, height)
<<~SVG
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="#{width}px" height="#{height}px" viewBox="0 0 #{width} #{height}">
<rect x="0" y="0" width="#{width}" height="#{height}" fill="white"/>
SVG
end
def indent_svg(svg)
indent = 0
lines = svg.gsub('><', ">\n<").split(/\n[ \n]*/).map do |s|
current_indent = indent
if s.start_with?('</')
current_indent -= 1
indent -= 1
elsif s.start_with?('<') && !s.start_with?('<?') && !s.match?(/<\/[^<>]+ *>$/) && !s.match?(/\/ *>/)
indent += 1
end
' ' * (2 * current_indent) + s + "\n"
end
warn 'wrong indent' unless indent == 0
lines.join
end
def create_short_long_file(file, template, content_left, content_right)
File.write file, indent_svg(<<~SVG)
#{svg_header(1000, 400)}
<defs>
#{template}
</defs>
<g font-family="#{FONT}">
<g transform="translate(40 120)">
#{short_irb_view}
#{content_left}
</g>
<g transform="translate(540 120)">
#{long_irb_view}
#{content_right}
</g>
</g>
</svg>
SVG
end
create_short_long_file 'line_editor_screen.svg', template, <<~EOS, <<~EOS
<text x="200" y="-7" font-size="10px" text-anchor="middle">@scroll_partial_screen == 0</text>
#{line_with_text([235, 100], [235, 140], text: '', font_size: 10)}
<text x="238" y="125" font-size="10px">wrapped_cursor_position[1]</text>
#{line_with_text([0, 150], [230, 150], text: 'wrapped_cursor_position[0]', font_size: 10)}
EOS
#{line_with_text([-10, 0], [-10, -80], text: '@scroll_partial_screen', font_size: 10, edge: [2, 0])}
#{line_with_text([265, -80], [265, 100], text: 'wrapped_cursor_position[1]', font_size: 10)}
#{line_with_text([0, 110], [260, 110], text: 'wrapped_cursor_position[0]', font_size: 10)}
EOS
create_short_long_file 'line_editor_screen_legacy.svg', template, <<~EOS, <<~EOS
<text x="200" y="-7" font-size="10px" text-anchor="middle">@scroll_partial_screen == nil</text>
#{line_with_text([0, 140], [230, 140], text: '@cursor', font_size: 10, edge: [2, 0])}
#{line_with_text([0, 160], [300, 160], text: '@cursor_max', font_size: 10, edge: [0, 2])}
<line x1="0" y1="120" x2="400" y2="120" stroke-width="1" stroke="black" />
<line x1="0" y1="160" x2="400" y2="160" stroke-width="1" stroke="black" />
#{line_with_text([235, 160], [235, 200], text: '', font_size: 10, edge: [2, 0])}
<text x="238" y="182" font-size="10px">@rest_height</text>
#{line_with_text([410, 120], [410, 160], text: '@highest_in_this', font_size: 10, edge: [2, 0])}
#{line_with_text([-10, 180], [-10, 100], text: '@highest_in_all', font_size: 10, edge: [2, 0])}
#{line_with_text([235, 100], [235, 120], text: '', font_size: 10)}
<text x="238" y="112" font-size="10px">@first_line_started_from</text>
#{line_with_text([235, 120], [235, 140], text: '', font_size: 10)}
<text x="238" y="132" font-size="10px">@started_from</text>
EOS
#{line_with_text([-10, 0], [-10, -80], text: '@scroll_partial_screen', font_size: 10, edge: [2, 0])}
#{line_with_text([265, 120], [265, 200], text: '@rest_height', font_size: 10)}
<line x1="0" y1="80" x2="400" y2="80" stroke-width="1" stroke="black" />
<line x1="0" y1="140" x2="400" y2="140" stroke-width="1" stroke="black" />
#{line_with_text([410, 80], [410, 140], text: '@highest_in_this', font_size: 10, edge: [2, 0])}
#{line_with_text([430, -80], [430, 260], text: '@highest_in_all', font_size: 10, edge: [25, 0])}
#{line_with_text([265, 80], [265, 100], text: '', font_size: 10)}
<text x="268" y="92" font-size="10px">@started_from</text>
#{line_with_text([0, 100], [260, 100], text: '@cursor', font_size: 10, edge: [2, 0])}
#{line_with_text([0, 120], [400, 120], text: '@cursor_max', font_size: 10, edge: [0, 2])}
#{line_with_text([265, -80], [265, 80], text: '@first_line_started_from', font_size: 10)}
EOS
def screen_cache_inspect(code, offset = 0, range:)
wrapped_lines = colorize_concat_prompt(code, 2)
screen_cache = wrapped_lines.flat_map do |line|
prompt_size = 15
prompt = line[0, prompt_size]
code = line[prompt_size..]
nil_prompt = "\e[36mnil\e[m"
lines = Reline::Unicode.split_by_width(prompt + code, 40).first.compact
inspect_item = ->(x, text) {
"[\e[34m#{x}\e[m, \e[34m#{Reline::Unicode.calculate_width(text, true)}\e[m, \e[31m\"\e[m#{text}\e[31m\"\e[m]"
}
lines.zip([prompt]).map do |line, prompt|
if prompt
"[#{inspect_item.call(0, prompt)}, #{inspect_item.call(prompt_size, line[prompt_size..])}]"
else
"[#{nil_prompt}, #{inspect_item.call(0, line.to_s)}]"
end
end
end[range]
lines_to_svg(screen_cache)
end
File.write 'line_editor_rendered.svg', indent_svg(<<~SVG)
#{svg_header(1200, 700)}
<defs>
#{short_irb_output_template}
#{short_irb_template}
#{long_irb_template}
</defs>
<g font-family="#{FONT}">
<g transform="translate(40 20)">
#{short_irb_view(label: true, uncontrolable_highlight: true)}
#{line_with_text([235, 100], [235, 0], text: '', font_size: 10)}
<text x="230" y="62" text-anchor="end" font-size="10px">@rendered_screen.base_y</text>
#{line_with_text([235, 100], [235, 140], text: '', font_size: 10)}
<text x="230" y="123" text-anchor="end" font-size="10px">@rendered_screen.cursor_y</text>
%(<line x1="0" y1="100" x2="1120" y2="100" stroke-width="1" stroke="black" />)
%(<line x1="0" y1="180" x2="1120" y2="180" stroke-width="1" stroke="black" />)
<g transform="translate(440 100)">
#{screen_cache_inspect(short_code, 2, range: 0..3)}
<text x="0" y="-4" font-size="10px">
@rendered_screen.lines
</text>
</g>
</g>
<g transform="translate(40 320)">
#{long_irb_view(label: true)}
<text x="265" y="-7" font-size="10px" text-anchor="middle">@rendered_screen.base_y == 0</text>
#{line_with_text([265, 0], [265, 100], text: '', font_size: 10)}
<text x="260" y="63" text-anchor="end" font-size="10px">@rendered_screen.cursor_y</text>
%(<line x1="0" y1="0" x2="1120" y2="0" stroke-width="1" stroke="black" />)
%(<line x1="0" y1="200" x2="1120" y2="200" stroke-width="1" stroke="black" />)
<g transform="translate(440 0)">
#{screen_cache_inspect(long_code, range: 4..13)}
<text x="0" y="-4" font-size="10px">
@rendered_screen.lines
</text>
</g>
</g>
<g transform="translate(40 605)" font-size="10px">
#{memo_to_svg(<<~MEMO)}
Uncontrolable area: Can't render here
Controlable area: Everything rendered is under Reline's control
@rendered_screen.base_y: Height of uncontrolable area
@rendered_screen.cursor_y: Rendered cursor row in controlable area
@rendered_screen.lines: Rendered text fragments (prompt, code, dialog, etc) for each line. Position may overlap.
@rendered_screen.lines[row_index]: Array of ([col, width, text] | nil)
MEMO
</g>
</g>
</svg>
SVG
File.write 'line_editor_buffer.svg', indent_svg(<<~SVG)
#{svg_header(640, 320)}
<defs>
#{short_irb_output_template}
#{short_irb_template}
</defs>
<g font-family="#{FONT}">
<text x="298" y="20" font-size="10px" text-anchor="middle">@buffer_of_lines</text>
<g transform="translate(20 28)">
<rect x="-2" y="-4" width="594" height="54" fill="none" stroke="black" stroke-width="1px"></rect>
<rect x="24" y="16" width="460" height="16" fill="silver"></rect>
<rect x="328" y="16" width="8" height="16" fill="gray"></rect>
#{lines_to_svg(short_code.lines.each_with_index.map{"\e[37m%d:\e[m %s" % [_2, _1]}, size: 16)}
#{line_with_text([24, 34], [328, 34], text: '', font_size: 10, edge: [0, 2])}
<text x="176" y="44" font-size="10px" text-anchor="middle">@byte_pointer == 38</text>
<text x="488" y="27" font-size="10px">@line_index == 1</text>
</g>
<g transform="translate(40 100)">
#{short_irb_view(code_opacity: 1, controlable_highlight: false, background: 'none')}
</g>
</g>
</svg>
SVG
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment