├── .github └── workflows │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── NOTICE ├── README.md ├── examples ├── noline.rs └── simple.rs └── src ├── lib.rs └── menu_manager.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: [push, pull_request] 4 | 5 | # Make sure CI fails on all warnings, including Clippy lints 6 | env: 7 | RUSTFLAGS: "-Dwarnings" 8 | 9 | jobs: 10 | formatting: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Check formatting 15 | run: cargo fmt -- --check 16 | 17 | build-test: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Build (default features) 22 | run: cargo build 23 | - name: Build (no features) 24 | run: cargo build --no-default-features 25 | - name: Build (all features) 26 | run: cargo build --all-features 27 | - name: Run Tests (default features) 28 | run: cargo test 29 | - name: Run Tests (no features) 30 | run: cargo test --no-default-features 31 | - name: Run Tests (all features) 32 | run: cargo test --all-features 33 | 34 | clippy: 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@v4 38 | - name: Clippy (default features) 39 | run: cargo clippy 40 | - name: Clippy (no features) 41 | run: cargo clippy --no-default-features 42 | - name: Clippy (all features) 43 | run: cargo clippy --all-features 44 | 45 | 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /target/ 3 | **/*.rs.bk 4 | Cargo.lock 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/). 7 | 8 | ## [Unreleased](https://github.com/rust-embedded-community/menu/compare/v0.6.1...master) 9 | 10 | ## [v0.6.1] - 2024-11-29 11 | 12 | ### Changed 13 | 14 | * For `Runner::input_byte` the buffer `B` does not need to be `Sized` 15 | 16 | ### Added 17 | 18 | * `impl core::error::Error for Error` on rust >= 1.81 19 | 20 | ## [v0.6.0] - 2024-08-30 21 | 22 | ### Changed 23 | 24 | * We now run clippy in CI 25 | * Add optional support for [`noline`](https://crates.io/crates/noline) as a line-editor with history 26 | * The interface we use for reading and writing bytes must now implement the [`embedded-io`](https://crates.io/crates/embedded-io) traits 27 | 28 | ## [v0.5.1] - 2024-08-22 29 | 30 | ### Fixed 31 | 32 | * Fix Menu entry call order 33 | 34 | ## [v0.5.0] - 2024-04-26 35 | 36 | ### Changed 37 | 38 | * [breaking] The `menu` `Context` is now borrowed during I/O input processing to support borrowed data 39 | * [breaking] The `pub context` item on the runner was updated to `pub interface` 40 | 41 | ## [v0.4.0] - 2023-09-16 42 | 43 | ### Changed 44 | 45 | * Changed the struct `Runner` to own the struct `Menu` instead of borrowing it 46 | 47 | ### Added 48 | 49 | * Made `struct Menu` implement `Clone` 50 | * Add the possibility to disable local echo (via `echo` feature, enabled by default) 51 | 52 | ## [v0.3.2] - 2019-08-22 53 | 54 | ### Changed 55 | 56 | * Tidied up help text 57 | 58 | ## [v0.3.1] - 2019-08-11 59 | 60 | 61 | ## [v0.3.0] - 2019-08-11 62 | 63 | ### Changed 64 | 65 | * Parameter / Argument support 66 | * Re-worked help text system 67 | * Example uses `pancurses` 68 | * Remove use of fixed width (assumes a Western set with one byte per glyph) 69 | 70 | ## [v0.2.1] - 2018-10-04 71 | 72 | ### Changed 73 | 74 | * Fixed broken example 75 | 76 | ## [v0.2.0] - 2018-10-04 77 | 78 | * Add context to menu callback 79 | * Fix width of help text 80 | 81 | ## [v0.1.1] - 2018-05-19 82 | 83 | * First release 84 | 85 | [v0.6.1]: https://github.com/rust-embedded-community/menu/releases/tag/v0.6.1 86 | [v0.6.0]: https://github.com/rust-embedded-community/menu/releases/tag/v0.6.0 87 | [v0.5.1]: https://github.com/rust-embedded-community/menu/releases/tag/v0.5.1 88 | [v0.5.0]: https://github.com/rust-embedded-community/menu/releases/tag/v0.5.0 89 | [v0.4.0]: https://github.com/rust-embedded-community/menu/releases/tag/v0.4.0 90 | [v0.3.2]: https://github.com/rust-embedded-community/menu/releases/tag/v0.3.2 91 | [v0.3.1]: https://github.com/rust-embedded-community/menu/releases/tag/v0.3.1 92 | [v0.3.0]: https://github.com/rust-embedded-community/menu/releases/tag/v0.3.0 93 | [v0.2.1]: https://github.com/rust-embedded-community/menu/releases/tag/v0.2.1 94 | [v0.2.0]: https://github.com/rust-embedded-community/menu/releases/tag/v0.2.0 95 | [v0.1.1]: https://github.com/rust-embedded-community/menu/releases/tag/v0.1.1 96 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "menu" 3 | version = "0.6.1" 4 | authors = ["Jonathan 'theJPster' Pallant "] 5 | description = "A simple #[no_std] command line interface." 6 | license = "MIT OR Apache-2.0" 7 | edition = "2021" 8 | repository = "https://github.com/rust-embedded-community/menu" 9 | readme = "README.md" 10 | 11 | [dependencies] 12 | embedded-io = "0.6.1" 13 | noline = { version = "0.5.0", optional = true } 14 | rustversion = "1.0.17" 15 | 16 | [features] 17 | default = ["echo"] 18 | echo = [] 19 | 20 | [dev-dependencies] 21 | noline = { version = "0.5.0", features = ["std"] } 22 | pancurses = "0.16" 23 | termion = "4.0.2" 24 | menu = {path = ".", features = ["noline"]} 25 | -------------------------------------------------------------------------------- /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. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018-2019 Jonathan 'theJPster' Pallant 2 | Copyright (c) 2019-2024 Rust Embedded Community developers 3 | 4 | Permission is hereby granted, free of charge, to any 5 | person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the 7 | Software without restriction, including without 8 | limitation the rights to use, copy, modify, merge, 9 | publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software 11 | is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice 15 | shall be included in all copies or substantial portions 16 | of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 19 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 20 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 21 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 22 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 23 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 24 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 25 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 26 | DEALINGS IN THE SOFTWARE. 27 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Apache 2.0 NOTICE file for https://github.com/rust-embedded-community/menu 2 | 3 | Copyright (c) 2018-2019 Jonathan 'theJPster' Pallant 4 | Copyright (c) 2019-2024 Rust Embedded Community developers 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Menu 2 | 3 | ## Introduction 4 | 5 | A simple command-line menu system in Rust. Works on embedded systems, but also 6 | on your command-line. 7 | 8 | **NOTE:** This crates works only in `&str` - there's no heap allocation, but 9 | there's also no automatic conversion to integers, boolean, etc. 10 | 11 | ```console 12 | user@host: ~/menu $ cargo run --example simple 13 | Compiling menu v0.5.0 (file:///home/user/menu) 14 | Finished dev [unoptimized + debuginfo] target(s) in 0.84 secs 15 | Running `target/debug/examples/simple` 16 | In enter_root() 17 | > help 18 | AVAILABLE ITEMS: 19 | foo [ ] [ OPTIONS... ] 20 | bar 21 | sub 22 | help [ ] 23 | 24 | 25 | > help foo 26 | SUMMARY: 27 | foo [ ] [ --verbose ] [ --level=INT ] 28 | 29 | PARAMETERS: 30 | 31 | This is the help text for 'a' 32 | 33 | No help text found 34 | --verbose 35 | No help text found 36 | --level=INT 37 | Set the level of the dangle 38 | 39 | 40 | DESCRIPTION: 41 | Makes a foo appear. 42 | 43 | This is some extensive help text. 44 | 45 | It contains multiple paragraphs and should be preceeded by the parameter list. 46 | 47 | > foo --level=3 --verbose 1 2 48 | In select_foo. Args = ["--level=3", "--verbose", "1", "2"] 49 | a = Ok(Some("1")) 50 | b = Ok(Some("2")) 51 | verbose = Ok(Some("")) 52 | level = Ok(Some("3")) 53 | no_such_arg = Err(()) 54 | 55 | 56 | > foo 57 | Error: Insufficient arguments given! 58 | 59 | > foo 1 2 3 3 60 | Error: Too many arguments given 61 | 62 | > sub 63 | 64 | /sub> help 65 | AVAILABLE ITEMS: 66 | baz 67 | quux 68 | exit 69 | help [ ] 70 | 71 | > exit 72 | 73 | > help 74 | AVAILABLE ITEMS: 75 | foo [ ] [ OPTIONS... ] 76 | bar 77 | sub 78 | help [ ] 79 | 80 | 81 | > ^C 82 | user@host: ~/menu $ 83 | ``` 84 | 85 | ## Using 86 | 87 | See `examples/simple.rs` for a working example that runs on Linux or Windows. Here's the menu definition from that example: 88 | 89 | ```rust 90 | const ROOT_MENU: Menu = Menu { 91 | label: "root", 92 | items: &[ 93 | &Item { 94 | item_type: ItemType::Callback { 95 | function: select_foo, 96 | parameters: &[ 97 | Parameter::Mandatory { 98 | parameter_name: "a", 99 | help: Some("This is the help text for 'a'"), 100 | }, 101 | Parameter::Optional { 102 | parameter_name: "b", 103 | help: None, 104 | }, 105 | Parameter::Named { 106 | parameter_name: "verbose", 107 | help: None, 108 | }, 109 | Parameter::NamedValue { 110 | parameter_name: "level", 111 | argument_name: "INT", 112 | help: Some("Set the level of the dangle"), 113 | }, 114 | ], 115 | }, 116 | command: "foo", 117 | help: Some( 118 | "Makes a foo appear. 119 | 120 | This is some extensive help text. 121 | 122 | It contains multiple paragraphs and should be preceeded by the parameter list. 123 | ", 124 | ), 125 | }, 126 | &Item { 127 | item_type: ItemType::Callback { 128 | function: select_bar, 129 | parameters: &[], 130 | }, 131 | command: "bar", 132 | help: Some("fandoggles a bar"), 133 | }, 134 | &Item { 135 | item_type: ItemType::Menu(&Menu { 136 | label: "sub", 137 | items: &[ 138 | &Item { 139 | item_type: ItemType::Callback { 140 | function: select_baz, 141 | parameters: &[], 142 | }, 143 | command: "baz", 144 | help: Some("thingamobob a baz"), 145 | }, 146 | &Item { 147 | item_type: ItemType::Callback { 148 | function: select_quux, 149 | parameters: &[], 150 | }, 151 | command: "quux", 152 | help: Some("maximum quux"), 153 | }, 154 | ], 155 | entry: Some(enter_sub), 156 | exit: Some(exit_sub), 157 | }), 158 | command: "sub", 159 | help: Some("enter sub-menu"), 160 | }, 161 | ], 162 | entry: Some(enter_root), 163 | exit: Some(exit_root), 164 | }; 165 | 166 | ``` 167 | 168 | ## Using with `noline` 169 | 170 | The [`noline`](https://crates.io/crates/noline) crate is a no-std line-editor 171 | with history buffer. You can create a `Runner` using a `noline::Editor` instead 172 | of a raw byte slice, and then you will get a `pub fn input_line(&mut self, 173 | context: &mut T) -> Result<(), NolineError>` instead of the `pub fn 174 | input_byte(&mut self, input: u8, context: &mut T)` you get when you pass a 175 | mutable byte slice and let `menu` do the input handling. Call `input_line` and 176 | it will use the `noline::Editor` to read a line of text from the user, with 177 | history (press 'Up'!) and other nice features. 178 | 179 | We chose this option as `noline` offers a bunch of benefits, but we didn't want 180 | to break everything for people who were quite happy with the basic line editing 181 | we have already. 182 | 183 | See [`examples/noline.rs`](./examples/noline.rs) for an example. 184 | 185 | ## Changelog 186 | 187 | See [`CHANGELOG.md`](./CHANGELOG.md). 188 | 189 | ## License 190 | 191 | The contents of this repository are dual-licensed under the _MIT OR Apache 2.0_ 192 | License. That means you can choose either the MIT license or the Apache 2.0 193 | license when you re-use this code. See [`LICENSE-MIT`](./LICENSE-MIT) or 194 | [`LICENSE-APACHE`](./LICENSE-APACHE) for more information on each specific 195 | license. Our Apache 2.0 notices can be found in [`NOTICE`](./NOTICE). 196 | 197 | Unless you explicitly state otherwise, any contribution intentionally submitted 198 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 199 | dual licensed as above, without any additional terms or conditions. 200 | -------------------------------------------------------------------------------- /examples/noline.rs: -------------------------------------------------------------------------------- 1 | extern crate menu; 2 | 3 | use embedded_io::{ErrorType, Read as EmbRead, Write as EmbWrite}; 4 | use menu::*; 5 | use noline::builder::EditorBuilder; 6 | use std::io::{self, Read as _, Stdin, Stdout, Write as _}; 7 | use termion::raw::IntoRawMode; 8 | 9 | pub struct IOWrapper { 10 | stdin: Stdin, 11 | stdout: Stdout, 12 | } 13 | 14 | impl IOWrapper { 15 | pub fn new() -> Self { 16 | Self { 17 | stdin: std::io::stdin(), 18 | stdout: std::io::stdout(), 19 | } 20 | } 21 | } 22 | 23 | impl Default for IOWrapper { 24 | fn default() -> Self { 25 | Self::new() 26 | } 27 | } 28 | 29 | impl ErrorType for IOWrapper { 30 | type Error = embedded_io::ErrorKind; 31 | } 32 | 33 | impl EmbRead for IOWrapper { 34 | fn read(&mut self, buf: &mut [u8]) -> Result { 35 | Ok(self.stdin.read(buf).map_err(|e| e.kind())?) 36 | } 37 | } 38 | 39 | impl EmbWrite for IOWrapper { 40 | fn write(&mut self, buf: &[u8]) -> Result { 41 | let mut written = 0; 42 | let parts = buf.split(|b| *b == b'\n').collect::>(); 43 | 44 | for (i, part) in parts.iter().enumerate() { 45 | written += self.stdout.write(part).map_err(|e| e.kind())?; 46 | 47 | if i != parts.len() - 1 { 48 | let _ = self.stdout.write(b"\r\n").map_err(|e| e.kind())?; 49 | written += 1; 50 | } 51 | } 52 | 53 | Ok(written) 54 | } 55 | 56 | fn flush(&mut self) -> Result<(), Self::Error> { 57 | Ok(self.stdout.flush().map_err(|e| e.kind())?) 58 | } 59 | } 60 | 61 | #[derive(Default)] 62 | struct Context { 63 | _inner: u32, 64 | } 65 | 66 | const ROOT_MENU: Menu = Menu { 67 | label: "root", 68 | items: &[ 69 | &Item { 70 | item_type: ItemType::Callback { 71 | function: select_foo, 72 | parameters: &[ 73 | Parameter::Mandatory { 74 | parameter_name: "a", 75 | help: Some("This is the help text for 'a'"), 76 | }, 77 | Parameter::Optional { 78 | parameter_name: "b", 79 | help: None, 80 | }, 81 | Parameter::Named { 82 | parameter_name: "verbose", 83 | help: None, 84 | }, 85 | Parameter::NamedValue { 86 | parameter_name: "level", 87 | argument_name: "INT", 88 | help: Some("Set the level of the dangle"), 89 | }, 90 | ], 91 | }, 92 | command: "foo", 93 | help: Some( 94 | "Makes a foo appear. 95 | 96 | This is some extensive help text. 97 | 98 | It contains multiple paragraphs and should be preceeded by the parameter list. 99 | ", 100 | ), 101 | }, 102 | &Item { 103 | item_type: ItemType::Callback { 104 | function: select_bar, 105 | parameters: &[], 106 | }, 107 | command: "bar", 108 | help: Some("fandoggles a bar"), 109 | }, 110 | &Item { 111 | item_type: ItemType::Menu(&Menu { 112 | label: "sub", 113 | items: &[ 114 | &Item { 115 | item_type: ItemType::Callback { 116 | function: select_baz, 117 | parameters: &[], 118 | }, 119 | command: "baz", 120 | help: Some("thingamobob a baz"), 121 | }, 122 | &Item { 123 | item_type: ItemType::Callback { 124 | function: select_quux, 125 | parameters: &[], 126 | }, 127 | command: "quux", 128 | help: Some("maximum quux"), 129 | }, 130 | ], 131 | entry: Some(enter_sub), 132 | exit: Some(exit_sub), 133 | }), 134 | command: "sub", 135 | help: Some("enter sub-menu"), 136 | }, 137 | ], 138 | entry: Some(enter_root), 139 | exit: Some(exit_root), 140 | }; 141 | 142 | fn main() { 143 | let _stdout = io::stdout().into_raw_mode().unwrap(); 144 | 145 | let mut io = IOWrapper::new(); 146 | let mut editor = EditorBuilder::new_unbounded() 147 | .with_unbounded_history() 148 | .build_sync(&mut io) 149 | .unwrap(); 150 | 151 | let mut context = Context::default(); 152 | let mut r = Runner::new(ROOT_MENU, &mut editor, io, &mut context); 153 | 154 | while let Ok(_) = r.input_line(&mut context) {} 155 | } 156 | 157 | fn enter_root(_menu: &Menu, interface: &mut IOWrapper, _context: &mut Context) { 158 | writeln!(interface, "In enter_root").unwrap(); 159 | } 160 | 161 | fn exit_root(_menu: &Menu, interface: &mut IOWrapper, _context: &mut Context) { 162 | writeln!(interface, "In exit_root").unwrap(); 163 | } 164 | 165 | fn select_foo( 166 | _menu: &Menu, 167 | item: &Item, 168 | args: &[&str], 169 | interface: &mut IOWrapper, 170 | _context: &mut Context, 171 | ) { 172 | writeln!(interface, "In select_foo. Args = {:?}", args).unwrap(); 173 | writeln!( 174 | interface, 175 | "a = {:?}", 176 | ::menu::argument_finder(item, args, "a") 177 | ) 178 | .unwrap(); 179 | writeln!( 180 | interface, 181 | "b = {:?}", 182 | ::menu::argument_finder(item, args, "b") 183 | ) 184 | .unwrap(); 185 | writeln!( 186 | interface, 187 | "verbose = {:?}", 188 | ::menu::argument_finder(item, args, "verbose") 189 | ) 190 | .unwrap(); 191 | writeln!( 192 | interface, 193 | "level = {:?}", 194 | ::menu::argument_finder(item, args, "level") 195 | ) 196 | .unwrap(); 197 | writeln!( 198 | interface, 199 | "no_such_arg = {:?}", 200 | ::menu::argument_finder(item, args, "no_such_arg") 201 | ) 202 | .unwrap(); 203 | } 204 | 205 | fn select_bar( 206 | _menu: &Menu, 207 | _item: &Item, 208 | args: &[&str], 209 | interface: &mut IOWrapper, 210 | _context: &mut Context, 211 | ) { 212 | writeln!(interface, "In select_bar. Args = {:?}", args).unwrap(); 213 | } 214 | 215 | fn enter_sub(_menu: &Menu, interface: &mut IOWrapper, _context: &mut Context) { 216 | writeln!(interface, "In enter_sub").unwrap(); 217 | } 218 | 219 | fn exit_sub(_menu: &Menu, interface: &mut IOWrapper, _context: &mut Context) { 220 | writeln!(interface, "In exit_sub").unwrap(); 221 | } 222 | 223 | fn select_baz( 224 | _menu: &Menu, 225 | _item: &Item, 226 | args: &[&str], 227 | interface: &mut IOWrapper, 228 | _context: &mut Context, 229 | ) { 230 | writeln!(interface, "In select_baz: Args = {:?}", args).unwrap(); 231 | } 232 | 233 | fn select_quux( 234 | _menu: &Menu, 235 | _item: &Item, 236 | args: &[&str], 237 | interface: &mut IOWrapper, 238 | _context: &mut Context, 239 | ) { 240 | writeln!(interface, "In select_quux: Args = {:?}", args).unwrap(); 241 | } 242 | -------------------------------------------------------------------------------- /examples/simple.rs: -------------------------------------------------------------------------------- 1 | extern crate menu; 2 | 3 | use embedded_io::Write; 4 | use menu::*; 5 | use pancurses::{endwin, initscr, noecho, Input}; 6 | use std::convert::Infallible; 7 | 8 | #[derive(Default)] 9 | struct Context { 10 | _inner: u32, 11 | } 12 | 13 | const ROOT_MENU: Menu = Menu { 14 | label: "root", 15 | items: &[ 16 | &Item { 17 | item_type: ItemType::Callback { 18 | function: select_foo, 19 | parameters: &[ 20 | Parameter::Mandatory { 21 | parameter_name: "a", 22 | help: Some("This is the help text for 'a'"), 23 | }, 24 | Parameter::Optional { 25 | parameter_name: "b", 26 | help: None, 27 | }, 28 | Parameter::Named { 29 | parameter_name: "verbose", 30 | help: None, 31 | }, 32 | Parameter::NamedValue { 33 | parameter_name: "level", 34 | argument_name: "INT", 35 | help: Some("Set the level of the dangle"), 36 | }, 37 | ], 38 | }, 39 | command: "foo", 40 | help: Some( 41 | "Makes a foo appear. 42 | 43 | This is some extensive help text. 44 | 45 | It contains multiple paragraphs and should be preceeded by the parameter list. 46 | ", 47 | ), 48 | }, 49 | &Item { 50 | item_type: ItemType::Callback { 51 | function: select_bar, 52 | parameters: &[], 53 | }, 54 | command: "bar", 55 | help: Some("fandoggles a bar"), 56 | }, 57 | &Item { 58 | item_type: ItemType::Menu(&Menu { 59 | label: "sub", 60 | items: &[ 61 | &Item { 62 | item_type: ItemType::Callback { 63 | function: select_baz, 64 | parameters: &[], 65 | }, 66 | command: "baz", 67 | help: Some("thingamobob a baz"), 68 | }, 69 | &Item { 70 | item_type: ItemType::Callback { 71 | function: select_quux, 72 | parameters: &[], 73 | }, 74 | command: "quux", 75 | help: Some("maximum quux"), 76 | }, 77 | ], 78 | entry: Some(enter_sub), 79 | exit: Some(exit_sub), 80 | }), 81 | command: "sub", 82 | help: Some("enter sub-menu"), 83 | }, 84 | ], 85 | entry: Some(enter_root), 86 | exit: Some(exit_root), 87 | }; 88 | 89 | struct Output(pancurses::Window); 90 | 91 | impl embedded_io::ErrorType for Output { 92 | type Error = Infallible; 93 | } 94 | 95 | impl Write for Output { 96 | fn write(&mut self, buf: &[u8]) -> Result { 97 | self.0.printw(core::str::from_utf8(buf).unwrap()); 98 | Ok(buf.len()) 99 | } 100 | 101 | fn flush(&mut self) -> Result<(), Self::Error> { 102 | Ok(()) 103 | } 104 | } 105 | 106 | fn main() { 107 | let window = initscr(); 108 | window.scrollok(true); 109 | noecho(); 110 | let mut buffer = [0u8; 64]; 111 | let mut context = Context::default(); 112 | let mut r = Runner::new(ROOT_MENU, &mut buffer, Output(window), &mut context); 113 | loop { 114 | match r.interface.0.getch() { 115 | Some(Input::Character('\n')) => { 116 | r.input_byte(b'\r', &mut context); 117 | } 118 | Some(Input::Character(c)) => { 119 | let mut buf = [0; 4]; 120 | for b in c.encode_utf8(&mut buf).bytes() { 121 | r.input_byte(b, &mut context); 122 | } 123 | } 124 | Some(Input::KeyDC) => break, 125 | Some(input) => { 126 | r.interface.0.addstr(&format!("{:?}", input)); 127 | } 128 | None => (), 129 | } 130 | } 131 | endwin(); 132 | } 133 | 134 | fn enter_root(_menu: &Menu, interface: &mut Output, _context: &mut Context) { 135 | writeln!(interface, "In enter_root").unwrap(); 136 | } 137 | 138 | fn exit_root(_menu: &Menu, interface: &mut Output, _context: &mut Context) { 139 | writeln!(interface, "In exit_root").unwrap(); 140 | } 141 | 142 | fn select_foo( 143 | _menu: &Menu, 144 | item: &Item, 145 | args: &[&str], 146 | interface: &mut Output, 147 | _context: &mut Context, 148 | ) { 149 | writeln!(interface, "In select_foo. Args = {:?}", args).unwrap(); 150 | writeln!( 151 | interface, 152 | "a = {:?}", 153 | ::menu::argument_finder(item, args, "a") 154 | ) 155 | .unwrap(); 156 | writeln!( 157 | interface, 158 | "b = {:?}", 159 | ::menu::argument_finder(item, args, "b") 160 | ) 161 | .unwrap(); 162 | writeln!( 163 | interface, 164 | "verbose = {:?}", 165 | ::menu::argument_finder(item, args, "verbose") 166 | ) 167 | .unwrap(); 168 | writeln!( 169 | interface, 170 | "level = {:?}", 171 | ::menu::argument_finder(item, args, "level") 172 | ) 173 | .unwrap(); 174 | writeln!( 175 | interface, 176 | "no_such_arg = {:?}", 177 | ::menu::argument_finder(item, args, "no_such_arg") 178 | ) 179 | .unwrap(); 180 | } 181 | 182 | fn select_bar( 183 | _menu: &Menu, 184 | _item: &Item, 185 | args: &[&str], 186 | interface: &mut Output, 187 | _context: &mut Context, 188 | ) { 189 | writeln!(interface, "In select_bar. Args = {:?}", args).unwrap(); 190 | } 191 | 192 | fn enter_sub(_menu: &Menu, interface: &mut Output, _context: &mut Context) { 193 | writeln!(interface, "In enter_sub").unwrap(); 194 | } 195 | 196 | fn exit_sub(_menu: &Menu, interface: &mut Output, _context: &mut Context) { 197 | writeln!(interface, "In exit_sub").unwrap(); 198 | } 199 | 200 | fn select_baz( 201 | _menu: &Menu, 202 | _item: &Item, 203 | args: &[&str], 204 | interface: &mut Output, 205 | _context: &mut Context, 206 | ) { 207 | writeln!(interface, "In select_baz: Args = {:?}", args).unwrap(); 208 | } 209 | 210 | fn select_quux( 211 | _menu: &Menu, 212 | _item: &Item, 213 | args: &[&str], 214 | interface: &mut Output, 215 | _context: &mut Context, 216 | ) { 217 | writeln!(interface, "In select_quux: Args = {:?}", args).unwrap(); 218 | } 219 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Menu 2 | //! 3 | //! A basic command-line interface for `#![no_std]` Rust programs. Peforms 4 | //! zero heap allocation. 5 | #![no_std] 6 | 7 | use menu_manager::MenuManager; 8 | 9 | #[cfg(feature = "noline")] 10 | use noline::{error::NolineError, history::History, line_buffer::Buffer, sync_editor::Editor}; 11 | 12 | pub mod menu_manager; 13 | 14 | /// The type of function we call when we enter/exit a menu. 15 | pub type MenuCallbackFn = fn(menu: &Menu, interface: &mut I, context: &mut T); 16 | 17 | /// The type of function we call when we a valid command has been entered. 18 | pub type ItemCallbackFn = 19 | fn(menu: &Menu, item: &Item, args: &[&str], interface: &mut I, context: &mut T); 20 | 21 | #[derive(Debug)] 22 | /// Describes a parameter to the command 23 | pub enum Parameter<'a> { 24 | /// A mandatory positional parameter 25 | Mandatory { 26 | /// A name for this mandatory positional parameter 27 | parameter_name: &'a str, 28 | /// Help text 29 | help: Option<&'a str>, 30 | }, 31 | /// An optional positional parameter. Must come after the mandatory positional arguments. 32 | Optional { 33 | /// A name for this optional positional parameter 34 | parameter_name: &'a str, 35 | /// Help text 36 | help: Option<&'a str>, 37 | }, 38 | /// An optional named parameter with no argument (e.g. `--verbose` or `--dry-run`) 39 | Named { 40 | /// The bit that comes after the `--` 41 | parameter_name: &'a str, 42 | /// Help text 43 | help: Option<&'a str>, 44 | }, 45 | /// A optional named parameter with argument (e.g. `--mode=foo` or `--level=3`) 46 | NamedValue { 47 | /// The bit that comes after the `--` 48 | parameter_name: &'a str, 49 | /// The bit that comes after the `--name=`, e.g. `INT` or `FILE`. It's mostly for help text. 50 | argument_name: &'a str, 51 | /// Help text 52 | help: Option<&'a str>, 53 | }, 54 | } 55 | 56 | /// Do we enter a sub-menu when this command is entered, or call a specific 57 | /// function? 58 | pub enum ItemType<'a, I, T> 59 | where 60 | T: 'a, 61 | { 62 | /// Call a function when this command is entered 63 | Callback { 64 | /// The function to call 65 | function: ItemCallbackFn, 66 | /// The list of parameters for this function. Pass an empty list if there aren't any. 67 | parameters: &'a [Parameter<'a>], 68 | }, 69 | /// This item is a sub-menu you can enter 70 | Menu(&'a Menu<'a, I, T>), 71 | /// Internal use only - do not use 72 | _Dummy, 73 | } 74 | 75 | /// An `Item` is a what our menus are made from. Each item has a `name` which 76 | /// you have to enter to select this item. Each item can also have zero or 77 | /// more parameters, and some optional help text. 78 | pub struct Item<'a, I, T> 79 | where 80 | T: 'a, 81 | { 82 | /// The word you need to enter to activate this item. It is recommended 83 | /// that you avoid whitespace in this string. 84 | pub command: &'a str, 85 | /// Optional help text. Printed if you enter `help`. 86 | pub help: Option<&'a str>, 87 | /// The type of this item - menu, callback, etc. 88 | pub item_type: ItemType<'a, I, T>, 89 | } 90 | 91 | /// A `Menu` is made of one or more `Item`s. 92 | pub struct Menu<'a, I, T> 93 | where 94 | T: 'a, 95 | { 96 | /// Each menu has a label which is visible in the prompt, unless you are 97 | /// the root menu. 98 | pub label: &'a str, 99 | /// A slice of menu items in this menu. 100 | pub items: &'a [&'a Item<'a, I, T>], 101 | /// A function to call when this menu is entered. If this is the root menu, this is called when the runner is created. 102 | pub entry: Option>, 103 | /// A function to call when this menu is exited. Never called for the root menu. 104 | pub exit: Option>, 105 | } 106 | 107 | /// This structure handles the menu. You feed it bytes as they are read from 108 | /// the console and it executes menu actions when commands are typed in 109 | /// (followed by Enter). 110 | pub struct Runner<'a, I, T, B: ?Sized> { 111 | buffer: &'a mut B, 112 | used: usize, 113 | pub interface: I, 114 | inner: InnerRunner<'a, I, T>, 115 | } 116 | 117 | struct InnerRunner<'a, I, T> { 118 | menu_mgr: menu_manager::MenuManager<'a, I, T>, 119 | } 120 | 121 | /// Describes the ways in which the API can fail 122 | #[derive(Debug, Clone, PartialEq, Eq)] 123 | pub enum Error { 124 | /// Tried to find arguments on an item that was a `Callback` item 125 | NotACallbackItem, 126 | /// The argument you asked for was not found 127 | NotFound, 128 | } 129 | 130 | impl core::fmt::Display for Error { 131 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 132 | core::fmt::Debug::fmt(self, f) 133 | } 134 | } 135 | 136 | #[rustversion::since(1.81)] 137 | impl core::error::Error for Error {} 138 | 139 | /// Looks for the named parameter in the parameter list of the item, then 140 | /// finds the correct argument. 141 | /// 142 | /// * Returns `Ok(None)` if `parameter_name` gives an optional or named 143 | /// parameter and that argument was not given. 144 | /// * Returns `Ok(arg)` if the argument corresponding to `parameter_name` was 145 | /// found. `arg` is the empty string if the parameter was `Parameter::Named` 146 | /// (and hence doesn't take a value). 147 | /// * Returns `Err(())` if `parameter_name` was not in `item.parameter_list` 148 | /// or `item` wasn't an Item::Callback 149 | pub fn argument_finder<'a, I, T>( 150 | item: &'a Item<'a, I, T>, 151 | argument_list: &'a [&'a str], 152 | name_to_find: &'a str, 153 | ) -> Result, Error> { 154 | let ItemType::Callback { parameters, .. } = item.item_type else { 155 | return Err(Error::NotACallbackItem); 156 | }; 157 | // Step 1 - Find `name_to_find` in the parameter list. 158 | let mut found_param = None; 159 | let mut mandatory_count = 0; 160 | let mut optional_count = 0; 161 | for param in parameters.iter() { 162 | match param { 163 | Parameter::Mandatory { parameter_name, .. } => { 164 | mandatory_count += 1; 165 | if *parameter_name == name_to_find { 166 | found_param = Some((param, mandatory_count)); 167 | } 168 | } 169 | Parameter::Optional { parameter_name, .. } => { 170 | optional_count += 1; 171 | if *parameter_name == name_to_find { 172 | found_param = Some((param, optional_count)); 173 | } 174 | } 175 | Parameter::Named { parameter_name, .. } => { 176 | if *parameter_name == name_to_find { 177 | found_param = Some((param, 0)); 178 | } 179 | } 180 | Parameter::NamedValue { parameter_name, .. } => { 181 | if *parameter_name == name_to_find { 182 | found_param = Some((param, 0)); 183 | } 184 | } 185 | } 186 | } 187 | // Step 2 - What sort of parameter is it? 188 | match found_param { 189 | // Step 2a - Mandatory Positional 190 | Some((Parameter::Mandatory { .. }, mandatory_idx)) => { 191 | // We want positional parameter number `mandatory_idx`. 192 | let mut positional_args_seen = 0; 193 | for arg in argument_list.iter().filter(|x| !x.starts_with("--")) { 194 | // Positional 195 | positional_args_seen += 1; 196 | if positional_args_seen == mandatory_idx { 197 | return Ok(Some(arg)); 198 | } 199 | } 200 | // Valid thing to ask for but we don't have it 201 | Ok(None) 202 | } 203 | // Step 2b - Optional Positional 204 | Some((Parameter::Optional { .. }, optional_idx)) => { 205 | // We want positional parameter number `mandatory_count + optional_idx`. 206 | let mut positional_args_seen = 0; 207 | for arg in argument_list.iter().filter(|x| !x.starts_with("--")) { 208 | // Positional 209 | positional_args_seen += 1; 210 | if positional_args_seen == (mandatory_count + optional_idx) { 211 | return Ok(Some(arg)); 212 | } 213 | } 214 | // Valid thing to ask for but we don't have it 215 | Ok(None) 216 | } 217 | // Step 2c - Named (e.g. `--verbose`) 218 | Some((Parameter::Named { parameter_name, .. }, _)) => { 219 | for arg in argument_list { 220 | if arg.starts_with("--") && (&arg[2..] == *parameter_name) { 221 | return Ok(Some("")); 222 | } 223 | } 224 | // Valid thing to ask for but we don't have it 225 | Ok(None) 226 | } 227 | // Step 2d - NamedValue (e.g. `--level=123`) 228 | Some((Parameter::NamedValue { parameter_name, .. }, _)) => { 229 | let name_start = 2; 230 | let equals_start = name_start + parameter_name.len(); 231 | let value_start = equals_start + 1; 232 | for arg in argument_list { 233 | if arg.starts_with("--") 234 | && (arg.len() >= value_start) 235 | && (arg.get(equals_start..=equals_start) == Some("=")) 236 | && (arg.get(name_start..equals_start) == Some(*parameter_name)) 237 | { 238 | return Ok(Some(&arg[value_start..])); 239 | } 240 | } 241 | // Valid thing to ask for but we don't have it 242 | Ok(None) 243 | } 244 | // Step 2e - not found 245 | _ => Err(Error::NotFound), 246 | } 247 | } 248 | 249 | enum Outcome { 250 | CommandProcessed, 251 | NeedMore, 252 | } 253 | 254 | impl<'a, I, T> core::clone::Clone for Menu<'a, I, T> { 255 | fn clone(&self) -> Menu<'a, I, T> { 256 | Menu { 257 | label: self.label, 258 | items: self.items, 259 | entry: self.entry, 260 | exit: self.exit, 261 | } 262 | } 263 | } 264 | 265 | #[derive(Clone)] 266 | enum PromptIterState { 267 | Newline, 268 | Menu(usize), 269 | Arrow, 270 | Done, 271 | } 272 | 273 | struct PromptIter<'a, I, T> { 274 | menu_mgr: &'a MenuManager<'a, I, T>, 275 | state: PromptIterState, 276 | } 277 | 278 | impl Clone for PromptIter<'_, I, T> { 279 | fn clone(&self) -> Self { 280 | Self { 281 | menu_mgr: self.menu_mgr, 282 | state: self.state.clone(), 283 | } 284 | } 285 | } 286 | 287 | impl<'a, I, T> PromptIter<'a, I, T> { 288 | fn new(menu_mgr: &'a MenuManager<'a, I, T>, newline: bool) -> Self { 289 | let state = if newline { 290 | PromptIterState::Newline 291 | } else { 292 | Self::first_menu() 293 | }; 294 | Self { menu_mgr, state } 295 | } 296 | 297 | const fn first_menu() -> PromptIterState { 298 | // Skip the first menu level which is root 299 | PromptIterState::Menu(1) 300 | } 301 | } 302 | 303 | impl<'a, I, T> Iterator for PromptIter<'a, I, T> { 304 | type Item = &'a str; 305 | 306 | fn next(&mut self) -> Option { 307 | loop { 308 | match self.state { 309 | PromptIterState::Newline => { 310 | self.state = Self::first_menu(); 311 | break Some("\n"); 312 | } 313 | PromptIterState::Menu(i) => { 314 | if i > self.menu_mgr.depth() { 315 | self.state = PromptIterState::Arrow; 316 | } else { 317 | let menu = self.menu_mgr.get_menu(Some(i)); 318 | self.state = PromptIterState::Menu(i + 1); 319 | break Some(menu.label); 320 | } 321 | } 322 | PromptIterState::Arrow => { 323 | self.state = PromptIterState::Done; 324 | break Some("> "); 325 | } 326 | PromptIterState::Done => break None, 327 | } 328 | } 329 | } 330 | } 331 | 332 | impl<'a, I, T, B: ?Sized> Runner<'a, I, T, B> 333 | where 334 | I: embedded_io::Write, 335 | { 336 | /// Create a new `Runner`. You need to supply a top-level menu, and a 337 | /// buffer that the `Runner` can use. Feel free to pass anything as the 338 | /// `context` type - the only requirement is that the `Runner` can 339 | /// `write!` to the context, which it will do for all text output. 340 | pub fn new(menu: Menu<'a, I, T>, buffer: &'a mut B, mut interface: I, context: &mut T) -> Self { 341 | if let Some(cb_fn) = menu.entry { 342 | cb_fn(&menu, &mut interface, context); 343 | } 344 | let mut r = Runner { 345 | buffer, 346 | used: 0, 347 | interface, 348 | inner: InnerRunner { 349 | menu_mgr: menu_manager::MenuManager::new(menu), 350 | }, 351 | }; 352 | r.inner.prompt(&mut r.interface, true); 353 | r 354 | } 355 | } 356 | 357 | #[cfg(feature = "noline")] 358 | impl<'a, I, T, B, H> Runner<'a, I, T, Editor> 359 | where 360 | B: Buffer, 361 | H: History, 362 | I: embedded_io::Read + embedded_io::Write, 363 | { 364 | pub fn input_line(&mut self, context: &mut T) -> Result<(), NolineError> { 365 | let prompt = PromptIter::new(&self.inner.menu_mgr, false); 366 | 367 | let line = self.buffer.readline(prompt, &mut self.interface)?; 368 | 369 | #[cfg(not(feature = "echo"))] 370 | { 371 | // Echo the command 372 | write!(self.interface, "\r").unwrap(); 373 | write!(self.interface, "{}", line).unwrap(); 374 | } 375 | 376 | self.inner 377 | .process_command(&mut self.interface, context, line); 378 | 379 | Ok(()) 380 | } 381 | } 382 | 383 | impl Runner<'_, I, T, B> 384 | where 385 | I: embedded_io::Write, 386 | B: AsMut<[u8]> + ?Sized, 387 | { 388 | /// Add a byte to the menu runner's buffer. If this byte is a 389 | /// carriage-return, the buffer is scanned and the appropriate action 390 | /// performed. 391 | /// By default, an echo feature is enabled to display commands on the terminal. 392 | pub fn input_byte(&mut self, input: u8, context: &mut T) { 393 | // Strip carriage returns 394 | if input == 0x0A { 395 | return; 396 | } 397 | let buffer = self.buffer.as_mut(); 398 | 399 | let outcome = if input == 0x0D { 400 | if let Ok(line) = core::str::from_utf8(&buffer[0..self.used]) { 401 | #[cfg(not(feature = "echo"))] 402 | { 403 | // Echo the command 404 | write!(self.interface, "\r").unwrap(); 405 | write!(self.interface, "{}", line).unwrap(); 406 | } 407 | // Handle the command 408 | self.inner 409 | .process_command(&mut self.interface, context, line); 410 | } else { 411 | // Hmm .. we did not have a valid string 412 | writeln!(self.interface, "Input was not valid UTF-8").unwrap(); 413 | } 414 | 415 | Outcome::CommandProcessed 416 | } else if (input == 0x08) || (input == 0x7F) { 417 | // Handling backspace or delete 418 | if self.used > 0 { 419 | write!(self.interface, "\u{0008} \u{0008}").unwrap(); 420 | self.used -= 1; 421 | } 422 | Outcome::NeedMore 423 | } else if self.used < buffer.len() { 424 | buffer[self.used] = input; 425 | self.used += 1; 426 | 427 | #[cfg(feature = "echo")] 428 | { 429 | // We have to do this song and dance because `self.prompt()` needs 430 | // a mutable reference to self, and we can't have that while 431 | // holding a reference to the buffer at the same time. 432 | // This line grabs the buffer, checks it's OK, then releases it again 433 | let valid = core::str::from_utf8(&buffer[0..self.used]).is_ok(); 434 | // Now we've released the buffer, we can draw the prompt 435 | if valid { 436 | write!(self.interface, "\r").unwrap(); 437 | self.inner.prompt(&mut self.interface, false); 438 | } 439 | // Grab the buffer again to render it to the screen 440 | if let Ok(s) = core::str::from_utf8(&buffer[0..self.used]) { 441 | write!(self.interface, "{}", s).unwrap(); 442 | } 443 | } 444 | Outcome::NeedMore 445 | } else { 446 | writeln!(self.interface, "Buffer overflow!").unwrap(); 447 | Outcome::NeedMore 448 | }; 449 | match outcome { 450 | Outcome::CommandProcessed => { 451 | self.used = 0; 452 | self.inner.prompt(&mut self.interface, true); 453 | } 454 | Outcome::NeedMore => {} 455 | } 456 | } 457 | } 458 | 459 | impl InnerRunner<'_, I, T> 460 | where 461 | I: embedded_io::Write, 462 | { 463 | /// Print out a new command prompt, including sub-menu names if 464 | /// applicable. 465 | pub fn prompt(&mut self, interface: &mut I, newline: bool) { 466 | let prompt = PromptIter::new(&self.menu_mgr, newline); 467 | 468 | for part in prompt { 469 | write!(interface, "{}", part).unwrap(); 470 | } 471 | } 472 | 473 | /// Scan the buffer and do the right thing based on its contents. 474 | fn process_command(&mut self, interface: &mut I, context: &mut T, command_line: &str) { 475 | // Go to the next line, below the prompt 476 | writeln!(interface).unwrap(); 477 | // We have a valid string 478 | let mut parts = command_line.split_whitespace(); 479 | if let Some(cmd) = parts.next() { 480 | let menu = self.menu_mgr.get_menu(None); 481 | if cmd == "help" { 482 | match parts.next() { 483 | Some(arg) => match menu.items.iter().find(|i| i.command == arg) { 484 | Some(item) => { 485 | self.print_long_help(interface, item); 486 | } 487 | None => { 488 | writeln!(interface, "I can't help with {:?}", arg).unwrap(); 489 | } 490 | }, 491 | _ => { 492 | writeln!(interface, "AVAILABLE ITEMS:").unwrap(); 493 | for item in menu.items { 494 | self.print_short_help(interface, item); 495 | } 496 | if self.menu_mgr.depth() != 0 { 497 | self.print_short_help( 498 | interface, 499 | &Item { 500 | command: "exit", 501 | help: Some("Leave this menu."), 502 | item_type: ItemType::_Dummy, 503 | }, 504 | ); 505 | } 506 | self.print_short_help( 507 | interface, 508 | &Item { 509 | command: "help [ ]", 510 | help: Some("Show this help, or get help on a specific command."), 511 | item_type: ItemType::_Dummy, 512 | }, 513 | ); 514 | } 515 | } 516 | } else if cmd == "exit" && self.menu_mgr.depth() != 0 { 517 | if let Some(cb_fn) = menu.exit { 518 | cb_fn(menu, interface, context); 519 | } 520 | self.menu_mgr.pop_menu(); 521 | } else { 522 | let mut found = false; 523 | for (i, item) in menu.items.iter().enumerate() { 524 | if cmd == item.command { 525 | match item.item_type { 526 | ItemType::Callback { 527 | function, 528 | parameters, 529 | } => Self::call_function( 530 | interface, 531 | context, 532 | function, 533 | parameters, 534 | menu, 535 | item, 536 | command_line, 537 | ), 538 | ItemType::Menu(incoming_menu) => { 539 | if let Some(cb_fn) = incoming_menu.entry { 540 | cb_fn(incoming_menu, interface, context); 541 | } 542 | self.menu_mgr.push_menu(i); 543 | } 544 | ItemType::_Dummy => { 545 | unreachable!(); 546 | } 547 | } 548 | found = true; 549 | break; 550 | } 551 | } 552 | if !found { 553 | writeln!(interface, "Command {:?} not found. Try 'help'.", cmd).unwrap(); 554 | } 555 | } 556 | } else { 557 | writeln!(interface, "Input was empty?").unwrap(); 558 | } 559 | } 560 | 561 | fn print_short_help(&mut self, interface: &mut I, item: &Item) { 562 | let mut has_options = false; 563 | match item.item_type { 564 | ItemType::Callback { parameters, .. } => { 565 | write!(interface, " {}", item.command).unwrap(); 566 | if !parameters.is_empty() { 567 | for param in parameters.iter() { 568 | match param { 569 | Parameter::Mandatory { parameter_name, .. } => { 570 | write!(interface, " <{}>", parameter_name).unwrap(); 571 | } 572 | Parameter::Optional { parameter_name, .. } => { 573 | write!(interface, " [ <{}> ]", parameter_name).unwrap(); 574 | } 575 | Parameter::Named { .. } => { 576 | has_options = true; 577 | } 578 | Parameter::NamedValue { .. } => { 579 | has_options = true; 580 | } 581 | } 582 | } 583 | } 584 | } 585 | ItemType::Menu(_menu) => { 586 | write!(interface, " {}", item.command).unwrap(); 587 | } 588 | ItemType::_Dummy => { 589 | write!(interface, " {}", item.command).unwrap(); 590 | } 591 | } 592 | if has_options { 593 | write!(interface, " [OPTIONS...]").unwrap(); 594 | } 595 | writeln!(interface).unwrap(); 596 | } 597 | 598 | fn print_long_help(&mut self, interface: &mut I, item: &Item) { 599 | writeln!(interface, "SUMMARY:").unwrap(); 600 | match item.item_type { 601 | ItemType::Callback { parameters, .. } => { 602 | write!(interface, " {}", item.command).unwrap(); 603 | if !parameters.is_empty() { 604 | for param in parameters.iter() { 605 | match param { 606 | Parameter::Mandatory { parameter_name, .. } => { 607 | write!(interface, " <{}>", parameter_name).unwrap(); 608 | } 609 | Parameter::Optional { parameter_name, .. } => { 610 | write!(interface, " [ <{}> ]", parameter_name).unwrap(); 611 | } 612 | Parameter::Named { parameter_name, .. } => { 613 | write!(interface, " [ --{} ]", parameter_name).unwrap(); 614 | } 615 | Parameter::NamedValue { 616 | parameter_name, 617 | argument_name, 618 | .. 619 | } => { 620 | write!(interface, " [ --{}={} ]", parameter_name, argument_name) 621 | .unwrap(); 622 | } 623 | } 624 | } 625 | writeln!(interface, "\n\nPARAMETERS:").unwrap(); 626 | let default_help = "Undocumented option"; 627 | for param in parameters.iter() { 628 | match param { 629 | Parameter::Mandatory { 630 | parameter_name, 631 | help, 632 | } => { 633 | writeln!( 634 | interface, 635 | " <{0}>\n {1}\n", 636 | parameter_name, 637 | help.unwrap_or(default_help), 638 | ) 639 | .unwrap(); 640 | } 641 | Parameter::Optional { 642 | parameter_name, 643 | help, 644 | } => { 645 | writeln!( 646 | interface, 647 | " <{0}>\n {1}\n", 648 | parameter_name, 649 | help.unwrap_or(default_help), 650 | ) 651 | .unwrap(); 652 | } 653 | Parameter::Named { 654 | parameter_name, 655 | help, 656 | } => { 657 | writeln!( 658 | interface, 659 | " --{0}\n {1}\n", 660 | parameter_name, 661 | help.unwrap_or(default_help), 662 | ) 663 | .unwrap(); 664 | } 665 | Parameter::NamedValue { 666 | parameter_name, 667 | argument_name, 668 | help, 669 | } => { 670 | writeln!( 671 | interface, 672 | " --{0}={1}\n {2}\n", 673 | parameter_name, 674 | argument_name, 675 | help.unwrap_or(default_help), 676 | ) 677 | .unwrap(); 678 | } 679 | } 680 | } 681 | } 682 | } 683 | ItemType::Menu(_menu) => { 684 | write!(interface, " {}", item.command).unwrap(); 685 | } 686 | ItemType::_Dummy => { 687 | write!(interface, " {}", item.command).unwrap(); 688 | } 689 | } 690 | if let Some(help) = item.help { 691 | writeln!(interface, "\n\nDESCRIPTION:\n{}", help).unwrap(); 692 | } 693 | } 694 | 695 | fn call_function( 696 | interface: &mut I, 697 | context: &mut T, 698 | callback_function: ItemCallbackFn, 699 | parameters: &[Parameter], 700 | parent_menu: &Menu, 701 | item: &Item, 702 | command: &str, 703 | ) { 704 | let mandatory_parameter_count = parameters 705 | .iter() 706 | .filter(|p| matches!(p, Parameter::Mandatory { .. })) 707 | .count(); 708 | let positional_parameter_count = parameters 709 | .iter() 710 | .filter(|p| matches!(p, Parameter::Mandatory { .. } | Parameter::Optional { .. })) 711 | .count(); 712 | if command.len() >= item.command.len() { 713 | // Maybe arguments 714 | let mut argument_buffer: [&str; 16] = [""; 16]; 715 | let mut argument_count = 0; 716 | let mut positional_arguments = 0; 717 | for (slot, arg) in argument_buffer 718 | .iter_mut() 719 | .zip(command[item.command.len()..].split_whitespace()) 720 | { 721 | *slot = arg; 722 | argument_count += 1; 723 | if let Some(tail) = arg.strip_prefix("--") { 724 | // Validate named argument 725 | let mut found = false; 726 | for param in parameters.iter() { 727 | match param { 728 | Parameter::Named { parameter_name, .. } => { 729 | if tail == *parameter_name { 730 | found = true; 731 | break; 732 | } 733 | } 734 | Parameter::NamedValue { parameter_name, .. } => { 735 | if arg.contains('=') { 736 | if let Some(given_name) = tail.split('=').next() { 737 | if given_name == *parameter_name { 738 | found = true; 739 | break; 740 | } 741 | } 742 | } 743 | } 744 | _ => { 745 | // Ignore 746 | } 747 | } 748 | } 749 | if !found { 750 | writeln!(interface, "Error: Did not understand {:?}", arg).unwrap(); 751 | return; 752 | } 753 | } else { 754 | positional_arguments += 1; 755 | } 756 | } 757 | if positional_arguments < mandatory_parameter_count { 758 | writeln!(interface, "Error: Insufficient arguments given").unwrap(); 759 | } else if positional_arguments > positional_parameter_count { 760 | writeln!(interface, "Error: Too many arguments given").unwrap(); 761 | } else { 762 | callback_function( 763 | parent_menu, 764 | item, 765 | &argument_buffer[0..argument_count], 766 | interface, 767 | context, 768 | ); 769 | } 770 | } else { 771 | // Definitely no arguments 772 | if mandatory_parameter_count == 0 { 773 | callback_function(parent_menu, item, &[], interface, context); 774 | } else { 775 | writeln!(interface, "Error: Insufficient arguments given").unwrap(); 776 | } 777 | } 778 | } 779 | } 780 | 781 | #[cfg(test)] 782 | mod tests { 783 | use super::*; 784 | 785 | fn dummy( 786 | _menu: &Menu<(), u32>, 787 | _item: &Item<(), u32>, 788 | _args: &[&str], 789 | _interface: &mut (), 790 | _context: &mut u32, 791 | ) { 792 | } 793 | 794 | #[test] 795 | fn find_arg_mandatory() { 796 | let item = Item { 797 | command: "dummy", 798 | help: None, 799 | item_type: ItemType::Callback { 800 | function: dummy, 801 | parameters: &[ 802 | Parameter::Mandatory { 803 | parameter_name: "foo", 804 | help: Some("Some help for foo"), 805 | }, 806 | Parameter::Mandatory { 807 | parameter_name: "bar", 808 | help: Some("Some help for bar"), 809 | }, 810 | Parameter::Mandatory { 811 | parameter_name: "baz", 812 | help: Some("Some help for baz"), 813 | }, 814 | ], 815 | }, 816 | }; 817 | assert_eq!( 818 | argument_finder(&item, &["a", "b", "c"], "foo"), 819 | Ok(Some("a")) 820 | ); 821 | assert_eq!( 822 | argument_finder(&item, &["a", "b", "c"], "bar"), 823 | Ok(Some("b")) 824 | ); 825 | assert_eq!( 826 | argument_finder(&item, &["a", "b", "c"], "baz"), 827 | Ok(Some("c")) 828 | ); 829 | // Not an argument 830 | assert_eq!( 831 | argument_finder(&item, &["a", "b", "c"], "quux"), 832 | Err(Error::NotFound) 833 | ); 834 | } 835 | 836 | #[test] 837 | fn find_arg_optional() { 838 | let item = Item { 839 | command: "dummy", 840 | help: None, 841 | item_type: ItemType::Callback { 842 | function: dummy, 843 | parameters: &[ 844 | Parameter::Mandatory { 845 | parameter_name: "foo", 846 | help: Some("Some help for foo"), 847 | }, 848 | Parameter::Mandatory { 849 | parameter_name: "bar", 850 | help: Some("Some help for bar"), 851 | }, 852 | Parameter::Optional { 853 | parameter_name: "baz", 854 | help: Some("Some help for baz"), 855 | }, 856 | ], 857 | }, 858 | }; 859 | assert_eq!( 860 | argument_finder(&item, &["a", "b", "c"], "foo"), 861 | Ok(Some("a")) 862 | ); 863 | assert_eq!( 864 | argument_finder(&item, &["a", "b", "c"], "bar"), 865 | Ok(Some("b")) 866 | ); 867 | assert_eq!( 868 | argument_finder(&item, &["a", "b", "c"], "baz"), 869 | Ok(Some("c")) 870 | ); 871 | // Not an argument 872 | assert_eq!( 873 | argument_finder(&item, &["a", "b", "c"], "quux"), 874 | Err(Error::NotFound) 875 | ); 876 | // Missing optional 877 | assert_eq!(argument_finder(&item, &["a", "b"], "baz"), Ok(None)); 878 | } 879 | 880 | #[test] 881 | fn find_arg_named() { 882 | let item = Item { 883 | command: "dummy", 884 | help: None, 885 | item_type: ItemType::Callback { 886 | function: dummy, 887 | parameters: &[ 888 | Parameter::Mandatory { 889 | parameter_name: "foo", 890 | help: Some("Some help for foo"), 891 | }, 892 | Parameter::Named { 893 | parameter_name: "bar", 894 | help: Some("Some help for bar"), 895 | }, 896 | Parameter::Named { 897 | parameter_name: "baz", 898 | help: Some("Some help for baz"), 899 | }, 900 | ], 901 | }, 902 | }; 903 | assert_eq!( 904 | argument_finder(&item, &["a", "--bar", "--baz"], "foo"), 905 | Ok(Some("a")) 906 | ); 907 | assert_eq!( 908 | argument_finder(&item, &["a", "--bar", "--baz"], "bar"), 909 | Ok(Some("")) 910 | ); 911 | assert_eq!( 912 | argument_finder(&item, &["a", "--bar", "--baz"], "baz"), 913 | Ok(Some("")) 914 | ); 915 | // Not an argument 916 | assert_eq!( 917 | argument_finder(&item, &["a", "--bar", "--baz"], "quux"), 918 | Err(Error::NotFound) 919 | ); 920 | // Missing named 921 | assert_eq!(argument_finder(&item, &["a"], "baz"), Ok(None)); 922 | } 923 | 924 | #[test] 925 | fn find_arg_namedvalue() { 926 | let item = Item { 927 | command: "dummy", 928 | help: None, 929 | item_type: ItemType::Callback { 930 | function: dummy, 931 | parameters: &[ 932 | Parameter::Mandatory { 933 | parameter_name: "foo", 934 | help: Some("Some help for foo"), 935 | }, 936 | Parameter::Named { 937 | parameter_name: "bar", 938 | help: Some("Some help for bar"), 939 | }, 940 | Parameter::NamedValue { 941 | parameter_name: "baz", 942 | argument_name: "TEST", 943 | help: Some("Some help for baz"), 944 | }, 945 | ], 946 | }, 947 | }; 948 | assert_eq!( 949 | argument_finder(&item, &["a", "--bar", "--baz"], "foo"), 950 | Ok(Some("a")) 951 | ); 952 | assert_eq!( 953 | argument_finder(&item, &["a", "--bar", "--baz"], "bar"), 954 | Ok(Some("")) 955 | ); 956 | // No argument so mark as not found 957 | assert_eq!( 958 | argument_finder(&item, &["a", "--bar", "--baz"], "baz"), 959 | Ok(None) 960 | ); 961 | // Empty argument 962 | assert_eq!( 963 | argument_finder(&item, &["a", "--bar", "--baz="], "baz"), 964 | Ok(Some("")) 965 | ); 966 | // Short argument 967 | assert_eq!( 968 | argument_finder(&item, &["a", "--bar", "--baz=1"], "baz"), 969 | Ok(Some("1")) 970 | ); 971 | // Long argument 972 | assert_eq!( 973 | argument_finder( 974 | &item, 975 | &["a", "--bar", "--baz=abcdefghijklmnopqrstuvwxyz"], 976 | "baz" 977 | ), 978 | Ok(Some("abcdefghijklmnopqrstuvwxyz")) 979 | ); 980 | // Not an argument 981 | assert_eq!( 982 | argument_finder(&item, &["a", "--bar", "--baz"], "quux"), 983 | Err(Error::NotFound) 984 | ); 985 | // Missing named 986 | assert_eq!(argument_finder(&item, &["a"], "baz"), Ok(None)); 987 | } 988 | } 989 | -------------------------------------------------------------------------------- /src/menu_manager.rs: -------------------------------------------------------------------------------- 1 | //! The Menu Manager looks after the menu and where we currently are within it. 2 | #![deny(missing_docs)] 3 | 4 | use super::{ItemType, Menu}; 5 | 6 | /// Holds a nested tree of Menus and remembers which menu within the tree we're 7 | /// currently looking at. 8 | pub struct MenuManager<'a, I, T> { 9 | menu: Menu<'a, I, T>, 10 | /// Maximum four levels deep 11 | menu_index: [Option; 4], 12 | } 13 | 14 | impl<'a, I, T> MenuManager<'a, I, T> { 15 | /// Create a new MenuManager. 16 | /// 17 | /// You will be at the top-level. 18 | pub fn new(menu: Menu<'a, I, T>) -> Self { 19 | Self { 20 | menu, 21 | menu_index: [None, None, None, None], 22 | } 23 | } 24 | 25 | /// How deep into the tree are we? 26 | pub fn depth(&self) -> usize { 27 | self.menu_index.iter().take_while(|x| x.is_some()).count() 28 | } 29 | 30 | /// Go back up to a higher-level menu 31 | pub fn pop_menu(&mut self) { 32 | if let Some(pos) = self.menu_index.iter_mut().rev().find(|x| x.is_some()) { 33 | pos.take(); 34 | } 35 | } 36 | 37 | /// Drop into a sub-menu. 38 | /// 39 | /// The index must be the index of a valid sub-menu, not any other kind of 40 | /// item. Do not push too many items. 41 | pub fn push_menu(&mut self, index: usize) { 42 | let menu = self.get_menu(None); 43 | let item = menu.items[index]; 44 | if !matches!(item.item_type, ItemType::Menu(_)) { 45 | panic!("Specified index is not a menu"); 46 | } 47 | 48 | let pos = self.menu_index.iter_mut().find(|x| x.is_none()).unwrap(); 49 | pos.replace(index); 50 | } 51 | 52 | /// Get a menu. 53 | /// 54 | /// Menus are nested. If `depth` is `None`, get the current menu. Otherwise 55 | /// if it is `Some(i)` get the menu at depth `i`. 56 | pub fn get_menu(&self, depth: Option) -> &Menu<'a, I, T> { 57 | let mut menu = &self.menu; 58 | 59 | let depth = depth.unwrap_or_else(|| self.depth()); 60 | 61 | for position in self 62 | .menu_index 63 | .iter() 64 | .take_while(|x| x.is_some()) 65 | .map(|x| x.unwrap()) 66 | .take(depth) 67 | { 68 | if let ItemType::Menu(m) = menu.items[position].item_type { 69 | menu = m 70 | } else { 71 | panic!("Selected item is not a menu"); 72 | } 73 | } 74 | 75 | menu 76 | } 77 | } 78 | --------------------------------------------------------------------------------