Kael
GPU-accelerated UI framework for native desktop apps in Rust.
Kael replaces Electron with a single Rust crate that gives you everything you need to build production desktop applications — IDEs, video editors, dashboards, design tools — with native GPU performance on macOS, Windows, and Linux.
What you get
| Layer | What Kael provides |
|---|---|
| Widgets | Button, TextInput, Checkbox, Toggle, RadioGroup, Slider, Select, DatePicker, Modal, Popover, Tabs, Disclosure, Progress, Toast, Splitter, and more |
| Layout | GPU-accelerated flexbox via Taffy, responsive sizing, scroll containers |
| Rendering | Metal (macOS), DirectX 11 (Windows), Vulkan (Linux) — 120fps, sRGB-correct, pixel-perfect |
| State | Reactive Entity<T> system with automatic re-rendering on change |
| Platform | File dialogs, system tray, native menus, global hotkeys, notifications, clipboard, printing, auto-updates, session persistence |
| Advanced | Plugin system (WASM sandboxed), multi-process IPC, accessibility, theming, gestures |
Quick start
Add to your Cargo.toml:
[dependencies]
kael = "0.1"
Write your first app:
use kael::*;
use kael::prelude::*;
struct Hello {
name: SharedString,
}
impl Render for Hello {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.size_full()
.flex()
.items_center()
.justify_center()
.bg(rgb(0x1E1E1E))
.text_xl()
.text_color(rgb(0xFFFFFF))
.child(format!("Hello, {}!", self.name))
}
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(400.0), px(300.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| cx.new(|_| Hello { name: "World".into() }),
).unwrap();
cx.activate(true);
});
}
Native rendering quality
Kael renders with the same sharpness as first-party platform apps:
- macOS: SF Pro Text/Display optical sizing (automatic swap at 20pt), sRGB Metal pipeline, continuous (squircle) corners matching SwiftUI, AppKit-matched font smoothing
- All platforms:
PixelSnapPolicyfor hairline strokes, device-pixel-aligned text baselines, and crisp fills at any DPI
Platform support
| Platform | Renderer | Status |
|---|---|---|
| macOS | Metal | Stable |
| Windows | DirectX 11 | Stable |
| Linux (X11) | Vulkan/Blade | Stable |
| Linux (Wayland) | Vulkan/Blade | Stable |
Acknowledgements
Kael is built on top of GPUI, the GPU-accelerated UI framework originally created by Zed Industries for the Zed code editor. We are grateful for their foundational work which made Kael possible.
Getting Started
Prerequisites
- Rust 1.85+ (edition 2024) — install via rustup
- Platform dependencies:
macOS: Xcode command line tools
xcode-select --install
Linux (Ubuntu/Debian):
sudo apt-get install -y \
libxkbcommon-dev libwayland-dev libxcb1-dev \
libvulkan-dev libfontconfig1-dev
Windows: Visual Studio Build Tools with C++ workload
Create a new project
cargo new my_app
cd my_app
Add Kael to Cargo.toml:
[dependencies]
kael = "0.1"
Your first window
Replace src/main.rs with:
use kael::*;
use kael::prelude::*;
struct Counter {
count: i32,
}
impl Render for Counter {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let entity = cx.entity();
div()
.size_full()
.flex()
.flex_col()
.items_center()
.justify_center()
.gap_4()
.bg(rgb(0x1E1E1E))
.text_color(rgb(0xFFFFFF))
.child(
div().text_3xl().child(format!("Count: {}", self.count))
)
.child(
div()
.flex()
.gap_2()
.child(
button("decrement")
.label("-1")
.on_click({
let entity = entity.clone();
move |_, _, cx| {
entity.update(cx, |this, cx| {
this.count -= 1;
cx.notify();
});
}
})
)
.child(
button("increment")
.label("+1")
.on_click({
let entity = entity.clone();
move |_, _, cx| {
entity.update(cx, |this, cx| {
this.count += 1;
cx.notify();
});
}
})
)
)
}
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(400.0), px(300.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| cx.new(|_| Counter { count: 0 }),
).unwrap();
cx.activate(true);
});
}
Run it:
cargo run
What just happened
Application::new().run()— boots the platform event loopcx.open_window()— creates a native window with GPU renderingcx.new(|_| Counter { count: 0 })— creates a reactiveEntity<Counter>impl Render for Counter— defines what the entity draws each framecx.notify()— tells the framework to re-render after state changes
Key patterns
Builder pattern for UI
Every element uses method chaining. No JSX, no templates — just Rust:
#![allow(unused)]
fn main() {
div()
.flex()
.flex_col()
.gap_4()
.p_4()
.bg(rgb(0x1E1E1E))
.text_color(rgb(0xFFFFFF))
.child("Hello")
}
Reactive state
State lives in your struct. Mutate it, call cx.notify(), and the framework re-renders:
#![allow(unused)]
fn main() {
entity.update(cx, |this, cx| {
this.count += 1;
cx.notify();
});
}
Custom rendering
Every widget accepts .render_with() for full visual control:
#![allow(unused)]
fn main() {
button("save")
.label("Save")
.render_with(|state, _window, _cx| {
div()
.px_4().py_2()
.rounded(px(8.0))
.bg(if state.focused { rgb(0x2563eb) } else { rgb(0x3b82f6) })
.text_color(rgb(0xffffff))
.child(state.label.unwrap_or_default())
.into_any_element()
})
}
Next steps
- Core Concepts — understand Entity, Context, and the render cycle
- Form Controls — buttons, inputs, checkboxes, sliders, and more
- Platform APIs — file dialogs, system tray, notifications
Core Concepts
Application lifecycle
Every Kael app follows this flow:
Application::new().run() → cx.open_window() → cx.new(|_| View) → render loop
fn main() {
Application::new().run(|cx: &mut App| {
cx.open_window(WindowOptions::default(), |window, cx| {
cx.new(|_| MyView { })
}).unwrap();
cx.activate(true);
});
}
Entity<T> — reactive state containers
An Entity<T> is a handle to a value stored in the framework’s arena. When the value changes and you call cx.notify(), any view rendering that entity re-renders automatically.
#![allow(unused)]
fn main() {
struct AppState {
user: String,
count: i32,
}
let state: Entity<AppState> = cx.new(|_cx| AppState {
user: "Alice".into(),
count: 0,
});
let name = state.read(cx).user.clone();
state.update(cx, |this, cx| {
this.count += 1;
cx.notify();
});
}
Entity vs. direct state
If your view struct holds state directly (like struct Counter { count: i32 }), the view IS the entity — cx.new() wraps it in Entity<Counter> automatically. Use separate entities when you need shared state across views:
#![allow(unused)]
fn main() {
struct Sidebar {
shared: Entity<AppState>,
}
struct Editor {
shared: Entity<AppState>,
}
}
The Render trait
Any type that implements Render can be displayed in a window:
#![allow(unused)]
fn main() {
impl Render for MyView {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div().child("Hello")
}
}
}
Parameters:
&mut self— mutable access to your statewindow: &mut Window— the window being rendered into (for window-level APIs)cx: &mut Context<Self>— entity-scoped context for creating entities, subscribing to events, and notifying changes
Return: Anything implementing IntoElement — a Div, a Button, or any widget.
Context types
| Context | Where you get it | What it does |
|---|---|---|
App | Application::new().run(|cx| { ... }) | Root context — open windows, set globals |
Context<T> | impl Render and cx.new() closures | Entity-scoped — notify, observe, subscribe |
Window | impl Render render method | Window-level — bounds, focus, painting |
Getting an entity handle inside render
#![allow(unused)]
fn main() {
impl Render for MyView {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let entity = cx.entity();
button("click-me")
.label("Click")
.on_click(move |_, _, cx| {
entity.update(cx, |this, cx| {
this.handle_click();
cx.notify();
});
})
}
}
}
Global state
For app-wide values (theme, user session, config), use the Global trait:
#![allow(unused)]
fn main() {
struct AppConfig {
dark_mode: bool,
font_size: f32,
}
impl Global for AppConfig {}
cx.set_global(AppConfig { dark_mode: true, font_size: 14.0 });
cx.read_global::<AppConfig, _>(|config, _| {
config.dark_mode
});
cx.update_global::<AppConfig, _>(|config, cx| {
config.dark_mode = false;
});
}
Element composition
Views compose by nesting elements with .child():
#![allow(unused)]
fn main() {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.flex()
.flex_col()
.child(self.render_header())
.child(self.render_content())
.child(self.render_footer())
}
fn render_header(&self) -> impl IntoElement {
div().h(px(48.0)).bg(rgb(0x2563eb)).child("Header")
}
}
Conditional rendering
Use .when() for conditional styling or .map() for conditional children:
#![allow(unused)]
fn main() {
div()
.when(self.is_active, |div| div.bg(rgb(0x2563eb)))
.when(!self.is_active, |div| div.bg(rgb(0x64748b)))
.child(if self.show_label { "Active" } else { "Inactive" })
}
Iterating children
Use .children() with an iterator:
#![allow(unused)]
fn main() {
div()
.flex()
.flex_col()
.children(self.items.iter().map(|item| {
div().px_2().py_1().child(item.name.clone())
}))
}
Event handling
All events pass (event_data, &mut Window, &mut App):
#![allow(unused)]
fn main() {
div()
.id("my-element")
.on_click(|event, window, cx| {
})
.on_mouse_down(MouseButton::Left, |event, window, cx| {
})
.on_key_down(|event, window, cx| {
})
}
Widget events use the same pattern:
#![allow(unused)]
fn main() {
text_input("name", self.name.clone())
.on_change(|new_value, window, cx| {
})
.on_submit(|value, window, cx| {
})
}
Subscriptions and observations
Watch for changes on other entities:
#![allow(unused)]
fn main() {
cx.observe(&other_entity, |this, other, cx| {
cx.notify();
});
cx.subscribe(&other_entity, |this, _other, event: &MyEvent, cx| {
});
}
Layout & Styling
Kael uses GPU-accelerated flexbox (powered by Taffy) with a Tailwind-inspired API. Every style is a method call on a Div.
Flexbox layout
#![allow(unused)]
fn main() {
div().flex().flex_row().gap_2()
.child(div().child("Left"))
.child(div().child("Right"))
div().flex().flex_col().gap_4()
.child(div().child("Top"))
.child(div().child("Bottom"))
}
Alignment
#![allow(unused)]
fn main() {
div().flex()
.items_center()
.justify_center()
.justify_between()
.items_start()
.items_end()
}
Flex sizing
#![allow(unused)]
fn main() {
div().flex_1()
div().flex_grow()
div().flex_shrink_0()
div().flex_none()
}
Grid layout
Switch a container to CSS Grid with .grid(), define tracks with .grid_cols(n) / .grid_rows(n), and place children with .col_span(n) / .row_span(n) (or .col_span_full() / .row_span_full()). .gap_*() sets the gutters:
#![allow(unused)]
fn main() {
div()
.grid()
.grid_cols(5)
.grid_rows(5)
.gap_1()
.child(div().row_span(1).col_span_full().child("Header"))
.child(div().col_span(1).row_span(3).child("Sidebar"))
.child(div().col_span(3).row_span(3).child("Content"))
.child(div().col_span(1).row_span(3).child("Aside"))
.child(div().row_span(1).col_span_full().child("Footer"))
}
See examples/grid_layout.rs for a full “holy grail” layout.
Sizing
#![allow(unused)]
fn main() {
div().w(px(200.0)).h(px(100.0))
div().w_full()
div().h_full()
div().size_full()
div().size_8()
div().w_12()
div().h_6()
div().min_w(px(200.0)).max_w(px(600.0))
}
Spacing
#![allow(unused)]
fn main() {
div().p_4()
div().px_3()
div().py_2()
div().pt_1()
div().pl(px(20.0))
div().m_4()
div().mx_auto()
div().mt_2()
div().flex().gap_2()
div().flex().gap_4()
}
Colors
#![allow(unused)]
fn main() {
div().bg(rgb(0x1E1E1E))
div().text_color(rgb(0xFFFFFF))
div().border_color(rgb(0x3C3C3C))
div().bg(rgba(0x00000080))
div().bg(kael::red())
div().bg(kael::blue())
div().bg(kael::white())
div().bg(kael::black())
use kael::hsla;
div().bg(hsla(210.0 / 360.0, 1.0, 0.5, 1.0))
}
Borders
#![allow(unused)]
fn main() {
div().border_1()
div().border_2()
div().border_t_1()
div().border_b_1()
div().border_l_1()
div().border_r_1()
div().border_color(rgb(0x3C3C3C))
div().border_dashed()
}
Corners
#![allow(unused)]
fn main() {
div().rounded_sm()
div().rounded_md()
div().rounded_lg()
div().rounded_full()
div().rounded(px(8.0))
}
By default, rounded corners use continuous (squircle) rounding to match SwiftUI’s
RoundedRectangle shape on macOS. Use .circular_corners() to opt into the
legacy pure quarter-circle look:
#![allow(unused)]
fn main() {
div().rounded(px(8.0)).circular_corners()
}
Shadows
#![allow(unused)]
fn main() {
div().shadow_sm()
div().shadow_md()
div().shadow_lg()
div().shadow_xl()
}
Typography
#![allow(unused)]
fn main() {
div()
.text_xs()
.text_sm()
.text_base()
.text_lg()
.text_xl()
.text_2xl()
.text_3xl()
div().font_weight(FontWeight::BOLD)
div().font_family(".SystemUIFont")
}
Overflow and scrolling
Control how content behaves when it exceeds the element bounds:
#![allow(unused)]
fn main() {
div().overflow_hidden()
div().overflow_x_scroll()
div().overflow_y_scroll()
div().overflow_y_auto()
.id("scroll-container")
}
When using overflow_y_scroll() or overflow_y_auto() with a ScrollHandle,
Kael automatically renders a macOS-style scrollbar thumb when content overflows.
No extra widget is needed:
#![allow(unused)]
fn main() {
let scroll_handle = ScrollHandle::new();
div()
.id("my-scrollable")
.overflow_y_scroll()
.track_scroll(&scroll_handle)
.child(long_content)
}
The auto-scrollbar appears only when content exceeds the viewport and tracks the scroll position automatically. To keep scrollbars visible at all times (instead of auto-hiding after idle):
#![allow(unused)]
fn main() {
let scroll_handle = ScrollHandle::new().always_show_scrollbars();
}
For custom scrollbar styling, use the explicit scroll_bar() widget instead
(see Lists & Data).
Positioning
#![allow(unused)]
fn main() {
div().relative()
.child(
div().absolute()
.top(px(10.0))
.right(px(10.0))
.child("Badge")
)
}
Opacity
#![allow(unused)]
fn main() {
div().opacity(0.5)
}
Cursor
#![allow(unused)]
fn main() {
div().cursor_pointer()
div().cursor_default()
}
Conditional styling with .when()
#![allow(unused)]
fn main() {
div()
.when(self.is_selected, |this| {
this.bg(rgb(0x2563eb)).text_color(rgb(0xffffff))
})
.when(!self.is_selected, |this| {
this.bg(rgb(0xffffff)).text_color(rgb(0x000000))
})
}
Animations
Kael drives animations from its render-on-demand loop: an animating element requests frames only while it is in flight, then the window returns to idle (0% CPU). There are two layers — explicit, time-driven animations you attach to any element, and the framework’s built-in motion such as elastic scrolling.
Animating an element
Bring the AnimationExt trait into scope and call with_animation on any element. You give it a stable id, an Animation describing the timeline, and an animator closure that receives the element and the eased progress delta in 0.0..=1.0:
#![allow(unused)]
fn main() {
use std::time::Duration;
use kael::{Animation, AnimationExt as _, Transformation, bounce, ease_in_out, percentage, svg};
svg()
.size_20()
.path(ARROW_CIRCLE_SVG)
.with_animation(
"spinner",
Animation::new(Duration::from_secs(2))
.repeat_forever()
.with_easing(bounce(ease_in_out)),
|svg, delta| svg.with_transformation(Transformation::rotate(percentage(delta))),
)
}
The Animation timeline
#![allow(unused)]
fn main() {
use kael::{Animation, Easing, Repeat};
Animation::new(Duration::from_millis(400))
.delay(Duration::from_millis(100)) // wait before starting
.easing(Easing::EaseInOut) // pick a curve (see below)
.repeat(Repeat::Count(3)); // Once | Count(n) | Forever
}
repeat_forever() is shorthand for repeat(Repeat::Forever), and with_easing(f) accepts any Fn(f32) -> f32 (including the helpers ease_in_out, ease_out_quint(), and bounce(inner)).
Easing curves
The Easing enum covers the common curves plus a physical spring:
| Variant | Curve |
|---|---|
Easing::Linear | constant rate |
Easing::EaseIn / EaseOut / EaseInOut | quadratic |
Easing::CubicBezier(x1, y1, x2, y2) | CSS-style cubic Bézier |
Easing::Spring { stiffness, damping, mass } | damped spring |
Easing::Custom(Rc<dyn Fn(f32) -> f32>) | your own |
#![allow(unused)]
fn main() {
Animation::new(Duration::from_millis(600))
.easing(Easing::Spring { stiffness: 180.0, damping: 12.0, mass: 1.0 });
}
Keyframes and sequences
For multi-stop transitions across common styled properties, build a Keyframes set and attach it with with_keyframes:
#![allow(unused)]
fn main() {
use kael::{Animation, AnimationExt as _, Keyframes};
div().with_keyframes(
"pulse",
Keyframes::new()
.at(0.0, |k| k.opacity(0.4))
.at(0.5, |k| k.opacity(1.0))
.at(1.0, |k| k.opacity(0.4)),
Animation::new(Duration::from_secs(1)).repeat_forever(),
)
}
Chain whole animations with AnimationSequence::new().then(...).then_for(duration).with_overlap(...) and drive them with with_animation_sequence. For animations you may need to interrupt, with_cancellable_animation returns an (element, AnimationHandle); call handle.cancel() to jump to the final state.
Elastic scrolling
Scrollable regions — overflow_*_scroll() containers, uniform_list, and list (via ListState) — get native rubber-band overscroll automatically on macOS: content stretches past its bounds on a trackpad pull and springs back on release. Use a ScrollHandle to read or set the offset programmatically:
#![allow(unused)]
fn main() {
use kael::{ScrollHandle, point, px};
let scroll = ScrollHandle::new();
scroll.set_offset(point(px(-360.0), px(0.0)));
let current = scroll.offset(); // Point<Pixels>
let max = scroll.max_offset(); // Size<Pixels>
}
See examples/animation.rs and examples/elastic_scrolling.rs for runnable demos.
Theming
Kael’s theme system provides JSON/TOML-based theming with hot-reload support. The Theme type implements Global, making it available anywhere in your app.
Built-in themes
#![allow(unused)]
fn main() {
// Initialize theme system
Theme::init(cx);
// Switch themes
cx.set_global(Theme::dark());
cx.set_global(Theme::light());
// Match system appearance
cx.set_global(Theme::for_appearance(window));
}
Theme from JSON
{
"name": "Ocean",
"appearance": "dark",
"background": "#0a1628",
"foreground": "#e2e8f0",
"primary": "#3b82f6",
"secondary": "#64748b",
"accent": "#06b6d4",
"error": "#ef4444",
"warning": "#f59e0b",
"success": "#22c55e",
"border": "#1e293b",
"surface": "#0f172a",
"muted": "#334155"
}
Load it:
#![allow(unused)]
fn main() {
let theme = Theme::from_json_str(json_str)?;
cx.set_global(theme);
}
Theme from TOML
name = "Forest"
appearance = "dark"
background = "#1a2e1a"
foreground = "#d4e6d4"
primary = "#22c55e"
#![allow(unused)]
fn main() {
let theme = Theme::from_toml_str(toml_str)?;
cx.set_global(theme);
}
Loading from file
#![allow(unused)]
fn main() {
let theme = Theme::from_path("themes/custom.json")?;
cx.set_global(theme);
}
Hot-reload
Automatically reload theme when the file changes:
#![allow(unused)]
fn main() {
use kael::ThemeRuntime;
ThemeRuntime::watch("themes/active.json", cx);
// Theme reloads automatically when the file is saved
}
Using theme colors in views
#![allow(unused)]
fn main() {
impl Render for MyView {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme = cx.global::<Theme>();
div()
.bg(theme.background)
.text_color(theme.foreground)
.border_color(theme.border)
.child(
div()
.bg(theme.primary)
.text_color(rgb(0xffffff))
.px_4().py_2()
.rounded(px(6.0))
.child("Primary button")
)
}
}
}
Theme properties
| Property | Description |
|---|---|
name | Theme name |
appearance | "light" or "dark" |
background | Main background color |
foreground | Main text color |
primary | Primary action color |
secondary | Secondary/muted action color |
accent | Highlight/accent color |
error | Error state color |
warning | Warning state color |
success | Success state color |
border | Default border color |
surface | Elevated surface color |
muted | Subdued text/element color |
Accessibility
Kael’s built-in widgets are accessible by default — every form control reports its role, state, and value to screen readers and supports full keyboard navigation.
Built-in accessibility
All form controls automatically provide:
- Roles: Button reports as button, checkbox as checkbox, etc.
- States: Focused, disabled, checked, selected, expanded
- Values: Slider reports its numeric value, progress reports percentage
- Labels: Set via
.label()builder method - Keyboard navigation: Tab between controls, Space/Enter to activate
You get this for free when using the built-in widgets.
Adding accessibility to custom elements
For custom div-based interactive elements, add accessibility attributes:
#![allow(unused)]
fn main() {
div()
.id("custom-toggle")
.role(AccessibilityRole::Switch)
.aria_checked(self.is_on)
.aria_label("Enable dark mode")
.on_click(|_, _, cx| { /* toggle */ })
}
Keyboard navigation
Focus management
#![allow(unused)]
fn main() {
// Create a focus handle
let focus = cx.focus_handle();
div()
.id("panel")
.track_focus(&focus)
.on_key_down(|event, window, cx| {
match event.keystroke.key.as_str() {
"enter" => { /* activate */ },
"escape" => { /* cancel */ },
_ => {}
}
})
}
Tab stops
Controls with IDs are automatically tab-focusable. Custom tab order:
#![allow(unused)]
fn main() {
div()
.id("first-field")
.tab_index(1)
div()
.id("second-field")
.tab_index(2)
}
Label association
Use the label element to associate labels with controls:
#![allow(unused)]
fn main() {
label("Email address", "email-input")
// Clicking the label focuses the associated input
text_input("email-input", self.email.clone())
}
Screen reader announcements
#![allow(unused)]
fn main() {
// Announce to screen readers
window.announce("File saved successfully");
}
Accessibility roles
| Role | Used by |
|---|---|
Button | button() |
Checkbox | checkbox() |
Radio | radio_group() options |
Slider | slider() |
TextInput | text_input() |
Switch | toggle() |
Dialog | modal() |
Tab | tabs() |
TabPanel | tabs() panel content |
ProgressBar | progress() |
Menu | context menus |
MenuItem | menu items |
Tree | tree views |
TreeItem | tree items |
Form Controls
Every form control follows the same pattern:
- Create with
widget_name(id, value, ...) - Chain builder methods for configuration
- Add
.on_change()for state updates - Optionally add
.render_with()for custom visuals
All controls support keyboard navigation and accessibility out of the box.
Button
A focusable, clickable element with label support.
#![allow(unused)]
fn main() {
use kael::button;
button("save-btn")
.label("Save File")
.on_click({
let entity = entity.clone();
move |_event, _window, cx| {
entity.update(cx, |this, cx| {
this.save();
cx.notify();
});
}
})
}
Builder methods:
| Method | Description |
|---|---|
.label(text) | Display text |
.disabled() | Disable interaction |
.on_click(handler) | Click handler (|event, window, cx| { ... }) |
.render_with(renderer) | Custom rendering with ButtonRenderState |
ButtonRenderState fields: label: Option<SharedString>, focused: bool, disabled: bool
TextInput
Full-featured text field with selection, clipboard, undo/redo, and password masking.
#![allow(unused)]
fn main() {
use kael::text_input;
text_input("project_name", self.name.clone())
.placeholder("Enter project name")
.on_change({
let entity = entity.clone();
move |value, _window, cx| {
entity.update(cx, |this, cx| {
this.name = value;
cx.notify();
});
}
})
}
Builder methods:
| Method | Description |
|---|---|
.placeholder(text) | Placeholder text when empty |
.multi_line() | Enable multiline editing |
.max_lines(n) | Limit visible height |
.password() | Mask input characters |
.mask(impl InputMask) | Custom input normalization |
.on_change(handler) | Text change handler (|value: SharedString, window, cx|) |
.on_submit(handler) | Enter key handler (|value: SharedString, window, cx|) |
.render_with(renderer) | Custom rendering with TextInputRenderState |
TextInputRenderState fields: value, display_text, placeholder, showing_placeholder, focused, hovered, multi_line, outer_bounds, field_bounds, text_bounds, line_height, lines, selection_bounds, cursor_bounds
Custom rendering helpers on state: state.paint_selection(color, window), state.paint_text(window, cx), state.paint_cursor(color, window)
Checkbox
Three-state checkbox (checked, unchecked, indeterminate) with undo/redo.
#![allow(unused)]
fn main() {
use kael::checkbox;
checkbox("notifications", self.enabled)
.label("Enable notifications")
.on_change({
let entity = entity.clone();
move |checked, _window, cx| {
entity.update(cx, |this, cx| {
this.enabled = *checked;
cx.notify();
});
}
})
}
Builder methods:
| Method | Description |
|---|---|
.label(text) | Label text |
.indeterminate(bool) | Show indeterminate state |
.disabled() | Disable interaction |
.on_change(handler) | State change (|&bool, window, cx|) |
.render_with(renderer) | Custom rendering with CheckboxRenderState |
CheckboxRenderState fields: checked, indeterminate, label, focused, disabled
Toggle
Boolean on/off switch with undo/redo.
#![allow(unused)]
fn main() {
use kael::toggle;
toggle("dark_mode", self.dark_mode)
.label("Dark mode")
.on_change({
let entity = entity.clone();
move |on, _window, cx| {
entity.update(cx, |this, cx| {
this.dark_mode = *on;
cx.notify();
});
}
})
}
Builder methods:
| Method | Description |
|---|---|
.label(text) | Label text |
.disabled() | Disable interaction |
.on_change(handler) | State change (|&bool, window, cx|) |
.render_with(renderer) | Custom rendering with ToggleRenderState |
ToggleRenderState fields: on, label, focused, disabled
RadioGroup
Mutually exclusive option selection with generic value types.
#![allow(unused)]
fn main() {
use kael::radio_group;
#[derive(Clone, Copy, PartialEq, Eq)]
enum Theme { Light, Dark, System }
radio_group("theme", self.theme, [
(Theme::Light, "Light"),
(Theme::Dark, "Dark"),
(Theme::System, "System"),
])
.on_change({
let entity = entity.clone();
move |value, _window, cx| {
entity.update(cx, |this, cx| {
this.theme = *value;
cx.notify();
});
}
})
}
Builder methods:
| Method | Description |
|---|---|
.on_change(handler) | Selection change (|&T, window, cx|) |
.render_with(renderer) | Custom rendering per option with RadioRenderState |
RadioRenderState fields: value, label, index, selected, focused
Slider
Continuous or discrete value control with drag support.
#![allow(unused)]
fn main() {
use kael::slider;
slider("volume", self.volume)
.min(0.0)
.max(100.0)
.step(5.0)
.on_change({
let entity = entity.clone();
move |value, _window, cx| {
entity.update(cx, |this, cx| {
this.volume = *value;
cx.notify();
});
}
})
}
Builder methods:
| Method | Description |
|---|---|
.min(f64) | Minimum value (default: 0.0) |
.max(f64) | Maximum value (default: 100.0) |
.step(f64) | Keyboard increment (default: 1.0) |
.discrete() | Snap to step values |
.vertical() | Vertical orientation |
.disabled() | Disable interaction |
.on_change(handler) | Value change (|&f64, window, cx|) |
.render_with(renderer) | Custom rendering with SliderRenderState |
SliderRenderState fields: value, min, max, percentage, dragging, focused, disabled
Select
Dropdown with popup menu, optional search, and generic value types.
#![allow(unused)]
fn main() {
use kael::select;
select("accent", self.accent, [
(AccentColor::Blue, "Atlantic"),
(AccentColor::Green, "Forest"),
(AccentColor::Orange, "Ember"),
])
.placeholder("Choose an accent")
.searchable()
.on_change({
let entity = entity.clone();
move |value, _window, cx| {
entity.update(cx, |this, cx| {
this.accent = *value;
cx.notify();
});
}
})
}
Builder methods:
| Method | Description |
|---|---|
.placeholder(text) | Placeholder when nothing selected |
.searchable() | Enable type-to-filter in popup |
.on_change(handler) | Selection change (|&T, window, cx|) |
.render_with(renderer) | Custom trigger rendering with SelectRenderState |
.render_options_with(renderer) | Custom option row rendering with SelectOptionRenderState<T> |
.render_popup_with(renderer) | Custom popup shell with SelectPopupRenderState |
.render_search_with(renderer) | Custom search field with SelectSearchRenderState |
SelectRenderState fields: open, display_text, selected_label, placeholder, showing_placeholder, focused
DatePicker
Calendar-based date selection with month/year navigation.
#![allow(unused)]
fn main() {
use kael::date_picker;
use time::Date;
date_picker("delivery", self.delivery_date)
.on_change({
let entity = entity.clone();
move |date, _window, cx| {
entity.update(cx, |this, cx| {
this.delivery_date = *date;
cx.notify();
});
}
})
}
Builder methods:
| Method | Description |
|---|---|
.on_change(handler) | Date selection (|&Date, window, cx|) |
.render_with(renderer) | Custom trigger rendering |
.render_days_with(renderer) | Custom day cell rendering with DateCellRenderState |
DateCellRenderState fields: day, selected, highlighted, disabled, today
Display & Feedback
Elements for showing information and providing feedback to users.
Text
Basic text rendering. Strings passed to .child() automatically become text elements:
#![allow(unused)]
fn main() {
div().child("Hello, world!")
div().text_xl().text_color(rgb(0x2563eb)).child("Title")
}
For styled inline text, use SharedString:
#![allow(unused)]
fn main() {
use kael::SharedString;
let label: SharedString = "Click me".into();
div().child(label)
}
Label
Accessible label that forwards focus to a target control:
#![allow(unused)]
fn main() {
use kael::label;
label("Email address", "email-input")
// Clicking the label focuses the text_input with id "email-input"
}
Icon
Render named icons from the icon set:
#![allow(unused)]
fn main() {
use kael::icon;
icon("folder")
icon("file").size(px(16.0))
}
Image
Display raster images with caching:
#![allow(unused)]
fn main() {
use kael::{img, ImageSource};
img(ImageSource::from_path("photo.png"))
.w(px(200.0))
.h(px(150.0))
.rounded_md()
}
SVG
Render SVG content:
#![allow(unused)]
fn main() {
use kael::svg;
svg()
.path("icons/logo.svg")
.w(px(24.0))
.h(px(24.0))
.text_color(rgb(0x2563eb)) // fills SVG with color
}
RichText
Compose text from inline styled spans, clickable entities (links, mentions, hashtags), inline code, and embedded elements:
#![allow(unused)]
fn main() {
use kael::{rich_text, FontWeight, HighlightStyle, rgb};
rich_text()
.text("The ")
.styled("quick brown fox", HighlightStyle {
color: Some(rgb(0xb45309).into()),
font_weight: Some(FontWeight::BOLD),
..Default::default()
})
.text(" jumps. See the ")
.link("docs", "https://augani.github.io/kael/", |_window, _app| {})
.text(" or ping ")
.mention("@team", "team-id", |_window, _app| {})
.text(". Run ")
.code("cargo run")
.selectable(true)
}
Builder methods: .text(), .styled(text, HighlightStyle), .link(text, target, on_click), .mention(text, payload, on_click), .hashtag(text, payload, on_click), .code(text), .inline_element(element), .selectable(bool), .selection_color(color). Entity click handlers have the signature Fn(&mut Window, &mut App).
Progress
Determinate or indeterminate progress indicator:
#![allow(unused)]
fn main() {
use kael::progress;
// Determinate (0.0 to 1.0)
progress("export", 0.65)
// Indeterminate
progress("loading", 0.0).indeterminate()
// Custom rendering
progress("download", self.progress)
.render_with(|state, bounds, window, _cx| {
// state.percentage: Option<f64>
// state.indeterminate: bool
// Paint track and fill bar using window.paint_quad()
window.paint_quad(fill(bounds, rgb(0xe2e8f0)).corner_radii(px(4.0)));
if let Some(pct) = state.percentage {
let width = bounds.size.width * pct as f32;
window.paint_quad(fill(
Bounds::new(bounds.origin, size(width, bounds.size.height)),
rgb(0x2563eb),
).corner_radii(px(4.0)));
}
})
}
ProgressRenderState fields: value, max, percentage, indeterminate
Toast
Auto-dismissing notification overlay:
#![allow(unused)]
fn main() {
use kael::{Toast, ToastStack};
// In your view, create a ToastStack entity
struct MyApp {
toasts: Entity<ToastStack>,
}
// Create it
let toasts = cx.new(|_| ToastStack::new());
// Push a toast from anywhere with the entity handle
toasts.update(cx, |stack, cx| {
stack.push(
Toast::new("File saved")
.body("changes written to disk")
.duration(Duration::from_secs(3)),
window,
cx,
);
});
// Render the stack in your view
impl Render for MyApp {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.size_full()
.child(/* your content */)
.child(self.toasts.clone()) // ToastStack renders as overlay
}
}
}
Toast positions: ToastPosition::TopRight, TopCenter, BottomRight
Canvas
GPU-accelerated custom drawing surface:
#![allow(unused)]
fn main() {
use kael::canvas;
canvas(|bounds, window, cx| {
// Custom painting with window.paint_quad(), window.paint_path(), etc.
window.paint_quad(fill(bounds, rgb(0x1E1E1E)));
})
.w(px(400.0))
.h(px(300.0))
}
Canvas & Graphics
Beyond the element tree, Kael gives you direct GPU drawing: an immediate-mode canvas, a vector path builder, gradients, backdrop blur, SVG, and Lottie playback. Everything renders through the same per-platform pipeline (Metal / DirectX 11 / Vulkan) with device-pixel snapping for crisp output at any DPI.
Canvas
canvas takes two closures — a prepaint pass (compute layout/state, returns a value) and a paint pass (draw into the bounds). Inside paint you call window.paint_quad and window.paint_path:
#![allow(unused)]
fn main() {
use kael::{canvas, fill, quad, px, rgb, Bounds, Pixels, Window, App};
canvas(
move |_bounds: Bounds<Pixels>, _window: &mut Window, _app: &mut App| {
// prepaint: return any state the paint pass needs
},
move |bounds: Bounds<Pixels>, _state, window: &mut Window, _app: &mut App| {
window.paint_quad(fill(bounds, rgb(0x1e1e1e)));
// window.paint_path(path, color);
},
)
.size_full()
}
Vector paths
Build filled or stroked paths with PathBuilder, then hand the result to window.paint_path:
#![allow(unused)]
fn main() {
use kael::{PathBuilder, point, px};
let mut builder = PathBuilder::fill(); // or PathBuilder::stroke(px(2.0))
builder.move_to(point(px(50.0), px(50.0)));
builder.line_to(point(px(130.0), px(50.0)));
builder.curve_to(point(px(130.0), px(130.0)), point(px(160.0), px(90.0))); // quadratic
builder.close();
let path = builder.build()?;
}
Segment methods: move_to, line_to, curve_to (quadratic Bézier), cubic_curve_to, arc_to, and close. Stroked builders also accept dash_array / dash_offset.
Gradients
Gradients are backgrounds you pass to .bg(...):
#![allow(unused)]
fn main() {
use kael::{linear_gradient, linear_color_stop, rgb};
div().bg(linear_gradient(
45.0,
linear_color_stop(rgb(0xff0080), 0.0),
linear_color_stop(rgb(0x7928ca), 1.0),
))
}
Also available: multi_stop_linear_gradient(angle, &[stops]), radial_gradient(cx, cy, radius, &[stops]), and conic_gradient(cx, cy, angle_offset, &[stops]).
Backdrop blur & frosted glass
backdrop_blur blurs whatever is painted behind an element — combine it with a translucent background for a frosted-glass panel:
#![allow(unused)]
fn main() {
use kael::{px, rgba};
div()
.backdrop_blur(px(20.0))
.bg(rgba(0xffffff20))
.rounded_xl()
}
SVG
svg() renders a vector asset; text_color fills monochrome SVGs and with_transformation applies rotation/scale:
#![allow(unused)]
fn main() {
use kael::{svg, px, rgb};
svg().path("icons/logo.svg").size(px(24.0)).text_color(rgb(0x2563eb))
}
Lottie
lottie() plays Lottie/dotLottie animations, decoding frames on a background thread so the UI stays responsive:
#![allow(unused)]
fn main() {
use kael::{lottie, LoopMode};
lottie("animations/loader.json")
.autoplay()
.loop_forever() // or .loop_mode(LoopMode::Loop) / .ping_pong()
}
Builders: .autoplay(), .loop_forever(), .loop_mode(LoopMode), .ping_pong(), .object_fit(ObjectFit), .prefetch_frames(n), .with_loading(|| element), .with_fallback(|| element).
See examples/painting.rs, gradient.rs, pattern.rs, shadow.rs, svg/main.rs, and gif_viewer.rs.
Containers & Overlays
Components for organizing content, managing layers, and showing floating UI.
Modal
Controlled dialog overlay with backdrop, escape-to-dismiss, and click-outside handling:
#![allow(unused)]
fn main() {
use kael::modal;
modal("confirm-dialog", self.is_open)
.label("Confirm action")
.backdrop(hsla(0.0, 0.0, 0.0, 0.5))
.dismiss_on_escape(true)
.dismiss_on_click_outside(true)
.render_with({
let entity = entity.clone();
move |state, _window, _cx| {
div()
.w(px(400.0))
.p_6()
.bg(rgb(0xffffff))
.rounded(px(12.0))
.shadow_xl()
.flex().flex_col().gap_4()
.child(div().text_lg().child("Are you sure?"))
.child(div().child("This action cannot be undone."))
.child(
div().flex().justify_end().gap_2()
.child(button("cancel").label("Cancel")
.on_click({
let entity = entity.clone();
move |_, _, cx| {
entity.update(cx, |this, cx| {
this.is_open = false;
cx.notify();
});
}
}))
.child(button("confirm").label("Confirm")
.on_click({
let entity = entity.clone();
move |_, _, cx| {
entity.update(cx, |this, cx| {
this.do_action();
this.is_open = false;
cx.notify();
});
}
}))
)
.into_any_element()
}
})
.on_change({
let entity = entity.clone();
move |open, _window, cx| {
entity.update(cx, |this, cx| {
this.is_open = *open;
cx.notify();
});
}
})
}
ModalRenderState fields: open, label, focused
Popover
Anchored floating panel with positioning:
#![allow(unused)]
fn main() {
use kael::popover;
popover("color-picker")
.anchor(|_window, _cx| {
button("show-colors").label("Colors").into_any_element()
})
.popup(|_window, _cx| {
div()
.w(px(200.0))
.p_3()
.bg(rgb(0xffffff))
.shadow_lg()
.rounded(px(8.0))
.child("Color picker content")
.into_any_element()
})
.dismiss_on_escape(true)
.dismiss_on_click_outside(true)
}
Tabs
Tabbed content switcher with keyboard navigation:
#![allow(unused)]
fn main() {
use kael::tabs;
#[derive(Clone, Copy, PartialEq, Eq)]
enum EditorTab { Code, Preview, Settings }
tabs("editor-tabs", self.active_tab, [
TabItem::new(EditorTab::Code, "Code", |_w, _cx| {
div().child("Code editor here").into_any_element()
}),
TabItem::new(EditorTab::Preview, "Preview", |_w, _cx| {
div().child("Live preview").into_any_element()
}),
TabItem::new(EditorTab::Settings, "Settings", |_w, _cx| {
div().child("Editor settings").into_any_element()
}),
])
.on_change({
let entity = entity.clone();
move |tab, _window, cx| {
entity.update(cx, |this, cx| {
this.active_tab = *tab;
cx.notify();
});
}
})
}
TabRenderState fields: value, label, index, tab_count, selected, focused
Disclosure
Collapsible section (accordion):
#![allow(unused)]
fn main() {
use kael::disclosure;
disclosure("advanced-settings", self.expanded)
.trigger(|_w, _cx| {
div().child("Advanced Settings ▾").into_any_element()
})
.panel(|_w, _cx| {
div().p_3().child("Hidden content here").into_any_element()
})
.on_change({
let entity = entity.clone();
move |open, _window, cx| {
entity.update(cx, |this, cx| {
this.expanded = *open;
cx.notify();
});
}
})
}
Splitter
Draggable pane divider for resizable layouts:
#![allow(unused)]
fn main() {
use kael::splitter;
splitter("main-split", self.split_ratio)
.on_change({
let entity = entity.clone();
move |ratio, _window, cx| {
entity.update(cx, |this, cx| {
this.split_ratio = *ratio;
cx.notify();
});
}
})
}
Use the ratio value to size adjacent panes:
#![allow(unused)]
fn main() {
let left_width = self.split_ratio * total_width;
div().flex().flex_row()
.child(div().w(px(left_width)).child("Left pane"))
.child(splitter("split", self.split_ratio).on_change(/* ... */))
.child(div().flex_1().child("Right pane"))
}
Context Menu
Right-click menus via .context_menu() on any Div:
#![allow(unused)]
fn main() {
div()
.id("file-item")
.child("document.txt")
.context_menu(|menu| {
menu.item("Open", |_w, cx| { /* handle open */ })
.item("Rename", |_w, cx| { /* handle rename */ })
.separator()
.item("Delete", |_w, cx| { /* handle delete */ })
})
}
Tooltip
Hover information via .tooltip() on any Div:
#![allow(unused)]
fn main() {
div()
.id("save-icon")
.child(icon("save"))
.tooltip("Save file (Cmd+S)")
// Custom tooltip content
div()
.id("status")
.child("●")
.tooltip_element(|| {
div()
.p_2()
.bg(rgb(0x1E1E1E))
.text_color(rgb(0xffffff))
.rounded(px(4.0))
.child("Connected to server")
})
}
Layer
Managed layer system for in-window modals and popovers:
#![allow(unused)]
fn main() {
use kael::layer;
layer("notification-layer")
.placement(LayerPlacement::Centered)
.child(/* floating content */)
}
Lists & Data
High-performance list components with virtualization for rendering thousands of items.
UniformList
Highest-performance list for items of equal height. Only renders visible items — handles 100K+ items smoothly:
#![allow(unused)]
fn main() {
use kael::{uniform_list, UniformListScrollHandle};
let scroll_handle = UniformListScrollHandle::new();
uniform_list(
"log-entries",
self.entries.len(),
{
let entries = self.entries.clone();
move |range, _window, _cx| {
entries[range.clone()]
.iter()
.map(|entry| {
div()
.px_3()
.py_1()
.text_sm()
.child(entry.message.clone())
.into_any_element()
})
.collect()
}
},
)
.track_scroll(scroll_handle.clone())
}
When to use: Log viewers, file lists, data tables — any list where every row has the same height.
List
Flexible list with alignment and overflow handling:
#![allow(unused)]
fn main() {
use kael::list;
// Basic list
list()
.child(div().child("Item 1"))
.child(div().child("Item 2"))
.child(div().child("Item 3"))
}
RecyclingList
Virtualized list for items with different heights. Recycles DOM nodes for performance:
#![allow(unused)]
fn main() {
use kael::recycling_list;
recycling_list(
"messages",
self.messages.len(),
move |index, _window, _cx| {
let msg = &messages[index];
div()
.p_3()
.child(div().font_weight(FontWeight::BOLD).child(msg.sender.clone()))
.child(div().text_sm().child(msg.body.clone()))
.into_any_element()
},
)
}
When to use: Chat messages, feed items — lists where rows vary in height.
SortableList
Drag-to-reorder list with auto-scroll and insertion indicator:
#![allow(unused)]
fn main() {
use kael::sortable_list;
sortable_list(
"layers",
self.layers.len(),
{
let layers = self.layers.clone();
move |index, _window, _cx| {
div()
.px_3()
.py_2()
.child(layers[index].name.clone())
.into_any_element()
}
},
)
.on_reorder({
let entity = entity.clone();
move |from, to, _window, cx| {
entity.update(cx, |this, cx| {
let item = this.layers.remove(from);
this.layers.insert(to, item);
cx.notify();
});
}
})
}
When to use: Layer panels, playlist editors, kanban columns — anywhere users reorder items by dragging.
ScrollBar
Kael provides automatic scrollbars for any element with overflow_y_scroll()
or overflow_y_auto() and a tracked ScrollHandle. The scrollbar appears
as a native-style dark rounded thumb when content overflows — no extra code
needed (see Layout & Styling).
For custom scroll bar rendering, use the explicit scroll_bar() widget:
#![allow(unused)]
fn main() {
use kael::scroll_bar;
scroll_bar(scroll_handle.clone())
.render_with(|state, bounds, window, _cx| {
// Custom scroll bar rendering
// state.thumb_bounds, state.dragging
})
}
Patterns
Data table with uniform_list
#![allow(unused)]
fn main() {
struct DataTable {
rows: Vec<Row>,
columns: Vec<Column>,
scroll: UniformListScrollHandle,
}
impl Render for DataTable {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let columns = self.columns.clone();
let rows = self.rows.clone();
div().flex().flex_col().size_full()
.child(self.render_header())
.child(
uniform_list("table-body", rows.len(), move |range, _w, _cx| {
rows[range.clone()].iter().map(|row| {
div().flex().flex_row()
.children(columns.iter().map(|col| {
div().w(px(col.width)).px_2().py_1()
.child(row.get(&col.key).clone())
}))
.into_any_element()
}).collect()
})
.track_scroll(self.scroll.clone())
)
}
}
}
Platform APIs
Kael provides native platform integration matching (and exceeding) Electron’s capabilities. All APIs work cross-platform on macOS, Windows, and Linux.
File Dialogs
Native open/save file pickers:
#![allow(unused)]
fn main() {
// Open file dialog
let paths = cx.prompt_for_paths(PathPromptOptions {
files: true,
directories: false,
multiple: true,
prompt: Some("Open".into()),
}).await;
// Save file dialog
let path = cx.prompt_for_new_path(
&std::env::current_dir()?,
Some("document.txt"),
).await;
}
Native Menus
Application menu bar (macOS menu bar, Windows/Linux window menu):
#![allow(unused)]
fn main() {
cx.set_menus(vec![
Menu {
name: "File".into(),
items: vec![
MenuItem::action("New", menu_action::New),
MenuItem::action("Open...", menu_action::Open),
MenuItem::separator(),
MenuItem::action("Save", menu_action::Save),
MenuItem::action("Save As...", menu_action::SaveAs),
MenuItem::separator(),
MenuItem::action("Quit", menu_action::Quit),
],
},
Menu {
name: "Edit".into(),
items: vec![
MenuItem::action("Undo", menu_action::Undo),
MenuItem::action("Redo", menu_action::Redo),
MenuItem::separator(),
MenuItem::action("Cut", menu_action::Cut),
MenuItem::action("Copy", menu_action::Copy),
MenuItem::action("Paste", menu_action::Paste),
],
},
]);
}
System Tray
Tray icon with menu and click handling:
#![allow(unused)]
fn main() {
// Set tray menu
cx.set_tray_menu(vec![
TrayMenuItem::Action {
label: "Show Window".into(),
id: "show".into(),
},
TrayMenuItem::Separator,
TrayMenuItem::Action {
label: "Quit".into(),
id: "quit".into(),
},
]);
cx.set_tray_tooltip("My App — Running");
// Handle tray menu actions
cx.on_tray_menu_action(|action_id, cx| {
if action_id.as_ref() == "show" {
// bring window to front
} else if action_id.as_ref() == "quit" {
cx.quit();
}
});
// Handle tray icon clicks
cx.on_tray_icon_event(|event, cx| {
match event {
TrayIconEvent::LeftClick => { /* toggle window */ },
TrayIconEvent::DoubleClick => { /* show window */ },
_ => {}
}
});
}
Clipboard
Read and write text and images:
#![allow(unused)]
fn main() {
// Write text
cx.write_to_clipboard(ClipboardItem::new_string("Hello, clipboard!".into()));
// Write text with metadata
cx.write_to_clipboard(ClipboardItem::new_string_with_metadata(
"formatted text".into(),
json!({"source": "my_app"}).to_string(),
));
// Read
if let Some(item) = cx.read_from_clipboard() {
if let Some(text) = item.text() {
println!("Got: {}", text);
}
}
}
Global Hotkeys
System-wide keyboard shortcuts (work even when app is unfocused):
#![allow(unused)]
fn main() {
cx.register_global_hotkey(1, &Keystroke::parse("cmd-shift-k")?)?;
cx.on_global_hotkey(|id| {
match id {
1 => { /* Cmd+Shift+K pressed anywhere */ },
_ => {}
}
});
}
Notifications
OS-level notifications (not in-app toasts):
#![allow(unused)]
fn main() {
cx.show_notification("Build Complete", "All tests passed")?;
cx.show_notification_with_actions(
"Update Available",
"Version 2.0 is ready to install",
&[
NotificationAction { id: "install".into(), label: "Install Now".into() },
NotificationAction { id: "later".into(), label: "Remind Later".into() },
],
|action_id| {
println!("User clicked: {}", action_id);
},
)?;
}
Deep Linking
Register and handle custom URL schemes. These methods are called on Application before .run():
#![allow(unused)]
fn main() {
Application::new()
// Handle all opened URLs
.on_open_urls(|urls| {
for url in urls {
println!("Opened: {}", url);
}
})
// Handle specific scheme with app context
.on_deep_link("myapp", |url, cx| {
// Handle myapp://path/to/resource
})
.run(|cx| {
// ...
});
}
Multi-Window
Open multiple windows with independent views:
#![allow(unused)]
fn main() {
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_window, cx| cx.new(|_| SettingsView::new()),
).unwrap();
}
Kael windows follow native platform conventions automatically:
- Scroll-to-focus — scrolling over an unfocused Kael window activates it, matching standard macOS/Windows behavior
- Smooth zoom — double-clicking the titlebar animates the window to fill the screen using native Core Animation transitions
- Live resize — content reflows smoothly during window drag-resizing
Auto-Update
Built-in application update pipeline:
#![allow(unused)]
fn main() {
let config = AutoUpdaterConfig {
feed_url: "https://releases.myapp.com/appcast.xml".into(),
..Default::default()
};
let updater = AutoUpdater::new(config, current_version, http_client);
// Check for updates
let status = updater.check_for_updates().await;
match status {
UpdateStatus::UpdateAvailable(info) => {
println!("New version: {}", info.version);
}
UpdateStatus::UpToDate => println!("Already up to date"),
_ => {}
}
}
Printing
Native print dialog and custom rendering:
#![allow(unused)]
fn main() {
let job = PrintJob::new("Document")
.orientation(PrintOrientation::Portrait)
.page(PrintPage::new(size(px(612.0), px(792.0)), |ctx| {
ctx.draw_text("Hello, printed world!", point(72.0, 72.0), style);
}));
window.show_print_dialog(job);
}
Power Management
Prevent sleep and detect power state:
#![allow(unused)]
fn main() {
// Prevent display sleep during video playback
let blocker = cx.start_power_save_blocker(PowerSaveBlockerKind::PreventDisplaySleep);
// Check power mode
match cx.power_mode() {
PowerMode::Performance => { /* full quality */ },
PowerMode::LowPower => { /* reduce effects */ },
_ => {}
}
// Detect idle time
if let Some(idle) = cx.system_idle_time() {
if idle > Duration::from_secs(300) { /* user is away */ }
}
// Listen for sleep/wake
cx.on_system_power_event(|event, cx| {
match event {
SystemPowerEvent::WillSleep => { /* save state */ },
SystemPowerEvent::DidWake => { /* refresh data */ },
_ => {}
}
});
}
Session Persistence
Save and restore window positions across launches:
#![allow(unused)]
fn main() {
let store = SessionStore::new("my-app")?;
// Save current window layout
store.save_window_states(&window_states)?;
// Restore on next launch
if let Ok(states) = store.load_window_states() {
for (id, state) in &states {
cx.open_window(WindowOptions {
window_bounds: Some(state.bounds),
..Default::default()
}, |_, cx| cx.new(|_| MyView::new()));
}
}
}
Display Information
Enumerate monitors and get DPI:
#![allow(unused)]
fn main() {
let displays = cx.displays();
let primary = cx.primary_display();
for display in &displays {
println!("Display {}: {:?}", display.id(), display.bounds());
}
}
Crash Reporting
Automatic crash capture with remote submission:
#![allow(unused)]
fn main() {
use kael::CrashReporter;
let mut reporter = CrashReporter {
app_id: "my-app".into(),
crash_dir: std::env::temp_dir().join("crashes"),
..Default::default()
};
reporter.install_hook();
}
App Lifecycle
Launch at login and update the dock/taskbar:
#![allow(unused)]
fn main() {
cx.set_auto_launch("com.example.app", true)?;
let enabled = cx.is_auto_launch_enabled("com.example.app");
cx.set_dock_badge(Some("3")); // None clears it
cx.set_dock_menu(dock_menu_items);
}
Enforce a single running instance — acquire a lock at startup and forward later launches to the existing process:
#![allow(unused)]
fn main() {
use kael::{SingleInstance, send_activate_to_existing};
match SingleInstance::acquire("com.example.app") {
Ok(instance) => {
instance.on_activate(Box::new(|| { /* focus the existing window */ }));
// ... run the app ...
}
Err(_already_running) => {
send_activate_to_existing("com.example.app")?;
return; // this duplicate launch exits
}
}
}
Biometric Authentication
Gate sensitive actions behind Touch ID / Face ID / Windows Hello. Check availability, then prompt with a reason string and a completion callback:
#![allow(unused)]
fn main() {
use kael::BiometricStatus;
if let BiometricStatus::Available(_kind) = cx.biometric_status() {
cx.authenticate_biometric("Unlock your vault", |success| {
if success { /* proceed */ }
});
}
}
BiometricStatus is Available(BiometricKind) or Unavailable; BiometricKind identifies the method (Touch ID, Face ID, fingerprint, Windows Hello).
Screen & Media Capture
Enumerate capturable displays/windows and stream frames (build with the screen-capture feature):
#![allow(unused)]
fn main() {
if cx.is_screen_capture_supported() {
// enumerate sources via cx.screen_capture_sources(..), then start a
// capture stream whose frames arrive as ScreenCaptureFrame values
}
}
See examples/capture_demo.rs.
Examples Gallery
Kael ships with 40+ runnable examples. Clone the repo and run any example:
git clone https://github.com/Augani/kael.git
cd kael
cargo run -p kael --example <name>
Getting started
| Example | Command | What it shows |
|---|---|---|
| Hello World | cargo run -p kael --example hello_world | Minimal window with styled text and colored boxes |
| Form Controls | cargo run -p kael --example form_controls | Every form widget: text input, checkbox, toggle, slider, radio, select, date picker, modal |
| Input | cargo run -p kael --example input | Text input with custom rendering |
Layout & styling
| Example | Command | What it shows |
|---|---|---|
| Grid Layout | cargo run -p kael --example grid_layout | CSS Grid-style layouts |
| Gradient | cargo run -p kael --example gradient | Linear and radial gradients |
| Shadow | cargo run -p kael --example shadow | Box shadow effects |
| Opacity | cargo run -p kael --example opacity | Transparency and blending |
| Pattern | cargo run -p kael --example pattern | Repeating pattern fills |
| Window | cargo run -p kael --example window | Window options and configuration |
| Window Positioning | cargo run -p kael --example window_positioning | Multi-display window placement |
Lists & data
| Example | Command | What it shows |
|---|---|---|
| Data Table | cargo run -p kael --example data_table | Virtual data table with sorting and selection |
| Uniform List | cargo run -p kael --example uniform_list | High-performance uniform-height list |
| Recycling List | cargo run -p kael --example recycling_list | Variable-height virtualized list |
| Tree | cargo run -p kael --example tree | Expandable tree view |
| Scrollable | cargo run -p kael --example scrollable | Scroll containers with elastic scrolling |
| Elastic Scrolling | cargo run -p kael --example elastic_scrolling | Momentum and bounce scrolling |
Text & rendering
| Example | Command | What it shows |
|---|---|---|
| Text | cargo run -p kael --example text | Text rendering and font features |
| Text Layout | cargo run -p kael --example text_layout | Text measurement and line breaking |
| Text Wrapper | cargo run -p kael --example text_wrapper | Word wrap and text overflow |
| Painting | cargo run -p kael --example painting | Custom GPU painting |
| SVG | cargo run -p kael --example svg | SVG rendering |
| Crispness Showcase | cargo run -p kael --example crispness_showcase | Pixel snapping, hairline strokes, sRGB gradients, corner shapes |
| Native Comparison | cargo run -p kael --example native_comparison | Side-by-side comparison with native AppKit rendering |
Media & animation
| Example | Command | What it shows |
|---|---|---|
| Animation | cargo run -p kael --example animation | Keyframe and spring animations |
| GIF Viewer | cargo run -p kael --example gif_viewer | Animated GIF playback |
| Image | cargo run -p kael --example image | Image loading and display |
| Image Gallery | cargo run -p kael --example image_gallery | Gallery with lazy loading |
| Image Loading | cargo run -p kael --example image_loading | Async image loading patterns |
Platform integration
| Example | Command | What it shows |
|---|---|---|
| Set Menus | cargo run -p kael --example set_menus | Native application menus |
| Tray Test | cargo run -p kael --example tray_test | System tray icon with menu |
| Platform Features | cargo run -p kael --example platform_features | Platform capability detection |
| Print Demo | cargo run -p kael --example print_demo | Native printing |
| WebView Demo | cargo run -p kael --example webview_demo | Embedded web content |
| Capture Demo | cargo run -p kael --example capture_demo | Screen/media capture |
| Drag & Drop | cargo run -p kael --example drag_drop | File drag-and-drop |
Advanced
| Example | Command | What it shows |
|---|---|---|
| Plugin Host | cargo run -p kael --example plugin_host | Extension loading and management |
| Daemon App | cargo run -p kael --example daemon_app | Background daemon with tray |
| Tab Stop | cargo run -p kael --example tab_stop | Keyboard focus navigation |
| Window Shadow | cargo run -p kael --example window_shadow | Custom window chrome |
| On Window Close Quit | cargo run -p kael --example on_window_close_quit | Window lifecycle handling |
Benchmarks
| Example | Command | What it shows |
|---|---|---|
| Perf Bench | cargo run -p kael --example perf_bench --release | Rendering performance measurement |
| Paths Bench | cargo run -p kael --example paths_bench --release | Path rendering performance |
Gestures
Kael provides built-in gesture recognizers for touch and pointer interactions.
Pan gesture
Detect drag/pan movements with velocity tracking:
#![allow(unused)]
fn main() {
use kael::gesture::PanGesture;
let pan = PanGesture::new()
.min_distance(px(5.0))
.on_start(|position, _window, _cx| { /* drag started */ })
.on_update(|delta, velocity, _window, _cx| { /* dragging */ })
.on_end(|velocity, _window, _cx| { /* drag ended */ });
}
Swipe gesture
Detect directional swipes:
#![allow(unused)]
fn main() {
use kael::gesture::SwipeGesture;
let swipe = SwipeGesture::new()
.on_swipe(|direction, _window, _cx| {
match direction {
SwipeDirection::Left => { /* swipe left */ },
SwipeDirection::Right => { /* swipe right */ },
SwipeDirection::Up => { /* swipe up */ },
SwipeDirection::Down => { /* swipe down */ },
}
});
}
Pinch gesture
Zoom/scale with pinch-to-zoom or Ctrl+scroll:
#![allow(unused)]
fn main() {
use kael::gesture::PinchGesture;
let pinch = PinchGesture::new()
.on_pinch(|scale, center, _window, _cx| {
// scale: f64 (1.0 = no change, >1 = zoom in, <1 = zoom out)
// center: Point<Pixels> (pinch center point)
});
}
Drag and drop
File drop (from OS)
#![allow(unused)]
fn main() {
div()
.id("drop-zone")
.on_file_drop(|event, _window, _cx| {
match event {
FileDropEvent::Entered(paths) => { /* files hovering */ },
FileDropEvent::Submit(paths) => { /* files dropped */ },
FileDropEvent::Exited => { /* drag cancelled */ },
_ => {}
}
})
}
Sortable reordering
See SortableList for drag-to-reorder within lists.
Scroll events
#![allow(unused)]
fn main() {
div()
.id("canvas")
.on_scroll_wheel(|event, _window, _cx| {
// event.delta: ScrollDelta (Pixels or Lines)
// event.modifiers: Modifiers (detect Ctrl for zoom)
})
}
Actions & Keybindings
Kael separates what happens (an action) from how it’s triggered (a keybinding or click). Actions are dispatched up the focused element tree, so a keystroke is routed to the nearest handler in the currently focused context — the same model that powers editor-grade keyboard UX.
Defining actions
The actions! macro generates zero-field action types in a namespace:
#![allow(unused)]
fn main() {
use kael::actions;
actions!(editor, [Save, Undo, Redo, Tab, TabPrev]);
}
Each entry becomes a type (Save, Undo, …) implementing the Action trait, with a stable name like editor::Save used for keymaps and dispatch.
Binding keys
Register bindings once at startup with cx.bind_keys. KeyBinding::new takes the keystroke string, the action, and an optional key context that scopes the binding:
#![allow(unused)]
fn main() {
use kael::{Application, App, KeyBinding};
Application::new().run(|cx: &mut App| {
cx.bind_keys([
KeyBinding::new("cmd-s", Save, None),
KeyBinding::new("cmd-z", Undo, None),
KeyBinding::new("cmd-shift-z", Redo, None),
KeyBinding::new("tab", Tab, Some("Editor")), // only in the "Editor" context
KeyBinding::new("shift-tab", TabPrev, Some("Editor")),
]);
// ... open windows ...
});
}
Keystroke syntax uses cmd / ctrl / alt / shift modifiers joined with -, and a space separates multi-key sequences (e.g. "cmd-k cmd-s"). Use cmd on macOS and ctrl on Windows/Linux.
Handling actions
In render, mark the element that owns a focus context with track_focus, then register handlers with on_action(cx.listener(...)). Handlers take &mut self, a reference to the action, the window, and the context:
#![allow(unused)]
fn main() {
use kael::{div, prelude::*, Context, FocusHandle, Render, Window};
struct Editor { focus_handle: FocusHandle }
impl Editor {
fn on_save(&mut self, _: &Save, _window: &mut Window, cx: &mut Context<Self>) {
// ... persist ...
cx.notify();
}
}
impl Render for Editor {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.track_focus(&self.focus_handle)
.on_action(cx.listener(Self::on_save))
.on_action(cx.listener(Self::on_undo))
.child("editor surface")
}
}
}
Focus & tab order
Create focus handles from the context and arrange tab order with tab_index / tab_stop. Move focus from the window:
#![allow(unused)]
fn main() {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let items = vec![
cx.focus_handle().tab_index(1).tab_stop(true),
cx.focus_handle().tab_index(2).tab_stop(true),
cx.focus_handle().tab_index(3).tab_stop(true),
];
let focus_handle = cx.focus_handle();
window.focus(&focus_handle);
Self { focus_handle, items }
}
}
Window focus methods: window.focus(&handle), window.focus_next() (Tab), and window.focus_prev() (Shift-Tab). Query state with handle.is_focused(window) and style focused elements with .focus(|s| s.border_color(...)).
See examples/tab_stop.rs for a complete focus-navigation demo, and crates/kael/docs/key_dispatch.md for the dispatch internals.
Plugins & Extensions
Kael ships an extension system with a contribution-point architecture. Extensions run out-of-process and can target one of two execution models: a sandboxed WASM module, or an external process that speaks the extension RPC protocol. The host loads extensions from a manifest, mediates their capabilities through a permission broker, and dispatches commands and notifications to them.
Defining a manifest
Build a PluginManifest with the builder. The positional arguments are id, name, version, api_version, entry_point, and execution_model; contribution points and capabilities are added fluently.
#![allow(unused)]
fn main() {
use kael::{ContributedCommand, ExecutionModel, PluginManifest};
let manifest = PluginManifest::builder(
"com.example.mock", // id
"Mock Plugin", // display name
"1.0.0", // plugin version
"1.0.0", // host API version it targets
"mock.wasm", // entry point
ExecutionModel::Wasm,
)
.command(ContributedCommand {
id: "mock.hello".to_string(),
title: "Say Hello".to_string(),
keybinding: None,
})
.build()?;
}
Manifests can also be loaded from disk with PluginManifest::from_json, PluginManifest::from_toml, or PluginManifest::load(path).
Loading and activating
ExtensionHostRuntime owns the installed extensions for an app. Load a manifest, then activate it — activate_with_broker runs the extension’s capability requests through a PermissionBroker first.
#![allow(unused)]
fn main() {
use kael::{ExtensionHostRuntime, PermissionBroker};
let mut runtime = ExtensionHostRuntime::new(&extensions_dir, "my-app");
runtime.load(manifest)?;
let broker = PermissionBroker::new();
for ext in runtime.all().iter().map(|e| e.manifest.id.clone()).collect::<Vec<_>>() {
runtime.activate_with_broker(&ext, &broker)?;
}
}
Other runtime operations: load_from_directory (dev mode), install_from_path, uninstall, activate / deactivate, unload, send_command, broadcast_notification, and all (returns &ExtensionInfo with manifest, is_active, process_id, load_path, dev_mode).
Contribution points
Extensions extend the host by contributing entries through the builder:
| Builder method | Contributes |
|---|---|
.command(ContributedCommand) | A command (id, title, optional keybinding) |
.menu_item(ContributedMenuItem) | A menu entry (target_menu, label, command_id) |
.panel(ContributedPanel) | A panel/view (id, title, default_position) |
.settings_schema(json) | A JSON schema for configurable settings |
.capability(Capability) | A requested capability (e.g. Capability::Notification) |
Panels position with PanelPosition::{Left, Right, Bottom, Floating}.
Execution models & RPC
ExecutionModel::Wasm runs the entry point as a sandboxed WebAssembly module. ExecutionModel::ExternalProcess launches a separate binary that connects over the platform transport and exchanges messages with the host.
The host and an external extension first complete a handshake (ExtensionHandshake, version-checked against EXTENSION_RPC_VERSION), then exchange a typed envelope:
| Direction | Type | Key variants |
|---|---|---|
| host → ext | ExtensionRequest | Activate, Deactivate, Shutdown, GetContributions, ExecuteCommand { command_id, args } |
| ext → host | ExtensionResponse | Ack, Contributions(Contributions) |
| host → ext | ExtensionNotification | SettingsChanged { key, value } |
Broadcast a notification to every active extension:
#![allow(unused)]
fn main() {
use kael::ExtensionNotification;
runtime.broadcast_notification(ExtensionNotification::SettingsChanged {
key: "theme".to_string(),
value: serde_json::json!("dark"),
});
}
See examples/plugin_host.rs for a complete host UI that loads both a WASM and an external-process extension, activates them through a broker, and streams a live log.
Multi-Process & IPC
Kael supports an Electron-style multi-process architecture: the UI runs in the main process while heavy or untrusted work runs in supervised child processes that communicate over typed IPC. Transport is platform-native — Unix domain sockets on macOS/Linux, named pipes on Windows — and the framework handles framing, request/response correlation, progress streaming, and crash reporting for you.
Process model
Every process has a class describing its role:
#![allow(unused)]
fn main() {
use kael::ProcessClass;
ProcessClass::Ui; // the main UI process
ProcessClass::Worker; // background compute
ProcessClass::Media; // media decode/playback
ProcessClass::Extension; // sandboxed plugins (see Plugins & Extensions)
}
A child process is described by a ProcessInfo, built fluently:
#![allow(unused)]
fn main() {
use kael::{ProcessId, ProcessInfo};
let info = ProcessInfo::worker(ProcessId(0), "thumbnailer")
.executable("/path/to/worker-binary")
.arg("--quiet")
.env("RUST_LOG", "warn");
}
Constructors exist for each role: ProcessInfo::worker, ProcessInfo::media, and ProcessInfo::extension.
Spawning a worker (host side)
WorkerHost owns the socket directory and supervises spawned children. request sends a typed payload and blocks for the response; fire_and_forget sends without waiting; health_check pings the child.
#![allow(unused)]
fn main() {
use kael::{ProcessClass, ProcessId, ProcessInfo, WorkerHost};
let mut host = WorkerHost::with_temp_dir();
let info = ProcessInfo::worker(ProcessId(0), "thumbnailer")
.executable(worker_binary_path);
let worker = host.spawn_worker(ProcessClass::Worker, info)?;
worker.health_check()?; // round-trip ping
let response: serde_json::Value = worker.request(serde_json::json!({
"op": "echo",
"message": "hello from host",
}))?;
assert_eq!(response["message"], "hello from host");
}
The worker child
The child binary connects back to the host with WorkerClient::connect_from_env (it reads the GPUI_WORKER_SOCKET / GPUI_WORKER_PIPE environment variable the host sets) and serves requests with run. The handler receives a WorkerRequest and a progress callback for streaming intermediate updates, and returns a WorkerResponse or WorkerError.
use anyhow::Result;
use kael::{WorkerClient, WorkerProgress, WorkerRequest, WorkerResponse};
fn main() -> Result<()> {
let client = WorkerClient::connect_from_env()?;
client.run(|request, progress| match request {
WorkerRequest::Ping => Ok(WorkerResponse::Pong),
WorkerRequest::Execute { payload } => {
progress(WorkerProgress::Update(serde_json::json!({ "step": 1 })));
// ... do work ...
Ok(WorkerResponse::Result(payload))
}
})
}
The message types:
| Type | Variants |
|---|---|
WorkerRequest | Ping, Execute { payload: serde_json::Value } |
WorkerResponse | Pong, Result(serde_json::Value) |
WorkerProgress | Update(serde_json::Value) |
WorkerError | Execution(String), Cancelled |
Supervision & crash handling
Register an event callback to observe lifecycle events. A child that crashes is reported as a SupervisorEvent::Exited rather than taking down the host:
#![allow(unused)]
fn main() {
use kael::SupervisorEvent;
host.on_event(|event| match event {
SupervisorEvent::Exited { id, .. } => eprintln!("worker {id:?} exited"),
_ => {}
});
}
Each WorkerHandle exposes id() to correlate it with supervisor events.
Extension processes
Extension children use the same transport but a richer RPC envelope (handshake, contribution discovery, command dispatch). They are managed by ExtensionHostRuntime rather than WorkerHost — see Plugins & Extensions.
Security & Permissions
Kael provides a capability-based security model for controlling what extensions and child processes can access.
Permission system
#![allow(unused)]
fn main() {
use kael::security::*;
let mut manager = PermissionManager::new();
// Request permission
let request = PermissionRequest::new(
PermissionKind::FileSystem,
"Read project files",
);
match manager.check(&request) {
PermissionStatus::Granted => { /* proceed */ },
PermissionStatus::Denied => { /* blocked */ },
PermissionStatus::Prompt => { /* ask user */ },
}
}
Network policy
Control outbound network access:
#![allow(unused)]
fn main() {
let policy = NetworkPolicy {
allowed_hosts: vec!["api.myapp.com".into()],
blocked_hosts: vec![],
allow_localhost: true,
};
}
Process capabilities
Limit what child processes can do:
#![allow(unused)]
fn main() {
let limits = ProcessLimits {
max_memory_mb: 512,
max_cpu_percent: 50,
max_open_files: 256,
};
let capabilities = vec![
ProcessCapability::FileRead,
ProcessCapability::Network,
];
}
Credential storage
Secure credential management via OS keychain:
#![allow(unused)]
fn main() {
let keychain = KeychainStore::new("my-app");
keychain.write("api-token", "secret-value")?;
let token = keychain.read("api-token")?;
keychain.delete("api-token")?;
}
For LLMs
This page explains how to use Kael’s LLM integration features.
llms.txt
Kael provides an llms.txt file at the site root following the llms.txt standard. This file contains a structured overview of the entire Kael API — widget primitives, layout system, platform APIs, and code patterns — optimized for LLM consumption.
Use it when:
- Pasting into ChatGPT, Claude, or other LLMs as context for building Kael apps
- Integrating with AI coding assistants that support llms.txt
- Building MCP servers or tool definitions that reference Kael
Copy for LLM button
Every page on this site has a “Copy for LLM” button in the bottom-right corner. Click it to copy the page content as clean markdown, ready to paste into any LLM conversation.
Direct link
https://augani.github.io/kael/llms.txt