Skip to content

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
~/.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.json atomically (temp file plus rename) and then calls the daemon's ReloadConfig method over D-Bus (org.kde.juhradialmx on path /org/kde/juhradialmx/Daemon). Changes apply live, no restart required.
  • If you edit config.json by hand, trigger a reload so the daemon picks it up. Either open and save once in the Settings app, or restart the daemon:
systemctl --user restart juhradialmx-daemon.service
  • 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 internal de_defaults_applied flag).

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.

Top-level structure of config.json

{
  "haptics": { ... },
  "theme": "catppuccin-mocha",
  "blur_enabled": true,
  "buttons": { ... },
  "thumbwheel": { ... },
  "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)
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,
  "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
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.cursorPosChanged and calls TriggerHaptic directly via workspace.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         none                custom

none disables the button. custom reserves the slot for a user-defined action configured in the UI.

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

"thumbwheel": {
  "mode": "off",
  "invert": false,
  "speed": 1
}
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:

  • off and scroll use the wheel's native hardware behaviour and are not diverted, so horizontal scroll works reliably on every compositor.
  • volume and zoom are diverted: each rotation tick is re-injected as Volume Up/Down or Ctrl +/- the number of times set by speed.

Radial menu

Display options

"radial": {
  "minimal_mode": false
}

minimal_mode shows icons only (no slice labels) when true.

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.

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
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
}
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

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:

"theme": "phosphor"

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.