git.lucas.co / cce-icons
SVG icon set
git clone https://git.lucas.co/cce-icons.git

commit1df5096b1d1dc4227d10d5d439bcdd280448495d
parent549d116e05
authorLucas Galante <[email protected]>
date2026-08-15 23:28
Add the cce cursor theme

A full XCursor theme under cursors/: 24 shapes covering the
freedesktop set, emitted as 25 cursors with 106 alias links at sizes
24/32/48/64/96.

The default pointer is a rounded plus rather than an arrow, following
the earlier crosshair-theme experiment and svg/plus.svg; white fill
with a #282a36 outline and a soft dark halo, which keeps it legible on
both the dark desktop and white document content.

build.py writes the XCursor container directly — header, per-size TOC,
premultiplied BGRA chunks — so xcursorgen is not a dependency (it is
not packaged here). Hotspots live in source viewBox units and scale
per size; badged cursors (copy/alias/help/context-menu/progress) use a
compact plus at (11,11) with the badge lower-right.

The alias lists are load-bearing: GTK and Qt still ask for X11 legacy
names and the hashed ones, and a missing alias is what makes a theme
look half-applied in one toolkit.

Verified end to end in a nested cce-fx pointed at the theme: wlroots
loaded it and drew the plus (screenshot-confirmed), every symlink
resolves, and all five sizes are present per cursor. Nothing selects
the theme yet — see cursors/README.md for the one-line
default/index.theme change that does.

Co-Authored-By: Claude Fable 5 <[email protected]>

 .gitignore                    |   3 +
 cursors/README.md             |  66 +++++++++++
 cursors/build.py              | 249 ++++++++++++++++++++++++++++++++++++++++++
 cursors/svg/alias.svg         |   7 ++
 cursors/svg/cell.svg          |   5 +
 cursors/svg/context-menu.svg  |   7 ++
 cursors/svg/copy.svg          |   7 ++
 cursors/svg/crosshair.svg     |   7 ++
 cursors/svg/ew-resize.svg     |   8 ++
 cursors/svg/grab.svg          |   7 ++
 cursors/svg/grabbing.svg      |   8 ++
 cursors/svg/help.svg          |   7 ++
 cursors/svg/move.svg          |  12 ++
 cursors/svg/nesw-resize.svg   |  10 ++
 cursors/svg/not-allowed.svg   |   6 +
 cursors/svg/ns-resize.svg     |  13 +++
 cursors/svg/nwse-resize.svg   |  10 ++
 cursors/svg/pencil.svg        |   7 ++
 cursors/svg/plus.svg          |   7 ++
 cursors/svg/pointer.svg       |  13 +++
 cursors/svg/progress.svg      |   7 ++
 cursors/svg/text.svg          |   7 ++
 cursors/svg/up-arrow.svg      |   8 ++
 cursors/svg/vertical-text.svg |   8 ++
 cursors/svg/wait.svg          |   7 ++
 cursors/svg/zoom-in.svg       |   8 ++
 cursors/svg/zoom-out.svg      |   8 ++
 27 files changed, 512 insertions(+)

diff --git a/.gitignore b/.gitignore
index d4bf142..d5b0e6b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,6 @@ tmp_*
 # derived build output
 xpm/
 png/
+
+# cursor theme build output
+cursors/dist/
diff --git a/cursors/README.md b/cursors/README.md
new file mode 100644
index 0000000..8784635
--- /dev/null
+++ b/cursors/README.md
@@ -0,0 +1,66 @@
+# cce cursor theme
+
+A full XCursor theme in the cce language: **white fill, `#282a36` outline**, with
+a rounded **plus** as the default pointer rather than an arrow (it follows the
+earlier `crosshair-theme` experiment, and matches `svg/plus.svg` in the icon set).
+
+```
+svg/        the sources — one SVG per distinct shape, 32x32 viewBox
+build.py    svg -> PNG (magick) -> XCursor binary + alias symlinks
+dist/       build output (untracked)
+```
+
+## Build
+
+```sh
+python3 build.py              # -> dist/cce
+python3 build.py --install    # -> $XDG_DATA_HOME/icons/cce
+python3 build.py --only text  # rebuild one cursor while iterating
+```
+
+`xcursorgen` is **not** required: `build.py` writes the XCursor container itself
+(header, per-size TOC, premultiplied BGRA image chunks). Sizes are 24/32/48/64/96,
+covering scale 1 through 4 at a 24px nominal cursor.
+
+## Using it
+
+The theme installs to `~/.local/share/icons/cce` but nothing selects it. The
+compositor asks for the theme literally named **`default`** — `Cursor::init`
+passes a null theme name to `wlr_xcursor_manager_create`, which does *not* read
+`$XCURSOR_THEME`. So point the `default` theme at this one:
+
+```ini
+# ~/.local/share/icons/default/index.theme
+[Icon Theme]
+Name=Default
+Inherits=cce
+```
+
+That covers GTK and Qt apps too. (`ccectl` has no cursor-theme command; the other
+route is the `cce-window-management` protocol's `set_xcursor_theme`, which reaches
+`Cursor::set_theme`.)
+
+## Adding or changing a cursor
+
+1. Drop a 32x32 SVG in `svg/`.
+2. Add a row to `CURSORS` in `build.py`: `(name, svg stem, hotspot x, hotspot y, [aliases])`.
+   Hotspots are in **source viewBox units** and are scaled per size.
+3. Rebuild.
+
+The alias lists are load-bearing, not decoration: GTK and Qt still request X11
+legacy names (`xterm`, `fleur`, `sb_v_double_arrow`) and hashed names
+(`00008160000006810000408080010102`). A missing alias is the usual reason a theme
+looks half-applied in one toolkit.
+
+Cursors with a state badge (`copy`, `alias`, `help`, `context-menu`, `progress`)
+use a compact plus in the upper-left with the badge lower-right, so their hotspot
+is `(11,11)`, not the centre.
+
+## Animation
+
+Every cursor here is single-frame. The compositor *can* drive animated cursors —
+see `Cursor::advance_xcursor_frame` in cce-compositor, which walks XCursor's
+per-frame delays — so an animated `wait`/`progress` only needs extra frames:
+give `build.py` a list of SVGs per cursor and a delay, and emit one image chunk
+per frame per size (the container already supports it; `xcursor()` takes a
+`delay` field it currently always writes as 0).
diff --git a/cursors/build.py b/cursors/build.py
new file mode 100644
index 0000000..73594fe
--- /dev/null
+++ b/cursors/build.py
@@ -0,0 +1,249 @@
+#!/usr/bin/env python3
+"""Build the `cce` XCursor theme from the SVG sources in ./svg.
+
+Renders each source at every size in SIZES, adds the soft dark halo that keeps
+a white cursor legible on white content, and writes the XCursor binary format
+directly -- `xcursorgen` is not a dependency (it isn't packaged on this
+machine, and the format is a header plus one chunk per image).
+
+Output lands in ./dist/cce (untracked). `--install` copies it to
+$XDG_DATA_HOME/icons/cce.
+
+Cursor names come from CURSORS below: each entry maps an SVG to its hotspot
+(in the 32x32 source viewBox) and every name that should resolve to it --
+freedesktop names, X11 legacy names, and the MD5-looking names GTK/Qt still
+ask for. Missing aliases are the usual reason a theme "doesn't work" in some
+app, so they are part of the data, not an afterthought.
+"""
+
+import argparse
+import os
+import shutil
+import struct
+import subprocess
+import sys
+from pathlib import Path
+
+HERE = Path(__file__).resolve().parent
+SVG_DIR = HERE / "svg"
+DIST = HERE / "dist" / "cce"
+
+# Covers scale 1 through 4 at a 24px nominal cursor.
+SIZES = [24, 32, 48, 64, 96]
+VIEWBOX = 32.0
+
+# Soft dark halo, in source-viewBox units, scaled per size.
+SHADOW_BLUR = 0.55
+SHADOW_OFFSET = (0.25, 0.4)
+SHADOW_ALPHA = 0.55
+
+# name -> (svg stem, hotspot x, hotspot y, [aliases])
+# Hotspots are in the 32x32 viewBox.
+CURSORS = [
+    ("default", "plus", 16, 16, [
+        "left_ptr", "arrow", "top_left_arrow", "top_left_corner_arrow",
+        "default", "pointer-arrow",
+    ]),
+    ("crosshair", "crosshair", 16, 16, ["cross", "tcross", "cross_reverse"]),
+    ("text", "text", 16, 16, ["xterm", "ibeam"]),
+    ("vertical-text", "vertical-text", 16, 16, ["vertical_text"]),
+    ("pointer", "pointer", 12, 4, [
+        "hand", "hand1", "hand2", "pointing_hand",
+        "9d800788f1b08800ae810202380a0822",  # hand2
+        "e29285e634086352946a0e7090d73106",  # hand2
+    ]),
+    ("progress", "progress", 11, 11, [
+        "half-busy", "left_ptr_watch",
+        "08e8e1c95fe2fc01f976f1e063a24ccd",
+        "3ecb610c1bf2410f44200f48c40d3599",
+        "00000000000000020006000e7e9ffc3f",
+        "9116a3ea924ed2162ecab71ba103b17f",
+    ]),
+    ("wait", "wait", 16, 16, ["watch", "clock"]),
+    ("help", "help", 11, 11, [
+        "question_arrow", "whats_this", "dnd-ask", "left_ptr_help",
+        "5c6cd98b3f3ebcb1f9c7f1c204630408",
+        "d9ce0ab605698f320427677b458ad60b",
+    ]),
+    ("context-menu", "context-menu", 11, 11, []),
+    ("not-allowed", "not-allowed", 16, 16, [
+        "forbidden", "crossed_circle", "circle",
+        "03b6e0fcb3499374a867c041f52298f0",
+    ]),
+    ("no-drop", "not-allowed", 16, 16, [
+        "dnd-none", "dnd_no_drop",
+        "b66166c04f8c3109214a4fbd64a50fc8",
+    ]),
+    ("copy", "copy", 11, 11, [
+        "dnd-copy", "copycur",
+        "1081e37283d90000800003c07f3ef6bf",
+        "6407b0e94181790501fd1e167b474872",
+    ]),
+    ("alias", "alias", 11, 11, [
+        "dnd-link", "link",
+        "640fb0e74195791501fd1ed57b41487f",
+        "a2a266d0498c3104214a47bd64ab0fc8",
+    ]),
+    ("move", "move", 16, 16, [
+        "fleur", "size_all", "all-scroll", "dnd-move",
+        "4498f0e0c1937ffe01fd06f973665830",
+        "fcf21c00b30f7e3f83fe0dfd12e71cff",
+        "38c5dff7c7b8962045400281044508d2",
+    ]),
+    ("grab", "grab", 16, 16, ["openhand", "hand1-grab"]),
+    ("grabbing", "grabbing", 16, 16, [
+        "closedhand", "dnd-grabbing",
+        "08ffe1cb5fe6fc01f906f1c063814ccf",
+        "5aca4d189052212118709018842178c0",
+    ]),
+    ("cell", "cell", 16, 16, ["plus"]),
+    ("zoom-in", "zoom-in", 14, 14, ["zoom_in"]),
+    ("zoom-out", "zoom-out", 14, 14, ["zoom_out"]),
+    ("ns-resize", "ns-resize", 16, 16, [
+        "n-resize", "s-resize", "row-resize", "v_double_arrow",
+        "sb_v_double_arrow", "split_v", "double_arrow", "size_ver",
+        "top_side", "bottom_side", "based_arrow_up", "based_arrow_down",
+        "00008160000006810000408080010102",
+        "2870a09082c103050810ffdffffe0204",
+    ]),
+    ("ew-resize", "ew-resize", 16, 16, [
+        "e-resize", "w-resize", "col-resize", "h_double_arrow",
+        "sb_h_double_arrow", "split_h", "size_hor",
+        "left_side", "right_side",
+        "028006030e0e7ebffc7f7070c0600140",
+        "14fef782d02440884392942c11205230",
+    ]),
+    ("nesw-resize", "nesw-resize", 16, 16, [
+        "ne-resize", "sw-resize", "size_bdiag", "fd_double_arrow",
+        "top_right_corner", "bottom_left_corner",
+        "fcf1c3c7cd4491d801f1e1c78f100000",
+    ]),
+    ("nwse-resize", "nwse-resize", 16, 16, [
+        "nw-resize", "se-resize", "size_fdiag", "bd_double_arrow",
+        "top_left_corner", "bottom_right_corner",
+        "c7088f0f3e6c8088236ef8e1e3e70000",
+    ]),
+    ("pencil", "pencil", 5, 27, ["draft", "draft_large", "draft_small"]),
+    ("up-arrow", "up-arrow", 16, 4, ["center_ptr", "sb_up_arrow"]),
+]
+
+XC_IMAGE = 0xFFFD0002
+
+
+def render(svg: Path, size: int):
+    """Render `svg` at `size`x`size` and composite the halo. Returns RGBA bytes."""
+    from PIL import Image, ImageFilter
+
+    out = subprocess.run(
+        ["magick", "-background", "none", "-density", str(int(size / VIEWBOX * 96)),
+         str(svg), "-resize", f"{size}x{size}", "png32:-"],
+        check=True, capture_output=True,
+    ).stdout
+    import io
+    img = Image.open(io.BytesIO(out)).convert("RGBA")
+    if img.size != (size, size):
+        canvas = Image.new("RGBA", (size, size), (0, 0, 0, 0))
+        canvas.paste(img, ((size - img.width) // 2, (size - img.height) // 2))
+        img = canvas
+
+    scale = size / VIEWBOX
+    blur = SHADOW_BLUR * scale
+    if blur > 0.05:
+        alpha = img.getchannel("A").filter(ImageFilter.GaussianBlur(blur))
+        alpha = alpha.point(lambda a: int(a * SHADOW_ALPHA))
+        shadow = Image.new("RGBA", img.size, (0, 0, 0, 0))
+        shadow.putalpha(alpha)
+        dx = int(round(SHADOW_OFFSET[0] * scale))
+        dy = int(round(SHADOW_OFFSET[1] * scale))
+        canvas = Image.new("RGBA", img.size, (0, 0, 0, 0))
+        canvas.paste(shadow, (dx, dy), shadow)
+        img = Image.alpha_composite(canvas, img)
+    return img.tobytes()
+
+
+def xcursor(frames):
+    """Serialize [(size, w, h, xhot, yhot, delay, rgba)] into XCursor bytes.
+
+    Pixels go out as premultiplied ARGB32 little-endian, i.e. B,G,R,A on the
+    wire -- the layout libXcursor and wlroots both read back.
+    """
+    ntoc = len(frames)
+    header = struct.pack("<4sIII", b"Xcur", 16, 0x10000, ntoc)
+    toc_size = ntoc * 12
+    pos = 16 + toc_size
+    toc, chunks = b"", b""
+    for size, w, h, xhot, yhot, delay, rgba in frames:
+        toc += struct.pack("<III", XC_IMAGE, size, pos)
+        body = bytearray(w * h * 4)
+        for i in range(0, len(rgba), 4):
+            r, g, b, a = rgba[i], rgba[i + 1], rgba[i + 2], rgba[i + 3]
+            body[i] = b * a // 255
+            body[i + 1] = g * a // 255
+            body[i + 2] = r * a // 255
+            body[i + 3] = a
+        chunk = struct.pack("<IIIIIIIII", 36, XC_IMAGE, size, 1,
+                            w, h, xhot, yhot, delay) + bytes(body)
+        chunks += chunk
+        pos += len(chunk)
+    return header + toc + chunks
+
+
+def main():
+    ap = argparse.ArgumentParser()
+    ap.add_argument("--install", action="store_true",
+                    help="copy the built theme into $XDG_DATA_HOME/icons/cce")
+    ap.add_argument("--only", help="build just this cursor name (fast iteration)")
+    args = ap.parse_args()
+
+    cursors_out = DIST / "cursors"
+    cursors_out.mkdir(parents=True, exist_ok=True)
+
+    built, skipped, links = 0, [], 0
+    for name, stem, hx, hy, aliases in CURSORS:
+        if args.only and name != args.only:
+            continue
+        svg = SVG_DIR / f"{stem}.svg"
+        if not svg.exists():
+            skipped.append(f"{name} (no {stem}.svg)")
+            continue
+
+        frames = []
+        for size in SIZES:
+            rgba = render(svg, size)
+            xhot = int(round(hx / VIEWBOX * size))
+            yhot = int(round(hy / VIEWBOX * size))
+            frames.append((size, size, size, xhot, yhot, 0, rgba))
+
+        (cursors_out / name).write_bytes(xcursor(frames))
+        built += 1
+
+        for alias in aliases:
+            if alias == name:
+                continue
+            link = cursors_out / alias
+            if link.is_symlink() or link.exists():
+                link.unlink()
+            link.symlink_to(name)
+            links += 1
+
+    (DIST / "index.theme").write_text(
+        "[Icon Theme]\nName=cce\nComment=CCE desktop cursors\n"
+    )
+    (DIST / "cursor.theme").write_text("[Icon Theme]\nName=cce\nInherits=cce\n")
+
+    print(f"built {built} cursors x {len(SIZES)} sizes, {links} aliases -> {DIST}")
+    if skipped:
+        print("skipped (source missing): " + ", ".join(skipped))
+
+    if args.install:
+        data = os.environ.get("XDG_DATA_HOME") or str(Path.home() / ".local/share")
+        target = Path(data) / "icons" / "cce"
+        if target.exists():
+            shutil.rmtree(target)
+        shutil.copytree(DIST, target, symlinks=True)
+        print(f"installed -> {target}")
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/cursors/svg/alias.svg b/cursors/svg/alias.svg
new file mode 100644
index 0000000..f539f40
--- /dev/null
+++ b/cursors/svg/alias.svg
@@ -0,0 +1,7 @@
+<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
+  <!-- Compact default plus (hotspot 11,11) with the alias badge -->
+  <path d="M11 2.5 L11 19.5 M2.5 11 L19.5 11" fill="none" stroke="#282a36" stroke-width="7.5" stroke-linecap="round"/>
+  <path d="M11 2.5 L11 19.5 M2.5 11 L19.5 11" fill="none" stroke="#ffffff" stroke-width="4" stroke-linecap="round"/>
+  <circle cx="23" cy="23" r="8.2" fill="#ffffff" stroke="#282a36" stroke-width="2.2"/>
+  <path d="M19.4 26.6 L26.6 19.4 M21.4 19.4 L26.6 19.4 L26.6 24.6" fill="none" stroke="#282a36" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
diff --git a/cursors/svg/cell.svg b/cursors/svg/cell.svg
new file mode 100644
index 0000000..596034b
--- /dev/null
+++ b/cursors/svg/cell.svg
@@ -0,0 +1,5 @@
+<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
+  <!-- Spreadsheet cell: square-cut plus, no rounding -->
+  <path d="M16 3 L16 29 M3 16 L29 16" fill="none" stroke="#282a36" stroke-width="8"/>
+  <path d="M16 3 L16 29 M3 16 L29 16" fill="none" stroke="#ffffff" stroke-width="3.6"/>
+</svg>
diff --git a/cursors/svg/context-menu.svg b/cursors/svg/context-menu.svg
new file mode 100644
index 0000000..3b7f179
--- /dev/null
+++ b/cursors/svg/context-menu.svg
@@ -0,0 +1,7 @@
+<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
+  <!-- Compact default plus (hotspot 11,11) with the context-menu badge -->
+  <path d="M11 2.5 L11 19.5 M2.5 11 L19.5 11" fill="none" stroke="#282a36" stroke-width="7.5" stroke-linecap="round"/>
+  <path d="M11 2.5 L11 19.5 M2.5 11 L19.5 11" fill="none" stroke="#ffffff" stroke-width="4" stroke-linecap="round"/>
+  <circle cx="23" cy="23" r="8.2" fill="#ffffff" stroke="#282a36" stroke-width="2.2"/>
+  <path d="M18.8 19.6 L27.2 19.6 M18.8 23 L27.2 23 M18.8 26.4 L27.2 26.4" fill="none" stroke="#282a36" stroke-width="2.4" stroke-linecap="round"/>
+</svg>
diff --git a/cursors/svg/copy.svg b/cursors/svg/copy.svg
new file mode 100644
index 0000000..2d54509
--- /dev/null
+++ b/cursors/svg/copy.svg
@@ -0,0 +1,7 @@
+<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
+  <!-- Compact default plus (hotspot 11,11) with the copy badge -->
+  <path d="M11 2.5 L11 19.5 M2.5 11 L19.5 11" fill="none" stroke="#282a36" stroke-width="7.5" stroke-linecap="round"/>
+  <path d="M11 2.5 L11 19.5 M2.5 11 L19.5 11" fill="none" stroke="#ffffff" stroke-width="4" stroke-linecap="round"/>
+  <circle cx="23" cy="23" r="8.2" fill="#ffffff" stroke="#282a36" stroke-width="2.2"/>
+  <path d="M23 18.6 L23 27.4 M18.6 23 L27.4 23" fill="none" stroke="#282a36" stroke-width="2.8" stroke-linecap="round"/>
+</svg>
diff --git a/cursors/svg/crosshair.svg b/cursors/svg/crosshair.svg
new file mode 100644
index 0000000..b6de193
--- /dev/null
+++ b/cursors/svg/crosshair.svg
@@ -0,0 +1,7 @@
+<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
+  <!-- Precision variant of the default: thinner arms, open centre -->
+  <path d="M16 2.5 L16 12 M16 20 L16 29.5 M2.5 16 L12 16 M20 16 L29.5 16"
+        fill="none" stroke="#282a36" stroke-width="6" stroke-linecap="round"/>
+  <path d="M16 2.5 L16 12 M16 20 L16 29.5 M2.5 16 L12 16 M20 16 L29.5 16"
+        fill="none" stroke="#ffffff" stroke-width="2.6" stroke-linecap="round"/>
+</svg>
diff --git a/cursors/svg/ew-resize.svg b/cursors/svg/ew-resize.svg
new file mode 100644
index 0000000..d66bab7
--- /dev/null
+++ b/cursors/svg/ew-resize.svg
@@ -0,0 +1,8 @@
+<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
+  <g fill="none" stroke-width="8.5" stroke-linecap="round" stroke-linejoin="round" stroke="#282a36">
+    <path d="M4 16 L28 16"/><path d="M9.5 10.5 L4 16 L9.5 21.5"/><path d="M22.5 10.5 L28 16 L22.5 21.5"/>
+  </g>
+  <g fill="none" stroke-width="3.6" stroke-linecap="round" stroke-linejoin="round" stroke="#ffffff">
+    <path d="M4 16 L28 16"/><path d="M9.5 10.5 L4 16 L9.5 21.5"/><path d="M22.5 10.5 L28 16 L22.5 21.5"/>
+  </g>
+</svg>
diff --git a/cursors/svg/grab.svg b/cursors/svg/grab.svg
new file mode 100644
index 0000000..ea1fe7f
--- /dev/null
+++ b/cursors/svg/grab.svg
@@ -0,0 +1,7 @@
+<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
+  <!-- Open hand: four fingers up, thumb out left -->
+  <path fill="#ffffff" stroke="#282a36" stroke-width="2.4" stroke-linejoin="round"
+        d="M11.6 13.2 v-4.6 a2.1 2.1 0 0 1 4.2 0 v4.2 h.7 v-5.6 a2.1 2.1 0 0 1 4.2 0 v5.6 h.7
+           v-4.6 a2.1 2.1 0 0 1 4.2 0 v6.1 h.6 v-2.6 a2 2 0 0 1 4 0 v7.4
+           a7.2 7.2 0 0 1 -7.2 7.2 h-4.6 a7 7 0 0 1 -5.2 -2.3 l-5.4 -6 a2.2 2.2 0 0 1 3 -3.3 z"/>
+</svg>
diff --git a/cursors/svg/grabbing.svg b/cursors/svg/grabbing.svg
new file mode 100644
index 0000000..e997a96
--- /dev/null
+++ b/cursors/svg/grabbing.svg
@@ -0,0 +1,8 @@
+<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
+  <!-- Closed fist: knuckle bumps along the top -->
+  <path fill="#ffffff" stroke="#282a36" stroke-width="2.4" stroke-linejoin="round"
+        d="M9.6 16.4 v-2.2 a2.1 2.1 0 0 1 3.6 -1.5 a2.1 2.1 0 0 1 3.7 -1.2
+           a2.1 2.1 0 0 1 3.7 -.2 a2.1 2.1 0 0 1 3.9 1.1 v2.1
+           a2 2 0 0 1 2.1 2 v4.6 a7.2 7.2 0 0 1 -7.2 7.2 h-4.4
+           a7 7 0 0 1 -5.1 -2.2 l-4.6 -4.9 a2.2 2.2 0 0 1 3 -3.2 z"/>
+</svg>
diff --git a/cursors/svg/help.svg b/cursors/svg/help.svg
new file mode 100644
index 0000000..b97f6a0
--- /dev/null
+++ b/cursors/svg/help.svg
@@ -0,0 +1,7 @@
+<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
+  <!-- Compact default plus (hotspot 11,11) with the help badge -->
+  <path d="M11 2.5 L11 19.5 M2.5 11 L19.5 11" fill="none" stroke="#282a36" stroke-width="7.5" stroke-linecap="round"/>
+  <path d="M11 2.5 L11 19.5 M2.5 11 L19.5 11" fill="none" stroke="#ffffff" stroke-width="4" stroke-linecap="round"/>
+  <circle cx="23" cy="23" r="8.2" fill="#ffffff" stroke="#282a36" stroke-width="2.2"/>
+  <path d="M20.3 20.8 a2.8 2.8 0 0 1 5.4 1 c0 1.9 -2.7 2.2 -2.7 4" fill="none" stroke="#282a36" stroke-width="2.5" stroke-linecap="round"/><circle cx="23" cy="27.2" r="1.5" fill="#282a36"/>
+</svg>
diff --git a/cursors/svg/move.svg b/cursors/svg/move.svg
new file mode 100644
index 0000000..ac0ff3b
--- /dev/null
+++ b/cursors/svg/move.svg
@@ -0,0 +1,12 @@
+<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
+  <g fill="none" stroke-width="8" stroke-linecap="round" stroke-linejoin="round" stroke="#282a36">
+    <path d="M16 3.5 L16 28.5 M3.5 16 L28.5 16"/>
+    <path d="M11.5 8 L16 3.5 L20.5 8"/><path d="M11.5 24 L16 28.5 L20.5 24"/>
+    <path d="M8 11.5 L3.5 16 L8 20.5"/><path d="M24 11.5 L28.5 16 L24 20.5"/>
+  </g>
+  <g fill="none" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round" stroke="#ffffff">
+    <path d="M16 3.5 L16 28.5 M3.5 16 L28.5 16"/>
+    <path d="M11.5 8 L16 3.5 L20.5 8"/><path d="M11.5 24 L16 28.5 L20.5 24"/>
+    <path d="M8 11.5 L3.5 16 L8 20.5"/><path d="M24 11.5 L28.5 16 L24 20.5"/>
+  </g>
+</svg>
diff --git a/cursors/svg/nesw-resize.svg b/cursors/svg/nesw-resize.svg
new file mode 100644
index 0000000..d54e7cf
--- /dev/null
+++ b/cursors/svg/nesw-resize.svg
@@ -0,0 +1,10 @@
+<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
+  <g transform="rotate(-45 16 16)">
+    <g fill="none" stroke-width="8.5" stroke-linecap="round" stroke-linejoin="round" stroke="#282a36">
+      <path d="M16 4 L16 28"/><path d="M10.5 9.5 L16 4 L21.5 9.5"/><path d="M10.5 22.5 L16 28 L21.5 22.5"/>
+    </g>
+    <g fill="none" stroke-width="3.6" stroke-linecap="round" stroke-linejoin="round" stroke="#ffffff">
+      <path d="M16 4 L16 28"/><path d="M10.5 9.5 L16 4 L21.5 9.5"/><path d="M10.5 22.5 L16 28 L21.5 22.5"/>
+    </g>
+  </g>
+</svg>
diff --git a/cursors/svg/not-allowed.svg b/cursors/svg/not-allowed.svg
new file mode 100644
index 0000000..1680d3b
--- /dev/null
+++ b/cursors/svg/not-allowed.svg
@@ -0,0 +1,6 @@
+<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
+  <circle cx="16" cy="16" r="11" fill="none" stroke="#282a36" stroke-width="9"/>
+  <path d="M8.5 8.5 L23.5 23.5" fill="none" stroke="#282a36" stroke-width="9" stroke-linecap="round"/>
+  <circle cx="16" cy="16" r="11" fill="none" stroke="#ffffff" stroke-width="4.4"/>
+  <path d="M8.5 8.5 L23.5 23.5" fill="none" stroke="#ffffff" stroke-width="4.4" stroke-linecap="round"/>
+</svg>
diff --git a/cursors/svg/ns-resize.svg b/cursors/svg/ns-resize.svg
new file mode 100644
index 0000000..04f42a8
--- /dev/null
+++ b/cursors/svg/ns-resize.svg
@@ -0,0 +1,13 @@
+<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
+  <!-- Vertical double arrow. Hotspot centre -->
+  <g fill="none" stroke="#282a36" stroke-width="8.5" stroke-linecap="round" stroke-linejoin="round">
+    <path d="M16 4 L16 28"/>
+    <path d="M10.5 9.5 L16 4 L21.5 9.5"/>
+    <path d="M10.5 22.5 L16 28 L21.5 22.5"/>
+  </g>
+  <g fill="none" stroke="#ffffff" stroke-width="3.6" stroke-linecap="round" stroke-linejoin="round">
+    <path d="M16 4 L16 28"/>
+    <path d="M10.5 9.5 L16 4 L21.5 9.5"/>
+    <path d="M10.5 22.5 L16 28 L21.5 22.5"/>
+  </g>
+</svg>
diff --git a/cursors/svg/nwse-resize.svg b/cursors/svg/nwse-resize.svg
new file mode 100644
index 0000000..eb25331
--- /dev/null
+++ b/cursors/svg/nwse-resize.svg
@@ -0,0 +1,10 @@
+<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
+  <g transform="rotate(45 16 16)">
+    <g fill="none" stroke-width="8.5" stroke-linecap="round" stroke-linejoin="round" stroke="#282a36">
+      <path d="M16 4 L16 28"/><path d="M10.5 9.5 L16 4 L21.5 9.5"/><path d="M10.5 22.5 L16 28 L21.5 22.5"/>
+    </g>
+    <g fill="none" stroke-width="3.6" stroke-linecap="round" stroke-linejoin="round" stroke="#ffffff">
+      <path d="M16 4 L16 28"/><path d="M10.5 9.5 L16 4 L21.5 9.5"/><path d="M10.5 22.5 L16 28 L21.5 22.5"/>
+    </g>
+  </g>
+</svg>
diff --git a/cursors/svg/pencil.svg b/cursors/svg/pencil.svg
new file mode 100644
index 0000000..1520766
--- /dev/null
+++ b/cursors/svg/pencil.svg
@@ -0,0 +1,7 @@
+<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
+  <!-- Nib points at the hotspot, bottom-left -->
+  <path fill="#ffffff" stroke="#282a36" stroke-width="2.4" stroke-linejoin="round"
+        d="M3.2 28.8 L6.6 20.4 L22.7 4.3 a3.4 3.4 0 0 1 4.8 0 l0.2 0.2
+           a3.4 3.4 0 0 1 0 4.8 L11.6 25.4 Z"/>
+  <path d="M20.4 6.6 L25.4 11.6" fill="none" stroke="#282a36" stroke-width="2"/>
+</svg>
diff --git a/cursors/svg/plus.svg b/cursors/svg/plus.svg
new file mode 100644
index 0000000..02631a5
--- /dev/null
+++ b/cursors/svg/plus.svg
@@ -0,0 +1,7 @@
+<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
+  <!-- cce default pointer: rounded plus, hotspot at centre (16,16) -->
+  <path d="M16 3.5 L16 28.5 M3.5 16 L28.5 16"
+        fill="none" stroke="#282a36" stroke-width="9.5" stroke-linecap="round"/>
+  <path d="M16 3.5 L16 28.5 M3.5 16 L28.5 16"
+        fill="none" stroke="#ffffff" stroke-width="5.5" stroke-linecap="round"/>
+</svg>
diff --git a/cursors/svg/pointer.svg b/cursors/svg/pointer.svg
new file mode 100644
index 0000000..51751a2
--- /dev/null
+++ b/cursors/svg/pointer.svg
@@ -0,0 +1,13 @@
+<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
+  <!-- Pointing hand: long index finger (hotspot at its tip), two folded
+       fingers, thumb swept down-left. One filled path so the outline is
+       continuous at every size. -->
+  <path fill="#ffffff" stroke="#282a36" stroke-width="2.3" stroke-linejoin="round"
+        d="M9.6 5.2 a2.7 2.7 0 0 1 5.4 0 v10.6
+           a2.2 2.2 0 0 1 4.4 .3 v1.0
+           a2.2 2.2 0 0 1 4.3 .5 v.9
+           a2.1 2.1 0 0 1 4.1 .6 v3.3
+           a7.3 7.3 0 0 1 -7.3 7.3 h-4.0
+           a7.1 7.1 0 0 1 -5.4 -2.5 l-4.9 -5.7
+           a2.3 2.3 0 0 1 3.3 -3.2 l2.1 2.0 z"/>
+</svg>
diff --git a/cursors/svg/progress.svg b/cursors/svg/progress.svg
new file mode 100644
index 0000000..84ca245
--- /dev/null
+++ b/cursors/svg/progress.svg
@@ -0,0 +1,7 @@
+<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
+  <!-- Compact default plus (hotspot 11,11) with the progress badge -->
+  <path d="M11 2.5 L11 19.5 M2.5 11 L19.5 11" fill="none" stroke="#282a36" stroke-width="7.5" stroke-linecap="round"/>
+  <path d="M11 2.5 L11 19.5 M2.5 11 L19.5 11" fill="none" stroke="#ffffff" stroke-width="4" stroke-linecap="round"/>
+  <circle cx="23" cy="23" r="8.2" fill="#ffffff" stroke="#282a36" stroke-width="2.2"/>
+  <path d="M23 17.6 a5.4 5.4 0 1 1 -3.8 1.6" fill="none" stroke="#282a36" stroke-width="2.9" stroke-linecap="round"/>
+</svg>
diff --git a/cursors/svg/text.svg b/cursors/svg/text.svg
new file mode 100644
index 0000000..c32ddda
--- /dev/null
+++ b/cursors/svg/text.svg
@@ -0,0 +1,7 @@
+<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
+  <!-- I-beam. Hotspot centre (16,16) -->
+  <path d="M11 4.5 L21 4.5 M16 4.5 L16 27.5 M11 27.5 L21 27.5"
+        fill="none" stroke="#282a36" stroke-width="7.5" stroke-linecap="round"/>
+  <path d="M11 4.5 L21 4.5 M16 4.5 L16 27.5 M11 27.5 L21 27.5"
+        fill="none" stroke="#ffffff" stroke-width="3.4" stroke-linecap="round"/>
+</svg>
diff --git a/cursors/svg/up-arrow.svg b/cursors/svg/up-arrow.svg
new file mode 100644
index 0000000..faf3d56
--- /dev/null
+++ b/cursors/svg/up-arrow.svg
@@ -0,0 +1,8 @@
+<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
+  <g fill="none" stroke-width="8.5" stroke-linecap="round" stroke-linejoin="round" stroke="#282a36">
+    <path d="M16 4 L16 28"/><path d="M9 11 L16 4 L23 11"/>
+  </g>
+  <g fill="none" stroke-width="3.6" stroke-linecap="round" stroke-linejoin="round" stroke="#ffffff">
+    <path d="M16 4 L16 28"/><path d="M9 11 L16 4 L23 11"/>
+  </g>
+</svg>
diff --git a/cursors/svg/vertical-text.svg b/cursors/svg/vertical-text.svg
new file mode 100644
index 0000000..07762be
--- /dev/null
+++ b/cursors/svg/vertical-text.svg
@@ -0,0 +1,8 @@
+<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
+  <g transform="rotate(90 16 16)">
+    <path d="M11 4.5 L21 4.5 M16 4.5 L16 27.5 M11 27.5 L21 27.5"
+          fill="none" stroke="#282a36" stroke-width="7.5" stroke-linecap="round"/>
+    <path d="M11 4.5 L21 4.5 M16 4.5 L16 27.5 M11 27.5 L21 27.5"
+          fill="none" stroke="#ffffff" stroke-width="3.4" stroke-linecap="round"/>
+  </g>
+</svg>
diff --git a/cursors/svg/wait.svg b/cursors/svg/wait.svg
new file mode 100644
index 0000000..f4e49e5
--- /dev/null
+++ b/cursors/svg/wait.svg
@@ -0,0 +1,7 @@
+<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
+  <!-- Busy: ring with a gap and a hand, static (the compositor can drive an
+       animated variant once one is authored) -->
+  <circle cx="16" cy="16" r="11.5" fill="#ffffff" stroke="#282a36" stroke-width="2.6"/>
+  <path d="M16 8.5 L16 16 L21.5 19.5" fill="none" stroke="#282a36" stroke-width="2.8"
+        stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
diff --git a/cursors/svg/zoom-in.svg b/cursors/svg/zoom-in.svg
new file mode 100644
index 0000000..1411e06
--- /dev/null
+++ b/cursors/svg/zoom-in.svg
@@ -0,0 +1,8 @@
+<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
+  <path d="M22 22 L29 29" fill="none" stroke="#282a36" stroke-width="8" stroke-linecap="round"/>
+  <path d="M22 22 L29 29" fill="none" stroke="#ffffff" stroke-width="4" stroke-linecap="round"/>
+  <circle cx="14" cy="14" r="9.5" fill="none" stroke="#282a36" stroke-width="6.5"/>
+  <circle cx="14" cy="14" r="9.5" fill="#ffffff" fill-opacity="0.14" stroke="#ffffff" stroke-width="2.8"/>
+  <path d="M14 9.5 L14 18.5 M9.5 14 L18.5 14" fill="none" stroke="#282a36" stroke-width="5" stroke-linecap="round"/>
+  <path d="M14 9.5 L14 18.5 M9.5 14 L18.5 14" fill="none" stroke="#ffffff" stroke-width="2.4" stroke-linecap="round"/>
+</svg>
diff --git a/cursors/svg/zoom-out.svg b/cursors/svg/zoom-out.svg
new file mode 100644
index 0000000..b5a9eeb
--- /dev/null
+++ b/cursors/svg/zoom-out.svg
@@ -0,0 +1,8 @@
+<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
+  <path d="M22 22 L29 29" fill="none" stroke="#282a36" stroke-width="8" stroke-linecap="round"/>
+  <path d="M22 22 L29 29" fill="none" stroke="#ffffff" stroke-width="4" stroke-linecap="round"/>
+  <circle cx="14" cy="14" r="9.5" fill="none" stroke="#282a36" stroke-width="6.5"/>
+  <circle cx="14" cy="14" r="9.5" fill="#ffffff" fill-opacity="0.14" stroke="#ffffff" stroke-width="2.8"/>
+  <path d="M9.5 14 L18.5 14" fill="none" stroke="#282a36" stroke-width="5" stroke-linecap="round"/>
+  <path d="M9.5 14 L18.5 14" fill="none" stroke="#ffffff" stroke-width="2.4" stroke-linecap="round"/>
+</svg>