key_equivalents.rs

  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}