Skip to content

Commit b553a66

Browse files
authored
feat(config): configurable primary modifiers (#1854)
1 parent 8be97ba commit b553a66

25 files changed

Lines changed: 526 additions & 86 deletions

‎configs/default-config.toml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ hide_overlay_in_screen_share = false
2020
# exec_shell = "/bin/bash" # e.g. "/bin/dash", "/bin/zsh"
2121
# exec_shell_args = ["-lc"] # last argument receives the command string
2222

23+
# What a Primary hotkey means on each platform, so one file serves every machine
24+
[general.primary_modifier]
25+
darwin = "Cmd"
26+
linux = "Ctrl"
27+
windows = "Ctrl"
28+
2329
# Theme palette
2430
# See https://github.com/y3owk1n/neru/blob/main/docs/reference/configuration.md#theme
2531
[theme.light]

‎docs/guide/getting-started.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ edits.
6060
Neru ships a launcher for every mode except monitor select, which is disabled
6161
and unbound, and the modes you declare yourself. Hints is
6262
`Primary+Shift+Space`, where `Primary` is `Cmd` on macOS and `Ctrl` on
63-
Windows. The [default hotkeys](../reference/configuration.md#global-hotkeys)
63+
Windows by default. The [default hotkeys](../reference/configuration.md#global-hotkeys)
6464
list the rest. Go on to [Try it](#try-it).
6565

6666
### Linux

‎docs/reference/configuration.md‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ see [Getting started](../guide/getting-started.md#bind-your-first-hotkey).
7575
| `Ctrl` | `Control` |
7676
| `Alt` | `Option` |
7777
| `Shift` | |
78-
| `Primary` | `Cmd` on macOS, `Ctrl` on Linux/Windows |
78+
| `Primary` | Set per platform by [`general.primary_modifier`](#general), `Cmd` on macOS and `Ctrl` on Linux/Windows by default |
7979

8080
| Category | Keys |
8181
| ---------- | ----------------------------------------------------------------------------------------------------- |
@@ -429,10 +429,25 @@ passthrough_unbounded_keys = true
429429
| `passthrough_unbounded_keys_blacklist` | array | `[]` | Shortcuts to keep consumed when passthrough is on |
430430
| `exec_shell` | string | `"/bin/bash"` | Shell binary used for `exec` hotkey commands. Windows default below |
431431
| `exec_shell_args` | array | `["-lc"]` | Shell arguments, with the command string appended last. Windows default below |
432+
| `primary_modifier` | table | see below | The modifier `Primary` stands for on each platform. See below |
432433

433434
On Windows, `exec_shell` defaults to `%SystemRoot%\System32\cmd.exe`
434435
(usually `C:\Windows\System32\cmd.exe`) and `exec_shell_args` to `["/c"]`.
435436

437+
`primary_modifier` names the modifier a `Primary` hotkey uses, with one entry
438+
per platform, so one config file can serve every machine:
439+
440+
```toml
441+
[general]
442+
primary_modifier = { darwin = "Cmd", linux = "Super", windows = "Ctrl" }
443+
```
444+
445+
Each entry is one of `Cmd`, `Ctrl` or `Alt`, or an alias of one from the
446+
[modifier table](#global-hotkeys). An entry left out keeps its default (`Cmd`
447+
on macOS, `Ctrl` on Linux and Windows). Platforms other than macOS and Windows
448+
read `linux`. A reload or `neru config set` applies a change at once, to every
449+
`Primary` hotkey in every table.
450+
436451
`kb_layout_to_use` decides which physical key a binding answers. Empty picks a
437452
layout with Latin letters, so bindings stay put while you type in another
438453
language. To force one, use your platform's name for it:

‎docs/reference/platform-support.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ implements it is in the contributor docs.
55

66
## Platform status
77

8-
| Platform | Status | Primary modifier | Ships as |
8+
| Platform | Status | Default `Primary` | Ships as |
99
| -------- | ---------- | ---------------- | --------------------------------- |
1010
| macOS 14+ | **Stable** | `Cmd` | `Neru.app`, codesigned |
1111
| Linux | **Beta** | `Ctrl` | Binary and install script |

‎internal/adapter/eventtap/linux/global_hotkey_keys.go‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ package linux
55
import (
66
"sort"
77
"strings"
8+
9+
"github.com/y3owk1n/neru/internal/domain/action"
810
)
911

1012
// Canonicalizes hotkey chord strings into a stable, order-independent signature
@@ -87,13 +89,16 @@ func chordSignature(chord string, baseKey func(string) string) string {
8789
}
8890

8991
// canonicalModifierToken maps the many modifier spellings Neru and the various
90-
// platforms use down to one of four canonical tokens. "Primary" resolves to
91-
// ctrl here because it is already Ctrl on Linux by the time hotkeys register.
92+
// platforms use down to one of four canonical tokens. Hotkeys register with
93+
// Primary already resolved. A "primary" that still reaches this function
94+
// resolves to the modifier general.primary_modifier set.
9295
func canonicalModifierToken(token string) string {
9396
switch strings.ToLower(strings.TrimSpace(token)) {
97+
case "primary":
98+
return canonicalModifierToken(action.PrimaryModifier().Canonical())
9499
case evdevModifierShift:
95100
return evdevModifierShift
96-
case evdevModifierCtrl, evdevModifierAliasControl, "primary":
101+
case evdevModifierCtrl, evdevModifierAliasControl:
97102
return evdevModifierCtrl
98103
case evdevModifierAlt, evdevModifierAliasOption, "opt":
99104
return evdevModifierAlt

‎internal/app/config.go‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,10 @@ func (a *App) restoreHotkeysAfterFailedReload() {
180180
}
181181

182182
func (a *App) updateConfigSnapshot(loadResult *config.LoadResult) {
183+
// Publish before the swap, so nothing reads the new configuration's keys
184+
// under the old meaning of Primary.
185+
config.UsePrimaryModifier(loadResult.Config.General.PrimaryModifier)
186+
183187
a.configMu.Lock()
184188
a.config = loadResult.Config
185189
a.ConfigPath = loadResult.ConfigPath

‎internal/app/ipcctrl/info_health.go‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"github.com/y3owk1n/neru/internal/adapter/ipc"
1313
"github.com/y3owk1n/neru/internal/adapter/platform"
14+
"github.com/y3owk1n/neru/internal/domain/action"
1415
"github.com/y3owk1n/neru/internal/ports"
1516
)
1617

@@ -240,7 +241,7 @@ func capabilitiesMap(capabilities ports.PlatformCapabilities) map[string]any {
240241
func profileMap(profile platform.Profile) map[string]any {
241242
return map[string]any{
242243
"os": string(profile.OS),
243-
"primary_modifier": profile.PrimaryModifier,
244+
"primary_modifier": action.PrimaryModifier().Canonical(),
244245
"display_server": string(profile.DisplayServer),
245246
"accessibility_backend": profile.Accessibility.Name,
246247
"accessibility_build_mode": string(profile.Accessibility.BuildMode),

‎internal/app/new.go‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ func New(opts ...Option) (*App, error) {
2828
app.config = config.DefaultConfig()
2929
}
3030

31+
// Publish before anything binds a hotkey or builds a keymap, since every
32+
// key comparison from here on reads it.
33+
config.UsePrimaryModifier(app.config.General.PrimaryModifier)
34+
3135
if app.logger == nil {
3236
logger, err := initializeLogger(app.config)
3337
if err != nil {

‎internal/config/config.go‎

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ const (
116116
modifierNameCtrl = "ctrl"
117117
modifierNameAlt = "alt"
118118
modifierNameShift = "shift"
119+
modifierDisplayCmd = "Cmd"
120+
modifierDisplayCtrl = "Ctrl"
121+
modifierDisplayAlt = "Alt"
122+
modifierDisplaySuper = "Super"
119123
)
120124

121125
// Config is the complete application configuration structure.
@@ -167,14 +171,25 @@ type ThemeConfig struct {
167171

168172
// GeneralConfig defines general application-wide settings.
169173
type GeneralConfig struct {
170-
ExcludedApps []string `json:"excludedApps" toml:"excluded_apps"`
171-
PassthroughUnboundedKeys bool `json:"passthroughUnboundedKeys" toml:"passthrough_unbounded_keys"`
172-
ShouldExitAfterPassthrough bool `json:"shouldExitAfterPassthrough" toml:"should_exit_after_passthrough"`
173-
PassthroughUnboundedKeysBlacklist []string `json:"passthroughUnboundedKeysBlacklist" toml:"passthrough_unbounded_keys_blacklist"`
174-
HideOverlayInScreenShare bool `json:"hideOverlayInScreenShare" toml:"hide_overlay_in_screen_share"`
175-
KBLayoutToUse string `json:"kbLayoutToUse" toml:"kb_layout_to_use"`
176-
ExecShell string `json:"execShell" toml:"exec_shell"`
177-
ExecShellArgs []string `json:"execShellArgs" toml:"exec_shell_args"`
174+
ExcludedApps []string `json:"excludedApps" toml:"excluded_apps"`
175+
PassthroughUnboundedKeys bool `json:"passthroughUnboundedKeys" toml:"passthrough_unbounded_keys"`
176+
ShouldExitAfterPassthrough bool `json:"shouldExitAfterPassthrough" toml:"should_exit_after_passthrough"`
177+
PassthroughUnboundedKeysBlacklist []string `json:"passthroughUnboundedKeysBlacklist" toml:"passthrough_unbounded_keys_blacklist"`
178+
HideOverlayInScreenShare bool `json:"hideOverlayInScreenShare" toml:"hide_overlay_in_screen_share"`
179+
KBLayoutToUse string `json:"kbLayoutToUse" toml:"kb_layout_to_use"`
180+
ExecShell string `json:"execShell" toml:"exec_shell"`
181+
ExecShellArgs []string `json:"execShellArgs" toml:"exec_shell_args"`
182+
PrimaryModifier PrimaryModifierConfig `json:"primaryModifier" toml:"primary_modifier"`
183+
}
184+
185+
// PrimaryModifierConfig names the modifier a hotkey written with Primary means
186+
// on each platform, so one shared config file can bind Cmd on macOS and Ctrl
187+
// or Super elsewhere. Every platform that is neither darwin nor windows reads
188+
// the linux entry.
189+
type PrimaryModifierConfig struct {
190+
Darwin string `json:"darwin" toml:"darwin"`
191+
Linux string `json:"linux" toml:"linux"`
192+
Windows string `json:"windows" toml:"windows"`
178193
}
179194

180195
// ModeIndicatorUI defines the visual/appearance settings for the mode indicator.

‎internal/config/config_defaults.go‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,17 @@ func defaultGeneral() GeneralConfig {
355355
KBLayoutToUse: "",
356356
ExecShell: DefaultExecShell,
357357
ExecShellArgs: []string{DefaultExecShellFlag},
358+
PrimaryModifier: defaultPrimaryModifier(),
359+
}
360+
}
361+
362+
// defaultPrimaryModifier is Cmd on macOS, where it is the shortcut modifier,
363+
// and Ctrl elsewhere, where Super usually belongs to the desktop.
364+
func defaultPrimaryModifier() PrimaryModifierConfig {
365+
return PrimaryModifierConfig{
366+
Darwin: modifierDisplayCmd,
367+
Linux: modifierDisplayCtrl,
368+
Windows: modifierDisplayCtrl,
358369
}
359370
}
360371

0 commit comments

Comments
 (0)