inlay 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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +67 -0
- data/Rakefile +9 -0
- data/docs/spikes.md +8 -0
- data/lib/inlay/adapters.rb +37 -0
- data/lib/inlay/config.rb +33 -0
- data/lib/inlay/irb.rb +49 -0
- data/lib/inlay/kernel.rb +11 -0
- data/lib/inlay/normalizer.rb +71 -0
- data/lib/inlay/pry.rb +38 -0
- data/lib/inlay/sizer.rb +60 -0
- data/lib/inlay/summary.rb +18 -0
- data/lib/inlay/terminal_output.rb +47 -0
- data/lib/inlay/version.rb +5 -0
- data/lib/inlay.rb +62 -0
- data/sig/inlay.rbs +32 -0
- metadata +155 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 4c4e3fe710798287aba2920de80b5a321c2d89543256b9b6079eba08d93dc9e3
|
|
4
|
+
data.tar.gz: 1f3f0803affda38c5dc7c8c821f2cfdbe7d18846a184e317712a0477a3f2a9e7
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 926cf085ff8b9dcad417763adf7da3d8de669a134df5e5c218dacf42c4a242091a54693e81672dd33e38ef598c31fdffd679e45711c5c92e9341a094a8cab3fa
|
|
7
|
+
data.tar.gz: 8272ef2ad9566553b4562107c74617d09d22680d6c672897168d7521dd6779387a425cb1b051986fa63ddd961e4dd0137a589db2a067291b6eeb7a66abdae7c8
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yudai Takada
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Inlay
|
|
2
|
+
|
|
3
|
+
Render Ruby graphics where you inspect them. Inlay shows Tessel images, RBGL surfaces, and chart output in terminals, IRB, and Pry.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
gem install inlay
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Add `inlay` to your Gemfile. Inlay uses [Tessel](https://github.com/rbgfx/tessel) for images and [Termvas](https://github.com/rbgfx/termvas) for terminal output.
|
|
12
|
+
|
|
13
|
+
## Use in IRB
|
|
14
|
+
|
|
15
|
+
Add this to `~/.irbrc`:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
require "inlay/irb"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Now returning an image from a prompt displays it inline and leaves a short summary as the result:
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
image = Tessel.read("brick.png").scale_nearest(64, 64)
|
|
25
|
+
image
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Pry uses the same integration style:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
require "inlay/pry"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Inlay uses the terminal protocol selected by Termvas: Kitty, iTerm2, Sixel, or true-color half blocks. Piped output, `TERM=dumb`, `INLAY=off`, and `NO_COLOR` with half blocks show only the summary.
|
|
35
|
+
|
|
36
|
+
## Use in a script
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
require "inlay"
|
|
40
|
+
|
|
41
|
+
image = Tessel.read("brick.png")
|
|
42
|
+
Inlay.show(image, width: 40)
|
|
43
|
+
Inlay.config.max_rows = 30
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
To install the short `inlay(value)` helper, require `inlay/kernel`. Explicit calls accept image paths; ordinary returned strings are never treated as paths.
|
|
47
|
+
|
|
48
|
+
Objects can implement `to_inlay` and return a `Tessel::Image`, `{ png: bytes }`, `{ svg: markup, png: -> { bytes } }`, or `{ frames: images, fps: number }`. Custom object types can also be registered:
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
Inlay.register(MySurface) { |surface| surface.to_tessel_image }
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Configuration
|
|
55
|
+
|
|
56
|
+
`Inlay.config.enabled` toggles terminal rendering. `max_rows` caps terminal height and `max_pixels` limits decoded and displayed input size. Small images (up to 32×32) are enlarged with nearest-neighbor scaling; larger images are reduced with alpha-aware area averaging. Set `INLAY=off` to disable terminal output for a process.
|
|
57
|
+
|
|
58
|
+
## Development
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
bundle install
|
|
62
|
+
bundle exec rake verify
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## License
|
|
66
|
+
|
|
67
|
+
MIT
|
data/Rakefile
ADDED
data/docs/spikes.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Integration spikes
|
|
2
|
+
|
|
3
|
+
- IRB 1.18's `IRB::Inspector` supports a streaming inspector that writes into IRB's result buffer. Inlay writes terminal escapes to `$stdout` separately and leaves a short summary in the buffer. IRB paging and assignment truncation therefore only see the summary. The supported minimum is IRB 1.13, the first version with the supported inspector API.
|
|
4
|
+
- `.irbrc` runs before the first prompt, so environment-based terminal detection can be cached at install time without competing with Reline input. Inlay does not issue device queries.
|
|
5
|
+
- Rails console uses IRB's configured inspector when initialized after `.irbrc`; re-running `Inlay.irb_install!` wraps a subsequently selected inspector. Rails itself is not part of the test dependency set.
|
|
6
|
+
- Pry's configured printer receives `(output, value, pry_instance)`; the adapter retains that printer and passes it the summary.
|
|
7
|
+
- IRuby's public display registry supports predicate matchers and dynamic MIME pairs. SVG is preferred; PNG and GIF-in-HTML are selected from the normalized return value.
|
|
8
|
+
- `@ruby/3.4-wasm-wasi` 2.10.1 / Ruby 3.4.1 with Larb and JS host bindings completed 3,600 simulated 60 fps callbacks in 61.95 seconds. With one stable JS trampoline and Ruby/JS GC every 600 frames, WASM memory changed from 71,499,776 to 71,565,312 bytes; JS heap grew 4,261,280 bytes. This validates the callback strategy under Node, not a browser or GPU workload.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Inlay
|
|
4
|
+
module Adapters
|
|
5
|
+
@adapters = {}
|
|
6
|
+
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def register(klass, adapter)
|
|
10
|
+
raise TypeError, "adapter key must be a Class or Module" unless klass.is_a?(Module)
|
|
11
|
+
|
|
12
|
+
@adapters[klass] = adapter
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def convert(object)
|
|
16
|
+
pair = @adapters.find { |klass, _| object.is_a?(klass) }
|
|
17
|
+
return pair.last.call(object) if pair
|
|
18
|
+
|
|
19
|
+
return object if defined?(Tessel::Image) && object.is_a?(Tessel::Image)
|
|
20
|
+
|
|
21
|
+
if defined?(RBGL::Engine::Framebuffer) && object.is_a?(RBGL::Engine::Framebuffer)
|
|
22
|
+
return Tessel::Image.from_rgba(object.width, object.height, object.to_rgba_bytes)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
if defined?(RBGL::Engine::Texture) && object.is_a?(RBGL::Engine::Texture)
|
|
26
|
+
bytes = object.data.flat_map { |color| [color.r, color.g, color.b, color.a] }.pack("C*")
|
|
27
|
+
return Tessel::Image.from_rgba(object.width, object.height, bytes)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
if defined?(Inkplot::Chart) && object.is_a?(Inkplot::Chart)
|
|
31
|
+
return object.to_inlay
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/inlay/config.rb
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Inlay
|
|
4
|
+
class Config
|
|
5
|
+
attr_accessor :enabled
|
|
6
|
+
attr_reader :max_rows, :max_pixels
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@enabled = true
|
|
10
|
+
@max_rows = 24
|
|
11
|
+
@max_pixels = 1_048_576
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def max_rows=(value)
|
|
15
|
+
@max_rows = positive_integer(value, :max_rows)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def max_pixels=(value)
|
|
19
|
+
@max_pixels = positive_integer(value, :max_pixels)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def positive_integer(value, name)
|
|
25
|
+
number = Integer(value)
|
|
26
|
+
raise ArgumentError, "#{name} must be positive" unless number.positive?
|
|
27
|
+
|
|
28
|
+
number
|
|
29
|
+
rescue ArgumentError, TypeError
|
|
30
|
+
raise ArgumentError, "#{name} must be a positive integer"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/inlay/irb.rb
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "inlay"
|
|
4
|
+
require "irb"
|
|
5
|
+
require "irb/inspector"
|
|
6
|
+
|
|
7
|
+
module Inlay
|
|
8
|
+
module IRBIntegration
|
|
9
|
+
class << self
|
|
10
|
+
attr_reader :original, :wrapper
|
|
11
|
+
|
|
12
|
+
def install
|
|
13
|
+
mode = IRB.conf[:INSPECT_MODE]
|
|
14
|
+
selected = mode.is_a?(IRB::Inspector) ? mode : IRB::Inspector::INSPECTORS[mode] || IRB::Inspector::INSPECTORS[mode.to_s] || IRB::Inspector::INSPECTORS[:p]
|
|
15
|
+
return false unless selected
|
|
16
|
+
|
|
17
|
+
@original = selected unless mode.equal?(@wrapper)
|
|
18
|
+
@protocol = TerminalOutput.protocol
|
|
19
|
+
@wrapper = IRB::Inspector.new(proc do |value, output, colorize: true|
|
|
20
|
+
begin
|
|
21
|
+
if Inlay.displayable?(value)
|
|
22
|
+
output << Inlay.show(value, protocol: @protocol)
|
|
23
|
+
else
|
|
24
|
+
@original.inspect_value(value, output, colorize: colorize)
|
|
25
|
+
end
|
|
26
|
+
rescue StandardError
|
|
27
|
+
@original.inspect_value(value, output, colorize: colorize)
|
|
28
|
+
end
|
|
29
|
+
end)
|
|
30
|
+
IRB.conf[:INSPECT_MODE] = @wrapper
|
|
31
|
+
context = IRB.CurrentContext if IRB.respond_to?(:CurrentContext)
|
|
32
|
+
context.inspect_mode = @wrapper if context&.respond_to?(:inspect_mode=)
|
|
33
|
+
true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def uninstall
|
|
37
|
+
return false unless @wrapper && IRB.conf[:INSPECT_MODE].equal?(@wrapper)
|
|
38
|
+
|
|
39
|
+
IRB.conf[:INSPECT_MODE] = @original
|
|
40
|
+
context = IRB.CurrentContext if IRB.respond_to?(:CurrentContext)
|
|
41
|
+
context.inspect_mode = @original if context&.respond_to?(:inspect_mode=)
|
|
42
|
+
@wrapper = @original = @protocol = nil
|
|
43
|
+
true
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
Inlay::IRBIntegration.install
|
data/lib/inlay/kernel.rb
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Inlay
|
|
4
|
+
Normalized = Struct.new(:image, :svg, :png, :frames, :fps, keyword_init: true)
|
|
5
|
+
|
|
6
|
+
module Normalizer
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def call(object, allow_path: false, seen: {})
|
|
10
|
+
raise Error, "recursive to_inlay result" if seen[object.object_id]
|
|
11
|
+
|
|
12
|
+
converted = Adapters.convert(object)
|
|
13
|
+
return Normalized.new(image: converted) if converted.is_a?(Tessel::Image)
|
|
14
|
+
return from_value(converted, allow_path: allow_path, seen: seen) unless converted.nil?
|
|
15
|
+
|
|
16
|
+
return from_value(object.to_inlay, allow_path: allow_path, seen: seen.merge(object.object_id => true)) if object.respond_to?(:to_inlay)
|
|
17
|
+
return from_value(object, allow_path: allow_path, seen: seen) if object.is_a?(Hash)
|
|
18
|
+
return from_path(object) if allow_path && path_value?(object)
|
|
19
|
+
|
|
20
|
+
Normalized.new
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def terminal_image(normalized)
|
|
24
|
+
return normalized.image if normalized.image
|
|
25
|
+
return Tessel.decode(normalized.png, max_pixels: Inlay.config.max_pixels) if normalized.png.is_a?(String)
|
|
26
|
+
if normalized.png.respond_to?(:call)
|
|
27
|
+
value = normalized.png.call
|
|
28
|
+
return value if value.is_a?(Tessel::Image)
|
|
29
|
+
return Tessel.decode(value, max_pixels: Inlay.config.max_pixels) if value.is_a?(String)
|
|
30
|
+
return call(value).image
|
|
31
|
+
end
|
|
32
|
+
return call(normalized.frames.first).image if normalized.frames&.any?
|
|
33
|
+
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def from_value(value, allow_path:, seen:)
|
|
38
|
+
raise Error, "recursive to_inlay result" if seen[value.object_id]
|
|
39
|
+
return Normalized.new(image: value) if value.is_a?(Tessel::Image)
|
|
40
|
+
return from_path(value) if allow_path && path_value?(value)
|
|
41
|
+
return Normalized.new unless value.is_a?(Hash)
|
|
42
|
+
|
|
43
|
+
data = value.each_with_object({}) { |(key, item), result| result[key.to_sym] = item if key.respond_to?(:to_sym) }
|
|
44
|
+
if data[:frames]
|
|
45
|
+
frames = Array(data[:frames])
|
|
46
|
+
raise ArgumentError, "frames must not be empty" if frames.empty?
|
|
47
|
+
raise TypeError, "frames must contain Tessel::Image values" unless frames.all? { |frame| frame.is_a?(Tessel::Image) }
|
|
48
|
+
raise TypeError, "fps must be a positive number" unless data[:fps].is_a?(Numeric) && data[:fps].positive?
|
|
49
|
+
|
|
50
|
+
return Normalized.new(frames: frames, fps: data[:fps])
|
|
51
|
+
end
|
|
52
|
+
raise TypeError, "png must be bytes or a callable" if data[:png] && !data[:png].is_a?(String) && !data[:png].respond_to?(:call)
|
|
53
|
+
raise TypeError, "svg must be a String" if data[:svg] && !data[:svg].is_a?(String)
|
|
54
|
+
|
|
55
|
+
Normalized.new(svg: data[:svg], png: data[:png])
|
|
56
|
+
end
|
|
57
|
+
private_class_method :from_value
|
|
58
|
+
|
|
59
|
+
def from_path(value)
|
|
60
|
+
bytes = value.is_a?(Pathname) ? File.binread(value) : String(value)
|
|
61
|
+
Normalized.new(image: Tessel.decode(bytes, max_pixels: Inlay.config.max_pixels))
|
|
62
|
+
end
|
|
63
|
+
private_class_method :from_path
|
|
64
|
+
|
|
65
|
+
def path_value?(value)
|
|
66
|
+
value.is_a?(Pathname) || (value.is_a?(String) && IMAGE_EXTENSIONS.include?(File.extname(value).downcase) && File.file?(value))
|
|
67
|
+
end
|
|
68
|
+
IMAGE_EXTENSIONS = %w[.png .ppm .pnm .bmp].freeze
|
|
69
|
+
private_class_method :path_value?
|
|
70
|
+
end
|
|
71
|
+
end
|
data/lib/inlay/pry.rb
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "inlay"
|
|
4
|
+
require "pry"
|
|
5
|
+
|
|
6
|
+
module Inlay
|
|
7
|
+
module PryIntegration
|
|
8
|
+
class << self
|
|
9
|
+
attr_reader :original, :wrapper
|
|
10
|
+
|
|
11
|
+
def install
|
|
12
|
+
return true if Pry.config.print.equal?(@wrapper)
|
|
13
|
+
|
|
14
|
+
@original = Pry.config.print
|
|
15
|
+
@wrapper = proc do |output, value, pry_instance|
|
|
16
|
+
begin
|
|
17
|
+
value = Inlay.show(value) if Inlay.displayable?(value)
|
|
18
|
+
rescue StandardError
|
|
19
|
+
# Preserve Pry's configured printer if image rendering fails.
|
|
20
|
+
end
|
|
21
|
+
@original.call(output, value, pry_instance)
|
|
22
|
+
end
|
|
23
|
+
Pry.config.print = @wrapper
|
|
24
|
+
true
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def uninstall
|
|
28
|
+
return false unless @wrapper && Pry.config.print.equal?(@wrapper)
|
|
29
|
+
|
|
30
|
+
Pry.config.print = @original
|
|
31
|
+
@wrapper = @original = nil
|
|
32
|
+
true
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
Inlay::PryIntegration.install
|
data/lib/inlay/sizer.rb
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Inlay
|
|
4
|
+
module Sizer
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def fit(image, width: nil, height: nil, scale: :auto, columns: nil, rows: nil)
|
|
8
|
+
raise TypeError, "expected a Tessel::Image" unless image.is_a?(Tessel::Image)
|
|
9
|
+
raise ArgumentError, "unknown scale mode: #{scale}" unless %i[auto fit pixel none].include?(scale)
|
|
10
|
+
return image if image.width.zero? || image.height.zero?
|
|
11
|
+
|
|
12
|
+
columns ||= width || Termvas::Terminal.new(alt_screen: false).size[0]
|
|
13
|
+
width = [Integer(width || columns), Integer(columns)].min
|
|
14
|
+
height = [Integer(height || Inlay.config.max_rows * 2), Inlay.config.max_rows * 2].min
|
|
15
|
+
raise ArgumentError, "display dimensions must be positive" unless width.positive? && height.positive?
|
|
16
|
+
pixel_scale = scale == :pixel || (scale == :auto && image.width <= 32 && image.height <= 32)
|
|
17
|
+
max_factor = [width / [image.width, 1].max, height / [image.height, 1].max].min
|
|
18
|
+
if pixel_scale && max_factor > 1
|
|
19
|
+
return image.scale_nearest(image.width * max_factor, image.height * max_factor)
|
|
20
|
+
end
|
|
21
|
+
return image if image.width <= width && image.height <= height
|
|
22
|
+
|
|
23
|
+
factor = [width.to_f / image.width, height.to_f / image.height, 1.0].min
|
|
24
|
+
resize_area(image, [(image.width * factor).floor, 1].max, [(image.height * factor).floor, 1].max)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def resize_area(image, width, height)
|
|
28
|
+
bytes = image.bytes
|
|
29
|
+
output = String.new(capacity: width * height * 4, encoding: Encoding::BINARY)
|
|
30
|
+
height.times do |y|
|
|
31
|
+
top = y * image.height.to_f / height
|
|
32
|
+
bottom = (y + 1) * image.height.to_f / height
|
|
33
|
+
width.times do |x|
|
|
34
|
+
left = x * image.width.to_f / width
|
|
35
|
+
right = (x + 1) * image.width.to_f / width
|
|
36
|
+
alpha = red = green = blue = area = 0.0
|
|
37
|
+
(top.floor...bottom.ceil).each do |source_y|
|
|
38
|
+
weight_y = [bottom, source_y + 1].min - [top, source_y].max
|
|
39
|
+
(left.floor...right.ceil).each do |source_x|
|
|
40
|
+
weight = weight_y * ([right, source_x + 1].min - [left, source_x].max)
|
|
41
|
+
offset = (source_y * image.width + source_x) * 4
|
|
42
|
+
a = bytes.getbyte(offset + 3)
|
|
43
|
+
area += weight
|
|
44
|
+
alpha += a * weight
|
|
45
|
+
red += bytes.getbyte(offset) * a * weight
|
|
46
|
+
green += bytes.getbyte(offset + 1) * a * weight
|
|
47
|
+
blue += bytes.getbyte(offset + 2) * a * weight
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
output << (alpha.zero? ? 0 : (red / alpha).round)
|
|
51
|
+
output << (alpha.zero? ? 0 : (green / alpha).round)
|
|
52
|
+
output << (alpha.zero? ? 0 : (blue / alpha).round)
|
|
53
|
+
output << (alpha / area).round
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
Tessel::Image.from_rgba(width, height, output)
|
|
57
|
+
end
|
|
58
|
+
private_class_method :resize_area
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Inlay
|
|
4
|
+
module Summary
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def call(object, normalized)
|
|
8
|
+
image = normalized.image || (normalized.frames&.first if normalized.frames)
|
|
9
|
+
dimensions = image ? " #{image.width}x#{image.height}" : ""
|
|
10
|
+
name = case object
|
|
11
|
+
when Tessel::Image then "Tessel::Image"
|
|
12
|
+
else object.class.name || object.class.to_s
|
|
13
|
+
end
|
|
14
|
+
frames = normalized.frames ? " frames=#{normalized.frames.length}" : ""
|
|
15
|
+
"#<#{name}#{dimensions}#{frames}>"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Inlay
|
|
4
|
+
module TerminalOutput
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def protocol(output: $stdout, env: ENV)
|
|
8
|
+
return :none unless Inlay.config.enabled
|
|
9
|
+
return :none unless output.tty?
|
|
10
|
+
return :none unless env["INLAY"] != "off"
|
|
11
|
+
return :none if env["TERM"] == "dumb"
|
|
12
|
+
|
|
13
|
+
selected = Termvas::Detector.protocol(env)
|
|
14
|
+
return :none if selected == :blocks && env.key?("NO_COLOR")
|
|
15
|
+
|
|
16
|
+
selected
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def write(image, width: nil, height: nil, scale: :auto, protocol: nil, output: $stdout, env: ENV)
|
|
20
|
+
selected = (protocol || self.protocol(output: output, env: env)).to_sym
|
|
21
|
+
return false unless Inlay.config.enabled && output.tty? && selected != :none
|
|
22
|
+
return false if selected == :blocks && env.key?("NO_COLOR")
|
|
23
|
+
return false if image.width * image.height > Inlay.config.max_pixels
|
|
24
|
+
|
|
25
|
+
image = Sizer.fit(image, width: width, height: height, scale: scale)
|
|
26
|
+
bytes = image.bytes
|
|
27
|
+
encoded = case selected.to_sym
|
|
28
|
+
when :blocks then Termvas::Encoders::Blocks.encode(bytes, image.width, image.height)
|
|
29
|
+
when :kitty then Termvas::Encoders::Kitty.encode(bytes, image.width, image.height, id: next_kitty_id)
|
|
30
|
+
when :iterm2
|
|
31
|
+
png = Tessel::PNG.encode(image)
|
|
32
|
+
Termvas::Encoders::ITerm2.encode(png, width: image.width, height: image.height)
|
|
33
|
+
when :sixel then Termvas::Encoders::Sixel.encode(bytes, image.width, image.height)
|
|
34
|
+
else return false
|
|
35
|
+
end
|
|
36
|
+
output.write(encoded)
|
|
37
|
+
output.write("\n")
|
|
38
|
+
true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def next_kitty_id
|
|
42
|
+
@kitty_id_lock ||= Mutex.new
|
|
43
|
+
@kitty_id_lock.synchronize { @kitty_id = (@kitty_id.to_i % 2_147_483_647) + 1 }
|
|
44
|
+
end
|
|
45
|
+
private_class_method :next_kitty_id
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/inlay.rb
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tessel"
|
|
4
|
+
require "termvas"
|
|
5
|
+
require "pathname"
|
|
6
|
+
require_relative "inlay/version"
|
|
7
|
+
require_relative "inlay/config"
|
|
8
|
+
require_relative "inlay/adapters"
|
|
9
|
+
require_relative "inlay/normalizer"
|
|
10
|
+
require_relative "inlay/sizer"
|
|
11
|
+
require_relative "inlay/summary"
|
|
12
|
+
require_relative "inlay/terminal_output"
|
|
13
|
+
|
|
14
|
+
module Inlay
|
|
15
|
+
class Error < StandardError; end
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
def config
|
|
19
|
+
@config ||= Config.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def register(klass, &adapter)
|
|
23
|
+
raise ArgumentError, "an adapter block is required" unless adapter
|
|
24
|
+
|
|
25
|
+
Adapters.register(klass, adapter)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def normalize(object, allow_path: false)
|
|
29
|
+
Normalizer.call(object, allow_path: allow_path)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def displayable?(object)
|
|
33
|
+
normalized = normalize(object)
|
|
34
|
+
!!(normalized.image || normalized.svg || normalized.png || normalized.frames)
|
|
35
|
+
rescue StandardError
|
|
36
|
+
false
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def protocol
|
|
40
|
+
TerminalOutput.protocol
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def irb_install!
|
|
44
|
+
require_relative "inlay/irb"
|
|
45
|
+
IRBIntegration.install
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def irb_uninstall!
|
|
49
|
+
IRBIntegration.uninstall if defined?(IRBIntegration)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def show(object, width: nil, height: nil, scale: :auto, protocol: nil)
|
|
53
|
+
normalized = normalize(object, allow_path: true)
|
|
54
|
+
image = Normalizer.terminal_image(normalized)
|
|
55
|
+
normalized.image ||= image
|
|
56
|
+
summary = Summary.call(object, normalized)
|
|
57
|
+
return summary unless image && TerminalOutput.write(image, width: width, height: height, scale: scale, protocol: protocol)
|
|
58
|
+
|
|
59
|
+
summary
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
data/sig/inlay.rbs
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module Inlay
|
|
2
|
+
class Error < StandardError
|
|
3
|
+
end
|
|
4
|
+
|
|
5
|
+
class Config
|
|
6
|
+
attr_accessor enabled: bool
|
|
7
|
+
attr_accessor max_rows: Integer
|
|
8
|
+
attr_accessor max_pixels: Integer
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class Normalized
|
|
12
|
+
attr_accessor image: untyped
|
|
13
|
+
attr_accessor svg: String?
|
|
14
|
+
attr_accessor png: untyped
|
|
15
|
+
attr_accessor frames: Array[untyped]?
|
|
16
|
+
attr_accessor fps: Numeric?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.config: () -> Config
|
|
20
|
+
def self.register: (Module klass) { (untyped object) -> untyped } -> Module
|
|
21
|
+
def self.normalize: (untyped object, ?allow_path: bool) -> Normalized
|
|
22
|
+
def self.displayable?: (untyped object) -> bool
|
|
23
|
+
def self.protocol: () -> Symbol
|
|
24
|
+
def self.show: (untyped object, ?width: Integer?, ?height: Integer?, ?scale: Symbol, ?protocol: Symbol?) -> String
|
|
25
|
+
def self.irb_install!: () -> bool
|
|
26
|
+
def self.irb_uninstall!: () -> (bool | nil)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
module Kernel
|
|
30
|
+
private
|
|
31
|
+
def inlay: (untyped object, **untyped options) -> String
|
|
32
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: inlay
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Yudai Takada
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: tessel
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 0.2.0
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '1'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: 0.2.0
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '1'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: termvas
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: 0.2.0
|
|
39
|
+
- - "<"
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '1'
|
|
42
|
+
type: :runtime
|
|
43
|
+
prerelease: false
|
|
44
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: 0.2.0
|
|
49
|
+
- - "<"
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: '1'
|
|
52
|
+
- !ruby/object:Gem::Dependency
|
|
53
|
+
name: base64
|
|
54
|
+
requirement: !ruby/object:Gem::Requirement
|
|
55
|
+
requirements:
|
|
56
|
+
- - ">="
|
|
57
|
+
- !ruby/object:Gem::Version
|
|
58
|
+
version: '0'
|
|
59
|
+
type: :runtime
|
|
60
|
+
prerelease: false
|
|
61
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
62
|
+
requirements:
|
|
63
|
+
- - ">="
|
|
64
|
+
- !ruby/object:Gem::Version
|
|
65
|
+
version: '0'
|
|
66
|
+
- !ruby/object:Gem::Dependency
|
|
67
|
+
name: irb
|
|
68
|
+
requirement: !ruby/object:Gem::Requirement
|
|
69
|
+
requirements:
|
|
70
|
+
- - ">="
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: '1.13'
|
|
73
|
+
type: :development
|
|
74
|
+
prerelease: false
|
|
75
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
76
|
+
requirements:
|
|
77
|
+
- - ">="
|
|
78
|
+
- !ruby/object:Gem::Version
|
|
79
|
+
version: '1.13'
|
|
80
|
+
- !ruby/object:Gem::Dependency
|
|
81
|
+
name: pry
|
|
82
|
+
requirement: !ruby/object:Gem::Requirement
|
|
83
|
+
requirements:
|
|
84
|
+
- - ">="
|
|
85
|
+
- !ruby/object:Gem::Version
|
|
86
|
+
version: '0'
|
|
87
|
+
type: :development
|
|
88
|
+
prerelease: false
|
|
89
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
90
|
+
requirements:
|
|
91
|
+
- - ">="
|
|
92
|
+
- !ruby/object:Gem::Version
|
|
93
|
+
version: '0'
|
|
94
|
+
- !ruby/object:Gem::Dependency
|
|
95
|
+
name: rspec
|
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
|
97
|
+
requirements:
|
|
98
|
+
- - "~>"
|
|
99
|
+
- !ruby/object:Gem::Version
|
|
100
|
+
version: '3.0'
|
|
101
|
+
type: :development
|
|
102
|
+
prerelease: false
|
|
103
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
104
|
+
requirements:
|
|
105
|
+
- - "~>"
|
|
106
|
+
- !ruby/object:Gem::Version
|
|
107
|
+
version: '3.0'
|
|
108
|
+
description: Render Tessel images and graphics buffers in terminals, IRB, and Pry.
|
|
109
|
+
email:
|
|
110
|
+
- t.yudai92@gmail.com
|
|
111
|
+
executables: []
|
|
112
|
+
extensions: []
|
|
113
|
+
extra_rdoc_files: []
|
|
114
|
+
files:
|
|
115
|
+
- CHANGELOG.md
|
|
116
|
+
- LICENSE.txt
|
|
117
|
+
- README.md
|
|
118
|
+
- Rakefile
|
|
119
|
+
- docs/spikes.md
|
|
120
|
+
- lib/inlay.rb
|
|
121
|
+
- lib/inlay/adapters.rb
|
|
122
|
+
- lib/inlay/config.rb
|
|
123
|
+
- lib/inlay/irb.rb
|
|
124
|
+
- lib/inlay/kernel.rb
|
|
125
|
+
- lib/inlay/normalizer.rb
|
|
126
|
+
- lib/inlay/pry.rb
|
|
127
|
+
- lib/inlay/sizer.rb
|
|
128
|
+
- lib/inlay/summary.rb
|
|
129
|
+
- lib/inlay/terminal_output.rb
|
|
130
|
+
- lib/inlay/version.rb
|
|
131
|
+
- sig/inlay.rbs
|
|
132
|
+
homepage: https://github.com/rbgfx/inlay
|
|
133
|
+
licenses:
|
|
134
|
+
- MIT
|
|
135
|
+
metadata:
|
|
136
|
+
homepage_uri: https://github.com/rbgfx/inlay
|
|
137
|
+
source_code_uri: https://github.com/rbgfx/inlay/tree/main
|
|
138
|
+
rdoc_options: []
|
|
139
|
+
require_paths:
|
|
140
|
+
- lib
|
|
141
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
142
|
+
requirements:
|
|
143
|
+
- - ">="
|
|
144
|
+
- !ruby/object:Gem::Version
|
|
145
|
+
version: 3.2.0
|
|
146
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
147
|
+
requirements:
|
|
148
|
+
- - ">="
|
|
149
|
+
- !ruby/object:Gem::Version
|
|
150
|
+
version: '0'
|
|
151
|
+
requirements: []
|
|
152
|
+
rubygems_version: 4.0.16
|
|
153
|
+
specification_version: 4
|
|
154
|
+
summary: Display Ruby graphics in terminals and REPLs
|
|
155
|
+
test_files: []
|