term-codec
term-codec
A Crystal library for decoding and encoding terminal escape sequences. Incoming CSI, OSC, DCS and APC sequences are parsed into typed packets; outgoing packets are serialized back to their canonical wire form.
Installation
Add the dependency to your shard.yml:
dependencies:
term-codec:
github: shpeckman/term-codec
Then run shards install. Requires Crystal >= 1.21.
Usage
Decoding
Route a single sequence to a typed packet:
require "term-codec"
packet = Term::Codec.route("\e[I")
# => Term::Codec::FocusTracking::Report(Term::Codec::FocusTracking::Focus::In)
Unrecognized or malformed sequences decode to Term::Codec::Unknown, which keeps the raw bytes.
Stream a byte flow through the router; it handles concatenated sequences, chunk boundaries and interleaved plain text:
router = Term::Codec::Router.new
router.feed(bytes) do |packet|
case packet
when Term::Codec::Mouse::Press then handle_press(packet)
when Term::Codec::Keyboard::KeyEvent then handle_key(packet)
when Term::Codec::Unknown then forward(packet.raw)
end
end
# or straight from an IO
Term::Codec::Router.each(STDIN) { |packet| handle(packet) }
Each protocol module also parses on its own:
Term::Codec::Mouse.parse("\e[<0;10;5M") # => Mouse::Press
Term::Codec::Mouse.parse?("garbage") # => nil
Encoding
Every codec serializes its packets back to the wire — either directly:
Term::Codec::Mouse.encode(Term::Codec::Mouse::Press.new(
Term::Codec::Mouse::Button::Left, Term::Codec::Mouse::Mods::None, 10, 5))
# => "\e[<0;10;5M"
or through the central dispatcher, which routes on the packet's type:
Term::Codec.encode(packet) # => String
Term::Codec.encode(packet, io) # writes to an IO
Term::Codec.encode accepts Term::Codec::EncodablePacket — a compile-time union of every encodable packet type plus Unknown (which passes its raw bytes through). Passing a packet type no codec can encode is a compile error, not a runtime failure.
Encoding is canonical: OSC sequences terminate with ST, base64 payloads are padded, hex output is uppercase. Decoding remains lenient (BEL or ST, relaxed base64, either hex case), so the round-trip guarantee is semantic:
Term::Codec.route(Term::Codec.encode(packet)) == packet
Multi-sequence operations (chunked clipboard sessions, notification bodies split across chunks) stay available as module-level helpers, e.g. Term::Codec::Clipboard::Osc5522.write(io, data) and Term::Codec::Notify.notify(io, title, body).
Protocols
| Module | Sequences |
|---|---|
Term::Codec::Clipboard |
OSC 52, OSC 5522 clipboard read/write |
Term::Codec::ColorControl |
OSC 21 dynamic colors, OSC 4/10x legacy palette |
Term::Codec::ColorScheme |
dark/light scheme query and report |
Term::Codec::DeviceAttributes |
DA1/DA2/DA3 requests and reports |
Term::Codec::DeviceStatus |
DSR, CPR, DECXCPR, DECRQPSR (DECCIR/DECTABSR) |
Term::Codec::Dnd |
OSC 73 drag-and-drop |
Term::Codec::FileTransfer |
OSC 5113 file transfer |
Term::Codec::FocusTracking |
focus in/out reports |
Term::Codec::Glyph |
APC glyph protocol (PUA glyph registration) |
Term::Codec::Graphics |
APC graphics protocol |
Term::Codec::InbandResize |
in-band size reports |
Term::Codec::Keyboard |
kitty keyboard protocol |
Term::Codec::Mouse |
SGR mouse events |
Term::Codec::MultipleCursors |
multiple cursor shapes and colors |
Term::Codec::Notify |
OSC 9/99 notifications |
Term::Codec::PointerShape |
OSC 22 pointer shapes |
Term::Codec::Visibility |
window visibility query and report |
Term::Codec::XtermQuery |
XTVERSION, XTGETTCAP, XTGETXRES |
Term::Codec::Xtwinops |
CSI t window operations, OSC title stack |
Defining a codec
Protocols are declared with the Term::Codec.codec macro: route declarations tell the router which sequences belong to the codec, a decode block parses bytes into a packet, and an optional encode block serializes packets back:
module Term::Codec::FocusTracking
abstract struct Packet
abstract def write(io : IO) : Nil
end
record Report < Packet, focus : Focus do
def write(io : IO) : Nil
io << (@focus.in? ? "\e[I" : "\e[O")
end
end
Term::Codec.codec do
route :csi, final: {'I', 'O'}
decode do |bytes|
# Bytes -> Packet?
end
encode do |packet, io|
packet.write(io)
end
end
end
Route kinds: :csi (by final char, optional prefix), :osc (by command number), :dcs and :apc (by prefix string). A codec without an encode block is left out of EncodablePacket at compile time.
Development
shards install # no runtime dependencies
crystal spec # run the test suite
License
MIT
term-codec
- 0
- 0
- 0
- 0
- 0
- about 4 hours ago
- September 7, 2026
MIT License
Tue, 08 Sep 2026 01:07:37 GMT