Skip to content

Repository files navigation

SharpMarker

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).


Current Capabilities

  • 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 (Shift line, Ctrl rectangle, Ctrl+Shift arrow, Tab ellipse, T toggles 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; hold Shift while scrolling (or use Ctrl+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+Scroll or Ctrl+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.json when the overlay closes, and every Session.AutosaveSeconds while it is open so a crash does not lose them. Set Session.RestoreOnStart to true to load them back on the next launch; an explicit --mode still 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), or grim -l output 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.

Project Layout

  • SharpMarker.slnx – solution that ties everything together.
  • src/SharpMarker.App – console entry point, argument parsing, config loading, and runtime orchestration.
  • src/SharpMarker.Core
    • Overlay/ – 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/slurp capture 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.

Prerequisites

  • .NET 11 SDK matching the version pinned in global.json.
    Verify with dotnet --version.
  • Linux Wayland compositor (tested with wlroots/Hyprland). WAYLAND_DISPLAY must be set.
  • Runtime tools (optional but used when present):
    • hyprctl or swaymsg for focused-output detection.
    • grim for screenshot capture and fallback monitor detection.
    • slurp for region selection capture.
    • wl-copy for clipboard captures.
    • gdbus for xdg-desktop-portal fallback capture.
  • System font libraries: SharpMarker bundles libSkiaSharp.so, but its Linux font resolver requires Fontconfig and FreeType at runtime. Install fontconfig and freetype2 on Arch, libfontconfig1 and libfreetype6 on Debian/Ubuntu, or fontconfig and freetype on Fedora-family systems. Future .deb, RPM, and Arch packages must declare these as required package dependencies. Generic tarball users must install them separately.

Build

dotnet build SharpMarker.slnx

Run the unit suite:

dotnet test SharpMarker.slnx

Generated Wayland bindings are emitted under src/SharpMarker.Core/obj/generated/… each build; no manual step is needed unless protocols change (see Development Notes).


Run Modes

One-shot overlay

dotnet run --project src/SharpMarker.App -- --active [--mode transparent|whiteboard|blackboard] [--config FILE] [--log-file FILE] [--verbose]

Daemon

dotnet run --project src/SharpMarker.App -- --daemon [--config FILE] [--log-file FILE] [--verbose]
  • The daemon listens for the configured POSIX signal (default SIGUSR1).
    Send kill -USR1 $(pgrep sharpmarker) to open/close the overlay.
  • --print-config dumps the merged configuration (built-ins plus file overrides) without launching anything.
  • --version prints 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.


Overlay Controls

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 under LayerShellSessionScrollTests.
  • The status bar respects Overlay.ShowStatusBar in the configuration; set it to false to hide the capsule.

Configuration

Configuration is JSON (trailing commas & comments supported) and mirrors the AppConfig record. Defaults are baked in, so you only need to supply overrides. Example:

{
  "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"]
  }
}

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.


Development Notes

  • Layer-shell bindings – Wayland.SourceGenerator consumes XML files from wayland-protocols/. Update those XMLs if you need newer protocol revisions, then rebuild.
  • Rendering – SkiaSharp renders into shared-memory buffers; when Performance.BufferCount > 1 the 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 + slurp for full/region capture, wl-copy for clipboard copies, and falls back to xdg-desktop-portal via gdbus when 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.cs defines versioned DTOs (points, colors, tools, primitives, boards, pages, history) that never reference SkiaSharp, so a Skia upgrade cannot change what an existing save means; SessionMapper is the only file that translates between those DTOs and the drawing runtime. SessionPersistence decides 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. SessionStore reads and writes $XDG_STATE_HOME/sharp-marker/session.json with a temporary-file → flush → rename sequence and keeps the previous readable revision in session.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 (both Load and Save refuse it outright). A damaged primary file falls back to the backup and is moved to session.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 on session.json.lock, so two overlays cannot interleave a save.

Known Limitations

  • 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+R after 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_DISPLAY is 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.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages