Module: Unmagic::Browser::Console::Graphics
- Defined in:
- lib/unmagic/browser/console/graphics.rb
Overview
Drawing an image in the terminal, over the kitty graphics protocol.
The terminal is handed the PNG bytes themselves, base64'd, wrapped in an APC escape sequence. That's the transmission mode every implementing terminal supports — the alternative, pointing at a file on disk, is faster but only works when the terminal is on the same machine and allowed to read it.
Constant Summary collapse
- CHUNK =
The protocol caps a single escape sequence's payload, so anything bigger goes out as a run of chunks flagged
m=1until the last. 4096- MARGIN =
Leave a little room so an image drawn at the full width doesn't wrap.
2- DEFAULT_SCALE =
How much of the terminal's width a drawn image may take.
A screenshot in a console is for glancing at — "did the page load, did the button move" — and a 1280px capture drawn edge to edge pushes the command that produced it off the screen. Half leaves the conversation readable, and the full-size file on disk is there when you want to look properly.
0.5- ID_BASE =
Where this process starts numbering its images.
Every transmission needs an id the terminal hasn't seen, or it may treat the new bytes as a redraw of an image it already has and leave the old one on screen — a screenshot that never seems to change. Untagged transmissions all land on the same implicit id, which is exactly that trap, so ids are seeded from the pid to keep two consoles sharing a terminal out of each other's way.
(Process.pid % 10_000) * 100_000
Class Method Summary collapse
- .draw(image) ⇒ void
-
.next_id ⇒ Integer
An id no image drawn from this process has used.
-
.save(image) ⇒ String
The path written to.
-
.scale ⇒ Float
The fraction of the terminal's width an image may take.
-
.scale=(fraction) ⇒ void
Draw images larger or smaller —
Graphics.scale = 1.0for full width. -
.scaled_columns(image) ⇒ Integer?
The width to draw at: #scale of the terminal, but never wider than the image really is.
-
.settings(image) ⇒ String
a=Ttransmits and displays in one go;f=100says the payload is a PNG, so the terminal does the decoding;i=gives this image an id of its own; andq=2tells the terminal to keep its acknowledgement to itself — it would otherwise writeOKback down the tty, where the console is reading keystrokes and would take it for typing. -
.show(image) ⇒ String
Write the image somewhere durable and draw it if the terminal can.
-
.timestamp ⇒ String
A sortable, collision-resistant filename fragment.
Class Method Details
.draw(image) ⇒ void
This method returns an undefined value.
91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
# File 'lib/unmagic/browser/console/graphics.rb', line 91 def draw(image) payload = Base64.strict_encode64(image).scan(/.{1,#{CHUNK}}/) payload.each_with_index do |chunk, index| more = index == payload.size - 1 ? 0 : 1 # Only the first chunk carries the full control data; the rest just # say whether more is coming. control = index.zero? ? "#{settings(image)},m=#{more}" : "m=#{more}" $stdout.write("\e_G#{control};#{chunk}\e\\") end $stdout.write("\n") $stdout.flush end |
.next_id ⇒ Integer
Returns an id no image drawn from this process has used.
48 49 50 51 |
# File 'lib/unmagic/browser/console/graphics.rb', line 48 def next_id @drawn = (@drawn || 0) + 1 ID_BASE + @drawn end |
.save(image) ⇒ String
Returns the path written to.
81 82 83 84 85 86 87 |
# File 'lib/unmagic/browser/console/graphics.rb', line 81 def save(image) directory = File.join(Dir.tmpdir, "unmagic-browser") FileUtils.mkdir_p(directory) path = File.join(directory, "#{image.name}-#{}.#{image.extension}") File.binwrite(path, image) path end |
.scale ⇒ Float
Returns the fraction of the terminal's width an image may take.
54 55 56 |
# File 'lib/unmagic/browser/console/graphics.rb', line 54 def scale @scale ||= DEFAULT_SCALE end |
.scale=(fraction) ⇒ void
This method returns an undefined value.
Draw images larger or smaller — Graphics.scale = 1.0 for full width.
62 63 64 |
# File 'lib/unmagic/browser/console/graphics.rb', line 62 def scale=(fraction) @scale = fraction end |
.scaled_columns(image) ⇒ Integer?
The width to draw at: #scale of the terminal, but never wider than the image really is. Scaling up would turn a screenshot of one button into a blurry banner — the cap is a ceiling, not a target.
129 130 131 132 133 134 135 136 |
# File 'lib/unmagic/browser/console/graphics.rb', line 129 def scaled_columns(image) width, = image.dimensions return nil unless width ceiling = [((Terminal.columns - MARGIN) * scale).round, 1].max natural = (width.to_f / Terminal.cell.first).ceil natural > ceiling ? ceiling : nil end |
.settings(image) ⇒ String
a=T transmits and displays in one go; f=100 says the payload is a
PNG, so the terminal does the decoding; i= gives this image an id of
its own; and q=2 tells the terminal to keep its acknowledgement to
itself — it would otherwise write OK back down the tty, where the
console is reading keystrokes and would take it for typing.
114 115 116 117 118 119 120 121 |
# File 'lib/unmagic/browser/console/graphics.rb', line 114 def settings(image) control = "a=T,f=100,i=#{next_id},q=2" columns = scaled_columns(image) # Given a width and no height, the terminal keeps the aspect ratio. # Left off entirely, the image draws at its true pixel size. control += ",c=#{columns}" if columns control end |
.show(image) ⇒ String
Write the image somewhere durable and draw it if the terminal can.
It's written either way: half the point of a screenshot in a console is being able to open it in something that isn't a terminal.
73 74 75 76 77 |
# File 'lib/unmagic/browser/console/graphics.rb', line 73 def show(image) path = save(image) draw(image) if Terminal.graphics? path end |
.timestamp ⇒ String
Returns a sortable, collision-resistant filename fragment.
139 140 141 |
# File 'lib/unmagic/browser/console/graphics.rb', line 139 def Time.now.strftime("%Y%m%d-%H%M%S-%L") end |