Skip to content

Commit 4213c21

Browse files
fix help formatting and add missing triggers
- Restructure help sections to use gohelp.Item() instead of raw .Text() blocks - Add missing .tappressrelease and .tapholdrelease to help and README - Update restrictions to accurately list all array-command behaviors
1 parent 295f542 commit 4213c21

6 files changed

Lines changed: 162 additions & 56 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,8 @@ Config lives at `~/.config/akeyshually/`:
187187
| `.longpress` / `.longpress(ms)` | `"key.longpress(500)"` | Fire once after threshold (one-shot) |
188188
| `.holdrelease` / `.holdrelease(ms)` | `"key.holdrelease(500)" = ["hold_cmd", "release_cmd"]` | Execute at hold threshold and on release |
189189
| `.taplongpress` / `.taplongpress(tap_ms, long_ms)` | `"key.taplongpress" = ["tap_cmd", "long_cmd"]` | Tap once, or tap-then-longpress |
190+
| `.tappressrelease` / `.tappressrelease(tap_ms)` | `"key.tappressrelease(200)" = ["press_cmd", "release_cmd"]` | Tap then press fires first, release fires second |
191+
| `.tapholdrelease` / `.tapholdrelease(tap_ms, hold_ms)` | `"key.tapholdrelease" = ["hold_cmd", "release_cmd"]` | Tap then hold fires first, release fires second |
190192

191193
**Modifiers** — change how the command executes:
192194

internal/commands/help.go

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -29,23 +29,27 @@ var (
2929
helpConfig = gohelp.NewPage("config", "configuration file documentation").
3030
Text("Config File: ~/.config/akeyshually/config.toml").
3131
Section("[settings]",
32-
gohelp.Item("default_interval", "Default interval for repeat behaviors (milliseconds, default: 150)"),
33-
gohelp.Item("disable_media_keys", "Forward media keys to system (default: false)"),
34-
gohelp.Item("shell", "Shell to use for commands (default: $SHELL, fallback: sh)"),
35-
gohelp.Item("env_file", "File to source before command execution (optional)"),
36-
gohelp.Item("notify_on_overlay_change", "Desktop notifications when overlays change (default: false)"),
32+
gohelp.Item("default_interval", "Default interval for repeat behaviors (milliseconds)", "default_interval = 150"),
33+
gohelp.Item("disable_media_keys", "Forward media keys to system", "disable_media_keys = false"),
34+
gohelp.Item("shell", "Shell to use for commands (default: $SHELL, fallback: sh)", "shell = \"/bin/bash\""),
35+
gohelp.Item("env_file", "File to source before command execution", "env_file = \"~/.profile\""),
36+
gohelp.Item("notify_on_overlay_change", "Desktop notifications when overlays change", "notify_on_overlay_change = true"),
3737
gohelp.Item("devices", "List of device name substrings to grab (case-insensitive)", "devices = [\"Huion\", \"Xbox Controller\"]"),
3838
).
39-
Text("[settings]\ndefault_interval = 150\ndisable_media_keys = false\nshell = \"/bin/bash\"\nenv_file = \"~/.profile\"\nnotify_on_overlay_change = false").
4039
Section("[shortcuts]",
4140
gohelp.Item("Key modifiers", "super, ctrl, alt, shift (lowercase, no left/right distinction)"),
4241
gohelp.Item("Keys", "lowercase letters, numbers, special keys (print, space, etc.)"),
4342
gohelp.Item("Axis inputs", "x, y, z, rx, ry, rz, abs_x, abs_y, etc. with +/- direction (see 'help axis')"),
44-
gohelp.Item("Syntax", "Use + to separate modifiers and key"),
43+
gohelp.Item("Syntax", "Use + to separate modifiers and key", "\"super+t\" = \"alacritty\""),
4544
gohelp.Item("Triggers", ".onpress (default), .hold, .doubletap, .taphold, .pressrelease, .longpress, etc."),
4645
gohelp.Item("Modifiers", ".switch, .repeat, .passthrough"),
4746
).
48-
Text("[shortcuts]\n\"super.pressrelease\" = [\"\", \"rofi\"] # Tap modifier key\n\"super+t\" = \"alacritty\"\n\"f9.onpress.repeat\" = \"xdotool click 1\" # Toggle auto-clicker\n\"rx+\" = \">scrollup\" # Axis input (drawing tablet touchstrip)").
47+
Section("Examples",
48+
gohelp.Item("Tap modifier key", "Execute command on modifier release", "\"super.pressrelease\" = [\"\", \"rofi\"]"),
49+
gohelp.Item("Launch terminal", "Simple key combo", "\"super+t\" = \"alacritty\""),
50+
gohelp.Item("Toggle auto-clicker", "Repeat while held or toggle on press", "\"f9.onpress.repeat\" = \"xdotool click 1\""),
51+
gohelp.Item("Axis scrolling", "Touchstrip or peripheral axis input", "\"rx+\" = \">scrollup\""),
52+
).
4953
Section("[command_variables]",
5054
gohelp.Item("browser", "Reusable command alias", "browser = \"brave-browser --new-window\""),
5155
gohelp.Item("terminal", "Reusable command alias", "terminal = \"alacritty --working-directory ~\""),
@@ -66,15 +70,14 @@ var (
6670
).
6771
Section("Commands",
6872
gohelp.Item("enable gaming.toml", "Enable overlay and restart daemon", "akeyshually enable gaming.toml"),
69-
gohelp.Item("disable gaming.toml", "Disable overlay and restart daemon"),
70-
gohelp.Item("list", "Show all config files and their status"),
71-
gohelp.Item("clear", "Disable all overlays"),
72-
gohelp.Item("config gaming", "Edit gaming.toml overlay"),
73+
gohelp.Item("disable gaming.toml", "Disable overlay and restart daemon", "akeyshually disable gaming.toml"),
74+
gohelp.Item("list", "Show all config files and their status", "akeyshually list"),
75+
gohelp.Item("clear", "Disable all overlays", "akeyshually clear"),
76+
gohelp.Item("config gaming", "Edit gaming.toml overlay", "akeyshually config gaming"),
7377
).
7478
Section("Settings",
75-
gohelp.Item("notify_on_overlay_change", "Desktop notifications when overlays change (default: false)"),
76-
).
77-
Text("Enable in config.toml:\n[settings]\nnotify_on_overlay_change = true")
79+
gohelp.Item("notify_on_overlay_change", "Desktop notifications when overlays change", "notify_on_overlay_change = true"),
80+
)
7881

7982
helpModifiers = gohelp.NewPage("modifiers", "triggers and modifiers syntax reference").
8083
Text("Triggers define when the action fires. Modifiers stack on top to change execution behavior.").
@@ -87,13 +90,18 @@ var (
8790
gohelp.Item(".longpress(ms)", "Fire once after threshold (one-shot)", "\"super+h.longpress(1000)\" = \"shutdown\""),
8891
gohelp.Item(".holdrelease(ms)", "Execute at hold threshold AND on release (2-command array)", "\"mute.holdrelease(500)\" = [\"enable-mic\", \"disable-mic\"]"),
8992
gohelp.Item(".taplongpress(tap_ms, long_ms)", "Tap fires first, tap-then-longpress fires second (2-command array)", "\"super+space.taplongpress\" = [\"quick\", \"long\"]"),
93+
gohelp.Item(".tappressrelease(tap_ms)", "Tap then press fires first, release fires second (2-command array)", "\"mute.tappressrelease(200)\" = [\"start\", \"stop\"]"),
94+
gohelp.Item(".tapholdrelease(tap_ms, hold_ms)", "Tap then hold fires first, release fires second (2-command array)", "\"f1.tapholdrelease\" = [\"hold-start\", \"hold-end\"]"),
9095
).
9196
Section("Modifiers",
9297
gohelp.Item(".switch", "Cycle through array of commands on each press", "\"f2.switch\" = [\"cmd1\", \"cmd2\", \"cmd3\"]"),
9398
gohelp.Item(".repeat", "Loop command: with .hold (while held) or .onpress (toggle)", "\"f9.onpress.repeat\" = \"xdotool click 1\""),
9499
gohelp.Item(".passthrough", "Match regardless of modifier state", "\"v.passthrough\" = \"copyq toggle\""),
95100
).
96-
Text("Restrictions:\n • .doubletap and .taphold only work on single keys (no combos)\n • .switch, .taphold, .pressrelease, .holdrelease, and .taplongpress require command arrays")
101+
Section("Restrictions",
102+
gohelp.Item("Single keys only", ".doubletap and .taphold only work on single keys (no combos)"),
103+
gohelp.Item("Array commands", ".switch, .pressrelease, .holdrelease, .taplongpress, .tappressrelease, and .tapholdrelease require 2+ commands"),
104+
)
97105

98106
helpAxis = gohelp.NewPage("axis", "absolute axis and peripheral support").
99107
Text("akeyshually supports absolute axis (ABS) events from evdev devices - bind any peripheral with axis input.").
@@ -103,14 +111,18 @@ var (
103111
).
104112
Section("Syntax",
105113
gohelp.Item("Direction suffix", "Append + or - to axis name for direction", "\"rx+\" or \"abs_y-\""),
106-
gohelp.Item("Commands", "Any command works, including remap scroll output", "\">scrollup\", \">scrolldown\", \">scrollleft\", \">scrollright\""),
114+
gohelp.Item("Scroll output", "Remap to scroll events", "\">scrollup\", \">scrolldown\", \">scrollleft\", \">scrollright\""),
115+
gohelp.Item("Shell command", "Any shell command works", "\"volume_up\", \"brightness-control +10\""),
116+
).
117+
Section("Examples",
118+
gohelp.Item("Touchstrip scroll up", "Positive direction triggers scroll up", "\"rx+\" = \">scrollup\""),
119+
gohelp.Item("Touchstrip scroll down", "Negative direction triggers scroll down", "\"rx-\" = \">scrolldown\""),
120+
gohelp.Item("Axis volume control", "Map axis movement to shell commands", "\"abs_y+\" = \"volume_up\""),
107121
).
108-
Text("[shortcuts]\n\"rx+\" = \">scrollup\" # Touchstrip up → scroll up\n\"rx-\" = \">scrolldown\" # Touchstrip down → scroll down\n\"abs_y+\" = \"volume_up\" # Axis movement → volume control").
109122
Section("Device Detection",
110123
gohelp.Item("Auto-detection", "Most devices auto-detected by capability flags"),
111-
gohelp.Item("Explicit grab", "Add device name substring to [settings] devices array if not detected", "devices = [\"Tablet Monitor Touch Strip\"]"),
112-
).
113-
Text("Example: Huion Kamvas Pro 13 touchstrip scrolling\n\n[settings]\ndevices = [\"Tablet Monitor Touch Strip\"]\n\n[shortcuts]\n\"rx+\" = \">scrollup\"\n\"rx-\" = \">scrolldown\"")
124+
gohelp.Item("Explicit grab", "Add device name substring to [settings] devices array", "devices = [\"Tablet Monitor Touch Strip\"]"),
125+
)
114126
)
115127

116128
// Help displays usage information or topic-specific help.

internal/config/config.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ const (
4545
BehaviorHoldRelease // Commands[0] at hold threshold (can be ""), Commands[1] on release after threshold
4646
BehaviorTapHold // tap fires Commands[0], tap-then-hold sustains Commands[1]
4747
BehaviorTapLongPress // tap fires Commands[0], tap-then-longpress fires Commands[1] once
48+
BehaviorTapPressRelease // tap, then Commands[0] on second press, Commands[1] on release
49+
BehaviorTapHoldRelease // tap, then Commands[0] at hold threshold, Commands[1] on release
4850
BehaviorEscapePending // pseudo-candidate: prevents early resolution when escape hatches exist
4951
)
5052

@@ -430,6 +432,8 @@ func ParseShortcut(key string, value interface{}) (*ParsedShortcut, error) {
430432
intervalRegex := regexp.MustCompile(`^(hold|longpress|doubletap|holdrelease)\((\d+\.?\d*|\d*\.\d+)\)$`)
431433
tapHoldRegex := regexp.MustCompile(`^tap(?:\((\d+\.?\d*|\d*\.\d+)\))?hold(?:\((\d+\.?\d*|\d*\.\d+)\))?$`)
432434
tapLongPressRegex := regexp.MustCompile(`^tap(?:\((\d+\.?\d*|\d*\.\d+)\))?longpress(?:\((\d+\.?\d*|\d*\.\d+)\))?$`)
435+
tapPressReleaseRegex := regexp.MustCompile(`^tap(?:\((\d+\.?\d*|\d*\.\d+)\))?pressrelease$`)
436+
tapHoldReleaseRegex := regexp.MustCompile(`^tap(?:\((\d+\.?\d*|\d*\.\d+)\))?holdrelease(?:\((\d+\.?\d*|\d*\.\d+)\))?$`)
433437

434438
for i := 1; i < len(parts); i++ {
435439
part := strings.ToLower(parts[i])
@@ -462,6 +466,30 @@ func ParseShortcut(key string, value interface{}) (*ParsedShortcut, error) {
462466
continue
463467
}
464468

469+
// Check for tappressrelease with optional interval: tap(N)pressrelease
470+
if matches := tapPressReleaseRegex.FindStringSubmatch(part); matches != nil {
471+
shortcut.Behavior = BehaviorTapPressRelease
472+
if matches[1] != "" {
473+
interval, _ := strconv.ParseFloat(matches[1], 64)
474+
shortcut.Interval = normalizeInterval(interval)
475+
}
476+
continue
477+
}
478+
479+
// Check for tapholdrelease with optional intervals: tap(N)holdrelease(N)
480+
if matches := tapHoldReleaseRegex.FindStringSubmatch(part); matches != nil {
481+
shortcut.Behavior = BehaviorTapHoldRelease
482+
if matches[1] != "" {
483+
interval, _ := strconv.ParseFloat(matches[1], 64)
484+
shortcut.Interval = normalizeInterval(interval)
485+
}
486+
if matches[2] != "" {
487+
interval, _ := strconv.ParseFloat(matches[2], 64)
488+
shortcut.HoldInterval = normalizeInterval(interval)
489+
}
490+
continue
491+
}
492+
465493
// Check for interval notation: hold(N), longpress(N), doubletap(N)
466494
if matches := intervalRegex.FindStringSubmatch(part); matches != nil {
467495
modifierName := matches[1]
@@ -497,6 +525,10 @@ func ParseShortcut(key string, value interface{}) (*ParsedShortcut, error) {
497525
shortcut.Behavior = BehaviorDoubleTap
498526
case "pressrelease":
499527
shortcut.Behavior = BehaviorPressRelease
528+
case "tappressrelease":
529+
shortcut.Behavior = BehaviorTapPressRelease
530+
case "tapholdrelease":
531+
shortcut.Behavior = BehaviorTapHoldRelease
500532
case "onrelease":
501533
return nil, fmt.Errorf("onrelease removed: use .pressrelease = [\"\", \"cmd\"]")
502534
case "onpress":
@@ -532,6 +564,10 @@ func behaviorName(b BehaviorMode) string {
532564
return "taphold"
533565
case BehaviorTapLongPress:
534566
return "taplongpress"
567+
case BehaviorTapPressRelease:
568+
return "tappressrelease"
569+
case BehaviorTapHoldRelease:
570+
return "tapholdrelease"
535571
default:
536572
return "unknown"
537573
}

internal/config/validate.go

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -255,15 +255,17 @@ func validateRemapCommand(cmd string) error {
255255

256256
// Lookup table mapping behaviors to their validation functions
257257
var behaviorValidators = map[BehaviorMode]func(*ParsedShortcut) error{
258-
BehaviorNormal: validateNormal,
259-
BehaviorHold: validateHold,
260-
BehaviorLongPress: validateLongPress,
261-
BehaviorSwitch: validateSwitch,
262-
BehaviorDoubleTap: validateDoubleTap,
263-
BehaviorPressRelease: validatePressRelease,
264-
BehaviorHoldRelease: validateHoldRelease,
265-
BehaviorTapHold: validateTapHold,
266-
BehaviorTapLongPress: validateTapLongPress,
258+
BehaviorNormal: validateNormal,
259+
BehaviorHold: validateHold,
260+
BehaviorLongPress: validateLongPress,
261+
BehaviorSwitch: validateSwitch,
262+
BehaviorDoubleTap: validateDoubleTap,
263+
BehaviorPressRelease: validatePressRelease,
264+
BehaviorHoldRelease: validateHoldRelease,
265+
BehaviorTapHold: validateTapHold,
266+
BehaviorTapLongPress: validateTapLongPress,
267+
BehaviorTapPressRelease: validateTapPressRelease,
268+
BehaviorTapHoldRelease: validateTapHoldRelease,
267269
}
268270

269271
// Individual behavior validators - each validates command count and behavior-specific rules
@@ -333,3 +335,17 @@ func validateTapLongPress(p *ParsedShortcut) error {
333335
}
334336
return nil
335337
}
338+
339+
func validateTapPressRelease(p *ParsedShortcut) error {
340+
if len(p.Commands) != 2 {
341+
return fmt.Errorf("tappressrelease behavior requires exactly 2 commands")
342+
}
343+
return nil
344+
}
345+
346+
func validateTapHoldRelease(p *ParsedShortcut) error {
347+
if len(p.Commands) != 2 {
348+
return fmt.Errorf("tapholdrelease behavior requires exactly 2 commands")
349+
}
350+
return nil
351+
}

internal/ladder/ladder.go

Lines changed: 66 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ func Run(
6969
// BUT: Skip early exit if the candidate is EscapePending (needs to wait for actual key events)
7070
if len(candidates) == 1 && len(ladder) == 0 && candidates[0].Shortcut.Behavior != config.BehaviorEscapePending {
7171
common.LogDebug(">>> LADDER %s: single candidate no timers, firing immediately", combo)
72-
fireWinner(combo, keyCode, value, &candidates[0], cfg, loopState, injector, virtual, modifiers, ctx, state)
72+
fireWinner(combo, keyCode, value, &candidates[0], cfg, loopState, injector, virtual, modifiers, ctx, state, pressed)
7373
return
7474
}
7575

@@ -153,7 +153,7 @@ func Run(
153153
if timer != nil {
154154
timer.Stop()
155155
}
156-
fireWinner(combo, keyCode, value, &candidates[0], cfg, loopState, injector, virtual, modifiers, ctx, state)
156+
fireWinner(combo, keyCode, value, &candidates[0], cfg, loopState, injector, virtual, modifiers, ctx, state, pressed)
157157
return
158158
}
159159

@@ -181,7 +181,7 @@ func Run(
181181
if timer != nil {
182182
timer.Stop()
183183
}
184-
fireWinner(combo, keyCode, value, &candidates[0], cfg, loopState, injector, virtual, modifiers, ctx, state)
184+
fireWinner(combo, keyCode, value, &candidates[0], cfg, loopState, injector, virtual, modifiers, ctx, state, pressed)
185185
return
186186
}
187187

@@ -198,7 +198,7 @@ func Run(
198198
// Last standing wins
199199
if len(candidates) == 1 {
200200
common.LogDebug(">>> LADDER %s: WINNER=%s (last standing after timer)", combo, behaviorName(candidates[0].Shortcut.Behavior))
201-
fireWinner(combo, keyCode, value, &candidates[0], cfg, loopState, injector, virtual, modifiers, ctx, state)
201+
fireWinner(combo, keyCode, value, &candidates[0], cfg, loopState, injector, virtual, modifiers, ctx, state, pressed)
202202
return
203203
}
204204

@@ -259,8 +259,8 @@ func buildTimerLadder(candidates []timers.Candidate, defaultInterval float64) []
259259
interval := intervalOrDefault(c.Shortcut.Interval, defaultInterval)
260260

261261
switch c.Shortcut.Behavior {
262-
case config.BehaviorDoubleTap:
263-
// Needs Phase 1 timer for doubletap window
262+
case config.BehaviorDoubleTap, config.BehaviorTapPressRelease:
263+
// Needs Phase 1 timer for doubletap/tap window
264264
if existing, ok := thresholds[1]; !ok || ms(interval) > existing {
265265
thresholds[1] = ms(interval)
266266
}
@@ -271,7 +271,7 @@ func buildTimerLadder(candidates []timers.Candidate, defaultInterval float64) []
271271
thresholds[1] = ms(interval)
272272
}
273273

274-
case config.BehaviorTapHold, config.BehaviorTapLongPress:
274+
case config.BehaviorTapHold, config.BehaviorTapLongPress, config.BehaviorTapHoldRelease:
275275
// Needs Phase 1 timer for tap window
276276
if existing, ok := thresholds[1]; !ok || ms(interval) > existing {
277277
thresholds[1] = ms(interval)
@@ -332,7 +332,7 @@ func isEliminated(b config.BehaviorMode, count int, pressed bool, phase int, has
332332
}
333333
return false
334334

335-
case config.BehaviorDoubleTap:
335+
case config.BehaviorDoubleTap, config.BehaviorTapPressRelease:
336336
// Eliminated by window expiry with no second press
337337
if phase >= 1 && count < 2 {
338338
return true
@@ -343,7 +343,7 @@ func isEliminated(b config.BehaviorMode, count int, pressed bool, phase int, has
343343
}
344344
return false
345345

346-
case config.BehaviorTapHold, config.BehaviorTapLongPress:
346+
case config.BehaviorTapHold, config.BehaviorTapLongPress, config.BehaviorTapHoldRelease:
347347
// Eliminated by releasing after tap window expired without second press
348348
if !pressed && count < 2 && phase >= 1 {
349349
return true
@@ -386,6 +386,7 @@ func fireWinner(
386386
modifiers matcher.ModifierState,
387387
ctx context.Context,
388388
state *timers.ComboState,
389+
pressed bool,
389390
) {
390391
s := winner.Shortcut
391392

@@ -418,14 +419,23 @@ func fireWinner(
418419
common.LogTrigger(resolvedCmd)
419420
executor.Run(resolvedCmd, execCtx)
420421
}
421-
// 10ms gap between press and release commands
422-
time.AfterFunc(10*time.Millisecond, func() {
423-
if s.Commands[1] != "" {
424-
resolvedCmd := cfg.ResolveCommand(s.Commands[1])
425-
common.LogTrigger(resolvedCmd)
426-
executor.Run(resolvedCmd, execCtx)
422+
// If key is still pressed, wait for release (seesaw behavior)
423+
// If key already released, fire second command immediately (rapid succession)
424+
if pressed {
425+
// Seesaw: wait for actual release event
426+
select {
427+
case <-ctx.Done():
428+
case <-state.ReleaseCh:
427429
}
428-
})
430+
} else {
431+
// Rapid succession: 10ms gap (key already released)
432+
time.Sleep(10 * time.Millisecond)
433+
}
434+
if s.Commands[1] != "" {
435+
resolvedCmd := cfg.ResolveCommand(s.Commands[1])
436+
common.LogTrigger(resolvedCmd)
437+
executor.Run(resolvedCmd, execCtx)
438+
}
429439

430440
case config.BehaviorHold, config.BehaviorLongPress:
431441
common.LogMatch(combo+".hold", combo)
@@ -505,6 +515,42 @@ func fireWinner(
505515
common.LogTrigger(resolvedCmd)
506516
executor.Run(resolvedCmd, execCtx)
507517

518+
case config.BehaviorTapPressRelease:
519+
common.LogMatch(combo+".tappressrelease", combo)
520+
if s.Commands[0] != "" {
521+
resolvedCmd := cfg.ResolveCommand(s.Commands[0])
522+
common.LogTrigger(resolvedCmd)
523+
executor.Run(resolvedCmd, execCtx)
524+
}
525+
// Wait for release
526+
select {
527+
case <-ctx.Done():
528+
case <-state.ReleaseCh:
529+
}
530+
if s.Commands[1] != "" {
531+
resolvedCmd := cfg.ResolveCommand(s.Commands[1])
532+
common.LogTrigger(resolvedCmd)
533+
executor.Run(resolvedCmd, execCtx)
534+
}
535+
536+
case config.BehaviorTapHoldRelease:
537+
common.LogMatch(combo+".tapholdrelease", combo)
538+
if s.Commands[0] != "" {
539+
resolvedCmd := cfg.ResolveCommand(s.Commands[0])
540+
common.LogTrigger(resolvedCmd)
541+
executor.Run(resolvedCmd, execCtx)
542+
}
543+
select {
544+
case <-ctx.Done():
545+
case <-state.ReleaseCh:
546+
}
547+
if s.Commands[1] != "" {
548+
resolvedCmd := cfg.ResolveCommand(s.Commands[1])
549+
common.LogMatch(combo+".tapholdrelease.release", combo)
550+
common.LogTrigger(resolvedCmd)
551+
executor.Run(resolvedCmd, execCtx)
552+
}
553+
508554
case config.BehaviorEscapePending:
509555
// No-op: this should never fire (eliminated before it can win)
510556
common.LogDebug("BUG: EscapePending won the ladder for %s (should be impossible)", combo)
@@ -588,6 +634,10 @@ func behaviorName(b config.BehaviorMode) string {
588634
return "taphold"
589635
case config.BehaviorTapLongPress:
590636
return "taplongpress"
637+
case config.BehaviorTapPressRelease:
638+
return "tappressrelease"
639+
case config.BehaviorTapHoldRelease:
640+
return "tapholdrelease"
591641
case config.BehaviorEscapePending:
592642
return "escape_pending"
593643
default:

0 commit comments

Comments
 (0)