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 modesCtrl+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 viewCtrl+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

ActionGesture
Exit overlayEsc
DrawLeft mouse button drag
Adjust thicknessMouse wheel / trackpad scroll, or - / =
Change backgroundCtrl+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 entryType directly; Shift+Enter newline, Enter commits, Esc cancels
Resize active textShift+Scroll or Ctrl+Shift++/Ctrl+Shift+-
Color shortcutsR red, G green, B blue, Y yellow, O orange, P purple, W white, K black
Toggle helpF10 / F1
Toggle status barF12 / F4
Open configuratorF11
Zoom in/outCtrl+Alt+Scroll or Ctrl+Alt++/Ctrl+Alt+-
Reset zoomCtrl+Alt+0
Toggle zoom lockCtrl+Alt+L
Refresh zoom captureCtrl+Alt+R
Pan zoom viewMiddle mouse drag or arrow keys (hold Shift for larger steps)
Capture full screenCtrl+Shift+P
Capture active windowCtrl+Shift+O
Capture selectionCtrl+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 bindingsWayland.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 persistencePersistence/ 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

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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 modesCtrl+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 viewCtrl+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

ActionGesture
Exit overlayEsc
DrawLeft mouse button drag
Adjust thicknessMouse wheel / trackpad scroll, or - / =
Change backgroundCtrl+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 entryType directly; Shift+Enter newline, Enter commits, Esc cancels
Resize active textShift+Scroll or Ctrl+Shift++/Ctrl+Shift+-
Color shortcutsR red, G green, B blue, Y yellow, O orange, P purple, W white, K black
Toggle helpF10 / F1
Toggle status barF12 / F4
Open configuratorF11
Zoom in/outCtrl+Alt+Scroll or Ctrl+Alt++/Ctrl+Alt+-
Reset zoomCtrl+Alt+0
Toggle zoom lockCtrl+Alt+L
Refresh zoom captureCtrl+Alt+R
Pan zoom viewMiddle mouse drag or arrow keys (hold Shift for larger steps)
Capture full screenCtrl+Shift+P
Capture active windowCtrl+Shift+O
Capture selectionCtrl+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 bindingsWayland.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 persistencePersistence/ 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

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 modesCtrl+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 viewCtrl+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

ActionGesture
Exit overlayEsc
DrawLeft mouse button drag
Adjust thicknessMouse wheel / trackpad scroll, or - / =
Change backgroundCtrl+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 entryType directly; Shift+Enter newline, Enter commits, Esc cancels
Resize active textShift+Scroll or Ctrl+Shift++/Ctrl+Shift+-
Color shortcutsR red, G green, B blue, Y yellow, O orange, P purple, W white, K black
Toggle helpF10 / F1
Toggle status barF12 / F4
Open configuratorF11
Zoom in/outCtrl+Alt+Scroll or Ctrl+Alt++/Ctrl+Alt+-
Reset zoomCtrl+Alt+0
Toggle zoom lockCtrl+Alt+L
Refresh zoom captureCtrl+Alt+R
Pan zoom viewMiddle mouse drag or arrow keys (hold Shift for larger steps)
Capture full screenCtrl+Shift+P
Capture active windowCtrl+Shift+O
Capture selectionCtrl+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 bindingsWayland.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 persistencePersistence/ 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

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 modesCtrl+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 viewCtrl+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

ActionGesture
Exit overlayEsc
DrawLeft mouse button drag
Adjust thicknessMouse wheel / trackpad scroll, or - / =
Change backgroundCtrl+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 entryType directly; Shift+Enter newline, Enter commits, Esc cancels
Resize active textShift+Scroll or Ctrl+Shift++/Ctrl+Shift+-
Color shortcutsR red, G green, B blue, Y yellow, O orange, P purple, W white, K black
Toggle helpF10 / F1
Toggle status barF12 / F4
Open configuratorF11
Zoom in/outCtrl+Alt+Scroll or Ctrl+Alt++/Ctrl+Alt+-
Reset zoomCtrl+Alt+0
Toggle zoom lockCtrl+Alt+L
Refresh zoom captureCtrl+Alt+R
Pan zoom viewMiddle mouse drag or arrow keys (hold Shift for larger steps)
Capture full screenCtrl+Shift+P
Capture active windowCtrl+Shift+O
Capture selectionCtrl+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 bindingsWayland.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 persistencePersistence/ 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

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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 modesCtrl+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 viewCtrl+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

ActionGesture
Exit overlayEsc
DrawLeft mouse button drag
Adjust thicknessMouse wheel / trackpad scroll, or - / =
Change backgroundCtrl+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 entryType directly; Shift+Enter newline, Enter commits, Esc cancels
Resize active textShift+Scroll or Ctrl+Shift++/Ctrl+Shift+-
Color shortcutsR red, G green, B blue, Y yellow, O orange, P purple, W white, K black
Toggle helpF10 / F1
Toggle status barF12 / F4
Open configuratorF11
Zoom in/outCtrl+Alt+Scroll or Ctrl+Alt++/Ctrl+Alt+-
Reset zoomCtrl+Alt+0
Toggle zoom lockCtrl+Alt+L
Refresh zoom captureCtrl+Alt+R
Pan zoom viewMiddle mouse drag or arrow keys (hold Shift for larger steps)
Capture full screenCtrl+Shift+P
Capture active windowCtrl+Shift+O
Capture selectionCtrl+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 bindingsWayland.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 persistencePersistence/ 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

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 modesCtrl+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 viewCtrl+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

ActionGesture
Exit overlayEsc
DrawLeft mouse button drag
Adjust thicknessMouse wheel / trackpad scroll, or - / =
Change backgroundCtrl+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 entryType directly; Shift+Enter newline, Enter commits, Esc cancels
Resize active textShift+Scroll or Ctrl+Shift++/Ctrl+Shift+-
Color shortcutsR red, G green, B blue, Y yellow, O orange, P purple, W white, K black
Toggle helpF10 / F1
Toggle status barF12 / F4
Open configuratorF11
Zoom in/outCtrl+Alt+Scroll or Ctrl+Alt++/Ctrl+Alt+-
Reset zoomCtrl+Alt+0
Toggle zoom lockCtrl+Alt+L
Refresh zoom captureCtrl+Alt+R
Pan zoom viewMiddle mouse drag or arrow keys (hold Shift for larger steps)
Capture full screenCtrl+Shift+P
Capture active windowCtrl+Shift+O
Capture selectionCtrl+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 bindingsWayland.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 persistencePersistence/ 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

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 modesCtrl+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 viewCtrl+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

ActionGesture
Exit overlayEsc
DrawLeft mouse button drag
Adjust thicknessMouse wheel / trackpad scroll, or - / =
Change backgroundCtrl+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 entryType directly; Shift+Enter newline, Enter commits, Esc cancels
Resize active textShift+Scroll or Ctrl+Shift++/Ctrl+Shift+-
Color shortcutsR red, G green, B blue, Y yellow, O orange, P purple, W white, K black
Toggle helpF10 / F1
Toggle status barF12 / F4
Open configuratorF11
Zoom in/outCtrl+Alt+Scroll or Ctrl+Alt++/Ctrl+Alt+-
Reset zoomCtrl+Alt+0
Toggle zoom lockCtrl+Alt+L
Refresh zoom captureCtrl+Alt+R
Pan zoom viewMiddle mouse drag or arrow keys (hold Shift for larger steps)
Capture full screenCtrl+Shift+P
Capture active windowCtrl+Shift+O
Capture selectionCtrl+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 bindingsWayland.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 persistencePersistence/ 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

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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 modesCtrl+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 viewCtrl+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

ActionGesture
Exit overlayEsc
DrawLeft mouse button drag
Adjust thicknessMouse wheel / trackpad scroll, or - / =
Change backgroundCtrl+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 entryType directly; Shift+Enter newline, Enter commits, Esc cancels
Resize active textShift+Scroll or Ctrl+Shift++/Ctrl+Shift+-
Color shortcutsR red, G green, B blue, Y yellow, O orange, P purple, W white, K black
Toggle helpF10 / F1
Toggle status barF12 / F4
Open configuratorF11
Zoom in/outCtrl+Alt+Scroll or Ctrl+Alt++/Ctrl+Alt+-
Reset zoomCtrl+Alt+0
Toggle zoom lockCtrl+Alt+L
Refresh zoom captureCtrl+Alt+R
Pan zoom viewMiddle mouse drag or arrow keys (hold Shift for larger steps)
Capture full screenCtrl+Shift+P
Capture active windowCtrl+Shift+O
Capture selectionCtrl+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 bindingsWayland.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 persistencePersistence/ 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