Slint backend for jailbroken Kindles. Create rich GUI apps with Rust!
Rust
76
62 commits
updated Sep 21, 2026
Slint backend for jailbroken Kindles. Allows for running Slint GUIS on Kindle devices.
⚠️ Experimental crate: limited device support. This crate is experimental and has not been tested on a wide variety of Kindle devices. See "Tested devices" further down. Please file an issue (or a PR) if you try it on different hardware!
poll(2) when there's nothing to do, so the SoC can idle instead of burning cpu cycles.For suggestions on how to set up your dev environment, see the getting started doc.
Add the Slint crate and the backend to your app:
cargo add slint --no-default-features --features compat-1-2,std,renderer-software
cargo add slint-backend-kindle
Slint is added with --no-default-features and only compat-1-2, std, and renderer-software because the Kindle has no GPU — any hardware-renderer feature is meaningless and would pull in unwanted system dependencies. renderer-software specifically is required: it's the only renderer that can drive the Kindle framebuffer.
Bundle a TTF/OTF font with your app and pass it to install() at startup. The font is required. The various Kindle models has no fontconfig and no default location for system fonts, so Slint's software renderer would panic on the first fallback query without one.
slint::slint! {
export component AppWindow inherits Window {
Text {
text: "Hello from Slint on Kindle";
}
}
}
static FONT: &[u8] = include_bytes!("../fonts/LiberationSans-Regular.ttf");
fn main() {
slint_backend_kindle::install(FONT).expect("failed to install Kindle backend");
let app = AppWindow::new().expect("failed to create window");
app.run().expect("event loop error");
}
The UI is declared inline with the slint::slint! macro, so this example needs no .slint file or build.rs. For a "real" application you'd typically keep the markup in a .slint file and pull it in with slint::include_modules!() instead. See the Slint documentation for how to build with Slint.
The font becomes the default, so Slint widgets that don't specify font-family render correctly. You can still reference the font by its real family name in your .slint files (e.g. font-family: "Liberation Sans").
install() returns a KindleBackend handle. To use more than one typeface, register the extras on the handle after constructing the window:
static DEFAULT_FONT: &[u8] = include_bytes!("../fonts/LiberationSans-Regular.ttf");
static FANCY_FONT: &[u8] = include_bytes!("../fonts/DancingScript-Regular.ttf");
fn main() {
let backend = slint_backend_kindle::install(DEFAULT_FONT)
.expect("failed to install Kindle backend");
let app = AppWindow::new().expect("failed to create window");
backend.register_font_from_memory(FANCY_FONT)
.expect("failed to register font");
app.run().expect("event loop error");
}
Reference each font in .slint by its real family name (the one in the font's name table), not the filename. DancingScript-Regular.ttf for instance reports itself as "Dancing Script", so the .slint must say font-family: "Dancing Script". If a glyph fails to render, that mismatch is the first thing to check — fc-query font.ttf or otfinfo --info font.ttf will show the family string the font advertises.
By default the event loop blocks in poll(2) when idle, so the system idles but doesn't enter the deep suspend-to-RAM state that powerd would normally use. For prolonged stand-by applications that you want to still update the display periodically, opt in to a wake schedule so the device actually sleeps between updates and wakes on its own to refresh:
use std::time::Duration;
use slint_backend_kindle::WakeSchedule;
fn main() {
let backend = slint_backend_kindle::install(FONT)
.expect("failed to install Kindle backend");
let app = AppWindow::new().expect("failed to create window");
// After 30s of no touch, suspend to RAM. Wake every 5 minutes to refresh.
let backend = backend.set_wake_schedule(WakeSchedule {
wake_interval: Duration::from_secs(5 * 60),
stay_awake: Duration::from_secs(30),
});
// Optional: run something each time the device wakes, like polling an API to update your view or whatever.
backend.on_wake(|| {
refresh_data();
});
app.run().expect("event loop error");
}
Touch activity during the awake window resets stay_awake, exactly like the device's normal idle timer. The cycle suppresses itself while Slint animations or queued event-loop closures are pending, so it never interrupts active UI work.
set_wake_schedule consumes the backend and returns a KindleBackend<Scheduled>. on_wake is only available on that scheduled form. Call set_wake_schedule again on the scheduled backend to change the schedule at runtime, or clear_wake_schedule() to disable suspension entirely.
on_wake()slint::invoke_from_event_loop.wake_interval. If you specifically need work done at each wake, put it in on_wake, don't rely on a Timer::Repeated matching the schedule.An E-ink panel can flip a pixel between pure black and white quickly and effortlessly, but showing any grey might * need a waveform that briefly drives the pixels through black before settling, causing a flicker. So grey fills, fading animations etc flashes on every update, while pure black-and-white content updates more cleanly.
Pure black-and-white mode is available for applications where you want as little flickering as poissible. It thresholds every pixel to pure black or white (at the luma midpoint) before it reaches the framebuffer, so the panel only ever does its fast, flicker-free update:
fn main() {
let backend = slint_backend_kindle::install(FONT)
.expect("failed to install Kindle backend");
backend.set_black_and_white(true); // pure black/white, flash-free
let app = AppWindow::new().expect("failed to create window");
app.run().expect("event loop error");
}
Buttons, toggles, drawing, and anything that redraws on touch update instantly with no black flash, much closer to "instant" than a greyscale UI feels on E-ink.
The trade-off is no anti-aliasing: text edges and thin strokes get harder/blockier, and any light grey is pushed to white (so it disappears). If you want to use this mode you should design for it: Use solid black and white.
*That is, at least on the author's device. This might vary between models.
The Kindle runs an ARMv7 musl userland. Suggested toolchain:
rustup target add armv7-unknown-linux-musleabihf
cargo install cargo-zigbuild
brew install zig # or your platform's equivalent
cargo zigbuild --release --target armv7-unknown-linux-musleabihf
The resulting binary is statically linked against musl and runs directly on the device.
So far, the backend has been tested to work on:
wait_for_link_up() helper for on_wake callbacks that need wifiThe code in this crate is dual-licensed under either of
at your option.
Note that this crate depends on slint, which is licensed under GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0. Any application built using this backend and Slint must comply with one of Slint's licenses.
Rust
100.0%
Slint backend for jailbroken Kindles. Create rich GUI apps with Rust!
Rust
76
62 commits
updated Sep 21, 2026
Slint backend for jailbroken Kindles. Allows for running Slint GUIS on Kindle devices.
⚠️ Experimental crate: limited device support. This crate is experimental and has not been tested on a wide variety of Kindle devices. See "Tested devices" further down. Please file an issue (or a PR) if you try it on different hardware!
poll(2) when there's nothing to do, so the SoC can idle instead of burning cpu cycles.For suggestions on how to set up your dev environment, see the getting started doc.
Add the Slint crate and the backend to your app:
cargo add slint --no-default-features --features compat-1-2,std,renderer-software
cargo add slint-backend-kindle
Slint is added with --no-default-features and only compat-1-2, std, and renderer-software because the Kindle has no GPU — any hardware-renderer feature is meaningless and would pull in unwanted system dependencies. renderer-software specifically is required: it's the only renderer that can drive the Kindle framebuffer.
Bundle a TTF/OTF font with your app and pass it to install() at startup. The font is required. The various Kindle models has no fontconfig and no default location for system fonts, so Slint's software renderer would panic on the first fallback query without one.
slint::slint! {
export component AppWindow inherits Window {
Text {
text: "Hello from Slint on Kindle";
}
}
}
static FONT: &[u8] = include_bytes!("../fonts/LiberationSans-Regular.ttf");
fn main() {
slint_backend_kindle::install(FONT).expect("failed to install Kindle backend");
let app = AppWindow::new().expect("failed to create window");
app.run().expect("event loop error");
}
The UI is declared inline with the slint::slint! macro, so this example needs no .slint file or build.rs. For a "real" application you'd typically keep the markup in a .slint file and pull it in with slint::include_modules!() instead. See the Slint documentation for how to build with Slint.
The font becomes the default, so Slint widgets that don't specify font-family render correctly. You can still reference the font by its real family name in your .slint files (e.g. font-family: "Liberation Sans").
install() returns a KindleBackend handle. To use more than one typeface, register the extras on the handle after constructing the window:
static DEFAULT_FONT: &[u8] = include_bytes!("../fonts/LiberationSans-Regular.ttf");
static FANCY_FONT: &[u8] = include_bytes!("../fonts/DancingScript-Regular.ttf");
fn main() {
let backend = slint_backend_kindle::install(DEFAULT_FONT)
.expect("failed to install Kindle backend");
let app = AppWindow::new().expect("failed to create window");
backend.register_font_from_memory(FANCY_FONT)
.expect("failed to register font");
app.run().expect("event loop error");
}
Reference each font in .slint by its real family name (the one in the font's name table), not the filename. DancingScript-Regular.ttf for instance reports itself as "Dancing Script", so the .slint must say font-family: "Dancing Script". If a glyph fails to render, that mismatch is the first thing to check — fc-query font.ttf or otfinfo --info font.ttf will show the family string the font advertises.
By default the event loop blocks in poll(2) when idle, so the system idles but doesn't enter the deep suspend-to-RAM state that powerd would normally use. For prolonged stand-by applications that you want to still update the display periodically, opt in to a wake schedule so the device actually sleeps between updates and wakes on its own to refresh:
use std::time::Duration;
use slint_backend_kindle::WakeSchedule;
fn main() {
let backend = slint_backend_kindle::install(FONT)
.expect("failed to install Kindle backend");
let app = AppWindow::new().expect("failed to create window");
// After 30s of no touch, suspend to RAM. Wake every 5 minutes to refresh.
let backend = backend.set_wake_schedule(WakeSchedule {
wake_interval: Duration::from_secs(5 * 60),
stay_awake: Duration::from_secs(30),
});
// Optional: run something each time the device wakes, like polling an API to update your view or whatever.
backend.on_wake(|| {
refresh_data();
});
app.run().expect("event loop error");
}
Touch activity during the awake window resets stay_awake, exactly like the device's normal idle timer. The cycle suppresses itself while Slint animations or queued event-loop closures are pending, so it never interrupts active UI work.
set_wake_schedule consumes the backend and returns a KindleBackend<Scheduled>. on_wake is only available on that scheduled form. Call set_wake_schedule again on the scheduled backend to change the schedule at runtime, or clear_wake_schedule() to disable suspension entirely.
on_wake()slint::invoke_from_event_loop.wake_interval. If you specifically need work done at each wake, put it in on_wake, don't rely on a Timer::Repeated matching the schedule.An E-ink panel can flip a pixel between pure black and white quickly and effortlessly, but showing any grey might * need a waveform that briefly drives the pixels through black before settling, causing a flicker. So grey fills, fading animations etc flashes on every update, while pure black-and-white content updates more cleanly.
Pure black-and-white mode is available for applications where you want as little flickering as poissible. It thresholds every pixel to pure black or white (at the luma midpoint) before it reaches the framebuffer, so the panel only ever does its fast, flicker-free update:
fn main() {
let backend = slint_backend_kindle::install(FONT)
.expect("failed to install Kindle backend");
backend.set_black_and_white(true); // pure black/white, flash-free
let app = AppWindow::new().expect("failed to create window");
app.run().expect("event loop error");
}
Buttons, toggles, drawing, and anything that redraws on touch update instantly with no black flash, much closer to "instant" than a greyscale UI feels on E-ink.
The trade-off is no anti-aliasing: text edges and thin strokes get harder/blockier, and any light grey is pushed to white (so it disappears). If you want to use this mode you should design for it: Use solid black and white.
*That is, at least on the author's device. This might vary between models.
The Kindle runs an ARMv7 musl userland. Suggested toolchain:
rustup target add armv7-unknown-linux-musleabihf
cargo install cargo-zigbuild
brew install zig # or your platform's equivalent
cargo zigbuild --release --target armv7-unknown-linux-musleabihf
The resulting binary is statically linked against musl and runs directly on the device.
So far, the backend has been tested to work on:
wait_for_link_up() helper for on_wake callbacks that need wifiThe code in this crate is dual-licensed under either of
at your option.
Note that this crate depends on slint, which is licensed under GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0. Any application built using this backend and Slint must comply with one of Slint's licenses.
Rust
100.0%