├── .github └── workflows │ ├── changelog.yml │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── no_std │ ├── rp2040-embassy │ │ ├── .cargo │ │ │ └── config.toml │ │ ├── Cargo.lock │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── build.rs │ │ ├── memory.x │ │ ├── rust-toolchain.toml │ │ └── src │ │ │ ├── blink.rs │ │ │ ├── main.rs │ │ │ ├── noline_async.rs │ │ │ └── usb.rs │ └── rp2040 │ │ ├── .cargo │ │ └── config.toml │ │ ├── Cargo.lock │ │ ├── Cargo.toml │ │ ├── build.rs │ │ ├── memory.x │ │ └── src │ │ └── main.rs └── std │ ├── .projectile │ ├── Cargo.lock │ ├── Cargo.toml │ └── src │ └── bin │ ├── std-async-tokio.rs │ └── std-sync.rs ├── noline ├── .projectile ├── Cargo.toml ├── README.tpl └── src │ ├── async_editor.rs │ ├── builder.rs │ ├── core.rs │ ├── error.rs │ ├── history.rs │ ├── input.rs │ ├── lib.rs │ ├── line_buffer.rs │ ├── output.rs │ ├── sync_editor.rs │ ├── terminal.rs │ ├── testlib.rs │ └── utf8.rs └── rust-toolchain.toml /.github/workflows/changelog.yml: -------------------------------------------------------------------------------- 1 | name: "Pull Request Workflow" 2 | on: 3 | pull_request: 4 | # The specific activity types are listed here to include "labeled" and "unlabeled" 5 | # (which are not included by default for the "pull_request" trigger). 6 | # This is needed to allow skipping enforcement of the changelog in PRs with specific labels, 7 | # as defined in the (optional) "skipLabels" property. 8 | types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled] 9 | 10 | jobs: 11 | # Enforces the update of a changelog file on every pull request 12 | changelog: 13 | runs-on: ubuntu-latest 14 | steps: 15 | # The checkout step is needed since the enforcer relies on local git commands 16 | - uses: actions/checkout@v2 17 | - uses: dangoslen/changelog-enforcer@v2 18 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | noline: 14 | runs-on: ubuntu-latest 15 | 16 | defaults: 17 | run: 18 | working-directory: ./noline 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Format 23 | run: cargo fmt --all -- --check 24 | - name: Doc 25 | run: cargo doc --verbose --all-features 26 | - name: Build 27 | run: cargo build --verbose --all-features 28 | - name: Run tests 29 | run: cargo test --verbose --all-features 30 | 31 | readme: 32 | runs-on: ubuntu-latest 33 | 34 | defaults: 35 | run: 36 | working-directory: ./noline 37 | 38 | steps: 39 | - uses: actions/checkout@v2 40 | - uses: actions-rs/install@v0.1 41 | with: 42 | crate: cargo-readme 43 | version: latest 44 | use-tool-cache: true 45 | - name: Readme 46 | run: cargo readme > ../README.md && git diff --exit-code 47 | 48 | # cargo-outdated: 49 | 50 | # runs-on: ubuntu-latest 51 | 52 | # defaults: 53 | # run: 54 | # working-directory: ./noline 55 | 56 | # steps: 57 | # - uses: actions/checkout@v2 58 | # - uses: actions-rs/install@v0.1 59 | # with: 60 | # crate: cargo-outdated 61 | # version: latest 62 | # use-tool-cache: true 63 | # - name: Outdated dependencies 64 | # run: cargo outdated --exit-code 1 65 | 66 | examples-std: 67 | runs-on: ubuntu-latest 68 | 69 | defaults: 70 | run: 71 | working-directory: ./examples/std 72 | 73 | steps: 74 | - uses: actions/checkout@v2 75 | - name: Build 76 | run: cargo build --verbose 77 | 78 | examples-rp2040: 79 | runs-on: ubuntu-latest 80 | 81 | defaults: 82 | run: 83 | working-directory: ./examples/no_std/rp2040 84 | 85 | steps: 86 | - uses: actions/checkout@v2 87 | 88 | - name: Install toolchain 89 | uses: actions-rs/toolchain@v1 90 | with: 91 | toolchain: stable 92 | target: thumbv6m-none-eabi 93 | 94 | - name: Install linker 95 | uses: actions-rs/install@v0.1 96 | with: 97 | crate: flip-link 98 | version: latest 99 | use-tool-cache: true 100 | 101 | - name: Build 102 | run: cargo build --verbose 103 | 104 | examples-rp2040-embassy: 105 | runs-on: ubuntu-latest 106 | 107 | defaults: 108 | run: 109 | working-directory: ./examples/no_std/rp2040-embassy 110 | 111 | steps: 112 | - uses: actions/checkout@v2 113 | 114 | - name: Install toolchain 115 | uses: actions-rs/toolchain@v1 116 | with: 117 | toolchain: stable 118 | target: thumbv6m-none-eabi 119 | 120 | - name: Build 121 | run: cargo build --verbose 122 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | Cargo.lock 3 | *~ 4 | .vscode/launch.json 5 | .vscode/tasts.json 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | - Bugfix: Missing CPR arguments causes panic 11 | 12 | ## [0.5.0 - 2024-12-12] 13 | 14 | - Removed initializer. Probe terminal size before prompt for every line. 15 | 16 | ## [0.5.0 - 2024-08-30] 17 | 18 | - Bugfix: Char boundary string splitting error 19 | - Allow dynamic prompt made from iterator over `&str` 20 | - Take slice instead of owning buffer for line and history 21 | 22 | ## [0.4.0 - 2024-08-27] 23 | 24 | - Fixed no_std examples build failure by checking in lock files 25 | - Removed IO abstraction 26 | - Fixed clippy warnings 27 | - Removed `sync` and `async` features 28 | - Removed `std` from default features 29 | 30 | ## [0.3.0 - 2024-07-05] 31 | 32 | - Use embedded_io and embedded_io_async Read/Write traits 33 | - This ensures the std and no-std implementations are equivilent at the IO API interface 34 | - Add sync and async examples for rp2040 async makes use of [embassy](https://embassy.dev/) 35 | - Removed stm32 example as there was no hardware available (can be replaced later) 36 | 37 | ## [0.2.1] - 2024-06-06 38 | 39 | - Added Linefeed as a valid line terminator 40 | - Fixed overflow error when attempting to navigate empty history 41 | 42 | ## [0.2.0] - 2022-03-22 43 | 44 | - Added basic line history 45 | 46 | - Added EditorBuilder for more ergonomic construction of editors 47 | 48 | ## [0.1.0] - 2022-03-14 49 | 50 | - Initial release 51 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = [ 4 | "noline", 5 | "examples/std", 6 | "examples/no_std/rp2040", 7 | "examples/no_std/rp2040-embassy", 8 | ] 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Pipeline](https://github.com/rustne-kretser/noline/actions/workflows/rust.yml/badge.svg) 2 | [![Crates.io](https://img.shields.io/crates/v/noline.svg)](https://crates.io/crates/noline) 3 | [![API reference](https://docs.rs/noline/badge.svg)](https://docs.rs/noline/) 4 | 5 | # noline 6 | 7 | Noline is an IO-agnostic `#[no_std]` line editor providing robust 8 | line editing for any system. The core functionality is IO-free, so 9 | it can be adapted to any system be it embedded, async, async 10 | embedded, WASM or IPoAC (IP over Avian Carriers). 11 | 12 | Features: 13 | - IO-free 14 | - Minimal dependencies 15 | - No allocation needed - Both heap-based and static buffers are provided 16 | - UTF-8 support 17 | - Emacs keybindings 18 | - Line history 19 | 20 | Possible future features: 21 | - Auto-completion and hints 22 | 23 | The API should be considered experimental and will change in the 24 | future. 25 | 26 | The core implementation consists of a state machine taking bytes as 27 | input and yielding iterators over byte slices. Because this is 28 | done without any IO, Noline can be adapted to work on any platform. 29 | 30 | Noline comes with multiple implemenations: 31 | - [`sync_editor::Editor`] – Editor for synchronous IO 32 | - [`async_editor::Editor`] - Editor for asynchronous IO 33 | 34 | Editors can be built using [`builder::EditorBuilder`]. 35 | 36 | ## Example 37 | ```rust 38 | let prompt = "> "; 39 | 40 | let mut io = MyIO {}; // IO handler, see full examples for details 41 | // how to implement it 42 | 43 | let mut editor = EditorBuilder::new_unbounded() 44 | .with_unbounded_history() 45 | .build_sync(&mut io) 46 | .unwrap(); 47 | 48 | while let Ok(line) = editor.readline(prompt, &mut io) { 49 | writeln!(io, "Read: '{}'", line).unwrap(); 50 | } 51 | ``` 52 | 53 | For more details, see [docs](https://docs.rs/noline/). 54 | 55 | # Usage 56 | 57 | Add this to your Cargo.toml: 58 | 59 | ```toml 60 | [dependencies] 61 | noline = "0.5.1" 62 | ``` 63 | 64 | # License 65 | 66 | MPL-2.0 67 | -------------------------------------------------------------------------------- /examples/no_std/rp2040-embassy/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.'cfg(all(target_arch = "arm", target_os = "none"))'] 2 | runner = "probe-rs run --chip RP2040" 3 | 4 | [build] 5 | target = "thumbv6m-none-eabi" 6 | 7 | [env] 8 | DEFMT_LOG = "trace" 9 | -------------------------------------------------------------------------------- /examples/no_std/rp2040-embassy/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Matt Spencer "] 3 | edition = "2018" 4 | name = "rp2040-embassy" 5 | version = "0.1.0" 6 | 7 | [features] 8 | default = ["defmt"] 9 | defmt = [] 10 | 11 | [dependencies] 12 | 13 | embassy-rp = { version = "0.1.0", features = [ 14 | "defmt", 15 | "unstable-pac", 16 | "time-driver", 17 | "critical-section-impl", 18 | ] } 19 | embassy-embedded-hal = { version = "0.1.0", features = ["defmt"] } 20 | embassy-executor = { version = "0.5.0", features = [ 21 | "arch-cortex-m", 22 | "executor-thread", 23 | "integrated-timers", 24 | "arch-cortex-m", 25 | "executor-thread", 26 | ] } 27 | embassy-sync = { version = "0.6.0", features = ["defmt"] } 28 | embassy-time = { version = "0.3.0", features = [ 29 | "defmt", 30 | "defmt-timestamp-uptime", 31 | ] } 32 | embassy-futures = { version = "0.1.0" } 33 | embassy-usb = { version = "0.2.0", features = ["defmt"] } 34 | 35 | 36 | defmt = "0.3" 37 | defmt-rtt = "0.4" 38 | fixed = "1.23.1" 39 | fixed-macro = "1.2" 40 | format_no_std = "1.2.0" 41 | 42 | #cortex-m = { version = "0.7.6", features = ["critical-section-single-core"] } 43 | cortex-m = { version = "0.7.6", features = ["inline-asm"] } 44 | cortex-m-rt = "0.7.0" 45 | panic-probe = { version = "0.3", features = ["print-defmt"] } 46 | heapless = "0.8.0" 47 | embedded-hal = { version = "1.0.0" } 48 | 49 | # fugit = "0.3.7" 50 | noline = { path = "../../../noline" } 51 | nb = "1.0.0" 52 | fixed-queue = "0.5.1" 53 | embedded-io-async = "0.6.1" 54 | 55 | 56 | [profile.release] 57 | debug = 2 58 | -------------------------------------------------------------------------------- /examples/no_std/rp2040-embassy/README.md: -------------------------------------------------------------------------------- 1 | # Embassy async enabled USB CDC interface 2 | 3 | This example code shows how to use the embassy async system to noline in a no_std environment. 4 | 5 | It has only been tested on the RP2040, but should be simple to port to other platforms supported by embassy. 6 | 7 | It has two async functions running to demonstrate that the two can co-exist. 8 | 9 | - blink::blinking_led which flashes the status LED on a regular cadance 10 | - noline_async::cli which reads and writes to the USB CDC serial interface 11 | -------------------------------------------------------------------------------- /examples/no_std/rp2040-embassy/build.rs: -------------------------------------------------------------------------------- 1 | //! This build script copies the `memory.x` file from the crate root into 2 | //! a directory where the linker can always find it at build time. 3 | //! For many projects this is optional, as the linker always searches the 4 | //! project root directory -- wherever `Cargo.toml` is. However, if you 5 | //! are using a workspace or have a more complicated build setup, this 6 | //! build script becomes required. Additionally, by requesting that 7 | //! Cargo re-run the build script whenever `memory.x` is changed, 8 | //! updating `memory.x` ensures a rebuild of the application with the 9 | //! new memory settings. 10 | 11 | use std::env; 12 | use std::fs::File; 13 | use std::io::Write; 14 | use std::path::PathBuf; 15 | 16 | fn main() { 17 | // Put `memory.x` in our output directory and ensure it's 18 | // on the linker search path. 19 | let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); 20 | File::create(out.join("memory.x")) 21 | .unwrap() 22 | .write_all(include_bytes!("memory.x")) 23 | .unwrap(); 24 | println!("cargo:rustc-link-search={}", out.display()); 25 | 26 | // By default, Cargo will re-run a build script whenever 27 | // any file in the project changes. By specifying `memory.x` 28 | // here, we ensure the build script is only re-run when 29 | // `memory.x` is changed. 30 | println!("cargo:rerun-if-changed=memory.x"); 31 | 32 | println!("cargo:rustc-link-arg-bins=--nmagic"); 33 | println!("cargo:rustc-link-arg-bins=-Tlink.x"); 34 | println!("cargo:rustc-link-arg-bins=-Tlink-rp.x"); 35 | println!("cargo:rustc-link-arg-bins=-Tdefmt.x"); 36 | } 37 | -------------------------------------------------------------------------------- /examples/no_std/rp2040-embassy/memory.x: -------------------------------------------------------------------------------- 1 | MEMORY { 2 | BOOT2 : ORIGIN = 0x10000000, LENGTH = 0x100 3 | FLASH : ORIGIN = 0x10000100, LENGTH = 2048K - 0x100 4 | RAM : ORIGIN = 0x20000000, LENGTH = 256K 5 | } -------------------------------------------------------------------------------- /examples/no_std/rp2040-embassy/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | # Before upgrading check that everything is available on all tier1 targets here: 2 | # https://rust-lang.github.io/rustup-components-history 3 | [toolchain] 4 | channel = "stable" 5 | components = ["rust-src", "rustfmt", "llvm-tools"] 6 | targets = [ 7 | "thumbv6m-none-eabi", 8 | ] 9 | 10 | -------------------------------------------------------------------------------- /examples/no_std/rp2040-embassy/src/blink.rs: -------------------------------------------------------------------------------- 1 | use embassy_rp::gpio; 2 | use embassy_time::{Duration, Timer}; 3 | use gpio::{AnyPin, Level, Output}; 4 | 5 | #[embassy_executor::task] 6 | pub async fn blinking_led(led: AnyPin) { 7 | let mut led = Output::new(led, Level::Low); 8 | 9 | loop { 10 | led.set_high(); 11 | Timer::after(Duration::from_millis(750)).await; 12 | 13 | led.set_low(); 14 | Timer::after(Duration::from_millis(250)).await; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/no_std/rp2040-embassy/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | 4 | mod blink; 5 | mod noline_async; 6 | mod usb; 7 | 8 | use embassy_executor::Spawner; 9 | use {defmt_rtt as _, panic_probe as _}; 10 | 11 | use blink::blinking_led; 12 | use usb::usb_handler; 13 | 14 | #[embassy_executor::main] 15 | async fn main(spawner: Spawner) { 16 | let p = embassy_rp::init(Default::default()); 17 | 18 | spawner.spawn(blinking_led(p.PIN_25.into())).unwrap(); 19 | spawner.spawn(usb_handler(p.USB)).unwrap(); 20 | } 21 | -------------------------------------------------------------------------------- /examples/no_std/rp2040-embassy/src/noline_async.rs: -------------------------------------------------------------------------------- 1 | use embassy_rp::usb::{Driver, Instance}; 2 | use embassy_usb::{ 3 | class::cdc_acm::{ControlChanged, Receiver, Sender}, 4 | driver::EndpointError, 5 | }; 6 | use embedded_io_async::{ErrorKind, Write}; 7 | use fixed_queue::VecDeque; 8 | use noline::builder::EditorBuilder; 9 | 10 | struct IO<'a, T> 11 | where 12 | T: Instance, 13 | { 14 | pub stdin: &'a mut Receiver<'a, Driver<'a, T>>, 15 | queue: VecDeque, 16 | pub stdout: &'a mut Sender<'a, Driver<'a, T>>, 17 | } 18 | 19 | impl<'a, T> IO<'a, T> 20 | where 21 | T: Instance, 22 | { 23 | fn new( 24 | stdin: &'a mut Receiver<'a, Driver<'a, T>>, 25 | stdout: &'a mut Sender<'a, Driver<'a, T>>, 26 | ) -> Self { 27 | Self { 28 | stdin, 29 | queue: VecDeque::new(), 30 | stdout, 31 | } 32 | } 33 | } 34 | 35 | #[derive(Debug)] 36 | struct Error(()); 37 | 38 | impl From for Error { 39 | fn from(_value: EndpointError) -> Self { 40 | Self(()) 41 | } 42 | } 43 | 44 | impl embedded_io_async::Error for Error { 45 | fn kind(&self) -> ErrorKind { 46 | ErrorKind::Other 47 | } 48 | } 49 | 50 | impl<'a, T> embedded_io_async::ErrorType for IO<'a, T> 51 | where 52 | T: Instance, 53 | { 54 | type Error = Error; 55 | } 56 | 57 | // Read data from the input and make it available asynchronously 58 | impl<'a, T> embedded_io_async::Read for IO<'a, T> 59 | where 60 | T: Instance, 61 | { 62 | async fn read(&mut self, buf: &mut [u8]) -> Result { 63 | // If the queue is empty 64 | while self.queue.is_empty() { 65 | let mut buf: [u8; 64] = [0; 64]; 66 | // Read a maximum of 64 bytes from the ouput 67 | let len = self.stdin.read_packet(&mut buf).await?; 68 | // This is safe because we only ever pull data when empty 69 | // And the queue has the same capacity as the input buffer 70 | for i in buf.iter().take(len) { 71 | self.queue.push_back(*i).expect("Buffer overflow"); 72 | } 73 | } 74 | 75 | if let Some(v) = self.queue.pop_front() { 76 | buf[0] = v; 77 | Ok(1) 78 | } else { 79 | Err(Error(())) 80 | } 81 | } 82 | } 83 | 84 | // Implement the noline writer trait to enable us to write to the USB output 85 | impl<'a, T> embedded_io_async::Write for IO<'a, T> 86 | where 87 | T: Instance, 88 | { 89 | async fn write(&mut self, buf: &[u8]) -> Result { 90 | self.stdout.write_packet(buf).await?; 91 | 92 | Ok(buf.len()) 93 | } 94 | 95 | async fn flush(&mut self) -> Result<(), Self::Error> { 96 | // TODO: Implement me 97 | Ok(()) 98 | } 99 | } 100 | 101 | const MAX_LINE_SIZE: usize = 64; 102 | 103 | pub async fn cli<'d, T: Instance + 'd>( 104 | send: &'d mut Sender<'d, Driver<'d, T>>, 105 | recv: &'d mut Receiver<'d, Driver<'d, T>>, 106 | control: &'d mut ControlChanged<'d>, 107 | ) { 108 | let prompt = "> "; 109 | 110 | let mut io = IO::new(recv, send); 111 | let mut buffer = [0; MAX_LINE_SIZE]; 112 | let mut history = [0; MAX_LINE_SIZE]; 113 | 114 | loop { 115 | io.stdout.wait_connection().await; 116 | 117 | while !(io.stdout.rts() && io.stdout.dtr()) { 118 | control.control_changed().await; 119 | } 120 | 121 | let mut editor = EditorBuilder::from_slice(&mut buffer) 122 | .with_slice_history(&mut history) 123 | .build_async(&mut io) 124 | .await 125 | .unwrap(); 126 | 127 | while let Ok(line) = editor.readline(prompt, &mut io).await { 128 | // Create a buffer that can take the MAX_LINE_SIZE along with the 'Read: ''\r/n' text 129 | let mut buf = [0u8; MAX_LINE_SIZE + 12]; 130 | let s = format_no_std::show(&mut buf, format_args!("Read: '{}'\r\n", line)) 131 | .expect("Format error"); 132 | 133 | // split s into slices of MAX_LINE_SIZE bytes as the USB output buffer has a 134 | // maximum size that we will overflow if we try and write more than this at one time 135 | for chunk in s.as_bytes().chunks(MAX_LINE_SIZE) { 136 | io.write(chunk).await.expect("Write error"); 137 | } 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /examples/no_std/rp2040-embassy/src/usb.rs: -------------------------------------------------------------------------------- 1 | use embassy_futures::join::join; 2 | use embassy_rp::bind_interrupts; 3 | use embassy_rp::peripherals::USB; 4 | use embassy_rp::usb::{Driver, InterruptHandler}; 5 | use embassy_usb::class::cdc_acm::{CdcAcmClass, State, USB_CLASS_CDC}; 6 | use embassy_usb::{Builder, Config}; 7 | 8 | use crate::noline_async::cli; 9 | 10 | bind_interrupts!(struct Irqs { 11 | USBCTRL_IRQ => InterruptHandler; 12 | }); 13 | 14 | const USB_CDC_SUBCLASS_ACM: u8 = 0x02; 15 | const USB_CDC_PROTOCOL_AT: u8 = 0x01; 16 | 17 | const BUF_SIZE_DESCRIPTOR: usize = 256; 18 | const BUF_SIZE_CONTROL: usize = 64; 19 | const MAX_PACKET_SIZE: u16 = 64; 20 | 21 | #[embassy_executor::task] 22 | pub async fn usb_handler(usb: USB) { 23 | // Create the driver, from the HAL. 24 | let driver = Driver::new(usb, Irqs); 25 | 26 | // Create embassy-usb Config 27 | let mut config = Config::new(0xc0de, 0xcafe); 28 | config.manufacturer = Some("Embassy"); 29 | config.product = Some("USB-serial example"); 30 | config.serial_number = Some("TEST"); 31 | config.max_power = 100; 32 | config.max_packet_size_0 = 64; 33 | 34 | // Required for windows compatibility. 35 | // https://developer.nordicsemi.com/nRF_Connect_SDK/doc/1.9.1/kconfig/CONFIG_CDC_ACM_IAD.html#help 36 | config.device_class = USB_CLASS_CDC; 37 | config.device_sub_class = USB_CDC_SUBCLASS_ACM; 38 | config.device_protocol = USB_CDC_PROTOCOL_AT; 39 | 40 | // Create embassy-usb DeviceBuilder using the driver and config. 41 | // It needs some buffers for building the descriptors. 42 | let mut config_descriptor = [0; BUF_SIZE_DESCRIPTOR]; 43 | let mut bos_descriptor = [0; BUF_SIZE_DESCRIPTOR]; 44 | let mut msos_descriptor = [0; BUF_SIZE_DESCRIPTOR]; 45 | let mut control_buf = [0; BUF_SIZE_CONTROL]; 46 | 47 | let mut state = State::new(); 48 | 49 | let mut builder = Builder::new( 50 | driver, 51 | config, 52 | //&mut device_descriptor, 53 | &mut config_descriptor, 54 | &mut bos_descriptor, 55 | &mut msos_descriptor, 56 | &mut control_buf, 57 | ); 58 | 59 | // Create classes on the builder. 60 | let serial = CdcAcmClass::new(&mut builder, &mut state, MAX_PACKET_SIZE); 61 | 62 | let (mut send, mut recv, mut control) = serial.split_with_control(); 63 | 64 | let noline_fut = async { 65 | cli(&mut send, &mut recv, &mut control).await; 66 | }; 67 | 68 | // Build the builder. 69 | let mut usb = builder.build(); 70 | 71 | // Run the USB device. 72 | let usb_fut = usb.run(); 73 | 74 | // Run everything concurrently. 75 | // If we had made everything `'static` above instead, we could do this using separate tasks instead. 76 | join(usb_fut, noline_fut).await; 77 | } 78 | -------------------------------------------------------------------------------- /examples/no_std/rp2040/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.'cfg(all(target_arch = "arm", target_os = "none"))'] 2 | # Choose a default "cargo run" tool (see README for more info) 3 | # - `probe-rs` provides flashing and defmt via a hardware debugger, and stack unwind on panic 4 | # - elf2uf2-rs loads firmware over USB when the rp2040 is in boot mode 5 | runner = "probe-rs run --chip RP2040 --protocol swd" 6 | # runner = "elf2uf2-rs -d" 7 | 8 | rustflags = [ 9 | "-C", "linker=flip-link", 10 | "-C", "link-arg=--nmagic", 11 | "-C", "link-arg=-Tlink.x", 12 | "-C", "link-arg=-Tdefmt.x", 13 | 14 | # Code-size optimizations. 15 | # trap unreachable can save a lot of space, but requires nightly compiler. 16 | # uncomment the next line if you wish to enable it 17 | # "-Z", "trap-unreachable=no", 18 | "-C", "inline-threshold=5", 19 | "-C", "no-vectorize-loops", 20 | ] 21 | 22 | [build] 23 | target = "thumbv6m-none-eabi" 24 | 25 | [env] 26 | DEFMT_LOG = "debug" -------------------------------------------------------------------------------- /examples/no_std/rp2040/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "arrayvec" 7 | version = "0.7.4" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" 10 | 11 | [[package]] 12 | name = "bare-metal" 13 | version = "0.2.5" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "5deb64efa5bd81e31fcd1938615a6d98c82eafcbcd787162b6f63b91d6bac5b3" 16 | dependencies = [ 17 | "rustc_version", 18 | ] 19 | 20 | [[package]] 21 | name = "bitfield" 22 | version = "0.13.2" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "46afbd2983a5d5a7bd740ccb198caf5b82f45c40c09c0eed36052d91cb92e719" 25 | 26 | [[package]] 27 | name = "bitfield" 28 | version = "0.14.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "2d7e60934ceec538daadb9d8432424ed043a904d8e0243f3c6446bce549a46ac" 31 | 32 | [[package]] 33 | name = "bitflags" 34 | version = "1.3.2" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 37 | 38 | [[package]] 39 | name = "byteorder" 40 | version = "1.4.3" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 43 | 44 | [[package]] 45 | name = "cortex-m" 46 | version = "0.7.7" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "8ec610d8f49840a5b376c69663b6369e71f4b34484b9b2eb29fb918d92516cb9" 49 | dependencies = [ 50 | "bare-metal", 51 | "bitfield 0.13.2", 52 | "embedded-hal 0.2.7", 53 | "volatile-register", 54 | ] 55 | 56 | [[package]] 57 | name = "cortex-m-rt" 58 | version = "0.7.3" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "ee84e813d593101b1723e13ec38b6ab6abbdbaaa4546553f5395ed274079ddb1" 61 | dependencies = [ 62 | "cortex-m-rt-macros", 63 | ] 64 | 65 | [[package]] 66 | name = "cortex-m-rt-macros" 67 | version = "0.7.0" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "f0f6f3e36f203cfedbc78b357fb28730aa2c6dc1ab060ee5c2405e843988d3c7" 70 | dependencies = [ 71 | "proc-macro2", 72 | "quote", 73 | "syn 1.0.109", 74 | ] 75 | 76 | [[package]] 77 | name = "crc-any" 78 | version = "2.5.0" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "a62ec9ff5f7965e4d7280bd5482acd20aadb50d632cf6c1d74493856b011fa73" 81 | dependencies = [ 82 | "debug-helper", 83 | ] 84 | 85 | [[package]] 86 | name = "critical-section" 87 | version = "1.1.2" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216" 90 | 91 | [[package]] 92 | name = "debug-helper" 93 | version = "0.3.13" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "f578e8e2c440e7297e008bb5486a3a8a194775224bbc23729b0dbdfaeebf162e" 96 | 97 | [[package]] 98 | name = "defmt" 99 | version = "0.3.8" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "a99dd22262668b887121d4672af5a64b238f026099f1a2a1b322066c9ecfe9e0" 102 | dependencies = [ 103 | "bitflags", 104 | "defmt-macros", 105 | ] 106 | 107 | [[package]] 108 | name = "defmt-macros" 109 | version = "0.3.9" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "e3a9f309eff1f79b3ebdf252954d90ae440599c26c2c553fe87a2d17195f2dcb" 112 | dependencies = [ 113 | "defmt-parser", 114 | "proc-macro-error", 115 | "proc-macro2", 116 | "quote", 117 | "syn 2.0.71", 118 | ] 119 | 120 | [[package]] 121 | name = "defmt-parser" 122 | version = "0.3.4" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "ff4a5fefe330e8d7f31b16a318f9ce81000d8e35e69b93eae154d16d2278f70f" 125 | dependencies = [ 126 | "thiserror", 127 | ] 128 | 129 | [[package]] 130 | name = "defmt-rtt" 131 | version = "0.4.1" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "bab697b3dbbc1750b7c8b821aa6f6e7f2480b47a99bc057a2ed7b170ebef0c51" 134 | dependencies = [ 135 | "critical-section", 136 | "defmt", 137 | ] 138 | 139 | [[package]] 140 | name = "either" 141 | version = "1.13.0" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 144 | 145 | [[package]] 146 | name = "embedded-dma" 147 | version = "0.2.0" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "994f7e5b5cb23521c22304927195f236813053eb9c065dd2226a32ba64695446" 150 | dependencies = [ 151 | "stable_deref_trait", 152 | ] 153 | 154 | [[package]] 155 | name = "embedded-hal" 156 | version = "0.2.7" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "35949884794ad573cf46071e41c9b60efb0cb311e3ca01f7af807af1debc66ff" 159 | dependencies = [ 160 | "nb 0.1.3", 161 | "void", 162 | ] 163 | 164 | [[package]] 165 | name = "embedded-hal" 166 | version = "1.0.0" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89" 169 | 170 | [[package]] 171 | name = "embedded-hal-async" 172 | version = "1.0.0" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "0c4c685bbef7fe13c3c6dd4da26841ed3980ef33e841cddfa15ce8a8fb3f1884" 175 | dependencies = [ 176 | "embedded-hal 1.0.0", 177 | ] 178 | 179 | [[package]] 180 | name = "embedded-hal-nb" 181 | version = "1.0.0" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "fba4268c14288c828995299e59b12babdbe170f6c6d73731af1b4648142e8605" 184 | dependencies = [ 185 | "embedded-hal 1.0.0", 186 | "nb 1.1.0", 187 | ] 188 | 189 | [[package]] 190 | name = "embedded-io" 191 | version = "0.6.1" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" 194 | 195 | [[package]] 196 | name = "embedded-io-async" 197 | version = "0.6.1" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "3ff09972d4073aa8c299395be75161d582e7629cd663171d62af73c8d50dba3f" 200 | dependencies = [ 201 | "embedded-io", 202 | ] 203 | 204 | [[package]] 205 | name = "encode_unicode" 206 | version = "0.3.6" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 209 | 210 | [[package]] 211 | name = "frunk" 212 | version = "0.4.2" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "11a351b59e12f97b4176ee78497dff72e4276fb1ceb13e19056aca7fa0206287" 215 | dependencies = [ 216 | "frunk_core", 217 | "frunk_derives", 218 | ] 219 | 220 | [[package]] 221 | name = "frunk_core" 222 | version = "0.4.2" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "af2469fab0bd07e64ccf0ad57a1438f63160c69b2e57f04a439653d68eb558d6" 225 | 226 | [[package]] 227 | name = "frunk_derives" 228 | version = "0.4.2" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "b0fa992f1656e1707946bbba340ad244f0814009ef8c0118eb7b658395f19a2e" 231 | dependencies = [ 232 | "frunk_proc_macro_helpers", 233 | "quote", 234 | "syn 2.0.71", 235 | ] 236 | 237 | [[package]] 238 | name = "frunk_proc_macro_helpers" 239 | version = "0.1.2" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "35b54add839292b743aeda6ebedbd8b11e93404f902c56223e51b9ec18a13d2c" 242 | dependencies = [ 243 | "frunk_core", 244 | "proc-macro2", 245 | "quote", 246 | "syn 2.0.71", 247 | ] 248 | 249 | [[package]] 250 | name = "fugit" 251 | version = "0.3.7" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "17186ad64927d5ac8f02c1e77ccefa08ccd9eaa314d5a4772278aa204a22f7e7" 254 | dependencies = [ 255 | "gcd", 256 | ] 257 | 258 | [[package]] 259 | name = "gcd" 260 | version = "2.3.0" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "1d758ba1b47b00caf47f24925c0074ecb20d6dfcffe7f6d53395c0465674841a" 263 | 264 | [[package]] 265 | name = "hash32" 266 | version = "0.3.1" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" 269 | dependencies = [ 270 | "byteorder", 271 | ] 272 | 273 | [[package]] 274 | name = "heapless" 275 | version = "0.8.0" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" 278 | dependencies = [ 279 | "hash32", 280 | "stable_deref_trait", 281 | ] 282 | 283 | [[package]] 284 | name = "itertools" 285 | version = "0.10.5" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 288 | dependencies = [ 289 | "either", 290 | ] 291 | 292 | [[package]] 293 | name = "nb" 294 | version = "0.1.3" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "801d31da0513b6ec5214e9bf433a77966320625a37860f910be265be6e18d06f" 297 | dependencies = [ 298 | "nb 1.1.0", 299 | ] 300 | 301 | [[package]] 302 | name = "nb" 303 | version = "1.1.0" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d" 306 | 307 | [[package]] 308 | name = "noline" 309 | version = "0.3.0" 310 | dependencies = [ 311 | "embedded-io", 312 | "embedded-io-async", 313 | "num_enum 0.7.2", 314 | ] 315 | 316 | [[package]] 317 | name = "num_enum" 318 | version = "0.5.11" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" 321 | dependencies = [ 322 | "num_enum_derive 0.5.11", 323 | ] 324 | 325 | [[package]] 326 | name = "num_enum" 327 | version = "0.7.2" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845" 330 | dependencies = [ 331 | "num_enum_derive 0.7.2", 332 | ] 333 | 334 | [[package]] 335 | name = "num_enum_derive" 336 | version = "0.5.11" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" 339 | dependencies = [ 340 | "proc-macro2", 341 | "quote", 342 | "syn 1.0.109", 343 | ] 344 | 345 | [[package]] 346 | name = "num_enum_derive" 347 | version = "0.7.2" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" 350 | dependencies = [ 351 | "proc-macro2", 352 | "quote", 353 | "syn 2.0.71", 354 | ] 355 | 356 | [[package]] 357 | name = "panic-probe" 358 | version = "0.3.2" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "4047d9235d1423d66cc97da7d07eddb54d4f154d6c13805c6d0793956f4f25b0" 361 | dependencies = [ 362 | "cortex-m", 363 | "defmt", 364 | ] 365 | 366 | [[package]] 367 | name = "paste" 368 | version = "1.0.15" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 371 | 372 | [[package]] 373 | name = "pio" 374 | version = "0.2.1" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "76e09694b50f89f302ed531c1f2a7569f0be5867aee4ab4f8f729bbeec0078e3" 377 | dependencies = [ 378 | "arrayvec", 379 | "num_enum 0.5.11", 380 | "paste", 381 | ] 382 | 383 | [[package]] 384 | name = "portable-atomic" 385 | version = "1.6.0" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" 388 | 389 | [[package]] 390 | name = "proc-macro-error" 391 | version = "1.0.4" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 394 | dependencies = [ 395 | "proc-macro-error-attr", 396 | "proc-macro2", 397 | "quote", 398 | "syn 1.0.109", 399 | "version_check", 400 | ] 401 | 402 | [[package]] 403 | name = "proc-macro-error-attr" 404 | version = "1.0.4" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 407 | dependencies = [ 408 | "proc-macro2", 409 | "quote", 410 | "version_check", 411 | ] 412 | 413 | [[package]] 414 | name = "proc-macro2" 415 | version = "1.0.86" 416 | source = "registry+https://github.com/rust-lang/crates.io-index" 417 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 418 | dependencies = [ 419 | "unicode-ident", 420 | ] 421 | 422 | [[package]] 423 | name = "quote" 424 | version = "1.0.36" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 427 | dependencies = [ 428 | "proc-macro2", 429 | ] 430 | 431 | [[package]] 432 | name = "rand_core" 433 | version = "0.6.4" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 436 | 437 | [[package]] 438 | name = "rp-pico" 439 | version = "0.9.0" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "b9342d3ac7011ac688300979e9b52a81f0add1d05feb02868cf94bfee0705b28" 442 | dependencies = [ 443 | "cortex-m-rt", 444 | "fugit", 445 | "rp2040-boot2", 446 | "rp2040-hal", 447 | "usb-device", 448 | ] 449 | 450 | [[package]] 451 | name = "rp2040" 452 | version = "0.1.0" 453 | dependencies = [ 454 | "cortex-m", 455 | "cortex-m-rt", 456 | "defmt", 457 | "defmt-rtt", 458 | "embedded-hal 1.0.0", 459 | "embedded-io", 460 | "heapless", 461 | "noline", 462 | "panic-probe", 463 | "rp-pico", 464 | "usb-device", 465 | "usbd-hid", 466 | "usbd-serial", 467 | ] 468 | 469 | [[package]] 470 | name = "rp2040-boot2" 471 | version = "0.3.0" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "7c92f344f63f950ee36cf4080050e4dce850839b9175da38f9d2ffb69b4dbb21" 474 | dependencies = [ 475 | "crc-any", 476 | ] 477 | 478 | [[package]] 479 | name = "rp2040-hal" 480 | version = "0.10.2" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "d11e711940087f2cdff8aeae9f4b902e2014c06a00b39a1092686b81ec973d6f" 483 | dependencies = [ 484 | "bitfield 0.14.0", 485 | "cortex-m", 486 | "critical-section", 487 | "embedded-dma", 488 | "embedded-hal 0.2.7", 489 | "embedded-hal 1.0.0", 490 | "embedded-hal-async", 491 | "embedded-hal-nb", 492 | "embedded-io", 493 | "frunk", 494 | "fugit", 495 | "itertools", 496 | "nb 1.1.0", 497 | "paste", 498 | "pio", 499 | "rand_core", 500 | "rp2040-hal-macros", 501 | "rp2040-pac", 502 | "usb-device", 503 | "vcell", 504 | "void", 505 | ] 506 | 507 | [[package]] 508 | name = "rp2040-hal-macros" 509 | version = "0.1.0" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | checksum = "86479063e497efe1ae81995ef9071f54fd1c7427e04d6c5b84cde545ff672a5e" 512 | dependencies = [ 513 | "cortex-m-rt", 514 | "proc-macro2", 515 | "quote", 516 | "syn 1.0.109", 517 | ] 518 | 519 | [[package]] 520 | name = "rp2040-pac" 521 | version = "0.6.0" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "83cbcd3f7a0ca7bbe61dc4eb7e202842bee4e27b769a7bf3a4a72fa399d6e404" 524 | dependencies = [ 525 | "cortex-m", 526 | "cortex-m-rt", 527 | "critical-section", 528 | "vcell", 529 | ] 530 | 531 | [[package]] 532 | name = "rustc_version" 533 | version = "0.2.3" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" 536 | dependencies = [ 537 | "semver", 538 | ] 539 | 540 | [[package]] 541 | name = "semver" 542 | version = "0.9.0" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" 545 | dependencies = [ 546 | "semver-parser", 547 | ] 548 | 549 | [[package]] 550 | name = "semver-parser" 551 | version = "0.7.0" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" 554 | 555 | [[package]] 556 | name = "serde" 557 | version = "1.0.204" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" 560 | dependencies = [ 561 | "serde_derive", 562 | ] 563 | 564 | [[package]] 565 | name = "serde_derive" 566 | version = "1.0.204" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" 569 | dependencies = [ 570 | "proc-macro2", 571 | "quote", 572 | "syn 2.0.71", 573 | ] 574 | 575 | [[package]] 576 | name = "ssmarshal" 577 | version = "1.0.0" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "f3e6ad23b128192ed337dfa4f1b8099ced0c2bf30d61e551b65fda5916dbb850" 580 | dependencies = [ 581 | "encode_unicode", 582 | "serde", 583 | ] 584 | 585 | [[package]] 586 | name = "stable_deref_trait" 587 | version = "1.2.0" 588 | source = "registry+https://github.com/rust-lang/crates.io-index" 589 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 590 | 591 | [[package]] 592 | name = "syn" 593 | version = "1.0.109" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 596 | dependencies = [ 597 | "proc-macro2", 598 | "quote", 599 | "unicode-ident", 600 | ] 601 | 602 | [[package]] 603 | name = "syn" 604 | version = "2.0.71" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462" 607 | dependencies = [ 608 | "proc-macro2", 609 | "quote", 610 | "unicode-ident", 611 | ] 612 | 613 | [[package]] 614 | name = "thiserror" 615 | version = "1.0.62" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb" 618 | dependencies = [ 619 | "thiserror-impl", 620 | ] 621 | 622 | [[package]] 623 | name = "thiserror-impl" 624 | version = "1.0.62" 625 | source = "registry+https://github.com/rust-lang/crates.io-index" 626 | checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c" 627 | dependencies = [ 628 | "proc-macro2", 629 | "quote", 630 | "syn 2.0.71", 631 | ] 632 | 633 | [[package]] 634 | name = "unicode-ident" 635 | version = "1.0.12" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 638 | 639 | [[package]] 640 | name = "usb-device" 641 | version = "0.3.2" 642 | source = "registry+https://github.com/rust-lang/crates.io-index" 643 | checksum = "98816b1accafbb09085168b90f27e93d790b4bfa19d883466b5e53315b5f06a6" 644 | dependencies = [ 645 | "heapless", 646 | "portable-atomic", 647 | ] 648 | 649 | [[package]] 650 | name = "usbd-hid" 651 | version = "0.7.0" 652 | source = "registry+https://github.com/rust-lang/crates.io-index" 653 | checksum = "41a2d4546ca3e6a5c6a85584e5caf29feabf3ec35d6cd6b772eb35bd3cff7256" 654 | dependencies = [ 655 | "serde", 656 | "ssmarshal", 657 | "usb-device", 658 | "usbd-hid-macros", 659 | ] 660 | 661 | [[package]] 662 | name = "usbd-hid-descriptors" 663 | version = "0.1.2" 664 | source = "registry+https://github.com/rust-lang/crates.io-index" 665 | checksum = "dcbee8c6735e90894fba04770bc41e11fd3c5256018856e15dc4dd1e6c8a3dd1" 666 | dependencies = [ 667 | "bitfield 0.13.2", 668 | ] 669 | 670 | [[package]] 671 | name = "usbd-hid-macros" 672 | version = "0.6.0" 673 | source = "registry+https://github.com/rust-lang/crates.io-index" 674 | checksum = "261079a9ada015fa1acac7cc73c98559f3a92585e15f508034beccf6a2ab75a2" 675 | dependencies = [ 676 | "byteorder", 677 | "proc-macro2", 678 | "quote", 679 | "serde", 680 | "syn 1.0.109", 681 | "usbd-hid-descriptors", 682 | ] 683 | 684 | [[package]] 685 | name = "usbd-serial" 686 | version = "0.2.2" 687 | source = "registry+https://github.com/rust-lang/crates.io-index" 688 | checksum = "065e4eaf93db81d5adac82d9cef8f8da314cb640fa7f89534b972383f1cf80fc" 689 | dependencies = [ 690 | "embedded-hal 0.2.7", 691 | "embedded-io", 692 | "nb 1.1.0", 693 | "usb-device", 694 | ] 695 | 696 | [[package]] 697 | name = "vcell" 698 | version = "0.1.3" 699 | source = "registry+https://github.com/rust-lang/crates.io-index" 700 | checksum = "77439c1b53d2303b20d9459b1ade71a83c716e3f9c34f3228c00e6f185d6c002" 701 | 702 | [[package]] 703 | name = "version_check" 704 | version = "0.9.4" 705 | source = "registry+https://github.com/rust-lang/crates.io-index" 706 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 707 | 708 | [[package]] 709 | name = "void" 710 | version = "1.0.2" 711 | source = "registry+https://github.com/rust-lang/crates.io-index" 712 | checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" 713 | 714 | [[package]] 715 | name = "volatile-register" 716 | version = "0.2.2" 717 | source = "registry+https://github.com/rust-lang/crates.io-index" 718 | checksum = "de437e2a6208b014ab52972a27e59b33fa2920d3e00fe05026167a1c509d19cc" 719 | dependencies = [ 720 | "vcell", 721 | ] 722 | -------------------------------------------------------------------------------- /examples/no_std/rp2040/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rp2040" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | cortex-m = "0.7.7" 8 | cortex-m-rt = "0.7.3" 9 | embedded-hal = "1.0.0" 10 | embedded-io = "0.6.1" 11 | rp-pico = "0.9.0" 12 | usb-device = "0.3.2" 13 | usbd-hid = "0.7.0" 14 | usbd-serial = "0.2.2" 15 | heapless = "0.8.0" 16 | defmt = "0.3" 17 | defmt-rtt = "0.4.1" 18 | panic-probe = { version = "0.3.2", features = ["print-defmt"] } 19 | 20 | noline = { path = "../../../noline" } 21 | -------------------------------------------------------------------------------- /examples/no_std/rp2040/build.rs: -------------------------------------------------------------------------------- 1 | //! This build script copies the `memory.x` file from the crate root into 2 | //! a directory where the linker can always find it at build time. 3 | //! For many projects this is optional, as the linker always searches the 4 | //! project root directory -- wherever `Cargo.toml` is. However, if you 5 | //! are using a workspace or have a more complicated build setup, this 6 | //! build script becomes required. Additionally, by requesting that 7 | //! Cargo re-run the build script whenever `memory.x` is changed, 8 | //! updating `memory.x` ensures a rebuild of the application with the 9 | //! new memory settings. 10 | 11 | use std::env; 12 | use std::fs::File; 13 | use std::io::Write; 14 | use std::path::PathBuf; 15 | 16 | fn main() { 17 | // Put `memory.x` in our output directory and ensure it's 18 | // on the linker search path. 19 | let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); 20 | File::create(out.join("memory.x")) 21 | .unwrap() 22 | .write_all(include_bytes!("memory.x")) 23 | .unwrap(); 24 | println!("cargo:rustc-link-search={}", out.display()); 25 | 26 | // By default, Cargo will re-run a build script whenever 27 | // any file in the project changes. By specifying `memory.x` 28 | // here, we ensure the build script is only re-run when 29 | // `memory.x` is changed. 30 | println!("cargo:rerun-if-changed=memory.x"); 31 | } 32 | -------------------------------------------------------------------------------- /examples/no_std/rp2040/memory.x: -------------------------------------------------------------------------------- 1 | MEMORY { 2 | BOOT2 : ORIGIN = 0x10000000, LENGTH = 0x100 3 | FLASH : ORIGIN = 0x10000100, LENGTH = 2048K - 0x100 4 | RAM : ORIGIN = 0x20000000, LENGTH = 256K 5 | } 6 | 7 | EXTERN(BOOT2_FIRMWARE) 8 | 9 | SECTIONS { 10 | /* ### Boot loader */ 11 | .boot2 ORIGIN(BOOT2) : 12 | { 13 | KEEP(*(.boot2)); 14 | } > BOOT2 15 | } INSERT BEFORE .text; -------------------------------------------------------------------------------- /examples/no_std/rp2040/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | 4 | use embedded_io::{ErrorKind, ErrorType, Read, ReadReady, Write, WriteReady}; 5 | use rp_pico as bsp; 6 | 7 | use bsp::entry; 8 | use defmt::*; 9 | use {defmt_rtt as _, panic_probe as _}; 10 | 11 | use bsp::hal::{clocks::init_clocks_and_plls, pac, usb::UsbBus, watchdog::Watchdog}; 12 | 13 | use core::fmt::Write as FmtWrite; 14 | 15 | use noline::builder::EditorBuilder; 16 | use noline::error::NolineError; 17 | 18 | use usb_device::bus::UsbBusAllocator; 19 | use usb_device::prelude::*; 20 | use usbd_serial::{DefaultBufferStore, SerialPort, USB_CLASS_CDC}; 21 | 22 | type SP<'a> = SerialPort<'a, UsbBus, DefaultBufferStore, DefaultBufferStore>; 23 | 24 | struct SerialWrapper<'a> { 25 | device: UsbDevice<'a, UsbBus>, 26 | serial: SP<'a>, 27 | ready: bool, 28 | } 29 | 30 | impl<'a> SerialWrapper<'a> { 31 | fn new(device: UsbDevice<'a, UsbBus>, serial: SP<'a>) -> Self { 32 | Self { 33 | device, 34 | serial, 35 | ready: false, 36 | } 37 | } 38 | 39 | fn poll(&mut self) -> bool { 40 | self.device.poll(&mut [&mut self.serial]) 41 | } 42 | 43 | fn is_ready(&mut self) -> bool { 44 | self.ready = self.ready | self.poll(); 45 | self.ready 46 | } 47 | } 48 | 49 | #[derive(Debug)] 50 | struct Error(UsbError); 51 | 52 | impl From for Error { 53 | fn from(value: UsbError) -> Self { 54 | Self(value) 55 | } 56 | } 57 | 58 | impl embedded_io::Error for Error { 59 | fn kind(&self) -> ErrorKind { 60 | ErrorKind::Other 61 | } 62 | } 63 | 64 | impl<'a> ErrorType for SerialWrapper<'a> { 65 | type Error = Error; 66 | } 67 | 68 | impl<'a> ReadReady for SerialWrapper<'a> { 69 | fn read_ready(&mut self) -> Result { 70 | Ok(self.is_ready() | self.serial.dtr() | self.serial.rts()) 71 | } 72 | } 73 | 74 | impl<'a> Read for SerialWrapper<'a> { 75 | fn read(&mut self, buf: &mut [u8]) -> Result { 76 | loop { 77 | while !self.read_ready()? { 78 | continue; 79 | } 80 | 81 | let res = self.serial.read(buf); 82 | if res == Err(UsbError::WouldBlock) { 83 | self.ready = false; 84 | continue; 85 | } 86 | 87 | break Ok(res?); 88 | } 89 | } 90 | } 91 | 92 | impl<'a> WriteReady for SerialWrapper<'a> { 93 | fn write_ready(&mut self) -> Result { 94 | Ok(self.is_ready() | self.serial.dtr() | self.serial.rts()) 95 | } 96 | } 97 | impl<'a> Write for SerialWrapper<'a> { 98 | fn write(&mut self, buf: &[u8]) -> Result { 99 | loop { 100 | while !self.write_ready()? { 101 | continue; 102 | } 103 | 104 | let res = self.serial.write(buf); 105 | if res == Err(UsbError::WouldBlock) { 106 | self.ready = false; 107 | continue; 108 | } 109 | 110 | break Ok(res?); 111 | } 112 | } 113 | 114 | fn flush(&mut self) -> Result<(), Self::Error> { 115 | Ok(self.serial.flush()?) 116 | } 117 | } 118 | 119 | #[entry] 120 | fn main() -> ! { 121 | info!("Starting..."); 122 | 123 | // Grab our singleton objects 124 | let mut pac = pac::Peripherals::take().unwrap(); 125 | 126 | // Set up the watchdog driver - needed by the clock setup code 127 | let mut watchdog = Watchdog::new(pac.WATCHDOG); 128 | 129 | // Configure the clocks 130 | // 131 | // The default is to generate a 125 MHz system clock 132 | let clocks = init_clocks_and_plls( 133 | rp_pico::XOSC_CRYSTAL_FREQ, 134 | pac.XOSC, 135 | pac.CLOCKS, 136 | pac.PLL_SYS, 137 | pac.PLL_USB, 138 | &mut pac.RESETS, 139 | &mut watchdog, 140 | ) 141 | .ok() 142 | .unwrap(); 143 | 144 | // Set up the USB driver 145 | let usb_bus = UsbBusAllocator::new(UsbBus::new( 146 | pac.USBCTRL_REGS, 147 | pac.USBCTRL_DPRAM, 148 | clocks.usb_clock, 149 | true, 150 | &mut pac.RESETS, 151 | )); 152 | 153 | // Set up the USB Communications Class Device driver 154 | let serial = SerialPort::new(&usb_bus); 155 | 156 | // Create a USB device with a fake VID and PID 157 | let usb_dev = UsbDeviceBuilder::new(&usb_bus, UsbVidPid(0x16c0, 0x27dd)) 158 | .strings(&[StringDescriptors::default() 159 | .manufacturer("Fake company") 160 | .product("Serial port") 161 | .serial_number("TEST")]) 162 | .unwrap() 163 | .device_class(USB_CLASS_CDC) 164 | .build(); 165 | 166 | let prompt = "> "; 167 | 168 | let mut io = SerialWrapper::new(usb_dev, serial); 169 | 170 | info!("Waiting for connection"); 171 | 172 | let mut buffer = [0; 128]; 173 | let mut history = [0; 128]; 174 | let mut editor = EditorBuilder::from_slice(&mut buffer) 175 | .with_slice_history(&mut history) 176 | .build_sync(&mut io) 177 | .unwrap(); 178 | 179 | loop { 180 | match editor.readline(prompt, &mut io) { 181 | Ok(s) => { 182 | if s.len() > 0 { 183 | writeln!(io, "Echo: {}\r", s).unwrap(); 184 | } else { 185 | // Writing emtpy slice causes panic 186 | writeln!(io, "Echo: \r").unwrap(); 187 | } 188 | } 189 | Err(err) => { 190 | let error = match err { 191 | NolineError::IoError(_) => "IoError", 192 | NolineError::ParserError => "ParserError", 193 | NolineError::Aborted => "Aborted", 194 | }; 195 | writeln!(io, "Error: {}\r", error).unwrap(); 196 | } 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /examples/std/.projectile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustne-kretser/noline/3bfe51d23ef250915ea8c85548a71128478a5f57/examples/std/.projectile -------------------------------------------------------------------------------- /examples/std/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "noline-example" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | tokio = { version = "1.38.0", features = [ 10 | "full", 11 | "io-util", 12 | "sync", 13 | "rt", 14 | "macros", 15 | "io-std", 16 | ] } 17 | noline = { path = "../../noline", features = ["std"] } 18 | heapless = "0.8.0" 19 | termion = "4.0.0" 20 | embedded-io-async = "0.6.1" 21 | embedded-io = "0.6.1" 22 | 23 | [[bin]] 24 | name = "std-sync" 25 | 26 | [[bin]] 27 | name = "std-async-tokio" 28 | -------------------------------------------------------------------------------- /examples/std/src/bin/std-async-tokio.rs: -------------------------------------------------------------------------------- 1 | use embedded_io_async::Write; 2 | use noline::builder::EditorBuilder; 3 | use termion::raw::IntoRawMode; 4 | 5 | use tokio::io; 6 | use tokio::io::{AsyncReadExt, AsyncWriteExt}; 7 | 8 | pub struct IOWrapper { 9 | stdin: io::Stdin, 10 | stdout: io::Stdout, 11 | } 12 | impl IOWrapper { 13 | pub fn new() -> Self { 14 | Self { 15 | stdin: io::stdin(), 16 | stdout: io::stdout(), 17 | } 18 | } 19 | } 20 | 21 | impl embedded_io_async::ErrorType for IOWrapper { 22 | type Error = embedded_io_async::ErrorKind; 23 | } 24 | 25 | impl embedded_io_async::Read for IOWrapper { 26 | async fn read(&mut self, buf: &mut [u8]) -> Result { 27 | self.stdin 28 | .read(buf) 29 | .await 30 | .map_err(|e| Self::Error::from(e.kind())) 31 | } 32 | } 33 | 34 | impl embedded_io_async::Write for IOWrapper { 35 | async fn write(&mut self, buf: &[u8]) -> Result { 36 | self.stdout.write(buf).await.map_err(|e| e.kind().into()) 37 | } 38 | async fn flush(&mut self) -> Result<(), Self::Error> { 39 | self.stdout.flush().await.map_err(|e| e.kind().into()) 40 | } 41 | } 42 | 43 | #[tokio::main(flavor = "current_thread")] 44 | async fn main() { 45 | let term_task = tokio::spawn(async { 46 | let _raw_term = std::io::stdout().into_raw_mode().unwrap(); 47 | let mut io = IOWrapper::new(); 48 | 49 | let prompt = "> "; 50 | 51 | let mut editor = EditorBuilder::new_unbounded() 52 | .with_unbounded_history() 53 | .build_async(&mut io) 54 | .await 55 | .unwrap(); 56 | 57 | while let Ok(line) = editor.readline(prompt, &mut io).await { 58 | let s = format!("Read: '{}'\n\r", line); 59 | io.stdout.write_all(s.as_bytes()).await.unwrap(); 60 | } 61 | }); 62 | 63 | match term_task.await { 64 | Ok(_) => (), 65 | Err(e) => eprintln!("Error: {:?}", e), 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /examples/std/src/bin/std-sync.rs: -------------------------------------------------------------------------------- 1 | use noline::builder::EditorBuilder; 2 | use std::io; 3 | use termion::raw::IntoRawMode; 4 | 5 | use embedded_io::{ErrorType, Read as EmbRead, Write as EmbWrite}; 6 | use std::io::{Read, Stdin, Stdout, Write}; 7 | 8 | pub struct IOWrapper { 9 | stdin: Stdin, 10 | stdout: Stdout, 11 | } 12 | 13 | impl IOWrapper { 14 | pub fn new() -> Self { 15 | Self { 16 | stdin: std::io::stdin(), 17 | stdout: std::io::stdout(), 18 | } 19 | } 20 | } 21 | 22 | impl Default for IOWrapper { 23 | fn default() -> Self { 24 | Self::new() 25 | } 26 | } 27 | 28 | impl ErrorType for IOWrapper { 29 | type Error = embedded_io::ErrorKind; 30 | } 31 | 32 | impl EmbRead for IOWrapper { 33 | fn read(&mut self, buf: &mut [u8]) -> Result { 34 | Ok(self.stdin.read(buf).map_err(|e| e.kind())?) 35 | } 36 | } 37 | 38 | impl EmbWrite for IOWrapper { 39 | fn write(&mut self, buf: &[u8]) -> Result { 40 | Ok(self.stdout.write(buf).map_err(|e| e.kind())?) 41 | } 42 | fn flush(&mut self) -> Result<(), Self::Error> { 43 | Ok(self.stdout.flush().map_err(|e| e.kind())?) 44 | } 45 | } 46 | 47 | fn main() { 48 | let _stdout = io::stdout().into_raw_mode().unwrap(); 49 | let prompt = "> "; 50 | 51 | let mut io = IOWrapper::new(); 52 | 53 | let mut editor = EditorBuilder::new_unbounded() 54 | .with_unbounded_history() 55 | .build_sync(&mut io) 56 | .unwrap(); 57 | 58 | while let Ok(line) = editor.readline(prompt, &mut io) { 59 | writeln!(io, "Read: '{}'", line).unwrap(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /noline/.projectile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustne-kretser/noline/3bfe51d23ef250915ea8c85548a71128478a5f57/noline/.projectile -------------------------------------------------------------------------------- /noline/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "noline" 3 | version = "0.5.1" 4 | edition = "2021" 5 | authors = ["Eivind Alexander Bergem "] 6 | license = "MPL-2.0" 7 | description = "A no_std line editor" 8 | repository = "https://github.com/rustne-kretser/noline" 9 | readme = "../README.md" 10 | categories = ["no-std", "command-line-interface"] 11 | keywords = ["no_std", "readline"] 12 | include = ["**/*.rs", "Cargo.toml"] 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [dependencies] 17 | embedded-io = "0.6.1" 18 | embedded-io-async = "0.6.1" 19 | num_enum = { version = "0.7.2", default-features = false } 20 | 21 | 22 | [features] 23 | default = [] 24 | std = ["embedded-io/std", "embedded-io-async/std"] 25 | alloc = [] 26 | 27 | [dev-dependencies] 28 | crossbeam = "0.8.1" 29 | termion = "4.0.0" 30 | 31 | [package.metadata.docs.rs] 32 | all-features = true 33 | -------------------------------------------------------------------------------- /noline/README.tpl: -------------------------------------------------------------------------------- 1 | ![Pipeline](https://github.com/rustne-kretser/noline/actions/workflows/rust.yml/badge.svg) 2 | [![Crates.io](https://img.shields.io/crates/v/noline.svg)](https://crates.io/crates/noline) 3 | [![API reference](https://docs.rs/noline/badge.svg)](https://docs.rs/noline/) 4 | 5 | # {{crate}} 6 | 7 | {{readme}} 8 | 9 | For more details, see [docs](https://docs.rs/noline/). 10 | 11 | # Usage 12 | 13 | Add this to your Cargo.toml: 14 | 15 | ```toml 16 | [dependencies] 17 | noline = "{{version}}" 18 | ``` 19 | 20 | # License 21 | 22 | {{license}} 23 | -------------------------------------------------------------------------------- /noline/src/async_editor.rs: -------------------------------------------------------------------------------- 1 | //! Line editor for async IO 2 | 3 | //! Implementation for async Editor 4 | 5 | use embedded_io_async::ReadExactError; 6 | 7 | use crate::{ 8 | core::{Line, Prompt}, 9 | error::NolineError, 10 | history::{get_history_entries, CircularSlice, History}, 11 | line_buffer::{Buffer, LineBuffer}, 12 | output::{Output, OutputItem}, 13 | terminal::Terminal, 14 | }; 15 | 16 | /// Line editor for async IO 17 | /// 18 | /// It is recommended to use [`crate::builder::EditorBuilder`] to build an editor. 19 | pub struct Editor { 20 | buffer: LineBuffer, 21 | terminal: Terminal, 22 | history: H, 23 | } 24 | 25 | impl Editor 26 | where 27 | B: Buffer, 28 | H: History, 29 | { 30 | /// Create and initialize line editor 31 | pub async fn new( 32 | buffer: LineBuffer, 33 | history: H, 34 | _io: &mut IO, 35 | ) -> Result { 36 | let terminal = Terminal::default(); 37 | 38 | Ok(Self { 39 | buffer, 40 | terminal, 41 | history, 42 | }) 43 | } 44 | 45 | async fn handle_output<'b, 'item, IO, I>( 46 | output: Output<'b, B, I>, 47 | io: &mut IO, 48 | ) -> Result, NolineError> 49 | where 50 | IO: embedded_io_async::Read + embedded_io_async::Write, 51 | I: Iterator + Clone, 52 | { 53 | for item in output { 54 | if let Some(bytes) = item.get_bytes() { 55 | io.write(bytes).await?; 56 | } 57 | 58 | io.flush().await?; 59 | 60 | match item { 61 | OutputItem::EndOfString => return Ok(Some(())), 62 | OutputItem::Abort => return Err(NolineError::Aborted), 63 | _ => (), 64 | } 65 | } 66 | 67 | Ok(None) 68 | } 69 | 70 | async fn read_byte(io: &mut IO) -> Result 71 | where 72 | IO: embedded_io_async::Read + embedded_io_async::Write, 73 | { 74 | let mut buf = [0x8; 1]; 75 | 76 | match io.read_exact(&mut buf).await { 77 | Ok(_) => Ok(buf[0]), 78 | Err(err) => match err { 79 | ReadExactError::UnexpectedEof => Err(NolineError::Aborted), 80 | ReadExactError::Other(err) => Err(err)?, 81 | }, 82 | } 83 | } 84 | 85 | /// Read line from `stdin` 86 | pub async fn readline<'b, 'item, IO, I>( 87 | &'b mut self, 88 | prompt: impl Into>, 89 | io: &mut IO, 90 | ) -> Result<&'b str, NolineError> 91 | where 92 | IO: embedded_io_async::Read + embedded_io_async::Write, 93 | I: Iterator + Clone, 94 | { 95 | let mut line = Line::new( 96 | prompt, 97 | &mut self.buffer, 98 | &mut self.terminal, 99 | &mut self.history, 100 | ); 101 | 102 | let mut reset = line.reset(); 103 | 104 | Self::handle_output(reset.start(), io).await?; 105 | 106 | loop { 107 | let byte = Self::read_byte(io).await?; 108 | 109 | if let Some(output) = reset.advance(byte) { 110 | Self::handle_output(output, io).await?; 111 | } else { 112 | break; 113 | } 114 | } 115 | 116 | loop { 117 | let byte = Self::read_byte(io).await?; 118 | 119 | if Self::handle_output(line.advance(byte), io).await?.is_some() { 120 | break; 121 | } 122 | } 123 | 124 | Ok(self.buffer.as_str()) 125 | } 126 | 127 | /// Load history from iterator 128 | pub fn load_history<'a>(&mut self, entries: impl Iterator) -> usize { 129 | self.history.load_entries(entries) 130 | } 131 | 132 | /// Get history as iterator over circular slices 133 | pub fn get_history(&self) -> impl Iterator> { 134 | get_history_entries(&self.history) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /noline/src/builder.rs: -------------------------------------------------------------------------------- 1 | //! Builder for editors 2 | 3 | use core::marker::PhantomData; 4 | 5 | use crate::{ 6 | async_editor, 7 | error::NolineError, 8 | history::{History, NoHistory, SliceHistory}, 9 | line_buffer::{Buffer, LineBuffer, NoBuffer, SliceBuffer}, 10 | sync_editor, 11 | }; 12 | 13 | #[cfg(any(test, doc, feature = "alloc", feature = "std"))] 14 | use crate::{history::UnboundedHistory, line_buffer::UnboundedBuffer}; 15 | 16 | /// Builder for [`sync_editor::Editor`] and [`async_editor::Editor`]. 17 | /// 18 | /// # Example 19 | /// ```no_run 20 | /// # use embedded_io::{Read, Write, ErrorType}; 21 | /// # use core::convert::Infallible; 22 | /// # struct MyIO {} 23 | /// # impl ErrorType for MyIO { 24 | /// # type Error = Infallible; 25 | /// # } 26 | /// # impl embedded_io::Write for MyIO { 27 | /// # fn write(&mut self, buf: &[u8]) -> Result { unimplemented!() } 28 | /// # fn flush(&mut self) -> Result<(), Self::Error> { unimplemented!() } 29 | /// # } 30 | /// # impl embedded_io::Read for MyIO { 31 | /// # fn read(&mut self, buf: &mut[u8]) -> Result { unimplemented!() } 32 | /// # } 33 | /// # let mut io = MyIO {}; 34 | /// use noline::builder::EditorBuilder; 35 | /// 36 | /// let mut buffer = [0; 100]; 37 | /// let mut history = [0; 200]; 38 | /// let mut editor = EditorBuilder::from_slice(&mut buffer) 39 | /// .with_slice_history(&mut history) 40 | /// .build_sync(&mut io) 41 | /// .unwrap(); 42 | /// ``` 43 | pub struct EditorBuilder { 44 | line_buffer: LineBuffer, 45 | history: H, 46 | _marker: PhantomData<(B, H)>, 47 | } 48 | 49 | impl EditorBuilder { 50 | /// Create builder for editor with static buffer 51 | /// 52 | /// # Example 53 | /// ``` 54 | /// use noline::builder::EditorBuilder; 55 | /// 56 | /// let mut buffer = [0; 100]; 57 | /// let builder = EditorBuilder::from_slice(&mut buffer); 58 | /// ``` 59 | pub fn from_slice(buffer: &mut [u8]) -> EditorBuilder, NoHistory> { 60 | EditorBuilder { 61 | line_buffer: LineBuffer::from_slice(buffer), 62 | history: NoHistory {}, 63 | _marker: PhantomData, 64 | } 65 | } 66 | 67 | #[cfg(any(test, doc, feature = "alloc", feature = "std"))] 68 | /// Create builder for editor with unbounded buffer 69 | /// 70 | /// # Example 71 | /// ``` 72 | /// use noline::builder::EditorBuilder; 73 | /// 74 | /// let builder = EditorBuilder::new_unbounded(); 75 | /// ``` 76 | pub fn new_unbounded() -> EditorBuilder { 77 | EditorBuilder { 78 | line_buffer: LineBuffer::new_unbounded(), 79 | history: NoHistory {}, 80 | _marker: PhantomData, 81 | } 82 | } 83 | } 84 | 85 | impl EditorBuilder { 86 | /// Add static history 87 | pub fn with_slice_history(self, buffer: &mut [u8]) -> EditorBuilder> { 88 | EditorBuilder { 89 | line_buffer: self.line_buffer, 90 | history: SliceHistory::new(buffer), 91 | _marker: PhantomData, 92 | } 93 | } 94 | 95 | #[cfg(any(test, feature = "alloc", feature = "std"))] 96 | /// Add unbounded history 97 | pub fn with_unbounded_history(self) -> EditorBuilder { 98 | EditorBuilder { 99 | line_buffer: self.line_buffer, 100 | history: UnboundedHistory::new(), 101 | _marker: PhantomData, 102 | } 103 | } 104 | 105 | /// Build [`sync_editor::Editor`]. Is equivalent of calling [`sync_editor::Editor::new()`]. 106 | pub fn build_sync( 107 | self, 108 | io: &mut IO, 109 | ) -> Result, NolineError> { 110 | sync_editor::Editor::new(self.line_buffer, self.history, io) 111 | } 112 | 113 | /// Build [`async_editor::Editor`]. Is equivalent of calling [`async_editor::Editor::new()`]. 114 | pub async fn build_async( 115 | self, 116 | io: &mut IO, 117 | ) -> Result, NolineError> { 118 | async_editor::Editor::new(self.line_buffer, self.history, io).await 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /noline/src/error.rs: -------------------------------------------------------------------------------- 1 | //! Error types 2 | 3 | /// Enum to hold various error types 4 | #[derive(Debug)] 5 | pub enum NolineError { 6 | ParserError, 7 | Aborted, 8 | IoError(embedded_io::ErrorKind), 9 | } 10 | -------------------------------------------------------------------------------- /noline/src/history.rs: -------------------------------------------------------------------------------- 1 | //! Line history 2 | 3 | use core::{ 4 | iter::{Chain, Zip}, 5 | ops::Range, 6 | slice, 7 | }; 8 | 9 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] 10 | #[cfg_attr(test, derive(Debug))] 11 | struct CircularIndex { 12 | index: usize, 13 | size: usize, 14 | } 15 | 16 | impl CircularIndex { 17 | fn new(index: usize, size: usize) -> Self { 18 | Self { index, size } 19 | } 20 | 21 | fn set(&mut self, index: usize) { 22 | self.index = index; 23 | } 24 | 25 | fn add(&mut self, value: usize) { 26 | self.set(self.index + value); 27 | } 28 | 29 | fn increment(&mut self) { 30 | self.add(1); 31 | } 32 | 33 | fn index(&self) -> usize { 34 | self.index % self.size 35 | } 36 | 37 | fn diff(&self, other: CircularIndex) -> isize { 38 | self.index as isize - other.index as isize 39 | } 40 | } 41 | 42 | struct Window { 43 | size: usize, 44 | start: CircularIndex, 45 | end: CircularIndex, 46 | } 47 | 48 | impl Window { 49 | fn new(size: usize) -> Self { 50 | let start = CircularIndex::new(0, size); 51 | let end = CircularIndex::new(0, size); 52 | Self { size, start, end } 53 | } 54 | 55 | fn len(&self) -> usize { 56 | self.end.diff(self.start) as usize 57 | } 58 | 59 | fn widen(&mut self) { 60 | self.end.increment(); 61 | 62 | if self.end.diff(self.start) as usize > self.size { 63 | self.start.increment(); 64 | } 65 | } 66 | 67 | fn narrow(&mut self) { 68 | if self.end.diff(self.start) > 0 { 69 | self.start.increment(); 70 | } 71 | } 72 | 73 | fn start(&self) -> usize { 74 | self.start.index() 75 | } 76 | 77 | fn end(&self) -> usize { 78 | self.end.index() 79 | } 80 | } 81 | 82 | #[cfg_attr(test, derive(Debug))] 83 | enum CircularRange { 84 | Consecutive(Range), 85 | Split(Range, Range), 86 | } 87 | 88 | impl CircularRange { 89 | fn new(start: usize, end: usize, len: usize, capacity: usize) -> Self { 90 | assert!(start <= capacity); 91 | assert!(end <= capacity); 92 | 93 | if len > 0 { 94 | if start < end { 95 | Self::Consecutive(start..end) 96 | } else { 97 | Self::Split(start..capacity, 0..end) 98 | } 99 | } else { 100 | Self::Consecutive(start..end) 101 | } 102 | } 103 | 104 | pub fn get_ranges(&self) -> (Range, Range) { 105 | match self { 106 | CircularRange::Consecutive(range) => (range.clone(), 0..0), 107 | CircularRange::Split(range1, range2) => (range1.clone(), range2.clone()), 108 | } 109 | } 110 | } 111 | 112 | impl IntoIterator for CircularRange { 113 | type Item = usize; 114 | 115 | type IntoIter = Chain, Range>; 116 | 117 | fn into_iter(self) -> Self::IntoIter { 118 | let (range1, range2) = self.get_ranges(); 119 | 120 | range1.chain(range2) 121 | } 122 | } 123 | 124 | /// Slice of a circular buffer 125 | /// 126 | /// Consists of two separate consecutive slices if the circular slice 127 | /// wraps around. 128 | pub struct CircularSlice<'a> { 129 | buffer: &'a [u8], 130 | range: CircularRange, 131 | } 132 | 133 | impl<'a> CircularSlice<'a> { 134 | fn new(buffer: &'a [u8], start: usize, end: usize, len: usize) -> Self { 135 | Self::from_range(buffer, CircularRange::new(start, end, len, buffer.len())) 136 | } 137 | 138 | fn from_range(buffer: &'a [u8], range: CircularRange) -> Self { 139 | Self { buffer, range } 140 | } 141 | 142 | pub(crate) fn get_ranges(&self) -> (Range, Range) { 143 | self.range.get_ranges() 144 | } 145 | 146 | pub(crate) fn get_slices(&self) -> (&'a [u8], &'a [u8]) { 147 | let (range1, range2) = self.get_ranges(); 148 | 149 | (&self.buffer[range1], &self.buffer[range2]) 150 | } 151 | } 152 | 153 | impl<'a> IntoIterator for CircularSlice<'a> { 154 | type Item = (usize, &'a u8); 155 | 156 | type IntoIter = 157 | Chain, slice::Iter<'a, u8>>, Zip, slice::Iter<'a, u8>>>; 158 | 159 | fn into_iter(self) -> Self::IntoIter { 160 | let (range1, range2) = self.get_ranges(); 161 | let (slice1, slice2) = self.get_slices(); 162 | 163 | range1.zip(slice1.iter()).chain(range2.zip(slice2.iter())) 164 | } 165 | } 166 | 167 | /// Trait for line history 168 | pub trait History { 169 | /// Return entry at index, or None if out of bounds 170 | fn get_entry(&self, index: usize) -> Option>; 171 | 172 | /// Add new entry at the end 173 | fn add_entry<'a>(&mut self, entry: &'a str) -> Result<(), &'a str>; 174 | 175 | /// Return number of entries in history 176 | fn number_of_entries(&self) -> usize; 177 | 178 | /// Add entries from an iterator 179 | fn load_entries<'a, I: Iterator>(&mut self, entries: I) -> usize { 180 | entries 181 | .take_while(|entry| self.add_entry(entry).is_ok()) 182 | .count() 183 | } 184 | } 185 | 186 | /// Return an iterator over history entries 187 | /// 188 | /// # Note 189 | /// 190 | /// This should ideally be in the [`History`] trait, but is 191 | /// until `type_alias_impl_trait` is stable. 192 | pub(crate) fn get_history_entries( 193 | history: &H, 194 | ) -> impl Iterator> { 195 | (0..(history.number_of_entries())).filter_map(|index| history.get_entry(index)) 196 | } 197 | 198 | /// Static history backed by array 199 | pub struct SliceHistory<'a> { 200 | buffer: &'a mut [u8], 201 | window: Window, 202 | } 203 | 204 | impl<'a> SliceHistory<'a> { 205 | /// Create new static history 206 | pub fn new(buffer: &'a mut [u8]) -> Self { 207 | Self { 208 | window: Window::new(buffer.len()), 209 | buffer, 210 | } 211 | } 212 | 213 | fn get_available_range(&self) -> CircularRange { 214 | let len = self.buffer.len(); 215 | CircularRange::new(self.window.end(), self.window.end(), len, len) 216 | } 217 | 218 | fn get_buffer(&self) -> CircularSlice<'_> { 219 | CircularSlice::new( 220 | self.buffer, 221 | self.window.start(), 222 | self.window.end(), 223 | self.window.len(), 224 | ) 225 | } 226 | 227 | fn get_entry_ranges(&self) -> impl Iterator + '_ { 228 | let delimeters = 229 | self.get_buffer() 230 | .into_iter() 231 | .filter_map(|(index, b)| if *b == 0x0 { Some(index) } else { None }); 232 | 233 | [self.window.start()] 234 | .into_iter() 235 | .chain(delimeters.clone().map(|i| i + 1)) 236 | .zip(delimeters.chain([self.window.end()])) 237 | .filter_map(|(start, end)| { 238 | if start != end { 239 | Some(CircularRange::new( 240 | start, 241 | end, 242 | self.window.len(), 243 | self.buffer.len(), 244 | )) 245 | } else { 246 | None 247 | } 248 | }) 249 | } 250 | 251 | fn get_entries(&self) -> impl Iterator> { 252 | self.get_entry_ranges() 253 | .map(|range| CircularSlice::from_range(self.buffer, range)) 254 | } 255 | } 256 | 257 | impl<'a> History for SliceHistory<'a> { 258 | fn add_entry<'b>(&mut self, entry: &'b str) -> Result<(), &'b str> { 259 | if entry.len() + 1 > self.buffer.len() { 260 | return Err(entry); 261 | } 262 | 263 | for (_, b) in self 264 | .get_available_range() 265 | .into_iter() 266 | .zip(entry.as_bytes().iter()) 267 | { 268 | self.buffer[self.window.end()] = *b; 269 | self.window.widen(); 270 | } 271 | 272 | if self.buffer[self.window.end()] != 0x0 { 273 | self.buffer[self.window.end()] = 0x0; 274 | 275 | self.window.widen(); 276 | 277 | while self.buffer[self.window.start()] != 0x0 { 278 | self.window.narrow(); 279 | } 280 | } else { 281 | self.window.widen(); 282 | } 283 | 284 | Ok(()) 285 | } 286 | 287 | fn number_of_entries(&self) -> usize { 288 | self.get_entries().count() 289 | } 290 | 291 | fn get_entry(&self, index: usize) -> Option> { 292 | self.get_entries().nth(index) 293 | } 294 | } 295 | 296 | /// Emtpy implementation for Editors with no history 297 | pub struct NoHistory {} 298 | 299 | impl NoHistory { 300 | pub fn new() -> Self { 301 | Self {} 302 | } 303 | } 304 | 305 | impl Default for NoHistory { 306 | fn default() -> Self { 307 | Self::new() 308 | } 309 | } 310 | 311 | impl History for NoHistory { 312 | fn get_entry(&self, _index: usize) -> Option> { 313 | None 314 | } 315 | 316 | fn add_entry<'a>(&mut self, entry: &'a str) -> Result<(), &'a str> { 317 | Err(entry) 318 | } 319 | 320 | fn number_of_entries(&self) -> usize { 321 | 0 322 | } 323 | } 324 | 325 | /// Wrapper used for history navigation in [`core::Line`] 326 | pub(crate) struct HistoryNavigator<'a, H: History> { 327 | pub(crate) history: &'a mut H, 328 | position: Option, 329 | } 330 | 331 | impl<'a, H: History> HistoryNavigator<'a, H> { 332 | pub(crate) fn new(history: &'a mut H) -> Self { 333 | Self { 334 | history, 335 | position: None, 336 | } 337 | } 338 | 339 | fn set_position(&mut self, position: usize) -> usize { 340 | *self.position.insert(position) 341 | } 342 | 343 | fn get_position(&mut self) -> usize { 344 | *self 345 | .position 346 | .get_or_insert_with(|| self.history.number_of_entries()) 347 | } 348 | 349 | pub(crate) fn move_up(&mut self) -> Result, ()> { 350 | let position = self.get_position(); 351 | 352 | if position > 0 { 353 | let position = self.set_position(position - 1); 354 | 355 | Ok(self.history.get_entry(position).unwrap()) 356 | } else { 357 | Err(()) 358 | } 359 | } 360 | 361 | pub(crate) fn move_down(&mut self) -> Result, ()> { 362 | let new_position = self.get_position() + 1; 363 | 364 | if new_position < self.history.number_of_entries() { 365 | let position = self.set_position(new_position); 366 | 367 | Ok(self.history.get_entry(position).unwrap()) 368 | } else { 369 | Err(()) 370 | } 371 | } 372 | 373 | pub(crate) fn reset(&mut self) { 374 | self.position = None; 375 | } 376 | 377 | pub(crate) fn is_active(&self) -> bool { 378 | self.position.is_some() 379 | } 380 | } 381 | 382 | #[cfg(any(test, doc, feature = "alloc", feature = "std"))] 383 | mod alloc { 384 | use super::*; 385 | use alloc::{ 386 | string::{String, ToString}, 387 | vec::Vec, 388 | }; 389 | 390 | extern crate alloc; 391 | 392 | /// Unbounded history backed by [`Vec`] 393 | pub struct UnboundedHistory { 394 | buffer: Vec, 395 | } 396 | 397 | impl UnboundedHistory { 398 | pub fn new() -> Self { 399 | Self { buffer: Vec::new() } 400 | } 401 | } 402 | 403 | impl Default for UnboundedHistory { 404 | fn default() -> Self { 405 | Self::new() 406 | } 407 | } 408 | 409 | impl History for UnboundedHistory { 410 | fn get_entry(&self, index: usize) -> Option> { 411 | let s = self.buffer[index].as_str(); 412 | 413 | Some(CircularSlice::new(s.as_bytes(), 0, s.len(), s.len())) 414 | } 415 | 416 | fn add_entry<'a>(&mut self, entry: &'a str) -> Result<(), &'a str> { 417 | self.buffer.push(entry.to_string()); 418 | 419 | #[cfg(test)] 420 | dbg!(entry); 421 | 422 | Ok(()) 423 | } 424 | 425 | fn number_of_entries(&self) -> usize { 426 | self.buffer.len() 427 | } 428 | } 429 | } 430 | 431 | #[cfg(any(test, doc, feature = "alloc", feature = "std"))] 432 | pub use alloc::UnboundedHistory; 433 | 434 | #[cfg(test)] 435 | mod tests { 436 | use std::vec::Vec; 437 | 438 | use std::string::String; 439 | 440 | use super::*; 441 | 442 | impl<'a> FromIterator> for Vec { 443 | fn from_iter>>(iter: T) -> Self { 444 | iter.into_iter() 445 | .map(|circular| { 446 | let bytes = circular.into_iter().map(|(_, b)| *b).collect::>(); 447 | String::from_utf8(bytes).unwrap() 448 | }) 449 | .collect() 450 | } 451 | } 452 | 453 | #[test] 454 | fn circular_range() { 455 | assert_eq!(CircularRange::new(0, 3, 10, 10).get_ranges(), (0..3, 0..0)); 456 | assert_eq!(CircularRange::new(0, 0, 10, 10).get_ranges(), (0..10, 0..0)); 457 | assert_eq!(CircularRange::new(0, 0, 0, 10).get_ranges(), (0..0, 0..0)); 458 | assert_eq!(CircularRange::new(7, 3, 10, 10).get_ranges(), (7..10, 0..3)); 459 | assert_eq!(CircularRange::new(0, 0, 10, 10).get_ranges(), (0..10, 0..0)); 460 | assert_eq!( 461 | CircularRange::new(0, 10, 10, 10).get_ranges(), 462 | (0..10, 0..0) 463 | ); 464 | assert_eq!(CircularRange::new(9, 9, 10, 10).get_ranges(), (9..10, 0..9)); 465 | assert_eq!( 466 | CircularRange::new(10, 10, 10, 10).get_ranges(), 467 | (10..10, 0..10) 468 | ); 469 | 470 | assert_eq!(CircularRange::new(0, 10, 10, 10).into_iter().count(), 10); 471 | assert_eq!(CircularRange::new(10, 10, 10, 10).into_iter().count(), 10); 472 | assert_eq!(CircularRange::new(4, 4, 10, 10).into_iter().count(), 10); 473 | } 474 | 475 | #[test] 476 | fn circular_slice() { 477 | assert_eq!( 478 | CircularSlice::new("abcdef".as_bytes(), 0, 3, 6).get_slices(), 479 | ("abc".as_bytes(), "".as_bytes()) 480 | ); 481 | 482 | assert_eq!( 483 | CircularSlice::new("abcdef".as_bytes(), 3, 0, 6).get_slices(), 484 | ("def".as_bytes(), "".as_bytes()) 485 | ); 486 | 487 | assert_eq!( 488 | CircularSlice::new("abcdef".as_bytes(), 3, 3, 6).get_slices(), 489 | ("def".as_bytes(), "abc".as_bytes()) 490 | ); 491 | 492 | assert_eq!( 493 | CircularSlice::new("abcdef".as_bytes(), 0, 6, 6).get_slices(), 494 | ("abcdef".as_bytes(), "".as_bytes()) 495 | ); 496 | 497 | assert_eq!( 498 | CircularSlice::new("abcdef".as_bytes(), 0, 0, 6).get_slices(), 499 | ("abcdef".as_bytes(), "".as_bytes()) 500 | ); 501 | 502 | assert_eq!( 503 | CircularSlice::new("abcdef".as_bytes(), 0, 0, 0).get_slices(), 504 | ("".as_bytes(), "".as_bytes()) 505 | ); 506 | 507 | assert_eq!( 508 | CircularSlice::new("abcdef".as_bytes(), 6, 6, 6).get_slices(), 509 | ("".as_bytes(), "abcdef".as_bytes()) 510 | ); 511 | } 512 | 513 | #[test] 514 | fn static_history() { 515 | let mut buffer = [0; 10]; 516 | let mut history: SliceHistory = SliceHistory::new(&mut buffer); 517 | 518 | assert_eq!(history.get_available_range().get_ranges(), (0..10, 0..0)); 519 | 520 | assert_eq!( 521 | history.get_entries().collect::>(), 522 | Vec::::new() 523 | ); 524 | 525 | history.add_entry("abc").unwrap(); 526 | 527 | // dbg!(history.start, history.end, history.len); 528 | // dbg!(history.get_entry_ranges().collect::>()); 529 | // dbg!(history.buffer); 530 | 531 | assert_eq!(history.get_entries().collect::>(), vec!["abc"]); 532 | 533 | history.add_entry("def").unwrap(); 534 | 535 | // dbg!(history.buffer); 536 | 537 | assert_eq!( 538 | history.get_entries().collect::>(), 539 | vec!["abc", "def"] 540 | ); 541 | 542 | history.add_entry("ghi").unwrap(); 543 | 544 | dbg!( 545 | history.window.start(), 546 | history.window.end(), 547 | history.window.len() 548 | ); 549 | 550 | assert_eq!( 551 | history.get_entries().collect::>(), 552 | vec!["def", "ghi"] 553 | ); 554 | 555 | history.add_entry("j").unwrap(); 556 | 557 | // dbg!(history.start, history.end, history.len); 558 | 559 | assert_eq!( 560 | history.get_entries().collect::>(), 561 | vec!["def", "ghi", "j"] 562 | ); 563 | 564 | history.add_entry("012345678").unwrap(); 565 | 566 | assert_eq!( 567 | history.get_entries().collect::>(), 568 | vec!["012345678"] 569 | ); 570 | 571 | assert!(history.add_entry("0123456789").is_err()); 572 | 573 | history.add_entry("abc").unwrap(); 574 | 575 | assert_eq!(history.get_entries().collect::>(), vec!["abc"]); 576 | 577 | history.add_entry("defgh").unwrap(); 578 | 579 | assert_eq!( 580 | history.get_entries().collect::>(), 581 | vec!["abc", "defgh"] 582 | ); 583 | } 584 | 585 | #[test] 586 | fn navigator() { 587 | let mut history = UnboundedHistory::new(); 588 | let mut navigator = HistoryNavigator::new(&mut history); 589 | 590 | assert!(navigator.move_up().is_err()); 591 | assert!(navigator.move_down().is_err()); 592 | 593 | navigator.history.add_entry("line 1").unwrap(); 594 | navigator.reset(); 595 | 596 | assert!(navigator.move_up().is_ok()); 597 | assert!(navigator.move_up().is_err()); 598 | 599 | assert!(navigator.move_down().is_err()); 600 | } 601 | } 602 | -------------------------------------------------------------------------------- /noline/src/input.rs: -------------------------------------------------------------------------------- 1 | use num_enum::{IntoPrimitive, TryFromPrimitive}; 2 | 3 | use crate::utf8::{Utf8Char, Utf8Decoder, Utf8DecoderStatus}; 4 | 5 | #[allow(clippy::upper_case_acronyms)] 6 | #[derive(Debug, Eq, PartialEq, Copy, Clone, IntoPrimitive, TryFromPrimitive)] 7 | #[repr(u8)] 8 | pub enum ControlCharacter { 9 | NUL = 0x0, 10 | CtrlA = 0x1, 11 | CtrlB = 0x2, 12 | CtrlC = 0x3, 13 | CtrlD = 0x4, 14 | CtrlE = 0x5, 15 | CtrlF = 0x6, 16 | CtrlG = 0x7, 17 | CtrlH = 0x8, 18 | Tab = 0x9, 19 | LineFeed = 0xA, 20 | CtrlK = 0xB, 21 | CtrlL = 0xC, 22 | CarriageReturn = 0xD, 23 | CtrlN = 0xE, 24 | CtrlO = 0xF, 25 | CtrlP = 0x10, 26 | CtrlQ = 0x11, 27 | CtrlR = 0x12, 28 | CtrlS = 0x13, 29 | CtrlT = 0x14, 30 | CtrlU = 0x15, 31 | CtrlV = 0x16, 32 | CtrlW = 0x17, 33 | CtrlX = 0x18, 34 | CtrlY = 0x19, 35 | CtrlZ = 0x1A, 36 | Escape = 0x1B, 37 | FS = 0x1C, 38 | GS = 0x1D, 39 | RS = 0x1E, 40 | US = 0x1F, 41 | Backspace = 0x7F, 42 | } 43 | 44 | impl ControlCharacter { 45 | fn new(byte: u8) -> Result { 46 | match Self::try_from(byte) { 47 | Ok(this) => Ok(this), 48 | Err(_) => Err(()), 49 | } 50 | } 51 | } 52 | 53 | #[allow(clippy::upper_case_acronyms)] 54 | #[derive(Debug, Eq, PartialEq, Copy, Clone)] 55 | pub enum CSI { 56 | CUU(usize), 57 | CUD(usize), 58 | CUF(usize), 59 | CUB(usize), 60 | CPR(usize, usize), 61 | CUP(usize, usize), 62 | ED(usize), 63 | DSR, 64 | SU(usize), 65 | SD(usize), 66 | Home, 67 | Delete, 68 | End, 69 | Unknown(u8), 70 | Invalid, 71 | } 72 | 73 | impl CSI { 74 | fn new(byte: u8, arg1: Option, arg2: Option) -> Self { 75 | let c = byte as char; 76 | 77 | match c { 78 | 'A' => Self::CUU(arg1.unwrap_or(1)), 79 | 'B' => Self::CUD(arg1.unwrap_or(1)), 80 | 'C' => Self::CUF(arg1.unwrap_or(1)), 81 | 'D' => Self::CUB(arg1.unwrap_or(1)), 82 | 'H' => Self::CUP(arg1.unwrap_or(1), arg2.unwrap_or(1)), 83 | 'J' => Self::ED(arg1.unwrap_or(0)), 84 | 'R' => { 85 | if let (Some(arg1), Some(arg2)) = (arg1, arg2) { 86 | Self::CPR(arg1, arg2) 87 | } else { 88 | Self::Invalid 89 | } 90 | } 91 | 'S' => Self::SU(arg1.unwrap_or(1)), 92 | 'T' => Self::SD(arg1.unwrap_or(1)), 93 | 'n' => Self::DSR, 94 | '~' => { 95 | if let Some(arg) = arg1 { 96 | match arg { 97 | 1 => Self::Home, 98 | 3 => Self::Delete, 99 | 4 => Self::End, 100 | _ => Self::Unknown(byte), 101 | } 102 | } else { 103 | Self::Unknown(byte) 104 | } 105 | } 106 | _ => Self::Unknown(byte), 107 | } 108 | } 109 | } 110 | 111 | #[cfg_attr(test, derive(Debug))] 112 | #[derive(Eq, PartialEq, Copy, Clone)] 113 | pub enum Action { 114 | Ignore, 115 | Print(Utf8Char), 116 | InvalidUtf8, 117 | ControlCharacter(ControlCharacter), 118 | EscapeSequence(u8), 119 | ControlSequenceIntroducer(CSI), 120 | } 121 | 122 | impl Action { 123 | fn escape_sequence(byte: u8) -> Self { 124 | Action::EscapeSequence(byte) 125 | } 126 | 127 | fn control_character(byte: u8) -> Self { 128 | Action::ControlCharacter(ControlCharacter::new(byte).unwrap()) 129 | } 130 | 131 | fn csi(byte: u8, arg1: Option, arg2: Option) -> Self { 132 | Action::ControlSequenceIntroducer(CSI::new(byte, arg1, arg2)) 133 | } 134 | } 135 | 136 | #[derive(Debug, Eq, PartialEq)] 137 | enum State { 138 | Ground, 139 | Utf8Sequence(Option), 140 | EscapeSequence, 141 | CSIStart, 142 | CSIArg1(Option), 143 | CSIArg2(Option, Option), 144 | } 145 | 146 | pub struct Parser { 147 | state: State, 148 | } 149 | 150 | impl Parser { 151 | pub fn new() -> Self { 152 | Self { 153 | state: State::Ground, 154 | } 155 | } 156 | 157 | pub fn advance(&mut self, byte: u8) -> Action { 158 | match self.state { 159 | State::Ground => match byte { 160 | 0x1b => { 161 | self.state = State::EscapeSequence; 162 | Action::Ignore 163 | } 164 | 0x0..=0x1a | 0x1c..=0x1f | 0x7f => Action::control_character(byte), 165 | 0x20..=0x7e | 0x80..=0xff => { 166 | let mut decoder = Utf8Decoder::new(); 167 | 168 | match decoder.advance(byte) { 169 | Utf8DecoderStatus::Continuation => { 170 | self.state = State::Utf8Sequence(Some(decoder)); 171 | Action::Ignore 172 | } 173 | Utf8DecoderStatus::Done(c) => Action::Print(c), 174 | Utf8DecoderStatus::Error => Action::InvalidUtf8, 175 | } 176 | } 177 | }, 178 | State::Utf8Sequence(ref mut decoder) => { 179 | let mut decoder = decoder.take().unwrap(); 180 | 181 | match decoder.advance(byte) { 182 | Utf8DecoderStatus::Continuation => { 183 | self.state = State::Utf8Sequence(Some(decoder)); 184 | Action::Ignore 185 | } 186 | Utf8DecoderStatus::Done(c) => { 187 | self.state = State::Ground; 188 | Action::Print(c) 189 | } 190 | Utf8DecoderStatus::Error => { 191 | self.state = State::Ground; 192 | Action::InvalidUtf8 193 | } 194 | } 195 | } 196 | State::EscapeSequence => { 197 | if byte == 0x5b { 198 | self.state = State::CSIStart; 199 | Action::Ignore 200 | } else { 201 | self.state = State::Ground; 202 | Action::escape_sequence(byte) 203 | } 204 | } 205 | State::CSIStart => match byte { 206 | 0x30..=0x39 => { 207 | let value: usize = (byte - 0x30) as usize; 208 | self.state = State::CSIArg1(Some(value)); 209 | Action::Ignore 210 | } 211 | 0x3b => { 212 | self.state = State::CSIArg2(None, None); 213 | Action::Ignore 214 | } 215 | 0x40..=0x7e => { 216 | self.state = State::Ground; 217 | Action::csi(byte, None, None) 218 | } 219 | _ => Action::Ignore, 220 | }, 221 | State::CSIArg1(value) => match byte { 222 | 0x30..=0x39 => { 223 | let value: usize = value.unwrap_or(0) * 10 + (byte - 0x30) as usize; 224 | self.state = State::CSIArg1(Some(value)); 225 | Action::Ignore 226 | } 227 | 0x3b => { 228 | self.state = State::CSIArg2(value, None); 229 | Action::Ignore 230 | } 231 | 0x40..=0x7e => { 232 | self.state = State::Ground; 233 | Action::csi(byte, value, None) 234 | } 235 | _ => Action::Ignore, 236 | }, 237 | State::CSIArg2(arg1, arg2) => match byte { 238 | 0x30..=0x39 => { 239 | let arg2: usize = arg2.unwrap_or(0) * 10 + (byte - 0x30) as usize; 240 | self.state = State::CSIArg2(arg1, Some(arg2)); 241 | Action::Ignore 242 | } 243 | 0x40..=0x7e => { 244 | self.state = State::Ground; 245 | Action::csi(byte, arg1, arg2) 246 | } 247 | _ => Action::Ignore, 248 | }, 249 | } 250 | } 251 | } 252 | 253 | #[cfg(test)] 254 | pub(crate) mod tests { 255 | use crate::testlib::ToByteVec; 256 | 257 | use super::*; 258 | use std::vec::Vec; 259 | use ControlCharacter::*; 260 | 261 | fn input_sequence(parser: &mut Parser, seq: impl ToByteVec) -> Vec { 262 | seq.to_byte_vec() 263 | .into_iter() 264 | .map(|b| parser.advance(b)) 265 | .collect() 266 | } 267 | 268 | #[test] 269 | fn parser() { 270 | let mut parser = Parser::new(); 271 | 272 | assert_eq!(parser.state, State::Ground); 273 | 274 | assert_eq!(parser.advance(b'a'), Action::Print(Utf8Char::from_str("a"))); 275 | assert_eq!(parser.advance(0x7), Action::ControlCharacter(CtrlG)); 276 | assert_eq!(parser.advance(0x3), Action::ControlCharacter(CtrlC)); 277 | 278 | let actions = input_sequence(&mut parser, "æ"); 279 | assert_eq!( 280 | actions, 281 | [Action::Ignore, Action::Print(Utf8Char::from_str("æ"))] 282 | ); 283 | 284 | let mut actions = input_sequence(&mut parser, "\x1b[312;836R"); 285 | assert_eq!( 286 | actions.pop().unwrap(), 287 | Action::ControlSequenceIntroducer(CSI::CPR(312, 836)) 288 | ); 289 | while let Some(action) = actions.pop() { 290 | assert_eq!(action, Action::Ignore); 291 | } 292 | 293 | let mut actions = input_sequence(&mut parser, "\x1b[R"); 294 | assert_eq!( 295 | actions.pop().unwrap(), 296 | Action::ControlSequenceIntroducer(CSI::Invalid) 297 | ); 298 | 299 | let mut actions = input_sequence(&mut parser, "\x1b[32R"); 300 | assert_eq!( 301 | actions.pop().unwrap(), 302 | Action::ControlSequenceIntroducer(CSI::Invalid) 303 | ); 304 | 305 | let mut actions = input_sequence(&mut parser, "\x1b[32;R"); 306 | assert_eq!( 307 | actions.pop().unwrap(), 308 | Action::ControlSequenceIntroducer(CSI::Invalid) 309 | ); 310 | 311 | let mut actions = input_sequence(&mut parser, "\x1b[A"); 312 | 313 | assert_eq!( 314 | actions.pop().unwrap(), 315 | Action::ControlSequenceIntroducer(CSI::CUU(1)) 316 | ); 317 | 318 | let mut actions = input_sequence(&mut parser, "\x1b[10B"); 319 | 320 | assert_eq!( 321 | actions.pop().unwrap(), 322 | Action::ControlSequenceIntroducer(CSI::CUD(10)) 323 | ); 324 | 325 | let mut actions = input_sequence(&mut parser, "\x1b[H"); 326 | 327 | assert_eq!( 328 | actions.pop().unwrap(), 329 | Action::ControlSequenceIntroducer(CSI::CUP(1, 1)) 330 | ); 331 | 332 | let mut actions = input_sequence(&mut parser, "\x1b[2;5H"); 333 | 334 | assert_eq!( 335 | actions.pop().unwrap(), 336 | Action::ControlSequenceIntroducer(CSI::CUP(2, 5)) 337 | ); 338 | 339 | let mut actions = input_sequence(&mut parser, "\x1b[;5H"); 340 | 341 | assert_eq!( 342 | actions.pop().unwrap(), 343 | Action::ControlSequenceIntroducer(CSI::CUP(1, 5)) 344 | ); 345 | 346 | let mut actions = input_sequence(&mut parser, "\x1b[17;H"); 347 | 348 | assert_eq!( 349 | actions.pop().unwrap(), 350 | Action::ControlSequenceIntroducer(CSI::CUP(17, 1)) 351 | ); 352 | 353 | let mut actions = input_sequence(&mut parser, "\x1b[;H"); 354 | 355 | assert_eq!( 356 | actions.pop().unwrap(), 357 | Action::ControlSequenceIntroducer(CSI::CUP(1, 1)) 358 | ); 359 | 360 | let mut actions = input_sequence(&mut parser, "\x1b[;10H"); 361 | 362 | assert_eq!( 363 | actions.pop().unwrap(), 364 | Action::ControlSequenceIntroducer(CSI::CUP(1, 10)) 365 | ); 366 | } 367 | } 368 | -------------------------------------------------------------------------------- /noline/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Noline is an IO-agnostic `#[no_std]` line editor providing robust 2 | //! line editing for any system. The core functionality is IO-free, so 3 | //! it can be adapted to any system be it embedded, async, async 4 | //! embedded, WASM or IPoAC (IP over Avian Carriers). 5 | //! 6 | //! Features: 7 | //! - IO-free 8 | //! - Minimal dependencies 9 | //! - No allocation needed - Both heap-based and static buffers are provided 10 | //! - UTF-8 support 11 | //! - Emacs keybindings 12 | //! - Line history 13 | //! 14 | //! Possible future features: 15 | //! - Auto-completion and hints 16 | //! 17 | //! The API should be considered experimental and will change in the 18 | //! future. 19 | //! 20 | //! The core implementation consists of a state machine taking bytes as 21 | //! input and yielding iterators over byte slices. Because this is 22 | //! done without any IO, Noline can be adapted to work on any platform. 23 | //! 24 | //! Noline comes with multiple implemenations: 25 | //! - [`sync_editor::Editor`] – Editor for synchronous IO 26 | //! - [`async_editor::Editor`] - Editor for asynchronous IO 27 | //! 28 | //! Editors can be built using [`builder::EditorBuilder`]. 29 | //! 30 | //! # Example 31 | //! ```no_run 32 | //! # use noline::{builder::EditorBuilder}; 33 | //! # use embedded_io::{Read, Write, ErrorType}; 34 | //! # use core::convert::Infallible; 35 | //! # struct MyIO {} 36 | //! # impl ErrorType for MyIO { 37 | //! # type Error = Infallible; 38 | //! # } 39 | //! # impl embedded_io::Write for MyIO { 40 | //! # fn write(&mut self, buf: &[u8]) -> Result { unimplemented!() } 41 | //! # fn flush(&mut self) -> Result<(), Self::Error> { unimplemented!() } 42 | //! # } 43 | //! # impl embedded_io::Read for MyIO { 44 | //! # fn read(&mut self, buf: &mut[u8]) -> Result { unimplemented!() } 45 | //! # } 46 | //! # let mut io = MyIO {}; 47 | //! let prompt = "> "; 48 | //! 49 | //! let mut io = MyIO {}; // IO handler, see full examples for details 50 | //! // how to implement it 51 | //! 52 | //! let mut editor = EditorBuilder::new_unbounded() 53 | //! .with_unbounded_history() 54 | //! .build_sync(&mut io) 55 | //! .unwrap(); 56 | //! 57 | //! while let Ok(line) = editor.readline(prompt, &mut io) { 58 | //! writeln!(io, "Read: '{}'", line).unwrap(); 59 | //! } 60 | //! ``` 61 | 62 | #![cfg_attr(not(test), no_std)] 63 | 64 | pub mod async_editor; 65 | pub mod builder; 66 | mod core; 67 | pub mod error; 68 | pub mod history; 69 | mod input; 70 | pub mod line_buffer; 71 | mod output; 72 | pub mod sync_editor; 73 | pub(crate) mod terminal; 74 | mod utf8; 75 | 76 | #[cfg(test)] 77 | pub(crate) mod testlib; 78 | -------------------------------------------------------------------------------- /noline/src/line_buffer.rs: -------------------------------------------------------------------------------- 1 | //! Buffer to hold line. 2 | //! 3 | //! Can be backed by [`std::vec::Vec`] for dynamic allocation or 4 | //! [`StaticBuffer`] for static allocation. Custom implementation can 5 | //! be provided with the [`Buffer`] trait. 6 | 7 | use crate::utf8::Utf8Char; 8 | use core::{ops::Range, str::from_utf8_unchecked}; 9 | 10 | /// Trait for defining underlying buffer 11 | pub trait Buffer { 12 | /// Return the current length of the buffer. This represents the 13 | /// number of bytes currently in the buffer, not the capacity. 14 | fn buffer_len(&self) -> usize; 15 | 16 | /// Return buffer capacity or None if unbounded. 17 | fn capacity(&self) -> Option; 18 | 19 | /// Truncate buffer, setting lenght to 0. 20 | fn truncate_buffer(&mut self, index: usize); 21 | 22 | /// Insert byte at index 23 | fn insert_byte(&mut self, index: usize, byte: u8); 24 | 25 | /// Remove byte from index and return byte 26 | fn remove_byte(&mut self, index: usize) -> u8; 27 | 28 | /// Return byte slice into buffer from 0 up to buffer length. 29 | fn as_slice(&self) -> &[u8]; 30 | } 31 | 32 | /// High level interface to line buffer 33 | pub struct LineBuffer { 34 | buf: B, 35 | } 36 | 37 | impl<'a> LineBuffer> { 38 | /// Create new static line buffer 39 | pub fn from_slice(buffer: &'a mut [u8]) -> Self { 40 | Self { 41 | buf: SliceBuffer::new(buffer), 42 | } 43 | } 44 | } 45 | 46 | impl LineBuffer { 47 | /// Return buffer as bytes slice 48 | pub fn as_slice(&self) -> &[u8] { 49 | self.buf.as_slice() 50 | } 51 | 52 | /// Return buffer length 53 | pub fn len(&self) -> usize { 54 | self.buf.buffer_len() 55 | } 56 | 57 | /// Return buffer as string. The buffer should only hold a valid 58 | /// UTF-8, so this function is infallible. 59 | pub fn as_str(&self) -> &str { 60 | // Pinky swear, it's only UTF-8! 61 | unsafe { from_utf8_unchecked(self.as_slice()) } 62 | } 63 | 64 | fn char_ranges(&self) -> impl Iterator, char)> + '_ { 65 | let s = self.as_str(); 66 | 67 | s.char_indices() 68 | .zip(s.char_indices().skip(1).chain([(s.len(), '\0')])) 69 | .map(|((start, c), (end, _))| (start..end, c)) 70 | } 71 | 72 | fn get_byte_position(&self, char_index: usize) -> usize { 73 | let s = self.as_str(); 74 | 75 | s.char_indices() 76 | .skip(char_index) 77 | .map(|(pos, _)| pos) 78 | .next() 79 | .unwrap_or(s.len()) 80 | } 81 | 82 | /// Delete character at character index. 83 | pub fn delete(&mut self, char_index: usize) { 84 | let mut ranges = self.char_ranges().skip(char_index); 85 | 86 | if let Some((range, _)) = ranges.next() { 87 | drop(ranges); 88 | 89 | let pos = range.start; 90 | 91 | for _ in range { 92 | self.buf.remove_byte(pos); 93 | } 94 | } 95 | } 96 | 97 | /// Delete buffer after character index 98 | pub fn delete_after_char(&mut self, char_index: usize) { 99 | let pos = self.get_byte_position(char_index); 100 | 101 | self.buf.truncate_buffer(pos); 102 | } 103 | 104 | /// Truncate buffer 105 | pub fn truncate(&mut self) { 106 | self.delete_after_char(0); 107 | } 108 | 109 | fn delete_range(&mut self, range: Range) { 110 | let pos = range.start; 111 | for _ in range { 112 | self.buf.remove_byte(pos); 113 | } 114 | } 115 | 116 | /// Delete previous word from character index 117 | pub fn delete_previous_word(&mut self, char_index: usize) -> usize { 118 | let mut word_start = 0; 119 | let mut word_end = 0; 120 | 121 | for (i, (range, c)) in self.char_ranges().enumerate().take(char_index) { 122 | if c == ' ' && i < char_index - 1 { 123 | word_start = range.end; 124 | } 125 | 126 | word_end = range.end; 127 | } 128 | 129 | let deleted = self.as_str()[word_start..word_end].chars().count(); 130 | 131 | self.delete_range(word_start..word_end); 132 | 133 | deleted 134 | } 135 | 136 | /// Swap characters at index 137 | pub fn swap_chars(&mut self, char_index: usize) { 138 | let mut ranges = self.char_ranges().skip(char_index - 1); 139 | 140 | if let Some((prev, _)) = ranges.next() { 141 | if let Some((cur, _)) = ranges.next() { 142 | drop(ranges); 143 | 144 | for (remove, insert) in cur.zip((prev.start)..) { 145 | let byte = self.buf.remove_byte(remove); 146 | self.buf.insert_byte(insert, byte); 147 | } 148 | } 149 | } 150 | } 151 | 152 | /// Insert bytes at index 153 | /// 154 | /// # Safety 155 | /// 156 | /// The caller must ensure that the input bytes are a valid UTF-8 157 | /// sequence and that the byte index aligns with a valid UTF-8 character index. 158 | pub unsafe fn insert_bytes(&mut self, index: usize, bytes: &[u8]) -> Result<(), ()> { 159 | if let Some(capacity) = self.buf.capacity() { 160 | if bytes.len() > capacity - self.buf.buffer_len() { 161 | return Err(()); 162 | } 163 | } 164 | 165 | for (i, byte) in bytes.iter().enumerate() { 166 | self.buf.insert_byte(index + i, *byte); 167 | } 168 | 169 | Ok(()) 170 | } 171 | 172 | /// Insert UTF-8 char at position 173 | pub fn insert_utf8_char(&mut self, char_index: usize, c: Utf8Char) -> Result<(), Utf8Char> { 174 | unsafe { 175 | self.insert_bytes(self.get_byte_position(char_index), c.as_bytes()) 176 | .map_err(|_| c) 177 | } 178 | } 179 | 180 | /// Insert string at char position 181 | pub fn insert_str(&mut self, char_index: usize, s: &str) -> Result<(), ()> { 182 | unsafe { self.insert_bytes(self.get_byte_position(char_index), s.as_bytes()) } 183 | } 184 | } 185 | 186 | /// Emtpy buffer used for builder 187 | pub struct NoBuffer {} 188 | 189 | impl Buffer for NoBuffer { 190 | fn buffer_len(&self) -> usize { 191 | unimplemented!() 192 | } 193 | 194 | fn capacity(&self) -> Option { 195 | unimplemented!() 196 | } 197 | 198 | fn truncate_buffer(&mut self, _index: usize) { 199 | unimplemented!() 200 | } 201 | 202 | fn insert_byte(&mut self, _index: usize, _byte: u8) { 203 | unimplemented!() 204 | } 205 | 206 | fn remove_byte(&mut self, _index: usize) -> u8 { 207 | unimplemented!() 208 | } 209 | 210 | fn as_slice(&self) -> &[u8] { 211 | unimplemented!() 212 | } 213 | } 214 | 215 | /// Static buffer backed by slice 216 | pub struct SliceBuffer<'a> { 217 | data: &'a mut [u8], 218 | len: usize, 219 | } 220 | 221 | impl<'a> SliceBuffer<'a> { 222 | pub fn new(data: &'a mut [u8]) -> Self { 223 | Self { data, len: 0 } 224 | } 225 | } 226 | 227 | impl<'a> Buffer for SliceBuffer<'a> { 228 | fn buffer_len(&self) -> usize { 229 | self.len 230 | } 231 | 232 | fn capacity(&self) -> Option { 233 | Some(self.data.len()) 234 | } 235 | 236 | fn truncate_buffer(&mut self, index: usize) { 237 | self.len = index; 238 | } 239 | 240 | fn insert_byte(&mut self, index: usize, byte: u8) { 241 | for i in (index..self.len).rev() { 242 | self.data[i + 1] = self.data[i]; 243 | } 244 | 245 | self.data[index] = byte; 246 | self.len += 1; 247 | } 248 | 249 | fn remove_byte(&mut self, index: usize) -> u8 { 250 | let byte = self.data[index]; 251 | 252 | for i in index..(self.len - 1) { 253 | self.data[i] = self.data[i + 1]; 254 | } 255 | 256 | self.len -= 1; 257 | 258 | byte 259 | } 260 | 261 | fn as_slice(&self) -> &[u8] { 262 | &self.data[0..self.len] 263 | } 264 | } 265 | 266 | #[cfg(any(test, doc, feature = "alloc", feature = "std"))] 267 | mod alloc { 268 | extern crate alloc; 269 | 270 | use self::alloc::vec::Vec; 271 | use super::*; 272 | 273 | impl LineBuffer { 274 | /// Create new static line buffer 275 | pub fn new_unbounded() -> Self { 276 | Self { 277 | buf: UnboundedBuffer::new(), 278 | } 279 | } 280 | } 281 | 282 | pub struct UnboundedBuffer { 283 | vec: Vec, 284 | } 285 | 286 | impl UnboundedBuffer { 287 | pub fn new() -> Self { 288 | Self { vec: Vec::new() } 289 | } 290 | } 291 | 292 | impl Buffer for UnboundedBuffer { 293 | fn buffer_len(&self) -> usize { 294 | self.vec.len() 295 | } 296 | 297 | fn capacity(&self) -> Option { 298 | None 299 | } 300 | 301 | fn truncate_buffer(&mut self, index: usize) { 302 | self.vec.truncate(index) 303 | } 304 | 305 | fn insert_byte(&mut self, index: usize, byte: u8) { 306 | self.vec.insert(index, byte); 307 | } 308 | 309 | fn remove_byte(&mut self, index: usize) -> u8 { 310 | self.vec.remove(index) 311 | } 312 | 313 | fn as_slice(&self) -> &[u8] { 314 | self.vec.as_slice() 315 | } 316 | } 317 | } 318 | 319 | #[cfg(any(test, doc, feature = "alloc", feature = "std"))] 320 | pub use self::alloc::*; 321 | 322 | #[cfg(test)] 323 | mod tests { 324 | use super::*; 325 | 326 | #[test] 327 | fn slice_buffer() { 328 | let mut array = [0; 20]; 329 | let mut buf = SliceBuffer::new(&mut array); 330 | 331 | for i in 0..20 { 332 | buf.insert_byte(i, 0x30); 333 | } 334 | 335 | buf.remove_byte(19); 336 | } 337 | 338 | fn insert_str(buf: &mut LineBuffer, index: usize, s: &str) { 339 | buf.insert_str(index, s).unwrap(); 340 | } 341 | 342 | fn test_line_buffer(buf: &mut LineBuffer) { 343 | insert_str(buf, 0, "Hello, World!"); 344 | 345 | assert_eq!(buf.as_str(), "Hello, World!"); 346 | 347 | buf.delete(12); 348 | 349 | assert_eq!(buf.as_str(), "Hello, World"); 350 | 351 | buf.delete(12); 352 | 353 | assert_eq!(buf.as_str(), "Hello, World"); 354 | 355 | buf.delete(0); 356 | insert_str(buf, 0, "h"); 357 | 358 | assert_eq!(buf.as_str(), "hello, World"); 359 | 360 | buf.delete(2); 361 | insert_str(buf, 2, "L"); 362 | 363 | assert_eq!(buf.as_str(), "heLlo, World"); 364 | 365 | buf.delete(11); 366 | 367 | assert_eq!(buf.as_str(), "heLlo, Worl"); 368 | 369 | buf.delete(5); 370 | 371 | assert_eq!(buf.as_str(), "heLlo Worl"); 372 | 373 | for _ in 0..5 { 374 | buf.delete(5); 375 | } 376 | 377 | assert_eq!(buf.as_str(), "heLlo"); 378 | 379 | insert_str(buf, 5, " æå"); 380 | 381 | assert_eq!(buf.as_str(), "heLlo æå"); 382 | 383 | insert_str(buf, 7, "ø"); 384 | 385 | assert_eq!(buf.as_str(), "heLlo æøå"); 386 | 387 | buf.delete(8); 388 | 389 | assert_eq!(buf.as_str(), "heLlo æø"); 390 | 391 | buf.delete(7); 392 | 393 | assert_eq!(buf.as_str(), "heLlo æ"); 394 | 395 | buf.delete_previous_word(7); 396 | 397 | assert_eq!(buf.as_str(), "heLlo "); 398 | 399 | buf.delete_previous_word(6); 400 | 401 | assert_eq!(buf.as_str(), ""); 402 | 403 | insert_str(buf, 0, "word1 word2 word3"); 404 | assert_eq!(buf.as_str(), "word1 word2 word3"); 405 | buf.delete_previous_word(12); 406 | 407 | assert_eq!(buf.as_str(), "word1 word3"); 408 | } 409 | 410 | #[test] 411 | fn test_slice_line_buffer() { 412 | let mut array = [0; 80]; 413 | let mut buf = LineBuffer::from_slice(&mut array); 414 | 415 | test_line_buffer(&mut buf); 416 | 417 | buf.delete_after_char(0); 418 | 419 | assert_eq!(buf.len(), 0); 420 | 421 | for i in 0..80 { 422 | assert!(buf.insert_utf8_char(i, Utf8Char::from_str("a")).is_ok()); 423 | } 424 | 425 | assert!(buf.insert_utf8_char(80, Utf8Char::from_str("a")).is_err()); 426 | } 427 | 428 | #[test] 429 | fn test_alloc_line_buffer() { 430 | let mut buf = LineBuffer::new_unbounded(); 431 | 432 | test_line_buffer(&mut buf); 433 | 434 | buf.delete_after_char(0); 435 | 436 | for i in 0..1000 { 437 | assert!(buf.insert_utf8_char(i, Utf8Char::from_str("a")).is_ok()); 438 | } 439 | } 440 | } 441 | -------------------------------------------------------------------------------- /noline/src/output.rs: -------------------------------------------------------------------------------- 1 | use core::marker::PhantomData; 2 | 3 | use crate::{ 4 | core::Prompt, 5 | line_buffer::{Buffer, LineBuffer}, 6 | terminal::{Cursor, Position, Terminal}, 7 | }; 8 | 9 | #[cfg_attr(test, derive(Debug))] 10 | pub enum OutputItem<'a> { 11 | Slice(&'a [u8]), 12 | UintToBytes(UintToBytes<4>), 13 | EndOfString, 14 | Abort, 15 | } 16 | 17 | impl<'a> OutputItem<'a> { 18 | pub fn get_bytes(&self) -> Option<&[u8]> { 19 | match self { 20 | Self::Slice(slice) => Some(slice), 21 | Self::UintToBytes(uint) => Some(uint.as_bytes()), 22 | Self::EndOfString | Self::Abort => None, 23 | } 24 | } 25 | } 26 | 27 | #[cfg_attr(test, derive(Debug))] 28 | #[derive(Copy, Clone)] 29 | pub enum CursorMove { 30 | Forward, 31 | Back, 32 | Start, 33 | End, 34 | } 35 | 36 | #[cfg_attr(test, derive(Debug))] 37 | #[derive(Copy, Clone)] 38 | pub enum OutputAction { 39 | Nothing, 40 | MoveCursor(CursorMove), 41 | ClearAndPrintPrompt, 42 | ClearAndPrintBuffer, 43 | PrintBufferAndMoveCursorForward, 44 | EraseAfterCursor, 45 | EraseAndPrintBuffer, 46 | ClearScreen, 47 | ClearLine, 48 | MoveCursorBackAndPrintBufferAndMoveForward, 49 | MoveCursorAndEraseAndPrintBuffer(isize), 50 | RingBell, 51 | ProbeSize, 52 | Done, 53 | Abort, 54 | } 55 | 56 | #[cfg_attr(test, derive(Debug))] 57 | #[derive(Copy, Clone)] 58 | pub struct UintToBytes { 59 | bytes: [u8; N], 60 | } 61 | 62 | impl UintToBytes { 63 | fn from_uint>(n: I) -> Option { 64 | let mut n: usize = n.into(); 65 | 66 | if n < 10_usize.pow(N as u32) { 67 | let mut bytes = [0; N]; 68 | 69 | for i in (0..N).rev() { 70 | bytes[i] = 0x30 + (n % 10) as u8; 71 | n /= 10; 72 | 73 | if n == 0 { 74 | break; 75 | } 76 | } 77 | 78 | Some(Self { bytes }) 79 | } else { 80 | None 81 | } 82 | } 83 | 84 | pub fn as_bytes(&self) -> &[u8] { 85 | let start = self.bytes.iter().take_while(|&&b| b == 0).count(); 86 | &self.bytes[start..] 87 | } 88 | } 89 | 90 | #[cfg_attr(test, derive(Debug))] 91 | enum MoveCursorState { 92 | New, 93 | ScrollPrefix, 94 | Scroll, 95 | ScrollFinalByte, 96 | MovePrefix, 97 | Row, 98 | Separator, 99 | Column, 100 | MoveFinalByte, 101 | Done, 102 | } 103 | 104 | #[cfg_attr(test, derive(Debug))] 105 | struct MoveCursor { 106 | state: MoveCursorState, 107 | cursor: Cursor, 108 | scroll: isize, 109 | } 110 | 111 | impl MoveCursor { 112 | fn new(cursor: Cursor, scroll: isize) -> Self { 113 | Self { 114 | state: MoveCursorState::New, 115 | cursor, 116 | scroll, 117 | } 118 | } 119 | } 120 | 121 | impl Iterator for MoveCursor { 122 | type Item = OutputItem<'static>; 123 | 124 | fn next(&mut self) -> Option { 125 | loop { 126 | match self.state { 127 | MoveCursorState::ScrollPrefix => { 128 | self.state = MoveCursorState::Scroll; 129 | break Some(OutputItem::Slice("\x1b[".as_bytes())); 130 | } 131 | MoveCursorState::Scroll => { 132 | self.state = MoveCursorState::ScrollFinalByte; 133 | 134 | break Some(OutputItem::UintToBytes( 135 | UintToBytes::from_uint(self.scroll.unsigned_abs()).unwrap(), 136 | )); 137 | } 138 | MoveCursorState::ScrollFinalByte => { 139 | self.state = MoveCursorState::MovePrefix; 140 | 141 | break Some(OutputItem::Slice(if self.scroll > 0 { 142 | "S".as_bytes() 143 | } else { 144 | "T".as_bytes() 145 | })); 146 | } 147 | MoveCursorState::New => { 148 | if self.scroll != 0 { 149 | self.state = MoveCursorState::ScrollPrefix; 150 | } else { 151 | self.state = MoveCursorState::MovePrefix; 152 | } 153 | continue; 154 | } 155 | MoveCursorState::MovePrefix => { 156 | self.state = MoveCursorState::Row; 157 | break Some(OutputItem::Slice("\x1b[".as_bytes())); 158 | } 159 | MoveCursorState::Row => { 160 | self.state = MoveCursorState::Separator; 161 | break Some(OutputItem::UintToBytes( 162 | UintToBytes::from_uint(self.cursor.row + 1).unwrap(), 163 | )); 164 | } 165 | MoveCursorState::Separator => { 166 | self.state = MoveCursorState::Column; 167 | break Some(OutputItem::Slice(";".as_bytes())); 168 | } 169 | MoveCursorState::Column => { 170 | self.state = MoveCursorState::MoveFinalByte; 171 | 172 | break Some(OutputItem::UintToBytes( 173 | UintToBytes::from_uint(self.cursor.column + 1).unwrap(), 174 | )); 175 | } 176 | MoveCursorState::MoveFinalByte => { 177 | self.state = MoveCursorState::Done; 178 | break Some(OutputItem::Slice("H".as_bytes())); 179 | } 180 | MoveCursorState::Done => break None, 181 | } 182 | } 183 | } 184 | } 185 | 186 | #[cfg_attr(test, derive(Debug))] 187 | enum MoveCursorToPosition { 188 | Position(Position), 189 | Move(MoveCursor), 190 | } 191 | 192 | impl MoveCursorToPosition { 193 | fn new(position: Position) -> Self { 194 | Self::Position(position) 195 | } 196 | 197 | fn get_move_cursor(&mut self, terminal: &mut Terminal) -> Option<&mut MoveCursor> { 198 | loop { 199 | match self { 200 | MoveCursorToPosition::Position(position) => { 201 | let scroll = terminal.move_cursor(*position); 202 | let cursor = terminal.get_cursor(); 203 | 204 | *self = MoveCursorToPosition::Move(MoveCursor::new(cursor, scroll)); 205 | continue; 206 | } 207 | MoveCursorToPosition::Move(move_cursor) => break Some(move_cursor), 208 | } 209 | } 210 | } 211 | } 212 | 213 | enum PrintableItem<'a> { 214 | Str(&'a str), 215 | Newline, 216 | } 217 | 218 | struct Printable<'a, I> { 219 | s: &'a str, 220 | newline: bool, 221 | iter: Option, 222 | } 223 | 224 | impl<'a, 'item, I> Printable<'a, I> 225 | where 226 | I: Iterator, 227 | 'item: 'a, 228 | { 229 | fn from_str(s: &'a str) -> Self { 230 | Self { 231 | s, 232 | newline: false, 233 | iter: None, 234 | } 235 | } 236 | 237 | fn from_iter(iter: I) -> Self { 238 | Self { 239 | s: "", 240 | newline: false, 241 | iter: Some(iter), 242 | } 243 | } 244 | 245 | fn next_item(&mut self, max_chars: usize) -> Option> { 246 | if self.newline { 247 | self.newline = false; 248 | Some(PrintableItem::Newline) 249 | } else { 250 | let s = if self.s.is_empty() { 251 | if let Some(iter) = &mut self.iter { 252 | iter.next()? 253 | } else { 254 | return None; 255 | } 256 | } else { 257 | self.s 258 | }; 259 | 260 | let split_at_char = max_chars.min(s.chars().count()); 261 | let split_at_byte = s 262 | .char_indices() 263 | .nth(split_at_char) 264 | .map(|(index, _)| index) 265 | .unwrap_or(s.len()); 266 | 267 | let (s, rest) = s.split_at(split_at_byte); 268 | 269 | if split_at_char == max_chars { 270 | self.newline = true 271 | } 272 | 273 | self.s = rest; 274 | Some(PrintableItem::Str(s)) 275 | } 276 | } 277 | } 278 | 279 | // #[cfg_attr(test, derive(Debug))] 280 | enum Step<'a, I> { 281 | Print(Printable<'a, I>), 282 | Move(MoveCursorToPosition), 283 | MoveCursorToEdge, 284 | GetPosition, 285 | SavePosition, 286 | RestorePosition, 287 | ClearLine, 288 | Erase, 289 | Newline, 290 | Bell, 291 | EndOfString, 292 | Abort, 293 | Done, 294 | } 295 | 296 | impl<'a, 'item, I> Step<'a, I> 297 | where 298 | I: Iterator, 299 | 'item: 'a, 300 | { 301 | fn transition( 302 | &mut self, 303 | new_state: Step<'a, I>, 304 | output: OutputItem<'a>, 305 | ) -> Option> { 306 | *self = new_state; 307 | Some(output) 308 | } 309 | 310 | fn advance(&mut self, terminal: &mut Terminal) -> Option> { 311 | match self { 312 | Print(printable) => { 313 | if let Some(item) = printable.next_item(terminal.columns_remaining()) { 314 | let s = match item { 315 | PrintableItem::Str(s) => { 316 | let position = terminal.relative_position(s.chars().count() as isize); 317 | terminal.move_cursor(position); 318 | 319 | s 320 | } 321 | PrintableItem::Newline => "\n\r", 322 | }; 323 | 324 | Some(OutputItem::Slice(s.as_bytes())) 325 | } else { 326 | *self = Step::Done; 327 | None 328 | } 329 | } 330 | Move(pos) => { 331 | if let Some(move_cursor) = pos.get_move_cursor(terminal) { 332 | if let Some(byte) = move_cursor.next() { 333 | return Some(byte); 334 | } 335 | } 336 | 337 | *self = Step::Done; 338 | None 339 | } 340 | MoveCursorToEdge => self.transition(Step::Done, OutputItem::Slice(b"\x1b[999;999H")), 341 | Erase => self.transition(Step::Done, OutputItem::Slice("\x1b[J".as_bytes())), 342 | Newline => { 343 | let mut position = terminal.get_position(); 344 | position.row += 1; 345 | position.column = 0; 346 | terminal.move_cursor(position); 347 | 348 | self.transition(Step::Done, OutputItem::Slice("\n\r".as_bytes())) 349 | } 350 | Bell => self.transition(Step::Done, OutputItem::Slice("\x07".as_bytes())), 351 | EndOfString => self.transition(Step::Done, OutputItem::EndOfString), 352 | Abort => self.transition(Step::Done, OutputItem::Abort), 353 | ClearLine => { 354 | terminal.move_cursor_to_start_of_line(); 355 | 356 | self.transition(Step::Done, OutputItem::Slice("\r\x1b[J".as_bytes())) 357 | } 358 | GetPosition => self.transition(Step::Done, OutputItem::Slice("\x1b[6n".as_bytes())), 359 | SavePosition => self.transition(Step::Done, OutputItem::Slice(b"\x1b7")), 360 | RestorePosition => self.transition(Step::Done, OutputItem::Slice(b"\x1b8")), 361 | Done => None, 362 | } 363 | } 364 | } 365 | 366 | use Step::*; 367 | 368 | pub struct OutputIter<'a, 'item, I> { 369 | terminal: &'a mut Terminal, 370 | steps: [Option>; 4], 371 | pos: usize, 372 | _marker: PhantomData<&'item ()>, 373 | } 374 | 375 | impl<'a, 'item, I> Iterator for OutputIter<'a, 'item, I> 376 | where 377 | I: Iterator, 378 | 'item: 'a, 379 | { 380 | type Item = OutputItem<'a>; 381 | 382 | fn next(&mut self) -> Option { 383 | loop { 384 | if let Some(step) = self.steps.get_mut(self.pos) { 385 | if let Some(step) = step.as_mut() { 386 | if let Some(item) = step.advance(self.terminal) { 387 | break Some(item); 388 | } else { 389 | self.pos += 1; 390 | } 391 | } else { 392 | break None; 393 | } 394 | } else { 395 | break None; 396 | } 397 | } 398 | } 399 | } 400 | 401 | fn byte_position(s: &str, char_pos: usize) -> usize { 402 | s.char_indices() 403 | .skip(char_pos) 404 | .map(|(pos, _)| pos) 405 | .next() 406 | .unwrap_or(s.len()) 407 | } 408 | 409 | pub struct Output<'a, B: Buffer, I> { 410 | prompt: &'a Prompt, 411 | buffer: &'a LineBuffer, 412 | terminal: &'a mut Terminal, 413 | action: OutputAction, 414 | } 415 | 416 | impl<'a, 'item, B, I> Output<'a, B, I> 417 | where 418 | B: Buffer, 419 | I: Iterator + Clone, 420 | { 421 | pub fn new( 422 | prompt: &'a Prompt, 423 | buffer: &'a LineBuffer, 424 | terminal: &'a mut Terminal, 425 | action: OutputAction, 426 | ) -> Self { 427 | Self { 428 | prompt, 429 | buffer, 430 | terminal, 431 | action, 432 | } 433 | } 434 | 435 | fn offset_from_position(&self, position: Position) -> usize { 436 | self.terminal.offset_from_position(position) as usize - self.prompt.len() 437 | } 438 | 439 | fn current_offset(&self) -> usize { 440 | self.offset_from_position(self.terminal.get_position()) 441 | } 442 | 443 | fn buffer_after_position(&self, position: Position) -> &'a str { 444 | let offset = self.offset_from_position(position); 445 | let s = self.buffer.as_str(); 446 | 447 | let pos = byte_position(s, offset); 448 | 449 | &s[pos..] 450 | } 451 | 452 | fn new_position(&self, cursor_move: CursorMove) -> Position { 453 | match cursor_move { 454 | CursorMove::Forward => self.terminal.relative_position(1), 455 | CursorMove::Back => self.terminal.relative_position(-1), 456 | CursorMove::Start => { 457 | let pos = self.current_offset() as isize; 458 | self.terminal.relative_position(-pos) 459 | } 460 | CursorMove::End => { 461 | let pos = self.current_offset() as isize; 462 | let len = self.buffer.as_str().chars().count() as isize; 463 | #[cfg(test)] 464 | dbg!(pos, len); 465 | self.terminal.relative_position(len - pos) 466 | } 467 | } 468 | } 469 | 470 | #[cfg(test)] 471 | pub fn into_vec(self) -> Vec { 472 | self.into_iter() 473 | .flat_map(|item| item.get_bytes().unwrap().to_vec()) 474 | .collect::>() 475 | } 476 | } 477 | 478 | impl<'a, 'item, B, I> IntoIterator for Output<'a, B, I> 479 | where 480 | B: Buffer, 481 | I: Iterator + Clone, 482 | 'item: 'a, 483 | { 484 | type Item = OutputItem<'a>; 485 | type IntoIter = OutputIter<'a, 'item, I>; 486 | 487 | fn into_iter(self) -> Self::IntoIter { 488 | fn pack(array: [T; IN]) -> [Option; OUT] { 489 | const { 490 | assert!(IN <= OUT); 491 | } 492 | 493 | let mut steps = [(); OUT].map(|()| None); 494 | 495 | for (i, step) in array.into_iter().enumerate() { 496 | steps[i] = Some(step); 497 | } 498 | 499 | steps 500 | } 501 | 502 | let steps = match self.action { 503 | OutputAction::MoveCursor(cursor_move) => { 504 | let position = self.new_position(cursor_move); 505 | 506 | let offset = 507 | self.terminal.offset_from_position(position) - self.prompt.len() as isize; 508 | let buffer_len = self.buffer.as_str().chars().count() as isize; 509 | 510 | if offset >= 0 && offset <= buffer_len { 511 | pack([Move(MoveCursorToPosition::new( 512 | self.new_position(cursor_move), 513 | ))]) 514 | } else { 515 | pack([Bell]) 516 | } 517 | } 518 | OutputAction::PrintBufferAndMoveCursorForward => pack([ 519 | Print(Printable::from_str( 520 | self.buffer_after_position(self.terminal.get_position()), 521 | )), 522 | Move(MoveCursorToPosition::new( 523 | self.terminal.relative_position(1), 524 | )), 525 | ]), 526 | OutputAction::EraseAfterCursor => pack([Erase]), 527 | OutputAction::EraseAndPrintBuffer => { 528 | let position = self.terminal.get_position(); 529 | 530 | pack([ 531 | Erase, 532 | Print(Printable::from_str(self.buffer_after_position(position))), 533 | Move(MoveCursorToPosition::new(position)), 534 | ]) 535 | } 536 | 537 | OutputAction::ClearScreen => { 538 | let rows = self.terminal.scroll_to_top(); 539 | self.terminal.move_cursor(Position::new(0, 0)); 540 | 541 | pack([ 542 | Move(MoveCursorToPosition::Move(MoveCursor::new( 543 | Cursor::new(0, 0), 544 | rows, 545 | ))), 546 | Erase, 547 | Print(Printable::from_iter(self.prompt.iter())), 548 | ]) 549 | } 550 | OutputAction::ClearLine => pack([ 551 | Move(MoveCursorToPosition::new( 552 | self.new_position(CursorMove::Start), 553 | )), 554 | Erase, 555 | ]), 556 | OutputAction::MoveCursorBackAndPrintBufferAndMoveForward => { 557 | let position = self.terminal.relative_position(-1); 558 | 559 | pack([ 560 | Move(MoveCursorToPosition::new(position)), 561 | Print(Printable::from_str(self.buffer_after_position(position))), 562 | Move(MoveCursorToPosition::new(self.terminal.get_position())), 563 | ]) 564 | } 565 | OutputAction::MoveCursorAndEraseAndPrintBuffer(steps) => { 566 | let position = self.terminal.relative_position(steps); 567 | 568 | pack([ 569 | Move(MoveCursorToPosition::new(position)), 570 | Erase, 571 | Print(Printable::from_str(self.buffer_after_position(position))), 572 | Move(MoveCursorToPosition::new(position)), 573 | ]) 574 | } 575 | OutputAction::RingBell => pack([Bell]), 576 | OutputAction::ClearAndPrintPrompt => pack([ 577 | ClearLine, 578 | Print(Printable::from_iter(self.prompt.iter())), 579 | GetPosition, 580 | ]), 581 | OutputAction::ClearAndPrintBuffer => { 582 | let position = self.new_position(CursorMove::Start); 583 | 584 | pack([ 585 | Move(MoveCursorToPosition::new(position)), 586 | Erase, 587 | Print(Printable::from_str(self.buffer.as_str())), 588 | ]) 589 | } 590 | OutputAction::ProbeSize => { 591 | pack([SavePosition, MoveCursorToEdge, GetPosition, RestorePosition]) 592 | } 593 | 594 | OutputAction::Done => pack([Newline, EndOfString]), 595 | OutputAction::Abort => pack([Newline, Abort]), 596 | OutputAction::Nothing => pack([]), 597 | }; 598 | 599 | OutputIter { 600 | terminal: self.terminal, 601 | steps, 602 | pos: 0, 603 | _marker: PhantomData, 604 | } 605 | } 606 | } 607 | 608 | #[cfg(test)] 609 | mod tests { 610 | use std::string::String; 611 | 612 | use crate::core::StrIter; 613 | 614 | use super::*; 615 | 616 | use std::vec::Vec; 617 | 618 | #[test] 619 | fn uint_to_bytes() { 620 | fn to_string(n: usize) -> String { 621 | let uint: UintToBytes = UintToBytes::from_uint(n).unwrap(); 622 | 623 | String::from_utf8(uint.as_bytes().to_vec()).unwrap() 624 | } 625 | 626 | assert_eq!(to_string::<4>(0), "0"); 627 | 628 | assert_eq!(to_string::<4>(42), "42"); 629 | 630 | assert_eq!(to_string::<4>(10), "10"); 631 | 632 | assert_eq!(to_string::<4>(9999), "9999"); 633 | } 634 | 635 | #[test] 636 | fn move_cursor() { 637 | fn to_string(cm: MoveCursor) -> String { 638 | String::from_utf8( 639 | cm.flat_map(|item| { 640 | if let Some(bytes) = item.get_bytes() { 641 | bytes.to_vec() 642 | } else { 643 | vec![] 644 | } 645 | }) 646 | .collect(), 647 | ) 648 | .unwrap() 649 | } 650 | 651 | assert_eq!( 652 | to_string(MoveCursor::new(Cursor::new(42, 0), 0)), 653 | "\x1b[43;1H" 654 | ); 655 | 656 | assert_eq!( 657 | to_string(MoveCursor::new(Cursor::new(0, 42), 0)), 658 | "\x1b[1;43H" 659 | ); 660 | 661 | assert_eq!( 662 | to_string(MoveCursor::new(Cursor::new(42, 43), 0)), 663 | "\x1b[43;44H" 664 | ); 665 | 666 | assert_eq!( 667 | to_string(MoveCursor::new(Cursor::new(0, 0), 0)), 668 | "\x1b[1;1H" 669 | ); 670 | 671 | assert_eq!( 672 | to_string(MoveCursor::new(Cursor::new(0, 9), 0)), 673 | "\x1b[1;10H" 674 | ); 675 | 676 | assert_eq!( 677 | to_string(MoveCursor::new(Cursor::new(0, 0), 1)), 678 | "\x1b[1S\x1b[1;1H" 679 | ); 680 | 681 | assert_eq!( 682 | to_string(MoveCursor::new(Cursor::new(0, 0), -1)), 683 | "\x1b[1T\x1b[1;1H" 684 | ); 685 | } 686 | 687 | #[test] 688 | fn step() { 689 | fn to_string<'a>(mut step: Step<'a, StrIter<'a>>, terminal: &mut Terminal) -> String { 690 | let mut bytes = Vec::new(); 691 | 692 | while let Some(item) = step.advance(terminal) { 693 | if let Some(slice) = item.get_bytes() { 694 | for b in slice { 695 | bytes.push(*b); 696 | } 697 | } 698 | } 699 | 700 | String::from_utf8(bytes).unwrap() 701 | } 702 | 703 | let mut terminal = Terminal::new(4, 10, Cursor::new(0, 0)); 704 | 705 | assert_eq!( 706 | to_string( 707 | Step::Print(Printable::from_str("01234567890123456789")), 708 | &mut terminal 709 | ), 710 | "0123456789\n\r0123456789\n\r" 711 | ); 712 | 713 | assert_eq!( 714 | to_string(Step::Print(Printable::from_str("01234")), &mut terminal), 715 | "01234" 716 | ); 717 | 718 | assert_eq!( 719 | to_string( 720 | Step::Print(Printable::from_str("5678901234567890")), 721 | &mut terminal 722 | ), 723 | "56789\n\r0123456789\n\r0" 724 | ); 725 | 726 | assert_eq!(terminal.get_position(), Position::new(4, 1)); 727 | 728 | assert_eq!( 729 | to_string( 730 | Step::Move(MoveCursorToPosition::new(Position::new(0, 3))), 731 | &mut terminal 732 | ), 733 | "\x1b[1T\x1b[1;4H" 734 | ); 735 | 736 | assert_eq!(terminal.get_position(), Position::new(0, 3)); 737 | 738 | assert_eq!(to_string(Step::Erase, &mut terminal), "\x1b[J"); 739 | assert_eq!(to_string(Step::Newline, &mut terminal), "\n\r"); 740 | assert_eq!(to_string(Step::Bell, &mut terminal), "\x07"); 741 | assert_eq!(to_string(Step::Done, &mut terminal), ""); 742 | } 743 | 744 | #[test] 745 | fn byte_iterator() { 746 | fn to_string(output: Output<'_, B, StrIter>) -> String { 747 | String::from_utf8( 748 | output 749 | .into_iter() 750 | .flat_map(|item| { 751 | if let Some(bytes) = item.get_bytes() { 752 | bytes.to_vec() 753 | } else { 754 | vec![] 755 | } 756 | }) 757 | .collect(), 758 | ) 759 | .unwrap() 760 | } 761 | 762 | let prompt: Prompt = "> ".into(); 763 | let mut line_buffer = LineBuffer::new_unbounded(); 764 | let mut terminal = Terminal::new(4, 10, Cursor::new(0, 0)); 765 | 766 | let result = to_string(Output::new( 767 | &prompt, 768 | &line_buffer, 769 | &mut terminal, 770 | OutputAction::ClearAndPrintPrompt, 771 | )); 772 | 773 | assert_eq!(result, "\r\x1b[J> \x1b[6n"); 774 | 775 | line_buffer.insert_str(0, "Hello, world!").unwrap(); 776 | 777 | let result = to_string(Output::new( 778 | &prompt, 779 | &line_buffer, 780 | &mut terminal, 781 | OutputAction::PrintBufferAndMoveCursorForward, 782 | )); 783 | 784 | assert_eq!(result, "Hello, w\n\rorld!\x1b[1;4H"); 785 | 786 | assert_eq!(terminal.get_cursor(), Cursor::new(0, 3)); 787 | 788 | let result = to_string(Output::new( 789 | &prompt, 790 | &line_buffer, 791 | &mut terminal, 792 | OutputAction::MoveCursor(CursorMove::Start), 793 | )); 794 | 795 | assert_eq!(result, "\x1b[1;3H"); 796 | assert_eq!(terminal.get_cursor(), Cursor::new(0, 2)); 797 | } 798 | 799 | #[test] 800 | fn split_utf8() { 801 | fn to_string<'a>(mut step: Step<'a, StrIter<'a>>, terminal: &mut Terminal) -> String { 802 | let mut bytes = Vec::new(); 803 | 804 | while let Some(item) = step.advance(terminal) { 805 | if let Some(slice) = item.get_bytes() { 806 | for b in slice { 807 | bytes.push(*b); 808 | } 809 | } 810 | } 811 | 812 | String::from_utf8(bytes).unwrap() 813 | } 814 | 815 | let mut terminal = Terminal::new(4, 10, Cursor::new(0, 0)); 816 | 817 | assert_eq!( 818 | to_string( 819 | Step::Print(Printable::from_str("aadfåpadfåaåfåaadåappaåadå")), 820 | &mut terminal 821 | ), 822 | "aadfåpadfå\n\raåfåaadåap\n\rpaåadå" 823 | ); 824 | } 825 | } 826 | -------------------------------------------------------------------------------- /noline/src/sync_editor.rs: -------------------------------------------------------------------------------- 1 | //! Line editor for synchronous IO. 2 | //! 3 | //! The editor takes a struct implementing the [`embedded_io::Read`] and [`embedded_io::Write`] 4 | //! traits. 5 | //! 6 | //! Use the [`crate::builder::EditorBuilder`] to build an editor. 7 | use embedded_io::{Read, ReadExactError, Write}; 8 | 9 | use crate::error::NolineError; 10 | 11 | use crate::history::{get_history_entries, CircularSlice, History}; 12 | use crate::line_buffer::{Buffer, LineBuffer}; 13 | 14 | use crate::core::{Line, Prompt}; 15 | use crate::output::{Output, OutputItem}; 16 | use crate::terminal::Terminal; 17 | 18 | /// Line editor for synchronous IO 19 | /// 20 | /// It is recommended to use [`crate::builder::EditorBuilder`] to build an Editor. 21 | pub struct Editor 22 | where 23 | B: Buffer, 24 | H: History, 25 | { 26 | buffer: LineBuffer, 27 | terminal: Terminal, 28 | history: H, 29 | } 30 | 31 | impl From for NolineError 32 | where 33 | E: embedded_io::Error, 34 | { 35 | fn from(value: E) -> Self { 36 | NolineError::IoError(value.kind()) 37 | } 38 | } 39 | 40 | impl Editor 41 | where 42 | B: Buffer, 43 | H: History, 44 | { 45 | /// Create and initialize line editor 46 | pub fn new( 47 | buffer: LineBuffer, 48 | history: H, 49 | _io: &mut IO, 50 | ) -> Result { 51 | let terminal = Terminal::default(); 52 | 53 | Ok(Self { 54 | buffer, 55 | terminal, 56 | history, 57 | }) 58 | } 59 | 60 | fn handle_output<'a, 'item, IO, I>( 61 | output: Output<'a, B, I>, 62 | io: &mut IO, 63 | ) -> Result, NolineError> 64 | where 65 | IO: Read + Write, 66 | I: Iterator + Clone, 67 | { 68 | for item in output { 69 | if let Some(bytes) = item.get_bytes() { 70 | io.write(bytes)?; 71 | } 72 | 73 | io.flush()?; 74 | 75 | match item { 76 | OutputItem::EndOfString => return Ok(Some(())), 77 | OutputItem::Abort => return Err(NolineError::Aborted), 78 | _ => (), 79 | } 80 | } 81 | 82 | Ok(None) 83 | } 84 | 85 | fn read_byte(io: &mut IO) -> Result 86 | where 87 | IO: Read + Write, 88 | { 89 | let mut buf = [0x8; 1]; 90 | 91 | match io.read_exact(&mut buf) { 92 | Ok(_) => Ok(buf[0]), 93 | Err(err) => match err { 94 | ReadExactError::UnexpectedEof => Err(NolineError::Aborted), 95 | ReadExactError::Other(err) => Err(err)?, 96 | }, 97 | } 98 | } 99 | 100 | /// Read line from `stdin` 101 | pub fn readline<'a, 'item, IO, I>( 102 | &'a mut self, 103 | prompt: impl Into>, 104 | io: &mut IO, 105 | ) -> Result<&str, NolineError> 106 | where 107 | IO: Read + Write, 108 | I: Iterator + Clone, 109 | { 110 | let mut line = Line::new( 111 | prompt, 112 | &mut self.buffer, 113 | &mut self.terminal, 114 | &mut self.history, 115 | ); 116 | 117 | let mut reset = line.reset(); 118 | 119 | Self::handle_output(reset.start(), io)?; 120 | 121 | loop { 122 | let byte = Self::read_byte(io)?; 123 | 124 | if let Some(output) = reset.advance(byte) { 125 | Self::handle_output(output, io)?; 126 | } else { 127 | break; 128 | } 129 | } 130 | 131 | loop { 132 | let byte = Self::read_byte(io)?; 133 | 134 | if Self::handle_output(line.advance(byte), io)?.is_some() { 135 | break; 136 | } 137 | } 138 | 139 | Ok(self.buffer.as_str()) 140 | } 141 | 142 | /// Load history from iterator 143 | pub fn load_history<'a>(&mut self, entries: impl Iterator) -> usize { 144 | self.history.load_entries(entries) 145 | } 146 | 147 | /// Get history as iterator over circular slices 148 | pub fn get_history(&self) -> impl Iterator> { 149 | get_history_entries(&self.history) 150 | } 151 | } 152 | 153 | #[cfg(test)] 154 | pub mod tests { 155 | //! IO implementation for `std`. Requires feature `std`. 156 | 157 | use std::string::ToString; 158 | use std::{thread, vec::Vec}; 159 | 160 | use crossbeam::channel::{unbounded, Receiver, Sender}; 161 | use embedded_io::{Read, Write}; 162 | 163 | use crate::builder::EditorBuilder; 164 | use crate::testlib::{test_cases, test_editor_with_case, MockTerminal}; 165 | 166 | struct MockStdout { 167 | buffer: Vec, 168 | tx: Sender, 169 | } 170 | 171 | impl MockStdout { 172 | fn new(tx: Sender) -> Self { 173 | Self { 174 | buffer: Vec::new(), 175 | tx, 176 | } 177 | } 178 | } 179 | 180 | struct MockStdin { 181 | rx: Receiver, 182 | } 183 | 184 | impl MockStdin { 185 | fn new(rx: Receiver) -> Self { 186 | Self { rx } 187 | } 188 | } 189 | 190 | struct MockIO { 191 | stdin: MockStdin, 192 | stdout: MockStdout, 193 | } 194 | 195 | impl MockIO { 196 | fn new(stdin: MockStdin, stdout: MockStdout) -> Self { 197 | Self { stdout, stdin } 198 | } 199 | 200 | fn from_terminal(terminal: &mut MockTerminal) -> Self { 201 | let (tx, rx) = terminal.take_io(); 202 | 203 | Self::new(MockStdin::new(rx), MockStdout::new(tx.unwrap())) 204 | } 205 | 206 | fn get_pipes(self) -> (MockStdin, MockStdout) { 207 | (self.stdin, self.stdout) 208 | } 209 | } 210 | 211 | impl embedded_io::ErrorType for MockIO { 212 | type Error = embedded_io::ErrorKind; 213 | } 214 | 215 | impl embedded_io::Read for MockIO { 216 | fn read(&mut self, buf: &mut [u8]) -> Result { 217 | for place in &mut *buf { 218 | match self.stdin.rx.recv() { 219 | Ok(byte) => *place = byte, 220 | // This should never happen as the error type is Infalliable 221 | Err(_) => return Err(Self::Error::Other), 222 | } 223 | } 224 | 225 | Ok(buf.len()) 226 | } 227 | } 228 | 229 | impl embedded_io::Write for MockIO { 230 | fn write(&mut self, buf: &[u8]) -> Result { 231 | self.stdout.buffer.extend(buf); 232 | Ok(buf.len()) 233 | } 234 | 235 | fn flush(&mut self) -> Result<(), Self::Error> { 236 | for byte in self.stdout.buffer.drain(0..) { 237 | self.stdout.tx.send(byte).unwrap(); 238 | } 239 | 240 | Ok(()) 241 | } 242 | } 243 | 244 | impl core::fmt::Write for MockIO { 245 | fn write_str(&mut self, s: &str) -> core::fmt::Result { 246 | self.write(s.as_bytes()).or(Err(core::fmt::Error))?; 247 | Ok(()) 248 | } 249 | } 250 | 251 | #[test] 252 | fn simple_test() { 253 | let (input_tx, input_rx) = unbounded(); 254 | let (output_tx, output_rx) = unbounded(); 255 | 256 | let mut io = MockIO::new(MockStdin::new(input_rx), MockStdout::new(output_tx)); 257 | 258 | let handle = thread::spawn(move || { 259 | let mut editor = EditorBuilder::new_unbounded().build_sync(&mut io).unwrap(); 260 | 261 | if let Ok(s) = editor.readline("> ", &mut io) { 262 | Some(s.to_string()) 263 | } else { 264 | None 265 | } 266 | }); 267 | 268 | for &b in b"\x1b7\x1b[999;999H\x1b[6n\x1b8" { 269 | let received = output_rx 270 | .recv_timeout(::core::time::Duration::from_millis(1000)) 271 | .unwrap(); 272 | println!("Received {:x}, expected: {:x}", received, b); 273 | assert_eq!(received, b); 274 | } 275 | 276 | for &b in b"\x1b[20;80R" { 277 | input_tx.send(b).unwrap(); 278 | } 279 | 280 | for &b in b"\r\x1b[J> \x1b[6n" { 281 | let received = output_rx 282 | .recv_timeout(::core::time::Duration::from_millis(1000)) 283 | .unwrap(); 284 | println!("Received {:x}, expected: {:x}", received, b); 285 | assert_eq!(received, b); 286 | } 287 | 288 | for &b in b"\x1b[1;3R" { 289 | input_tx.send(b).unwrap(); 290 | } 291 | 292 | for &b in "abc\r".as_bytes() { 293 | input_tx.send(b).unwrap(); 294 | } 295 | 296 | assert_eq!(handle.join().unwrap(), Some("abc".to_string())); 297 | } 298 | 299 | #[test] 300 | fn mock_stdin() { 301 | let (tx, rx) = unbounded(); 302 | 303 | let mut io = MockIO::new(MockStdin::new(rx), MockStdout::new(tx)); 304 | for i in 0u8..10 { 305 | io.write(&[i]).unwrap(); 306 | } 307 | 308 | io.flush().unwrap(); 309 | 310 | let mut buf = [0]; 311 | for i in 0..10 { 312 | io.read(&mut buf).unwrap(); 313 | 314 | assert_eq!(buf[0], i); 315 | } 316 | } 317 | 318 | #[test] 319 | fn editor() { 320 | let prompt = "> "; 321 | 322 | for case in test_cases() { 323 | test_editor_with_case( 324 | case, 325 | prompt, 326 | |term| MockIO::from_terminal(term).get_pipes(), 327 | |(stdin, stdout), string_tx| { 328 | thread::spawn(move || { 329 | let mut io = MockIO::new(stdin, stdout); 330 | let mut editor = EditorBuilder::new_unbounded() 331 | .with_unbounded_history() 332 | .build_sync(&mut io) 333 | .unwrap(); 334 | 335 | while let Ok(s) = editor.readline(prompt, &mut io) { 336 | string_tx.send(s.to_string()).unwrap(); 337 | } 338 | }) 339 | }, 340 | ) 341 | } 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /noline/src/terminal.rs: -------------------------------------------------------------------------------- 1 | fn distance_from_window(start: isize, end: isize, point: isize) -> isize { 2 | if point < start { 3 | point - start 4 | } else if point > end { 5 | point - end 6 | } else { 7 | 0 8 | } 9 | } 10 | 11 | #[cfg_attr(test, derive(Debug))] 12 | #[derive(Copy, Clone, PartialEq, Eq)] 13 | pub struct Cursor { 14 | pub row: usize, 15 | pub column: usize, 16 | } 17 | 18 | impl Cursor { 19 | pub fn new(row: usize, column: usize) -> Self { 20 | Self { row, column } 21 | } 22 | } 23 | 24 | #[cfg_attr(test, derive(Debug))] 25 | #[derive(Copy, Clone, PartialEq, Eq)] 26 | pub struct Position { 27 | pub row: usize, 28 | pub column: usize, 29 | } 30 | 31 | impl Position { 32 | pub fn new(row: usize, column: usize) -> Self { 33 | Self { row, column } 34 | } 35 | } 36 | 37 | #[cfg_attr(test, derive(Debug, PartialEq, Eq))] 38 | pub struct Terminal { 39 | rows: usize, 40 | columns: usize, 41 | cursor: Cursor, 42 | row_offset: isize, 43 | } 44 | 45 | impl Default for Terminal { 46 | fn default() -> Self { 47 | Self::new(24, 80, Cursor::new(0, 0)) 48 | } 49 | } 50 | 51 | impl Terminal { 52 | pub fn new(rows: usize, columns: usize, cursor: Cursor) -> Self { 53 | let row_offset = -(cursor.row as isize); 54 | 55 | Self { 56 | rows, 57 | columns, 58 | cursor, 59 | row_offset, 60 | } 61 | } 62 | 63 | pub fn resize(&mut self, rows: usize, columns: usize) { 64 | self.rows = rows; 65 | self.columns = columns; 66 | } 67 | 68 | pub fn reset(&mut self, cursor: Cursor) { 69 | self.cursor = cursor; 70 | self.row_offset = -(cursor.row as isize); 71 | } 72 | 73 | pub fn get_cursor(&self) -> Cursor { 74 | self.cursor 75 | } 76 | 77 | pub fn get_position(&self) -> Position { 78 | self.cursor_to_position(self.cursor) 79 | } 80 | 81 | pub fn scrolling_needed(&self, position: Position) -> isize { 82 | distance_from_window( 83 | self.row_offset, 84 | self.row_offset + self.rows as isize - 1, 85 | position.row as isize, 86 | ) 87 | } 88 | 89 | pub fn scroll_to_top(&mut self) -> isize { 90 | let rows = self.row_offset; 91 | self.row_offset = 0; 92 | 93 | rows 94 | } 95 | 96 | pub fn scroll(&mut self, rows: isize) { 97 | self.row_offset += rows; 98 | } 99 | 100 | pub fn move_cursor(&mut self, position: Position) -> isize { 101 | let rows = self.scrolling_needed(position); 102 | self.scroll(rows); 103 | 104 | #[cfg(test)] 105 | dbg!(rows, position); 106 | 107 | self.cursor = self 108 | .position_to_cursor(position) 109 | .unwrap_or_else(|| unreachable!()); 110 | 111 | rows 112 | } 113 | 114 | pub fn move_cursor_to_start_of_line(&mut self) { 115 | self.cursor.column = 0; 116 | } 117 | 118 | pub fn position_to_cursor(&self, position: Position) -> Option { 119 | let row = position.row as isize - self.row_offset; 120 | 121 | if row >= 0 && row < self.rows as isize { 122 | Some(Cursor::new(row as usize, position.column)) 123 | } else { 124 | None 125 | } 126 | } 127 | 128 | pub fn cursor_to_position(&self, position: Cursor) -> Position { 129 | #[cfg(test)] 130 | dbg!(self.row_offset); 131 | 132 | Position::new( 133 | (position.row as isize + self.row_offset) as usize, 134 | position.column, 135 | ) 136 | } 137 | 138 | pub fn offset_from_position(&self, position: Position) -> isize { 139 | position.row as isize * self.columns as isize + position.column as isize 140 | } 141 | 142 | pub fn current_offset(&self) -> isize { 143 | let position = self.cursor_to_position(self.cursor); 144 | self.offset_from_position(position) 145 | } 146 | 147 | fn position_from_offset(&self, offset: isize) -> Position { 148 | let row = offset.div_euclid(self.columns as isize); 149 | let column = offset.rem_euclid(self.columns as isize); 150 | Position::new(row as usize, column as usize) 151 | } 152 | 153 | pub fn relative_position(&self, steps: isize) -> Position { 154 | let offset = self.offset_from_position(self.cursor_to_position(self.cursor)); 155 | 156 | self.position_from_offset(offset + steps) 157 | } 158 | 159 | pub fn columns_remaining(&self) -> usize { 160 | self.columns - self.cursor.column 161 | } 162 | 163 | #[cfg(test)] 164 | pub fn get_size(&self) -> (usize, usize) { 165 | (self.rows, self.columns) 166 | } 167 | } 168 | 169 | #[cfg(test)] 170 | mod tests { 171 | use super::*; 172 | 173 | #[test] 174 | fn test_distance_from_window() { 175 | assert_eq!(distance_from_window(4, 8, 2), -2); 176 | assert_eq!(distance_from_window(4, 8, 4), 0); 177 | assert_eq!(distance_from_window(4, 8, 8), 0); 178 | assert_eq!(distance_from_window(4, 8, 10), 2); 179 | 180 | assert_eq!(distance_from_window(-3, 8, 2), 0); 181 | assert_eq!(distance_from_window(-3, 8, -5), -2); 182 | assert_eq!(distance_from_window(-3, 8, 9), 1); 183 | } 184 | 185 | #[test] 186 | fn position_from_top() { 187 | let term = Terminal::new(4, 10, Cursor::new(0, 0)); 188 | 189 | assert_eq!( 190 | term.cursor_to_position(term.get_cursor()), 191 | Position::new(0, 0) 192 | ); 193 | 194 | assert_eq!( 195 | term.cursor_to_position(Cursor::new(3, 9)), 196 | Position::new(3, 9) 197 | ); 198 | 199 | assert_eq!( 200 | term.cursor_to_position(Cursor::new(4, 9)), 201 | Position::new(4, 9) 202 | ); 203 | 204 | assert_eq!( 205 | term.position_to_cursor(Position::new(3, 9)), 206 | Some(Cursor::new(3, 9)) 207 | ); 208 | 209 | assert_eq!(term.position_to_cursor(Position::new(4, 9)), None); 210 | } 211 | 212 | #[test] 213 | fn position_from_second_line() { 214 | let term = Terminal::new(4, 10, Cursor::new(1, 0)); 215 | 216 | assert_eq!( 217 | term.cursor_to_position(term.get_cursor()), 218 | Position::new(0, 0) 219 | ); 220 | 221 | assert_eq!( 222 | term.cursor_to_position(Cursor::new(3, 9)), 223 | Position::new(2, 9) 224 | ); 225 | 226 | assert_eq!( 227 | term.position_to_cursor(Position::new(2, 9)), 228 | Some(Cursor::new(3, 9)) 229 | ); 230 | } 231 | 232 | #[test] 233 | fn position_scroll() { 234 | let mut term = Terminal::new(4, 10, Cursor::new(0, 0)); 235 | 236 | assert_eq!(term.move_cursor(Position::new(7, 0)), 4); 237 | 238 | assert_eq!( 239 | term.cursor_to_position(term.get_cursor()), 240 | Position::new(7, 0) 241 | ); 242 | 243 | assert_eq!( 244 | term.cursor_to_position(Cursor::new(3, 9)), 245 | Position::new(7, 9) 246 | ); 247 | 248 | assert_eq!( 249 | term.cursor_to_position(Cursor::new(0, 0)), 250 | Position::new(4, 0) 251 | ); 252 | 253 | assert_eq!(term.position_to_cursor(Position::new(2, 9)), None); 254 | } 255 | 256 | #[test] 257 | fn position_scroll_offset() { 258 | let mut term = Terminal::new(4, 10, Cursor::new(3, 9)); 259 | 260 | let position = term.relative_position(1); 261 | 262 | assert_eq!(position, Position::new(1, 0)); 263 | assert_eq!(term.position_to_cursor(position), None); 264 | 265 | assert_eq!(term.move_cursor(Position::new(1, 0)), 1); 266 | } 267 | 268 | #[test] 269 | fn move_cursor() { 270 | let mut term = Terminal::new(4, 10, Cursor::new(0, 0)); 271 | 272 | let pos = Position::new(3, 9); 273 | assert_eq!(term.scrolling_needed(pos), 0); 274 | 275 | assert_eq!(term.move_cursor(pos), 0); 276 | 277 | let pos = Position::new(4, 0); 278 | assert_eq!(term.scrolling_needed(pos), 1); 279 | 280 | assert_eq!(term.move_cursor(pos), 1); 281 | 282 | assert_eq!(term.get_cursor(), Cursor::new(3, 0)); 283 | assert_eq!(term.get_position(), Position::new(4, 0)); 284 | assert_eq!(term.current_offset(), 40); 285 | 286 | let pos = Position::new(0, 0); 287 | assert_eq!(term.scrolling_needed(pos), -1); 288 | 289 | assert_eq!(term.move_cursor(pos), -1); 290 | 291 | assert_eq!(term.get_cursor(), Cursor::new(0, 0)); 292 | assert_eq!(term.get_position(), Position::new(0, 0)); 293 | } 294 | 295 | #[test] 296 | fn offset() { 297 | let term = Terminal::new(4, 10, Cursor::new(1, 0)); 298 | 299 | assert_eq!(term.get_cursor(), Cursor::new(1, 0)); 300 | assert_eq!(term.get_position(), Position::new(0, 0)); 301 | assert_eq!(term.current_offset(), 0); 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /noline/src/testlib.rs: -------------------------------------------------------------------------------- 1 | use core::time::Duration; 2 | use std::string::String; 3 | use std::thread; 4 | use std::thread::JoinHandle; 5 | use std::vec::Vec; 6 | 7 | use crossbeam::channel::{unbounded, Receiver, Sender}; 8 | 9 | use crate::input::{Action, ControlCharacter, Parser, CSI}; 10 | use crate::terminal::Cursor; 11 | 12 | use ControlCharacter::*; 13 | 14 | pub mod csi { 15 | pub const UP: &str = "\x1b[A"; 16 | pub const DOWN: &str = "\x1b[B"; 17 | pub const LEFT: &str = "\x1b[D"; 18 | pub const RIGHT: &str = "\x1b[C"; 19 | pub const HOME: &str = "\x1b[1~"; 20 | pub const DELETE: &str = "\x1b[3~"; 21 | pub const END: &str = "\x1b[4~"; 22 | } 23 | 24 | pub struct MockTerminal { 25 | parser: Parser, 26 | screen: Vec>, 27 | pub cursor: Cursor, 28 | pub rows: usize, 29 | pub columns: usize, 30 | saved_cursor: Option, 31 | pub bell: bool, 32 | pub terminal_tx: Option>, 33 | pub terminal_rx: Receiver, 34 | pub keyboard_tx: Sender, 35 | pub keyboard_rx: Receiver, 36 | } 37 | 38 | impl MockTerminal { 39 | pub fn new(rows: usize, columns: usize, origin: Cursor) -> Self { 40 | let (terminal_tx, terminal_rx) = unbounded(); 41 | let (keyboard_tx, keyboard_rx) = unbounded(); 42 | 43 | Self { 44 | parser: Parser::new(), 45 | screen: vec![vec!['\0'; columns]; rows], 46 | cursor: origin, 47 | rows, 48 | columns, 49 | saved_cursor: None, 50 | bell: false, 51 | terminal_tx: Some(terminal_tx), 52 | terminal_rx, 53 | keyboard_tx, 54 | keyboard_rx, 55 | } 56 | } 57 | 58 | pub fn current_line(&mut self) -> &mut Vec { 59 | let cursor = self.get_cursor(); 60 | 61 | &mut self.screen[cursor.row] 62 | } 63 | 64 | pub fn screen_as_string(&self) -> String { 65 | self.screen 66 | .iter() 67 | .map(|v| v.iter().take_while(|&&c| c != '\0').collect::()) 68 | .filter(|s| !s.is_empty()) 69 | .collect::>() 70 | .join("\n") 71 | } 72 | 73 | pub fn current_line_as_string(&self) -> String { 74 | self.screen[self.cursor.row] 75 | .iter() 76 | .take_while(|&&c| c != '\0') 77 | .collect() 78 | } 79 | 80 | fn move_column(&mut self, steps: isize) { 81 | self.cursor.column = 82 | 0.max((self.cursor.column as isize + steps).min(self.columns as isize - 1)) as usize; 83 | dbg!(self.cursor.column); 84 | } 85 | 86 | fn scroll_up(&mut self, lines: usize) { 87 | for _ in 0..lines { 88 | self.screen.remove(0); 89 | self.screen.push(vec!['\0'; self.columns]); 90 | } 91 | } 92 | 93 | fn scroll_down(&mut self, lines: usize) { 94 | for _ in 0..lines { 95 | self.screen.pop(); 96 | self.screen.insert(0, vec!['\0'; self.columns]); 97 | } 98 | } 99 | 100 | pub fn advance(&mut self, byte: u8) -> Option> { 101 | let mock_term_action = self.parser.advance(byte); 102 | 103 | dbg!(mock_term_action); 104 | match mock_term_action { 105 | Action::Ignore => (), 106 | Action::Print(c) => { 107 | let pos = self.cursor.column; 108 | let line = self.current_line(); 109 | 110 | line[pos] = c.as_char(); 111 | self.move_column(1); 112 | } 113 | Action::ControlSequenceIntroducer(csi) => match csi { 114 | CSI::CUU(_) => unimplemented!(), 115 | CSI::CUD(_) => unimplemented!(), 116 | CSI::CUF(_) => unimplemented!(), 117 | CSI::CUB(_) => unimplemented!(), 118 | CSI::CPR(_, _) => unimplemented!(), 119 | CSI::CUP(row, column) => { 120 | self.cursor = Cursor::new( 121 | (row - 1).min(self.rows - 1), 122 | (column - 1).min(self.columns - 1), 123 | ); 124 | } 125 | CSI::ED(_) => { 126 | let cursor = self.get_cursor(); 127 | 128 | for row in cursor.row..self.rows { 129 | let start = if row == cursor.row { cursor.column } else { 0 }; 130 | for column in (start)..self.columns { 131 | self.screen[row][column] = '\0'; 132 | } 133 | } 134 | } 135 | CSI::DSR => { 136 | return Some( 137 | format!("\x1b[{};{}R", self.cursor.row + 1, self.cursor.column + 1) 138 | .bytes() 139 | .collect::>(), 140 | ); 141 | } 142 | CSI::Unknown(b) => { 143 | dbg!(b as char); 144 | unimplemented!() 145 | } 146 | CSI::SU(lines) => { 147 | self.scroll_up(lines); 148 | } 149 | CSI::SD(lines) => { 150 | self.scroll_down(lines); 151 | } 152 | CSI::Home => unimplemented!(), 153 | CSI::Delete => unimplemented!(), 154 | CSI::End => unimplemented!(), 155 | CSI::Invalid => unimplemented!(), 156 | }, 157 | Action::InvalidUtf8 => unreachable!(), 158 | Action::ControlCharacter(ctrl) => { 159 | dbg!(ctrl); 160 | 161 | match ctrl { 162 | ControlCharacter::CarriageReturn => self.cursor.column = 0, 163 | ControlCharacter::LineFeed => { 164 | if self.cursor.row + 1 == self.rows { 165 | self.scroll_up(1); 166 | } else { 167 | self.cursor.row += 1; 168 | } 169 | } 170 | ControlCharacter::CtrlG => self.bell = true, 171 | _ => (), 172 | } 173 | } 174 | Action::EscapeSequence(esc) => match esc { 175 | 0x37 => { 176 | self.saved_cursor = Some(self.cursor); 177 | } 178 | 0x38 => { 179 | let cursor = self.saved_cursor.unwrap(); 180 | self.cursor = cursor; 181 | } 182 | _ => { 183 | dbg!(esc); 184 | } 185 | }, 186 | } 187 | 188 | None 189 | } 190 | 191 | pub fn get_cursor(&self) -> Cursor { 192 | self.cursor 193 | } 194 | 195 | pub fn listen(&mut self) { 196 | while let Ok(b_in) = self.terminal_rx.recv() { 197 | if let Some(output) = self.advance(b_in) { 198 | for b_out in output { 199 | self.keyboard_tx.send(b_out).unwrap(); 200 | } 201 | } 202 | } 203 | } 204 | 205 | pub fn start_thread(mut self) -> JoinHandle { 206 | thread::spawn(move || { 207 | self.listen(); 208 | self 209 | }) 210 | } 211 | 212 | pub fn take_io(&mut self) -> (Option>, Receiver) { 213 | (self.terminal_tx.take(), self.keyboard_rx.clone()) 214 | } 215 | } 216 | 217 | impl ToByteVec for &str { 218 | fn to_byte_vec(self) -> Vec { 219 | self.bytes().collect() 220 | } 221 | } 222 | 223 | impl ToByteVec for ControlCharacter { 224 | fn to_byte_vec(self) -> Vec { 225 | [self.into()].into_iter().collect() 226 | } 227 | } 228 | 229 | impl ToByteVec for Vec { 230 | fn to_byte_vec(self) -> Vec { 231 | self.into_iter().map(|c| c.into()).collect() 232 | } 233 | } 234 | 235 | impl ToByteVec for [ControlCharacter; N] { 236 | fn to_byte_vec(self) -> Vec { 237 | self.into_iter().map(|c| c.into()).collect() 238 | } 239 | } 240 | 241 | impl ToByteVec for Vec<&str> { 242 | fn to_byte_vec(self) -> Vec { 243 | self.into_iter() 244 | .flat_map(|s| s.as_bytes().iter().copied()) 245 | .collect() 246 | } 247 | } 248 | 249 | impl ToByteVec for [&str; N] { 250 | fn to_byte_vec(self) -> Vec { 251 | self.into_iter() 252 | .flat_map(|s| s.as_bytes().iter().copied()) 253 | .collect() 254 | } 255 | } 256 | 257 | pub trait ToByteVec { 258 | fn to_byte_vec(self) -> Vec; 259 | } 260 | 261 | #[derive(Debug)] 262 | pub struct TestCase { 263 | pub input: Vec>, 264 | pub output: Vec, 265 | } 266 | 267 | impl TestCase { 268 | pub fn new( 269 | input: impl IntoIterator, 270 | output: impl IntoIterator>, 271 | ) -> Self { 272 | Self { 273 | input: input.into_iter().map(|item| item.to_byte_vec()).collect(), 274 | output: output.into_iter().map(|s| s.into()).collect(), 275 | } 276 | } 277 | 278 | pub fn screen_as_string(&self, prompt: &str, columns: usize) -> String { 279 | let mut screen = Vec::new(); 280 | let mut line = Vec::new(); 281 | 282 | line.extend(prompt.chars()); 283 | 284 | for s in &self.output { 285 | for c in s.chars() { 286 | line.push(c); 287 | 288 | if line.len() >= columns { 289 | screen.extend(line.drain(0..)); 290 | screen.push('\n'); 291 | } 292 | } 293 | 294 | if !line.is_empty() { 295 | screen.extend(line.drain(0..)); 296 | screen.push('\n'); 297 | screen.extend(prompt.chars()); 298 | } 299 | } 300 | 301 | screen.into_iter().collect() 302 | } 303 | } 304 | 305 | struct InputBuilder { 306 | items: Vec, 307 | } 308 | 309 | impl InputBuilder { 310 | fn new() -> Self { 311 | Self { items: Vec::new() } 312 | } 313 | 314 | fn add(&mut self, input: impl ToByteVec) { 315 | self.items.extend(input.to_byte_vec().iter()); 316 | } 317 | } 318 | 319 | impl ToByteVec for InputBuilder { 320 | fn to_byte_vec(self) -> Vec { 321 | self.items 322 | } 323 | } 324 | 325 | pub fn test_cases() -> Vec { 326 | vec![ 327 | TestCase::new(["Hello, World!"], ["Hello, World!"]), 328 | { 329 | let mut input = InputBuilder::new(); 330 | 331 | input.add("abc"); 332 | input.add(csi::LEFT); 333 | input.add(CtrlD); 334 | input.add("de"); 335 | 336 | TestCase::new([input], ["abde"]) 337 | }, 338 | TestCase::new(["abc", "def"], ["abc", "def"]), 339 | ] 340 | } 341 | 342 | pub fn test_editor_with_case( 343 | case: TestCase, 344 | prompt: &str, 345 | get_io: impl FnOnce(&mut MockTerminal) -> IO, 346 | spawn_thread: impl FnOnce(IO, Sender) -> JoinHandle<()>, 347 | ) { 348 | let (rows, columns) = (20, 80); 349 | 350 | let (string_tx, string_rx) = unbounded(); 351 | 352 | let mut term = MockTerminal::new(rows, columns, Cursor::new(0, 0)); 353 | 354 | let keyboard_tx = term.keyboard_tx.clone(); 355 | 356 | let io = get_io(&mut term); 357 | 358 | let term = term.start_thread(); 359 | let handle = spawn_thread(io, string_tx); 360 | 361 | let output: Vec = case 362 | .input 363 | .iter() 364 | .map(|seq| { 365 | // To avoid race with prompt reset, we need to wait a 366 | // little. This is not ideal, but will do for now. 367 | thread::sleep(core::time::Duration::from_millis(100)); 368 | 369 | for &b in seq { 370 | keyboard_tx.send(b).unwrap(); 371 | } 372 | 373 | keyboard_tx.send(0xd).unwrap(); 374 | 375 | string_rx.recv().unwrap() 376 | }) 377 | .collect(); 378 | 379 | // Added delay to prevent race with terminal reset 380 | std::thread::sleep(Duration::from_millis(100)); 381 | 382 | keyboard_tx.send(0x3).unwrap(); 383 | 384 | drop(keyboard_tx); 385 | let term = term.join().unwrap(); 386 | 387 | handle.join().unwrap(); 388 | 389 | assert_eq!(output.len(), case.output.len()); 390 | 391 | for (seen, expected) in output.iter().zip(case.output.iter()) { 392 | assert_eq!(seen, expected); 393 | } 394 | 395 | assert_eq!( 396 | term.screen_as_string(), 397 | case.screen_as_string(prompt, columns) 398 | ); 399 | } 400 | -------------------------------------------------------------------------------- /noline/src/utf8.rs: -------------------------------------------------------------------------------- 1 | enum Utf8ByteType { 2 | SingleByte, 3 | StartTwoByte, 4 | StartThreeByte, 5 | StartFourByte, 6 | Continuation, 7 | Invalid, 8 | } 9 | 10 | trait Utf8Byte { 11 | fn utf8_byte_type(&self) -> Utf8ByteType; 12 | fn utf8_is_continuation(&self) -> bool; 13 | } 14 | 15 | impl Utf8Byte for u8 { 16 | fn utf8_byte_type(&self) -> Utf8ByteType { 17 | let byte = *self; 18 | 19 | if byte & 0b10000000 == 0 { 20 | Utf8ByteType::SingleByte 21 | } else if byte & 0b11000000 == 0b10000000 { 22 | Utf8ByteType::Continuation 23 | } else if byte & 0b11100000 == 0b11000000 { 24 | Utf8ByteType::StartTwoByte 25 | } else if byte & 0b11110000 == 0b11100000 { 26 | Utf8ByteType::StartThreeByte 27 | } else if byte & 0b11111000 == 0b11110000 { 28 | Utf8ByteType::StartFourByte 29 | } else { 30 | Utf8ByteType::Invalid 31 | } 32 | } 33 | 34 | fn utf8_is_continuation(&self) -> bool { 35 | matches!(self.utf8_byte_type(), Utf8ByteType::Continuation) 36 | } 37 | } 38 | 39 | #[derive(Debug, Eq, PartialEq)] 40 | enum Utf8DecoderState { 41 | New, 42 | ExpectingOneByte, 43 | ExpectingTwoBytes, 44 | ExpectingThreeBytes, 45 | Done, 46 | } 47 | 48 | #[derive(Eq, PartialEq, Copy, Clone)] 49 | pub struct Utf8Char { 50 | buf: [u8; 4], 51 | len: u8, 52 | } 53 | 54 | #[cfg(test)] 55 | impl std::fmt::Debug for Utf8Char { 56 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 57 | f.debug_tuple("Utf8Char").field(&self.as_char()).finish() 58 | } 59 | } 60 | 61 | impl Utf8Char { 62 | fn new(bytes: &[u8; 4], len: usize) -> Self { 63 | Self { 64 | len: len as u8, 65 | buf: *bytes, 66 | } 67 | } 68 | 69 | #[cfg(test)] 70 | pub(crate) fn from_str(s: &str) -> Self { 71 | let bytes = s.as_bytes(); 72 | assert!(bytes.len() <= 4); 73 | 74 | let mut c = Self { 75 | len: bytes.len() as u8, 76 | buf: [0; 4], 77 | }; 78 | 79 | for (i, b) in bytes.iter().enumerate() { 80 | c.buf[i] = *b; 81 | } 82 | 83 | c 84 | } 85 | 86 | #[cfg(test)] 87 | pub(crate) fn as_char(&self) -> char { 88 | char::from_u32( 89 | self.as_bytes() 90 | .iter() 91 | .fold(0, |codepoint, &b| match b.utf8_byte_type() { 92 | Utf8ByteType::SingleByte => b as u32, 93 | Utf8ByteType::StartTwoByte => (b & 0x1f) as u32, 94 | Utf8ByteType::StartThreeByte => (b & 0xf) as u32, 95 | Utf8ByteType::StartFourByte => (b & 0x7) as u32, 96 | Utf8ByteType::Continuation => (codepoint << 6) | (b & 0x3f) as u32, 97 | Utf8ByteType::Invalid => unreachable!(), 98 | }), 99 | ) 100 | .unwrap() 101 | } 102 | 103 | pub fn as_bytes(&self) -> &[u8] { 104 | &self.buf[0..(self.len as usize)] 105 | } 106 | } 107 | 108 | #[cfg_attr(test, derive(Debug))] 109 | #[derive(Eq, PartialEq)] 110 | pub enum Utf8DecoderStatus { 111 | Continuation, 112 | Done(Utf8Char), 113 | Error, 114 | } 115 | 116 | #[derive(Debug, Eq, PartialEq)] 117 | pub struct Utf8Decoder { 118 | state: Utf8DecoderState, 119 | buf: [u8; 4], 120 | pos: usize, 121 | } 122 | 123 | impl Utf8Decoder { 124 | pub fn new() -> Self { 125 | Self { 126 | state: Utf8DecoderState::New, 127 | buf: [0, 0, 0, 0], 128 | pos: 0, 129 | } 130 | } 131 | 132 | fn insert_byte(&mut self, byte: u8) -> Result<(), ()> { 133 | if self.pos > 0 && !byte.utf8_is_continuation() { 134 | return Err(()); 135 | } 136 | 137 | self.buf[self.pos] = byte; 138 | self.pos += 1; 139 | 140 | Ok(()) 141 | } 142 | 143 | pub fn advance(&mut self, byte: u8) -> Utf8DecoderStatus { 144 | match self.state { 145 | Utf8DecoderState::New => { 146 | self.insert_byte(byte).unwrap(); 147 | 148 | match self.buf[0].utf8_byte_type() { 149 | Utf8ByteType::SingleByte => { 150 | self.state = Utf8DecoderState::Done; 151 | Utf8DecoderStatus::Done(Utf8Char::new(&self.buf, 1)) 152 | } 153 | Utf8ByteType::StartTwoByte => { 154 | self.state = Utf8DecoderState::ExpectingOneByte; 155 | Utf8DecoderStatus::Continuation 156 | } 157 | Utf8ByteType::StartThreeByte => { 158 | self.state = Utf8DecoderState::ExpectingTwoBytes; 159 | Utf8DecoderStatus::Continuation 160 | } 161 | Utf8ByteType::StartFourByte => { 162 | self.state = Utf8DecoderState::ExpectingThreeBytes; 163 | Utf8DecoderStatus::Continuation 164 | } 165 | Utf8ByteType::Continuation | Utf8ByteType::Invalid => { 166 | self.state = Utf8DecoderState::Done; 167 | Utf8DecoderStatus::Error 168 | } 169 | } 170 | } 171 | Utf8DecoderState::ExpectingOneByte => { 172 | if self.insert_byte(byte).is_ok() { 173 | self.state = Utf8DecoderState::Done; 174 | Utf8DecoderStatus::Done(Utf8Char::new(&self.buf, self.pos)) 175 | } else { 176 | Utf8DecoderStatus::Error 177 | } 178 | } 179 | Utf8DecoderState::ExpectingTwoBytes => { 180 | if self.insert_byte(byte).is_ok() { 181 | self.state = Utf8DecoderState::ExpectingOneByte; 182 | Utf8DecoderStatus::Continuation 183 | } else { 184 | Utf8DecoderStatus::Error 185 | } 186 | } 187 | Utf8DecoderState::ExpectingThreeBytes => { 188 | if self.insert_byte(byte).is_ok() { 189 | self.state = Utf8DecoderState::ExpectingTwoBytes; 190 | Utf8DecoderStatus::Continuation 191 | } else { 192 | Utf8DecoderStatus::Error 193 | } 194 | } 195 | Utf8DecoderState::Done => Utf8DecoderStatus::Error, 196 | } 197 | } 198 | } 199 | 200 | #[cfg(test)] 201 | mod tests { 202 | use super::*; 203 | 204 | #[test] 205 | fn ascii() { 206 | let mut parser = Utf8Decoder::new(); 207 | 208 | assert_eq!( 209 | parser.advance(b'a'), 210 | Utf8DecoderStatus::Done(Utf8Char::from_str("a")) 211 | ); 212 | 213 | assert_eq!(parser.advance(b'a'), Utf8DecoderStatus::Error); 214 | } 215 | 216 | #[test] 217 | fn twobyte() { 218 | let mut parser = Utf8Decoder::new(); 219 | 220 | let bytes = "æ".as_bytes(); 221 | 222 | assert_eq!(parser.advance(bytes[0]), Utf8DecoderStatus::Continuation); 223 | 224 | assert_eq!( 225 | parser.advance(bytes[1]), 226 | Utf8DecoderStatus::Done(Utf8Char::from_str("æ")) 227 | ); 228 | 229 | assert_eq!(parser.advance(b'a'), Utf8DecoderStatus::Error); 230 | } 231 | 232 | #[test] 233 | fn threebyte() { 234 | let mut parser = Utf8Decoder::new(); 235 | 236 | let bytes = "€".as_bytes(); 237 | 238 | assert_eq!(parser.advance(bytes[0]), Utf8DecoderStatus::Continuation); 239 | assert_eq!(parser.advance(bytes[1]), Utf8DecoderStatus::Continuation); 240 | 241 | assert_eq!( 242 | parser.advance(bytes[2]), 243 | Utf8DecoderStatus::Done(Utf8Char::from_str("€")) 244 | ); 245 | 246 | assert_eq!(parser.advance(b'a'), Utf8DecoderStatus::Error); 247 | } 248 | 249 | #[test] 250 | fn fourbyte() { 251 | let mut parser = Utf8Decoder::new(); 252 | 253 | let symbol = "😂"; 254 | 255 | let bytes = symbol.as_bytes(); 256 | dbg!(bytes); 257 | 258 | assert_eq!(parser.advance(bytes[0]), Utf8DecoderStatus::Continuation); 259 | assert_eq!(parser.advance(bytes[1]), Utf8DecoderStatus::Continuation); 260 | assert_eq!(parser.advance(bytes[2]), Utf8DecoderStatus::Continuation); 261 | 262 | assert_eq!( 263 | parser.advance(bytes[3]), 264 | Utf8DecoderStatus::Done(Utf8Char::from_str(symbol)) 265 | ); 266 | 267 | assert_eq!(parser.advance(b'a'), Utf8DecoderStatus::Error); 268 | } 269 | 270 | #[test] 271 | fn invalid_start() { 272 | let mut parser = Utf8Decoder::new(); 273 | 274 | assert_eq!(parser.advance(0b10000000), Utf8DecoderStatus::Error); 275 | } 276 | 277 | #[test] 278 | fn invalid_continuation() { 279 | let mut parser = Utf8Decoder::new(); 280 | 281 | assert_eq!(parser.advance(0b11000000), Utf8DecoderStatus::Continuation); 282 | assert_eq!(parser.advance(0b00000000), Utf8DecoderStatus::Error); 283 | } 284 | 285 | #[test] 286 | fn to_char() { 287 | assert_eq!(Utf8Char::from_str("€").as_char(), '€'); 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | components = [ "rustfmt", "clippy" ] 3 | --------------------------------------------------------------------------------