Skip to content

Instantly share code, notes, and snippets.

@VinceGuidry
Created September 24, 2025 13:27
Show Gist options
  • Select an option

  • Save VinceGuidry/b44f02117d452bde7471dae2effb709a to your computer and use it in GitHub Desktop.

Select an option

Save VinceGuidry/b44f02117d452bde7471dae2effb709a to your computer and use it in GitHub Desktop.
Kitty terminal protocol parsing
require 'io/console'
require 'ansi/terminal'
require 'ansi/string'
require 'pry'
module Vinced
class Keypress
attr_reader :string, :parsed, :printable
def initialize(string)
@string = string
@parsed = parse
@printable = lookup
end
def printable?
case string[0]
when "\e"
false
when "\x7F"
false
when "\r"
false
when "\t"
false
else
true
end
end
def kitty_unprintable_lookup
{
"27" => :escape,
"127" => :backspace,
"13" => :enter,
}[md['codepoint']]
end
def csi? = (string[0] == "\e")
def bytes = string.bytes.map{|b| b.to_s(16)}
def hex_bytes = bytes
def ==(str) = (string == str)
def md = parsed[1]
def type = parsed[0]
def inspect
return "<Keypress type='#{type}' lookup='#{lookup}' mods='#{modifiers.join(' ')}' str=#{string.inspect}>"
end
def other_lookup
{
"\t" => :tab,
"\x7F" => :backspace,
"\x08" => :backspace,
"\r" => :enter,
}[md['other']]
end
def func_lookup
{
'27 u' => :escape, '13 u' => :enter,
'9 u' => :tab, '127 u' => :backspace,
"2 ~" => :insert, "3 ~" => :delete,
"1 D" => :left, "D" => :left,
"1 C" => :right, "C" => :right,
"1 A" => :up, "A" => :up,
"1 B" => :down, "B" => :down,
"5 ~" => :page_up, "6 ~" => :page_down,
"1 H" => :home, "7 ~" => :home,
"1 F" => :end, "8 ~" => :end,
"1 P" => :f1, "11 ~" => :f1,
"1 Q" => :f2, "12 ~" => :f2,
"13 ~" => :f3, "1 E" => :kp_begin,
"1 S" => :f4, "14 ~" => :f4,
"15 ~" => :f5, "17 ~" => :f6,
"18 ~" => :f7, "19 ~" => :f8,
"20 ~" => :f9, "21 ~" => :f10,
"23 ~" => :f11, "24 ~" => :f12,
}[cp_term]
end
def legacy_lookup
{
"A" => :up,
"B" => :down,
"C" => :right,
"D" => :left,
"E" => :kp_begin,
"F" => :end,
"H" => :home,
"P" => :f1,
"Q" => :f2,
"S" => :f4,
}[md['legacy']]
end
def cp_term
return kitty_unprintable_lookup if kitty_unprintable_lookup
[md['codepoint'].to_i].pack('U')
end
def lookup
return other_lookup if type == :other
return func_lookup if type == :func
return legacy_lookup if type == :legacy
return cp_term if type == :kitty
return string
end
def modifiers
m = md['modifiers'] rescue nil
return [] if m.nil?
s = m.to_i.-(1).to_s(2)
s.prepend("0" * (8 - s.length))
s = s.chars.map(&:to_i)
a = []
m = [:num_lock, :caps_lock, :meta, :hyper, :super, :ctrl, :alt, :shift]
m.each_index do |i|
next if m[i].nil?
a << m[i] if s[i] == 1
end
a.reverse
end
def other_parse_regex
/^(?<other>[\r\t\x7F\x08])$/
end
def legacy_parse_regex
/^\e\[(?<legacy>\w)$/
end
def func_parse_regex
/^\e\[((?<codepoint>\d*);(?<modifiers>\d+))?(?<term>[ABCDEFHPQS~])$/
end
def kitty_parse_regex
/^\e\[(?<codepoint>\d+)(;(?<modifiers>\d+))?(?<term>u)$/
end
def parse
return [:kitty, Regexp.last_match] if !kitty_parse_regex.match(string).nil?
return [:func, Regexp.last_match] if !func_parse_regex.match(string).nil?
return [:legacy, Regexp.last_match] if !legacy_parse_regex.match(string).nil?
return [:other, Regexp.last_match] if !other_parse_regex.match(string).nil?
return [:unicode, string]
end
def character
return unprintable if unprintable
return string if printable?
[codepoint].pack('U')
end
def to_s
return string if printable?
return string.bytes.join(' ')
end
end
class Terminal
def get_key
begin
str = STDIN.read_nonblock(100)
rescue IO::WaitReadable
IO.select([STDIN])
retry
end
Keypress.new(str)
end
def raw
STDIN.raw do
kitty_disambig do
yield
end
end
end
def cooked
STDIN.cooked do
yield
end
end
def kitty_supported?
end
def kitty_disambig
send_control_char(">1")
yield
send_control_char("<")
end
def eof?
STDIN.eof?
end
def csi = [0x1b, 0x5b]
def send_ansi(string)
bytearray = csi.concat(string.bytes)
STDOUT.print(bytearray.pack('U*'))
end
def send_control_char(string)
term = [0x75]
bytearray = csi.concat(string.bytes).concat(term)
STDOUT.print(bytearray.pack('U*'))
end
def clear_screen
send_ansi("2J")
send_ansi(";H")
end
end
class Editor
attr_reader :term
def initialize(term)
@term = term
end
def do_pry(c)
term.cooked do
binding.pry
end
end
def run
c = nil
term.raw do
term.clear_screen
loop do
c = term.get_key
print c.inspect
print "\r\n"
do_pry(c) if c == 'p'
break if c == 'q' or term.eof?
end
print "\r\n"
end
end
end
end
include Vinced
if ARGV.include?('-p')
t = Terminal.new
binding.pry
else
Editor.new(Terminal.new).run
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment