SVG icon set
git clone https://git.lucas.co/cce-icons.git
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>