Skip to content

Commit c396d86

Browse files
committed
[pwsh keymap] Add basics for analyzing key mappings.
1 parent d9542f8 commit c396d86

File tree

3 files changed

+342
-0
lines changed

3 files changed

+342
-0
lines changed
Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
# Copyright (c) 2023 Matthias Wolf, Mawosoft.
2+
3+
using namespace System
4+
using namespace System.Collections.Generic
5+
6+
<#
7+
.SYNOPSIS
8+
Key map for German standard keyboard layout.
9+
#>
10+
class KeyMapGerman {
11+
static [List[KeyMapGerman]]$CommonMap
12+
static [List[KeyMapGerman]]$NumPadMap
13+
[ConsoleKey]$ConsoleKey
14+
[char]$KeyChar
15+
[char]$KeyCharShift
16+
[char]$KeyCharAltGr
17+
18+
static KeyMapGerman() {
19+
[KeyMapGerman]::CommonMap = @(
20+
[KeyMapGerman]::new('D0', '0', '=', '}')
21+
[KeyMapGerman]::new('D1', '1', '!')
22+
[KeyMapGerman]::new('D2', '2', '"', '²')
23+
[KeyMapGerman]::new('D3', '3', '§', '³')
24+
[KeyMapGerman]::new('D4', '4', '$')
25+
[KeyMapGerman]::new('D5', '5', '%')
26+
[KeyMapGerman]::new('D6', '6', '&')
27+
[KeyMapGerman]::new('D7', '7', '/', '{')
28+
[KeyMapGerman]::new('D8', '8', '(', '[')
29+
[KeyMapGerman]::new('D9', '9', ')', ']')
30+
[KeyMapGerman]::new('Oem1', 'ü', 'Ü')
31+
[KeyMapGerman]::new('Oem2', '#', '''')
32+
[KeyMapGerman]::new('Oem3', 'ö', 'Ö')
33+
[KeyMapGerman]::new('Oem4', 'ß', '?', '\')
34+
[KeyMapGerman]::new('Oem5', '^', '°')
35+
[KeyMapGerman]::new('Oem6', '´', '`')
36+
[KeyMapGerman]::new('Oem7', 'ä', 'Ä')
37+
# Oem8 doesn't exist on German keyboard
38+
[KeyMapGerman]::new('Oem102', '<', '>', '|')
39+
[KeyMapGerman]::new('OemPlus', '+', '*', '~')
40+
[KeyMapGerman]::new('OemComma', ',', ';')
41+
[KeyMapGerman]::new('OemMinus', '-', '_')
42+
[KeyMapGerman]::new('OemPeriod', '.', ':')
43+
)
44+
[KeyMapGerman]::NumPadMap = @(
45+
[KeyMapGerman]::new('NumPad0', '0')
46+
[KeyMapGerman]::new('NumPad1', '1')
47+
[KeyMapGerman]::new('NumPad2', '2')
48+
[KeyMapGerman]::new('NumPad3', '3')
49+
[KeyMapGerman]::new('NumPad4', '4')
50+
[KeyMapGerman]::new('NumPad5', '5')
51+
[KeyMapGerman]::new('NumPad6', '6')
52+
[KeyMapGerman]::new('NumPad7', '7')
53+
[KeyMapGerman]::new('NumPad8', '8')
54+
[KeyMapGerman]::new('NumPad9', '9')
55+
[KeyMapGerman]::new('Multiply', '*')
56+
[KeyMapGerman]::new('Add', '+')
57+
[KeyMapGerman]::new('Separator', ',')
58+
[KeyMapGerman]::new('Subtract', '-')
59+
[KeyMapGerman]::new('Decimal', ',')
60+
[KeyMapGerman]::new('Divide', '/')
61+
)
62+
63+
for ([ConsoleKey]$k = [ConsoleKey]::A; $k -le [ConsoleKey]::Z; $k++) {
64+
[KeyMapGerman]::CommonMap.Add([KeyMapGerman]::new($k, $k -bor 0x20, $k))
65+
}
66+
[KeyMapGerman]::CommonMap.Find({ param([KeyMapGerman]$m) $m.ConsoleKey -eq [ConsoleKey]::Q }).KeyCharAltGr = '@'
67+
[KeyMapGerman]::CommonMap.Find({ param([KeyMapGerman]$m) $m.ConsoleKey -eq [ConsoleKey]::E }).KeyCharAltGr = ''
68+
[KeyMapGerman]::CommonMap.Find({ param([KeyMapGerman]$m) $m.ConsoleKey -eq [ConsoleKey]::M }).KeyCharAltGr = 'µ'
69+
foreach ($k in [Enum]::GetValues([ConsoleKey])) {
70+
if (-not [KeyMapGerman]::CommonMap.Exists({ param([KeyMapGerman]$m) $m.ConsoleKey -eq $k }) -and
71+
-not [KeyMapGerman]::NumPadMap.Exists({ param([KeyMapGerman]$m) $m.ConsoleKey -eq $k })) {
72+
[KeyMapGerman]::CommonMap.Add([KeyMapGerman]::new($k))
73+
}
74+
}
75+
76+
}
77+
78+
<#
79+
.SYNOPSIS
80+
Find keymap entry for given key or key name.
81+
#>
82+
static [KeyMapGerman]Find([string]$key, [bool]$noNumPad) {
83+
if (-not $key) { return $null }
84+
if ($key.Length -gt 1) {
85+
if ($key.Length -eq 5 -and $key.StartsWith('Num ', [StringComparison]::OrdinalIgnoreCase)) {
86+
[char]$c = $key[4]
87+
[KeyMapGerman]$km = [KeyMapGerman]::NumPadMap.Find({ param([KeyMapGerman]$m) $m.KeyChar -ceq $c })
88+
if ($km) {
89+
$key = $km.ConsoleKey.ToString()
90+
}
91+
}
92+
elseif ($key.StartsWith('NumPad', [StringComparison]::OrdinalIgnoreCase)) {
93+
if ($key.Length -gt 6 -and $key[6] -ceq '_') {
94+
$key = $key.Remove(6, 1)
95+
}
96+
if ($key.Length -gt 6 -and $key[6] -cnotlike '[0-9]') {
97+
$key = $key.Substring(6)
98+
}
99+
}
100+
elseif ($key.StartsWith('Oem', [StringComparison]::OrdinalIgnoreCase)) {
101+
if ($key.Length -gt 3 -and $key[3] -ceq '_') {
102+
$key = $key.Remove(3, 1)
103+
}
104+
}
105+
elseif ($key -in 'down', 'up', 'left', 'right') {
106+
$key += 'Arrow'
107+
}
108+
elseif ($key.EndsWith(' Arrow', [StringComparison]::OrdinalIgnoreCase)) {
109+
$key = $key.Remove($Key.Length - 6, 1)
110+
}
111+
else {
112+
switch ($key) {
113+
'space' { $key = 'Spacebar'; break; }
114+
'Bkspce' { $key = 'Backspace'; break; }
115+
'Del' { $key = 'Delete'; break; }
116+
'Esc' { $key = 'Escape'; break; }
117+
'Ins' { $key = 'Insert'; break; }
118+
'PgDn' { $key = 'PageDown'; break; }
119+
'PgUp' { $key = 'PageUp'; break; }
120+
'Break' { $key = 'Pause'; break; }
121+
Default {}
122+
}
123+
}
124+
}
125+
if ($key.Length -gt 1) {
126+
[ConsoleKey]$ck = $key
127+
[KeyMapGerman]$km = [KeyMapGerman]::NumPadMap.Find({ param([KeyMapGerman]$m) $m.ConsoleKey -eq $ck })
128+
if ($km) {
129+
if (-not $noNumPad) { return $km }
130+
$key = $km.KeyChar
131+
}
132+
else {
133+
$km = [KeyMapGerman]::CommonMap.Find({ param([KeyMapGerman]$m) $m.ConsoleKey -eq $ck })
134+
if ($km) { return $km }
135+
}
136+
}
137+
if ($key.Length -eq 1) {
138+
[char]$c = $key[0]
139+
[KeyMapGerman]$km = [KeyMapGerman]::CommonMap.Find({ param([KeyMapGerman]$m) $m.KeyChar -ceq $c })
140+
if ($km) { return $km }
141+
[KeyMapGerman]$km = [KeyMapGerman]::CommonMap.Find({ param([KeyMapGerman]$m) $m.KeyCharShift -ceq $c })
142+
if ($km) { return $km }
143+
return [KeyMapGerman]::CommonMap.Find({ param([KeyMapGerman]$m) $m.KeyCharAltGr -ceq $c })
144+
}
145+
return $null
146+
}
147+
148+
<#
149+
.SYNOPSIS
150+
Parse one or more chords (key combo with modifiers) into [Chord] objects.
151+
.PARAMETER $chord
152+
The chord to parse, e.g. 'Ctrl+k,Ctrl+j'
153+
.PARAMETER $separator
154+
Separator for multiple chords.
155+
This becomes part of a regex, so escape appropriately.
156+
.PARAMETER $noNumPad
157+
If true, maps any numpad keys defined in the chord to their standard block counterparts,
158+
e.g. NumPad0 -> D0, Multiply -> *
159+
#>
160+
static [Chord[]]ParseChord([string]$chord, [string]$separator, [bool]$noNumPad) {
161+
[string[]]$parts = [regex]::Split($chord, '(?<!\+)' + $separator)
162+
return $parts.ForEach({ [Chord]::new($_, $noNumPad) })
163+
}
164+
165+
<#
166+
.SYNOPSIS
167+
Parse the chord defined in a binding object into [Chord] objects.
168+
.PARAMETER $bindingObject
169+
Single entry from imported key bindings (usually from a JSON file).
170+
Bindings from PSReadline, VSCode, and Visual Studio are recognized automatically.
171+
.PARAMETER $noNumPad
172+
If true, maps any numpad keys defined in the chord to their standard block counterparts,
173+
e.g. NumPad0 -> D0, Multiply -> *
174+
#>
175+
static [Chord[]]ParseChord([psobject]$bindingObject, [bool]$noNumPad) {
176+
$p = $bindingObject.psobject.Properties
177+
if ($p['KeyBinding'] -and $p['Scope']) {
178+
# Visual Studio
179+
return [KeyMapGerman]::ParseChord($p['KeyBinding'].Value, ', ', $noNumpad)
180+
}
181+
elseif ($p['key'] -and $p['command']) {
182+
# VSCode
183+
return [KeyMapGerman]::ParseChord($p['key'].Value, ' ', $noNumpad)
184+
}
185+
elseif ($p['Key'] -and $p['Function']) {
186+
# PSReadline
187+
return [KeyMapGerman]::ParseChord($p['Key'].Value, ',', $noNumpad)
188+
}
189+
throw [ArgumentException]::new($null, 'bindingObject')
190+
}
191+
192+
KeyMapGerman() {
193+
}
194+
KeyMapGerman([ConsoleKey]$consoleKey) {
195+
$this.ConsoleKey = $consoleKey
196+
}
197+
KeyMapGerman([ConsoleKey]$consoleKey, [char]$keyChar) {
198+
$this.ConsoleKey = $consoleKey
199+
$this.KeyChar = $keyChar
200+
}
201+
KeyMapGerman([ConsoleKey]$consoleKey, [char]$keyChar, [char]$keyCharShift) {
202+
$this.ConsoleKey = $consoleKey
203+
$this.KeyChar = $keyChar
204+
$this.KeyCharShift = $keyCharShift
205+
}
206+
KeyMapGerman([ConsoleKey]$consoleKey, [char]$keyChar, [char]$keyCharShift, [char]$keyCharAltGr) {
207+
$this.ConsoleKey = $consoleKey
208+
$this.KeyChar = $keyChar
209+
$this.KeyCharShift = $keyCharShift
210+
$this.KeyCharAltGr = $keyCharAltGr
211+
}
212+
213+
<#
214+
.SYNOPSIS
215+
Gets a normalized string representation of the key map entry with the specified
216+
modifiers applied.
217+
#>
218+
[string]GetNormalized([ConsoleModifiers]$modifiers, [bool]$windowsKeyModifier) {
219+
[string]$s = ''
220+
if ($modifiers.HasFlag([ConsoleModifiers]::Control)) {
221+
$s += 'Ctrl+'
222+
}
223+
if ($modifiers.HasFlag([ConsoleModifiers]::Alt)) {
224+
$s += 'Alt+'
225+
}
226+
if ($modifiers.HasFlag([ConsoleModifiers]::Shift)) {
227+
$s += 'Shift+'
228+
}
229+
if ($windowsKeyModifier) {
230+
$s += 'Win+'
231+
}
232+
if ($this.ConsoleKey -in [System.ConsoleKey]::A..[System.ConsoleKey]::Z) {
233+
return $s + $this.KeyChar
234+
}
235+
else {
236+
return $s + $this.ConsoleKey.ToString()
237+
}
238+
}
239+
240+
<#
241+
.SYNOPSIS
242+
Checks if the key map entry is valid with the given modifiers.
243+
.NOTES
244+
For example, Ctrl+Alt+? doesn't work on a German keyboard because it really is
245+
Ctrl+Alt+Shift+Oem4, which has an AltGr character (ß ? \).
246+
#>
247+
[bool]IsValid([ConsoleModifiers]$modifiers) {
248+
if (-not $modifiers -or -not $this.KeyChar -or
249+
-not $modifiers.HasFlag([System.ConsoleModifiers]'Control, Alt, Shift')) {
250+
return $true
251+
}
252+
return -not $this.KeyCharAltGr
253+
}
254+
}
255+
256+
<#
257+
.SYNOPSIS
258+
Contains information about a parsed chord.
259+
#>
260+
class Chord {
261+
[string]$Original
262+
[string]$OriginalKey
263+
[string]$Normalized
264+
[KeyMapGerman]$KeyMap
265+
[ConsoleModifiers]$Modifiers
266+
[bool]$WindowsKeyModifier
267+
[bool]$IsValid
268+
269+
Chord() {}
270+
271+
Chord([string]$chord, [bool]$noNumPad) {
272+
$this.Original = $chord
273+
[bool]$invalid = $false
274+
[string[]]$keys = [regex]::Split($chord, '(?<!\+)\+')
275+
[int]$n = $keys.Length - 1
276+
for ([int]$i = 0; $i -lt $n; $i++) {
277+
switch ($keys[$i]) {
278+
'shift' { $this.Modifiers = $this.Modifiers -bor [ConsoleModifiers]::Shift; break }
279+
'alt' { $this.Modifiers = $this.Modifiers -bor [ConsoleModifiers]::Alt; break }
280+
'ctrl' { $this.Modifiers = $this.Modifiers -bor [ConsoleModifiers]::Control; break }
281+
'win' { $this.WindowsKeyModifier = $true; break }
282+
Default { $invalid = $true }
283+
}
284+
}
285+
$this.OriginalKey = $keys[$keys.Length - 1]
286+
$this.KeyMap = [KeyMapGerman]::Find($this.OriginalKey, $noNumPad)
287+
if ($this.KeyMap) {
288+
if ($this.OriginalKey.Length -eq 1) {
289+
if ($this.OriginalKey[0] -ceq $this.KeyMap.KeyCharShift) {
290+
$this.Modifiers = $this.Modifiers -bor [ConsoleModifiers]::Shift;
291+
}
292+
elseif ($this.OriginalKey[0] -ceq $this.KeyMap.KeyCharAltGr) {
293+
$this.Modifiers = $this.Modifiers -bor [ConsoleModifiers]::Control -bor [ConsoleModifiers]::Alt;
294+
}
295+
}
296+
$this.Normalized = $this.KeyMap.GetNormalized($this.Modifiers, $this.WindowsKeyModifier)
297+
if (-not $invalid) {
298+
$this.IsValid = $this.KeyMap.IsValid($this.Modifiers)
299+
}
300+
}
301+
}
302+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Copyright (c) 2023 Matthias Wolf, Mawosoft.
2+
3+
<#
4+
.SYNOPSIS
5+
Validates PSReadLine keybindings for German keyboard layout.
6+
.OUTPUTS
7+
[Chord] objects for invalid keybindings.
8+
.NOTES
9+
Currently (PSReadLine 2.3.4) returns 'Ctrl+Alt+?'.
10+
#>
11+
[CmdletBinding()]
12+
[OutputType([object])]
13+
param()
14+
15+
. "$PSScriptRoot/KeyMapGerman.ps1"
16+
17+
$allHandlers = Get-PSReadLineKeyHandler
18+
$allHandlers | ForEach-Object {
19+
[Chord[]]$chords = [KeyMapGerman]::ParseChord($_, $true)
20+
if ($chords.IsValid -contains $false) {
21+
$chords
22+
}
23+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Copyright (c) 2023 Matthias Wolf, Mawosoft.
2+
3+
<#
4+
.SYNOPSIS
5+
Verifies consistency of PSReadLine keybindings.
6+
.OUTPUTS
7+
Compare-Object results
8+
.NOTES
9+
Currently (PSReadLine 2.3.4) returns 'Ctrl+Alt+?'.
10+
#>
11+
[CmdletBinding()]
12+
[OutputType([psobject])]
13+
param()
14+
$allHandlers = Get-PSReadLineKeyHandler
15+
[string[]]$allKeys = $allHandlers | Select-Object -ExpandProperty Key
16+
$individualHandlers = Get-PSReadLineKeyHandler -Chord $allKeys
17+
Compare-Object -ReferenceObject $allHandlers -DifferenceObject $individualHandlers -Property Key, Function, Group, Description

0 commit comments

Comments
 (0)