Configuration¶
JuhRadial MX is configured through plain JSON files under your XDG config directory. The Settings app (GTK4) writes these files for you, but every value is human readable and safe to edit by hand. This page is the full reference: where the files live, every top-level section and its key fields, the theme list, the per-application profile format, and autostart.
See also: Features for what each setting does in practice, Installation for first run, and Troubleshooting if a change does not take effect.
File locations¶
All configuration lives in ~/.config/juhradial/ (or $XDG_CONFIG_HOME/juhradial/ when that variable is set).
| File / directory | Written by | Purpose |
|---|---|---|
config.json |
Settings app, daemon | Main configuration: haptics, buttons, thumb-wheel, theme, scroll, flow, gaming, app and device settings |
profiles.json |
Settings app, daemon | Per-application radial layouts and per-app hardware overrides |
macros/<uuid>.json |
Settings app | One file per saved macro |
icons/ |
Settings app | Application icons imported for radial slices and quick links |
themes/ |
You | Custom overlay themes (see Themes) |
flow_keys/ |
Overlay | Flow pairing identity; never exported |
plugins/<name>/plugin.json |
You | Plugin actions for radial slices (see Plugins); not part of backups |
~/.config/autostart/juhradial-mx.desktop |
Settings app | Login autostart entry (created/removed by the Start at Login toggle) |
Note
The daemon reads haptics, theme, blur_enabled, buttons, and thumbwheel from config.json. The remaining sections (scroll, pointer, flow, gaming, app, device_mode, desktop_environment, language, radial, radial_menu) are consumed by the Settings app and overlay and applied through helper scripts or D-Bus. Unknown keys are ignored, so the two consumers coexist in one file.
How configuration is applied¶
- The Settings app writes
config.jsonatomically (temp file plus rename) and then calls the daemon'sReloadConfigmethod over D-Bus (org.kde.juhradialmxon path/org/kde/juhradialmx/Daemon). Changes apply live, no restart required. - If you edit
config.jsonby hand, trigger a reload so the daemon picks it up. Either open and save once in the Settings app, or restart the daemon:
- Missing fields fall back to defaults, so a minimal
{}file is valid. On first run the Settings app auto-detects your desktop environment and fills in environment-appropriate commands for the radial slices (controlled by the internalde_defaults_appliedflag).
Warning
The daemon writes only VOLATILE HID++ state to the device (no onboard-memory writes). Diverts and hardware overrides are re-applied on reconnect, on radio wake (power switch / sleep, where the receiver's device nodes never disappear), and on ReloadConfig (which re-diverts the gesture and haptic buttons too), so the mouse comes back to your configured state automatically.
Backup and restore¶
Settings → Settings → Backup exports everything under ~/.config/juhradial/ that describes your setup into one zip file, and imports such a file again on this machine or another one. The daemon binary offers the same two commands, which is what the Settings buttons run:
The archive holds manifest.json (format version, app version, creation time, file list), config.json, profiles.json, and every file in macros/, icons/ and themes/. Flow pairing keys (flow_keys/), UI state and scratch files (.tmp, .bak, .bad) stay on the machine.
Import validates before it writes: the manifest must be JuhRadial's and of a format this build understands, every entry must be one of the files above (absolute paths, .., nested directories and anything else are refused, so a tampered archive cannot write elsewhere), and every JSON entry must parse as an object. Only then does it keep the current config.json and profiles.json as config.json.bak and profiles.json.bak, write each file atomically, and ask a running daemon to reload its configuration and macro triggers. Files on disk that the archive does not mention (extra macros or icons) are left in place.
Top-level structure of config.json¶
{
"haptics": { ... },
"theme": "catppuccin-mocha",
"blur_enabled": true,
"buttons": { ... },
"thumbwheel": { ... },
"keyboard": { ... },
"radial": { "minimal_mode": false },
"radial_menu": { ... },
"scroll": { ... },
"pointer": { ... },
"flow": { ... },
"gaming": { ... },
"app": { ... },
"device_mode": "auto",
"desktop_environment": "auto",
"language": "system"
}
| Section | Type | What it controls |
|---|---|---|
haptics |
object | Haptic feedback patterns and debounce timing |
theme |
string | Active UI / overlay theme (see Themes) |
blur_enabled |
bool | Overlay blur effect (auto-disabled on slow GPUs) |
buttons |
object | Physical button action assignments |
thumbwheel |
object | Thumb-wheel behaviour (volume / scroll / zoom / off) |
keyboard |
object | Keyboard support, beta and off by default (generic remap table, MX Keys S battery and backlight) |
radial |
object | Radial menu display options (minimal_mode) |
radial_menu |
object | The 8 radial slices, easy-switch options |
scroll |
object | Scroll direction, smoothness, SmartShift |
pointer |
object | Pointer speed, DPI, acceleration |
flow |
object | Multi-machine edge flow (created once configured) |
gaming |
object | Gaming mode and DPI profiles (created once configured) |
app |
object | Tray icon and autostart toggles |
device_mode |
string | UI layout: auto, logitech, or generic |
desktop_environment |
string | DE for default commands: auto, kde, gnome, cosmic, generic |
language |
string | UI language (system or a locale code) |
Haptics¶
"haptics": {
"enabled": true,
"intensity": 70,
"default_pattern": "subtle_collision",
"per_event": {
"menu_appear": "damp_state_change",
"slice_change": "subtle_collision",
"confirm": "sharp_state_change",
"invalid": "angry_alert",
"window_switch": "subtle_collision",
"monitor_switch": "subtle_collision"
},
"debounce_ms": 20,
"slice_debounce_ms": 20,
"reentry_debounce_ms": 50,
"window_switch_enabled": true,
"monitor_switch_enabled": true
}
| Field | Type | Default | Meaning |
|---|---|---|---|
enabled |
bool | true |
Master toggle for haptic feedback |
intensity |
int | 70 |
Master strength 0-100. 0 silences every pulse. The MX Master 4's firmware waveforms have no amplitude byte, so on that mouse any non-zero value plays at native strength; legacy force-feedback devices scale their pulse amplitude by it. |
default_pattern |
string | subtle_collision |
Fallback waveform when no per-event pattern is set |
per_event.menu_appear |
string | damp_state_change |
Pulse when the radial menu opens |
per_event.slice_change |
string | subtle_collision |
Pulse when hovering a different slice |
per_event.confirm |
string | sharp_state_change |
Pulse when selecting an action |
per_event.invalid |
string | angry_alert |
Pulse for a blocked or invalid action |
per_event.window_switch |
string | subtle_collision |
Pulse when the focused application window changes |
per_event.monitor_switch |
string | subtle_collision |
Pulse when the cursor moves to a different physical monitor |
debounce_ms |
int | 20 |
Minimum milliseconds between any two pulses |
slice_debounce_ms |
int | 20 |
Minimum milliseconds between slice-change pulses |
reentry_debounce_ms |
int | 50 |
Window that suppresses a duplicate pulse when the cursor re-enters the same slice |
window_switch_enabled |
bool | true |
Enable the app-switch pulse independently of the radial menu's own events |
monitor_switch_enabled |
bool | true |
Enable the monitor-switch pulse independently of the radial menu's own events |
The app-switch pulse fires whenever the daemon's window tracker (used for per-application profiles) sees the focused window's class change — it runs whenever window tracking is available (KDE, Hyprland, X11), whether or not any per-app profiles are configured, and is capped at one pulse per distinct app switch, not per window-focus event within the same app.
The monitor-switch pulse has two independent sources, chosen per desktop:
- KDE — a persistent KWin script, installed once alongside the active-window tracking script, connects to
workspace.cursorPosChangedand callsTriggerHapticdirectly viaworkspace.screenAt()whenever the screen under the cursor changes. This is KWin-native and always sees the live cursor position. - Hyprland, GNOME, X11 — an ambient timer in the overlay process (running continuously, independent of the radial menu) polls the cursor position every 400ms and compares which physical monitor it's on against the previous reading, using each desktop's own live cursor source (Hyprland IPC, the GNOME Shell helper extension, or native X11).
KDE deliberately does not use the overlay's ambient timer: XWayland only tracks the cursor while it's over an XWayland window, so on Wayland outside of an open radial menu the timer's QCursor.pos()/XQueryPointer reads return a stale, frozen position almost all the time (confirmed live — two successive queries returned the identical stale point). The same staleness affects COSMIC and niri, which aren't given a native alternative yet, so a crossing there may occasionally be missed or delayed.
Pattern names are MX Master 4 HID++ waveform IDs (for example subtle_collision, damp_state_change, sharp_state_change, angry_alert). Pick from the patterns offered in the HAPTIC FEEDBACK page of the Settings app.
Buttons¶
Each physical control maps to one action. Defaults preserve the mouse's native behaviour.
"buttons": {
"gesture": "virtual_desktops",
"thumb": "radial_menu",
"middle": "middle_click",
"shift_wheel": "smartshift",
"forward": "forward",
"back": "back",
"horizontal_scroll": "scroll_left_right"
}
| Field | Physical control | Default action |
|---|---|---|
gesture |
Gesture button (thumb pad) | virtual_desktops |
thumb |
Actions-ring / haptic button | radial_menu |
middle |
Scroll-wheel click | middle_click |
shift_wheel |
Mode-shift button below the wheel | smartshift |
forward |
Upper thumb button | forward |
back |
Lower thumb button | back |
horizontal_scroll |
Thumb wheel | scroll_left_right |
Available action values¶
Any button field accepts one of these snake_case values:
radial_menu virtual_desktops middle_click back
forward copy paste undo
redo screenshot smartshift scroll_left_right
volume_up volume_down play_pause mute
zoom_in zoom_out show_desktop switch_desktop_left
switch_desktop_right task_switcher close_window lock_screen
calculator maximize_window minimize_window none
custom
none disables the button. custom reserves the slot for a user-defined action configured in the UI.
Other controls (buttons.controls)¶
Mice expose more remappable controls than the named slots above (side buttons on an MX Anywhere, the DPI switch on an MX Vertical). The daemon enumerates them from the HID++ REPROG_CONTROLS_V4 feature and publishes the list on D-Bus as ListControls (a JSON array with each control's id, name and capability bits). Any divertable control can carry an action keyed by its control id:
Keys are the control id in hex (0x00D7) or decimal. A control with an action is diverted to the daemon; none (or removing the key and reloading) hands it back to the mouse. Settings → Buttons lists these under "Other controls" whenever the connected mouse reports any.
Directional gestures¶
The gesture button can run a different action per drag direction. Hold it, move the mouse, release: the dominant axis picks the action, and a press that moves less than threshold_px is a click that runs buttons.gesture (or click, when set). Off by default; Settings → Buttons → Directional gestures has the switch, the pickers, and the drag distance.
The four corners are optional diagonals. While up_left, up_right, down_left and down_right are all none, a drag is classified four ways; once one of them has an action the plane splits into eight 45 degree sectors, and a corner still at none hands its drags to the nearest axis.
A direction set to custom runs the custom action saved under buttons.custom.gesture_<direction> (gesture_up, gesture_down_left, ...); a plain press set to custom uses gesture_click, or the button's own gesture slot when click is unset. The Settings picker offers Custom on every direction and opens the same editor as the buttons.
"buttons": {
"gesture": "virtual_desktops",
"gesture_directions": {
"enabled": true,
"up": "show_desktop",
"down": "task_switcher",
"left": "switch_desktop_left",
"right": "switch_desktop_right",
"up_right": "maximize_window",
"threshold_px": 15
}
}
| Field | Meaning | Default |
|---|---|---|
enabled |
Turn directional gestures on. Absent or false keeps the single-action behaviour exactly as before. |
false |
up, down, left, right |
Action for a drag in that direction. Any button action the picker offers (not radial_menu, dpi_shift, or the thumb wheel's scroll_left_right), including custom. |
none |
up_left, up_right, down_left, down_right |
Optional diagonal actions; see above for how they change the classification. | none |
click |
Action for a press without a drag. Omit it to keep using buttons.gesture. |
unset |
threshold_px |
Movement below this many sensor counts, as if the mouse ran at 1000 DPI, counts as a click (15 is about 0.4 mm). The daemon scales it by the DPI the mouse runs at, so one value means one physical flick whether the sensor is at 800 or 4000. Thumb-held drags are short, especially forward and back, so keep it low. | 15 |
The gesture button is diverted with HID++ raw XY while directional gestures are on: the mouse reports the drag over HID++ and the pointer stays where it was, so a gesture never nudges the cursor off what you were pointing at. On a mouse whose gesture button does not offer raw XY the drag is measured from the mouse's relative motion instead (and the pointer moves with it). Either way it works on every compositor, and a directional press never opens the radial menu. This applies to the HID++-diverted gesture button (the normal state on the MX Master 4, 3S and 3); it is not available on the evdev-only fallback path.
Warning
The gesture and actions-ring (thumb) buttons are always diverted to the daemon. The back, forward, middle, and shift-wheel buttons are only HID++-diverted when you reassign them away from their native default. Leaving one at its default keeps the firmware behaviour intact (and reassigning back to the default releases the divert without a reconnect). Reassigning horizontal_scroll is recorded in the schema but the thumb wheel's native scroll is handled by the thumbwheel section below.
Thumb-wheel¶
| Field | Type | Default | Meaning |
|---|---|---|---|
mode |
string | off |
off, volume, scroll, or zoom |
invert |
bool | false |
Reverse rotation direction (applied in software) |
speed |
int | 1 |
Action repeats per rotation tick, clamped to 1 to 8 |
How modes behave:
offandscrolluse the wheel's native hardware behaviour and are not diverted, so horizontal scroll works reliably on every compositor.volumeandzoomare diverted: each rotation tick is re-injected as Volume Up/Down or Ctrl +/- the number of times set byspeed.
Per-device overrides (devices)¶
Every setting in this file applies to whichever mouse is connected. To keep separate settings per mouse (two mice on one machine, or a travel mouse with its own button map), add a devices block keyed by the mouse's unit id, which Settings → Devices shows as "Unit 0x…" (GetUnitId on D-Bus). Any subset of the top-level keys is allowed and is deep-merged over the top-level values while that mouse is connected; the file itself is never rewritten.
"devices": {
"0x1234ABCD": {
"buttons": { "back": "undo", "forward": "redo" },
"thumbwheel": { "mode": "zoom" }
}
}
Single-mouse configs without a devices block behave exactly as before.
Keyboard (beta)¶
Off by default and inert until enabled: with the section absent or every switch false the daemon never opens, grabs or talks to a keyboard, so a mouse-only install is unaffected.
| Field | Type | Default | Meaning |
|---|---|---|---|
enabled |
bool | false |
Master switch for the generic remap path. |
remap |
object | {} |
Source evdev key code to target key code, as stringified integers ("58": 29 is CapsLock to Left Ctrl). Only when enabled is true and the table is non-empty is the first physical keyboard grabbed and forwarded through a virtual keyboard with these codes rewritten. |
mx_keys.enabled |
bool | false |
Let the daemon talk HID++ to an MX Keys S for battery readback (GetKeyboardBattery), presence from the receiver's pairing table (GetKeyboardPaired, true even while the keyboard's radio sleeps) and the backlight (SetKeyboardBacklight, only sent on an explicit request; verified on an MX Keys S). |
ListKeyboardKeys returns the evdev key codes of the first keyboard for a remap picker without grabbing it.
MX Keypad (keypad)¶
| Key | Type | Default | Meaning |
|---|---|---|---|
enabled |
bool | true |
Drive a connected MX Keypad (046d:c354) |
pages |
array | [] |
Pages of nine keys (name, keys, optional apps window classes that bring the page up); Settings → MX Keypad edits them |
active_page |
int | 0 |
The page shown now |
brightness |
int | unset | Panel brightness 1 to 100; unset leaves the keypad's own level |
dim_on_lock |
bool | true |
While the screen is locked the keys go blank and do nothing (a text key never types into the lock screen). Follows logind's LockedHint, which KDE Plasma and GNOME set; on desktops that do not set it the keypad stays as it is |
Radial menu¶
Display options¶
"radial": {
"minimal_mode": false,
"auto_fit": true,
"outer_radius": 150,
"inner_radius": 45,
"icon_scale": 1.0
}
| Key | Type | Default | Meaning |
|---|---|---|---|
minimal_mode |
bool | false |
Simplified wheel: the action icons without the ring (also the "Simplified" switch in the Settings header) |
auto_fit |
bool | unset | Automatic sizing: the ring keeps its default size, scaled to the monitor it opens on, and the three keys below stay in the file but are ignored. Unset means on, unless one of the three keys below is set (a ring sized before 0.4.5 keeps its size) |
outer_radius |
int or null |
null (150) |
Ring radius in pixels, 80 to 250. Icons, submenus and the centre label scale with it |
inner_radius |
int or null |
null (45) |
Centre zone radius in pixels, 20 up to 30 less than the outer radius |
icon_scale |
number or null |
null (1.0) |
Slice icon size on top of the ring scale, 0.6 to 1.6 |
null (or a missing key) uses the default. Settings → Settings → Radial menu sets all of them (Automatic, Ring size, Center zone, Icon size, Default sizes).
Slices¶
The 8-way ring is defined under radial_menu.slices. Each slice is an object:
{
"label": "Play/Pause",
"action_id": "play_pause",
"type": "exec",
"command": "playerctl play-pause",
"color": "green",
"icon": "media-playback-start-symbolic"
}
| Slice field | Meaning |
|---|---|
label |
Text shown on the slice |
action_id |
Stable identifier used for default-command mapping |
type |
How the slice acts: exec, shortcut, emoji, settings, submenu, or none |
command |
For exec: a shell command. For shortcut: a key combo such as ctrl+c. Empty for other types |
color |
A theme color name (green, yellow, red, blue, mauve, pink, sapphire, teal, and so on) |
icon |
A freedesktop symbolic icon name, an emoji, or a path to .png / .svg / .ico |
Tip
The Settings app's slice editor has a Pick Application… button (for exec slices) that lists installed applications the same way your desktop menu would. Picking one fills command with its launch command and icon with a path to a cached copy of its real icon, under ~/.config/juhradial/icons/.
Slice type values:
| Type | Behaviour |
|---|---|
exec |
Run command as a shell command |
shortcut |
Inject the key combination in command |
emoji |
Open the system emoji picker |
settings |
Open the JuhRadial MX Settings app |
submenu |
Open a nested ring |
none |
Do nothing |
Tip
On first run the slice commands are auto-filled for your desktop environment. For example, the Screenshot slice becomes spectacle on KDE, gnome-screenshot --interactive on GNOME, cosmic-screenshot on COSMIC, and flameshot gui on generic desktops. Set desktop_environment to pin a specific mapping.
Submenus¶
A submenu-type slice carries up to four entries under submenu, each either a link or an app:
"submenu": [
{ "label": "Docs", "url": "https://docs.example.com" },
{ "label": "Files", "type": "exec", "command": "dolphin", "icon": "/home/user/.config/juhradial/icons/org.kde.dolphin.desktop.png" }
]
| Entry field | Meaning |
|---|---|
label |
Text shown for the submenu item |
url |
For link entries: opened in the default browser |
type |
Set to exec for an app entry (omit for a link entry) |
command |
For exec entries: the launch command |
icon |
For exec entries: a path to the app's cached icon |
The Settings app's submenu editor has an App… button per row for picking an installed application, filling command/icon the same way the slice-level app picker does; a Clear button switches the row back to a plain link. Leaving all four rows empty falls back to the default AI-assistant links.
Easy-switch¶
"radial_menu": {
"easy_switch_shortcuts": false,
"easy_switch_host_os": ["linux", "unknown", "unknown"]
}
| Field | Meaning |
|---|---|
easy_switch_shortcuts |
Enable easy-switch slot shortcuts in the ring |
easy_switch_host_os |
Host OS label per channel (1, 2, 3): linux, windows, mac, or unknown |
Scroll and pointer¶
"scroll": {
"natural": false,
"smooth": true,
"smartshift": true,
"smartshift_threshold": 50,
"mode": "smartshift"
},
"pointer": {
"speed": 10,
"acceleration": true
}
scroll field |
Type | Default | Meaning |
|---|---|---|---|
natural |
bool | false |
Invert scroll direction |
smooth |
bool | true |
Hi-res (free-spin) scrolling versus ratchet clicks |
smartshift |
bool | true |
Auto-disengage the ratchet on fast flicks |
smartshift_threshold |
int | 50 |
Flick force (1 to 100) that triggers free-spin |
mode |
string | smartshift |
Scroll mode selector |
speed |
int | (UI) | Scroll speed, written by the Point & Scroll page |
pointer field |
Type | Default | Meaning |
|---|---|---|---|
speed |
int | 10 |
Pointer speed |
acceleration |
bool | true |
Enable pointer acceleration |
dpi |
int | (UI) | Explicit DPI, written when set in the Point & Scroll page. The daemon re-applies it on every (re)connect; remove the key to leave the DPI to the mouse or to another tool (Solaar, libratbag), since only keys present in the file are ever written to the device |
accel_profile |
string | adaptive |
Acceleration profile when written by the UI |
Flow¶
The flow section appears once you configure multi-machine edge flow on the FLOW page. Keys are written on demand:
| Field | Type | Default | Meaning |
|---|---|---|---|
enabled |
bool | false |
Master flow toggle |
direction |
string | right |
Edge that crosses to the other machine (left / right) |
share_clipboard |
bool | true |
Sync clipboard across machines |
edge_trigger |
bool | true |
Cross when the pointer hits the screen edge |
edge_sensitivity |
int | 50 |
Edge trigger sensitivity |
monitor |
string | "" |
Monitor whose edge triggers flow (empty = any) |
hide_indicator |
bool | false |
Hide the on-screen flow indicator |
extend_edge_zone |
bool | false |
Enlarge the edge activation zone |
Gaming¶
The gaming section appears once you open the GAMING page. DPI profiles let you switch precision quickly.
"gaming": {
"enabled": false,
"suppress_overlay": true,
"active_dpi_profile": 1,
"dpi_profiles": [
{ "name": "Precision", "dpi": 400, "color": "blue" },
{ "name": "Normal", "dpi": 1000, "color": "green" },
{ "name": "Fast", "dpi": 3200, "color": "red" }
]
}
| Field | Type | Default | Meaning |
|---|---|---|---|
enabled |
bool | false |
Gaming mode master toggle |
suppress_overlay |
bool | true |
Hide the radial overlay while gaming |
active_dpi_profile |
int | 1 |
Index into dpi_profiles |
dpi_profiles |
array | (3 built-ins) | Each entry has name, dpi, and color |
Device mode and desktop environment¶
| Field | Values | Meaning |
|---|---|---|
device_mode |
auto, logitech, generic |
UI layout only. The daemon always runs both the MX and generic input loops, so both mice work regardless. generic hides the Logitech-only tabs (HAPTIC FEEDBACK, EASY-SWITCH, FLOW) |
desktop_environment |
auto, kde, gnome, cosmic, generic |
Chooses default commands for radial slices |
language |
system or a locale code |
UI language |
App settings and autostart¶
"app": {
"start_at_login": true,
"show_tray_icon": true,
"suggest_profiles": true,
"profile_prompted": ["gimp"]
}
| Field | Type | Default | Meaning |
|---|---|---|---|
start_at_login |
bool | true |
Launch the overlay/settings helper at login |
show_tray_icon |
bool | true |
Show the system tray icon |
suggest_profiles |
bool | true |
Offer to create an app profile the first time an app without one is focused (Settings → App profiles) |
profile_prompted |
list of strings | [] |
Window classes already offered a profile; each is asked about once (newest 200 kept) |
Toggling Start at Login in the SETTINGS page creates or removes ~/.config/autostart/juhradial-mx.desktop:
[Desktop Entry]
Type=Application
Name=JuhRadial MX
Comment=Radial menu for Logitech MX Master
Exec=/usr/local/bin/juhradial-mx
Icon=juhradial-mx
Terminal=false
Categories=Utility;
X-GNOME-Autostart-enabled=true
The background daemon runs separately as a systemd user service installed at setup time:
systemctl --user enable --now juhradialmx-daemon.service
systemctl --user status juhradialmx-daemon.service
Themes¶
Set the active theme with the theme field, or pick it from the SETTINGS page (changes apply to both the overlay and the Settings UI). Use the key from the left column as the theme value.
| Theme key | Display name | Style |
|---|---|---|
phosphor |
PHOSPHOR | Dark, obsidian with a single mint accent |
juhradial-mx |
JuhRadial MX | Dark, vibrant cyan accents |
catppuccin-mocha |
Catppuccin Mocha | Dark, pastel lavender accents |
nord |
Nord | Dark, arctic blue palette |
dracula |
Dracula | Dark, purple accents |
catppuccin-latte |
Catppuccin Latte | Light, pastel |
github-light |
GitHub Light | Light, clean |
solarized-light |
Solarized Light | Light, precision palette |
3d-blossom |
Pearl Blossom (3D) | Dark, pre-rendered rose-gold wheel |
3d-neon |
Neon Sci-Fi (3D) | Dark, cyberpunk neon wheel |
3d-pastel |
Dark Ember (3D) | Dark, golden ember wheel |
3d-crystal |
Golden Classic (3D) | Dark, ornamental golden wheel |
The 3d-* themes render the ring from a pre-baked image; the others are drawn as vectors. To switch by hand:
Note
If theme is missing, set to system, or names an unknown theme, the overlay falls back to phosphor. The default config.json written on install uses catppuccin-mocha. The companion blur_enabled flag controls the overlay's background blur and may be auto-disabled on slow GPUs.
Per-application profiles (profiles.json)¶
profiles.json holds per-app radial layouts and per-app hardware overrides, matched against the focused window's resource class (case-insensitive). The Settings app writes a flat shape: top-level keys are application names, plus one hardware map.
{
"default": {
"name": "default",
"app_class": "default",
"slices": []
},
"Firefox": {
"name": "Firefox",
"app_class": "Firefox",
"slices": [ { "label": "Copy", "action_id": "copy", "type": "shortcut", "command": "ctrl+c" } ]
},
"hardware": {
"Firefox": {
"dpi": 1200,
"smartshift": { "enabled": true, "threshold": 40 },
"hires": true,
"thumbwheel": "scroll"
}
}
}
Per-app radial entry¶
| Field | Meaning |
|---|---|
name |
Display name of the profile |
app_class |
Window resource class to match |
slices |
Up to 8 slice objects (same shape as radial_menu.slices); padded to 8 |
Per-app hardware override¶
Each key under hardware is an application name mapping to a hardware profile. Every field is optional: only the fields present are applied while that app is focused, and each maps to a volatile HID++ setter. A missing field means "leave unchanged".
| Field | Type | Maps to |
|---|---|---|
dpi |
int | Pointer DPI (ADJUSTABLE_DPI) |
smartshift |
object | { "enabled": bool, "threshold": int } (HiResScroll SmartShift) |
hires |
bool | Hi-res (free-spin) vs ratchet scroll |
thumbwheel |
string | off, volume, scroll, or zoom (divert derived: any non-off mode diverts) |
buttons |
object | Per-button action overrides keyed by gesture / thumb / middle / back / forward / shift_wheel (recorded in the schema; applied via config, not as device state) |
Note
The daemon also accepts a structured form with a top-level version, a profiles array, and a hardware map (schema v2). Older v1 files (no hardware map) load unchanged and are migrated automatically. The flat UI shape and the structured shape are both read; the built-in default profile is always present even if the file omits it.
Macros¶
Saved macros live one-per-file under ~/.config/juhradial/macros/<uuid>.json, each holding the macro's id, name, and recorded steps. Manage them from the MACROS page rather than editing the directory by hand.
Minimal example¶
A complete, valid config.json only needs the fields you want to change; everything else falls back to defaults.
{
"theme": "phosphor",
"blur_enabled": true,
"buttons": {
"gesture": "radial_menu",
"thumb": "virtual_desktops"
},
"thumbwheel": { "mode": "volume", "invert": false, "speed": 2 },
"haptics": { "enabled": true, "default_pattern": "subtle_collision" }
}
After editing by hand, reload with the Settings app or systemctl --user restart juhradialmx-daemon.service. If a value still does not take effect, check Troubleshooting and Compositor-Support.