feat: every key on the keyboard is bindable - the F-row, the six-pack, Caps Lock and the numpad

The platform gave these keys no code at all, so a game's rebinding screen could not take
one and nothing said why. Windows asked the active layout what they type and got nothing
(w_vk_char answers 0 for a key with no character); macOS let them fall through to
charactersIgnoringModifiers, which reports NSF1FunctionKey and its neighbours at 0xF704
and up - outside the 256-bit held set either way.

w_keyval and ev_keyval now name them, with the same codes on both: 132-143 F1-F12,
144-149 Home / End / PageUp / PageDown / Insert / Delete, 150 Caps Lock, 152-161 the
numpad digits, 162-166 its * + - . and /. The numpad's Enter is Enter.

Key.F1, Key.Home, Key.Numpad0 and the rest fold at compile time, and Input.key_label
names them without asking the layout - a key that types nothing is called the same thing
on every layout.

examples/library/input_typeless_keys.ludic covers the codes, the names, the held set and
the press edge; 150 passed in ludic-dev test, 33 in selfhost-test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-18 00:57:02 +03:00
parent 9dfa2951f6
commit 4979cc0c94
11 changed files with 21327 additions and 20462 deletions

19
changes/typeless-keys.md Normal file
View file

@ -0,0 +1,19 @@
bump: minor
type: feat
**Every key on the keyboard is bindable** — the F-row, Home/End/PageUp/PageDown,
Insert/Delete, Caps Lock and the numpad reach a game as codes 132-166.
Until now the platform gave them no code at all, so a game's rebinding screen could
not take one and nothing said why. Windows asked the active layout what they type and
got nothing back (`w_vk_char` answers 0 for a key with no character); macOS let them
fall through to `charactersIgnoringModifiers`, which reports `NSF1FunctionKey` and its
neighbours at 0xF704 and up - outside the 256-bit held set either way.
- **The codes are the same on both platforms**: 132-143 are F1-F12, 144-149 are Home,
End, PageUp, PageDown, Insert and Delete, 150 is Caps Lock, 152-161 are the numpad
digits and 162-166 its `*`, `+`, `-`, `.` and `/`. The numpad's Enter is Enter.
- **`Key.F1`, `Key.Home`, `Key.Numpad0`** and the rest fold at compile time like the
other named keys.
- **`Input.key_label`** names them - "F1", "Home", "CapsLock", "Num 0", "Num /" - and
does *not* ask the layout, because a key that types nothing is called the same thing
on every layout.

View file

@ -11,7 +11,7 @@ ns: Input
member: key_down member: key_down
--- ---
Returns whether a physical <code>key</code> is held this frame — from the platform when windowed, from the polled key when headless, and from <a href="input-press.html"><code>Input.press</code></a> injection on any target. Unlike the single-key <a href="input-down.html"><code>Input.down</code></a>, several keys can be held at once (move left <em>and</em> jump). Returns whether a physical <code>key</code> is held this frame — from the platform when windowed, from the polled key when headless, and from <a href="input-press.html"><code>Input.press</code></a> injection on any target. Unlike the single-key <a href="input-down.html"><code>Input.down</code></a>, several keys can be held at once (move left <em>and</em> jump). Every key on the keyboard has a code: letters, digits and punctuation are their US-layout character, Space, Enter, Esc, Tab, Backspace, Shift, Ctrl and Alt their usual ones, the arrows 128-131, and the keys that type nothing 132-166 - F1-F12 at 132, Home, End, PageUp, PageDown, Insert and Delete at 144, Caps Lock at 150, the numpad digits at 152 and its <code>*</code>, <code>+</code>, <code>-</code>, <code>.</code> and <code>/</code> at 162. <code>Key.F1</code>, <code>Key.Home</code> and <code>Key.Numpad0</code> name them.
```ludic ```ludic
program Demo { program Demo {

View file

@ -11,7 +11,7 @@ ns: Input
member: key_label member: key_label
--- ---
Returns the name to show for <code>key</code>. The letter, digit and punctuation codes that <a href="input-key_down.html"><code>Input.key_down</code></a> reads are <em>physical</em> keys, named for what they type on a US layout: <code>'w'</code> is the key above <code>'s'</code> on every keyboard, whatever layout, language, input method or Caps Lock state the player has. So WASD is always under the left hand, and the label is what that key types in the player's layout - <code>"W"</code> on QWERTY, <code>"Z"</code> on AZERTY. Space, Enter, Esc, Tab, Backspace, Shift, Ctrl, Alt and the arrows get words, and a code with no name returns <code>""</code>. Returns the name to show for <code>key</code>. The letter, digit and punctuation codes that <a href="input-key_down.html"><code>Input.key_down</code></a> reads are <em>physical</em> keys, named for what they type on a US layout: <code>'w'</code> is the key above <code>'s'</code> on every keyboard, whatever layout, language, input method or Caps Lock state the player has. So WASD is always under the left hand, and the label is what that key types in the player's layout - <code>"W"</code> on QWERTY, <code>"Z"</code> on AZERTY. Space, Enter, Esc, Tab, Backspace, Shift, Ctrl, Alt and the arrows get words, and a code with no name returns <code>""</code>. The keys that type no character - F1-F12, Home, End, PageUp, PageDown, Insert, Delete, Caps Lock and the numpad, codes 132-166 - are named <code>"F1"</code>, <code>"Home"</code>, <code>"CapsLock"</code>, <code>"Num 0"</code>, <code>"Num /"</code> and so on, and the layout is <em>not</em> asked: a key that types nothing is called the same thing on every layout.
On Windows the label is read from the active layout. Headless, on macOS and in the browser it is the US character, upper case. On Windows the label is read from the active layout. Headless, on macOS and in the browser it is the US character, upper case.

View file

@ -0,0 +1,38 @@
# input_typeless_keys.ludic — the keys that type no character: the F-row, the six-pack,
# Caps Lock and the numpad. Until 0.22 the platform gave them no code at all - Windows
# asked the layout what they type and got nothing, macOS got NSF1FunctionKey (0xF704 and
# up, outside the 256-bit held set) - so a game could not bind one and nothing said why.
# They are codes 132-166 now, the same on both, with a name that is not asked of the
# layout because it is the same on every layout.
#
# Deterministic; a full run prints: 132 143 144 149 150 152 161 F1 F12 Home Delete CapsLock Num 0 Num / 1 0 1
program InputTypelessKeys {
function bi(b: bool) -> int { if b { return 1 }; return 0 }
entry {
print(Key.F1)
print(Key.F12)
print(Key.Home)
print(Key.Delete)
print(Key.CapsLock)
print(Key.Numpad0)
print(Key.Numpad9)
print(Input.key_label(Key.F1))
print(Input.key_label(Key.F12))
print(Input.key_label(Key.Home))
print(Input.key_label(Key.Delete))
print(Input.key_label(Key.CapsLock))
print(Input.key_label(Key.Numpad0))
print(Input.key_label(Key.NumpadDivide))
Input.press(Key.F5) # hold F5
Input.poll()
print(bi(Input.key_down(Key.F5))) # 1 — it is in the held set like any other key
print(bi(Input.key_down(Key.F6))) # 0 — and only that one
Input.release(Key.F5)
Input.press(Key.PageDown)
Input.poll()
print(bi(Input.key_pressed(Key.PageDown))) # 1 — the edge fires for it too
}
}

View file

@ -380,6 +380,45 @@ special:
i32 36, label %vret i32 36, label %vret
i32 53, label %vesc i32 53, label %vesc
i32 51, label %vbksp i32 51, label %vbksp
; the keys that type nothing: charactersIgnoringModifiers answers them with
; NSF1FunctionKey and its neighbours, 0xF700 and up, which is outside the 256-bit held
; set - so a game could not bind an F-key, Home, Delete or the numpad at all. They get
; the same codes win32.ll's w_keyval gives them.
i32 122, label %vf1
i32 120, label %vf2
i32 99, label %vf3
i32 118, label %vf4
i32 96, label %vf5
i32 97, label %vf6
i32 98, label %vf7
i32 100, label %vf8
i32 101, label %vf9
i32 109, label %vf10
i32 103, label %vf11
i32 111, label %vf12
i32 115, label %vhome
i32 119, label %vend
i32 116, label %vpgup
i32 121, label %vpgdn
i32 114, label %vins
i32 117, label %vdel
i32 57, label %vcaps
i32 82, label %vnp0
i32 83, label %vnp1
i32 84, label %vnp2
i32 85, label %vnp3
i32 86, label %vnp4
i32 87, label %vnp5
i32 88, label %vnp6
i32 89, label %vnp7
i32 91, label %vnp8
i32 92, label %vnp9
i32 67, label %vnpmul
i32 69, label %vnpadd
i32 78, label %vnpsub
i32 65, label %vnpdec
i32 75, label %vnpdiv
i32 76, label %vret
] ]
vw: ret i32 128 vw: ret i32 128
vs: ret i32 129 vs: ret i32 129
@ -388,6 +427,40 @@ vd: ret i32 131
vspace: ret i32 32 vspace: ret i32 32
vret: ret i32 10 vret: ret i32 10
vesc: ret i32 27 vesc: ret i32 27
vf1: ret i32 132
vf2: ret i32 133
vf3: ret i32 134
vf4: ret i32 135
vf5: ret i32 136
vf6: ret i32 137
vf7: ret i32 138
vf8: ret i32 139
vf9: ret i32 140
vf10: ret i32 141
vf11: ret i32 142
vf12: ret i32 143
vhome: ret i32 144
vend: ret i32 145
vpgup: ret i32 146
vpgdn: ret i32 147
vins: ret i32 148
vdel: ret i32 149
vcaps: ret i32 150
vnp0: ret i32 152
vnp1: ret i32 153
vnp2: ret i32 154
vnp3: ret i32 155
vnp4: ret i32 156
vnp5: ret i32 157
vnp6: ret i32 158
vnp7: ret i32 159
vnp8: ret i32 160
vnp9: ret i32 161
vnpmul: ret i32 162
vnpadd: ret i32 163
vnpsub: ret i32 164
vnpdec: ret i32 165
vnpdiv: ret i32 166
; kVK_Delete (51), the key marked delete or backspace: AppKit reports its character as ; kVK_Delete (51), the key marked delete or backspace: AppKit reports its character as
; NSDeleteCharacter (127), and Key.Backspace is 8 ; NSDeleteCharacter (127), and Key.Backspace is 8
vbksp: ret i32 8 vbksp: ret i32 8

View file

@ -390,6 +390,22 @@ function input_key_label(k: int) -> string {
if k == 129 { return "Down" } if k == 129 { return "Down" }
if k == 130 { return "Left" } if k == 130 { return "Left" }
if k == 131 { return "Right" } if k == 131 { return "Right" }
# the keys that type nothing on any layout, so there is no character to show: the name is
# the same everywhere, which is why it is not asked of the layout
if k >= 132 and k <= 143 { return "F" + string(k - 131) }
if k == 144 { return "Home" }
if k == 145 { return "End" }
if k == 146 { return "PageUp" }
if k == 147 { return "PageDown" }
if k == 148 { return "Insert" }
if k == 149 { return "Delete" }
if k == 150 { return "CapsLock" }
if k >= 152 and k <= 161 { return "Num " + string(k - 152) }
if k == 162 { return "Num *" }
if k == 163 { return "Num +" }
if k == 164 { return "Num -" }
if k == 165 { return "Num ." }
if k == 166 { return "Num /" }
var c = 0 var c = 0
if is_windowed() { c = win_key_char(k) } if is_windowed() { c = win_key_char(k) }
if c <= 0 and k > 32 and k < 127 { if c <= 0 and k > 32 and k < 127 {

View file

@ -425,6 +425,42 @@ byvk:
i32 18, label %alt i32 18, label %alt
i32 8, label %bksp i32 8, label %bksp
i32 9, label %tab i32 9, label %tab
; the keys that type nothing, so w_vk_char answers 0 for them and they had no code at
; all: a game could not bind an F-key, Home, Delete or the numpad, and nothing said why
i32 112, label %f1
i32 113, label %f2
i32 114, label %f3
i32 115, label %f4
i32 116, label %f5
i32 117, label %f6
i32 118, label %f7
i32 119, label %f8
i32 120, label %f9
i32 121, label %f10
i32 122, label %f11
i32 123, label %f12
i32 36, label %khome
i32 35, label %kend
i32 33, label %kpgup
i32 34, label %kpgdn
i32 45, label %kins
i32 46, label %kdel
i32 20, label %kcaps
i32 96, label %np0
i32 97, label %np1
i32 98, label %np2
i32 99, label %np3
i32 100, label %np4
i32 101, label %np5
i32 102, label %np6
i32 103, label %np7
i32 104, label %np8
i32 105, label %np9
i32 106, label %npmul
i32 107, label %npadd
i32 109, label %npsub
i32 110, label %npdec
i32 111, label %npdiv
] ]
up: ret i32 128 up: ret i32 128
down: ret i32 129 down: ret i32 129
@ -438,6 +474,40 @@ ctrl: ret i32 17
alt: ret i32 18 alt: ret i32 18
bksp: ret i32 8 bksp: ret i32 8
tab: ret i32 9 tab: ret i32 9
f1: ret i32 132
f2: ret i32 133
f3: ret i32 134
f4: ret i32 135
f5: ret i32 136
f6: ret i32 137
f7: ret i32 138
f8: ret i32 139
f9: ret i32 140
f10: ret i32 141
f11: ret i32 142
f12: ret i32 143
khome: ret i32 144
kend: ret i32 145
kpgup: ret i32 146
kpgdn: ret i32 147
kins: ret i32 148
kdel: ret i32 149
kcaps: ret i32 150
np0: ret i32 152
np1: ret i32 153
np2: ret i32 154
np3: ret i32 155
np4: ret i32 156
np5: ret i32 157
np6: ret i32 158
np7: ret i32 159
np8: ret i32 160
np9: ret i32 161
npmul: ret i32 162
npadd: ret i32 163
npsub: ret i32 164
npdec: ret i32 165
npdiv: ret i32 166
chars: chars:
%c = call i32 @w_vk_char(i32 %vk) %c = call i32 @w_vk_char(i32 %vk)
; the held set is keyed by the key, not the case: fold A-Z (Shift is its own code) ; the held set is keyed by the key, not the case: fold A-Z (Shift is its own code)

View file

@ -20,6 +20,42 @@ function key_lookup(name: pointer) -> int {
if (name == "Down") { return 129 } if (name == "Down") { return 129 }
if (name == "Left") { return 130 } if (name == "Left") { return 130 }
if (name == "Right") { return 131 } if (name == "Right") { return 131 }
# the keys that type nothing on any layout (0.22): the F-row, the six-pack, Caps Lock and
# the numpad. Before these the platform gave them no code at all, so nothing could bind one.
if (name == "F1") { return 132 }
if (name == "F2") { return 133 }
if (name == "F3") { return 134 }
if (name == "F4") { return 135 }
if (name == "F5") { return 136 }
if (name == "F6") { return 137 }
if (name == "F7") { return 138 }
if (name == "F8") { return 139 }
if (name == "F9") { return 140 }
if (name == "F10") { return 141 }
if (name == "F11") { return 142 }
if (name == "F12") { return 143 }
if (name == "Home") { return 144 }
if (name == "End") { return 145 }
if (name == "PageUp") { return 146 }
if (name == "PageDown") { return 147 }
if (name == "Insert") { return 148 }
if (name == "Delete") { return 149 }
if (name == "CapsLock") { return 150 }
if (name == "Numpad0") { return 152 }
if (name == "Numpad1") { return 153 }
if (name == "Numpad2") { return 154 }
if (name == "Numpad3") { return 155 }
if (name == "Numpad4") { return 156 }
if (name == "Numpad5") { return 157 }
if (name == "Numpad6") { return 158 }
if (name == "Numpad7") { return 159 }
if (name == "Numpad8") { return 160 }
if (name == "Numpad9") { return 161 }
if (name == "NumpadMultiply") { return 162 }
if (name == "NumpadAdd") { return 163 }
if (name == "NumpadSubtract") { return 164 }
if (name == "NumpadDecimal") { return 165 }
if (name == "NumpadDivide") { return 166 }
if (name == "Digit0") { return 48 } if (name == "Digit0") { return 48 }
if (name == "Digit1") { return 49 } if (name == "Digit1") { return 49 }
if (name == "Digit2") { return 50 } if (name == "Digit2") { return 50 }

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -715,6 +715,7 @@ function cmd_dev_test() -> int {
feat_case("library/input_movement", "", "1 0 -1 -1 0", "input_movement.ludic (#79 Input.axis_i turns WASD into a -1/0/1 movement intent, no bool->int glue)") feat_case("library/input_movement", "", "1 0 -1 -1 0", "input_movement.ludic (#79 Input.axis_i turns WASD into a -1/0/1 movement intent, no bool->int glue)")
feat_case("library/input_arrows", "", "128 129 130 131 1 0 -1 1 0", "input_arrows.ludic (Key.Up/Down/Left/Right are the held set's 128-131: key_down and move_i take the arrows)") feat_case("library/input_arrows", "", "128 129 130 131 1 0 -1 1 0", "input_arrows.ludic (Key.Up/Down/Left/Right are the held set's 128-131: key_down and move_i take the arrows)")
feat_case("library/input_key_label", "", "W A ; 1 Space Esc Shift Left Right 0 1", "input_key_label.ludic (Input.key_label names a physical key: the US character headless, words for the named keys and arrows)") feat_case("library/input_key_label", "", "W A ; 1 Space Esc Shift Left Right 0 1", "input_key_label.ludic (Input.key_label names a physical key: the US character headless, words for the named keys and arrows)")
feat_case("library/input_typeless_keys", "", "132 143 144 149 150 152 161 F1 F12 Home Delete CapsLock Num 0 Num / 1 0 1", "input_typeless_keys.ludic (the F-row, the six-pack, Caps Lock and the numpad are codes 132-166: bindable, in the held set, and named without asking the layout)")
feat_case("library/audio", "", "0 0 0 0 1", "audio.ludic (#22 Audio.* load/play/music/volume/pitch/stop/is_playing — headless no-op)") feat_case("library/audio", "", "0 0 0 0 1", "audio.ludic (#22 Audio.* load/play/music/volume/pitch/stop/is_playing — headless no-op)")
# #6 Http.* — the client links Foundation (macOS-only), so build it through the # #6 Http.* — the client links Foundation (macOS-only), so build it through the
# canonical `ludicc -o` path (which wires the framework) and gate on Darwin. The # canonical `ludicc -o` path (which wires the framework) and gate on Darwin. The