A Rust LD_PRELOAD library that renders a vector cursor directly on the DRM hardware cursor plane. Which allows for a true system cursor rendering without compositor involvement. Should works with any Wayland compositor using atomic modesetting.
Note: This is a somewhat hacky workaround that intercepts DRM calls. Use at your own risk. See Issues/Limitations (out of order) look below.
Many Wayland users has experience cursor issues (me included). Such as, and probably not limited to:
corruption, lag, or invisibility (especially on NVIDIA).
The common fix for this is WLR_NO_HARDWARE_CURSORS=1 which disables hardware cursors entirely,
falling back to software rendering.
The Constellation Cursor takes the opposite approach: it forces the hardware cursor plane to work by intercepting DRM calls and rendering a custom vector cursor directly to the cursor plane. Because, ofcourse it does.
Features:
- Compositor agnostic Which means it should Work with Hyprland, Sway, and others
- Multi-Layer Design Allowing for more detailed and targeted designs
- Transparency Make the cursor, or parts of the cursor, see-through.
- System Cursor Meaning, the same cursor for everything, not per app based.
- Always on top Means the Hardware cursor plane is your daddy... Also, that it is composited by the GPU, not the compositor
- Input passthrough Means it should Work exactly like a normal cursor
- Resolution independent Because vector-based rendering scales cleanly
The library automatically creates and reads a config file at ~/.config/constellation_cursor/cursor.conf. This file is created with default values on first run.
# Constellation Cursor Config
# Edit this file to customize cursor behavior
#
# Changes are detected automatically when you save this file.
# To manually refresh use: touch /tmp/constellation_cursor_refresh
# Cursor size multiplier (default 1.5)
cursor_scale=2.5
# Outline thickness override (0 = use cursor default, 0.5-5.0 for custom)
outline_thickness=5
# Enable fade-out effect when cursor hides (runs in background thread)
# (Buggy)
fade_enabled=false
# Enable fade-in effect when cursor appears
# (Buggy)
fade_in_enabled=false
# Fade speed (1-255, higher = faster fade)
fade_speed=30
# Frosted glass intensity (0-100)
# (Doesn't look great at the moment)
frost_intensity=0
# Smooth hotspot transitions between cursor types
# For positional syncing, is likely to cause issues if not needed
hotspot_smoothing=false
# Threshold for hotspot change detection (pixels)
hotspot_threshold=0
# --- Config Hot-Reload Settings ---
# The cursor library can automatically detect when this file changes.
# Set to false to disable automatic reloading (saves a tiny bit of CPU).
# To re-enable: edit this file to set config_polling=true, then run:
# touch /tmp/constellation_cursor_refresh
config_polling=true
# How often to check for config changes (number of cursor moves between checks)
# Lower = more responsive, Higher = less CPU. Default: 50
config_poll_interval=50
| Setting | Values | Description |
|---|---|---|
cursor_scale |
0.5-10.0 |
Cursor size multiplier (1.5 = default) |
outline_thickness |
0-5.0 |
Outline thickness override (0 = use cursor default) |
fade_enabled |
true/false |
Enable smooth fade-out when cursor hides (runs in background) |
fade_in_enabled |
true/false |
Enable smooth fade-in when cursor appears |
fade_speed |
1-255 |
How fast cursor fades (higher = faster) |
frost_intensity |
0-100 |
Frosted glass effect strength (0 = disabled, 100 = full) |
hotspot_smoothing |
true/false |
Smooth cursor position when hotspot changes |
hotspot_threshold |
0-50 |
Pixel threshold before hotspot smoothing triggers |
config_polling |
true/false |
Enable automatic config reload on file save |
config_poll_interval |
1-1000 |
Cursor moves between config file checks (50 = default) |
# Edit with your preferred editor
nano ~/.config/constellation_cursor/cursor.conf
# Or use sed for quick changes
sed -i 's/fade_enabled=false/fade_enabled=true/' ~/.config/constellation_cursor/cursor.confNote: By default, config changes are detected automatically when you save the file - just move the cursor and the new settings apply. No restart needed.
If you've disabled config_polling, or the automatic config reload does not work, manually trigger a refresh with:
touch /tmp/constellation_cursor_refreshEnvironment variables are read at startup and override config file settings.
| Variable | Description | Example |
|---|---|---|
CONSTELLATION_CURSOR_TYPE |
Initial cursor type | CONSTELLATION_CURSOR_TYPE=pointer |
CONSTELLATION_CURSOR_SCALE |
Initial cursor scale | CONSTELLATION_CURSOR_SCALE=2.0 |
CONSTELLATION_CURSOR_DEBUG |
Enable debug logging | CONSTELLATION_CURSOR_DEBUG=1 |
CONSTELLATION_CURSOR_INFO |
Show version info | CONSTELLATION_CURSOR_INFO=1 |
CONSTELLATION_CURSOR_FADE |
Enable fade effect | CONSTELLATION_CURSOR_FADE=1 |
You can change some of these at runtime with the refresh file. If type or scale files are not applied automatically. You must touch the refresh file to trigger a re-render:
# This WILL NOT change the scale of the cursor:
echo "2.1" > /tmp/constellation_cursor_scale
# This WILL change the cursor:
echo "2.1" > /tmp/constellation_cursor_scale && touch /tmp/constellation_cursor_refreshThe same applies to runtime cursor shape changes
# Change to hourglass/wait cursor
echo "wait" > /tmp/constellation_cursor_type && touch /tmp/constellation_cursor_refresh
# Change back to default arrow
echo "default" > /tmp/constellation_cursor_type && touch /tmp/constellation_cursor_refreshAvailable cursor types:
default/arrow- Standard constellation arrow cursor (will change)pointer/hand- Clickable element cursor (a normal copy/pasted cursor)text/ibeam- Text input cursor (I-beam, if you squint)crosshair/cross- Precision selection cursor (currently off-center for extra precision)wait/loading/busy- Loading/busy cursor (hourglass if you are generous)grab/grabbing- Draggable element cursor (a testament to my superior design capabilities)not-allowed/forbidden/no- Prohibited action cursor (slightly missaligned for hotspot alignment)
Open cursor_designer.html in your browser to create custom cursor shapes.
Drawing Controls:
- Click empty space - Add point
- Drag point - Move it
- Shift+drag point - Convert to Bezier curve
- Right-click point - Delete it
- Ctrl+drag - Box select multiple points
- Multiple points selected + Shift+drag point - Rotate the points
- Delete key - Remove selected points
Multi-Select Features: When multiple points are selected (via Ctrl+drag), a Transform panel appears:
- Rotate slider - Rotate selected points around their center
- Scale slider - Scale selected points
- Apply - Commit the transformation
- Reset - Revert to original positions
Bounds Validation: The designer warns if the first point (cursor hotspot) is not near (0,0). The hotspot should be at the cursor's "tip" for proper click positioning.
- Design your cursor in the designer
- Go to Export tab
- Click Download All Cursors to get Rust code
- Replace the cursor functions in
src/lib.rs - Rebuild:
cargo build --release - Restart your compositor to load the new design
Note: Custom designs require a rebuild and compositor restart. For the moment the runtime control files only switch between the built-in cursor types.
Known Issues
- Slight cursor re-adjustments happen when moving across certain areas this is likely due to the compositor/app changing the hotspot position. Seems to happen because the underlying cursor shape changes.
- The fade out effect currently hinders keyboard input at cursor position when keyboard input is tied to hiding the mouse cursor
- The frost effect AND the fade out/in effects look bad.
- Import function in the designer is currently not loading and displaying the imported design.
- Slight hotspot miss-alignment most likely caused by runtime scale change and/or compositor sync instructions.
Limitations
- May not work with all GPU vendors (tested on my NVIDIA RTX 3080)
- Might conflict with future kernel/driver changes
- LD_PRELOAD approach is likely fragile
- Needs manual cursor signaling to for changing the cursor
As I am currently on Hyprland, I will be using it in the examples. You do not need to use Hyprland for the cursor to work. It bypasses the Wayland compositor by intercepting instructions between the compossitor and the drm cursor plane.
# Clone the repository
git clone https://github.com/Mauitron/the_constellation_cursor
cd the_constellation_cursor
# Build release
cargo build --release
# The library is at:
# target/release/libthe_constellation_cursor.so1. Add the flake input
constellation-cursor = {
url = "github:Mauitron/The_Constellation_Cursor";
inputs.nixpkgs.follows = "nixpkgs";
};2. Add the Home Manager module to your system configuration
modules = [
home-manager.nixosModules.home-manager
inputs.constellation-cursor.homeManagerModules.constellation-cursor
];3. Enable the program in home.nix and configure
programs.constellation-cursor = {
enable = true;
# Required: install the package from the flake
package = inputs.constellation-cursor.packages.${pkgs.system}.default;
# Optional
settings = {
cursor_scale = 1.5;
outline_thickness = 0.0;
fade_enabled = false;
fade_in_enabled = false;
fade_speed = 30;
frost_intensity = 0;
hotspot_smoothing = false;
hotspot_threshold = 0;
config_polling = true;
config_poll_interval = 50;
};
};# Launch your compositor with the cursor. If you are on hyprland, like so:
LD_PRELOAD=/path/to/libthe_constellation_cursor.so HyprlandAdd the above flag to your start command.
LD_PRELOAD=/path/to/the/libthe_constellation_cursor.so Hyprland
or replace 'Hyprland' with whatever compositor you are using
| Variable | Description |
|---|---|
CONSTELLATION_CURSOR_INFO=1 |
Print version and intercepted DRM calls |
CONSTELLATION_CURSOR_DEBUG=1 |
Enable verbose debug logging |
Example:
CONSTELLATION_CURSOR_INFO=1 LD_PRELOAD=./target/release/libthe_constellation_cursor.so hyprlandOutput:
DRM Constellation Cursor v0.1.0
─────────────────────────────────────
Intercepted DRM calls:
ioctl MODE_CURSOR, MODE_CURSOR2
drmModeSetCursor legacy cursor set
drmModeSetCursor2 legacy cursor set v2
drmModeMoveCursor cursor position update
drmModeGetPlane cursor plane detection
drmModeAtomicAddProperty FB_ID replacement
Environment variables:
CONSTELLATION_CURSOR_DEBUG=1 verbose logging
CONSTELLATION_CURSOR_INFO=1 show this info
Open cursor_designer.html in your browser to create custom cursor shapes:
- Click on the canvas to add polygon points
- Right-click to undo
- Adjust colors, scale, and shadow
- Go to Export tab and copy the Rust code
- Replace the
render_arrow_cursor()function insrc/lib.rs - Rebuild
The designer includes these presets:
- Arrow (default)
- Pointer
- Crosshair
- I-Beam
- Hand
- Wait/Loading
┌─────────────────────────────────────────────────────────────┐
│ Your Compositor │
│ (Hyprland, Sway, etc.) │
│ │
│ Sets cursor image ──────┐ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ libthe_constellation_cursor.so │ │
│ │ │ │
│ │ 1. Intercepts drmModeAtomicAddProperty │ │
│ │ 2. Detects cursor plane via "type" property │ │
│ │ 3. Creates our own framebuffer with vector cursor │ │
│ │ 4. Replaces compositor's FB_ID with ours │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ DRM Kernel API │
│ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────┐
│ Hardware Cursor Plane │
│ (Always on top) │
└─────────────────────────┘
- Linux with DRM/KMS
- Wayland compositor using atomic modesetting, legacy mode untested
- Rust 1.70+ (for building)
| Compositor | Status |
|---|---|
| Hyprland | ✅ Working |
| Sway | 🔄 Should work |
| KWin | ❓ Untested |
| Mutter | ❓ Untested |
Enable debug mode to see what's happening:
CONSTELLATION_CURSOR_DEBUG=1 LD_PRELOAD=... hyprland 2>&1 | tee cursor.logLook for:
- "Captured DRM fd" would mean library is intercepting calls
- "Detected cursor plane" means it found the cursor plane
- "Created cursor buffer" gave birth to our buffer
- "Replacing FB_ID" managed to successfully replace cursor image
The default is 256x256 buffer with 1.5x scale (~32px cursor). Edit src/lib.rs:
let scale = 2.0; // Larger cursorWith some prophet like guesswork, I suspect some compositors may not handle the FB replacement gracefully. If that is the case try:
- Update your compositor to the latest version
- Check if your GPU driver supports the required DRM features
- File an issue with debug logs and I'll get around to it eventually
If you like The Constellation Cursor and want to support the project, please consider feeding me some Pizza 🍕
- Fork the repository
- Create a feature branch
- Make your changes
- Run
cargo build --releaseto verify - Submit a pull request
- Await JUDGEMENT!
Extra bonus points if you fix my awesome cursor designs... Or just create your own and share them!
MIT License - see LICENSE for details.
- Inspired by my unending frustration over the Wayland cursor approach and the need for resolution-independent cursors
- Part of the Starwell::Constellation project
Circumventing compositor dictatorship since 2025
