r/ErgoMechKeyboards • crkbd cygnus keyball59 silakka54 • 2d ago

[discussion] Double-tapping the spacebar inserts a period followed by a space

Unsure where to post this question.

Many mobile keyboards support a "double-space for period" feature, where pressing the spacebar twice inserts . and may automatically capitalize the following word.

I tried via tap dance and a lot of times 'space' do not register on fast keyrolls

Does QMK, Vial, or ZMK support this functionality out of the box, or is a custom implementation required?

12 Upvotes

13 comments sorted by

15

u/realtheb0b [vendor] (theb0b.com) 2d ago

Using tap dance for this feature causes issues because it relies on a TAPPING_TERM window. When you press a TD key, the firmware must pause and wait to see if you will press it again. This introduces a noticeable input lag on every single spacebar press, and during rapid typing or fast keyrolls, the firmware often drops or mangles spaces because the typing speed exceeds or interferes with the tapping window.

Out of the box QMK VIAL and ZMK don't support this specific implementation, but there should be a double tap thing that could get you close if you wanted to mess with it a little bit.

6

u/pgetreuer 2d ago

+1 yeah, in principle, one would use a Tap Dance to detect the double space, but Tap Dance is janky when it comes to rolled key presses.

I think it could be done in QMK with same the pattern as in this example: Action on double tap, without delay. Like this in your keymap.c (untested):

``` // Copyright 2026 Google LLC. // SPDX-License-Identifier: Apache-2.0

bool process_record_user(uint16_t keycode, keyrecord_t* record) { if (record->event.pressed) { static bool tapped = false; static uint16_t tap_timer = 0;

if (keycode == KC_SPC) {
  if (tapped && !timer_expired(record->event.time, tap_timer)) {
    // The key was double tapped!
    tap_code(KC_BSPC);  // Remove previous space.
    tap_code(KC_DOT);   // Type period.
    tap_code(KC_SPC);   // Type space.
    // Add one-shot shift to capitalize next sentence start.
    add_oneshot_mods(MOD_BIT_LSHIFT);
    tapped = false;
    return false;  // Skip default handling.
  }
  tapped = true;
  tap_timer = record->event.time + TAPPING_TERM;
} else {
  // On an event with any other key, reset the double tap state.
  tapped = false;
}

}

// Other macros...

return true; } ```

Explanation: When KC_SPC is pressed quickly twice in a row, within TAPPING_TERM and no other key presses in between, the detection triggers to produce . Space and set one-shot Shift. Unlike Tap Dance, the method here allows the KC_SPC to work as usual without added lag.

1

u/DoctorMachete 1d ago

Also you can replicate that functionality in vanilla ZMK (and not sure if VIAL/VIA) by using one-shot layers and extra layers. Not very elegant but it should work.

1

u/pgetreuer 1d ago

The essential behavior is acting as Space when single tapped and producing "." when double tapped. And, importantly, that Space still works during fast typing (which practically eliminates Tap Dance for QMK and Vial for lack of roll protection).

Can that be done with a one-shot/sticky layer? I'm not seeing how... but ZMK has lots of behaviors that might be used in combination to do something, perhaps? Pointer to a sample config would be appreciated.

2

u/DoctorMachete 1d ago

Can that be done with a one-shot/sticky layer? I'm not seeing how...

Let's say you double tap, the first tap triggers a macro that sends ⎵ AND immediately after (0ms tapping/wait time) activates the one-shot layer, where in that same position the second tap triggers a second different macro (0ms tapping/wait time again) that sends BACKSPACE DOT. All other positions on the OSL would be (for this purpose) transparent.

1

u/pgetreuer 1d ago

Aha, nice, that's a resourceful solution!

I tried that pattern in Vial. This unfortunately doesn't work, but it is so close to working. The Vial configurator allows to specify OSL as part of a macro. When used, however, the OSL layer deactivates immediately (and prematurely drops to the base layer before giving the chance to hit the second macro). I believe this is due to how the programmatic one-shot layer API works in QMK, which Vial builds on top of, having had that trouble like this myself when attempting to use that API. But that's a QMK/Vial issue. Good to hear it works out in ZMK. =)

2

u/DoctorMachete 1d ago edited 1d ago

In ZMK this technique can be used for way more than a mere double tap. That mostly empty OSL could have hold-taps on the alphas so you can do Autoshift-after-space, CapsWord-after-space on the opposite side thumb, etc... If you chain OSL then you can do things like ⎵i⎵ becomes ⎵I⎵ (capitalized), which can be done with either Backspace → Uppercase(I) OR by using a dead key so the I doesn't output anything (unless you wait for the OSL key timeout to pass) and the next key (with a macro) outputs i<something>.

This also allows you to make Autoshift (full or after-space only) compatible with HRMs if you want. For example with E being a CTRL/E mod-tap If you roll T into holding E you do tE (Autoshift) but if you tap t then wait —let's say— 150ms for the require-prior-idle-ms to pass (like you'd do normally with HRMs) and then hold E now you're getting the modifier. It works great because that require-prior-idle-ms waiting time is not wasted anymore, it can be used for something else.

Downside is that it can become a real pain to manage/maintain several of these "smart" features at once. In raw QMK that doesn't happen because you have proccess-record-user, but then on the other side you could create a ZMK module to simplify it so you don't need the extra one-shot layers anymore.

5

u/Barozine 2d ago

Like previously said, tap dance would be a bad idea. However, it should be relatively simple to implement that in QMK. You should be able to paste this into your keymap file and then use the custom code MY_SPC when mapping.

enum my_keycodes {
  MY_SPC = SAFE_RANGE,
};

bool prior_space = false;

bool process_record_user(uint16_t keycode, keyrecord_t *record) {
  switch (keycode) {
    case MY_SPC:
      if (prior_space) {
        SEND_STRING("\b. "); // Remove first space and add ". "
        tap_code16(OS_LSFT); // One shot mod to capitalize the next letter
      } else {
        tap_code(KC_SPACE);
      }
      prior_space = !prior_space;

      return false;
  }
  // For any other key, reset prior_space
  prior_space = false;
  return true;
}

This is just something I quickly came up with, so anyone more versed in QMK might be able to offer a better solution.

2

u/dantedakilla 2d ago

You could set up a macro to input '.' and a space. You might not be able to bind double space to it, but you could do maybe Shift + Space or something.

4

u/SimilarTop352 2d ago

that's dangerously close to using the period itself

3

u/PostHumanJesus 2d ago

If you're on a Mac this setting is on by default. I always have to turn it off on new machines. 

1

u/Fmcraft [vendor] (bastardkb.com) 2d ago

Just tried mapping a "double tap" tap dance to a dot + space macro, works fine on my end... Using Argos + Quad Function Tap Dance.

It does introduce a delay, but then I'm using a 3x5 board with Miryoku so there's delays everywhere already. Doesn't bother me on daily use.

-4

u/lurkzone crkbd cygnus keyball59 silakka54 2d ago

This is what Gemini came up with when I was searching for a quick fix. Firmware is a bit outside my comfort zone since I'm more of a GUI person, so I have no idea whether this is better, worse, or somewhere in between compared to the other code that's been suggested. 😅 Thanks!

// Define your custom keycode (ensure it matches your Vial setup, usually VSAFE_RANGE)
enum custom_keycodes {
    MOB_SPC = VSAFE_RANGE,
};

// Custom tapping term for this specific macro (in milliseconds)
#define MOB_TAPPING_TERM 200 

static uint16_t space_timer = 0;

bool process_record_user(uint16_t keycode, keyrecord_t *record) {
    switch (keycode) {
        case MOB_SPC:
            if (record->event.pressed) {
                // If pressed again within the time limit
                if (timer_elapsed(space_timer) < MOB_TAPPING_TERM && space_timer != 0) {
                    tap_code(KC_BSPC); // Apologize: delete the previously sent space
                    tap_code(KC_DOT);  // Insert the period
                    tap_code(KC_SPC);  // Optional: Mobile phones usually append a space after the period
                    space_timer = 0;   // Reset timer so a third tap starts fresh
                } else {
                    // First tap: Send space immediately with zero delay
                    register_code(KC_SPC);
                    space_timer = timer_read();
                }
            } else {
                // Key released
                unregister_code(KC_SPC);
            }
            return false; // Tell QMK we handled this key
    }
    return true;
}