SharpMarker is the in-progress C# port of the Wayscriber Wayland annotation tool.
It targets .NET 11, uses SkiaSharp for drawing, and relies on wlr-layer-shell bindings generated at build time to run as a fullscreen overlay above all compositor UI (Waybar included).
- Wayland overlay that covers panels – the layer surface is anchored to every edge and uses
set_exclusive_zone(-1)so the annotation plane always renders above Waybar and other reserved zones. - Real-time drawing primitives – freehand, line, rectangle, ellipse, arrow, and text tools powered by SkiaSharp and shared-memory buffers.
- Live status bar – bottom-left capsule shows active mode, color name, stroke thickness, tool, and text size; adapts to board modes for contrast.
- Tool & color shortcuts – modifier chords switch tools on the fly (
Shiftline,Ctrlrectangle,Ctrl+Shiftarrow,Tabellipse,Ttoggles text). Single-key color hotkeys (R,G,B,Y,O,P,W,K) update both stroke and active text. - Thickness & font adjustment – scroll wheel and
-/=keys alter stroke thickness; holdShiftwhile scrolling (or useCtrl+Shift++/Ctrl+Shift+-) to resize text. - Configurable keybindings – shortcuts can be remapped via JSON config for faster workflows.
- Board modes –
Ctrl+W(whiteboard),Ctrl+B(blackboard),Ctrl+Shift+T(transparent) swap the background palette instantly. - Capture shortcuts – full screen, active window, or region capture via
grim/slurp, with clipboard and file variants. - Zoom view –
Ctrl+Alt+ScrollorCtrl+Alt++/Ctrl+Alt+-zooms the frozen background with lock/pan controls and refresh capture. - Session persistence – drawings, history, active board, and tool state are written to
$XDG_STATE_HOME/sharp-marker/session.jsonwhen the overlay closes, and everySession.AutosaveSecondswhile it is open so a crash does not lose them. SetSession.RestoreOnStarttotrueto load them back on the next launch; an explicit--modestill wins over the restored board. - Signal-driven daemon – optional daemon mode listens for POSIX signals (default
SIGUSR1) and toggles the overlay without restarting the app. - Active-monitor detection – prefers Hyprland (
hyprctl), Sway (swaymsg), orgrim -loutput to pin the overlay to the focused monitor; falls back to compositor defaults when commands are unavailable.
What is still experimental:
- Multi-monitor simultaneous overlays are still on the roadmap.
SharpMarker.slnx– solution that ties everything together.src/SharpMarker.App– console entry point, argument parsing, config loading, and runtime orchestration.src/SharpMarker.CoreOverlay/– Wayland bindings, layer-shell session, input handling, rendering, and status bar drawing.Configuration/– strongly-typed JSON config with defaults and normalization helpers.Daemon/– signal-based activation loop.Capture/–grim/slurpcapture pipeline, clipboard integration, and portal fallback helpers.Persistence/– versioned, SkiaSharp-free session contracts plus the atomic session store.Wayland/– protocol, shared-memory, display, and active-output helpers.
tests/SharpMarker.Tests– xUnit coverage for drawing, input, persistence, capture, lifecycle, and Wayland behavior.wayland-protocols/– XML protocol descriptions consumed by the source generator.
- .NET 11 SDK matching the version pinned in
global.json.
Verify withdotnet --version. - Linux Wayland compositor (tested with wlroots/Hyprland).
WAYLAND_DISPLAYmust be set. - Runtime tools (optional but used when present):
hyprctlorswaymsgfor focused-output detection.grimfor screenshot capture and fallback monitor detection.slurpfor region selection capture.wl-copyfor clipboard captures.gdbusfor xdg-desktop-portal fallback capture.
- System font libraries: SharpMarker bundles
libSkiaSharp.so, but its Linux font resolver requires Fontconfig and FreeType at runtime. Installfontconfigandfreetype2on Arch,libfontconfig1andlibfreetype6on Debian/Ubuntu, orfontconfigandfreetypeon Fedora-family systems. Future.deb, RPM, and Arch packages must declare these as required package dependencies. Generic tarball users must install them separately.
dotnet build SharpMarker.slnxRun the unit suite:
dotnet test SharpMarker.slnxGenerated Wayland bindings are emitted under src/SharpMarker.Core/obj/generated/… each build; no manual step is needed unless protocols change (see Development Notes).
dotnet run --project src/SharpMarker.App -- --active [--mode transparent|whiteboard|blackboard] [--config FILE] [--log-file FILE] [--verbose]dotnet run --project src/SharpMarker.App -- --daemon [--config FILE] [--log-file FILE] [--verbose]- The daemon listens for the configured POSIX signal (default
SIGUSR1).
Sendkill -USR1 $(pgrep sharpmarker)to open/close the overlay. --print-configdumps the merged configuration (built-ins plus file overrides) without launching anything.--versionprints the current assembly version.
On launch the app looks for a config file, preferring ~/.config/sharp-marker/config.json unless --config overrides the path. Missing files are tolerated; defaults kick in automatically.
| Action | Gesture |
|---|---|
| Exit overlay | Esc |
| Draw | Left mouse button drag |
| Adjust thickness | Mouse wheel / trackpad scroll, or - / = |
| Change background | Ctrl+W whiteboard, Ctrl+B blackboard, Ctrl+Shift+T transparent |
| Switch tool | (default) freehand, hold Shift for line, Ctrl rectangle, Ctrl+Shift arrow, Tab ellipse, press T to toggle text mode |
| Text entry | Type directly; Shift+Enter newline, Enter commits, Esc cancels |
| Resize active text | Shift+Scroll or Ctrl+Shift++/Ctrl+Shift+- |
| Color shortcuts | R red, G green, B blue, Y yellow, O orange, P purple, W white, K black |
| Toggle help | F10 / F1 |
| Toggle status bar | F12 / F4 |
| Open configurator | F11 |
| Zoom in/out | Ctrl+Alt+Scroll or Ctrl+Alt++/Ctrl+Alt+- |
| Reset zoom | Ctrl+Alt+0 |
| Toggle zoom lock | Ctrl+Alt+L |
| Refresh zoom capture | Ctrl+Alt+R |
| Pan zoom view | Middle mouse drag or arrow keys (hold Shift for larger steps) |
| Capture full screen | Ctrl+Shift+P |
| Capture active window | Ctrl+Shift+O |
| Capture selection | Ctrl+Shift+I |
| Capture full (clipboard) | Ctrl+C |
| Capture full (file) | Ctrl+S |
| Capture selection (clipboard) | Ctrl+Shift+C |
| Capture selection (file) | Ctrl+Shift+S |
| Capture region (clipboard) | Ctrl+6 |
| Capture region (file) | Ctrl+Shift+6 |
Additional notes:
- Scroll input distinguishes smooth vs. discrete deltas to keep stylus wheels predictable.
Tests validating the behaviour live underLayerShellSessionScrollTests. - The status bar respects
Overlay.ShowStatusBarin the configuration; set it tofalseto hide the capsule.
Configuration is JSON (trailing commas & comments supported) and mirrors the AppConfig record. Defaults are baked in, so you only need to supply overrides. Example:
Only include sections you need to override; unspecified values fall back to the defaults seen in Configuration/AppConfig.cs.
Keybindings are defined as a JSON object with action names mapped to arrays of shortcuts. Use a comma-separated list in the configurator UI, or edit the JSON directly to override defaults.
- Layer-shell bindings –
Wayland.SourceGeneratorconsumes XML files fromwayland-protocols/. Update those XMLs if you need newer protocol revisions, then rebuild. - Rendering – SkiaSharp renders into shared-memory buffers; when
Performance.BufferCount > 1the buffer chain handles frame pacing. - Status bar – draws every frame and factors in active mode, tool override, and text metrics. Disable via config if a clean canvas is preferred.
- Capture pipeline – uses
grim+slurpfor full/region capture,wl-copyfor clipboard copies, and falls back to xdg-desktop-portal viagdbuswhen configured. - Coding style – nullable reference types and implicit usings are enabled. Unsafe code is allowed because generated Wayland bindings require it.
- Session persistence –
Persistence/holds the saved-session format.SessionContracts.csdefines versioned DTOs (points, colors, tools, primitives, boards, pages, history) that never reference SkiaSharp, so a Skia upgrade cannot change what an existing save means;SessionMapperis the only file that translates between those DTOs and the drawing runtime.SessionPersistencedecides when to save and restore: the overlay controller observes the current revision once when it first builds the model, optionally restores its contents, and saves when the overlay hides with a shutdown retry after a write failure. Failures—including omitted drawings or an unflushed directory entry—always reach the log and, in daemon mode, the status bar on the next successful activation; a one-shot run reports them through the log and a non-zero exit code. A save carries the revision it was built beside even when restoration is disabled, so a second SharpMarker cannot replace edits it never saw.SessionStorereads and writes$XDG_STATE_HOME/sharp-marker/session.jsonwith a temporary-file → flush → rename sequence and keeps the previous readable revision insession.json.bak. Each save inspects the file already on disk, so the revision counter is monotonic whether or not the caller loaded first, and a session written by a newer build is never overwritten or silently downgraded (bothLoadandSaverefuse it outright). A damaged primary file falls back to the backup and is moved tosession.json.corrupt; documents are validated structurally both before any caller sees them and before anything is written. Writers are serialized across processes by an advisory lock onsession.json.lock, so two overlays cannot interleave a save.
- Capture exports do not bake annotations into the saved image (captures are of the underlying screen).
- Zoom uses a frozen background; refresh it with
Ctrl+Alt+Rafter the underlying screen changes. - Multi-monitor overlays are limited to the focused output; simultaneous multi-surface sessions are not yet supported.
- Windows/X11 backends are out of scope; the app exits gracefully when
WAYLAND_DISPLAYis missing. - Presenter tools and selection/editing of existing drawings are still in progress.
Contributions and issue reports are welcome while the prototype continues to close the parity gap with the Rust implementation.
{ "Overlay": { "DefaultMode": "Transparent", "ShowStatusBar": true, "PreferredOutput": "DP-1" }, "Drawing": { "DefaultColor": "red", "DefaultThickness": 6, "MinThickness": 1, "MaxThickness": 50, "FontFamily": "Sans", "FontSize": 20 }, "Capture": { "Enabled": true, "ScreenshotDirectory": "~/Pictures", "FilenameTemplate": "screenshot_%Y-%m-%d_%H%M%S", "Format": "png", "CopyToClipboard": true, "ExitAfterCapture": false, "UseGrimTooling": true, "AllowPortalFallback": true }, "Daemon": { "Enabled": true, "ActivationSignal": "SIGUSR1", "ShowTrayIcon": true, "ActivationDebounceMs": 250 }, "Performance": { "BufferCount": 3, "UseFrameCallbacks": true, "EnableAntialiasing": true }, "Session": { "Save": true, "RestoreOnStart": false, "AutosaveSeconds": 30, "Path": "" }, "Keybindings": { "toggle_help": ["F10", "F1"], "toggle_status_bar": ["F12", "F4"], "undo": ["Ctrl+Z"], "redo": ["Ctrl+Shift+Z", "Ctrl+Y"] } }