A lightweight on-screen annotation tool in Rust.
Draw, highlight, and annotate on your screen during presentations, video calls, tutorials, all without interrupting your workflow.
- Transparent fullscreen overlay — draws on top of everything, invisible when not in use
- Passthrough mode — click-through to the desktop when you're not drawing; toggle with Space or the toolbar button
- Tools
- Pen — freehand drawing with configurable width (1 / 3 / 8 px)
- Highlighter — semi-transparent strokes (12 / 24 / 40 px)
- Eraser — removes strokes by proximity
- Shapes — Arrow (default), Line, Rectangle, Circle; drag to draw; click to cycle shape
- Text — click to place, type, Enter to commit
- 24-color palette — pick from the color popup
- Fading ink — strokes fade out automatically (1 / 2 / 3 / 5 s); on by default at 2 s
- Cursor spotlight — tool icon drawn at the pointer position, great for screen sharing
- Backgrounds — Transparent / White (50% opacity) / Black (50% opacity)
- Undo (Ctrl+Z) and Clear all (Delete)
- Persistent settings — tool, color, widths, and toolbar position are saved automatically
Download the latest rabisco binary and its checksum from the Releases page.
Verify and run:
sha256sum -c rabisco.sha256
chmod +x rabisco
./rabiscoNo runtime dependencies beyond a display server (X11 or Wayland) and a Vulkan-capable GPU.
- Linux (X11 or Wayland)
- A GPU with Vulkan support (via wgpu)
cargo build --release
./target/release/rabiscoRequires a running display server. Debug builds work fine for development:
cargo run| Key | Action |
|---|---|
| Space | Toggle draw / passthrough mode |
| 1–5 | Switch tool (Pen / Highlighter / Eraser / Shapes / Text) |
| Enter | Commit text (Text tool) |
| Ctrl+Z | Undo last stroke |
| Delete / Backspace | Clear all strokes |
| Escape | Switch to passthrough mode |
The floating toolbar lives in its own OS window so it stays clickable even when the canvas is in passthrough mode.
- Click a tool button to select it
- Hold a tool button (0.4 s) to open its options popup (width presets, shape picker, etc.)
- Shapes tool: click again while selected to cycle Arrow → Line → Rect → Circle
- Drag the grip dots at the top to reposition the toolbar anywhere on screen
- Collapse with the
◉button; the pill shows the active drawing color - Single-click the pill to toggle draw/passthrough mode
- Double-click the pill to expand the toolbar again
Settings are saved to ~/.config/rabisco/settings.json automatically whenever you switch tools or change any style option.
src/
main.rs — eframe entry point, window configuration
app.rs — RabiscoApp: main loop, keyboard shortcuts, command dispatch
canvas.rs — Canvas, DrawnStroke, StrokeKind; render, erase, fading
config.rs — DrawStyle (Copy), ShapeKind, Background, PALETTE
settings.rs — Settings struct, JSON load/save (~/.config/rabisco/)
tools/
mod.rs — Tool trait + CanvasCommand enum
pen.rs
highlighter.rs
eraser.rs
shapes.rs — ShapesTool, shape_icon(), cycle logic
text_tool.rs — click-to-place, direct keyboard capture
ui/
toolbar.rs — floating toolbar viewport, hold-to-open popups
mod.rs
Two-window model. The app runs two OS windows: a fullscreen transparent canvas and a small toolbar. The toolbar is a secondary egui viewport (show_viewport_immediate), which requires the wgpu renderer — the glow backend does not support multiple viewports. The main canvas uses ViewportCommand::MousePassthrough to become click-through; the toolbar is never passthrough, so it stays interactive regardless of draw mode.
Command pattern. Tools never hold a reference to Canvas. Tool::handle_input() returns Vec<CanvasCommand>, which RabiscoApp::apply_commands() processes after the borrow of self.tools[i] is released. This avoids borrow-checker conflicts. DrawStyle is Copy for the same reason — it is passed by value so &mut self.tools[i] and self.style can coexist.
Settings change detection. app.rs snapshots style, active_tool, and background before the toolbar viewport call each frame. If anything changed afterwards, save_settings() writes the JSON immediately. No timer or dirty flag is needed.
- Create
src/tools/my_tool.rsand implement theTooltrait:
use egui::{Painter, Response};
use crate::config::DrawStyle;
use super::{CanvasCommand, Tool};
pub struct MyTool;
impl Tool for MyTool {
fn name(&self) -> &'static str { "My Tool" }
// Use ASCII or safe Unicode only — emoji may not render in egui's fonts
fn icon(&self) -> &'static str { "M" }
fn handle_input(
&mut self,
response: &Response,
painter: &Painter,
style: DrawStyle,
time: f64,
) -> Vec<CanvasCommand> {
vec![]
}
}- Register it in
app.rs:
let tools: Vec<Box<dyn Tool>> = vec![
// ...existing tools...
Box::new(MyTool),
];That's all. The toolbar discovers tools from the Vec automatically and assigns keyboard shortcuts 1–5 in order.
Optional trait methods to override:
| Method | Purpose |
|---|---|
on_activate() |
Called when this tool is selected |
on_deactivate() |
Called when switching away — discard in-progress state here |
on_confirm() |
Called on Enter — commit pending state (e.g. text) |
captures_keyboard() -> bool |
Return true while consuming keyboard input; suppresses Space, Delete, Backspace, and number shortcuts in the main loop |
options_ui() |
Render extra controls inside the hold popup |
Emitting canvas changes — use CanvasCommand:
// Add a stroke
CanvasCommand::AddStroke { kind: StrokeKind::Pen { .. }, time, fading }
// Erase strokes near a point
CanvasCommand::EraseAt { pos, radius }
// Undo / clear
CanvasCommand::Undo
CanvasCommand::ClearIcon rendering note. egui's embedded fonts cover ASCII, Latin Extended, and Geometric Shapes (U+25A0–U+25FF). Emoji (U+1F000+) and Dingbats (U+2700–U+27BF) are unreliable — use ASCII characters for icons.
Add a variant to StrokeKind in canvas.rs, then handle it in Canvas::render(). Look at the existing Pen, Highlighter, Shape, and Text variants for reference.
Add a field to Settings in settings.rs. Fields that may be absent in existing JSON files must use #[serde(default)]. Mirror the field in from_state() and to_draw_style() (or wherever it maps).
Requires the GitHub CLI (gh).
make release automatically reads the current version from Cargo.toml, increments the patch number, writes it back, builds the binary, and prints the next steps.
make release # 0.1.0 → 0.1.1 (patch bump, default)
make release BUMP=minor # 0.1.x → 0.2.0
make release BUMP=major # 0.x.x → 1.0.0Then follow the printed next steps (copy-paste ready):
git add Cargo.toml Cargo.lock
git commit -m "chore: release v0.1.1"
git tag v0.1.1 && git push origin main --tags
gh release create v0.1.1 release/rabisco release/rabisco.sha256 --title "v0.1.1"The release binary lives in release/ (gitignored) and is only uploaded as a GitHub Release asset — it is never committed to the repository.
Contributions are welcome. The project follows standard Rust conventions.
- Fork the repository
- Create a feature branch:
git checkout -b feat/my-feature - Make your changes and verify the build is clean:
cargo build cargo clippy -- -D warnings
- Commit with a descriptive message
- Open a pull request against
main
- Keep PRs focused — one feature or fix per PR
- PRs that add a new tool should include the tool registered in
app.rsand tested manually against X11 and Wayland if possible - Icon characters must be ASCII or confirmed to render in egui's default fonts
- New
Settingsfields need#[serde(default)]for backwards compatibility - No new dependencies without prior discussion in an issue
If Rabisco has helped you give better presentations or saved you some time, consider fueling its development with a coffee! It's completely optional, but always deeply appreciated.
0x89F2d3AF38990478C43e0c47a57E7e62b330b86A |
J6d3y3865LXFa5fyK39V7xjx6vnRmdhQHkYjJirKNkJq |
|
0x89F2d3AF38990478C43e0c47a57E7e62b330b86A |
||


