Creating Custom Backends
Relevant source files
The following files were used as context for generating this wiki page:
The guiltty library is designed with a backend-agnostic architecture. While the primary implementation uses the Kitty graphics protocol, the system is extensible via a unified Backend trait. This allows developers to implement custom rendering targets that can consume the pixel-level data generated by the core library.
The core responsibility of a backend is to receive a Canvas and "present" its pixel buffer to a specific output medium, such as a terminal emulator, a windowing system, or a file.
Sources: crates/guiltty-core/src/lib.rs:9-12, README.md:14-18
The Backend Trait
The foundation of any custom backend is the Backend trait defined in guiltty-core. This trait defines how the library interacts with external rendering targets.
Trait Definition
pub trait Backend {
type Error: std::error::Error;
fn present(&mut self, canvas: &Canvas) -> Result<(), Self::Error>;
}
Sources: crates/guiltty-core/src/lib.rs:88-96
Key Components
| Component | Description |
|---|---|
type Error | An associated type that must implement std::error::Error. This allows backends to define specific error conditions (e.g., IO failures, protocol violations). |
present | The primary execution method. It receives a reference to the Canvas and is responsible for transmitting the pixel data to the target. |
Sources: crates/guiltty-core/src/lib.rs:90-95
Data Flow for Rendering
The rendering process involves converting the internal Canvas state into a format suitable for the backend. Backends typically consume the raw pixel data through the rgba8_bytes() method.
Pixel Buffer Extraction
The Canvas provides a flattened row-major RGBA8 byte vector (4 bytes per pixel: Red, Green, Blue, Alpha). Backends use this data to perform their specific wire-encoding or drawing operations.
Sources: crates/guiltty-core/src/lib.rs:163-169
Architecture Overview
The following diagram illustrates the relationship between the core Canvas, the Backend trait, and a specific implementation (e.g., KittyBackend).
Implementation Example: KittyBackend
The KittyBackend serves as the reference implementation for the Backend trait. It demonstrates how to handle complex requirements such as protocol encoding and data chunking.
Handling Canvas Data
In KittyBackend::present, the backend retrieves the byte buffer and performs Base64 encoding to satisfy the Kitty graphics protocol requirements.
fn present(&mut self, canvas: &Canvas) -> Result<(), Error> {
let rgba = canvas.rgba8_bytes();
let encoded = BASE64.encode(&rgba);
let chunks: Vec<&[u8]> = encoded.as_bytes().chunks(MAX_CHUNK_LEN).collect();
// ... transmission logic ...
}
Sources: crates/guiltty-kitty/src/lib.rs:62-65
Protocol Transmission Sequence
The backend manages the sequence of escape codes and data chunks sent to the terminal.
Best Practices for Custom Backends
- Injectable Writers: Use a generic writer (
W: Write) in the backend struct. This allows for easier testing by injecting aVec<u8>or similar buffer instead of forcing output tostdout. - Error Handling: Convert internal IO errors into the backend's associated
Errortype using a mapping function to provide meaningful context to the caller. - Zero-Size Handling: Check if the
Canvashas a zero width or height. Most protocols (including Kitty) cannot represent 0x0 images; the backend should returnOk(())or a specific error. - State Persistence: If the target requires unique identifiers for images (like Kitty's
i=<id>), manage these within the backend instance to avoid collisions between multiple backends.
Summary
Creating a custom backend for guiltty involves implementing the Backend trait and handling the transmission of RGBA8 pixel data. By abstracting the output logic away from the Canvas and drawing primitives, the library remains portable across different terminal protocols and graphical environments.
Sources: crates/guiltty-core/src/lib.rs:9-12, crates/guiltty/src/lib.rs:4-10