Allow double tap key to output a unique keybind without breaking hold

Author: eugenesvkCreated Mar 19, 2023Updated Sep 16, 2026

I'd like to do the following remapping:

  1. (single tap) ; to ; but only after a delay (to allow for a double tap/hold to have a different functionality), so this sets tap︔ variable to 1
  2. (single tap+X) ;x(any key≠;) to ;x (no change in behavior here), this resets tap︔ back to 0
  3. (double tap) ;; to : (replace semicolon with colon on double tap), this works by making ; with tap︔=1 condition output ⇧; and reset tap︔ back to 0
  4. (hold) ; to enter a new mode via setting hold︔ variable to 1 (and resetting it back to 0 on release)

And it seems to me that it's currently impossible since same-key tap is treated as cancelled and same-key hold is reated as invoked, so there is no place for me to put ; output that would not break either double tap (and output :; instead of :) or hold (and output ; on hold) I'm using the following complex-modifications features

  • to_if_held_down sets hold︔ to 1
  • to_after_key_up resets the hold︔ back to 0, works fine
  • to_if_alone sets varSemicolonTap variable to 1 (or regular to woks)
  • to_delayed_action is where the main issue is, I need it to reset varSemicolonTap back to 0 after some delay, but also print the actual ;
    • to_if_canceled this helps with rule 2 as any x≠; in treated as cancellation, so if I output ; here it's inserted before x just fine. However it's also triggered when I tap ; again, and this is the buggy part. I don't
    • to_if_invoked works fine with printing ; after a delay but then holding the key is also treated as key being invoked, so on hold I get an extra ; I don't need

Not sure what the best resolution is but maybe Either to_if_canceled could be optionally split into 2 conditions:

  • to_if_canceled_by_self
  • to_if_canceled_by_other

Or to_if_invoked could be optionally split into 2 conditions:

  • to_if_invoked_unless_hold
  • to_if_invoked_hold

Then I could skip printing ; on double tap in the cancelled clause, only reserving it for ;x Or I could print ; only at the to_if_invoked_unless_hold call while my hold will work by just changing the variables, but not printing anything

below is a snippen of goku config (much more readable and easier to reason about vs the default)

edn
 {:des "  ;×2 ⟶ :     hold; ⟶ layer" :rules [
  [:semicolon [:!Ssemicolon  ["tap︔"0]["q︔"0]] [["tap︔"1]] ]
  [:q         [:semicolon :w ["tap︔"0]["q︔"1]] [["tap︔"1]] ] ;w to test correctness
  ; ↑ this is a poor substitute for :canceled since fails on quick ;q typing
  [:semicolon nil [] {
    :alone[["tap︔"1]] ; adding :semicolon here means...
    ;:alone[:semicolon ["tap︔"1]] ; ...double tap prints ;:
    :delayed{ ;
      ;:invoked[{:set["tap︔"0] :halt true}]             ; adding :semicolon here means...
      :invoked[:semicolon {:set["tap︔"0] :halt true}] ; ... hold prints ;
      :canceled[["tap︔"0]]}              ; adding :semicolon here means...
      ;:canceled[:semicolon ["tap︔"0]]} ;...  double tap prints :;
      ; but not adding it here means extra manual bindings bindings to manually enable the missing `;x`, and those bindings are a poor substitute as they fail on quick typing
    :held[{:set["hold︔"1]} {:set["tap︔"0] :repeat false}] :afterup[["hold︔"0]]
    :params{:alone 514 :held 515 :delay 515} }];
  ]}
same config in json
json
         {
        "description" : "  ;×2 ⟶ :     hold; ⟶ layer",
        "manipulators" : [ {
          "from" : {
            "key_code" : "semicolon"
          },
          "to" : [ {
            "key_code" : "semicolon",
            "modifiers" : [ "left_shift" ]
          }, {
            "set_variable" : {
              "name" : "tap︔",
              "value" : 0
            }
          }, {
            "set_variable" : {
              "name" : "q︔",
              "value" : 0
            }
          } ],
          "conditions" : [ {
            "name" : "tap︔",
            "value" : 1,
            "type" : "variable_if"
          } ],
          "type" : "basic"
        }, {
          "from" : {
            "key_code" : "q"
          },
          "to" : [ {
            "key_code" : "semicolon"
          }, {
            "key_code" : "w"
          }, {
            "set_variable" : {
              "name" : "tap︔",
              "value" : 0
            }
          }, {
            "set_variable" : {
              "name" : "q︔",
              "value" : 1
            }
          } ],
          "conditions" : [ {
            "name" : "tap︔",
            "value" : 1,
            "type" : "variable_if"
          } ],
          "type" : "basic"
        }, {
          "to_if_alone" : [ {
            "set_variable" : {
              "name" : "tap︔",
              "value" : 1
            }
          } ],
          "to_if_held_down" : [ {
            "set_variable" : {
              "name" : "hold︔",
              "value" : 1
            }
          }, {
            "set_variable" : {
              "name" : "tap︔",
              "value" : 0
            },
            "repeat" : false
          } ],
          "to_after_key_up" : [ {
            "set_variable" : {
              "name" : "hold︔",
              "value" : 0
            }
          } ],
          "to_delayed_action" : {
            "to_if_invoked" : [ {
              "key_code" : "semicolon"
            }, {
              "set_variable" : {
                "name" : "tap︔",
                "value" : 0
              },
              "halt" : true
            } ],
            "to_if_canceled" : [ {
              "set_variable" : {
                "name" : "tap︔",
                "value" : 0
              }
            } ]
          },
          "parameters" : {
            "basic.to_if_alone_timeout_milliseconds" : 514,
            "basic.to_if_held_down_threshold_milliseconds" : 515,
            "basic.to_delayed_action_delay_milliseconds" : 515
          },
          "from" : {
            "key_code" : "semicolon"
          },
          "conditions" : [ ],
          "type" : "basic"
        } ]
      },

Source: pqrs-org/Karabiner-Elements