├── .clippy.toml ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── convert.py ├── docs ├── ATTRIBUTION.md ├── keyboard_left_shift_key.svg ├── keyboard_numpad_1_key.svg ├── keyboard_right_shift_key.svg └── keyboard_standard_1_key.svg └── src ├── code.rs ├── lib.rs ├── location.rs ├── modifiers.rs ├── named_key.rs ├── shortcuts.rs └── webdriver.rs /.clippy.toml: -------------------------------------------------------------------------------- 1 | doc-valid-idents = ["WebDriver", ".."] 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: Continuous integration 4 | 5 | jobs: 6 | check-test: 7 | name: Check and test crate 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | toolchain: 12 | - stable 13 | - "1.61" 14 | - "1.81" 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: dtolnay/rust-toolchain@master 18 | with: 19 | toolchain: ${{ matrix.toolchain }} 20 | # All features 21 | - run: cargo check --all-targets --all-features 22 | - run: cargo test --all-features 23 | # No default features. Only works on Rust 1.81 24 | - if: matrix.toolchain != 1.61 25 | run: cargo check --all-targets --no-default-features 26 | - if: matrix.toolchain != 1.61 27 | run: cargo test --no-default-features 28 | 29 | clippy-fmt: 30 | name: Run Clippy and format code 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/checkout@v4 34 | - uses: dtolnay/rust-toolchain@stable 35 | with: 36 | components: clippy, rustfmt 37 | - run: cargo clippy --all-targets -- -D warnings 38 | - run: cargo fmt --check 39 | 40 | check-docs: 41 | name: Check the docs 42 | runs-on: ubuntu-latest 43 | steps: 44 | - uses: actions/checkout@v4 45 | - uses: dtolnay/rust-toolchain@stable 46 | - run: cargo doc --all-features --no-deps --document-private-items 47 | env: 48 | RUSTDOCFLAGS: '-D warnings' 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | **/*.rs.bk 3 | Cargo.lock 4 | uievents-code.html 5 | uievents-key.html -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "keyboard-types" 3 | version = "0.8.0" 4 | authors = ["Pyfisch "] 5 | description = "Contains types to define keyboard related events." 6 | readme = "README.md" 7 | license = "MIT OR Apache-2.0" 8 | repository = "https://github.com/pyfisch/keyboard-types" 9 | keywords = ["keyboard", "input", "event", "webdriver"] 10 | edition = "2021" 11 | # 1.61 with `std` feature, 1.81 without. 12 | rust-version = "1.61" 13 | 14 | [features] 15 | default = ["std"] 16 | serde = ["dep:serde", "bitflags/serde"] 17 | std = ["serde?/std"] 18 | webdriver = ["dep:unicode-segmentation", "std"] 19 | 20 | [dependencies] 21 | bitflags = "2" 22 | serde = { version = "1.0.0", optional = true, default-features = false, features = ["derive"] } 23 | unicode-segmentation = { version = "1.2.0", optional = true } 24 | 25 | [package.metadata.docs.rs] 26 | all-features = true 27 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Pyfisch 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Keyboard Types 2 | ============== 3 | 4 | [![Build Status](https://github.com/pyfisch/keyboard-types/actions/workflows/ci.yml/badge.svg)](https://github.com/pyfisch/keyboard-types/actions/workflows/ci.yml) 5 | [![Crates.io](https://img.shields.io/crates/v/keyboard-types.svg)](https://crates.io/crates/keyboard-types) 6 | [![Documentation](https://docs.rs/keyboard-types/badge.svg)](https://docs.rs/keyboard-types) 7 | 8 | Contains types to define keyboard related events. 9 | 10 | The naming and conventions follow the UI Events specification 11 | but this crate should be useful for anyone implementing keyboard 12 | input in a cross-platform way. 13 | 14 | See also: [UI Events Specification](https://w3c.github.io/uievents/), and in 15 | particular [the section on keyboard events](https://w3c.github.io/uievents/#keys). 16 | 17 | Minimum Support Rust Version (MSRV) 18 | ----------------------------------- 19 | 20 | The minimum supported Rust version is 1.61, or 1.81 if the `"std"` Cargo 21 | feature is disabled. This is not defined by policy, and may change at any time 22 | in a patch release. 23 | 24 | Updating Generated Code 25 | ----------------------- 26 | 27 | The file `src/key.rs` and `src/code.rs` are derived from the two 28 | W3C working drafts 29 | 30 | * [UI Events KeyboardEvent key Values](https://w3c.github.io/uievents-key/) and 31 | * [UI Events KeyboardEvent code Values](https://w3c.github.io/uievents-code/) 32 | 33 | in the most recent version. A Python 3 script (requires the `requests` and 34 | `beautifulsoup4` libraries) downloads the files and updates the tables. 35 | 36 | Manually check if any modifier keys were changed and update the 37 | `src/modifiers.rs` file if necessary. 38 | -------------------------------------------------------------------------------- /convert.py: -------------------------------------------------------------------------------- 1 | import re 2 | import requests 3 | from bs4 import BeautifulSoup 4 | 5 | 6 | def parse(text): 7 | # The document is not valid XML, so we need a more lenient parser than 'html.parser' 8 | soup = BeautifulSoup(text, 'lxml') 9 | display = [] 10 | for table in soup.find_all('table'): 11 | section_name = table.find_previous_sibling('h3').find(class_='content').text 12 | 13 | # Skip duplicate table 14 | if table['id'] == 'key-table-media-controller-dup': 15 | continue 16 | 17 | # Mark legacy modifier keys as deprecated. 18 | deprecated = table['id'] == 'key-table-modifier-legacy' or table['id'] == 'table-key-code-legacy-modifier' 19 | 20 | for row in table.find('tbody').find_all('tr'): 21 | [name, _required, typical_usage] = row.find_all('td') 22 | 23 | name = name.text.strip().strip('"') 24 | 25 | # Skip F keys here 26 | if re.match(r'^F\d+$', name): 27 | continue 28 | 29 | # Strip tags 30 | for a in typical_usage.find_all('a'): 31 | a.replace_with(a.text) 32 | 33 | # Use the semantic `` element instead. 34 | for keycap in typical_usage.find_all(class_='keycap'): 35 | kbd = soup.new_tag("kbd") 36 | kbd.string = keycap.text 37 | keycap.replace_with(kbd) 38 | 39 | # Link to the relevant type. 40 | for code in typical_usage.find_all(class_='code'): 41 | text = code.text.strip().strip('"') 42 | code.replace_with(f"[`{text}`][Code::{text}]") 43 | for code in typical_usage.find_all(class_='key'): 44 | text = code.text.strip().strip('"') 45 | code.replace_with(f"[`{text}`][NamedKey::{text}]") 46 | 47 | comment = re.sub(r"[ \t][ \t]+", "\n", typical_usage.decode_contents()) 48 | 49 | display.append([name, comment, deprecated, [], []]) 50 | return display 51 | 52 | 53 | def emit_enum_entries(display, file): 54 | for [key, doc_comment, deprecated, alternatives, aliases] in display: 55 | for line in doc_comment.split('\n'): 56 | line = line.strip() 57 | if len(line) == 0: 58 | continue 59 | print(f" /// {line}", file=file) 60 | if deprecated: 61 | print(" #[deprecated = \"marked as legacy in the spec, use Meta instead\"]", file=file) 62 | for alias in aliases: 63 | print(f" #[doc(alias = \"{alias}\")]", file=file) 64 | print(f" {key},", file=file) 65 | 66 | 67 | def print_display_entries(display, file): 68 | for [key, doc_comment, deprecated, alternatives, aliases] in display: 69 | print(" {0} => f.write_str(\"{0}\"),".format( 70 | key), file=file) 71 | 72 | 73 | def print_from_str_entries(display, file): 74 | for [key, doc_comment, deprecated, alternatives, aliases] in display: 75 | print(" \"{0}\"".format(key), file=file, end='') 76 | for alternative in alternatives: 77 | print(" | \"{0}\"".format(alternative), file=file, end='') 78 | print(" => Ok({0}),".format(key), file=file) 79 | 80 | 81 | def add_comment_to(display, key, comment): 82 | for (i, [found_key, doc_comment, deprecated, alternatives, aliases]) in enumerate(display): 83 | if found_key != key: 84 | continue 85 | doc_comment = doc_comment + "\n" + comment 86 | display[i] = [found_key, doc_comment, deprecated, alternatives, aliases] 87 | 88 | 89 | def add_alias_for(display, key, alias): 90 | for [found_key, doc_comment, deprecated, alternatives, aliases] in display: 91 | if found_key != key: 92 | continue 93 | aliases.append(alias) 94 | 95 | 96 | def add_alternative_for(display, key, alternative): 97 | for [found_key, doc_comment, deprecated, alternatives, aliases] in display: 98 | if found_key != key: 99 | continue 100 | alternatives.append(alternative) 101 | # Alternatives are also listed as aliases 102 | aliases.append(alternative) 103 | 104 | 105 | def convert_key(text, file): 106 | print(""" 107 | // AUTO GENERATED CODE - DO NOT EDIT 108 | #![cfg_attr(rustfmt, rustfmt_skip)] 109 | #![allow(clippy::doc_markdown)] 110 | #![allow(deprecated)] 111 | 112 | use core::fmt::{self, Display}; 113 | use core::str::FromStr; 114 | #[cfg(not(feature = "std"))] 115 | use core::error::Error; 116 | #[cfg(feature = "std")] 117 | use std::error::Error; 118 | 119 | /// Key represents the meaning of a keypress. 120 | /// 121 | /// Specification: 122 | /// 123 | #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)] 124 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 125 | #[non_exhaustive] 126 | pub enum NamedKey {""", file=file) 127 | display = parse(text) 128 | 129 | for i in range(1, 36): 130 | display.append([ 131 | 'F{}'.format(i), 132 | 'The F{0} key, a general purpose function key, as index {0}.'.format(i), 133 | False, 134 | [], 135 | [], 136 | ]) 137 | 138 | add_comment_to(display, 'Meta', 'In Linux (XKB) terminology, this is often referred to as "Super".') 139 | 140 | add_alias_for(display, 'Meta', 'Super') 141 | add_alias_for(display, 'Enter', 'Return') 142 | 143 | emit_enum_entries(display, file) 144 | print("}", file=file) 145 | 146 | print(""" 147 | 148 | impl Display for NamedKey { 149 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 150 | use self::NamedKey::*; 151 | match *self {""", file=file) 152 | print_display_entries(display, file) 153 | print(""" 154 | } 155 | } 156 | } 157 | 158 | impl FromStr for NamedKey { 159 | type Err = UnrecognizedNamedKeyError; 160 | 161 | fn from_str(s: &str) -> Result { 162 | use crate::NamedKey::*; 163 | match s {""", file=file) 164 | print_from_str_entries(display, file) 165 | print(""" 166 | _ => Err(UnrecognizedNamedKeyError), 167 | } 168 | } 169 | } 170 | 171 | /// Parse from string error, returned when string does not match to any [`NamedKey`] variant. 172 | #[derive(Clone, Debug)] 173 | pub struct UnrecognizedNamedKeyError; 174 | 175 | impl fmt::Display for UnrecognizedNamedKeyError { 176 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 177 | write!(f, "Unrecognized key") 178 | } 179 | } 180 | 181 | impl Error for UnrecognizedNamedKeyError {}""", file=file) 182 | 183 | 184 | def convert_code(text, file): 185 | print(""" 186 | // AUTO GENERATED CODE - DO NOT EDIT 187 | #![cfg_attr(rustfmt, rustfmt_skip)] 188 | #![allow(clippy::doc_markdown)] 189 | #![allow(deprecated)] 190 | 191 | use core::fmt::{self, Display}; 192 | use core::str::FromStr; 193 | #[cfg(not(feature = "std"))] 194 | use core::error::Error; 195 | #[cfg(feature = "std")] 196 | use std::error::Error; 197 | 198 | /// Code is the physical position of a key. 199 | /// 200 | /// The names are based on the US keyboard. If the key 201 | /// is not present on US keyboards, a name from another 202 | /// layout is used. 203 | /// 204 | /// Specification: 205 | /// 206 | #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)] 207 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 208 | #[non_exhaustive] 209 | pub enum Code {""", file=file) 210 | display = parse(text) 211 | 212 | for i in range(1, 36): 213 | display.append([ 214 | 'F{}'.format(i), 215 | 'F{}'.format(i), 216 | False, 217 | [], 218 | [], 219 | ]) 220 | 221 | chromium_key_codes = [ 222 | 'BrightnessDown', 223 | 'BrightnessUp', 224 | 'DisplayToggleIntExt', 225 | 'KeyboardLayoutSelect', 226 | 'LaunchAssistant', 227 | 'LaunchControlPanel', 228 | 'LaunchScreenSaver', 229 | 'MailForward', 230 | 'MailReply', 231 | 'MailSend', 232 | 'MediaFastForward', 233 | 'MediaPause', 234 | 'MediaPlay', 235 | 'MediaRecord', 236 | 'MediaRewind', 237 | 'MicrophoneMuteToggle', 238 | 'PrivacyScreenToggle', 239 | 'KeyboardBacklightToggle', 240 | 'SelectTask', 241 | 'ShowAllWindows', 242 | 'ZoomToggle', 243 | ] 244 | 245 | for chromium_only in chromium_key_codes: 246 | display.append([ 247 | chromium_only, 248 | 'Non-standard code value supported by Chromium.', 249 | False, 250 | [], 251 | [], 252 | ]) 253 | 254 | add_comment_to(display, 'Backquote', 'This is also called a backtick or grave.') 255 | add_comment_to(display, 'Quote', 'This is also called an apostrophe.') 256 | add_comment_to(display, 'MetaLeft', 'In Linux (XKB) terminology, this is often referred to as the left "Super".') 257 | add_comment_to(display, 'MetaRight', 'In Linux (XKB) terminology, this is often referred to as the right "Super".') 258 | 259 | add_alias_for(display, 'Backquote', 'Backtick') 260 | add_alias_for(display, 'Backquote', 'Grave') 261 | add_alias_for(display, 'Quote', 'Apostrophe') 262 | add_alias_for(display, 'MetaLeft', 'SuperLeft') 263 | add_alias_for(display, 'MetaRight', 'SuperRight') 264 | add_alias_for(display, 'Enter', 'Return') 265 | 266 | add_alternative_for(display, 'MetaLeft', 'OSLeft') 267 | add_alternative_for(display, 'MetaRight', 'OSRight') 268 | add_alternative_for(display, 'AudioVolumeDown', 'VolumeDown') 269 | add_alternative_for(display, 'AudioVolumeMute', 'VolumeMute') 270 | add_alternative_for(display, 'AudioVolumeUp', 'VolumeUp') 271 | add_alternative_for(display, 'MediaSelect', 'LaunchMediaPlayer') 272 | 273 | emit_enum_entries(display, file) 274 | print("}", file=file) 275 | 276 | print(""" 277 | 278 | impl Display for Code { 279 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 280 | use self::Code::*; 281 | match *self {""", file=file) 282 | print_display_entries(display, file) 283 | print(""" 284 | } 285 | } 286 | } 287 | 288 | impl FromStr for Code { 289 | type Err = UnrecognizedCodeError; 290 | 291 | fn from_str(s: &str) -> Result { 292 | use crate::Code::*; 293 | match s {""", file=file) 294 | print_from_str_entries(display, file) 295 | print(""" 296 | _ => Err(UnrecognizedCodeError), 297 | } 298 | } 299 | } 300 | 301 | /// Parse from string error, returned when string does not match to any [`Code`] variant. 302 | #[derive(Clone, Debug)] 303 | pub struct UnrecognizedCodeError; 304 | 305 | impl fmt::Display for UnrecognizedCodeError { 306 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 307 | write!(f, "Unrecognized code") 308 | } 309 | } 310 | 311 | impl Error for UnrecognizedCodeError {}""", file=file) 312 | 313 | 314 | if __name__ == '__main__': 315 | input = requests.get('https://w3c.github.io/uievents-key/').text 316 | with open('src/named_key.rs', 'w', encoding='utf-8') as output: 317 | convert_key(input, output) 318 | input = requests.get('https://w3c.github.io/uievents-code/').text 319 | with open('src/code.rs', 'w', encoding='utf-8') as output: 320 | convert_code(input, output) 321 | -------------------------------------------------------------------------------- /docs/ATTRIBUTION.md: -------------------------------------------------------------------------------- 1 | # Image Attribution 2 | 3 | ## `keyboard_*` 4 | 5 | These files are a modified version of "[ANSI US QWERTY (Windows)](https://commons.wikimedia.org/wiki/File:ANSI_US_QWERTY_(Windows).svg)" 6 | by [Tomiĉo] (https://commons.wikimedia.org/wiki/User:Tomi%C4%89o). It was 7 | originally released under the [CC-BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/deed.en) 8 | License. 9 | 10 | Minor modifications have been made by [John Nunley](https://github.com/notgull), 11 | which have been released under the same license as a derivative work. 12 | -------------------------------------------------------------------------------- /docs/keyboard_left_shift_key.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/code.rs: -------------------------------------------------------------------------------- 1 | 2 | // AUTO GENERATED CODE - DO NOT EDIT 3 | #![cfg_attr(rustfmt, rustfmt_skip)] 4 | #![allow(clippy::doc_markdown)] 5 | #![allow(deprecated)] 6 | 7 | use core::fmt::{self, Display}; 8 | use core::str::FromStr; 9 | #[cfg(not(feature = "std"))] 10 | use core::error::Error; 11 | #[cfg(feature = "std")] 12 | use std::error::Error; 13 | 14 | /// Code is the physical position of a key. 15 | /// 16 | /// The names are based on the US keyboard. If the key 17 | /// is not present on US keyboards, a name from another 18 | /// layout is used. 19 | /// 20 | /// Specification: 21 | /// 22 | #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)] 23 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 24 | #[non_exhaustive] 25 | pub enum Code { 26 | /// `~ on a US keyboard. This is the 半角/全角/漢字 (hankaku/zenkaku/kanji) key on Japanese keyboards 27 | /// This is also called a backtick or grave. 28 | #[doc(alias = "Backtick")] 29 | #[doc(alias = "Grave")] 30 | Backquote, 31 | /// Used for both the US \| (on the 101-key layout) and also for the key 32 | /// located between the " and Enter keys on row C of the 102-, 33 | /// 104- and 106-key layouts. 34 | /// Labelled #~ on a UK (102) keyboard. 35 | Backslash, 36 | /// [{ on a US keyboard. 37 | BracketLeft, 38 | /// ]} on a US keyboard. 39 | BracketRight, 40 | /// ,< on a US keyboard. 41 | Comma, 42 | /// 0) on a US keyboard. 43 | Digit0, 44 | /// 1! on a US keyboard. 45 | Digit1, 46 | /// 2@ on a US keyboard. 47 | Digit2, 48 | /// 3# on a US keyboard. 49 | Digit3, 50 | /// 4$ on a US keyboard. 51 | Digit4, 52 | /// 5% on a US keyboard. 53 | Digit5, 54 | /// 6^ on a US keyboard. 55 | Digit6, 56 | /// 7& on a US keyboard. 57 | Digit7, 58 | /// 8* on a US keyboard. 59 | Digit8, 60 | /// 9( on a US keyboard. 61 | Digit9, 62 | /// =+ on a US keyboard. 63 | Equal, 64 | /// Located between the left Shift and Z keys. 65 | /// Labelled \| on a UK keyboard. 66 | IntlBackslash, 67 | /// Located between the / and right Shift keys. 68 | /// Labelled \ろ (ro) on a Japanese keyboard. 69 | IntlRo, 70 | /// Located between the = and Backspace keys. 71 | /// Labelled ¥ (yen) on a Japanese keyboard. \/ on a 72 | /// Russian keyboard. 73 | IntlYen, 74 | /// a on a US keyboard. 75 | /// Labelled q on an AZERTY (e.g., French) keyboard. 76 | KeyA, 77 | /// b on a US keyboard. 78 | KeyB, 79 | /// c on a US keyboard. 80 | KeyC, 81 | /// d on a US keyboard. 82 | KeyD, 83 | /// e on a US keyboard. 84 | KeyE, 85 | /// f on a US keyboard. 86 | KeyF, 87 | /// g on a US keyboard. 88 | KeyG, 89 | /// h on a US keyboard. 90 | KeyH, 91 | /// i on a US keyboard. 92 | KeyI, 93 | /// j on a US keyboard. 94 | KeyJ, 95 | /// k on a US keyboard. 96 | KeyK, 97 | /// l on a US keyboard. 98 | KeyL, 99 | /// m on a US keyboard. 100 | KeyM, 101 | /// n on a US keyboard. 102 | KeyN, 103 | /// o on a US keyboard. 104 | KeyO, 105 | /// p on a US keyboard. 106 | KeyP, 107 | /// q on a US keyboard. 108 | /// Labelled a on an AZERTY (e.g., French) keyboard. 109 | KeyQ, 110 | /// r on a US keyboard. 111 | KeyR, 112 | /// s on a US keyboard. 113 | KeyS, 114 | /// t on a US keyboard. 115 | KeyT, 116 | /// u on a US keyboard. 117 | KeyU, 118 | /// v on a US keyboard. 119 | KeyV, 120 | /// w on a US keyboard. 121 | /// Labelled z on an AZERTY (e.g., French) keyboard. 122 | KeyW, 123 | /// x on a US keyboard. 124 | KeyX, 125 | /// y on a US keyboard. 126 | /// Labelled z on a QWERTZ (e.g., German) keyboard. 127 | KeyY, 128 | /// z on a US keyboard. 129 | /// Labelled w on an AZERTY (e.g., French) keyboard, and y on a 130 | /// QWERTZ (e.g., German) keyboard. 131 | KeyZ, 132 | /// -_ on a US keyboard. 133 | Minus, 134 | /// .> on a US keyboard. 135 | Period, 136 | /// '" on a US keyboard. 137 | /// This is also called an apostrophe. 138 | #[doc(alias = "Apostrophe")] 139 | Quote, 140 | /// ;: on a US keyboard. 141 | Semicolon, 142 | /// /? on a US keyboard. 143 | Slash, 144 | /// Alt, Option or . 145 | AltLeft, 146 | /// Alt, Option or . 147 | /// This is labelled AltGr key on many keyboard layouts. 148 | AltRight, 149 | /// Backspace or . 150 | /// Labelled Delete on Apple keyboards. 151 | Backspace, 152 | /// CapsLock or 153 | CapsLock, 154 | /// The application context menu key, which is typically found between the right Meta key and the right Control key. 155 | ContextMenu, 156 | /// Control or 157 | ControlLeft, 158 | /// Control or 159 | ControlRight, 160 | /// Enter or . Labelled Return on Apple keyboards. 161 | #[doc(alias = "Return")] 162 | Enter, 163 | /// The Windows, , Command or other OS symbol key. 164 | /// In Linux (XKB) terminology, this is often referred to as the left "Super". 165 | #[doc(alias = "SuperLeft")] 166 | #[doc(alias = "OSLeft")] 167 | MetaLeft, 168 | /// The Windows, , Command or other OS symbol key. 169 | /// In Linux (XKB) terminology, this is often referred to as the right "Super". 170 | #[doc(alias = "SuperRight")] 171 | #[doc(alias = "OSRight")] 172 | MetaRight, 173 | /// Shift or 174 | ShiftLeft, 175 | /// Shift or 176 | ShiftRight, 177 | ///   (space) 178 | Space, 179 | /// Tab or 180 | Tab, 181 | /// Japanese: 変換 (henkan) 182 | Convert, 183 | /// Japanese: カタカナ/ひらがな/ローマ字 (katakana/hiragana/romaji) 184 | KanaMode, 185 | /// Korean: HangulMode 한/영 (han/yeong)
Japanese (Mac keyboard): かな (kana) 186 | Lang1, 187 | /// Korean: Hanja 한자 (hanja)
Japanese (Mac keyboard): 英数 (eisu) 188 | Lang2, 189 | /// Japanese (word-processing keyboard): Katakana 190 | Lang3, 191 | /// Japanese (word-processing keyboard): Hiragana 192 | Lang4, 193 | /// Japanese (word-processing keyboard): Zenkaku/Hankaku 194 | Lang5, 195 | /// Japanese: 無変換 (muhenkan) 196 | NonConvert, 197 | /// . The forward delete key. 198 | /// Note that on Apple keyboards, the key labelled Delete on the main part of 199 | /// the keyboard should be encoded as [`Backspace`][Code::Backspace]. 200 | Delete, 201 | /// End or 202 | End, 203 | /// Help. Not present on standard PC keyboards. 204 | Help, 205 | /// Home or 206 | Home, 207 | /// Insert or Ins. Not present on Apple keyboards. 208 | Insert, 209 | /// Page Down, PgDn or 210 | PageDown, 211 | /// Page Up, PgUp or 212 | PageUp, 213 | /// 214 | ArrowDown, 215 | /// 216 | ArrowLeft, 217 | /// 218 | ArrowRight, 219 | /// 220 | ArrowUp, 221 | /// On the Mac, the [`NumLock`][Code::NumLock] code should be used for the numpad Clear key. 222 | NumLock, 223 | /// 0 Ins on a keyboard
0 on a phone or remote control 224 | Numpad0, 225 | /// 1 End on a keyboard
1 or 1 QZ on a phone or 226 | /// remote control 227 | Numpad1, 228 | /// 2 ↓ on a keyboard
2 ABC on a phone or remote control 229 | Numpad2, 230 | /// 3 PgDn on a keyboard
3 DEF on a phone or remote control 231 | Numpad3, 232 | /// 4 ← on a keyboard
4 GHI on a phone or remote control 233 | Numpad4, 234 | /// 5 on a keyboard
5 JKL on a phone or remote control 235 | Numpad5, 236 | /// 6 → on a keyboard
6 MNO on a phone or remote control 237 | Numpad6, 238 | /// 7 Home on a keyboard
7 PQRS or 7 PRS on a phone 239 | /// or remote control 240 | Numpad7, 241 | /// 8 ↑ on a keyboard
8 TUV on a phone or remote control 242 | Numpad8, 243 | /// 9 PgUp on a keyboard
9 WXYZ or 9 WXY on a phone 244 | /// or remote control 245 | Numpad9, 246 | /// + 247 | NumpadAdd, 248 | /// Found on the Microsoft Natural Keyboard. 249 | NumpadBackspace, 250 | /// C or AC (All Clear). Also for use with numpads that have a Clear key that is separate from the NumLock key. On the Mac, the numpad Clear key should always 251 | /// be encoded as [`NumLock`][Code::NumLock]. 252 | NumpadClear, 253 | /// CE (Clear Entry) 254 | NumpadClearEntry, 255 | /// , (thousands separator). For locales where the thousands separator 256 | /// is a "." (e.g., Brazil), this key may generate a .. 257 | NumpadComma, 258 | /// . Del. For locales where the decimal separator is "," (e.g., 259 | /// Brazil), this key may generate a ,. 260 | NumpadDecimal, 261 | /// / 262 | NumpadDivide, 263 | NumpadEnter, 264 | /// = 265 | NumpadEqual, 266 | /// # on a phone or remote control device. This key is typically found 267 | /// below the 9 key and to the right of the 0 key. 268 | NumpadHash, 269 | /// M+ Add current entry to the value stored in memory. 270 | NumpadMemoryAdd, 271 | /// MC Clear the value stored in memory. 272 | NumpadMemoryClear, 273 | /// MR Replace the current entry with the value stored in memory. 274 | NumpadMemoryRecall, 275 | /// MS Replace the value stored in memory with the current entry. 276 | NumpadMemoryStore, 277 | /// M- Subtract current entry from the value stored in memory. 278 | NumpadMemorySubtract, 279 | /// * on a keyboard. For use with numpads that provide mathematical 280 | /// operations (+, -, * and /).
Use [`NumpadStar`][Code::NumpadStar] for the * key on phones and remote controls. 281 | NumpadMultiply, 282 | /// ( Found on the Microsoft Natural Keyboard. 283 | NumpadParenLeft, 284 | /// ) Found on the Microsoft Natural Keyboard. 285 | NumpadParenRight, 286 | /// * on a phone or remote control device. 287 | /// This key is typically found below the 7 key and to the left of 288 | /// the 0 key.
Use [`NumpadMultiply`][Code::NumpadMultiply] for the * key on 289 | /// numeric keypads. 290 | NumpadStar, 291 | /// - 292 | NumpadSubtract, 293 | /// Esc or 294 | Escape, 295 | /// Fn This is typically a hardware key that does not generate a separate 296 | /// code. Most keyboards do not place this key in the function section, but it is 297 | /// included here to keep it with related keys. 298 | Fn, 299 | /// FLock or FnLock. Function Lock key. Found on the Microsoft 300 | /// Natural Keyboard. 301 | FnLock, 302 | /// PrtScr SysRq or Print Screen 303 | PrintScreen, 304 | /// Scroll Lock 305 | ScrollLock, 306 | /// Pause Break 307 | Pause, 308 | /// Some laptops place this key to the left of the key. 309 | BrowserBack, 310 | BrowserFavorites, 311 | /// Some laptops place this key to the right of the key. 312 | BrowserForward, 313 | BrowserHome, 314 | BrowserRefresh, 315 | BrowserSearch, 316 | BrowserStop, 317 | /// Eject or . This key is placed in the function 318 | /// section on some Apple keyboards. 319 | Eject, 320 | /// Sometimes labelled My Computer on the keyboard 321 | LaunchApp1, 322 | /// Sometimes labelled Calculator on the keyboard 323 | LaunchApp2, 324 | LaunchMail, 325 | MediaPlayPause, 326 | #[doc(alias = "LaunchMediaPlayer")] 327 | MediaSelect, 328 | MediaStop, 329 | MediaTrackNext, 330 | MediaTrackPrevious, 331 | /// This key is placed in the function section on some Apple keyboards, 332 | /// replacing the Eject key. 333 | Power, 334 | Sleep, 335 | #[doc(alias = "VolumeDown")] 336 | AudioVolumeDown, 337 | #[doc(alias = "VolumeMute")] 338 | AudioVolumeMute, 339 | #[doc(alias = "VolumeUp")] 340 | AudioVolumeUp, 341 | WakeUp, 342 | #[deprecated = "marked as legacy in the spec, use Meta instead"] 343 | Hyper, 344 | #[deprecated = "marked as legacy in the spec, use Meta instead"] 345 | Super, 346 | #[deprecated = "marked as legacy in the spec, use Meta instead"] 347 | Turbo, 348 | Abort, 349 | Resume, 350 | Suspend, 351 | /// Found on Sun’s USB keyboard. 352 | Again, 353 | /// Found on Sun’s USB keyboard. 354 | Copy, 355 | /// Found on Sun’s USB keyboard. 356 | Cut, 357 | /// Found on Sun’s USB keyboard. 358 | Find, 359 | /// Found on Sun’s USB keyboard. 360 | Open, 361 | /// Found on Sun’s USB keyboard. 362 | Paste, 363 | /// Found on Sun’s USB keyboard. 364 | Props, 365 | /// Found on Sun’s USB keyboard. 366 | Select, 367 | /// Found on Sun’s USB keyboard. 368 | Undo, 369 | /// Use for dedicated ひらがな key found on some Japanese word processing keyboards. 370 | Hiragana, 371 | /// Use for dedicated カタカナ key found on some Japanese word processing keyboards. 372 | Katakana, 373 | /// This value code should be used when no other 374 | /// value given in this specification is appropriate. 375 | Unidentified, 376 | /// F1 377 | F1, 378 | /// F2 379 | F2, 380 | /// F3 381 | F3, 382 | /// F4 383 | F4, 384 | /// F5 385 | F5, 386 | /// F6 387 | F6, 388 | /// F7 389 | F7, 390 | /// F8 391 | F8, 392 | /// F9 393 | F9, 394 | /// F10 395 | F10, 396 | /// F11 397 | F11, 398 | /// F12 399 | F12, 400 | /// F13 401 | F13, 402 | /// F14 403 | F14, 404 | /// F15 405 | F15, 406 | /// F16 407 | F16, 408 | /// F17 409 | F17, 410 | /// F18 411 | F18, 412 | /// F19 413 | F19, 414 | /// F20 415 | F20, 416 | /// F21 417 | F21, 418 | /// F22 419 | F22, 420 | /// F23 421 | F23, 422 | /// F24 423 | F24, 424 | /// F25 425 | F25, 426 | /// F26 427 | F26, 428 | /// F27 429 | F27, 430 | /// F28 431 | F28, 432 | /// F29 433 | F29, 434 | /// F30 435 | F30, 436 | /// F31 437 | F31, 438 | /// F32 439 | F32, 440 | /// F33 441 | F33, 442 | /// F34 443 | F34, 444 | /// F35 445 | F35, 446 | /// Non-standard code value supported by Chromium. 447 | BrightnessDown, 448 | /// Non-standard code value supported by Chromium. 449 | BrightnessUp, 450 | /// Non-standard code value supported by Chromium. 451 | DisplayToggleIntExt, 452 | /// Non-standard code value supported by Chromium. 453 | KeyboardLayoutSelect, 454 | /// Non-standard code value supported by Chromium. 455 | LaunchAssistant, 456 | /// Non-standard code value supported by Chromium. 457 | LaunchControlPanel, 458 | /// Non-standard code value supported by Chromium. 459 | LaunchScreenSaver, 460 | /// Non-standard code value supported by Chromium. 461 | MailForward, 462 | /// Non-standard code value supported by Chromium. 463 | MailReply, 464 | /// Non-standard code value supported by Chromium. 465 | MailSend, 466 | /// Non-standard code value supported by Chromium. 467 | MediaFastForward, 468 | /// Non-standard code value supported by Chromium. 469 | MediaPause, 470 | /// Non-standard code value supported by Chromium. 471 | MediaPlay, 472 | /// Non-standard code value supported by Chromium. 473 | MediaRecord, 474 | /// Non-standard code value supported by Chromium. 475 | MediaRewind, 476 | /// Non-standard code value supported by Chromium. 477 | MicrophoneMuteToggle, 478 | /// Non-standard code value supported by Chromium. 479 | PrivacyScreenToggle, 480 | /// Non-standard code value supported by Chromium. 481 | KeyboardBacklightToggle, 482 | /// Non-standard code value supported by Chromium. 483 | SelectTask, 484 | /// Non-standard code value supported by Chromium. 485 | ShowAllWindows, 486 | /// Non-standard code value supported by Chromium. 487 | ZoomToggle, 488 | } 489 | 490 | 491 | impl Display for Code { 492 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 493 | use self::Code::*; 494 | match *self { 495 | Backquote => f.write_str("Backquote"), 496 | Backslash => f.write_str("Backslash"), 497 | BracketLeft => f.write_str("BracketLeft"), 498 | BracketRight => f.write_str("BracketRight"), 499 | Comma => f.write_str("Comma"), 500 | Digit0 => f.write_str("Digit0"), 501 | Digit1 => f.write_str("Digit1"), 502 | Digit2 => f.write_str("Digit2"), 503 | Digit3 => f.write_str("Digit3"), 504 | Digit4 => f.write_str("Digit4"), 505 | Digit5 => f.write_str("Digit5"), 506 | Digit6 => f.write_str("Digit6"), 507 | Digit7 => f.write_str("Digit7"), 508 | Digit8 => f.write_str("Digit8"), 509 | Digit9 => f.write_str("Digit9"), 510 | Equal => f.write_str("Equal"), 511 | IntlBackslash => f.write_str("IntlBackslash"), 512 | IntlRo => f.write_str("IntlRo"), 513 | IntlYen => f.write_str("IntlYen"), 514 | KeyA => f.write_str("KeyA"), 515 | KeyB => f.write_str("KeyB"), 516 | KeyC => f.write_str("KeyC"), 517 | KeyD => f.write_str("KeyD"), 518 | KeyE => f.write_str("KeyE"), 519 | KeyF => f.write_str("KeyF"), 520 | KeyG => f.write_str("KeyG"), 521 | KeyH => f.write_str("KeyH"), 522 | KeyI => f.write_str("KeyI"), 523 | KeyJ => f.write_str("KeyJ"), 524 | KeyK => f.write_str("KeyK"), 525 | KeyL => f.write_str("KeyL"), 526 | KeyM => f.write_str("KeyM"), 527 | KeyN => f.write_str("KeyN"), 528 | KeyO => f.write_str("KeyO"), 529 | KeyP => f.write_str("KeyP"), 530 | KeyQ => f.write_str("KeyQ"), 531 | KeyR => f.write_str("KeyR"), 532 | KeyS => f.write_str("KeyS"), 533 | KeyT => f.write_str("KeyT"), 534 | KeyU => f.write_str("KeyU"), 535 | KeyV => f.write_str("KeyV"), 536 | KeyW => f.write_str("KeyW"), 537 | KeyX => f.write_str("KeyX"), 538 | KeyY => f.write_str("KeyY"), 539 | KeyZ => f.write_str("KeyZ"), 540 | Minus => f.write_str("Minus"), 541 | Period => f.write_str("Period"), 542 | Quote => f.write_str("Quote"), 543 | Semicolon => f.write_str("Semicolon"), 544 | Slash => f.write_str("Slash"), 545 | AltLeft => f.write_str("AltLeft"), 546 | AltRight => f.write_str("AltRight"), 547 | Backspace => f.write_str("Backspace"), 548 | CapsLock => f.write_str("CapsLock"), 549 | ContextMenu => f.write_str("ContextMenu"), 550 | ControlLeft => f.write_str("ControlLeft"), 551 | ControlRight => f.write_str("ControlRight"), 552 | Enter => f.write_str("Enter"), 553 | MetaLeft => f.write_str("MetaLeft"), 554 | MetaRight => f.write_str("MetaRight"), 555 | ShiftLeft => f.write_str("ShiftLeft"), 556 | ShiftRight => f.write_str("ShiftRight"), 557 | Space => f.write_str("Space"), 558 | Tab => f.write_str("Tab"), 559 | Convert => f.write_str("Convert"), 560 | KanaMode => f.write_str("KanaMode"), 561 | Lang1 => f.write_str("Lang1"), 562 | Lang2 => f.write_str("Lang2"), 563 | Lang3 => f.write_str("Lang3"), 564 | Lang4 => f.write_str("Lang4"), 565 | Lang5 => f.write_str("Lang5"), 566 | NonConvert => f.write_str("NonConvert"), 567 | Delete => f.write_str("Delete"), 568 | End => f.write_str("End"), 569 | Help => f.write_str("Help"), 570 | Home => f.write_str("Home"), 571 | Insert => f.write_str("Insert"), 572 | PageDown => f.write_str("PageDown"), 573 | PageUp => f.write_str("PageUp"), 574 | ArrowDown => f.write_str("ArrowDown"), 575 | ArrowLeft => f.write_str("ArrowLeft"), 576 | ArrowRight => f.write_str("ArrowRight"), 577 | ArrowUp => f.write_str("ArrowUp"), 578 | NumLock => f.write_str("NumLock"), 579 | Numpad0 => f.write_str("Numpad0"), 580 | Numpad1 => f.write_str("Numpad1"), 581 | Numpad2 => f.write_str("Numpad2"), 582 | Numpad3 => f.write_str("Numpad3"), 583 | Numpad4 => f.write_str("Numpad4"), 584 | Numpad5 => f.write_str("Numpad5"), 585 | Numpad6 => f.write_str("Numpad6"), 586 | Numpad7 => f.write_str("Numpad7"), 587 | Numpad8 => f.write_str("Numpad8"), 588 | Numpad9 => f.write_str("Numpad9"), 589 | NumpadAdd => f.write_str("NumpadAdd"), 590 | NumpadBackspace => f.write_str("NumpadBackspace"), 591 | NumpadClear => f.write_str("NumpadClear"), 592 | NumpadClearEntry => f.write_str("NumpadClearEntry"), 593 | NumpadComma => f.write_str("NumpadComma"), 594 | NumpadDecimal => f.write_str("NumpadDecimal"), 595 | NumpadDivide => f.write_str("NumpadDivide"), 596 | NumpadEnter => f.write_str("NumpadEnter"), 597 | NumpadEqual => f.write_str("NumpadEqual"), 598 | NumpadHash => f.write_str("NumpadHash"), 599 | NumpadMemoryAdd => f.write_str("NumpadMemoryAdd"), 600 | NumpadMemoryClear => f.write_str("NumpadMemoryClear"), 601 | NumpadMemoryRecall => f.write_str("NumpadMemoryRecall"), 602 | NumpadMemoryStore => f.write_str("NumpadMemoryStore"), 603 | NumpadMemorySubtract => f.write_str("NumpadMemorySubtract"), 604 | NumpadMultiply => f.write_str("NumpadMultiply"), 605 | NumpadParenLeft => f.write_str("NumpadParenLeft"), 606 | NumpadParenRight => f.write_str("NumpadParenRight"), 607 | NumpadStar => f.write_str("NumpadStar"), 608 | NumpadSubtract => f.write_str("NumpadSubtract"), 609 | Escape => f.write_str("Escape"), 610 | Fn => f.write_str("Fn"), 611 | FnLock => f.write_str("FnLock"), 612 | PrintScreen => f.write_str("PrintScreen"), 613 | ScrollLock => f.write_str("ScrollLock"), 614 | Pause => f.write_str("Pause"), 615 | BrowserBack => f.write_str("BrowserBack"), 616 | BrowserFavorites => f.write_str("BrowserFavorites"), 617 | BrowserForward => f.write_str("BrowserForward"), 618 | BrowserHome => f.write_str("BrowserHome"), 619 | BrowserRefresh => f.write_str("BrowserRefresh"), 620 | BrowserSearch => f.write_str("BrowserSearch"), 621 | BrowserStop => f.write_str("BrowserStop"), 622 | Eject => f.write_str("Eject"), 623 | LaunchApp1 => f.write_str("LaunchApp1"), 624 | LaunchApp2 => f.write_str("LaunchApp2"), 625 | LaunchMail => f.write_str("LaunchMail"), 626 | MediaPlayPause => f.write_str("MediaPlayPause"), 627 | MediaSelect => f.write_str("MediaSelect"), 628 | MediaStop => f.write_str("MediaStop"), 629 | MediaTrackNext => f.write_str("MediaTrackNext"), 630 | MediaTrackPrevious => f.write_str("MediaTrackPrevious"), 631 | Power => f.write_str("Power"), 632 | Sleep => f.write_str("Sleep"), 633 | AudioVolumeDown => f.write_str("AudioVolumeDown"), 634 | AudioVolumeMute => f.write_str("AudioVolumeMute"), 635 | AudioVolumeUp => f.write_str("AudioVolumeUp"), 636 | WakeUp => f.write_str("WakeUp"), 637 | Hyper => f.write_str("Hyper"), 638 | Super => f.write_str("Super"), 639 | Turbo => f.write_str("Turbo"), 640 | Abort => f.write_str("Abort"), 641 | Resume => f.write_str("Resume"), 642 | Suspend => f.write_str("Suspend"), 643 | Again => f.write_str("Again"), 644 | Copy => f.write_str("Copy"), 645 | Cut => f.write_str("Cut"), 646 | Find => f.write_str("Find"), 647 | Open => f.write_str("Open"), 648 | Paste => f.write_str("Paste"), 649 | Props => f.write_str("Props"), 650 | Select => f.write_str("Select"), 651 | Undo => f.write_str("Undo"), 652 | Hiragana => f.write_str("Hiragana"), 653 | Katakana => f.write_str("Katakana"), 654 | Unidentified => f.write_str("Unidentified"), 655 | F1 => f.write_str("F1"), 656 | F2 => f.write_str("F2"), 657 | F3 => f.write_str("F3"), 658 | F4 => f.write_str("F4"), 659 | F5 => f.write_str("F5"), 660 | F6 => f.write_str("F6"), 661 | F7 => f.write_str("F7"), 662 | F8 => f.write_str("F8"), 663 | F9 => f.write_str("F9"), 664 | F10 => f.write_str("F10"), 665 | F11 => f.write_str("F11"), 666 | F12 => f.write_str("F12"), 667 | F13 => f.write_str("F13"), 668 | F14 => f.write_str("F14"), 669 | F15 => f.write_str("F15"), 670 | F16 => f.write_str("F16"), 671 | F17 => f.write_str("F17"), 672 | F18 => f.write_str("F18"), 673 | F19 => f.write_str("F19"), 674 | F20 => f.write_str("F20"), 675 | F21 => f.write_str("F21"), 676 | F22 => f.write_str("F22"), 677 | F23 => f.write_str("F23"), 678 | F24 => f.write_str("F24"), 679 | F25 => f.write_str("F25"), 680 | F26 => f.write_str("F26"), 681 | F27 => f.write_str("F27"), 682 | F28 => f.write_str("F28"), 683 | F29 => f.write_str("F29"), 684 | F30 => f.write_str("F30"), 685 | F31 => f.write_str("F31"), 686 | F32 => f.write_str("F32"), 687 | F33 => f.write_str("F33"), 688 | F34 => f.write_str("F34"), 689 | F35 => f.write_str("F35"), 690 | BrightnessDown => f.write_str("BrightnessDown"), 691 | BrightnessUp => f.write_str("BrightnessUp"), 692 | DisplayToggleIntExt => f.write_str("DisplayToggleIntExt"), 693 | KeyboardLayoutSelect => f.write_str("KeyboardLayoutSelect"), 694 | LaunchAssistant => f.write_str("LaunchAssistant"), 695 | LaunchControlPanel => f.write_str("LaunchControlPanel"), 696 | LaunchScreenSaver => f.write_str("LaunchScreenSaver"), 697 | MailForward => f.write_str("MailForward"), 698 | MailReply => f.write_str("MailReply"), 699 | MailSend => f.write_str("MailSend"), 700 | MediaFastForward => f.write_str("MediaFastForward"), 701 | MediaPause => f.write_str("MediaPause"), 702 | MediaPlay => f.write_str("MediaPlay"), 703 | MediaRecord => f.write_str("MediaRecord"), 704 | MediaRewind => f.write_str("MediaRewind"), 705 | MicrophoneMuteToggle => f.write_str("MicrophoneMuteToggle"), 706 | PrivacyScreenToggle => f.write_str("PrivacyScreenToggle"), 707 | KeyboardBacklightToggle => f.write_str("KeyboardBacklightToggle"), 708 | SelectTask => f.write_str("SelectTask"), 709 | ShowAllWindows => f.write_str("ShowAllWindows"), 710 | ZoomToggle => f.write_str("ZoomToggle"), 711 | 712 | } 713 | } 714 | } 715 | 716 | impl FromStr for Code { 717 | type Err = UnrecognizedCodeError; 718 | 719 | fn from_str(s: &str) -> Result { 720 | use crate::Code::*; 721 | match s { 722 | "Backquote" => Ok(Backquote), 723 | "Backslash" => Ok(Backslash), 724 | "BracketLeft" => Ok(BracketLeft), 725 | "BracketRight" => Ok(BracketRight), 726 | "Comma" => Ok(Comma), 727 | "Digit0" => Ok(Digit0), 728 | "Digit1" => Ok(Digit1), 729 | "Digit2" => Ok(Digit2), 730 | "Digit3" => Ok(Digit3), 731 | "Digit4" => Ok(Digit4), 732 | "Digit5" => Ok(Digit5), 733 | "Digit6" => Ok(Digit6), 734 | "Digit7" => Ok(Digit7), 735 | "Digit8" => Ok(Digit8), 736 | "Digit9" => Ok(Digit9), 737 | "Equal" => Ok(Equal), 738 | "IntlBackslash" => Ok(IntlBackslash), 739 | "IntlRo" => Ok(IntlRo), 740 | "IntlYen" => Ok(IntlYen), 741 | "KeyA" => Ok(KeyA), 742 | "KeyB" => Ok(KeyB), 743 | "KeyC" => Ok(KeyC), 744 | "KeyD" => Ok(KeyD), 745 | "KeyE" => Ok(KeyE), 746 | "KeyF" => Ok(KeyF), 747 | "KeyG" => Ok(KeyG), 748 | "KeyH" => Ok(KeyH), 749 | "KeyI" => Ok(KeyI), 750 | "KeyJ" => Ok(KeyJ), 751 | "KeyK" => Ok(KeyK), 752 | "KeyL" => Ok(KeyL), 753 | "KeyM" => Ok(KeyM), 754 | "KeyN" => Ok(KeyN), 755 | "KeyO" => Ok(KeyO), 756 | "KeyP" => Ok(KeyP), 757 | "KeyQ" => Ok(KeyQ), 758 | "KeyR" => Ok(KeyR), 759 | "KeyS" => Ok(KeyS), 760 | "KeyT" => Ok(KeyT), 761 | "KeyU" => Ok(KeyU), 762 | "KeyV" => Ok(KeyV), 763 | "KeyW" => Ok(KeyW), 764 | "KeyX" => Ok(KeyX), 765 | "KeyY" => Ok(KeyY), 766 | "KeyZ" => Ok(KeyZ), 767 | "Minus" => Ok(Minus), 768 | "Period" => Ok(Period), 769 | "Quote" => Ok(Quote), 770 | "Semicolon" => Ok(Semicolon), 771 | "Slash" => Ok(Slash), 772 | "AltLeft" => Ok(AltLeft), 773 | "AltRight" => Ok(AltRight), 774 | "Backspace" => Ok(Backspace), 775 | "CapsLock" => Ok(CapsLock), 776 | "ContextMenu" => Ok(ContextMenu), 777 | "ControlLeft" => Ok(ControlLeft), 778 | "ControlRight" => Ok(ControlRight), 779 | "Enter" => Ok(Enter), 780 | "MetaLeft" | "OSLeft" => Ok(MetaLeft), 781 | "MetaRight" | "OSRight" => Ok(MetaRight), 782 | "ShiftLeft" => Ok(ShiftLeft), 783 | "ShiftRight" => Ok(ShiftRight), 784 | "Space" => Ok(Space), 785 | "Tab" => Ok(Tab), 786 | "Convert" => Ok(Convert), 787 | "KanaMode" => Ok(KanaMode), 788 | "Lang1" => Ok(Lang1), 789 | "Lang2" => Ok(Lang2), 790 | "Lang3" => Ok(Lang3), 791 | "Lang4" => Ok(Lang4), 792 | "Lang5" => Ok(Lang5), 793 | "NonConvert" => Ok(NonConvert), 794 | "Delete" => Ok(Delete), 795 | "End" => Ok(End), 796 | "Help" => Ok(Help), 797 | "Home" => Ok(Home), 798 | "Insert" => Ok(Insert), 799 | "PageDown" => Ok(PageDown), 800 | "PageUp" => Ok(PageUp), 801 | "ArrowDown" => Ok(ArrowDown), 802 | "ArrowLeft" => Ok(ArrowLeft), 803 | "ArrowRight" => Ok(ArrowRight), 804 | "ArrowUp" => Ok(ArrowUp), 805 | "NumLock" => Ok(NumLock), 806 | "Numpad0" => Ok(Numpad0), 807 | "Numpad1" => Ok(Numpad1), 808 | "Numpad2" => Ok(Numpad2), 809 | "Numpad3" => Ok(Numpad3), 810 | "Numpad4" => Ok(Numpad4), 811 | "Numpad5" => Ok(Numpad5), 812 | "Numpad6" => Ok(Numpad6), 813 | "Numpad7" => Ok(Numpad7), 814 | "Numpad8" => Ok(Numpad8), 815 | "Numpad9" => Ok(Numpad9), 816 | "NumpadAdd" => Ok(NumpadAdd), 817 | "NumpadBackspace" => Ok(NumpadBackspace), 818 | "NumpadClear" => Ok(NumpadClear), 819 | "NumpadClearEntry" => Ok(NumpadClearEntry), 820 | "NumpadComma" => Ok(NumpadComma), 821 | "NumpadDecimal" => Ok(NumpadDecimal), 822 | "NumpadDivide" => Ok(NumpadDivide), 823 | "NumpadEnter" => Ok(NumpadEnter), 824 | "NumpadEqual" => Ok(NumpadEqual), 825 | "NumpadHash" => Ok(NumpadHash), 826 | "NumpadMemoryAdd" => Ok(NumpadMemoryAdd), 827 | "NumpadMemoryClear" => Ok(NumpadMemoryClear), 828 | "NumpadMemoryRecall" => Ok(NumpadMemoryRecall), 829 | "NumpadMemoryStore" => Ok(NumpadMemoryStore), 830 | "NumpadMemorySubtract" => Ok(NumpadMemorySubtract), 831 | "NumpadMultiply" => Ok(NumpadMultiply), 832 | "NumpadParenLeft" => Ok(NumpadParenLeft), 833 | "NumpadParenRight" => Ok(NumpadParenRight), 834 | "NumpadStar" => Ok(NumpadStar), 835 | "NumpadSubtract" => Ok(NumpadSubtract), 836 | "Escape" => Ok(Escape), 837 | "Fn" => Ok(Fn), 838 | "FnLock" => Ok(FnLock), 839 | "PrintScreen" => Ok(PrintScreen), 840 | "ScrollLock" => Ok(ScrollLock), 841 | "Pause" => Ok(Pause), 842 | "BrowserBack" => Ok(BrowserBack), 843 | "BrowserFavorites" => Ok(BrowserFavorites), 844 | "BrowserForward" => Ok(BrowserForward), 845 | "BrowserHome" => Ok(BrowserHome), 846 | "BrowserRefresh" => Ok(BrowserRefresh), 847 | "BrowserSearch" => Ok(BrowserSearch), 848 | "BrowserStop" => Ok(BrowserStop), 849 | "Eject" => Ok(Eject), 850 | "LaunchApp1" => Ok(LaunchApp1), 851 | "LaunchApp2" => Ok(LaunchApp2), 852 | "LaunchMail" => Ok(LaunchMail), 853 | "MediaPlayPause" => Ok(MediaPlayPause), 854 | "MediaSelect" | "LaunchMediaPlayer" => Ok(MediaSelect), 855 | "MediaStop" => Ok(MediaStop), 856 | "MediaTrackNext" => Ok(MediaTrackNext), 857 | "MediaTrackPrevious" => Ok(MediaTrackPrevious), 858 | "Power" => Ok(Power), 859 | "Sleep" => Ok(Sleep), 860 | "AudioVolumeDown" | "VolumeDown" => Ok(AudioVolumeDown), 861 | "AudioVolumeMute" | "VolumeMute" => Ok(AudioVolumeMute), 862 | "AudioVolumeUp" | "VolumeUp" => Ok(AudioVolumeUp), 863 | "WakeUp" => Ok(WakeUp), 864 | "Hyper" => Ok(Hyper), 865 | "Super" => Ok(Super), 866 | "Turbo" => Ok(Turbo), 867 | "Abort" => Ok(Abort), 868 | "Resume" => Ok(Resume), 869 | "Suspend" => Ok(Suspend), 870 | "Again" => Ok(Again), 871 | "Copy" => Ok(Copy), 872 | "Cut" => Ok(Cut), 873 | "Find" => Ok(Find), 874 | "Open" => Ok(Open), 875 | "Paste" => Ok(Paste), 876 | "Props" => Ok(Props), 877 | "Select" => Ok(Select), 878 | "Undo" => Ok(Undo), 879 | "Hiragana" => Ok(Hiragana), 880 | "Katakana" => Ok(Katakana), 881 | "Unidentified" => Ok(Unidentified), 882 | "F1" => Ok(F1), 883 | "F2" => Ok(F2), 884 | "F3" => Ok(F3), 885 | "F4" => Ok(F4), 886 | "F5" => Ok(F5), 887 | "F6" => Ok(F6), 888 | "F7" => Ok(F7), 889 | "F8" => Ok(F8), 890 | "F9" => Ok(F9), 891 | "F10" => Ok(F10), 892 | "F11" => Ok(F11), 893 | "F12" => Ok(F12), 894 | "F13" => Ok(F13), 895 | "F14" => Ok(F14), 896 | "F15" => Ok(F15), 897 | "F16" => Ok(F16), 898 | "F17" => Ok(F17), 899 | "F18" => Ok(F18), 900 | "F19" => Ok(F19), 901 | "F20" => Ok(F20), 902 | "F21" => Ok(F21), 903 | "F22" => Ok(F22), 904 | "F23" => Ok(F23), 905 | "F24" => Ok(F24), 906 | "F25" => Ok(F25), 907 | "F26" => Ok(F26), 908 | "F27" => Ok(F27), 909 | "F28" => Ok(F28), 910 | "F29" => Ok(F29), 911 | "F30" => Ok(F30), 912 | "F31" => Ok(F31), 913 | "F32" => Ok(F32), 914 | "F33" => Ok(F33), 915 | "F34" => Ok(F34), 916 | "F35" => Ok(F35), 917 | "BrightnessDown" => Ok(BrightnessDown), 918 | "BrightnessUp" => Ok(BrightnessUp), 919 | "DisplayToggleIntExt" => Ok(DisplayToggleIntExt), 920 | "KeyboardLayoutSelect" => Ok(KeyboardLayoutSelect), 921 | "LaunchAssistant" => Ok(LaunchAssistant), 922 | "LaunchControlPanel" => Ok(LaunchControlPanel), 923 | "LaunchScreenSaver" => Ok(LaunchScreenSaver), 924 | "MailForward" => Ok(MailForward), 925 | "MailReply" => Ok(MailReply), 926 | "MailSend" => Ok(MailSend), 927 | "MediaFastForward" => Ok(MediaFastForward), 928 | "MediaPause" => Ok(MediaPause), 929 | "MediaPlay" => Ok(MediaPlay), 930 | "MediaRecord" => Ok(MediaRecord), 931 | "MediaRewind" => Ok(MediaRewind), 932 | "MicrophoneMuteToggle" => Ok(MicrophoneMuteToggle), 933 | "PrivacyScreenToggle" => Ok(PrivacyScreenToggle), 934 | "KeyboardBacklightToggle" => Ok(KeyboardBacklightToggle), 935 | "SelectTask" => Ok(SelectTask), 936 | "ShowAllWindows" => Ok(ShowAllWindows), 937 | "ZoomToggle" => Ok(ZoomToggle), 938 | 939 | _ => Err(UnrecognizedCodeError), 940 | } 941 | } 942 | } 943 | 944 | /// Parse from string error, returned when string does not match to any [`Code`] variant. 945 | #[derive(Clone, Debug)] 946 | pub struct UnrecognizedCodeError; 947 | 948 | impl fmt::Display for UnrecognizedCodeError { 949 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 950 | write!(f, "Unrecognized code") 951 | } 952 | } 953 | 954 | impl Error for UnrecognizedCodeError {} 955 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Contains types to define keyboard related events. 2 | //! 3 | //! The naming and conventions follow the UI Events specification 4 | //! but this crate should be useful for anyone implementing keyboard 5 | //! input in a cross-platform way. 6 | 7 | #![warn(clippy::doc_markdown)] 8 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] 9 | #![no_std] 10 | 11 | extern crate alloc; 12 | 13 | #[cfg(feature = "std")] 14 | extern crate std; 15 | 16 | use alloc::string::{String, ToString}; 17 | use core::fmt; 18 | use core::str::FromStr; 19 | 20 | pub use crate::code::{Code, UnrecognizedCodeError}; 21 | pub use crate::location::Location; 22 | pub use crate::modifiers::Modifiers; 23 | pub use crate::named_key::{NamedKey, UnrecognizedNamedKeyError}; 24 | pub use crate::shortcuts::ShortcutMatcher; 25 | 26 | mod code; 27 | mod location; 28 | mod modifiers; 29 | mod named_key; 30 | mod shortcuts; 31 | #[cfg(feature = "webdriver")] 32 | pub mod webdriver; 33 | 34 | #[cfg(feature = "serde")] 35 | use serde::{Deserialize, Serialize}; 36 | 37 | /// Describes the state a key is in. 38 | #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] 39 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 40 | pub enum KeyState { 41 | /// The key is pressed down. 42 | /// 43 | /// Often emitted in a [keydown] event, see also [the MDN documentation][mdn] on that. 44 | /// 45 | /// [keydown]: https://w3c.github.io/uievents/#event-type-keydown 46 | /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Element/keydown_event 47 | Down, 48 | /// The key is not pressed / was just released. 49 | /// 50 | /// Often emitted in a [keyup] event, see also [the MDN documentation][mdn] on that. 51 | /// 52 | /// [keyup]: https://w3c.github.io/uievents/#event-type-keyup 53 | /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Element/keyup_event 54 | Up, 55 | } 56 | 57 | impl KeyState { 58 | /// The [type] name of the corresponding key event. 59 | /// 60 | /// This is either `"keydown"` or `"keyup"`. 61 | /// 62 | /// [type]: https://w3c.github.io/uievents/#events-keyboard-types 63 | pub const fn event_type(self) -> &'static str { 64 | match self { 65 | Self::Down => "keydown", 66 | Self::Up => "keyup", 67 | } 68 | } 69 | 70 | /// True if the key is pressed down. 71 | pub const fn is_down(self) -> bool { 72 | matches!(self, Self::Down) 73 | } 74 | 75 | /// True if the key is released. 76 | pub const fn is_up(self) -> bool { 77 | matches!(self, Self::Up) 78 | } 79 | } 80 | 81 | /// Keyboard events are issued for all pressed and released keys. 82 | #[derive(Clone, Debug, Default, Eq, Hash, PartialEq, PartialOrd, Ord)] 83 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 84 | pub struct KeyboardEvent { 85 | /// Whether the key is pressed or released. 86 | pub state: KeyState, 87 | /// Logical key value. 88 | pub key: Key, 89 | /// Physical key position. 90 | pub code: Code, 91 | /// Location for keys with multiple instances on common keyboards. 92 | pub location: Location, 93 | /// Flags for pressed modifier keys. 94 | pub modifiers: Modifiers, 95 | /// True if the key is currently auto-repeated. 96 | pub repeat: bool, 97 | /// Events with this flag should be ignored in a text editor 98 | /// and instead [composition events](CompositionEvent) should be used. 99 | pub is_composing: bool, 100 | } 101 | 102 | /// Describes the state of a composition session. 103 | #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] 104 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 105 | pub enum CompositionState { 106 | /// The [compositionstart] event. 107 | /// 108 | /// See also [the MDN documentation][mdn]. 109 | /// 110 | /// [compositionstart]: https://w3c.github.io/uievents/#event-type-compositionstart 111 | /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Element/compositionstart_event 112 | Start, 113 | /// The [compositionupdate] event. 114 | /// 115 | /// See also [the MDN documentation][mdn]. 116 | /// 117 | /// [compositionupdate]: https://w3c.github.io/uievents/#event-type-compositionupdate 118 | /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Element/compositionupdate_event 119 | Update, 120 | /// The [compositionend] event. 121 | /// 122 | /// In a text editor, in this state the data should be added to the input. 123 | /// 124 | /// See also [the MDN documentation][mdn]. 125 | /// 126 | /// [compositionend]: https://w3c.github.io/uievents/#event-type-compositionend 127 | /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Element/compositionend_event 128 | End, 129 | } 130 | 131 | impl CompositionState { 132 | /// The [type] name of the corresponding composition event. 133 | /// 134 | /// This is either `"compositionstart"`, `"compositionupdate"` or `"compositionend"`. 135 | /// 136 | /// [type]: https://w3c.github.io/uievents/#events-composition-types 137 | pub const fn event_type(self) -> &'static str { 138 | match self { 139 | Self::Start => "compositionstart", 140 | Self::Update => "compositionupdate", 141 | Self::End => "compositionend", 142 | } 143 | } 144 | } 145 | 146 | /// Event to expose input methods to program logic. 147 | /// 148 | /// Provides information about entered sequences from 149 | /// dead key combinations and IMEs. 150 | /// 151 | /// A composition session is always started by a [`CompositionState::Start`] 152 | /// event followed by zero or more [`CompositionState::Update`] events 153 | /// and terminated by a single [`CompositionState::End`] event. 154 | #[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] 155 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 156 | pub struct CompositionEvent { 157 | /// Describes the event kind. 158 | pub state: CompositionState, 159 | /// Current composition data. May be empty. 160 | pub data: String, 161 | } 162 | 163 | /// The value received from the keypress. 164 | #[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] 165 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 166 | pub enum Key { 167 | /// A key string that corresponds to the character typed by the user, 168 | /// taking into account the user’s current locale setting, modifier state, 169 | /// and any system-level keyboard mapping overrides that are in effect. 170 | Character(String), 171 | Named(NamedKey), 172 | } 173 | 174 | /// Parse from string error, returned when string does not match to any [`Key`] variant. 175 | #[derive(Clone, Debug)] 176 | pub struct UnrecognizedKeyError; 177 | 178 | impl fmt::Display for Key { 179 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 180 | match self { 181 | Self::Character(s) => f.write_str(s), 182 | Self::Named(k) => k.fmt(f), 183 | } 184 | } 185 | } 186 | 187 | impl FromStr for Key { 188 | type Err = UnrecognizedKeyError; 189 | 190 | fn from_str(s: &str) -> Result { 191 | if is_key_string(s) { 192 | Ok(Self::Character(s.to_string())) 193 | } else { 194 | Ok(Self::Named( 195 | NamedKey::from_str(s).map_err(|_| UnrecognizedKeyError)?, 196 | )) 197 | } 198 | } 199 | } 200 | 201 | impl Key { 202 | /// Determine a *charCode* value for a key with a character value. 203 | /// 204 | /// For all other keys the value is zero. 205 | /// The *charCode* is an implementation specific legacy property of DOM keyboard events. 206 | /// 207 | /// Specification: 208 | pub fn legacy_charcode(&self) -> u32 { 209 | // Spec: event.charCode = event.key.charCodeAt(0) 210 | // otherwise 0 211 | match self { 212 | Key::Character(ref c) => c.chars().next().unwrap_or('\0') as u32, 213 | Key::Named(_) => 0, 214 | } 215 | } 216 | 217 | /// Determine a *keyCode* value for a key. 218 | /// 219 | /// The *keyCode* is an implementation specific legacy property of DOM keyboard events. 220 | /// 221 | /// Specification: 222 | pub fn legacy_keycode(&self) -> u32 { 223 | match self { 224 | // See: https://w3c.github.io/uievents/#fixed-virtual-key-codes 225 | Key::Named(NamedKey::Backspace) => 8, 226 | Key::Named(NamedKey::Tab) => 9, 227 | Key::Named(NamedKey::Enter) => 13, 228 | Key::Named(NamedKey::Shift) => 16, 229 | Key::Named(NamedKey::Control) => 17, 230 | Key::Named(NamedKey::Alt) => 18, 231 | Key::Named(NamedKey::CapsLock) => 20, 232 | Key::Named(NamedKey::Escape) => 27, 233 | Key::Named(NamedKey::PageUp) => 33, 234 | Key::Named(NamedKey::PageDown) => 34, 235 | Key::Named(NamedKey::End) => 35, 236 | Key::Named(NamedKey::Home) => 36, 237 | Key::Named(NamedKey::ArrowLeft) => 37, 238 | Key::Named(NamedKey::ArrowUp) => 38, 239 | Key::Named(NamedKey::ArrowRight) => 39, 240 | Key::Named(NamedKey::ArrowDown) => 40, 241 | Key::Named(NamedKey::Delete) => 46, 242 | Key::Character(ref c) if c.len() == 1 => match first_char(c) { 243 | ' ' => 32, 244 | x @ '0'..='9' => x as u32, 245 | x @ 'a'..='z' => x.to_ascii_uppercase() as u32, 246 | x @ 'A'..='Z' => x as u32, 247 | // See: https://w3c.github.io/uievents/#optionally-fixed-virtual-key-codes 248 | ';' | ':' => 186, 249 | '=' | '+' => 187, 250 | ',' | '<' => 188, 251 | '-' | '_' => 189, 252 | '.' | '>' => 190, 253 | '/' | '?' => 191, 254 | '`' | '~' => 192, 255 | '[' | '{' => 219, 256 | '\\' | '|' => 220, 257 | ']' | '}' => 221, 258 | '\'' | '\"' => 222, 259 | _ => 0, 260 | }, 261 | _ => 0, 262 | } 263 | } 264 | } 265 | 266 | impl Default for KeyState { 267 | fn default() -> KeyState { 268 | KeyState::Down 269 | } 270 | } 271 | 272 | impl Default for Key { 273 | fn default() -> Self { 274 | Self::Named(NamedKey::default()) 275 | } 276 | } 277 | 278 | impl Default for NamedKey { 279 | fn default() -> Self { 280 | Self::Unidentified 281 | } 282 | } 283 | 284 | impl Default for Code { 285 | fn default() -> Code { 286 | Code::Unidentified 287 | } 288 | } 289 | 290 | impl Default for Location { 291 | fn default() -> Location { 292 | Location::Standard 293 | } 294 | } 295 | 296 | /// Return the first codepoint of a string. 297 | /// 298 | /// # Panics 299 | /// Panics if the string is empty. 300 | fn first_char(s: &str) -> char { 301 | s.chars().next().expect("empty string") 302 | } 303 | 304 | /// Check if string can be used as a `Key::Character` _keystring_. 305 | /// 306 | /// This check is simple and is meant to prevents common mistakes like mistyped keynames 307 | /// (e.g. `Ennter`) from being recognized as characters. 308 | fn is_key_string(s: &str) -> bool { 309 | s.chars().all(|c| !c.is_control()) && s.chars().skip(1).all(|c| !c.is_ascii()) 310 | } 311 | 312 | #[cfg(test)] 313 | mod test { 314 | use super::*; 315 | 316 | #[test] 317 | fn test_is_key_string() { 318 | assert!(is_key_string("A")); 319 | assert!(!is_key_string("AA")); 320 | assert!(!is_key_string(" ")); 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /src/location.rs: -------------------------------------------------------------------------------- 1 | /// The location attribute contains an indication of the physical location of the key on the device. 2 | /// 3 | /// Certain keys on the keyboard can have the same value, but are in different locations. For 4 | /// instance, the Shift key can be on the left or right side of the keyboard, or the 5 | /// number keys can be above the letters or on the numpad. This enum allows differentiating them. 6 | /// 7 | /// See also [MDN's documentation](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/location). 8 | #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] 9 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 10 | pub enum Location { 11 | /// The key is in its "normal" location on the keyboard. 12 | /// 13 | /// The key activation MUST NOT be distinguished as the left or right 14 | /// version of the key, and (other than the `NumLock` key) did not 15 | /// originate from the numeric keypad (or did not originate with a 16 | /// virtual key corresponding to the numeric keypad). 17 | /// 18 | /// This variant is the default, and is also used when the location of the key cannot be 19 | /// identified. 20 | /// 21 | /// # Example 22 | /// 23 | /// The 1 key above the Q key on a QWERTY keyboard will use this location. 24 | /// 25 | #[doc = include_str!("../docs/keyboard_standard_1_key.svg")] 26 | /// 27 | /// For image attribution, see the [ATTRIBUTION.md] file. 28 | /// 29 | #[doc = concat!( 30 | "[ATTRIBUTION.md]: https://docs.rs/crate/keyboard-types/", 31 | env!("CARGO_PKG_VERSION"), 32 | "/source/docs/ATTRIBUTION.md", 33 | )] 34 | Standard = 0x00, 35 | 36 | /// The key activated originated from the left key location (when there 37 | /// is more than one possible location for this key). 38 | /// 39 | /// # Example 40 | /// 41 | /// The left Shift key below the Caps Lock key on a QWERTY keyboard will 42 | /// use this location. 43 | /// 44 | #[doc = include_str!("../docs/keyboard_left_shift_key.svg")] 45 | /// 46 | /// For image attribution, see the [ATTRIBUTION.md] file. 47 | /// 48 | #[doc = concat!( 49 | "[ATTRIBUTION.md]: https://docs.rs/crate/keyboard-types/", 50 | env!("CARGO_PKG_VERSION"), 51 | "/source/docs/ATTRIBUTION.md", 52 | )] 53 | Left = 0x01, 54 | 55 | /// The key activation originated from the right key location (when 56 | /// there is more than one possible location for this key). 57 | /// 58 | /// # Example 59 | /// 60 | /// The right Shift key below the Enter key on a QWERTY keyboard will use 61 | /// this location. 62 | /// 63 | #[doc = include_str!("../docs/keyboard_right_shift_key.svg")] 64 | /// 65 | /// For image attribution, see the [ATTRIBUTION.md] file. 66 | /// 67 | #[doc = concat!( 68 | "[ATTRIBUTION.md]: https://docs.rs/crate/keyboard-types/", 69 | env!("CARGO_PKG_VERSION"), 70 | "/source/docs/ATTRIBUTION.md", 71 | )] 72 | Right = 0x02, 73 | 74 | /// The key activation originated on the numeric keypad or with a virtual 75 | /// key corresponding to the numeric keypad (when there is more than one 76 | /// possible location for this key). Note that the `NumLock` key should 77 | /// always be encoded with a location of `Location::Standard`. 78 | /// 79 | /// # Example 80 | /// 81 | /// The 1 key on the numpad will use this location. 82 | /// 83 | #[doc = include_str!("../docs/keyboard_numpad_1_key.svg")] 84 | /// 85 | /// For image attribution, see the [ATTRIBUTION.md] file. 86 | /// 87 | #[doc = concat!( 88 | "[ATTRIBUTION.md]: https://docs.rs/crate/keyboard-types/", 89 | env!("CARGO_PKG_VERSION"), 90 | "/source/docs/ATTRIBUTION.md", 91 | )] 92 | Numpad = 0x03, 93 | } 94 | -------------------------------------------------------------------------------- /src/modifiers.rs: -------------------------------------------------------------------------------- 1 | //! Modifier key data. 2 | //! 3 | //! Modifier keys like Shift and Control alter the character value 4 | //! and are used in keyboard shortcuts. 5 | //! 6 | //! Use the constants to match for combinations of the modifier keys. 7 | 8 | bitflags::bitflags! { 9 | /// Pressed modifier keys. 10 | /// 11 | /// Specification: 12 | /// 13 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 14 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 15 | pub struct Modifiers: u32 { 16 | const ALT = 0x01; 17 | const ALT_GRAPH = 0x2; 18 | const CAPS_LOCK = 0x4; 19 | const CONTROL = 0x8; 20 | const FN = 0x10; 21 | const FN_LOCK = 0x20; 22 | const META = 0x40; 23 | const NUM_LOCK = 0x80; 24 | const SCROLL_LOCK = 0x100; 25 | const SHIFT = 0x200; 26 | const SYMBOL = 0x400; 27 | const SYMBOL_LOCK = 0x800; 28 | #[deprecated = "marked as legacy in the spec, use META instead"] 29 | const HYPER = 0x1000; 30 | #[deprecated = "marked as legacy in the spec, use META instead"] 31 | const SUPER = 0x2000; 32 | } 33 | } 34 | 35 | impl Modifiers { 36 | /// Return `true` if a shift key is pressed. 37 | pub fn shift(&self) -> bool { 38 | self.contains(Modifiers::SHIFT) 39 | } 40 | 41 | /// Return `true` if a control key is pressed. 42 | pub fn ctrl(&self) -> bool { 43 | self.contains(Modifiers::CONTROL) 44 | } 45 | 46 | /// Return `true` if an alt key is pressed. 47 | pub fn alt(&self) -> bool { 48 | self.contains(Modifiers::ALT) 49 | } 50 | 51 | /// Return `true` if a meta key is pressed. 52 | pub fn meta(&self) -> bool { 53 | self.contains(Modifiers::META) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/shortcuts.rs: -------------------------------------------------------------------------------- 1 | use crate::{Key, KeyState, KeyboardEvent, Modifiers}; 2 | 3 | /// Match keyboard shortcuts and execute actions. 4 | /// 5 | /// Every shortcut consists of a list of modifier keys pressed and a 6 | /// single non-modifier key pressed. 7 | /// 8 | /// The Control + C shortcut requires the user to hold down the Control 9 | /// modifier key. When the C key is pressed the action (usually copy) 10 | /// is triggered. The event is consumed so other matchers don't also 11 | /// act on the shortcut. This is also true for the release of the 12 | /// C key as else only key release events would be forwarded. 13 | /// 14 | /// ASCII letters are compared ignoring case. Only takes 15 | /// the shift, control, alt and meta modifiers into account. 16 | /// If other modifiers beside those expected are found 17 | /// the shortcut is not matched. 18 | pub struct ShortcutMatcher { 19 | state: KeyState, 20 | key: Key, 21 | modifiers: Modifiers, 22 | matched: bool, 23 | value: Option, 24 | } 25 | 26 | impl ShortcutMatcher { 27 | /// Create a new shortcut matcher. 28 | pub fn new(state: KeyState, key: Key, mut modifiers: Modifiers) -> ShortcutMatcher { 29 | modifiers &= Modifiers::SHIFT | Modifiers::CONTROL | Modifiers::ALT | Modifiers::META; 30 | ShortcutMatcher { 31 | state, 32 | key, 33 | modifiers, 34 | matched: false, 35 | value: None, 36 | } 37 | } 38 | 39 | /// Create a new matcher from an event. 40 | /// 41 | /// Only state, key and modifiers are stored. The other attributes are discarded. 42 | pub fn from_event(key_event: KeyboardEvent) -> ShortcutMatcher { 43 | ShortcutMatcher::new(key_event.state, key_event.key, key_event.modifiers) 44 | } 45 | 46 | /// Test a keyboard shortcut. 47 | /// 48 | /// If the modifiers are active and the key is pressed, 49 | /// execute the provided function. 50 | /// 51 | /// ```rust 52 | /// # use keyboard_types::{Key, KeyboardEvent, Modifiers, NamedKey, ShortcutMatcher}; 53 | /// # fn do_something() {} 54 | /// # fn forward_event() {} 55 | /// # let event = KeyboardEvent { 56 | /// # state: keyboard_types::KeyState::Down, 57 | /// # key: Key::Named(NamedKey::Enter), 58 | /// # code: keyboard_types::Code::Enter, 59 | /// # location: keyboard_types::Location::Standard, 60 | /// # modifiers: Modifiers::empty(), 61 | /// # repeat: false, 62 | /// # is_composing: false, 63 | /// # }; 64 | /// // Create a matcher from a keyboard event. 65 | /// // Shortcuts are tested in-order. 66 | /// ShortcutMatcher::from_event(event) 67 | /// // Do something if the Tab key is pressed. 68 | /// .shortcut(Modifiers::empty(), Key::Named(NamedKey::Tab), do_something) 69 | /// // If Shift + Tab are pressed do something. 70 | /// // This is executed because the previous shortcut requires modifiers to be empty. 71 | /// .shortcut(Modifiers::SHIFT, Key::Named(NamedKey::Tab), do_something) 72 | /// // Instead of named keys letters and other characters can be used. 73 | /// .shortcut(Modifiers::CONTROL, 'L', do_something) 74 | /// // Multiple modifiers are combined with bitwise OR (`|`) to form a new mask. 75 | /// .shortcut(Modifiers::CONTROL | Modifiers::SHIFT, 'X', do_something) 76 | /// // If none of the previous shortcuts matched forward the event. 77 | /// .otherwise(forward_event); 78 | /// ``` 79 | pub fn shortcut(mut self, modifiers: Modifiers, key: K, f: F) -> ShortcutMatcher 80 | where 81 | K: MatchKey, 82 | F: (FnOnce() -> T), 83 | { 84 | if self.matched { 85 | return self; 86 | } 87 | if modifiers == self.modifiers && key.match_key(&self.key) { 88 | if self.state.is_down() { 89 | self.value = Some(f()); 90 | } 91 | self.matched = true; 92 | } 93 | self 94 | } 95 | 96 | /// Only test a shortcut if the enabled flag is set. 97 | /// 98 | /// If the `enabled` flag is true behaves the same as 99 | /// `shortcut` otherwise does nothing. 100 | /// 101 | /// This is especially useful for platform specific shortcuts. 102 | /// 103 | /// ```rust 104 | /// # use keyboard_types::{Key, KeyboardEvent, Modifiers, NamedKey, ShortcutMatcher}; 105 | /// # fn copy() {} 106 | /// # fn quit() {} 107 | /// # let event = KeyboardEvent { 108 | /// # state: keyboard_types::KeyState::Down, 109 | /// # key: Key::Named(NamedKey::Enter), 110 | /// # code: keyboard_types::Code::Enter, 111 | /// # location: keyboard_types::Location::Standard, 112 | /// # modifiers: Modifiers::empty(), 113 | /// # repeat: false, 114 | /// # is_composing: false, 115 | /// # }; 116 | /// ShortcutMatcher::from_event(event) 117 | /// .shortcut(Modifiers::CONTROL, 'c', copy) 118 | /// .optional_shortcut(cfg!(target_os="macos"), Modifiers::META, 'q', quit) 119 | /// .shortcut(Modifiers::CONTROL, 'w', quit); 120 | /// ``` 121 | /// 122 | /// In the example the app supports the copy action on all platforms 123 | /// and can be closed with Control + W everywhere but additionally 124 | /// with Command + Q on Mac OS. 125 | pub fn optional_shortcut( 126 | self, 127 | enabled: bool, 128 | modifiers: Modifiers, 129 | key: K, 130 | f: F, 131 | ) -> ShortcutMatcher 132 | where 133 | K: MatchKey, 134 | F: (FnOnce() -> T), 135 | { 136 | if !enabled { 137 | return self; 138 | } 139 | self.shortcut(modifiers, key, f) 140 | } 141 | 142 | /// Execute the function is no keyboard shortcut matched. 143 | /// 144 | /// Note that the passed function is executed on both 145 | /// keydown and keyup unlike the shortcuts which only 146 | /// run on keydown. 147 | pub fn otherwise(self, f: F) -> Option 148 | where 149 | F: (FnOnce() -> T), 150 | { 151 | if !self.matched { 152 | Some(f()) 153 | } else { 154 | self.value 155 | } 156 | } 157 | } 158 | 159 | pub trait MatchKey { 160 | fn match_key(&self, key: &Key) -> bool; 161 | } 162 | 163 | impl MatchKey for Key { 164 | fn match_key(&self, key: &Key) -> bool { 165 | self == key 166 | } 167 | } 168 | 169 | impl MatchKey for char { 170 | fn match_key(&self, key: &Key) -> bool { 171 | match key { 172 | Key::Character(text) => { 173 | let mut buf = [0; 4]; 174 | text.eq_ignore_ascii_case(self.encode_utf8(&mut buf)) 175 | } 176 | _ => false, 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/webdriver.rs: -------------------------------------------------------------------------------- 1 | //! Keyboard related WebDriver functionality. 2 | //! 3 | //! The low-level [`KeyInputState::dispatch_keydown`] and 4 | //! [`KeyInputState::dispatch_keyup`] API creates keyboard events 5 | //! from WebDriver codes. It is used in the *Perform Actions* API. 6 | //! 7 | //! ```rust 8 | //! # extern crate keyboard_types; 9 | //! # use keyboard_types::*; 10 | //! # use keyboard_types::webdriver::*; 11 | //! let mut state = KeyInputState::new(); 12 | //! let mut keyboard_event = state.dispatch_keydown('a'); 13 | //! assert_eq!(keyboard_event.state, KeyState::Down); 14 | //! assert_eq!(keyboard_event.key, Key::Character("a".to_owned())); 15 | //! assert_eq!(keyboard_event.code, Code::KeyA); 16 | //! 17 | //! // The `\u{E029}` code is the WebDriver id for the Numpad divide key. 18 | //! keyboard_event = state.dispatch_keydown('\u{E050}'); 19 | //! assert_eq!(keyboard_event.key, Key::Named(NamedKey::Shift)); 20 | //! assert_eq!(keyboard_event.code, Code::ShiftRight); 21 | //! assert_eq!(keyboard_event.location, Location::Right); 22 | //! 23 | //! keyboard_event = state.dispatch_keyup('\u{E050}').expect("key is released"); 24 | //! keyboard_event = state.dispatch_keyup('a').expect("key is released"); 25 | //! ``` 26 | //! 27 | //! The higher level [`send_keys`] function is used for the *Element Send Keys* 28 | //! WebDriver API. It accepts a string and returns a sequence of [`KeyboardEvent`] 29 | //! and [`CompositionEvent`] values. 30 | //! 31 | //! ```rust 32 | //! # extern crate keyboard_types; 33 | //! # use keyboard_types::*; 34 | //! # use keyboard_types::webdriver::*; 35 | //! let events = send_keys("Hello world!\u{E006}"); 36 | //! println!("{:#?}", events); 37 | //! 38 | //! let events = send_keys("A\u{0308}"); 39 | //! println!("{:#?}", events); 40 | //! ``` 41 | //! 42 | //! Specification: 43 | 44 | use alloc::borrow::ToOwned; 45 | use alloc::string::{String, ToString}; 46 | use alloc::vec::Vec; 47 | use std::collections::HashSet; 48 | 49 | use unicode_segmentation::UnicodeSegmentation; 50 | 51 | use crate::{first_char, NamedKey}; 52 | use crate::{Code, Key, KeyState, KeyboardEvent, Location, Modifiers}; 53 | use crate::{CompositionEvent, CompositionState}; 54 | 55 | // Spec: 56 | // normalised (sic) as in british spelling 57 | fn normalised_key_value(raw_key: char) -> Key { 58 | match raw_key { 59 | '\u{E000}' => Key::Named(NamedKey::Unidentified), 60 | '\u{E001}' => Key::Named(NamedKey::Cancel), 61 | '\u{E002}' => Key::Named(NamedKey::Help), 62 | '\u{E003}' => Key::Named(NamedKey::Backspace), 63 | '\u{E004}' => Key::Named(NamedKey::Tab), 64 | '\u{E005}' => Key::Named(NamedKey::Clear), 65 | // FIXME: spec says "Return" 66 | '\u{E006}' => Key::Named(NamedKey::Enter), 67 | '\u{E007}' => Key::Named(NamedKey::Enter), 68 | '\u{E008}' => Key::Named(NamedKey::Shift), 69 | '\u{E009}' => Key::Named(NamedKey::Control), 70 | '\u{E00A}' => Key::Named(NamedKey::Alt), 71 | '\u{E00B}' => Key::Named(NamedKey::Pause), 72 | '\u{E00C}' => Key::Named(NamedKey::Escape), 73 | '\u{E00D}' => Key::Character(" ".to_string()), 74 | '\u{E00E}' => Key::Named(NamedKey::PageUp), 75 | '\u{E00F}' => Key::Named(NamedKey::PageDown), 76 | '\u{E010}' => Key::Named(NamedKey::End), 77 | '\u{E011}' => Key::Named(NamedKey::Home), 78 | '\u{E012}' => Key::Named(NamedKey::ArrowLeft), 79 | '\u{E013}' => Key::Named(NamedKey::ArrowUp), 80 | '\u{E014}' => Key::Named(NamedKey::ArrowRight), 81 | '\u{E015}' => Key::Named(NamedKey::ArrowDown), 82 | '\u{E016}' => Key::Named(NamedKey::Insert), 83 | '\u{E017}' => Key::Named(NamedKey::Delete), 84 | '\u{E018}' => Key::Character(";".to_string()), 85 | '\u{E019}' => Key::Character("=".to_string()), 86 | '\u{E01A}' => Key::Character("0".to_string()), 87 | '\u{E01B}' => Key::Character("1".to_string()), 88 | '\u{E01C}' => Key::Character("2".to_string()), 89 | '\u{E01D}' => Key::Character("3".to_string()), 90 | '\u{E01E}' => Key::Character("4".to_string()), 91 | '\u{E01F}' => Key::Character("5".to_string()), 92 | '\u{E020}' => Key::Character("6".to_string()), 93 | '\u{E021}' => Key::Character("7".to_string()), 94 | '\u{E022}' => Key::Character("8".to_string()), 95 | '\u{E023}' => Key::Character("9".to_string()), 96 | '\u{E024}' => Key::Character("*".to_string()), 97 | '\u{E025}' => Key::Character("+".to_string()), 98 | '\u{E026}' => Key::Character(",".to_string()), 99 | '\u{E027}' => Key::Character("-".to_string()), 100 | '\u{E028}' => Key::Character(".".to_string()), 101 | '\u{E029}' => Key::Character("/".to_string()), 102 | '\u{E031}' => Key::Named(NamedKey::F1), 103 | '\u{E032}' => Key::Named(NamedKey::F2), 104 | '\u{E033}' => Key::Named(NamedKey::F3), 105 | '\u{E034}' => Key::Named(NamedKey::F4), 106 | '\u{E035}' => Key::Named(NamedKey::F5), 107 | '\u{E036}' => Key::Named(NamedKey::F6), 108 | '\u{E037}' => Key::Named(NamedKey::F7), 109 | '\u{E038}' => Key::Named(NamedKey::F8), 110 | '\u{E039}' => Key::Named(NamedKey::F9), 111 | '\u{E03A}' => Key::Named(NamedKey::F10), 112 | '\u{E03B}' => Key::Named(NamedKey::F11), 113 | '\u{E03C}' => Key::Named(NamedKey::F12), 114 | '\u{E03D}' => Key::Named(NamedKey::Meta), 115 | '\u{E040}' => Key::Named(NamedKey::ZenkakuHankaku), 116 | '\u{E050}' => Key::Named(NamedKey::Shift), 117 | '\u{E051}' => Key::Named(NamedKey::Control), 118 | '\u{E052}' => Key::Named(NamedKey::Alt), 119 | '\u{E053}' => Key::Named(NamedKey::Meta), 120 | '\u{E054}' => Key::Named(NamedKey::PageUp), 121 | '\u{E055}' => Key::Named(NamedKey::PageDown), 122 | '\u{E056}' => Key::Named(NamedKey::End), 123 | '\u{E057}' => Key::Named(NamedKey::Home), 124 | '\u{E058}' => Key::Named(NamedKey::ArrowLeft), 125 | '\u{E059}' => Key::Named(NamedKey::ArrowUp), 126 | '\u{E05A}' => Key::Named(NamedKey::ArrowRight), 127 | '\u{E05B}' => Key::Named(NamedKey::ArrowDown), 128 | '\u{E05C}' => Key::Named(NamedKey::Insert), 129 | '\u{E05D}' => Key::Named(NamedKey::Delete), 130 | _ => Key::Character(raw_key.to_string()), 131 | } 132 | } 133 | 134 | /// Spec: 135 | fn code(raw_key: char) -> Code { 136 | match raw_key { 137 | '`' | '~' => Code::Backquote, 138 | '\\' | '|' => Code::Backslash, 139 | '\u{E003}' => Code::Backspace, 140 | '[' | '{' => Code::BracketLeft, 141 | ']' | '}' => Code::BracketRight, 142 | ',' | '<' => Code::Comma, 143 | '0' | ')' => Code::Digit0, 144 | '1' | '!' => Code::Digit1, 145 | '2' | '@' => Code::Digit2, 146 | '3' | '#' => Code::Digit3, 147 | '4' | '$' => Code::Digit4, 148 | '5' | '%' => Code::Digit5, 149 | '6' | '^' => Code::Digit6, 150 | '7' | '&' => Code::Digit7, 151 | '8' | '*' => Code::Digit8, 152 | '9' | '(' => Code::Digit9, 153 | '=' | '+' => Code::Equal, 154 | // FIXME: spec has '<' | '>' => Code::IntlBackslash, 155 | 'a' | 'A' => Code::KeyA, 156 | 'b' | 'B' => Code::KeyB, 157 | 'c' | 'C' => Code::KeyC, 158 | 'd' | 'D' => Code::KeyD, 159 | 'e' | 'E' => Code::KeyE, 160 | 'f' | 'F' => Code::KeyF, 161 | 'g' | 'G' => Code::KeyG, 162 | 'h' | 'H' => Code::KeyH, 163 | 'i' | 'I' => Code::KeyI, 164 | 'j' | 'J' => Code::KeyJ, 165 | 'k' | 'K' => Code::KeyK, 166 | 'l' | 'L' => Code::KeyL, 167 | 'm' | 'M' => Code::KeyM, 168 | 'n' | 'N' => Code::KeyN, 169 | 'o' | 'O' => Code::KeyO, 170 | 'p' | 'P' => Code::KeyP, 171 | 'q' | 'Q' => Code::KeyQ, 172 | 'r' | 'R' => Code::KeyR, 173 | 's' | 'S' => Code::KeyS, 174 | 't' | 'T' => Code::KeyT, 175 | 'u' | 'U' => Code::KeyU, 176 | 'v' | 'V' => Code::KeyV, 177 | 'w' | 'W' => Code::KeyW, 178 | 'x' | 'X' => Code::KeyX, 179 | 'y' | 'Y' => Code::KeyY, 180 | 'z' | 'Z' => Code::KeyZ, 181 | '-' | '_' => Code::Minus, 182 | '.' | '>' => Code::Period, 183 | '\'' | '"' => Code::Quote, 184 | ';' | ':' => Code::Semicolon, 185 | '/' | '?' => Code::Slash, 186 | '\u{E00A}' => Code::AltLeft, 187 | '\u{E052}' => Code::AltRight, 188 | '\u{E009}' => Code::ControlLeft, 189 | '\u{E051}' => Code::ControlRight, 190 | '\u{E006}' => Code::Enter, 191 | // FIXME: spec says "OSLeft" 192 | '\u{E03D}' => Code::MetaLeft, 193 | // FIXME: spec says "OSRight" 194 | '\u{E053}' => Code::MetaRight, 195 | '\u{E008}' => Code::ShiftLeft, 196 | '\u{E050}' => Code::ShiftRight, 197 | ' ' | '\u{E00D}' => Code::Space, 198 | '\u{E004}' => Code::Tab, 199 | '\u{E017}' => Code::Delete, 200 | '\u{E010}' => Code::End, 201 | '\u{E002}' => Code::Help, 202 | '\u{E011}' => Code::Home, 203 | '\u{E016}' => Code::Insert, 204 | // FIXME: spec says '\u{E01E}' => Code::PageDown, which is Numpad 4 205 | '\u{E00F}' => Code::PageDown, 206 | // FIXME: spec says '\u{E01F}' => Code::PageUp, which is Numpad 5 207 | '\u{E00E}' => Code::PageUp, 208 | '\u{E015}' => Code::ArrowDown, 209 | '\u{E012}' => Code::ArrowLeft, 210 | '\u{E014}' => Code::ArrowRight, 211 | '\u{E013}' => Code::ArrowUp, 212 | '\u{E00C}' => Code::Escape, 213 | '\u{E031}' => Code::F1, 214 | '\u{E032}' => Code::F2, 215 | '\u{E033}' => Code::F3, 216 | '\u{E034}' => Code::F4, 217 | '\u{E035}' => Code::F5, 218 | '\u{E036}' => Code::F6, 219 | '\u{E037}' => Code::F7, 220 | '\u{E038}' => Code::F8, 221 | '\u{E039}' => Code::F9, 222 | '\u{E03A}' => Code::F10, 223 | '\u{E03B}' => Code::F11, 224 | '\u{E03C}' => Code::F12, 225 | '\u{E01A}' | '\u{E05C}' => Code::Numpad0, 226 | '\u{E01B}' | '\u{E056}' => Code::Numpad1, 227 | '\u{E01C}' | '\u{E05B}' => Code::Numpad2, 228 | '\u{E01D}' | '\u{E055}' => Code::Numpad3, 229 | '\u{E01E}' | '\u{E058}' => Code::Numpad4, 230 | '\u{E01F}' => Code::Numpad5, 231 | '\u{E020}' | '\u{E05A}' => Code::Numpad6, 232 | '\u{E021}' | '\u{E057}' => Code::Numpad7, 233 | '\u{E022}' | '\u{E059}' => Code::Numpad8, 234 | '\u{E023}' | '\u{E054}' => Code::Numpad9, 235 | // FIXME: spec says uE024 236 | '\u{E025}' => Code::NumpadAdd, 237 | '\u{E026}' => Code::NumpadComma, 238 | '\u{E028}' | '\u{E05D}' => Code::NumpadDecimal, 239 | '\u{E029}' => Code::NumpadDivide, 240 | '\u{E007}' => Code::NumpadEnter, 241 | '\u{E024}' => Code::NumpadMultiply, 242 | // FIXME: spec says uE026 243 | '\u{E027}' => Code::NumpadSubtract, 244 | _ => Code::Unidentified, 245 | } 246 | } 247 | 248 | fn is_shifted_character(raw_key: char) -> bool { 249 | matches!( 250 | raw_key, 251 | '~' | '|' 252 | | '{' 253 | | '}' 254 | | '<' 255 | | ')' 256 | | '!' 257 | | '@' 258 | | '#' 259 | | '$' 260 | | '%' 261 | | '^' 262 | | '&' 263 | | '*' 264 | | '(' 265 | | '+' 266 | | '>' 267 | | '_' 268 | | '\"' 269 | | ':' 270 | | '?' 271 | | '\u{E00D}' 272 | | '\u{E05C}' 273 | | '\u{E056}' 274 | | '\u{E05B}' 275 | | '\u{E055}' 276 | | '\u{E058}' 277 | | '\u{E05A}' 278 | | '\u{E057}' 279 | | '\u{E059}' 280 | | '\u{E054}' 281 | | '\u{E05D}' 282 | | 'A'..='Z' 283 | ) 284 | } 285 | 286 | fn key_location(raw_key: char) -> Location { 287 | match raw_key { 288 | '\u{E007}'..='\u{E00A}' => Location::Left, 289 | '\u{E01A}'..='\u{E029}' => Location::Numpad, 290 | '\u{E03D}' => Location::Left, 291 | '\u{E050}'..='\u{E053}' => Location::Right, 292 | '\u{E054}'..='\u{E05D}' => Location::Numpad, 293 | _ => Location::Standard, 294 | } 295 | } 296 | 297 | fn get_modifier(key: &Key) -> Modifiers { 298 | match key { 299 | Key::Named(NamedKey::Alt) => Modifiers::ALT, 300 | Key::Named(NamedKey::Shift) => Modifiers::SHIFT, 301 | Key::Named(NamedKey::Control) => Modifiers::CONTROL, 302 | Key::Named(NamedKey::Meta) => Modifiers::META, 303 | _ => Modifiers::empty(), 304 | } 305 | } 306 | 307 | /// Store pressed keys and modifiers. 308 | /// 309 | /// Spec: 310 | #[derive(Clone, Debug, Default)] 311 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 312 | pub struct KeyInputState { 313 | pressed: HashSet, 314 | modifiers: Modifiers, 315 | } 316 | 317 | impl KeyInputState { 318 | /// New state without any keys or modifiers pressed. 319 | /// 320 | /// Same as the default value. 321 | pub fn new() -> KeyInputState { 322 | KeyInputState::default() 323 | } 324 | 325 | /// Get a keyboard-keydown event from a WebDriver key value. 326 | /// 327 | /// Stores that the key is pressed in the state object. 328 | /// 329 | /// The input cancel list is not implemented here but can be emulated 330 | /// by adding the `raw_key` value with a `keyUp` action to a list 331 | /// before executing this function. 332 | /// 333 | /// Specification: 334 | pub fn dispatch_keydown(&mut self, raw_key: char) -> KeyboardEvent { 335 | let key = normalised_key_value(raw_key); 336 | let repeat = self.pressed.contains(&key); 337 | let code = code(raw_key); 338 | let location = key_location(raw_key); 339 | self.modifiers.insert(get_modifier(&key)); 340 | self.pressed.insert(key.clone()); 341 | KeyboardEvent { 342 | state: KeyState::Down, 343 | key, 344 | code, 345 | location, 346 | modifiers: self.modifiers, 347 | repeat, 348 | is_composing: false, 349 | } 350 | } 351 | 352 | /// Get a keyboard-keyup event from a WebDriver key value. 353 | /// 354 | /// Updates state. Returns `None` if the key is not listed as pressed. 355 | /// 356 | /// Specification: 357 | pub fn dispatch_keyup(&mut self, raw_key: char) -> Option { 358 | let key = normalised_key_value(raw_key); 359 | if !self.pressed.contains(&key) { 360 | return None; 361 | } 362 | let code = code(raw_key); 363 | let location = key_location(raw_key); 364 | self.modifiers.remove(get_modifier(&key)); 365 | self.pressed.remove(&key); 366 | Some(KeyboardEvent { 367 | state: KeyState::Up, 368 | key, 369 | code, 370 | location, 371 | modifiers: self.modifiers, 372 | repeat: false, 373 | is_composing: false, 374 | }) 375 | } 376 | 377 | fn clear(&mut self, undo_actions: &mut HashSet, result: &mut Vec) { 378 | let mut actions: Vec<_> = undo_actions.drain().collect(); 379 | actions.sort_unstable(); 380 | for action in actions { 381 | result.push(self.dispatch_keyup(action).unwrap().into()); 382 | } 383 | assert!(undo_actions.is_empty()); 384 | } 385 | 386 | fn dispatch_typeable(&mut self, text: &mut String, result: &mut Vec) { 387 | for character in text.chars() { 388 | let shifted = self.modifiers.contains(Modifiers::SHIFT); 389 | if is_shifted_character(character) && !shifted { 390 | // dispatch left shift down 391 | result.push(self.dispatch_keydown('\u{E008}').into()); 392 | } 393 | if !is_shifted_character(character) && shifted { 394 | // dispatch left shift up 395 | result.push(self.dispatch_keyup('\u{E008}').unwrap().into()); 396 | } 397 | result.push(self.dispatch_keydown(character).into()); 398 | result.push(self.dispatch_keyup(character).unwrap().into()); 399 | } 400 | text.clear(); 401 | } 402 | } 403 | 404 | /// Either a [`KeyboardEvent`] or a [`CompositionEvent`]. 405 | /// 406 | /// Returned by the [`send_keys`] function. 407 | #[derive(Clone, Eq, PartialEq, Hash, Debug, PartialOrd, Ord)] 408 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 409 | pub enum Event { 410 | Keyboard(KeyboardEvent), 411 | Composition(CompositionEvent), 412 | } 413 | 414 | impl From for Event { 415 | fn from(v: KeyboardEvent) -> Event { 416 | Event::Keyboard(v) 417 | } 418 | } 419 | 420 | impl From for Event { 421 | fn from(v: CompositionEvent) -> Event { 422 | Event::Composition(v) 423 | } 424 | } 425 | 426 | /// Compute the events resulting from a WebDriver *Element Send Keys* command. 427 | /// 428 | /// Spec: 429 | pub fn send_keys(text: &str) -> Vec { 430 | #[allow(deprecated)] 431 | fn is_modifier(text: &str) -> bool { 432 | if text.chars().count() != 1 { 433 | return false; 434 | } 435 | // values from 436 | matches!( 437 | normalised_key_value(first_char(text)), 438 | Key::Named( 439 | NamedKey::Alt 440 | | NamedKey::AltGraph 441 | | NamedKey::CapsLock 442 | | NamedKey::Control 443 | | NamedKey::Fn 444 | | NamedKey::FnLock 445 | | NamedKey::Meta 446 | | NamedKey::NumLock 447 | | NamedKey::ScrollLock 448 | | NamedKey::Shift 449 | | NamedKey::Symbol 450 | | NamedKey::SymbolLock 451 | | NamedKey::Hyper 452 | | NamedKey::Super 453 | ) 454 | ) 455 | } 456 | 457 | /// Spec: 458 | fn is_typeable(text: &str) -> bool { 459 | text.chars().count() == 1 460 | } 461 | 462 | let mut result = Vec::new(); 463 | let mut typeable_text = String::new(); 464 | let mut state = KeyInputState::new(); 465 | let mut undo_actions = HashSet::new(); 466 | for cluster in UnicodeSegmentation::graphemes(text, true) { 467 | match cluster { 468 | "\u{E000}" => { 469 | state.dispatch_typeable(&mut typeable_text, &mut result); 470 | state.clear(&mut undo_actions, &mut result); 471 | } 472 | s if is_modifier(s) => { 473 | state.dispatch_typeable(&mut typeable_text, &mut result); 474 | let raw_modifier = first_char(s); 475 | result.push(state.dispatch_keydown(raw_modifier).into()); 476 | undo_actions.insert(raw_modifier); 477 | } 478 | s if is_typeable(s) => typeable_text.push_str(s), 479 | s => { 480 | state.dispatch_typeable(&mut typeable_text, &mut result); 481 | // FIXME: Spec says undefined instead of empty string 482 | result.push( 483 | CompositionEvent { 484 | state: CompositionState::Start, 485 | data: String::new(), 486 | } 487 | .into(), 488 | ); 489 | result.push( 490 | CompositionEvent { 491 | state: CompositionState::Update, 492 | data: s.to_owned(), 493 | } 494 | .into(), 495 | ); 496 | result.push( 497 | CompositionEvent { 498 | state: CompositionState::End, 499 | data: s.to_owned(), 500 | } 501 | .into(), 502 | ); 503 | } 504 | } 505 | } 506 | state.dispatch_typeable(&mut typeable_text, &mut result); 507 | state.clear(&mut undo_actions, &mut result); 508 | result 509 | } 510 | --------------------------------------------------------------------------------