Trying the Zsa-Voyager With Ergo-L
Fleetingdiscovering the ergol layout
https://ergol.org/claviers/compacts/#kbd_4x6
Switch using
per device layout
setxkbmap fr ergol setxkbmap fr bepo
I can configure xorg to use ergol on the voyager only, leaving bépo in my typematrix.
{ ... }:
{
services.xserver.inputClassSections = [
''
Identifier "voyager-ergol"
MatchIsKeyboard "on"
MatchUSBID "3297:1977"
Option "XkbLayout" "fr"
Option "XkbVariant" "ergol"
''
];
}
dicovering the voyager layout
https://configure.zsa.io/voyager/layouts/default
If I understand correctly, people leave the firmware keyboard as qwerty1, and don’t try to map the ergol keys in the keyboard itself2. The only hardware requirement is AltGr easy to press3.
what to pick in oryx
Because the physical layout thinks in qwerty, a mapping is needed to fit with the ergol layout. The layer 0 is already taken care of by the X ergol layout, and most keys of the layers 1 and 2 are ok, but in case I want special keys, I have to make the reverse translation manually, this table will help provide the correct key to the physical layout.
# Cheapest first: a character reachable with no modifier must not be listed
# under one that needs shift or altgr, wherever it turns up in the table.
LEVELS = ("", "shift", "right alt (in oryx)")
recipes = {}
for column, modifier in enumerate(LEVELS, start=1):
for row in glyphs:
char = str(row[column])
if char and char not in recipes:
recipes[char] = (str(row[0]), modifier)
print("| to type | pick in oryx | holding |")
print("|---+---+---|")
for char, (keycode, modifier) in sorted(recipes.items()):
print("| %s | %s | %s |" % (char, keycode, modifier))
| to type | pick in oryx | holding |
|---|---|---|
| ! | O | shift |
| " | DOUBLE_QUOTE | |
| # | LEFT_PAREN | |
| $ | DOLLAR | |
| % | PERCENT | |
| & | AMPERSAND | |
| ' | QUOTE | |
| ( | S | right alt (in oryx) |
| ) | D | right alt (in oryx) |
| * | ASTERISK | |
| + | PLUS | |
| , | DOT | |
| - | C | |
| . | N | |
| 0 | 0 | |
| 1 | 1 | |
| 2 | 2 | |
| 3 | 3 | |
| 4 | 4 | |
| 5 | 5 | |
| 6 | 6 | |
| 7 | 7 | |
| 8 | 8 | |
| 9 | 9 | |
| : | N | shift |
| ; | RIGHT_ANGLE_BRACKET | |
| < | W | right alt (in oryx) |
| = | EQUAL | |
| > | E | right alt (in oryx) |
| ? | C | shift |
| @ | RIGHT_PAREN | |
| A | A | |
| B | B | |
| C | W | |
| D | I | |
| E | D | |
| F | G | |
| G | COMMA | |
| H | M | |
| I | L | |
| J | Y | |
| K | SLASH | |
| L | H | |
| M | U | |
| N | F | |
| O | E | |
| P | R | |
| Q | Q | |
| R | J | |
| S | S | |
| T | K | |
| U | SEMICOLON | |
| V | V | |
| W | T | |
| X | X | |
| Y | P | |
| Z | Z | |
| [ | LEFT_BRACKET | |
| \ | BACKSLASH | |
| | | PIPE | |
| ] | RIGHT_BRACKET | |
| ^ | CIRCUMFLEX | |
| _ | UNDERSCORE | |
| ` | GRAVE | |
| { | LEFT_CURLY_BRACE | |
| } | RIGHT_CURLY_BRACE | |
| ~ | TILDE | |
| « | AT | |
| » | NONUS_HASH | |
| ₀ | 0 | right alt (in oryx) |
| ₁ | 1 | right alt (in oryx) |
| ₂ | 2 | right alt (in oryx) |
| ₃ | 3 | right alt (in oryx) |
| ₄ | 4 | right alt (in oryx) |
| ₅ | 5 | right alt (in oryx) |
| ₆ | 6 | right alt (in oryx) |
| ₇ | 7 | right alt (in oryx) |
| ₈ | 8 | right alt (in oryx) |
| ₉ | 9 | right alt (in oryx) |
| € | EXCLAIM | |
| ⍽ | SPACE | shift |
| ␣ | SPACE | |
| ★ | O |
my layout
From the default, I changed:
- added home row mod, for emacs makes use of the modifiers a lot
- made the modifiers symmetric, to ease remembering them
- I made the L1 layer provide the correct -, +, /, *, ., = characters beside the numpad
https://configure.zsa.io/voyager/layouts/Y6Lnb/latest/1
[[https://ipfs.konubinix.eu/p/bafkreigmgxdszoubm3yf5xk7huz5afngl5mmtbvqudull5eswut4slp2mm?filename=zsa_voyager_y6lnb_azrwdb_konubinix.bin][zsa_voyager_Y6Lnb_AzRwdB_konubinix.bin]]
[[https://ipfs.konubinix.eu/p/bafkreigqk7235mdc4zhue2cx3puixn6np44qcbta5gy4pnu6splaabkjde?filename=zsa_voyager_y6lnb_azrwdb_konubinix_source.zip][zsa_voyager_Y6Lnb_AzRwdB_konubinix_source.zip]]
patching in what oryx cannot express
Oryx cannot express a key override, so shift+backspace giving delete has to be patched into the export, then built rather than downloaded. The guards make it safe to re-run on a fresh export.
cd ~/.cache/voyager-ergol
rm -rf source && unzip -q "${SOURCE%%\?*}" -d source
cd source/zsa_voyager_konubinix_source
grep -q KEY_OVERRIDE_ENABLE rules.mk ||
echo 'KEY_OVERRIDE_ENABLE = yes' >> rules.mk
grep -q key_overrides keymap.c || cat >> keymap.c <<'EOF'
const key_override_t delete_l1_override =
ko_make_basic(MOD_MASK_SHIFT, KC_BSPC, KC_DEL);
const key_override_t delete_thumb_override =
ko_make_basic(MOD_MASK_SHIFT, MT(MOD_LSFT, KC_BSPC), KC_DEL);
const key_override_t *key_overrides[] = {
&delete_l1_override,
&delete_thumb_override,
NULL
};
EOF
grep -c ko_make_basic keymap.c
2
building the patched export
It needs ZSA’s own qmk fork4 and an arm toolchain. The flake takes the patched export as its source, so the bin is the output of a derivation rather than of a tree left lying around.
{
inputs = {
nixpkgs.url = "nixpkgs";
qmk_firmware = {
url = "git+https://github.com/zsa/qmk_firmware?submodules=1&shallow=1";
flake = false;
};
};
outputs =
{ nixpkgs, qmk_firmware, ... }:
let
pkgs = nixpkgs.legacyPackages.x86_64-linux;
in
{
packages.x86_64-linux.default = pkgs.stdenv.mkDerivation {
name = "zsa_voyager_konubinix.bin";
src = ./source/zsa_voyager_konubinix_source;
# qmk propagates gcc-arm-embedded, gnumake and dfu-util.
nativeBuildInputs = [ pkgs.qmk ];
# qmk builds inside its own tree, the store copy is read only.
buildPhase = ''
export HOME=$TMPDIR QMK_HOME=$TMPDIR/qmk
cp -r ${qmk_firmware} $QMK_HOME
chmod -R u+w $QMK_HOME
cp -r . $QMK_HOME/keyboards/zsa/voyager/keymaps/konubinix
# version.h is stamped from git describe, the store copy has no .git
qmk compile -e SKIP_VERSION=yes -kb zsa/voyager -km konubinix
'';
installPhase = "cp $QMK_HOME/*.bin $out";
};
};
}
Too long to run in babel, it would freeze emacs. Call it from a shell.
nix build ~/.cache/voyager-ergol --print-out-paths \
--out-link ~/.cache/voyager-ergol/built.bin
flashing
Press the boot key of L2 to enter the bootloader. The keyboard stops typing, and lsusb replaces 3297:1977 by 3297:0791 and say it’s in bootloader mode
lsusb | grep --ignore-case zsa
Bus 001 Device 012: ID 3297:0791 ZSA Technology Labs Voyager Bootloader
Talking to the bootloader needs udev rules.
{ ... }:
{
hardware.keyboard.zsa.enable = true;
}
dfu-util --device 3297:0791 --alt 0 \
--dfuse-address 0x08002000:leave \
--download ~/.cache/voyager-ergol/built.bin 2>&1
dfu-util 0.11
Copyright 2005-2009 Weston Schmidt, Harald Welte and OpenMoko Inc.
Copyright 2010-2021 Tormod Volden and Stefan Schmidt
This program is Free Software and has ABSOLUTELY NO WARRANTY
Please report bugs to http://sourceforge.net/p/dfu-util/tickets/
Opening DFU capable USB device...
Device ID 3297:0791
Device DFU version 011a
Claiming USB DFU Interface...
Setting Alternate Interface #0 ...
Determining device status...
DFU state(2) = dfuIDLE, status(0) = No error condition is present
DFU mode device DFU version 011a
Device returned transfer size 2048
DfuSe interface name: "Internal Flash "
Downloading element to address = 0x08002000, size = 54292
Erase [ ] 0% 0 bytes
Erase [= ] 7% 4096 bytes
Erase [== ] 11% 6144 bytes
Erase [=== ] 15% 8192 bytes
Erase [==== ] 18% 10240 bytes
Erase [===== ] 22% 12288 bytes
Erase [====== ] 26% 14336 bytes
Erase [======= ] 30% 16384 bytes
Erase [======== ] 33% 18432 bytes
Erase [========= ] 37% 20480 bytes
Erase [========== ] 41% 22528 bytes
Erase [=========== ] 45% 24576 bytes
Erase [============ ] 49% 26624 bytes
Erase [============= ] 52% 28672 bytes
Erase [============== ] 56% 30720 bytes
Erase [=============== ] 60% 32768 bytes
Erase [================ ] 64% 34816 bytes
Erase [================= ] 71% 38912 bytes
Erase [================== ] 75% 40960 bytes
Erase [=================== ] 79% 43008 bytes
Erase [==================== ] 82% 45056 bytes
Erase [===================== ] 86% 47104 bytes
Erase [====================== ] 90% 49152 bytes
Erase [======================= ] 94% 51200 bytes
Erase [======================== ] 98% 53248 bytes
Erase [=========================] 100% 54292 bytes
Erase done.
Download [ ] 0% 0 bytes
Download [= ] 7% 4096 bytes
Download [== ] 11% 6144 bytes
Download [=== ] 15% 8192 bytes
Download [==== ] 18% 10240 bytes
Download [===== ] 22% 12288 bytes
Download [====== ] 26% 14336 bytes
Download [======= ] 30% 16384 bytes
Download [======== ] 33% 18432 bytes
Download [========= ] 37% 20480 bytes
Download [========== ] 41% 22528 bytes
Download [=========== ] 45% 24576 bytes
Download [============ ] 49% 26624 bytes
Download [============= ] 52% 28672 bytes
Download [============== ] 56% 30720 bytes
Download [=============== ] 60% 32768 bytes
Download [================ ] 64% 34816 bytes
Download [================= ] 71% 38912 bytes
Download [================== ] 75% 40960 bytes
Download [=================== ] 79% 43008 bytes
Download [==================== ] 82% 45056 bytes
Download [===================== ] 86% 47104 bytes
Download [====================== ] 90% 49152 bytes
Download [======================= ] 94% 51200 bytes
Download [======================== ] 98% 53248 bytes
Download [=========================] 100% 54292 bytes
Download done.
File downloaded successfully
Submitting leave request...
Transitioning to dfuMANIFEST state
the cheatsheet
To get a cheatsheet showing my layout with the ergol mapping, I need a bit of code.
the tooling
from keymap_drawer.config import ParseConfig
from xkbcommon import xkb
# The alphanumeric keys. Above them the keypad repeats the same glyphs.
SPACE = 65
MAIN_BLOCK = [*range(9, 62), SPACE]
# The spaces ergol puts on that key are invisible, so name them.
VISIBLE = {" ": "␣", " ": "⍽", " ": "⎵"}
# ergol reaches its accented letters through the ★ dead key, which is level 5.
ONE_DEAD_KEY = 4
# Level 4: the maths and the superscripts, plus the non-breaking space.
ALTGR_SHIFT = 3
context = xkb.Context()
def glyphs(layout, variant, held):
"""What each key of the main block produces, with `held` down."""
keymap = context.keymap_new_from_names(layout=layout, variant=variant)
state = keymap.state_new()
if held:
state.update_key(keymap.key_by_name(held), xkb.XKB_KEY_DOWN)
return {code: state.key_get_string(code) for code in MAIN_BLOCK}
def level(layout, variant, index):
"""What each key produces at that level, for levels no modifier reaches."""
keymap = context.keymap_new_from_names(layout=layout, variant=variant)
out = {}
for code in MAIN_BLOCK:
syms = []
if keymap.num_levels_for_key(code, 0) > index:
syms = keymap.key_get_syms_by_level(code, 0, index)
out[code] = (xkb.keysym_to_string(syms[0]) or "") if syms else ""
return out
def ergol_levels():
"""Every key of ergol, as (base, shifted, altgr, dead key, altgr+shift)."""
base = glyphs("fr", "ergol", None)
shifted = glyphs("fr", "ergol", "LFSH")
altgr = glyphs("fr", "ergol", "RALT")
dead = level("fr", "ergol", ONE_DEAD_KEY)
hard = level("fr", "ergol", ALTGR_SHIFT)
return {
code: (base[code] or "★", shifted[code], altgr[code], dead[code], hard[code])
for code in MAIN_BLOCK
}
def keycode_map():
"""keymap-drawer's qmk_keycode_map, with the ergol levels of that same key."""
plain = glyphs("us", None, None)
caps = glyphs("us", None, "LFSH")
ergol = ergol_levels()
# A keycode that already means "shift + something" lands on one glyph only.
unshifted = {plain[code]: ergol[code] for code in MAIN_BLOCK if plain[code]}
shifted = {
caps[code]: (ergol[code][1], "", "", "", "")
for code in MAIN_BLOCK
if caps[code]
}
# Letters are absent from that map: it falls back to the keycode name.
out = {glyph.upper(): levels for glyph, levels in unshifted.items() if glyph.isalnum()}
for keycode, glyph in ParseConfig().qmk_keycode_map.items():
out[keycode] = unshifted.get(glyph) or shifted.get(glyph) or (glyph, "", "", "", "")
out["SPACE"] = ergol[SPACE]
return out
for keycode, (base, shifted, altgr, dead, hard) in keycode_map().items():
# Showing "q Q" on every letter is noise.
shifted = "" if shifted == base.upper() else shifted
# Keycaps are printed in majuscule.
base = base.upper() if base.isalpha() else base
# A cell cannot hold a bare pipe.
cells = [
VISIBLE.get(cell, cell).replace("|", r"\vert")
for cell in (base, shifted, altgr, dead, hard)
]
print("%s|%s|%s|%s|%s|%s" % (keycode, *cells))
nix shell --impure --expr 'with import <nixpkgs> {}; python3.withPackages (ps: [ ps.xkbcommon ps.keymap-drawer ])' --command python3 <<'EOF'
<<ergol_glyphs_py>>
EOF
| 1 | 1 | € | ₁ | „ | ¹ |
|---|---|---|---|---|---|
| 2 | 2 | « | ₂ | “ | ² |
| 3 | 3 | » | ₃ | ” | ³ |
| 4 | 4 | $ | ₄ | ¢ | ⁴ |
| 5 | 5 | % | ₅ | ‰ | ⁵ |
| 6 | 6 | ^ | ₆ | ⁶ | |
| 7 | 7 | & | ₇ | ⁷ | |
| 8 | 8 | * | ₈ | § | ⁸ |
| 9 | 9 | # | ₉ | ¶ | ⁹ |
| 0 | 0 | @ | ₀ | ° | ⁰ |
| Q | Q | ^ | â | ||
| W | C | < | ç | ≤ | |
| E | O | > | œ | ≥ | |
| R | P | $ | ô | ||
| T | W | % | ‰ | ||
| Y | J | @ | |||
| U | M | & | µ | ||
| I | D | * | _ | × | |
| O | ★ | ! | ' | ||
| P | Y | ` | û | ||
| A | A | { | à | ||
| S | S | ( | é | ||
| D | E | ) | è | ||
| F | N | } | ê | ||
| G | F | = | ñ | ≠ | |
| H | L | \ | ( | ||
| J | R | + | ) | ± | |
| K | T | - | î | ||
| L | I | / | ï | ÷ | |
| Z | Z | ~ | æ | ||
| X | X | [ | ß | ||
| C | - | ? | ] | ‑ | |
| V | V | _ | – | – | |
| B | B | # | — | ||
| N | . | : | | | … | ¦ |
| M | H | ! | ¬ | ||
| XXXXXXX | |||||
| NO | |||||
| MINUS | / | _ | ÷ | ||
| MINS | / | _ | ÷ | ||
| EQUAL | = | + | ≠ | ||
| EQL | = | + | ≠ | ||
| LEFT_BRACKET | [ | { | |||
| LBRC | [ | { | |||
| RIGHT_BRACKET | ] | } | |||
| RBRC | ] | } | |||
| BACKSLASH | \ | | | |||
| BSLS | \ | | | |||
| NONUS_HASH | » | ||||
| NUHS | » | ||||
| SEMICOLON | U | " | ù | ||
| SCLN | U | " | ù | ||
| QUOTE | ' | " | |||
| QUOT | ' | " | |||
| GRAVE | ` | ~ | |||
| GRV | ` | ~ | |||
| COMMA | G | ; | |||
| COMM | G | ; | |||
| DOT | , | ; | : | · | : |
| SLASH | K | ? | |||
| SLSH | K | ? | |||
| TILDE | ~ | ||||
| TILD | ~ | ||||
| EXCLAIM | € | ||||
| EXLM | € | ||||
| AT | « | ||||
| HASH | » | ||||
| DOLLAR | $ | ||||
| DLR | $ | ||||
| PERCENT | % | ||||
| PERC | % | ||||
| CIRCUMFLEX | ^ | ||||
| CIRC | ^ | ||||
| AMPERSAND | & | ||||
| AMPR | & | ||||
| ASTERISK | * | ||||
| ASTR | * | ||||
| LEFT_PAREN | # | ||||
| LPRN | # | ||||
| RIGHT_PAREN | @ | ||||
| RPRN | @ | ||||
| UNDERSCORE | _ | ||||
| UNDS | _ | ||||
| PLUS | + | ||||
| LEFT_CURLY_BRACE | { | ||||
| LCBR | { | ||||
| RIGHT_CURLY_BRACE | } | ||||
| RCBR | } | ||||
| PIPE | | | ||||
| COLON | U | ||||
| COLN | U | ||||
| DOUBLE_QUOTE | " | ||||
| DQUO | " | ||||
| DQT | " | ||||
| LEFT_ANGLE_BRACKET | G | ||||
| LABK | G | ||||
| LT | G | ||||
| RIGHT_ANGLE_BRACKET | ; | ||||
| RABK | ; | ||||
| GT | ; | ||||
| QUESTION | K | ||||
| QUES | K | ||||
| SPACE | ␣ | ⍽ | ␣ | ’ | ⎵ |
import json
import os
import re
import zipfile
KEYMAP = "zsa_voyager_konubinix_source/keymap.c"
# One key per element: either FUNC(args) or a bare keycode.
KEY = r"\w+\((?:[^()]|\([^()]*\))*\)|\w+"
# Keycodes keymap-drawer leaves as their raw name.
RENAMES = {
"BSPC": "DEL",
"LEFT_GUI": "super",
"RIGHT_GUI": "super",
"CW_TOGG": "caps word",
"QK_BOOT": "boot",
"AUDIO_VOL_DOWN": "vol -",
"AUDIO_VOL_UP": "vol +",
"AUDIO_MUTE": "mute",
"MEDIA_PREV_TRACK": "prev",
"MEDIA_NEXT_TRACK": "next",
"MEDIA_STOP": "stop",
"MEDIA_PLAY_PAUSE": "play",
"RGB_TOG": "rgb",
"RGB_MODE_FORWARD": "rgb anim",
"RGB_SLD": "rgb solid",
"RGB_VAD": "dimmer",
"RGB_VAI": "brighter",
"TOGGLE_LAYER_COLOR": "layer colors",
# rgblight_sethsv with those hues, per process_record_user.
"HSV_0_255_255": "red",
"HSV_74_255_255": "green",
"HSV_169_255_255": "blue",
}
# What a glyph is coloured by is the modifier that reaches it.
COLOURS = {
"shift": "#b03a2e",
"altgr": "#1a5fb4",
"both": "#7d3c98",
"dead": "#1e7d32",
# The modifiers that select no layout level.
"plain": "#6b6f76",
}
STYLE = """
rect.homerow { fill: #d7e3f2; }
rect.thumbrest { fill: #eaf0f8; }
/* Restated last: the thumb that reaches the layer stays the held pink. */
rect.held { fill: #fdd; }
text.glyph.tl { font-size: 24px; }
text.tr, text.left, text.right { font-size: 18px; }
text.glyph.tap { font-size: 15px; }
text.hold { font-size: 12px; }
text.left, text.right { dominant-baseline: alphabetic; }
text.left, text.right { transform: translateY(14px); }
text.hold { transform: translateY(11px); }
text.glyph.tap { transform: translateY(9px); }
text.tr, text.shiftmod.hold { fill: %(shift)s; }
text.right, text.altgrmod.hold { fill: %(altgr)s; }
text.glyph.tap { fill: %(both)s; }
text.left, text.deadkeymod.tl { fill: %(dead)s; }
text.plainmod.hold, text.modkey.tap { fill: %(plain)s; }
""" % COLOURS
# footer_text lands raw inside a <text>, so it can carry its own colours.
LEGEND = (
'base'
' · <tspan fill="%(shift)s">shift</tspan>'
' · <tspan fill="%(altgr)s">altgr</tspan>'
' · <tspan fill="%(both)s">altgr+shift</tspan>'
' · <tspan fill="%(dead)s">★ then key</tspan>'
) % COLOURS
# Nothing to do when run on its own rather than through the call.
if not SOURCE:
raise SystemExit
# An ipfs link may carry a ?filename= suffix, which is not part of the path.
source = zipfile.ZipFile(SOURCE.split("?")[0]).read(KEYMAP).decode()
layers = [
re.findall(KEY, re.sub("//.*", "", body))
for body in re.findall(r"LAYOUT_voyager\((.*?)\n\s*\),", source, re.S)
]
OUT = os.path.expanduser("~/.cache/voyager-ergol")
os.makedirs(OUT, exist_ok=True)
with open(os.path.join(OUT, "voyager.json"), "w") as f:
json.dump({"keyboard": "zsa/voyager", "layers": layers}, f)
with open(os.path.join(OUT, "ergol.json"), "w") as f:
keycode_map = {}
for keycode, base, shifted, altgr, dead, hard in glyphs:
spec = {"t": base, "s": shifted, "right": altgr, "left": dead, "br": hard}
keycode_map[str(keycode)] = {
field: str(value).replace(r"\vert", "|")
for field, value in spec.items()
if value != ""
}
keycode_map.update(RENAMES)
# Taps = and holds escape, per process_record_user.
keycode_map["DUAL_FUNC_0"] = {**keycode_map["EQUAL"], "h": "esc"}
json.dump(
{
"parse_config": {"qmk_keycode_map": keycode_map},
"draw_config": {
"svg_extra_style": STYLE,
"footer_text": LEGEND,
"shrink_wide_legends": 5,
"key_w": 76,
"key_h": 72,
# Clearance between a corner legend and the key border.
"small_pad": 15,
},
},
f,
)
import json
import sys
import yaml
# Readable label, and a class for the two that select a layout level, so the
# css can colour them like the glyphs they produce.
MODS = {
"MOD_LSFT": ("shift", "shiftmod"),
"MOD_RSFT": ("shift", "shiftmod"),
"MOD_RALT": ("altgr", "altgrmod"),
"MOD_LALT": ("alt", "plainmod"),
"MOD_LCTL": ("ctrl", "plainmod"),
"MOD_RCTL": ("ctrl", "plainmod"),
"MOD_LGUI": ("super", "plainmod"),
"MOD_RGUI": ("super", "plainmod"),
}
# A key that is only a modifier, rather than a modifier held on a letter.
MOD_NAMES = {"shift", "altgr", "alt", "ctrl", "super"}
# Third of the four rows, minus the outer pinky column and the inner index
# column, which are stretches: four fingers per hand actually rest here.
HOME_ROW = (25, 26, 27, 28, 31, 32, 33, 34)
# Of the two keys per thumb, the layer-tap is the one under the thumb at rest.
THUMBS = (48, 51)
path, config_path = sys.argv[1], sys.argv[2]
keymap = yaml.safe_load(open(path, encoding="utf-8"))
config = json.load(open(config_path, encoding="utf-8"))
# keymap-drawer builds mod-taps as tap/hold/shifted only, dropping left and
# right, which is exactly where the accents and the symbols live.
levels = {
spec["t"]: spec
for spec in config["parse_config"]["qmk_keycode_map"].values()
if isinstance(spec, dict) and "t" in spec
}
for layer in keymap["layers"].values():
for index, key in enumerate(layer):
# A key with nothing but a tap is a bare string, and cannot carry a class.
if isinstance(key, str):
key = layer[index] = {"t": key}
if not isinstance(key, dict):
continue
# keymap-drawer already typed the thumb that reaches the layer as
# held, and the transparent keys as trans. Keep what it found.
classes = key.get("type", "").split()
if index in HOME_ROW:
classes.append("homerow")
elif index in THUMBS:
classes.append("thumbrest")
if key.get("h") in MODS:
key["h"], mod_class = MODS[key["h"]]
if mod_class:
classes.append(mod_class)
if key.get("t") == "★":
classes.append("deadkeymod")
if key.get("t") in MOD_NAMES:
classes.append("modkey")
for side in ("left", "right", "br"):
if side not in key and side in levels.get(key.get("t"), {}):
key[side] = levels[key["t"]][side]
# A layout glyph goes to the top corners, leaving the sides for the
# other levels and the bottom middle for the modifier name. A word
# label needs the whole key and stays in the middle.
if len(str(key.get("t", ""))) == 1:
classes.append("glyph")
key["tl"] = key.pop("t")
if "s" in key:
key["tr"] = key.pop("s")
# The middle is free once the letter moved to the corner, and it is
# the only spot altgr+shift does not land on the altgr glyph.
if "br" in key:
key["t"] = key.pop("br")
if classes:
key["type"] = " ".join(classes)
with open(path, "w", encoding="utf-8") as f:
yaml.safe_dump(keymap, f, allow_unicode=True, sort_keys=False)
my cheatsheet
cd ~/.cache/voyager-ergol
nix shell nixpkgs#keymap-drawer --command keymap -c ergol.json parse -q voyager.json -o voyager.yaml
nix shell nixpkgs#python3Packages.pyyaml --command python3 - voyager.yaml ergol.json <<'EOF'
import json
import sys
import yaml
# Readable label, and a class for the two that select a layout level, so the
# css can colour them like the glyphs they produce.
MODS = {
"MOD_LSFT": ("shift", "shiftmod"),
"MOD_RSFT": ("shift", "shiftmod"),
"MOD_RALT": ("altgr", "altgrmod"),
"MOD_LALT": ("alt", "plainmod"),
"MOD_LCTL": ("ctrl", "plainmod"),
"MOD_RCTL": ("ctrl", "plainmod"),
"MOD_LGUI": ("super", "plainmod"),
"MOD_RGUI": ("super", "plainmod"),
}
# A key that is only a modifier, rather than a modifier held on a letter.
MOD_NAMES = {"shift", "altgr", "alt", "ctrl", "super"}
# Third of the four rows, minus the outer pinky column and the inner index
# column, which are stretches: four fingers per hand actually rest here.
HOME_ROW = (25, 26, 27, 28, 31, 32, 33, 34)
# Of the two keys per thumb, the layer-tap is the one under the thumb at rest.
THUMBS = (48, 51)
path, config_path = sys.argv[1], sys.argv[2]
keymap = yaml.safe_load(open(path, encoding="utf-8"))
config = json.load(open(config_path, encoding="utf-8"))
# keymap-drawer builds mod-taps as tap/hold/shifted only, dropping left and
# right, which is exactly where the accents and the symbols live.
levels = {
spec["t"]: spec
for spec in config["parse_config"]["qmk_keycode_map"].values()
if isinstance(spec, dict) and "t" in spec
}
for layer in keymap["layers"].values():
for index, key in enumerate(layer):
# A key with nothing but a tap is a bare string, and cannot carry a class.
if isinstance(key, str):
key = layer[index] = {"t": key}
if not isinstance(key, dict):
continue
# keymap-drawer already typed the thumb that reaches the layer as
# held, and the transparent keys as trans. Keep what it found.
classes = key.get("type", "").split()
if index in HOME_ROW:
classes.append("homerow")
elif index in THUMBS:
classes.append("thumbrest")
if key.get("h") in MODS:
key["h"], mod_class = MODS[key["h"]]
if mod_class:
classes.append(mod_class)
if key.get("t") == "★":
classes.append("deadkeymod")
if key.get("t") in MOD_NAMES:
classes.append("modkey")
for side in ("left", "right", "br"):
if side not in key and side in levels.get(key.get("t"), {}):
key[side] = levels[key["t"]][side]
# A layout glyph goes to the top corners, leaving the sides for the
# other levels and the bottom middle for the modifier name. A word
# label needs the whole key and stays in the middle.
if len(str(key.get("t", ""))) == 1:
classes.append("glyph")
key["tl"] = key.pop("t")
if "s" in key:
key["tr"] = key.pop("s")
# The middle is free once the letter moved to the corner, and it is
# the only spot altgr+shift does not land on the altgr glyph.
if "br" in key:
key["t"] = key.pop("br")
if classes:
key["type"] = " ".join(classes)
with open(path, "w", encoding="utf-8") as f:
yaml.safe_dump(keymap, f, allow_unicode=True, sort_keys=False)
EOF
nix shell nixpkgs#keymap-drawer --command keymap -c ergol.json draw voyager.yaml -o cheatsheet.svg
nix shell nixpkgs#librsvg --command rsvg-convert -z 1.5 -b white cheatsheet.svg -o cheatsheet.png
printf '%s' "$PWD/cheatsheet.png"
footnotes
Permalink
-
« Il y a deux façons d’utiliser Ergo‑L sur un clavier programmable : soit on laisse le clavier en Qwerty dans QMK/ZMK/Vial, et c’est le pilote côté ordi qui s’occupe de la disposition […] ; soit on cherche à émuler Ergo‑L côté QMK/ZMK/Vial, pour avoir la disposition sur un ordi qui reste configuré en Azerty. La première méthode est vivement recommandée. » — Installation, Ergo‑L, note aux utilisateurices de claviers programmables. ↩︎
-
« L’émulation est beaucoup plus compliquée, et n’est pertinente qu’en dernier ressort : ordinateur totalement verrouillé (même pour le pilote nomade), administration de nombreuses machines, ou autre besoin spécifique. » — Installation, Ergo‑L. ↩︎
-
« On peut alors configurer son clavier très facilement, il faut juste avoir AltGr sur une touche de pouce pour accéder aux symboles. » — Installation, Ergo‑L. ↩︎
-
“Choose whether to build your layout against ZSA’s QMK fork or mainline QMK. Using ZSA’s fork means your firmware will compile easily, but it is slower to update and does not pull all the new features of mainline QMK. […] If you would like to compile against ZSA’s fork, make sure to manually set the path when going through the setup: https://github.com/zsa/qmk_firmware/ (ZSA’s QMK fork). Otherwise, the setup process will default to mainline QMK (qmk/qmk_firmware).” — README.md, Oryx source export. ↩︎