My agentic slop goes here. Not intended for anyone else!

sync tgp

+113 -46
tgp/README.md
···
-
# Textsize
+
# Termext - Terminal Extensions for OCaml
-
A clean, standalone OCaml library implementing the [Kitty text sizing protocol](https://sw.kovidgoyal.net/kitty/text-sizing-protocol/).
+
A clean OCaml library implementing Kitty terminal protocols for enhanced terminal output.
## Overview
-
The Kitty text sizing protocol (introduced in kitty v0.40.0) allows terminals to render text in different sizes, enabling typographic features like headlines, superscripts, and subscripts.
+
Termext provides OCaml bindings for modern terminal protocols:
+
+
- **Text Sizing**: The [Kitty text sizing protocol](https://sw.kovidgoyal.net/kitty/text-sizing-protocol/) for rendering text at different sizes
+
- **Graphics**: The [Kitty graphics protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/) for displaying images and animations
## Features
+
### Text Sizing (Textsize module)
- โœจ Clean, type-safe API with validation
-
- ๐Ÿ“ฆ Zero dependencies (besides OCaml standard library)
- ๐ŸŽฏ Full protocol support (scale, width, fractional sizing, alignment)
-
- ๐Ÿš€ Convenient helper functions for common use cases
-
- ๐Ÿ“ Comprehensive documentation
+
- ๐Ÿš€ Convenient helper functions (double, triple, superscript, subscript)
+
- ๐Ÿ“ Fmt-style formatters
+
+
### Graphics (Termgraph module - In Development)
+
- ๐Ÿ–ผ๏ธ PNG image display
+
- ๐Ÿ“ File and direct transmission
+
- ๐ŸŽฌ Animation support
+
- ๐ŸŽจ Z-index layering
+
- ๐Ÿ“ฆ Fmt-style formatters
+
- ๐ŸŽฏ Type-safe placement control
## Installation
```bash
# Via opam (once published)
-
opam install textsize
+
opam install termext
# Or add to your dune-project dependencies
(depends
-
(textsize (>= 0.1.0)))
+
(termext (>= 0.1.0)))
```
## Quick Start
+
### Text Sizing
+
```ocaml
(* Simple convenience functions *)
-
print_string (Textsize.double "Hello, World!");;
-
print_string (Textsize.triple "Big Text");;
-
print_string (Textsize.half "Small text");;
+
print_string (Termext.Textsize.double "Hello, World!");;
+
print_string (Termext.Textsize.triple "Big Text");;
+
print_string (Termext.Textsize.half "Small text");;
(* Superscripts and subscripts *)
print_string "E=mc";
-
print_string (Textsize.superscript "2");;
+
print_string (Termext.Textsize.superscript "2");;
print_string "H";
-
print_string (Textsize.subscript "2");
+
print_string (Termext.Textsize.subscript "2");
print_string "O";;
-
(* Custom sizing with metadata *)
-
let custom =
-
Textsize.empty
-
|> Textsize.with_scale (Textsize.make_scale 4)
-
|> Textsize.with_width (Textsize.make_width 6)
+
(* Fmt integration *)
+
Fmt.pr "The answer is %a\n"
+
(Termext.Textsize.styled_double Fmt.int) 42;;
+
```
+
+
### Graphics (Preview)
+
+
```ocaml
+
(* Display a PNG file *)
+
print_string (Termext.Termgraph.display_png_file "image.png");;
+
+
(* With size control *)
+
print_string (Termext.Termgraph.display_png_file
+
~rows:10
+
~columns:20
+
"logo.png");;
+
+
(* Fmt integration *)
+
let img = Termext.Termgraph.v
+
~rows:15
+
(Termext.Termgraph.File { path = "banner.png" })
+
()
in
-
print_string (Textsize.render custom "Custom sized");;
+
Fmt.pr "Welcome! %a\n" Termext.Termgraph.pp img;;
+
+
(* Animation *)
+
let anim_id = Termext.Termgraph.image_id_of_string "spinner" in
+
let frame1 = Termext.Termgraph.Animation.frame
+
~gap:100
+
(Termext.Termgraph.File { path = "frame1.png" })
+
1
+
in
+
print_string (Termext.Termgraph.Animation.render_frame anim_id frame1);;
```
## API Overview
-
### Core Types
+
### Textsize Module
-
- `scale` - Text scale factor (1-7)
-
- `width` - Width in cells (0-7)
-
- `numerator`/`denominator` - Fractional scaling (0-15)
-
- `vertical_align` - Vertical alignment (Bottom, Center, Top)
-
- `horizontal_align` - Horizontal alignment (Left, Center, Right)
-
- `metadata` - Combined sizing metadata
-
-
### Convenience Functions
+
**Types & Builders**
+
- `vertical`, `horizontal` - Alignment types
+
- `v ?scale ?width ?fraction ?vertical ?horizontal ()` - Create sizing metadata
+
- `empty` - Empty metadata
+
**Convenience Functions**
- `double`, `triple`, `quadruple` - Quick size multipliers
- `half` - Half-sized text
- `superscript`, `subscript` - Typographic positioning
- `scaled n` - Arbitrary scale factor
-
### Metadata Builders
+
**Rendering**
+
- `render metadata text` - Generate escape sequence
+
- `render_to_channel oc metadata text` - Write to channel
+
- `pp`, `styled` - Fmt formatters
-
- `empty` - Start with empty metadata
-
- `with_scale` - Set scale factor
-
- `with_width` - Set width
-
- `with_fraction` - Set fractional scaling
-
- `with_vertical_align` - Set vertical alignment
-
- `with_horizontal_align` - Set horizontal alignment
+
### Termgraph Module (In Development)
+
+
**Core Types**
+
- `format` - RGB, RGBA, PNG
+
- `compression` - No_compression, Zlib
+
- `transmission` - Direct, File
+
- `image_id`, `placement_id` - Identifiers
+
+
**Placement**
+
- `v ?image_id ?x ?y ?width ?height ?rows ?columns ?z_index transmission ()` - Create placement
+
+
**Animation**
+
- `Animation.frame` - Define animation frames
+
- `Animation.render_frame` - Upload frames
+
- `Animation.render_control` - Control playback
+
+
**Deletion**
+
- `Delete.render` - Delete images by various criteria
-
### Rendering
+
**Convenience**
+
- `display_png_file` - Quick PNG display
+
- `display_png_bytes` - Display PNG from bytes
+
- `pp`, `pp_delete`, `pp_animation_*` - Fmt formatters
-
- `render metadata text` - Generate escape sequence
-
- `render_to_channel oc metadata text` - Write to channel
+
See [TODO.md](TODO.md) for implementation status and roadmap.
## Building from Source
```bash
-
cd textsize
+
cd termext
dune build
-
dune exec example/demo.exe # Run example
+
dune exec example/demo.exe # Text sizing demo
+
dune exec example/graphics_demo.exe # Graphics demo (requires PNG files)
```
## Terminal Compatibility
-
This library generates escape sequences for the Kitty text sizing protocol. It requires a terminal emulator that supports this protocol:
+
This library generates escape sequences for Kitty terminal protocols.
+
**Text Sizing Protocol:**
- โœ… [Kitty](https://sw.kovidgoyal.net/kitty/) (v0.40.0+)
-
- โœ… [Ghostty](https://ghostty.org/) (with protocol support)
+
- โœ… [Ghostty](https://ghostty.org/)
+
+
**Graphics Protocol:**
+
- โœ… [Kitty](https://sw.kovidgoyal.net/kitty/)
+
- โœ… [Ghostty](https://ghostty.org/)
+
- โš ๏ธ [WezTerm](https://wezfurlong.org/wezterm/) (partial support)
+
- โš ๏ธ [Konsole](https://konsole.kde.org/) (partial support)
-
Other terminals will typically ignore these sequences gracefully.
+
Other terminals will typically ignore these sequences gracefully (no errors, just no special rendering).
## Protocol Reference
-
The library implements OSC 66 sequences as specified in the [Kitty text sizing protocol documentation](https://sw.kovidgoyal.net/kitty/text-sizing-protocol/).
+
**Text Sizing:** OSC 66 sequences - `ESC ] 66 ; metadata ; text BEL`
+
- See: [Kitty Text Sizing Protocol](https://sw.kovidgoyal.net/kitty/text-sizing-protocol/)
+
+
**Graphics:** APC sequences - `ESC _G<control>;<payload>ESC \`
+
- See: [Kitty Graphics Protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/)
-
Format: `ESC ] 66 ; metadata ; text BEL`
+
## Dependencies
-
Where metadata is colon-separated key=value pairs.
+
- **fmt**: Formatting library (already widely used)
+
- **base64**: Base64 encoding for graphics transmission
+
- **camlzip**: Zlib compression for graphics (optional optimization)
## License
+252
tgp/TODO.md
···
+
# TODO - Termext Library Development
+
+
## Current Status
+
+
### Completed
+
- โœ… Text sizing protocol (Textsize module)
+
- โœ… Graphics protocol interface design (Termgraph module)
+
- โœ… Library renamed from `textsize` to `termext`
+
- โœ… Dependencies added (base64, camlzip)
+
+
### In Progress
+
- ๐Ÿšง Termgraph module implementation
+
- ๐Ÿšง Animation support
+
- ๐Ÿšง Example binaries
+
+
## Graphics Protocol Implementation Plan
+
+
### Phase 1: Core Transmission (Current)
+
+
**Direct Transmission**
+
- [x] Interface design
+
- [ ] Base64 encoding of image data
+
- [ ] Chunking for large images (4096 byte chunks)
+
- [ ] PNG format support (user-provided bytes)
+
- [ ] RGB/RGBA raw format support
+
- [ ] Escape sequence generation (APC format)
+
+
**File Transmission**
+
- [x] Interface design
+
- [ ] File path validation
+
- [ ] Escape sequence generation for file paths
+
- [ ] Handle file transmission medium (t=f)
+
+
**Placement Control**
+
- [x] Interface design
+
- [ ] Position control (x, y offsets)
+
- [ ] Size control (width, height, rows, columns)
+
- [ ] Z-index support
+
- [ ] Cursor movement control
+
- [ ] Image and placement IDs
+
+
**Deletion**
+
- [x] Interface design
+
- [ ] Delete by image ID
+
- [ ] Delete by placement ID
+
- [ ] Delete at cursor
+
- [ ] Delete all images
+
+
### Phase 2: Animation Support
+
+
**Frame Transmission**
+
- [x] Interface design
+
- [ ] Frame numbering (1-based)
+
- [ ] Composition modes (blend, overwrite)
+
- [ ] Frame gap timing (milliseconds)
+
- [ ] Multi-frame upload
+
+
**Animation Control**
+
- [x] Interface design
+
- [ ] Set gap for specific frames
+
- [ ] Loop control (count, infinite)
+
- [ ] Start/stop animation
+
- [ ] Frame composition control
+
+
### Phase 3: Advanced Features (Future)
+
+
**Compression**
+
- [ ] ZLIB compression for image data
+
- [ ] Automatic compression for large images
+
- [ ] Compression level control
+
+
**Temporary File Transmission**
+
- [ ] Temp file creation
+
- [ ] Escape sequence generation (t=t)
+
- [ ] Cleanup handling
+
+
**Shared Memory Transmission**
+
- [ ] Shared memory name generation
+
- [ ] Escape sequence generation (t=s)
+
- [ ] Platform-specific support (POSIX shared memory)
+
+
**Virtual Placements**
+
- [ ] Unicode placeholder support
+
- [ ] Virtual placement references
+
- [ ] Diacritic composition
+
+
**Query/Response Handling**
+
- [ ] Terminal capability detection
+
- [ ] Success/failure response parsing
+
- [ ] Error handling
+
+
**Advanced Placement Options**
+
- [ ] Source rectangle (crop images)
+
- [ ] Cell offset positioning
+
- [ ] Relative positioning
+
- [ ] Background/foreground placement
+
+
**Image Composition**
+
- [ ] Alpha blending modes
+
- [ ] Composition operations
+
- [ ] Multi-layer support
+
+
### Phase 4: Eio Integration (Future)
+
+
**Async Operations**
+
- [ ] Eio-based file reading
+
- [ ] Async chunked transmission
+
- [ ] Streaming large images
+
- [ ] Non-blocking operations
+
+
**Buf_read/Buf_write Integration**
+
- [ ] Use Eio.Buf_read for parsing responses
+
- [ ] Use Eio.Buf_write for efficient serialization
+
- [ ] Zero-copy optimizations
+
+
### Phase 5: Quality & Documentation
+
+
**Testing**
+
- [ ] Unit tests for escape sequence generation
+
- [ ] Unit tests for chunking logic
+
- [ ] Unit tests for base64 encoding
+
- [ ] Integration tests with mock terminal
+
- [ ] Animation frame sequencing tests
+
- [ ] Example binaries for manual testing
+
+
**Documentation**
+
- [ ] Complete API documentation
+
- [ ] Usage examples
+
- [ ] Animation tutorial
+
- [ ] Performance guidelines
+
- [ ] Terminal compatibility matrix
+
+
**Examples**
+
- [ ] Simple image display
+
- [ ] Animated GIF-style display
+
- [ ] Image gallery
+
- [ ] Terminal image viewer
+
- [ ] Progress indicator with graphics
+
+
## Design Decisions
+
+
### Transmission Format
+
+
The Kitty graphics protocol uses APC escape sequences:
+
```
+
ESC _G<control>;<payload>ESC \
+
```
+
+
- **Control data**: Comma-separated key=value pairs
+
- **Payload**: Base64-encoded image data (for direct transmission)
+
+
### Key Control Parameters
+
+
| Key | Description | Values |
+
|-----|-------------|--------|
+
| `a` | Action | `t`=transmit+display, `T`=transmit only, `p`=display, `d`=delete, `f`=frame, `a`=animate |
+
| `f` | Format | `24`=RGB, `32`=RGBA, `100`=PNG |
+
| `t` | Transmission | `d`=direct, `f`=file, `t`=temp, `s`=shared |
+
| `i` | Image ID | Number or string (max 24 chars) |
+
| `p` | Placement ID | Number |
+
| `s` | Width | Pixels |
+
| `v` | Height | Pixels |
+
| `c` | Columns | Terminal cells |
+
| `r` | Rows | Terminal cells |
+
| `x` | X offset | Pixels |
+
| `y` | Y offset | Pixels |
+
| `z` | Z-index | Integer |
+
| `C` | Cursor | `0`=don't move, `1`=move |
+
| `o` | Compression | `z`=zlib |
+
| `m` | More chunks | `1`=more coming, `0`=last |
+
+
### Animation-Specific Parameters
+
+
| Key | Description | Values |
+
|-----|-------------|--------|
+
| `z` | Frame number | Integer (1-based) |
+
| `g` | Gap | Milliseconds (0-65535) |
+
| `c` | Composition | `0`=blend, `1`=overwrite |
+
| `v` | Loop | Number (0=infinite) |
+
| `s` | Animation state | `1`=stop, `2`=run |
+
+
### Chunking Strategy
+
+
- Maximum chunk size: 4096 bytes (protocol limit)
+
- For images larger than 4096 bytes when base64-encoded:
+
1. First chunk: Include full control data, set `m=1`
+
2. Middle chunks: Only payload data, set `m=1`
+
3. Last chunk: Only payload data, set `m=0`
+
+
### Error Handling
+
+
- Validate all parameters before encoding
+
- Check file existence for file-based transmission
+
- Ensure image data dimensions match declared width/height
+
- Validate ID formats (image_id max 24 chars)
+
- Raise `Invalid_argument` for out-of-range values
+
+
### Memory Efficiency
+
+
- Use `bytes` for image data (mutable, efficient)
+
- Stream large files rather than loading into memory
+
- Base64 encode in chunks to limit memory usage
+
- Consider compression for large images
+
+
## Future Enhancements
+
+
### Additional Protocols
+
- [ ] Kitty keyboard protocol
+
- [ ] Kitty clipboard protocol
+
- [ ] Synchronized output protocol
+
+
### Utilities
+
- [ ] Image format detection
+
- [ ] Automatic format conversion
+
- [ ] Image resizing/scaling helpers
+
- [ ] Color quantization
+
+
### Integration
+
- [ ] Notty integration
+
- [ ] Direct terminal detection
+
- [ ] Fallback for non-Kitty terminals
+
+
## Notes
+
+
### Dependencies Rationale
+
+
- **fmt**: Already used in Textsize, provides consistent formatting API
+
- **base64**: Standard library for base64 encoding/decoding, well-tested
+
- **camlzip**: ZLIB compression support, mature library
+
+
### No Image Library Dependencies
+
+
Deliberately not depending on image libraries (camlimages, imagelib) because:
+
1. Keeps library lightweight
+
2. Users may already have image data in memory
+
3. PNG format can be passed through without decoding
+
4. Allows flexibility in image source (file, network, generated, etc.)
+
5. User can choose their preferred image library
+
+
### Compatibility
+
+
**Terminals with full support:**
+
- Kitty (v0.40.0+ for text sizing, earlier for graphics)
+
- Ghostty (with graphics protocol support)
+
+
**Terminals with partial support:**
+
- WezTerm (graphics protocol)
+
- Konsole (partial graphics support)
+
+
**Graceful degradation:**
+
- Other terminals typically ignore unknown escape sequences
+
- No visible output, but no crashes or errors
+8 -5
tgp/dune-project
···
(lang dune 3.0)
-
(name textsize)
+
(name termext)
(version 0.1.0)
(generate_opam_files true)
-
(source (github username/textsize))
+
(source (github username/termext))
(license MIT)
(authors "Anonymous")
(maintainers "Anonymous")
(package
-
(name textsize)
-
(synopsis "OCaml implementation of the Kitty text sizing protocol")
-
(description "A clean, standalone library for generating escape sequences to render text in different sizes using the Kitty terminal's text sizing protocol.")
+
(name termext)
+
(synopsis "OCaml implementation of Kitty terminal protocols")
+
(description "A library for generating escape sequences for Kitty terminal protocols including text sizing and graphics display.")
(depends
(ocaml (>= 4.14))
+
(fmt (>= 0.9.0))
+
(base64 (>= 3.5.0))
+
(camlzip (>= 1.11))
dune))
+22 -22
tgp/example/demo.ml
···
print_endline "=== Kitty Text Sizing Protocol Demo ===\n";
(* Simple convenience functions *)
-
print_string (Textsize.double "Double sized text");
+
print_string (Termext.Textsize.double "Double sized text");
print_endline "\n";
-
print_string (Textsize.triple "Triple sized text");
+
print_string (Termext.Textsize.triple "Triple sized text");
print_endline "\n\n";
-
print_string (Textsize.quadruple "Quadruple sized text");
+
print_string (Termext.Textsize.quadruple "Quadruple sized text");
print_endline "\n\n\n";
(* Scaled text *)
-
print_string (Textsize.scaled 5 "Scale 5 text");
+
print_string (Termext.Textsize.scaled 5 "Scale 5 text");
print_endline "\n\n\n\n";
(* Fractional scaling *)
-
print_string (Textsize.half "Half sized text");
+
print_string (Termext.Textsize.half "Half sized text");
print_newline ();
(* Superscripts and subscripts *)
print_string "E=mc";
-
print_string (Textsize.superscript "2");
+
print_string (Termext.Textsize.superscript "2");
print_newline ();
print_string "H";
-
print_string (Textsize.subscript "2");
+
print_string (Termext.Textsize.subscript "2");
print_string "O";
print_newline ();
(* Custom metadata *)
print_newline ();
-
let custom = Textsize.v ~scale:3 ~width:5 () in
-
print_string (Textsize.render custom "Custom sized text");
+
let custom = Termext.Textsize.v ~scale:3 ~width:5 () in
+
print_string (Termext.Textsize.render custom "Custom sized text");
print_endline "\n\n";
(* Fractional with alignment *)
-
let aligned = Textsize.v ~fraction:(1, 3) ~vertical:`Center ~horizontal:`Center () in
-
print_string (Textsize.render aligned "Centered 1/3 size text");
+
let aligned = Termext.Textsize.v ~fraction:(1, 3) ~vertical:`Center ~horizontal:`Center () in
+
print_string (Termext.Textsize.render aligned "Centered 1/3 size text");
print_newline ();
(* Fmt-style combinators *)
print_endline "\n=== Fmt-style Examples ===\n";
(* Basic pp formatter *)
-
Fmt.pr "This is %a text\n" Textsize.pp_double "double-sized";
-
Fmt.pr "This is %a text\n" Textsize.pp_triple "triple-sized";
+
Fmt.pr "This is %a text\n" Termext.Textsize.pp_double "double-sized";
+
Fmt.pr "This is %a text\n" Termext.Textsize.pp_triple "triple-sized";
(* Superscripts and subscripts with Fmt *)
-
Fmt.pr "E=mc%a\n" Textsize.pp_superscript "2";
-
Fmt.pr "H%aO\n" Textsize.pp_subscript "2";
+
Fmt.pr "E=mc%a\n" Termext.Textsize.pp_superscript "2";
+
Fmt.pr "H%aO\n" Termext.Textsize.pp_subscript "2";
(* Styled combinators wrapping other formatters *)
-
Fmt.pr "The answer is %a\n" (Textsize.styled_double Fmt.int) 42;
-
Fmt.pr "Pi is approximately %a\n" (Textsize.styled_triple Fmt.float) 3.14159;
+
Fmt.pr "The answer is %a\n" (Termext.Textsize.styled_double Fmt.int) 42;
+
Fmt.pr "Pi is approximately %a\n" (Termext.Textsize.styled_triple Fmt.float) 3.14159;
(* Complex formatting with Fmt *)
Fmt.pr "Temperature: %aยฐC (that's %aยฐF)\n"
-
(Textsize.styled_superscript Fmt.int) 25
-
(Textsize.styled_subscript Fmt.int) 77;
+
(Termext.Textsize.styled_superscript Fmt.int) 25
+
(Termext.Textsize.styled_subscript Fmt.int) 77;
(* Custom metadata with Fmt *)
-
let custom_meta = Textsize.v ~scale:4 () in
-
Fmt.pr "\n%a\n" (Textsize.pp custom_meta) "Custom Fmt-styled text";
+
let custom_meta = Termext.Textsize.v ~scale:4 () in
+
Fmt.pr "\n%a\n" (Termext.Textsize.pp custom_meta) "Custom Fmt-styled text";
(* Composing with other Fmt combinators *)
Fmt.pr "\nList items: %a\n"
-
(Fmt.list ~sep:(Fmt.any ", ") (Textsize.styled_double Fmt.string))
+
(Fmt.list ~sep:(Fmt.any ", ") (Termext.Textsize.styled_double Fmt.string))
["apple"; "banana"; "cherry"];
print_endline "\n=== Demo Complete ===";
+7 -1
tgp/example/dune
···
(executable
(name demo)
-
(libraries textsize fmt))
+
(modules demo)
+
(libraries termext fmt))
+
+
(executable
+
(name graphics_demo)
+
(modules graphics_demo)
+
(libraries termext fmt unix))
+156
tgp/example/graphics_demo.ml
···
+
(* Example demonstrating the Termgraph module of termext library *)
+
+
let () =
+
print_endline "=== Kitty Graphics Protocol Demo ===\n";
+
+
(* Example 1: Display a PNG file *)
+
print_endline "--- Example 1: Display PNG file ---";
+
let png_path = "example.png" in
+
(* Simple convenience function *)
+
print_string (Termext.Termgraph.display_png_file png_path);
+
print_endline "\n";
+
+
(* Example 2: Display PNG file with size control *)
+
print_endline "--- Example 2: PNG file with sizing ---";
+
print_string (Termext.Termgraph.display_png_file
+
~rows:10
+
~columns:20
+
png_path);
+
print_endline "\n";
+
+
(* Example 3: Using the full placement API *)
+
print_endline "--- Example 3: Full placement control ---";
+
let img_id = Termext.Termgraph.image_id_of_string "demo-image" in
+
let placement = Termext.Termgraph.v
+
~image_id:img_id
+
~x:10
+
~y:5
+
~rows:15
+
~columns:30
+
~z_index:10
+
(Termext.Termgraph.File { path = png_path })
+
()
+
in
+
print_string (Termext.Termgraph.render placement);
+
print_endline "\n";
+
+
(* Example 4: Direct transmission with raw PNG data *)
+
print_endline "--- Example 4: Direct transmission ---";
+
(* In a real use case, you would load PNG data from somewhere *)
+
let png_data = Bytes.of_string "\x89PNG\r\n\x1a\n..." in
+
print_string (Termext.Termgraph.display_png_bytes
+
~w:100
+
~h:100
+
~rows:10
+
~columns:10
+
png_data);
+
print_endline "\n";
+
+
(* Example 5: Fmt-style formatters *)
+
print_endline "--- Example 5: Fmt formatters ---";
+
let img = Termext.Termgraph.v
+
(Termext.Termgraph.File { path = "logo.png" })
+
()
+
in
+
Fmt.pr "Here's our logo: %a\n" (Termext.Termgraph.pp img) ();
+
print_endline "";
+
+
(* Example 6: Animation *)
+
print_endline "--- Example 6: Animation frames ---";
+
let anim_id = Termext.Termgraph.image_id_of_string "animation" in
+
+
(* Upload frame 1 *)
+
let frame1 = Termext.Termgraph.Animation.frame
+
~gap:100 (* 100ms gap *)
+
(Termext.Termgraph.File { path = "frame1.png" })
+
1 (* Frame number *)
+
in
+
print_string (Termext.Termgraph.Animation.render_frame anim_id frame1);
+
+
(* Upload frame 2 *)
+
let frame2 = Termext.Termgraph.Animation.frame
+
~gap:100
+
~composition:Termext.Termgraph.Animation.Blend
+
(Termext.Termgraph.File { path = "frame2.png" })
+
2
+
in
+
print_string (Termext.Termgraph.Animation.render_frame anim_id frame2);
+
+
(* Upload frame 3 *)
+
let frame3 = Termext.Termgraph.Animation.frame
+
~gap:100
+
(Termext.Termgraph.File { path = "frame3.png" })
+
3
+
in
+
print_string (Termext.Termgraph.Animation.render_frame anim_id frame3);
+
+
(* Set animation to loop infinitely *)
+
let loop_control = Termext.Termgraph.Animation.Set_loop 0 in
+
print_string (Termext.Termgraph.Animation.render_control anim_id loop_control);
+
print_endline "\n";
+
+
(* Example 7: Using Fmt for animation *)
+
print_endline "--- Example 7: Animation with Fmt ---";
+
Fmt.pr "Frame 1: %a\n"
+
(Termext.Termgraph.pp_animation_frame anim_id frame1) ();
+
Fmt.pr "Setting loop: %a\n"
+
(Termext.Termgraph.pp_animation_control anim_id (Termext.Termgraph.Animation.Set_loop 3)) ();
+
print_endline "";
+
+
(* Example 8: Deletion *)
+
print_endline "--- Example 8: Deleting images ---";
+
Unix.sleep 2; (* Show images for 2 seconds *)
+
+
(* Delete specific image *)
+
print_string (Termext.Termgraph.delete_by_id img_id);
+
+
(* Or delete all images *)
+
print_string (Termext.Termgraph.delete_all ());
+
print_endline "";
+
+
(* Example 9: Complex scene with z-index *)
+
print_endline "--- Example 9: Layered images ---";
+
(* Background layer *)
+
let bg = Termext.Termgraph.v
+
~image_id:(Termext.Termgraph.image_id_of_string "background")
+
~z_index:(-10)
+
~rows:20
+
~columns:40
+
(Termext.Termgraph.File { path = "background.png" })
+
()
+
in
+
print_string (Termext.Termgraph.render bg);
+
+
(* Foreground layer *)
+
let fg = Termext.Termgraph.v
+
~image_id:(Termext.Termgraph.image_id_of_string "foreground")
+
~z_index:10
+
~x:50
+
~y:50
+
~rows:10
+
~columns:20
+
(Termext.Termgraph.File { path = "sprite.png" })
+
()
+
in
+
print_string (Termext.Termgraph.render fg);
+
print_endline "\n";
+
+
(* Example 10: Combining text sizing and graphics *)
+
print_endline "--- Example 10: Mixed text and graphics ---";
+
print_string (Termext.Textsize.double "Large Title");
+
print_endline "\n";
+
+
let small_img = Termext.Termgraph.v
+
~rows:5
+
~columns:10
+
(Termext.Termgraph.File { path = "icon.png" })
+
()
+
in
+
Fmt.pr "Icon: %a Description text here\n"
+
(Termext.Termgraph.pp small_img) ();
+
+
print_string (Termext.Textsize.subscript "footnote");
+
print_endline "\n";
+
+
print_endline "=== Demo Complete ===";
+
print_endline "Note: This demo requires PNG files and a Kitty-compatible terminal.";
+4 -3
tgp/src/dune
···
(library
-
(name textsize)
-
(public_name textsize)
-
(libraries fmt))
+
(name termext)
+
(public_name termext)
+
(libraries fmt base64 zip)
+
(modules_without_implementation termgraph))
+229
tgp/src/termgraph.mli
···
+
(** OCaml implementation of the Kitty graphics protocol.
+
+
This library provides a clean API for generating escape sequences to display
+
images and animations in terminals that support the Kitty graphics protocol.
+
+
The protocol uses APC (Application Program Command) escape sequences to transmit
+
and display images with extensive control over placement, sizing, and composition.
+
+
See {{: https://sw.kovidgoyal.net/kitty/graphics-protocol/} Kitty Graphics Protocol}
+
for the complete specification.
+
*)
+
+
(** {1 Core Types} *)
+
+
(** Image format for raw pixel data. *)
+
type format =
+
| RGB (** 24-bit RGB (3 bytes per pixel) *)
+
| RGBA (** 32-bit RGBA (4 bytes per pixel) *)
+
| PNG (** PNG format *)
+
+
(** Compression method for image data. *)
+
type compression =
+
| No_compression
+
| Zlib
+
+
(** Transmission medium. *)
+
type transmission =
+
| Direct of {
+
format : format;
+
data : bytes;
+
width : int; (** Width in pixels *)
+
height : int; (** Height in pixels *)
+
compression : compression;
+
}
+
| File of {
+
path : string;
+
}
+
+
(** Image identifier (number or string up to 24 chars). *)
+
type image_id = private string
+
+
(** Placement identifier. *)
+
type placement_id = private int
+
+
(** {1 Image Identifiers} *)
+
+
(** [image_id_of_int n] creates an image ID from an integer.
+
@raise Invalid_argument if [n] is negative *)
+
val image_id_of_int : int -> image_id
+
+
(** [image_id_of_string s] creates an image ID from a string.
+
@raise Invalid_argument if [s] is empty or longer than 24 characters *)
+
val image_id_of_string : string -> image_id
+
+
(** [placement_id_of_int n] creates a placement ID from an integer.
+
@raise Invalid_argument if [n] is negative *)
+
val placement_id_of_int : int -> placement_id
+
+
(** {1 Placement Configuration} *)
+
+
(** Placement configuration for displaying images. *)
+
type placement
+
+
(** [v ?image_id ?placement_id ?x ?y ?width ?height ?rows ?columns ?z_index
+
?cursor_movement transmission ()] creates a placement configuration.
+
+
@param image_id Image identifier for reuse/deletion
+
@param placement_id Placement identifier for this specific display
+
@param x Left edge offset in pixels
+
@param y Top edge offset in pixels
+
@param width Width in pixels (scales image if different from source)
+
@param height Height in pixels (scales image if different from source)
+
@param rows Height in terminal cells
+
@param columns Width in terminal cells
+
@param z_index Z-order for layering (-2^31 to 2^31-1, default 0)
+
@param cursor_movement If false, cursor doesn't move after display (default true)
+
@param transmission The image data transmission method *)
+
val v :
+
?image_id:image_id ->
+
?placement_id:placement_id ->
+
?x:int ->
+
?y:int ->
+
?width:int ->
+
?height:int ->
+
?rows:int ->
+
?columns:int ->
+
?z_index:int ->
+
?cursor_movement:bool ->
+
transmission ->
+
unit ->
+
placement
+
+
(** {1 Rendering} *)
+
+
(** [render placement] generates the complete escape sequence(s) for the placement.
+
For large images that require chunking, this returns a single string with all chunks. *)
+
val render : placement -> string
+
+
(** [render_chunked placement] generates escape sequences as a list of chunks.
+
Useful for streaming large images or interleaving with other output. *)
+
val render_chunked : placement -> string list
+
+
(** {1 Deletion} *)
+
+
(** Delete images or placements. *)
+
module Delete : sig
+
(** Deletion target. *)
+
type target =
+
| By_id of image_id
+
| By_image_id of image_id
+
| By_placement_id of placement_id
+
| At_cursor
+
| All
+
+
(** [render target] generates the escape sequence to delete images. *)
+
val render : target -> string
+
end
+
+
(** {1 Animation} *)
+
+
(** Animation support for multi-frame images. *)
+
module Animation : sig
+
(** Frame composition mode. *)
+
type composition =
+
| Blend (** Alpha blend with previous frame *)
+
| Overwrite (** Replace previous frame *)
+
+
(** Animation frame configuration. *)
+
type frame
+
+
(** [frame ?composition ?gap transmission frame_number] creates a frame configuration.
+
+
@param composition How to compose this frame with previous frames
+
@param gap Gap before next frame in milliseconds (0-65535)
+
@param transmission The frame image data
+
@param frame_number Frame index (1-based) *)
+
val frame :
+
?composition:composition ->
+
?gap:int ->
+
transmission ->
+
int ->
+
frame
+
+
(** Animation control. *)
+
type control =
+
| Set_gap of {
+
frame_number : int; (** Which frame to modify *)
+
gap : int; (** New gap in milliseconds *)
+
}
+
| Set_loop of int (** Number of loops (0 = infinite) *)
+
| Stop (** Stop animation *)
+
| Run (** Resume animation *)
+
+
(** [render_frame image_id frame] generates escape sequence for transmitting an animation frame.
+
+
@param image_id The animation's image identifier *)
+
val render_frame : image_id -> frame -> string
+
+
(** [render_control image_id control] generates escape sequence for animation control. *)
+
val render_control : image_id -> control -> string
+
end
+
+
(** {1 Fmt-style Formatters} *)
+
+
(** [pp placement] creates a Fmt formatter that displays an image.
+
+
Example:
+
{[
+
let img = Graphics.v (Graphics.File { path = "image.png" }) () in
+
Fmt.pr "Here's an image: %a" Graphics.pp img
+
]}
+
*)
+
val pp : placement -> unit Fmt.t
+
+
(** [pp_delete target] creates a Fmt formatter that deletes images.
+
+
Example:
+
{[
+
Fmt.pr "Clearing screen: %a" Graphics.pp_delete Graphics.Delete.All
+
]}
+
*)
+
val pp_delete : Delete.target -> unit Fmt.t
+
+
(** [pp_animation_frame image_id frame] creates a Fmt formatter for animation frames. *)
+
val pp_animation_frame : image_id -> Animation.frame -> unit Fmt.t
+
+
(** [pp_animation_control image_id control] creates a Fmt formatter for animation control. *)
+
val pp_animation_control : image_id -> Animation.control -> unit Fmt.t
+
+
(** {1 Convenience Functions} *)
+
+
(** [display_png_file ?x ?y ?width ?height ?rows ?columns path] displays a PNG file.
+
+
@param path Path to PNG file
+
@return Escape sequence string *)
+
val display_png_file :
+
?x:int ->
+
?y:int ->
+
?width:int ->
+
?height:int ->
+
?rows:int ->
+
?columns:int ->
+
string ->
+
string
+
+
(** [display_png_bytes ?x ?y ?width ?height ?rows ?columns ~width:w ~height:h data]
+
displays PNG data from bytes.
+
+
@param width Image width in pixels
+
@param height Image height in pixels
+
@param data PNG-encoded image data
+
@return Escape sequence string *)
+
val display_png_bytes :
+
?x:int ->
+
?y:int ->
+
?width:int ->
+
?height:int ->
+
?rows:int ->
+
?columns:int ->
+
w:int ->
+
h:int ->
+
bytes ->
+
string
+
+
(** [delete_all ()] generates escape sequence to delete all images. *)
+
val delete_all : unit -> string
+
+
(** [delete_by_id id] generates escape sequence to delete image by ID. *)
+
val delete_by_id : image_id -> string
+34
tgp/termext.opam
···
+
# This file is generated by dune, edit dune-project instead
+
opam-version: "2.0"
+
version: "0.1.0"
+
synopsis: "OCaml implementation of Kitty terminal protocols"
+
description:
+
"A library for generating escape sequences for Kitty terminal protocols including text sizing and graphics display."
+
maintainer: ["Anonymous"]
+
authors: ["Anonymous"]
+
license: "MIT"
+
homepage: "https://github.com/username/termext"
+
bug-reports: "https://github.com/username/termext/issues"
+
depends: [
+
"ocaml" {>= "4.14"}
+
"fmt" {>= "0.9.0"}
+
"base64" {>= "3.5.0"}
+
"camlzip" {>= "1.11"}
+
"dune" {>= "3.0"}
+
"odoc" {with-doc}
+
]
+
build: [
+
["dune" "subst"] {dev}
+
[
+
"dune"
+
"build"
+
"-p"
+
name
+
"-j"
+
jobs
+
"@install"
+
"@runtest" {with-test}
+
"@doc" {with-doc}
+
]
+
]
+
dev-repo: "git+https://github.com/username/termext.git"
+1 -1
tgp/test/dune
···
(test
(name test_textsize)
-
(libraries textsize))
+
(libraries termext))
+17 -17
tgp/test/test_textsize.ml
···
(* Test scale validation *)
test "scale validation - valid range" (fun () ->
-
let _ = Textsize.v ~scale:1 () in
-
let _ = Textsize.v ~scale:7 () in
+
let _ = Termext.Textsize.v ~scale:1 () in
+
let _ = Termext.Textsize.v ~scale:7 () in
()
);
test "scale validation - rejects invalid" (fun () ->
try
-
let _ = Textsize.v ~scale:0 () in
+
let _ = Termext.Textsize.v ~scale:0 () in
failwith "Should have raised Invalid_argument"
with Invalid_argument _ -> ()
);
(* Test width validation *)
test "width validation - valid range" (fun () ->
-
let _ = Textsize.v ~width:0 () in
-
let _ = Textsize.v ~width:7 () in
+
let _ = Termext.Textsize.v ~width:0 () in
+
let _ = Termext.Textsize.v ~width:7 () in
()
);
(* Test escape sequence generation *)
test "double escape sequence" (fun () ->
-
let result = Textsize.double "test" in
+
let result = Termext.Textsize.double "test" in
assert (String.length result > 0);
assert (result = "\x1b]66;s=2;test\x07")
);
test "triple escape sequence" (fun () ->
-
let result = Textsize.triple "hello" in
+
let result = Termext.Textsize.triple "hello" in
assert (result = "\x1b]66;s=3;hello\x07")
);
test "half escape sequence" (fun () ->
-
let result = Textsize.half "tiny" in
+
let result = Termext.Textsize.half "tiny" in
assert (result = "\x1b]66;n=1:d=2;tiny\x07")
);
test "superscript escape sequence" (fun () ->
-
let result = Textsize.superscript "2" in
+
let result = Termext.Textsize.superscript "2" in
assert (result = "\x1b]66;n=1:d=2:v=2;2\x07")
);
test "subscript escape sequence" (fun () ->
-
let result = Textsize.subscript "2" in
+
let result = Termext.Textsize.subscript "2" in
assert (result = "\x1b]66;n=1:d=2:v=0;2\x07")
);
(* Test custom metadata *)
test "custom metadata - scale and width" (fun () ->
-
let metadata = Textsize.v ~scale:3 ~width:5 () in
-
let result = Textsize.render metadata "custom" in
+
let metadata = Termext.Textsize.v ~scale:3 ~width:5 () in
+
let result = Termext.Textsize.render metadata "custom" in
assert (result = "\x1b]66;s=3:w=5;custom\x07")
);
test "custom metadata - fractional" (fun () ->
-
let metadata = Textsize.v ~fraction:(2, 3) () in
-
let result = Textsize.render metadata "frac" in
+
let metadata = Termext.Textsize.v ~fraction:(2, 3) () in
+
let result = Termext.Textsize.render metadata "frac" in
assert (result = "\x1b]66;n=2:d=3;frac\x07")
);
test "empty metadata" (fun () ->
-
let result = Textsize.render (Textsize.v ()) "plain" in
+
let result = Termext.Textsize.render (Termext.Textsize.v ()) "plain" in
assert (result = "\x1b]66;;plain\x07")
);
(* Test text length validation *)
test "text length validation - accepts valid" (fun () ->
let text = String.make 4096 'a' in
-
let _ = Textsize.double text in
+
let _ = Termext.Textsize.double text in
()
);
test "text length validation - rejects oversized" (fun () ->
let text = String.make 4097 'a' in
try
-
let _ = Textsize.double text in
+
let _ = Termext.Textsize.double text in
failwith "Should have raised Invalid_argument"
with Invalid_argument _ -> ()
);
-31
tgp/textsize.opam
···
-
# This file is generated by dune, edit dune-project instead
-
opam-version: "2.0"
-
version: "0.1.0"
-
synopsis: "OCaml implementation of the Kitty text sizing protocol"
-
description:
-
"A clean, standalone library for generating escape sequences to render text in different sizes using the Kitty terminal's text sizing protocol."
-
maintainer: ["Anonymous"]
-
authors: ["Anonymous"]
-
license: "MIT"
-
homepage: "https://github.com/username/textsize"
-
bug-reports: "https://github.com/username/textsize/issues"
-
depends: [
-
"ocaml" {>= "4.14"}
-
"dune" {>= "3.0"}
-
"odoc" {with-doc}
-
]
-
build: [
-
["dune" "subst"] {dev}
-
[
-
"dune"
-
"build"
-
"-p"
-
name
-
"-j"
-
jobs
-
"@install"
-
"@runtest" {with-test}
-
"@doc" {with-doc}
-
]
-
]
-
dev-repo: "git+https://github.com/username/textsize.git"