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 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
+ ![Ruby](https://img.shields.io/badge/language-Ruby-red) [![Gem Version](https://badge.fury.io/rb/termpix.svg)](https://badge.fury.io/rb/termpix) ![Unlicense](https://img.shields.io/badge/license-Unlicense-green) ![Stay Amazing](https://img.shields.io/badge/Stay-Amazing-important)
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
@@ -0,0 +1,3 @@
1
+ module Termpix
2
+ VERSION = "0.1.0"
3
+ 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: []