termpix 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/README.md +101 -0
- data/lib/termpix/protocols.rb +166 -0
- data/lib/termpix/version.rb +3 -0
- data/lib/termpix.rb +120 -0
- metadata +49 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 321b92b52842de9fc8f7cd06688662f235423478f947ce9d5c96cac2083f582c
|
|
4
|
+
data.tar.gz: 76d16a71354bed1f1cc8e3196d01d48a09800c3f9a1eb9f330605f720ef3d06a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 5d510b46a5ad6de11c7ae89d43c7d0a07192db3a04590e5b684f9516113a18408089effc2048be2be0fe35cd9cf1ae9f4de0704d0a7e009773bb18b44beeb5b6
|
|
7
|
+
data.tar.gz: a52a9e6bace28d65a4b70db051c277979866ce2c00f6276f471d51c1b6f53b86c36a44b7c38a5977ff406de1bf0130e7484404a6aa589362db52fd123a1c478f
|
data/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Termpix - Modern Terminal Image Display
|
|
2
|
+
|
|
3
|
+
 [](https://badge.fury.io/rb/termpix)  
|
|
4
|
+
|
|
5
|
+
<img src="img/logo.svg" align="left" width="150" alt="Termpix Logo">
|
|
6
|
+
|
|
7
|
+
Display images in the terminal using the best available protocol.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- Auto-detects terminal capabilities
|
|
12
|
+
- Supports multiple protocols:
|
|
13
|
+
- Kitty Graphics Protocol (Kitty, WezTerm, Ghostty)
|
|
14
|
+
- Sixel (xterm, mlterm, foot, konsole, iTerm2)
|
|
15
|
+
- Überzug++ (modern X11/Wayland)
|
|
16
|
+
- w3mimgdisplay (legacy fallback)
|
|
17
|
+
- Clean, simple API
|
|
18
|
+
- Graceful fallbacks
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
gem install termpix
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Or add to your Gemfile:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
gem 'termpix'
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
require 'termpix'
|
|
36
|
+
|
|
37
|
+
# Create display instance (auto-detects best protocol)
|
|
38
|
+
display = Termpix::Display.new
|
|
39
|
+
|
|
40
|
+
# Show an image
|
|
41
|
+
display.show('path/to/image.png',
|
|
42
|
+
x: 10, # X position in terminal characters
|
|
43
|
+
y: 5, # Y position in terminal characters
|
|
44
|
+
max_width: 80, # Maximum width in characters
|
|
45
|
+
max_height: 40) # Maximum height in characters
|
|
46
|
+
|
|
47
|
+
# Clear the image
|
|
48
|
+
display.clear
|
|
49
|
+
|
|
50
|
+
# Check if images are supported
|
|
51
|
+
puts "Supported!" if display.supported?
|
|
52
|
+
|
|
53
|
+
# Get protocol info
|
|
54
|
+
info = display.info
|
|
55
|
+
puts "Using protocol: #{info[:protocol]}"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Force a Specific Protocol
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
# Force Kitty protocol
|
|
62
|
+
display = Termpix::Display.new(protocol: :kitty)
|
|
63
|
+
|
|
64
|
+
# Force Sixel
|
|
65
|
+
display = Termpix::Display.new(protocol: :sixel)
|
|
66
|
+
|
|
67
|
+
# Force w3m
|
|
68
|
+
display = Termpix::Display.new(protocol: :w3m)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Dependencies
|
|
72
|
+
|
|
73
|
+
- ImageMagick (`identify` and/or `convert` commands)
|
|
74
|
+
- For w3m/Überzug++: `xwininfo` and `xdotool`
|
|
75
|
+
|
|
76
|
+
## Terminal Support
|
|
77
|
+
|
|
78
|
+
### Kitty Protocol
|
|
79
|
+
- Kitty
|
|
80
|
+
- WezTerm
|
|
81
|
+
- Ghostty
|
|
82
|
+
- Konsole (partial)
|
|
83
|
+
|
|
84
|
+
### Sixel
|
|
85
|
+
- xterm (with `-ti vt340`)
|
|
86
|
+
- mlterm
|
|
87
|
+
- foot
|
|
88
|
+
- WezTerm
|
|
89
|
+
- iTerm2
|
|
90
|
+
|
|
91
|
+
### w3m/Überzug++
|
|
92
|
+
- Most X11 terminals
|
|
93
|
+
- Wayland terminals (Überzug++ only)
|
|
94
|
+
|
|
95
|
+
## License
|
|
96
|
+
|
|
97
|
+
Unlicense - Public Domain
|
|
98
|
+
|
|
99
|
+
## Author
|
|
100
|
+
|
|
101
|
+
Geir Isene - https://isene.com
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
require 'shellwords'
|
|
2
|
+
require 'base64'
|
|
3
|
+
|
|
4
|
+
module Termpix
|
|
5
|
+
module Protocols
|
|
6
|
+
# Kitty Graphics Protocol
|
|
7
|
+
module Kitty
|
|
8
|
+
def self.display(image_path, x:, y:, max_width:, max_height:)
|
|
9
|
+
# Read image and encode to base64
|
|
10
|
+
image_data = Base64.strict_encode64(File.read(image_path))
|
|
11
|
+
|
|
12
|
+
# Use virtual placement (no cursor positioning - avoids curses conflicts)
|
|
13
|
+
# Transmit image without positioning, let it flow inline
|
|
14
|
+
$stdout.write "\e_Ga=T,f=100,q=2;#{image_data}\e\\"
|
|
15
|
+
$stdout.flush
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.clear
|
|
19
|
+
# Delete all Kitty images
|
|
20
|
+
$stdout.write "\e_Ga=d,d=A\e\\"
|
|
21
|
+
$stdout.flush
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def self.get_dimensions(image_path)
|
|
27
|
+
escaped = Shellwords.escape(image_path)
|
|
28
|
+
dimensions = `identify -format "%wx%h" #{escaped}[0] 2>/dev/null`.strip
|
|
29
|
+
return nil if dimensions.empty?
|
|
30
|
+
dimensions.split('x').map(&:to_i)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.scale_dimensions(w, h, max_w, max_h)
|
|
34
|
+
if w > max_w || h > max_h
|
|
35
|
+
scale = [max_w.to_f / w, max_h.to_f / h].min
|
|
36
|
+
w = (w * scale).to_i
|
|
37
|
+
h = (h * scale).to_i
|
|
38
|
+
end
|
|
39
|
+
[w, h]
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Sixel Protocol
|
|
44
|
+
module Sixel
|
|
45
|
+
def self.display(image_path, x:, y:, max_width:, max_height:)
|
|
46
|
+
escaped = Shellwords.escape(image_path)
|
|
47
|
+
|
|
48
|
+
# Convert character dimensions to approximate pixel dimensions
|
|
49
|
+
# Average character cell is roughly 10x20 pixels in most terminals
|
|
50
|
+
pixel_width = max_width * 10
|
|
51
|
+
pixel_height = max_height * 20
|
|
52
|
+
|
|
53
|
+
# Position cursor at the specified character position
|
|
54
|
+
print "\e[#{y};#{x}H"
|
|
55
|
+
# Convert to sixel and display with proper pixel dimensions
|
|
56
|
+
system("convert #{escaped} -resize #{pixel_width}x#{pixel_height} sixel:- 2>/dev/null")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.clear
|
|
60
|
+
# Sixel images are inline - they don't need explicit clearing
|
|
61
|
+
# The terminal will handle this when content is redrawn
|
|
62
|
+
# Don't use \e[2J as that clears the entire screen including curses content!
|
|
63
|
+
true
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Überzug++ Protocol
|
|
68
|
+
module Ueberzug
|
|
69
|
+
def self.display(image_path, x:, y:, max_width:, max_height:)
|
|
70
|
+
# Get terminal pixel dimensions
|
|
71
|
+
terminfo = `xwininfo -id $(xdotool getactivewindow 2>/dev/null) 2>/dev/null`
|
|
72
|
+
return unless terminfo && !terminfo.empty?
|
|
73
|
+
|
|
74
|
+
term_w = terminfo.match(/Width: (\d+)/)[1].to_i
|
|
75
|
+
term_h = terminfo.match(/Height: (\d+)/)[1].to_i
|
|
76
|
+
|
|
77
|
+
# Calculate character dimensions
|
|
78
|
+
char_w = term_w / `tput cols`.to_i
|
|
79
|
+
char_h = term_h / `tput lines`.to_i
|
|
80
|
+
|
|
81
|
+
# Convert character positions to pixels
|
|
82
|
+
img_x = char_w * x
|
|
83
|
+
img_y = char_h * y
|
|
84
|
+
img_w = char_w * max_width
|
|
85
|
+
img_h = char_h * max_height
|
|
86
|
+
|
|
87
|
+
# TODO: Implement actual Überzug++ protocol
|
|
88
|
+
# For now, placeholder
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def self.clear
|
|
92
|
+
system('clear')
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# w3mimgdisplay Protocol
|
|
97
|
+
module W3m
|
|
98
|
+
@imgdisplay = '/usr/lib/w3m/w3mimgdisplay'
|
|
99
|
+
|
|
100
|
+
def self.display(image_path, x:, y:, max_width:, max_height:)
|
|
101
|
+
# Get terminal pixel dimensions
|
|
102
|
+
terminfo = `xwininfo -id $(xdotool getactivewindow 2>/dev/null) 2>/dev/null`
|
|
103
|
+
return unless terminfo && !terminfo.empty?
|
|
104
|
+
|
|
105
|
+
term_w = terminfo.match(/Width: (\d+)/)[1].to_i
|
|
106
|
+
term_h = terminfo.match(/Height: (\d+)/)[1].to_i
|
|
107
|
+
|
|
108
|
+
# Calculate character dimensions
|
|
109
|
+
cols = `tput cols`.to_i
|
|
110
|
+
lines = `tput lines`.to_i
|
|
111
|
+
char_w = term_w / cols
|
|
112
|
+
char_h = term_h / lines
|
|
113
|
+
|
|
114
|
+
# Convert to pixel coordinates
|
|
115
|
+
img_x = char_w * x
|
|
116
|
+
img_y = char_h * y
|
|
117
|
+
img_max_w = char_w * max_width
|
|
118
|
+
img_max_h = char_h * max_height
|
|
119
|
+
|
|
120
|
+
# Get image dimensions
|
|
121
|
+
escaped = Shellwords.escape(image_path)
|
|
122
|
+
dimensions = `identify -format "%wx%h" #{escaped}[0] 2>/dev/null`.strip
|
|
123
|
+
return if dimensions.empty?
|
|
124
|
+
|
|
125
|
+
img_w, img_h = dimensions.split('x').map(&:to_i)
|
|
126
|
+
|
|
127
|
+
# Scale if needed
|
|
128
|
+
if img_w > img_max_w || img_h > img_max_h
|
|
129
|
+
scale_w = img_max_w.to_f / img_w
|
|
130
|
+
scale_h = img_max_h.to_f / img_h
|
|
131
|
+
scale = [scale_w, scale_h].min
|
|
132
|
+
img_w = (img_w * scale).to_i
|
|
133
|
+
img_h = (img_h * scale).to_i
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Display using w3mimgdisplay protocol
|
|
137
|
+
`echo '0;1;#{img_x};#{img_y};#{img_w};#{img_h};;;;;#{image_path}
|
|
138
|
+
4;
|
|
139
|
+
3;' | #{@imgdisplay} 2>/dev/null`
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def self.clear(x:, y:, width:, height:, term_width:, term_height:)
|
|
143
|
+
# Clear only the image overlay in the specified area
|
|
144
|
+
terminfo = `xwininfo -id $(xdotool getactivewindow 2>/dev/null) 2>/dev/null`
|
|
145
|
+
return true unless terminfo && !terminfo.empty?
|
|
146
|
+
|
|
147
|
+
term_w = terminfo.match(/Width: (\d+)/)[1].to_i
|
|
148
|
+
term_h = terminfo.match(/Height: (\d+)/)[1].to_i
|
|
149
|
+
|
|
150
|
+
# Calculate character dimensions
|
|
151
|
+
char_w = term_w / term_width
|
|
152
|
+
char_h = term_h / term_height
|
|
153
|
+
|
|
154
|
+
# Convert to pixel coordinates with slight margin adjustment
|
|
155
|
+
img_x = (char_w * x) - char_w
|
|
156
|
+
img_y = char_h * y
|
|
157
|
+
img_max_w = (char_w * width) + char_w + 2
|
|
158
|
+
img_max_h = char_h * height + 2
|
|
159
|
+
|
|
160
|
+
# Use w3mimgdisplay command "6" to clear just the image area
|
|
161
|
+
`echo "6;#{img_x};#{img_y};#{img_max_w};#{img_max_h};\n4;\n3;" | #{@imgdisplay} 2>/dev/null`
|
|
162
|
+
true
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
data/lib/termpix.rb
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
require_relative 'termpix/version'
|
|
2
|
+
require_relative 'termpix/protocols'
|
|
3
|
+
|
|
4
|
+
module Termpix
|
|
5
|
+
class Display
|
|
6
|
+
attr_reader :protocol
|
|
7
|
+
|
|
8
|
+
def initialize(protocol: nil)
|
|
9
|
+
@protocol = protocol || detect_protocol
|
|
10
|
+
@current_image = nil
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Display an image at the specified position
|
|
14
|
+
# @param image_path [String] Path to the image file
|
|
15
|
+
# @param x [Integer] X position in terminal characters
|
|
16
|
+
# @param y [Integer] Y position in terminal characters
|
|
17
|
+
# @param max_width [Integer] Maximum width in terminal characters
|
|
18
|
+
# @param max_height [Integer] Maximum height in terminal characters
|
|
19
|
+
def show(image_path, x: 0, y: 0, max_width: 80, max_height: 24)
|
|
20
|
+
return false unless @protocol
|
|
21
|
+
return false unless File.exist?(image_path)
|
|
22
|
+
|
|
23
|
+
case @protocol
|
|
24
|
+
when :kitty
|
|
25
|
+
Protocols::Kitty.display(image_path, x: x, y: y, max_width: max_width, max_height: max_height)
|
|
26
|
+
when :sixel
|
|
27
|
+
Protocols::Sixel.display(image_path, x: x, y: y, max_width: max_width, max_height: max_height)
|
|
28
|
+
when :ueberzug
|
|
29
|
+
Protocols::Ueberzug.display(image_path, x: x, y: y, max_width: max_width, max_height: max_height)
|
|
30
|
+
when :w3m
|
|
31
|
+
Protocols::W3m.display(image_path, x: x, y: y, max_width: max_width, max_height: max_height)
|
|
32
|
+
else
|
|
33
|
+
return false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
@current_image = image_path
|
|
37
|
+
true
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Clear the currently displayed image
|
|
41
|
+
def clear(x: 0, y: 0, width: 80, height: 24, term_width: 80, term_height: 24)
|
|
42
|
+
return false unless @protocol
|
|
43
|
+
|
|
44
|
+
case @protocol
|
|
45
|
+
when :kitty
|
|
46
|
+
Protocols::Kitty.clear
|
|
47
|
+
when :sixel
|
|
48
|
+
Protocols::Sixel.clear
|
|
49
|
+
when :ueberzug
|
|
50
|
+
Protocols::Ueberzug.clear
|
|
51
|
+
when :w3m
|
|
52
|
+
Protocols::W3m.clear(x: x, y: y, width: width, height: height, term_width: term_width, term_height: term_height)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
@current_image = nil
|
|
56
|
+
true
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Check if image display is supported
|
|
60
|
+
def supported?
|
|
61
|
+
!@protocol.nil?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Get information about the current protocol
|
|
65
|
+
def info
|
|
66
|
+
{
|
|
67
|
+
protocol: @protocol,
|
|
68
|
+
supported: supported?,
|
|
69
|
+
current_image: @current_image
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def detect_protocol
|
|
76
|
+
# Check for Sixel support first - works better with curses apps
|
|
77
|
+
# Note: urxvt/rxvt-unicode does NOT support sixel unless specially compiled
|
|
78
|
+
# Kitty's sixel support doesn't work properly (shows ASCII) - use w3m instead
|
|
79
|
+
if ENV['TERM']&.match(/^xterm(?!-kitty)|^mlterm|^foot/)
|
|
80
|
+
return :sixel if check_dependency('convert')
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Kitty graphics protocol disabled - incompatible with curses apps
|
|
84
|
+
# Kitty protocol needs full terminal control, conflicts with curses rendering
|
|
85
|
+
# Users can choose between:
|
|
86
|
+
# 1. Enable w3m for Kitty (has brief flash) - set TERMPIX_KITTY_USE_W3M=1
|
|
87
|
+
# 2. No images in Kitty (clean UI)
|
|
88
|
+
|
|
89
|
+
# Überzug++ - disabled for now (implementation incomplete)
|
|
90
|
+
# TODO: Implement proper Überzug++ JSON-RPC communication
|
|
91
|
+
# if command_exists?('ueberzug') || command_exists?('ueberzugpp')
|
|
92
|
+
# if check_dependencies('xwininfo', 'xdotool', 'identify')
|
|
93
|
+
# return :ueberzug
|
|
94
|
+
# end
|
|
95
|
+
# end
|
|
96
|
+
|
|
97
|
+
# Fall back to w3m (works everywhere but has brief flash in Kitty)
|
|
98
|
+
if command_exists?('/usr/lib/w3m/w3mimgdisplay')
|
|
99
|
+
if check_dependencies('xwininfo', 'xdotool', 'identify')
|
|
100
|
+
return :w3m
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# No supported protocol found
|
|
105
|
+
nil
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def command_exists?(cmd)
|
|
109
|
+
system("which #{cmd} > /dev/null 2>&1")
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def check_dependency(cmd)
|
|
113
|
+
command_exists?(cmd)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def check_dependencies(*cmds)
|
|
117
|
+
cmds.all? { |cmd| command_exists?(cmd) }
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: termpix
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Geir Isene
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-10-26 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Termpix provides a clean API for displaying images in the terminal using
|
|
14
|
+
the best available protocol (Kitty, Sixel, Überzug++, or w3m). Auto-detects terminal
|
|
15
|
+
capabilities and falls back gracefully.
|
|
16
|
+
email: g@isene.com
|
|
17
|
+
executables: []
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- README.md
|
|
22
|
+
- lib/termpix.rb
|
|
23
|
+
- lib/termpix/protocols.rb
|
|
24
|
+
- lib/termpix/version.rb
|
|
25
|
+
homepage: https://github.com/isene/termpix
|
|
26
|
+
licenses:
|
|
27
|
+
- Unlicense
|
|
28
|
+
metadata:
|
|
29
|
+
source_code_uri: https://github.com/isene/termpix
|
|
30
|
+
post_install_message:
|
|
31
|
+
rdoc_options: []
|
|
32
|
+
require_paths:
|
|
33
|
+
- lib
|
|
34
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: 2.7.0
|
|
39
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
40
|
+
requirements:
|
|
41
|
+
- - ">="
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: '0'
|
|
44
|
+
requirements: []
|
|
45
|
+
rubygems_version: 3.4.20
|
|
46
|
+
signing_key:
|
|
47
|
+
specification_version: 4
|
|
48
|
+
summary: Modern terminal image display with multiple protocol support
|
|
49
|
+
test_files: []
|