1use collections::HashMap;
2
3// On some keyboards (e.g. German QWERTZ) it is not possible to type the full ASCII range
4// without using option. This means that some of our built in keyboard shortcuts do not work
5// for those users.
6//
7// The way macOS solves this problem is to move shortcuts around so that they are all reachable,
8// even if the mnemoic changes. https://developer.apple.com/documentation/swiftui/keyboardshortcut/localization-swift.struct
9//
10// For example, cmd-> is the "switch window" shortcut because the > key is right above tab.
11// To ensure this doesn't cause problems for shortcuts defined for a QWERTY layout, apple moves
12// any shortcuts defined as cmd-> to cmd-:. Coincidentally this s also the same keyboard position
13// as cmd-> on a QWERTY layout.
14//
15// Another example is cmd-[ and cmd-], as they cannot be typed without option, those keys are remapped to cmd-ö
16// and cmd-ä. These shortcuts are not in the same position as a QWERTY keyboard, because on a QWERTZ keyboard
17// the + key is in the way; and shortcuts bound to cmd-+ are still typed as cmd-+ on either keyboard (though the
18// specific key moves)
19//
20// As far as I can tell, there's no way to query the mappings Apple uses except by rendering a menu with every
21// possible key combination, and inspecting the UI to see what it rendered. So that's what we did...
22//
23// These mappings were generated by running https://github.com/ConradIrwin/keyboard-inspector, tidying up the
24// output to remove languages with no mappings and other oddities, and converting it to a less verbose representation with:
25// jq -s 'map(to_entries | map({key: .key, value: [(.value | to_entries | map(.key) | join("")), (.value | to_entries | map(.value) | join(""))]}) | from_entries) | add'
26// From there I used multi-cursor to produce this match statement.
27#[cfg(target_os = "macos")]
28pub fn get_key_equivalents(layout: &str) -> Option<HashMap<char, char>> {
29 let (from, to) = match layout {
30 "com.apple.keylayout.Welsh" => ("#", "£"),
31 "com.apple.keylayout.Turkmen" => ("qc]Q`|[XV\\^v~Cx}{", "äçöÄžŞňÜÝş№ýŽÇüÖŇ"),
32 "com.apple.keylayout.Turkish-QWERTY-PC" => (
33 "$\\|`'[}^=.#{*+:/~;)(@<,&]>\"",
34 "+,;<ığÜ&.ç^Ğ(:Ş*>ş=)'Öö/üÇI",
35 ),
36 "com.apple.keylayout.Sami-PC" => (
37 "}*x\"w[~^/@`]{|<)>W(\\X=Qq&':;",
38 "Æ(čŊšøŽ&´\"žæØĐ;=:Š)đČ`Áá/ŋÅå",
39 ),
40 "com.apple.keylayout.LatinAmerican" => {
41 ("[^~>`(<\\@{;*&/):]|='}\"", "{&>:<);¿\"[ñ(/'=Ñ}¡*´]¨")
42 }
43 "com.apple.keylayout.IrishExtended" => ("#", "£"),
44 "com.apple.keylayout.Icelandic" => ("[}=:/'){(*&;^|`\"\\>]<~@", "æ´*Ð'ö=Æ)(/ð&Þ<Öþ:´;>\""),
45 "com.apple.keylayout.German-DIN-2137" => {
46 ("}~/<^>{`:\\)&=[]@|;#'\"(*", "Ä>ß;&:Ö<Ü#=/*öä\"'ü§´`)(")
47 }
48 "com.apple.keylayout.FinnishSami-PC" => {
49 (")=*\"\\[@{:>';/<|~(]}^`&", "=`(ˆ@ö\"ÖÅ:¨å´;*>)äÄ&</")
50 }
51 "com.apple.keylayout.FinnishExtended" => {
52 ("];{`:'*<~=/}\\|&[\"($^)>@", "äåÖ<Ũ(;>`´Ä'*/öˆ)€&=:\"")
53 }
54 "com.apple.keylayout.Faroese" => ("}\";/$>^@~`:&[*){|]=(\\<'", "ÐØæ´€:&\"><Æ/å(=Å*ð`)';ø"),
55 "com.apple.keylayout.Croatian-PC" => {
56 ("{@~;<=>(&*['|]\":/}^`)\\", "Š\">č;*:)/(šćŽđĆČ'Đ&<=ž")
57 }
58 "com.apple.keylayout.Croatian" => ("{@;<~=>(&*['|]\":}^)\\`", "Š\"č;>*:)'(šćŽđĆČĐ&=ž<"),
59 "com.apple.keylayout.Azeri" => (":{W?./\"[}<]|,>';w", "IÖÜ,ş.ƏöĞÇğ/çŞəıü"),
60 "com.apple.keylayout.Albanian" => ("\\'~;:|<>`\"@", "ë@>çÇË;:<'\""),
61 "com.apple.keylayout.SwissFrench" => (
62 ":@&'~^)$;\"][\\/#={!|*+`<(>}",
63 "ü\"/^>&=çè`àé$'*¨ö+£(!<;):ä",
64 ),
65 "com.apple.keylayout.Swedish" => ("(]\\\"~$`^{|/>*:;<)&=[}'@", ")ä'^>€<&Ö*´:(Åå;=/`öĨ\""),
66 "com.apple.keylayout.Swedish-Pro" => {
67 ("/^*`'{|)$>&<[\\;(~\"}@]:=", "´&(<¨Ö*=€:/;ö'å)>^Ä\"äÅ`")
68 }
69 "com.apple.keylayout.Spanish" => ("|!\\<{[:;@`/~].'>}\"^", "\"¡'¿Ññº´!<.>;ç`Ç:¨/"),
70 "com.apple.keylayout.Spanish-ISO" => (
71 "|~`]/:)(<&^>*;#}\"{.\\['@",
72 "\"><;.º=)¿/&Ç(´·not found¨Ñç'ñ`\"",
73 ),
74 "com.apple.keylayout.Portuguese" => (")`/'^\"<];>[:{@}(&*=~", "=<'´&`;~º:çªÇ\"^)/(*>"),
75 "com.apple.keylayout.Italian" => (
76 "*7};8:!5%(1&4]^\\6)32>.</0|$,'{[`\"~9#@",
77 "8)*ò£!1ç59&7($6§è0'\"/:.,é°4;ù^ì<%>à32",
78 ),
79 "com.apple.keylayout.Italian-Pro" => {
80 ("/:@[]'\\=){;|#<\"(*^&`}>~", "'é\"òàìù*=çè§£;^)(&/<°:>")
81 }
82 "com.apple.keylayout.Irish" => ("#", "£"),
83 "com.apple.keylayout.German" => ("=`#'}:)/\"^&]*{;|[<(>~@\\", "*<§´ÄÜ=ß`&/ä(Öü'ö;):>\"#"),
84 "com.apple.keylayout.French" => (
85 "*}7;8:!5%(1&4]\\^6)32>.</0|${'[`\"~9#@",
86 "8*è)!°1(59&7'$`6§0\"é/;.:à£4¨ù^<%>ç32",
87 ),
88 "com.apple.keylayout.French-numerical" => (
89 "|!52;][>&@\"%'{)<~7.1/^(}*8#0$9`6\\3:4",
90 "£1(é)$^/72%5ù¨0.>è;&:69*8!3à4ç<§`\"°'",
91 ),
92 "com.apple.keylayout.French-PC" => (
93 "!&\"_$}/72>8]#:31)*<%4;6\\-{['@(0|5.`9~^",
94 "17%°4£:èé/_$3§\"&08.5'!-*)¨^ù29àμ(;<ç>6",
95 ),
96 "com.apple.keylayout.Finnish" => ("/^*`)'{|$>&<[\\~;(\"}@]:=", "´&(<=¨Ö*€:/;ö'>å)^Ä\"äÅ`"),
97 "com.apple.keylayout.Danish" => ("=[;'`{}|>]*^(&@~)<\\/$\":", "`æå¨<ÆØ*:ø(&)/\">=;'´€^Å"),
98 "com.apple.keylayout.Canadian-CSA" => ("\\?']/><[{}|~`\"", "àÉèçé\"'^¨ÇÀÙùÈ"),
99 "com.apple.keylayout.British" => ("#", "£"),
100 "com.apple.keylayout.Brazilian-ABNT2" => ("\"|~?`'/^\\", "`^\"Ç'´ç¨~"),
101 "com.apple.keylayout.Belgian" => (
102 "`3/*<\\8>7#&96@);024(|'1\":$[~5.%^}]{!",
103 "<\":8.`!/è37ç§20)àé'9£ù&%°4^>(;56*$¨1",
104 ),
105 "com.apple.keylayout.Austrian" => ("/^*`'{|)>&<[\\;(~\"}@]:=#", "ß&(<´Ö'=:/;ö#ü)>`Ä\"äÜ*§"),
106 "com.apple.keylayout.Slovak-QWERTY" => (
107 "):9;63'\"]^/+@~>`?<!#5&${2}%*18(704[",
108 "0\"íôžš§!ä6'%2Ň:ňˇ?13ť74ÚľÄ58+á9ýéčú",
109 ),
110 "com.apple.keylayout.Slovak" => (
111 "!$`10&:#4^*~{%5')}6/\"[8]97?;<@23>(+",
112 "14ň+é7\"3č68ŇÚ5ť§0Äž'!úáäíýˇô?2ľš:9%",
113 ),
114 "com.apple.keylayout.Polish" => (
115 "&)|?,%:;^}]_{!+#(*`/[~<\"$.>'@=\\",
116 ":\"$Ż.+Łł=)(ćź§]!/_<żó>śę?,ńą%[;",
117 ),
118 "com.apple.keylayout.Lithuanian" => ("+#&=!%1*@73^584$26", "ŽĘŲžĄĮąŪČųęŠįūėĖčš"),
119 "com.apple.keylayout.Hungarian" => (
120 "}(*@\"{=/|;>'[`<~\\!$&0#:]^)+",
121 "Ú)(\"ÁŐóüŰé:áőíÜÍű'!=ö+Éú/ÖÓ",
122 ),
123 "com.apple.keylayout.Hungarian-QWERTY" => (
124 "=]#>@/&<`0')~(\\!:*;$\"+^{|}[",
125 "óú+:\"ü=ÜíöáÖÍ)ű'É(é!ÁÓ/ŐŰÚő",
126 ),
127 "com.apple.keylayout.Czech-QWERTY" => (
128 "9>0[2()\"}@]46%5;#8{*7^~+!3?&'<$/1`:",
129 "í:éúě90!(2)čž5řů3áÚ8ý6`%1šˇ7§?4'+¨\"",
130 ),
131 "com.apple.keylayout.Maltese" => ("[`}{#]~", "ġżĦĠ£ħŻ"),
132 "com.apple.keylayout.Turkish" => (
133 "|}(#>&^-/`$%@]~*,[\"<_.{:'\\)",
134 "ÜI%\"Ç)/ş.<'(*ı>_öğ-ÖŞçĞ$,ü:",
135 ),
136 "com.apple.keylayout.Turkish-Standard" => {
137 ("|}(#>=&^`@]~*,;[\"<.{:'\\)", "ÜI)^;*'&ö\"ıÖ(.çğŞ:,ĞÇşü=")
138 }
139 "com.apple.keylayout.NorwegianSami-PC" => {
140 ("\"}~<`&>':{@*^|\\)=([]/;", "ˆÆ>;</:¨ÅØ\"(&*@=`)øæ´å")
141 }
142 "com.apple.keylayout.Serbian-Latin" => {
143 (";\\@>&'<]\"|(=}^)`[~:*{", "čž\":'ć;đĆŽ)*Đ&=<š>Č(Š")
144 }
145 "com.apple.keylayout.Slovenian" => ("]`^@)&\":'*=<{;}(~>\\|[", "đ<&\"='ĆČć(*;ŠčĐ)>:žŽš"),
146 "com.apple.keylayout.SwedishSami-PC" => {
147 ("@=<^|`>){'&\"}]~[/:*\\(;", "\"`;&*<:=Ö¨/ˆÄä>ö´Å(@)å")
148 }
149 "com.apple.keylayout.SwissGerman" => (
150 "={#:\\}!(+]/<\";$'`*[>&^~@)|",
151 "¨é*è$à+)!ä';`üç^<(ö:/&>\"=£",
152 ),
153 "com.apple.keylayout.Hawaiian" => ("'", "ʻ"),
154 "com.apple.keylayout.NorthernSami" => (
155 ":/[<{X\"wQx\\(;~>W}`*@])'^|=q&",
156 "Å´ø;ØČŊšÁčđ)åŽ:ŠÆž(\"æ=ŋ&Đ`á/",
157 ),
158 "com.apple.keylayout.USInternational-PC" => ("^~", "ˆ˜"),
159 "com.apple.keylayout.NorwegianExtended" => ("^~", "ˆ˜"),
160 "com.apple.keylayout.Norwegian" => ("`'~\"\\*|=/@)[:}&><]{(^;", "<¨>^@(*`´\"=øÅÆ/:;æØ)&å"),
161 "com.apple.keylayout.ABC-QWERTZ" => {
162 ("\"}~<`>'&#:{@*^|\\)=(]/;[", "`Ä>;<:´/§ÜÖ\"(&'#=*)äßüö")
163 }
164 "com.apple.keylayout.ABC-AZERTY" => (
165 ">[$61%@7|)&8\":}593(.4^<!{`2]\\#;~*/'0",
166 "/^4§&52è£07!%°*(ç\"9;'6.1¨<é$`3)>8:ùà",
167 ),
168 "com.apple.keylayout.Czech" => (
169 "(7*#193620?/{)@~!$8+;:%4\">`^]&5}[<'",
170 "9ý83+íšžěéˇ'Ú02`14á%ů\"5č!:¨6)7ř(ú?§",
171 ),
172 "com.apple.keylayout.Brazilian-Pro" => ("^~", "ˆ˜"),
173 _ => {
174 return None;
175 }
176 };
177 debug_assert!(from.chars().count() == to.chars().count());
178
179 Some(HashMap::from_iter(from.chars().zip(to.chars())))
180}
181
182#[cfg(not(target_os = "macos"))]
183pub fn get_key_equivalents(_layout: &str) -> Option<HashMap<char, char>> {
184 None
185}