├── .github ├── readme-image.png └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples ├── rainbow.rs ├── simple.rs └── wrapper.rs ├── macros ├── Cargo.toml └── src │ ├── err.rs │ ├── gen.rs │ ├── ir.rs │ ├── lib.rs │ ├── parse │ ├── fmt.rs │ ├── mod.rs │ └── style.rs │ └── tests.rs ├── mine.zsh-theme ├── src └── lib.rs └── tests └── main.rs /.github/readme-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LukasKalbertodt/bunt/620c13e4f23a7919bd399f7ba5cf3ee1cca172e6/.github/readme-image.png -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [ master ] 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | RUSTFLAGS: --deny warnings 11 | 12 | jobs: 13 | style: 14 | name: Check basic style 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: LukasKalbertodt/check-basic-style@v0.1 19 | 20 | check: 21 | name: 'Build & test' 22 | runs-on: ubuntu-20.04 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Restore backend cache 26 | uses: Swatinem/rust-cache@v2 27 | - name: Build 28 | run: cargo build 29 | - name: Run tests 30 | run: | 31 | cargo test 32 | cargo test -p bunt-macros 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | 6 | ## [Unreleased] 7 | 8 | ## [0.2.8] - 2023-01-29 9 | ### Added 10 | - Add support for 8bit ANSI color codes via `@` prefix, e.g. `@197`. 11 | 12 | ## [0.2.7] - 2022-11-29 13 | ### Added 14 | - Add `[set_]stdout_color_choice` and `[set_]stderr_color_choice` to configure `ColorChoice` used by `[e]print[ln]`. 15 | 16 | ## [0.2.6] - 2021-09-09 17 | ### Added 18 | - `println`, `eprintln` and `writeln` can now be used without format string and arguments to only emit a single newline. 19 | 20 | ## [0.2.5] - 2021-06-09 21 | ### Changed 22 | - Use crate `litrs` for string literal parsing instead of having custom code for that. 23 | This should get rid of some parsing errors for some edge cases. 24 | It also makes maintenance easier, as it removes quite a bit of code from `bunt`. 25 | 26 | ## [0.2.4] - 2020-11-19 27 | ### Added 28 | - Add `dimmed` attribute, now requiring `termcolor = "1.1.1"` ([#19](https://github.com/LukasKalbertodt/bunt/pull/19)) 29 | 30 | ## [0.2.3] - 2020-10-03 31 | ### Added 32 | - Add way to pass multiple format strings (`println!(["abc", "bar"])` to work around the `concat!` limitation ([#17](https://github.com/LukasKalbertodt/bunt/pull/17)) 33 | 34 | ### Changed 35 | - Clarify that `bunt-macros` is treated as internal code and that you must not depend on it directly. That crate does *not* follow semantic versioning. 36 | 37 | ## [0.2.2] - 2020-09-26 38 | ### Fixed 39 | - Make `?` work inside arguments (e.g. `println!("{}", foo?)`) ([#16](https://github.com/LukasKalbertodt/bunt/pull/16)) 40 | 41 | ## [0.2.1] - 2020-09-20 42 | ### Added 43 | - Add `eprint!` and `eprintln!` ([#13](https://github.com/LukasKalbertodt/bunt/pull/13)) 44 | 45 | ## [0.2.0] - 2020-09-13 46 | ### Breaking changes 47 | - Minimal Rust version bumped to 1.46.0 48 | 49 | ### Changed 50 | - `syn` dependency removed ([#8](https://github.com/LukasKalbertodt/bunt/pull/8)) 51 | - Emit error if arguments are not used 52 | 53 | ### Fixed 54 | - Implement width and precision non-constant arguments (e.g. `{:0$}` or 55 | `{:.prec$}` or `{:.*}`) ([#10](https://github.com/LukasKalbertodt/bunt/pull/10)) 56 | - Fix named arguments also working as positional ones 57 | - Fix bug in parsing 0 flag in format spec (`{:0$}` now parses as "the width is 58 | specified in the first argument" instead of the zero flag) 59 | 60 | 61 | ## [0.1.1] - 2020-09-05 62 | ### Fixed 63 | - Minor documentation fixes 64 | 65 | 66 | ## 0.1.0 - 2020-07-30 67 | ### Added 68 | - Everything (`write`, `writeln`, `print`, `println`, `style`) 69 | 70 | 71 | [Unreleased]: https://github.com/LukasKalbertodt/bunt/compare/v0.2.8...HEAD 72 | [0.2.8]: https://github.com/LukasKalbertodt/bunt/compare/v0.2.7...v0.2.8 73 | [0.2.7]: https://github.com/LukasKalbertodt/bunt/compare/v0.2.6...v0.2.7 74 | [0.2.6]: https://github.com/LukasKalbertodt/bunt/compare/v0.2.5...v0.2.6 75 | [0.2.5]: https://github.com/LukasKalbertodt/bunt/compare/v0.2.4...v0.2.5 76 | [0.2.4]: https://github.com/LukasKalbertodt/bunt/compare/v0.2.3...v0.2.4 77 | [0.2.3]: https://github.com/LukasKalbertodt/bunt/compare/v0.2.2...v0.2.3 78 | [0.2.2]: https://github.com/LukasKalbertodt/bunt/compare/v0.2.1...v0.2.2 79 | [0.2.1]: https://github.com/LukasKalbertodt/bunt/compare/v0.2.0...v0.2.1 80 | [0.2.0]: https://github.com/LukasKalbertodt/bunt/compare/v0.1.1...v0.2.0 81 | [0.1.1]: https://github.com/LukasKalbertodt/bunt/compare/v0.1.0...v0.1.1 82 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bunt" 3 | version = "0.2.8" 4 | authors = ["Lukas Kalbertodt "] 5 | edition = "2018" 6 | 7 | description = """ 8 | Simple macros to write colored and formatted text to a terminal. 9 | Based on `termcolor`, thus also cross-platform. 10 | """ 11 | documentation = "https://docs.rs/bunt/" 12 | repository = "https://github.com/LukasKalbertodt/bunt/" 13 | readme = "README.md" 14 | license = "MIT/Apache-2.0" 15 | 16 | keywords = ["color", "term", "terminal", "format", "style"] 17 | categories = ["command-line-interface"] 18 | exclude = [".github"] 19 | 20 | [dependencies] 21 | bunt-macros = { version = "=0.2.8", path = "macros" } 22 | termcolor = "1.1.1" 23 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Project Developers 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bunt: simple macro-based terminal colors and styles 2 | 3 | [CI status of master](https://github.com/LukasKalbertodt/bunt/actions/workflows/ci.yml) 4 | [Crates.io Version](https://crates.io/crates/bunt) 5 | [docs.rs](https://docs.rs/bunt) 6 | 7 | 8 | `bunt` offers macros to easily print colored and formatted text to a terminal. 9 | It is just a convenience API on top of [`termcolor`](https://crates.io/crates/termcolor). 10 | `bunt` is implemented using procedural macros, but it does not depend on `syn` and compiles fairly quickly. 11 | 12 | *Minimum Supported Rust Version*: 1.46.0 13 | 14 | ```rust 15 | // Style tags will color/format text between the tags. 16 | bunt::println!("I really like {$yellow}lemons{/$}! Like, {$blue+italic}a lot{/$}."); 17 | 18 | // To style a single argument, you can also use the `{[style]...}` syntax. This 19 | // can be combined with style tags. 20 | let v = vec![1, 2, 3]; 21 | bunt::println!("Here is some data: {[green]:?}. {$bold}Length: {[cyan]}{/$}", v, v.len()); 22 | ``` 23 | 24 |

25 | 26 |

27 | 28 | See [**the documentation**](https://docs.rs/bunt) for more information. 29 | 30 | ## Status of this project 31 | 32 | This is still a young project, but I already use it in two applications of mine. 33 | The syntax is certainly not final yet. 34 | [Seeking feedback from the community!](https://github.com/LukasKalbertodt/bunt/issues/1) 35 | 36 | 37 |
38 | 39 | --- 40 | 41 | ## License 42 | 43 | Licensed under either of Apache License, Version 44 | 2.0 or MIT license at your option. 45 | Unless you explicitly state otherwise, any contribution intentionally submitted 46 | for inclusion in this project by you, as defined in the Apache-2.0 license, 47 | shall be dual licensed as above, without any additional terms or conditions. 48 | -------------------------------------------------------------------------------- /examples/rainbow.rs: -------------------------------------------------------------------------------- 1 | // Note: this is basically the worst case for `bunt`. Here we want to iterate 2 | // through all colors, which is not something `bunt` is designed for. 3 | 4 | fn main() { 5 | // ===== Foreground vs. style =============================================================== 6 | let dummy = "Bunt ♥"; 7 | let sep = " "; 8 | 9 | bunt::println!("{$bold+blue+intense}Foreground colors and styles:{/$}"); 10 | println!(); 11 | 12 | println!(" normal bold dimmed italic underline \ 13 | intense bold+intense"); 14 | bunt::println!( 15 | "black {$black}\ 16 | {0}{1}{[bold]0}{1}{[dimmed]0}{1}{[italic]0}{1}{[underline]0}\ 17 | {1}{[intense]0}{1}{[bold+intense]0}\ 18 | {/$}", 19 | dummy, 20 | sep, 21 | ); 22 | bunt::println!( 23 | "blue {$blue}\ 24 | {0}{1}{[bold]0}{1}{[dimmed]0}{1}{[italic]0}{1}{[underline]0}\ 25 | {1}{[intense]0}{1}{[bold+intense]0}\ 26 | {/$}", 27 | dummy, 28 | sep, 29 | ); 30 | bunt::println!( 31 | "green {$green}\ 32 | {0}{1}{[bold]0}{1}{[dimmed]0}{1}{[italic]0}{1}{[underline]0}\ 33 | {1}{[intense]0}{1}{[bold+intense]0}\ 34 | {/$}", 35 | dummy, 36 | sep, 37 | ); 38 | bunt::println!( 39 | "red {$red}\ 40 | {0}{1}{[bold]0}{1}{[dimmed]0}{1}{[italic]0}{1}{[underline]0}\ 41 | {1}{[intense]0}{1}{[bold+intense]0}\ 42 | {/$}", 43 | dummy, 44 | sep, 45 | ); 46 | bunt::println!( 47 | "cyan {$cyan}\ 48 | {0}{1}{[bold]0}{1}{[dimmed]0}{1}{[italic]0}{1}{[underline]0}\ 49 | {1}{[intense]0}{1}{[bold+intense]0}\ 50 | {/$}", 51 | dummy, 52 | sep, 53 | ); 54 | bunt::println!( 55 | "magenta {$magenta}\ 56 | {0}{1}{[bold]0}{1}{[dimmed]0}{1}{[italic]0}{1}{[underline]0}\ 57 | {1}{[intense]0}{1}{[bold+intense]0}\ 58 | {/$}", 59 | dummy, 60 | sep, 61 | ); 62 | bunt::println!( 63 | "yellow {$yellow}\ 64 | {0}{1}{[bold]0}{1}{[dimmed]0}{1}{[italic]0}{1}{[underline]0}\ 65 | {1}{[intense]0}{1}{[bold+intense]0}\ 66 | {/$}", 67 | dummy, 68 | sep, 69 | ); 70 | bunt::println!( 71 | "white {$white}\ 72 | {0}{1}{[bold]0}{1}{[dimmed]0}{1}{[italic]0}{1}{[underline]0}\ 73 | {1}{[intense]0}{1}{[bold+intense]0}\ 74 | {/$}", 75 | dummy, 76 | sep, 77 | ); 78 | bunt::println!( 79 | "default \ 80 | {0}{1}{[bold]0}{1}{[dimmed]0}{1}{[italic]0}{1}{[underline]0}\ 81 | {1}{[intense]0}{1}{[bold+intense]0}", 82 | dummy, 83 | sep, 84 | ); 85 | 86 | 87 | // ===== Foreground vs. style =============================================================== 88 | println!(); 89 | bunt::println!("{$bold+blue+intense}Foreground and background colors:{/$}"); 90 | println!(); 91 | 92 | let dummy = " Bunt ♥ "; 93 | 94 | println!(" bg:black bg:blue bg:green bg:red bg:cyan \ 95 | bg:magenta bg:yellow bg:white"); 96 | bunt::println!( 97 | "black {$black}\ 98 | {[bg:black]0}{[bg:blue]0}{[bg:green]0}{[bg:red]0}{[bg:cyan]0}\ 99 | {[bg:magenta]0}{[bg:yellow]0}{[bg:white]0}\ 100 | {/$}", 101 | dummy, 102 | ); 103 | bunt::println!( 104 | "blue {$blue}\ 105 | {[bg:black]0}{[bg:blue]0}{[bg:green]0}{[bg:red]0}{[bg:cyan]0}\ 106 | {[bg:magenta]0}{[bg:yellow]0}{[bg:white]0}\ 107 | {/$}", 108 | dummy, 109 | ); 110 | bunt::println!( 111 | "green {$green}\ 112 | {[bg:black]0}{[bg:blue]0}{[bg:green]0}{[bg:red]0}{[bg:cyan]0}\ 113 | {[bg:magenta]0}{[bg:yellow]0}{[bg:white]0}\ 114 | {/$}", 115 | dummy, 116 | ); 117 | bunt::println!( 118 | "red {$red}\ 119 | {[bg:black]0}{[bg:blue]0}{[bg:green]0}{[bg:red]0}{[bg:cyan]0}\ 120 | {[bg:magenta]0}{[bg:yellow]0}{[bg:white]0}\ 121 | {/$}", 122 | dummy, 123 | ); 124 | bunt::println!( 125 | "cyan {$cyan}\ 126 | {[bg:black]0}{[bg:blue]0}{[bg:green]0}{[bg:red]0}{[bg:cyan]0}\ 127 | {[bg:magenta]0}{[bg:yellow]0}{[bg:white]0}\ 128 | {/$}", 129 | dummy, 130 | ); 131 | bunt::println!( 132 | "magenta {$magenta}\ 133 | {[bg:black]0}{[bg:blue]0}{[bg:green]0}{[bg:red]0}{[bg:cyan]0}\ 134 | {[bg:magenta]0}{[bg:yellow]0}{[bg:white]0}\ 135 | {/$}", 136 | dummy, 137 | ); 138 | bunt::println!( 139 | "yellow {$yellow}\ 140 | {[bg:black]0}{[bg:blue]0}{[bg:green]0}{[bg:red]0}{[bg:cyan]0}\ 141 | {[bg:magenta]0}{[bg:yellow]0}{[bg:white]0}\ 142 | {/$}", 143 | dummy, 144 | ); 145 | bunt::println!( 146 | "white {$white}\ 147 | {[bg:black]0}{[bg:blue]0}{[bg:green]0}{[bg:red]0}{[bg:cyan]0}\ 148 | {[bg:magenta]0}{[bg:yellow]0}{[bg:white]0}\ 149 | {/$}", 150 | dummy, 151 | ); 152 | bunt::println!( 153 | "default \ 154 | {[bg:black]0}{[bg:blue]0}{[bg:green]0}{[bg:red]0}{[bg:cyan]0}\ 155 | {[bg:magenta]0}{[bg:yellow]0}{[bg:white]0}", 156 | dummy, 157 | ); 158 | 159 | println!(); 160 | 161 | // TODO: add a real rainbow with RGB colors 162 | } 163 | -------------------------------------------------------------------------------- /examples/simple.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!(); 3 | bunt::println!("I really like {$yellow}lemons{/$}! Like, {$blue+italic}a lot{/$}."); 4 | 5 | let v = vec![1, 2, 3]; 6 | bunt::println!("Here is some data: {[green]:?}. {$bold}Length: {[cyan]}{/$}", v, v.len()); 7 | println!(); 8 | 9 | let ty = "u32"; 10 | bunt::eprintln!("{$bold+red}error:{/$} invalid value for type `{[blue]}`", ty); 11 | bunt::eprintln!(""); 12 | bunt::eprintln!("{$italic}Just {$yellow}kidding{/$}{/$}, there is no {$magenta}error{/$} :)"); 13 | println!(); 14 | } 15 | -------------------------------------------------------------------------------- /examples/wrapper.rs: -------------------------------------------------------------------------------- 1 | //! Sometimes, you might want to wrap bunt's macros into your own 2 | //! project-specific macro. Often, you want to print a prefix or something like 3 | //! that. 4 | 5 | 6 | macro_rules! log { 7 | ($fmt:literal $(, $arg:expr)* $(,)?) => { 8 | bunt::println!( 9 | // Bunt macros allow to pass an "array" of format strings which are 10 | // then concatenated by bunt. 11 | [ 12 | // Our prefix 13 | "[{[magenta] log_module_path}] ", 14 | // What the user passed 15 | $fmt, 16 | // Our postfix 17 | " {$cyan}({log_file}:{log_line}){/$}", 18 | ], 19 | $($arg ,)* 20 | log_module_path = std::module_path!(), 21 | log_file = std::file!(), 22 | log_line = std::line!(), 23 | ) 24 | 25 | // This solution is not optimal though. For one, it would be nice to 26 | // `stringify!(std::module_path!())` instead of passing it as runtime 27 | // string argument. That's not possible because macro expansions are 28 | // lazy. 29 | // 30 | // Futhermore, we pass the arguments with names like `log_module_path`. 31 | // In theory, the user could also use a named argument with that name. 32 | // This `log!` macro is very much an internal helper macro and not 33 | // something you would want to put into your public API. 34 | }; 35 | } 36 | 37 | 38 | fn main() { 39 | log!("Hello {}", "peter"); 40 | banana::do_something(); 41 | } 42 | 43 | mod banana { 44 | pub fn do_something() { 45 | log!("I'm doing something with {[yellow]:?}", vec![1, 2, 4]); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bunt-macros" 3 | version = "0.2.8" 4 | authors = ["Lukas Kalbertodt "] 5 | edition = "2018" 6 | 7 | description = """ 8 | Helper crate for `bunt`. Please see the docs of `bunt` for more information. Do 9 | not use this crate directly, API stability is not guaranteed! 10 | """ 11 | repository = "https://github.com/LukasKalbertodt/bunt/" 12 | license = "MIT/Apache-2.0" 13 | 14 | [lib] 15 | proc-macro = true 16 | 17 | [dependencies] 18 | litrs = "0.2.3" 19 | proc-macro2 = "1.0.19" 20 | quote = "1" 21 | unicode-xid = "0.2.1" 22 | -------------------------------------------------------------------------------- /macros/src/err.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Span, TokenStream}; 2 | use quote::quote_spanned; 3 | 4 | 5 | /// Helper macro to easily create an error with a span. 6 | macro_rules! err { 7 | ($fmt:literal $($t:tt)*) => { Error { span: Span::call_site(), msg: format!($fmt $($t)*) } }; 8 | ($span:expr, $($t:tt)+) => { Error { span: $span, msg: format!($($t)+) } }; 9 | } 10 | 11 | /// Simply contains a message and a span. Can be converted to a `compile_error!` 12 | /// via `to_compile_error`. 13 | #[derive(Debug)] 14 | pub(crate) struct Error { 15 | pub(crate) msg: String, 16 | pub(crate) span: Span, 17 | } 18 | 19 | impl Error { 20 | pub(crate) fn to_compile_error(&self) -> TokenStream { 21 | let msg = &self.msg; 22 | quote_spanned! {self.span=> 23 | { 24 | compile_error!(#msg); 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /macros/src/gen.rs: -------------------------------------------------------------------------------- 1 | //! Generating the output tokens from the parsed intermediate representation. 2 | 3 | use proc_macro2::{Ident, Span, TokenStream}; 4 | use quote::quote; 5 | use std::{ 6 | cell::RefCell, 7 | collections::BTreeSet, 8 | fmt::{self, Write}, 9 | }; 10 | use crate::{ 11 | err::Error, 12 | ir::{ 13 | WriteInput, FormatStrFragment, ArgRefKind, Style, Color, Expr, 14 | FormatSpec, Align, Sign, Width, Precision, 15 | }, 16 | }; 17 | 18 | 19 | impl WriteInput { 20 | pub(crate) fn gen_output(&self) -> Result { 21 | fn arg_ident(id: usize) -> Ident { 22 | Ident::new(&format!("arg{}", id), Span::mixed_site()) 23 | } 24 | 25 | // Create a binding for each given argument. This is useful for two 26 | // reasons: 27 | // - The given expression could have side effects or be compuationally 28 | // expensive. The formatting macros from std guarantee that the 29 | // expression is evaluated only once, so we want to guarantee the 30 | // same. 31 | // - We can then very easily refer to all arguments later. Without these 32 | // bindings, we have to do lots of tricky logic to get the right 33 | // arguments in each invidiual `write` call. 34 | let mut arg_bindings = TokenStream::new(); 35 | for (i, arg) in self.args.exprs.iter().enumerate() { 36 | let ident = arg_ident(i); 37 | arg_bindings.extend(quote! { 38 | let #ident = &#arg; 39 | }) 40 | } 41 | 42 | // Prepare the actual process of writing to the target according to the 43 | // format string. 44 | let buf = Ident::new("buf", Span::mixed_site()); 45 | let mut style_stack = Vec::new(); 46 | let mut writes = TokenStream::new(); 47 | let mut next_arg_index = 0; 48 | 49 | // We want to keep track of all expressions that were used somehow to 50 | // emit an error if there are unused ones. As the easiest way to update 51 | // this is within `ident_for_pos` and `ident_for_name` and since two 52 | // closures cannot borrow the vector mutably at the same time, we use a 53 | // ref cell. It's totally fine here as the vector will only be borrowed 54 | // very briefly. 55 | let used_expressions = RefCell::new(vec![false; self.args.exprs.len()]); 56 | 57 | for segment in &self.format_str.fragments { 58 | match segment { 59 | // A formatting fragment. This is the more tricky one. We have 60 | // to construct a `std::write!` invocation that has the right 61 | // fmt string, the right arguments (and no additional ones!) and 62 | // the correct argument references. 63 | FormatStrFragment::Fmt { fmt_str_parts, args } => { 64 | // Two helper functions to get the correct identifier for an 65 | // argument. 66 | let ident_for_pos = |pos| -> Result { 67 | if self.args.exprs.get(pos).is_none() { 68 | return Err(err!( 69 | "invalid reference to positional argument {} (there are \ 70 | not that many arguments)", 71 | pos, 72 | )); 73 | } 74 | 75 | used_expressions.borrow_mut()[pos] = true; 76 | Ok(arg_ident(pos)) 77 | }; 78 | let ident_for_name = |name| -> Result { 79 | let index = self.args.name_indices.get(name) 80 | .ok_or(err!("there is no argument named `{}`", name))?; 81 | 82 | used_expressions.borrow_mut()[*index] = true; 83 | Ok(arg_ident(*index)) 84 | }; 85 | 86 | let mut fmt_str = fmt_str_parts[0].clone(); 87 | let mut used_args = BTreeSet::new(); 88 | 89 | for (i, arg) in args.into_iter().enumerate() { 90 | // Check width and precision parameters. Those are the 91 | // only two things in the format spec that can refer to 92 | // arguments. If they do, we change them to always refer 93 | // to a named parameter hat is inserted into 94 | // `used_args`. 95 | // 96 | // We check those before the main argument because of 97 | // the `.*` precision modifier which, like `{}`, refers 98 | // to the next argument. BUT the `.*` comes first. So 99 | // `println!("{:.*}", 2, 3.1415926)` prints `3.14` and 100 | // swapping the arguments would result in an error. 101 | let mut format_spec = arg.format_spec.clone(); 102 | 103 | match &arg.format_spec.width { 104 | None | Some(Width::Constant(_)) => {} 105 | Some(Width::Name(name)) => { 106 | let ident = ident_for_name(name)?; 107 | format_spec.width = Some(Width::Name(ident.to_string())); 108 | used_args.insert(ident); 109 | } 110 | Some(Width::Position(pos)) => { 111 | let ident = ident_for_pos(*pos)?; 112 | format_spec.width = Some(Width::Name(ident.to_string())); 113 | used_args.insert(ident); 114 | } 115 | } 116 | 117 | match &arg.format_spec.precision { 118 | None | Some(Precision::Constant(_)) => {} 119 | Some(Precision::Name(name)) => { 120 | let ident = ident_for_name(name)?; 121 | format_spec.precision = Some(Precision::Name(ident.to_string())); 122 | used_args.insert(ident); 123 | } 124 | Some(Precision::Position(pos)) => { 125 | let ident = ident_for_pos(*pos)?; 126 | format_spec.precision = Some(Precision::Name(ident.to_string())); 127 | used_args.insert(ident); 128 | } 129 | Some(Precision::Bundled) => { 130 | if self.args.exprs.get(next_arg_index).is_none() { 131 | return Err(err!( 132 | "invalid '.*' precision argument reference to \ 133 | argument {} (too few actual arguments)", 134 | next_arg_index, 135 | )); 136 | } 137 | 138 | let ident = arg_ident(next_arg_index); 139 | format_spec.precision = Some(Precision::Name(ident.to_string())); 140 | 141 | used_expressions.borrow_mut()[next_arg_index] = true; 142 | used_args.insert(ident); 143 | next_arg_index += 1; 144 | } 145 | } 146 | 147 | // Check the main argument. 148 | let ident = match &arg.kind { 149 | ArgRefKind::Next => { 150 | if self.args.exprs.get(next_arg_index).is_none() { 151 | return Err( 152 | err!("invalid '{{}}' argument reference \ 153 | (too few actual arguments)") 154 | ); 155 | } 156 | 157 | used_expressions.borrow_mut()[next_arg_index] = true; 158 | let ident = arg_ident(next_arg_index); 159 | next_arg_index += 1; 160 | ident 161 | } 162 | ArgRefKind::Position(pos) => ident_for_pos(*pos)?, 163 | ArgRefKind::Name(name) => ident_for_name(name)?, 164 | }; 165 | 166 | // Create the full fmt argument and also push the next 167 | // string part. 168 | std::write!(fmt_str, "{{{}:{}}}", ident, format_spec).unwrap(); 169 | used_args.insert(ident); 170 | fmt_str.push_str(&fmt_str_parts[i + 1]); 171 | } 172 | 173 | 174 | // Combine everything in `write!` invocation. 175 | writes.extend(quote! { 176 | if let std::result::Result::Err(e) 177 | = std::write!(#buf, #fmt_str #(, #used_args = #used_args)* ) 178 | { 179 | break std::result::Result::Err(e); 180 | } 181 | }); 182 | } 183 | 184 | // A style start tag: we simply create the `ColorSpec` and call 185 | // `set_color`. The interesting part is how the styles stack and 186 | // merge. 187 | FormatStrFragment::StyleStart(style) => { 188 | let last_style = style_stack.last().copied().unwrap_or(Style::default()); 189 | let new_style = style.or(last_style); 190 | let style_def = new_style.to_tokens(); 191 | style_stack.push(new_style); 192 | writes.extend(quote! { 193 | if let std::result::Result::Err(e) 194 | = ::bunt::termcolor::WriteColor::set_color(#buf, &#style_def) 195 | { 196 | break std::result::Result::Err(e); 197 | } 198 | }); 199 | } 200 | 201 | // Revert the last style tag. This means that we pop the topmost 202 | // style from the stack and apply the *then* topmost style 203 | // again. 204 | FormatStrFragment::StyleEnd => { 205 | style_stack.pop().ok_or(err!("unmatched closing style tag"))?; 206 | let style = style_stack.last().copied().unwrap_or(Style::default()); 207 | let style_def = style.to_tokens(); 208 | writes.extend(quote! { 209 | if let std::result::Result::Err(e) 210 | = ::bunt::termcolor::WriteColor::set_color(#buf, &#style_def) 211 | { 212 | break std::result::Result::Err(e); 213 | } 214 | }); 215 | } 216 | } 217 | } 218 | 219 | // Check if the style tags are balanced 220 | if !style_stack.is_empty() { 221 | return Err(err!("unclosed style tag")); 222 | } 223 | 224 | if let Some(unused_pos) = used_expressions.into_inner().iter().position(|used| !used) { 225 | let expr = &self.args.exprs[unused_pos]; 226 | return Err(err!(expr.span, "argument never used: `{}`", expr.tokens)); 227 | } 228 | 229 | // Combine everything. 230 | let target = &self.target; 231 | Ok(quote! { 232 | loop { 233 | use std::io::Write as _; 234 | 235 | #arg_bindings 236 | let #buf = &mut #target; 237 | #writes 238 | 239 | break Ok(()); 240 | } 241 | }) 242 | } 243 | } 244 | 245 | impl quote::ToTokens for Expr { 246 | fn to_tokens(&self, tokens: &mut TokenStream) { 247 | tokens.extend(self.tokens.clone()) 248 | } 249 | } 250 | 251 | impl fmt::Display for FormatSpec { 252 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 253 | if let Some(fill) = self.fill { 254 | f.write_char(fill)?; 255 | } 256 | if let Some(align) = self.align { 257 | let c = match align { 258 | Align::Left => '<', 259 | Align::Center => '^', 260 | Align::Right => '>', 261 | }; 262 | f.write_char(c)?; 263 | } 264 | if let Some(sign) = self.sign { 265 | let c = match sign { 266 | Sign::Plus => '+', 267 | Sign::Minus => '-', 268 | }; 269 | f.write_char(c)?; 270 | } 271 | if self.alternate { 272 | f.write_char('#')?; 273 | } 274 | if self.zero { 275 | f.write_char('0')?; 276 | } 277 | 278 | match &self.width { 279 | Some(Width::Constant(n)) => write!(f, "{}", n)?, 280 | Some(Width::Position(n)) => write!(f, "{}$", n)?, 281 | Some(Width::Name(s)) => write!(f, "{}$", s)?, 282 | None => {} 283 | } 284 | 285 | match &self.precision { 286 | Some(Precision::Constant(n)) => write!(f, ".{}", n)?, 287 | Some(Precision::Position(n)) => write!(f, ".{}$", n)?, 288 | Some(Precision::Name(s)) => write!(f, ".{}$", s)?, 289 | Some(Precision::Bundled) => write!(f, ".*")?, 290 | None => {} 291 | } 292 | 293 | if let Some(t) = self.ty { 294 | f.write_char(t)?; 295 | } 296 | 297 | Ok(()) 298 | } 299 | } 300 | 301 | impl Style { 302 | /// Returns a token stream representing an expression constructing the 303 | /// `ColorSpec` value corresponding to `self`. 304 | pub(crate) fn to_tokens(&self) -> TokenStream { 305 | let ident = Ident::new("color_spec", Span::mixed_site()); 306 | let mut method_calls = TokenStream::new(); 307 | 308 | if let Some(fg) = self.fg { 309 | let fg = fg.to_tokens(); 310 | method_calls.extend(quote! { 311 | #ident.set_fg(Some(#fg)); 312 | }) 313 | } 314 | if let Some(bg) = self.bg { 315 | let bg = bg.to_tokens(); 316 | method_calls.extend(quote! { 317 | #ident.set_bg(Some(#bg)); 318 | }) 319 | } 320 | 321 | macro_rules! attr { 322 | ($field:ident, $method:ident) => { 323 | if let Some(b) = self.$field { 324 | method_calls.extend(quote! { 325 | #ident.$method(#b); 326 | }); 327 | } 328 | }; 329 | } 330 | 331 | attr!(bold, set_bold); 332 | attr!(dimmed, set_dimmed); 333 | attr!(italic, set_italic); 334 | attr!(underline, set_underline); 335 | attr!(intense, set_intense); 336 | 337 | quote! { 338 | { 339 | let mut #ident = ::bunt::termcolor::ColorSpec::new(); 340 | #method_calls 341 | #ident 342 | } 343 | } 344 | } 345 | } 346 | 347 | impl Color { 348 | /// Returns a token stream representing a value of type `termcolor::Color`. 349 | fn to_tokens(&self) -> TokenStream { 350 | let variant = match self { 351 | Self::Black => Some(quote! { Black }), 352 | Self::Blue => Some(quote! { Blue }), 353 | Self::Green => Some(quote! { Green }), 354 | Self::Red => Some(quote! { Red }), 355 | Self::Cyan => Some(quote! { Cyan }), 356 | Self::Magenta => Some(quote! { Magenta }), 357 | Self::Yellow => Some(quote! { Yellow }), 358 | Self::White => Some(quote! { White }), 359 | Self::Ansi256(ansi) => Some(quote!{ Ansi256(#ansi) }), 360 | Self::Rgb(r, g, b) => Some(quote! { Rgb(#r, #g, #b) }), 361 | }; 362 | 363 | quote! { ::bunt::termcolor::Color:: #variant } 364 | } 365 | } 366 | -------------------------------------------------------------------------------- /macros/src/ir.rs: -------------------------------------------------------------------------------- 1 | //! Types for the intermediate representation of the macro input. This parsed 2 | //! representation allows the functions in `gen.rs` to work more easily. 3 | 4 | use proc_macro2::{Span, TokenStream}; 5 | use std::collections::HashMap; 6 | 7 | 8 | /// Input for the `write!` and `writeln!` macro. 9 | #[derive(Debug)] 10 | pub(crate) struct WriteInput { 11 | pub(crate) target: Expr, 12 | pub(crate) format_str: FormatStr, 13 | pub(crate) args: FormatArgs, 14 | } 15 | 16 | /// Our own `expr` type. We use this instead of `syn` to avoid `syn` 17 | /// alltogether. We don't need to introspect the expression, we just need to 18 | /// skip over them and the emit them again. 19 | #[derive(Debug)] 20 | pub(crate) struct Expr { 21 | pub(crate) span: Span, 22 | pub(crate) tokens: TokenStream, 23 | } 24 | 25 | /// A parsed format string. 26 | #[derive(Debug)] 27 | pub(crate) struct FormatStr { 28 | pub(crate) fragments: Vec, 29 | } 30 | 31 | impl FormatStr { 32 | /// Adds `\n` to the end of the formatting string. 33 | pub(crate) fn add_newline(&mut self) { 34 | match self.fragments.last_mut() { 35 | // If the last fragment is an `fmt` one, we can easily add the 36 | // newline to its last part (which is guaranteed to exist). 37 | Some(FormatStrFragment::Fmt { fmt_str_parts, .. }) => { 38 | fmt_str_parts.last_mut() 39 | .expect("bug: fmt_str_parts empty") 40 | .push('\n'); 41 | } 42 | 43 | // Otherwise (style closing tag is last fragment), we have to add a 44 | // new `Fmt` fragment. 45 | _ => { 46 | self.fragments.push(FormatStrFragment::Fmt { 47 | fmt_str_parts: vec!["\n".into()], 48 | args: vec![], 49 | }); 50 | } 51 | } 52 | } 53 | } 54 | 55 | /// One fragment of the format string. 56 | #[derive(Debug)] 57 | pub(crate) enum FormatStrFragment { 58 | /// A format string without style tags, but potentially with arguments. 59 | /// 60 | /// `fmt_str_parts` always has exactly one element more than `args`. 61 | Fmt { 62 | /// The format string as parts between the arguments. 63 | fmt_str_parts: Vec, 64 | 65 | /// Information about argument that are referenced. 66 | args: Vec, 67 | }, 68 | 69 | /// A `{$...}` style start tag. 70 | StyleStart(Style), 71 | 72 | /// A `{/$}` style end tag. 73 | StyleEnd, 74 | } 75 | 76 | #[derive(Debug)] 77 | pub(crate) struct ArgRef { 78 | pub(crate) kind: ArgRefKind, 79 | pub(crate) format_spec: FormatSpec, 80 | } 81 | 82 | /// How a format argument is referred to. 83 | #[derive(Debug)] 84 | pub(crate) enum ArgRefKind { 85 | /// `{}` 86 | Next, 87 | /// `{2}` 88 | Position(usize), 89 | /// `{peter}` 90 | Name(String), 91 | } 92 | 93 | #[derive(Debug, Clone)] 94 | #[cfg_attr(test, derive(PartialEq))] 95 | pub(crate) struct FormatSpec { 96 | pub(crate) fill: Option, 97 | pub(crate) align: Option, 98 | pub(crate) sign: Option, 99 | pub(crate) alternate: bool, 100 | pub(crate) zero: bool, 101 | pub(crate) width: Option, 102 | pub(crate) precision: Option, 103 | pub(crate) ty: Option, 104 | } 105 | 106 | #[cfg(test)] 107 | impl Default for FormatSpec { 108 | fn default() -> Self { 109 | Self { 110 | fill: None, 111 | align: None, 112 | sign: None, 113 | alternate: false, 114 | zero: false, 115 | width: None, 116 | precision: None, 117 | ty: None, 118 | } 119 | } 120 | } 121 | 122 | #[derive(Debug, Clone, Copy)] 123 | #[cfg_attr(test, derive(PartialEq))] 124 | pub(crate) enum Align { 125 | Left, 126 | Center, 127 | Right, 128 | } 129 | 130 | #[derive(Debug, Clone, Copy)] 131 | #[cfg_attr(test, derive(PartialEq))] 132 | pub(crate) enum Sign { 133 | Plus, 134 | Minus, 135 | } 136 | 137 | #[derive(Debug, Clone)] 138 | #[cfg_attr(test, derive(PartialEq))] 139 | pub(crate) enum Width { 140 | Constant(usize), 141 | Name(String), 142 | Position(usize), 143 | } 144 | 145 | #[derive(Debug, Clone)] 146 | #[cfg_attr(test, derive(PartialEq))] 147 | pub(crate) enum Precision { 148 | Constant(usize), 149 | Name(String), 150 | Position(usize), 151 | /// `.*` 152 | Bundled, 153 | } 154 | 155 | /// Parsed formatting arguments. 156 | #[derive(Debug)] 157 | pub(crate) struct FormatArgs { 158 | /// All argument expressions in order, including the named ones (without the 159 | /// `name =` part). 160 | pub(crate) exprs: Vec, 161 | 162 | /// Mapping from named argument name to index in `self.exprs`. 163 | pub(crate) name_indices: HashMap, 164 | } 165 | 166 | #[derive(Debug, Clone, Copy)] 167 | pub(crate) enum Color { 168 | Black, 169 | Blue, 170 | Green, 171 | Red, 172 | Cyan, 173 | Magenta, 174 | Yellow, 175 | White, 176 | Ansi256(u8), 177 | Rgb(u8, u8, u8), 178 | } 179 | 180 | #[derive(Debug, Default, Clone, Copy)] 181 | pub(crate) struct Style { 182 | pub(crate) fg: Option, 183 | pub(crate) bg: Option, 184 | pub(crate) bold: Option, 185 | pub(crate) intense: Option, 186 | pub(crate) underline: Option, 187 | pub(crate) italic: Option, 188 | pub(crate) dimmed: Option, 189 | pub(crate) reset: Option, 190 | } 191 | 192 | impl Style { 193 | /// Like `Option::or`: all style values set in `self` are kept, all unset 194 | /// ones are overwritten with the values from `style_b`. 195 | pub(crate) fn or(&self, style_b: Self) -> Self { 196 | Self { 197 | fg: self.fg.or(style_b.fg), 198 | bg: self.bg.or(style_b.bg), 199 | bold: self.bold.or(style_b.bold), 200 | intense: self.intense.or(style_b.intense), 201 | underline: self.underline.or(style_b.underline), 202 | italic: self.italic.or(style_b.italic), 203 | dimmed: self.dimmed.or(style_b.dimmed), 204 | reset: self.reset.or(style_b.reset), 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! These are the docs for the crate `bunt-macros`. This is just implementation 2 | //! detail, please see the crate `bunt` for the real docs. 3 | 4 | use proc_macro::TokenStream as TokenStream1; 5 | use proc_macro2::TokenStream; 6 | 7 | #[macro_use] 8 | mod err; 9 | mod gen; 10 | mod ir; 11 | mod parse; 12 | 13 | #[cfg(test)] 14 | mod tests; 15 | 16 | use crate::{ 17 | err::Error, 18 | ir::{Style, WriteInput}, 19 | }; 20 | 21 | 22 | // Docs are in the `bunt` reexport. 23 | #[proc_macro] 24 | pub fn style(input: TokenStream1) -> TokenStream1 { 25 | run(input, |input| { 26 | let style = Style::parse_from_tokens(input)?; 27 | Ok(style.to_tokens()) 28 | }) 29 | } 30 | 31 | // Docs are in the `bunt` reexport. 32 | #[proc_macro] 33 | pub fn write(input: TokenStream1) -> TokenStream1 { 34 | run(input, |input| parse::parse(input, WriteInput::parse)?.gen_output()) 35 | } 36 | 37 | // Docs are in the `bunt` reexport. 38 | #[proc_macro] 39 | pub fn writeln(input: TokenStream1) -> TokenStream1 { 40 | run(input, |input| { 41 | let mut input = parse::parse(input, WriteInput::parse)?; 42 | input.format_str.add_newline(); 43 | input.gen_output() 44 | }) 45 | } 46 | 47 | /// Performs the conversion from and to `proc_macro::TokenStream` and converts 48 | /// `Error`s into `compile_error!` tokens. 49 | fn run( 50 | input: TokenStream1, 51 | f: impl FnOnce(TokenStream) -> Result, 52 | ) -> TokenStream1 { 53 | f(input.into()) 54 | .unwrap_or_else(|e| e.to_compile_error()) 55 | .into() 56 | } 57 | -------------------------------------------------------------------------------- /macros/src/parse/fmt.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{ 2 | Span, 3 | token_stream::IntoIter as TokenIterator, TokenTree, Delimiter, 4 | }; 5 | use unicode_xid::UnicodeXID; 6 | use std::str::Chars; 7 | use crate::{ 8 | err::Error, 9 | ir::{ 10 | ArgRefKind, ArgRef, FormatStr, Style, 11 | FormatStrFragment, FormatSpec, Align, Sign, Width, Precision, 12 | }, 13 | }; 14 | use super::{parse, expect_helper_group, expect_str_literal}; 15 | 16 | 17 | impl FormatStr { 18 | /// Parses `["foo"]`, `["foo" "bar"]`. 19 | pub(crate) fn parse(it: &mut TokenIterator) -> Result { 20 | /// Searches for the next closing `}`. Returns a pair of strings, the 21 | /// first starting like `s` and ending at the closing brace, the second 22 | /// starting at the brace and ending like `s`. Both strings exclude the 23 | /// brace itself. If a closing brace can't be found, an error is 24 | /// returned. 25 | fn split_at_closing_brace(s: &str, span: Span) -> Result<(&str, &str), Error> { 26 | // I *think* there can't be escaped closing braces inside the fmt 27 | // format, so we can simply search for a single closing one. 28 | let end = s.find("}") 29 | .ok_or(err!(span, "unclosed '{{' in format string"))?; 30 | Ok((&s[..end], &s[end + 1..])) 31 | } 32 | 33 | // We expect a []-delimited group 34 | let (inner, span) = match it.next() { 35 | Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Bracket => { 36 | (g.stream(), g.span()) 37 | } 38 | Some(TokenTree::Group(g)) => { 39 | return Err(err!( 40 | g.span(), 41 | "expected `[]` delimited group, but delimiter is {:?} (note: do not use \ 42 | the macros from `bunt-macros` directly, but only through `bunt`)", 43 | g.delimiter(), 44 | )); 45 | } 46 | Some(tt) => { 47 | return Err(err!( 48 | tt.span(), 49 | "expected `[]` delimited group, but found different token tree (note: do \ 50 | not use the macros from `bunt-macros` directly, but only through `bunt`)", 51 | )) 52 | } 53 | None => return Err(err!("expected `[]` delimited group, found EOF")), 54 | }; 55 | 56 | if inner.is_empty() { 57 | return Err(err!( 58 | span, 59 | "at least one format string has to be provided, but `[]` was passed (note: do not \ 60 | use the macros from `bunt-macros` directly, but only through `bunt`)" 61 | )); 62 | } 63 | 64 | // Concat all string literals 65 | let mut raw = String::new(); 66 | for tt in inner { 67 | let (literal, _) = expect_helper_group(Some(tt))?; 68 | let (string_data, _) = parse(literal, expect_str_literal)?; 69 | raw += &string_data; 70 | } 71 | 72 | // Scan the whole string 73 | let mut fragments = Vec::new(); 74 | let mut s = &raw[..]; 75 | while !s.is_empty() { 76 | fn string_without<'a>(a: &'a str, b: &'a str) -> &'a str { 77 | let end = b.as_ptr() as usize - a.as_ptr() as usize; 78 | &a[..end] 79 | } 80 | 81 | // let start_string = s; 82 | let mut args = Vec::new(); 83 | let mut fmt_str_parts = Vec::new(); 84 | 85 | // Scan until we reach a style tag. 86 | let mut scanner = s; 87 | loop { 88 | match scanner.find('{') { 89 | Some(brace_pos) => scanner = &scanner[brace_pos..], 90 | None => { 91 | // EOF reached: stop searching 92 | scanner = &scanner[scanner.len()..]; 93 | break; 94 | } 95 | } 96 | 97 | 98 | match () { 99 | // Escaped brace: skip. 100 | () if scanner.starts_with("{{") => scanner = &scanner[2..], 101 | 102 | // Found a style tag: stop searching! 103 | () if scanner.starts_with("{$") => break, 104 | () if scanner.starts_with("{/$") => break, 105 | 106 | // Found a styled argument: stop searching! 107 | () if scanner.starts_with("{[") => break, 108 | 109 | // An formatting argument. Gather some information about it 110 | // and remember it for later. 111 | _ => { 112 | let (inner, rest) = split_at_closing_brace(&scanner[1..], span)?; 113 | args.push(ArgRef::parse(inner)?); 114 | fmt_str_parts.push(string_without(s, scanner).to_owned()); 115 | s = rest; 116 | scanner = rest; 117 | } 118 | } 119 | } 120 | 121 | // Add the last string part and then push this fragment, unless it 122 | // is completely empty. 123 | fmt_str_parts.push(string_without(s, scanner).to_owned()); 124 | s = scanner; 125 | if !args.is_empty() || fmt_str_parts.iter().any(|s| !s.is_empty()) { 126 | fragments.push(FormatStrFragment::Fmt { args, fmt_str_parts }); 127 | } 128 | 129 | if s.is_empty() { 130 | break; 131 | } 132 | 133 | // At this point, `s` starts with either a styled argument or a 134 | // style tag. 135 | match () { 136 | // Closing style tag. 137 | () if s.starts_with("{/$}") => { 138 | fragments.push(FormatStrFragment::StyleEnd); 139 | s = &s[4..]; 140 | } 141 | 142 | // Opening style tag. 143 | () if s.starts_with("{$") => { 144 | let (inner, rest) = split_at_closing_brace(&s[2..], span)?; 145 | let style = Style::parse(inner, span)?; 146 | fragments.push(FormatStrFragment::StyleStart(style)); 147 | s = rest; 148 | } 149 | 150 | () if s.starts_with("{[") => { 151 | let (inner, rest) = split_at_closing_brace(&s[1..], span)?; 152 | 153 | // Parse style information 154 | let style_end = inner.find(']') 155 | .ok_or(err!(span, "unclosed '[' in format string argument"))?; 156 | let style = Style::parse(&inner[1..style_end], span)?; 157 | fragments.push(FormatStrFragment::StyleStart(style)); 158 | 159 | // Parse the standard part of this arg reference. 160 | let standard_inner = inner[style_end + 1..].trim_start(); 161 | let arg = ArgRef::parse(standard_inner)?; 162 | fragments.push(FormatStrFragment::Fmt { 163 | args: vec![arg], 164 | fmt_str_parts: vec!["".into(), "".into()], 165 | }); 166 | 167 | fragments.push(FormatStrFragment::StyleEnd); 168 | 169 | s = rest; 170 | } 171 | 172 | _ => panic!("bug: at this point, there should be a style tag or styled arg"), 173 | } 174 | } 175 | 176 | Ok(Self { fragments }) 177 | } 178 | } 179 | 180 | impl ArgRef { 181 | /// (Partially) parses the inside of an format arg (`{...}`). The given 182 | /// string `s` must be the inside of the arg and must *not* contain the 183 | /// outer braces. 184 | pub(crate) fn parse(s: &str) -> Result { 185 | // Split argument reference and format specs. 186 | let (arg_str, format_spec) = match s.find(':') { 187 | None => (s, ""), 188 | Some(colon_pos) => (&s[..colon_pos], &s[colon_pos + 1..]), 189 | }; 190 | 191 | // Check kind of argument reference. 192 | let kind = if arg_str.is_empty() { 193 | ArgRefKind::Next 194 | } else if let Ok(pos) = arg_str.parse::() { 195 | ArgRefKind::Position(pos) 196 | } else { 197 | // TODO: make sure the string is a valid Rust identifier 198 | ArgRefKind::Name(arg_str.into()) 199 | }; 200 | 201 | let format_spec = FormatSpec::parse(format_spec)?; 202 | 203 | Ok(Self { kind, format_spec }) 204 | } 205 | } 206 | 207 | impl FormatSpec { 208 | /// Parses the format specification that comes after the `:` inside an 209 | /// `{...}` argument. The given string must not include the `:` but might be 210 | /// empty. 211 | pub(crate) fn parse(s: &str) -> Result { 212 | /// Helper iterator for scanning the input 213 | struct Peek2<'a> { 214 | peek: Option, 215 | peek2: Option, 216 | rest: Chars<'a>, 217 | } 218 | 219 | impl<'a> Peek2<'a> { 220 | fn new(s: &'a str) -> Self { 221 | let mut rest = s.chars(); 222 | let peek = rest.next(); 223 | let peek2 = rest.next(); 224 | 225 | Self { peek, peek2, rest } 226 | } 227 | 228 | fn next_if(&mut self, f: impl FnOnce(char) -> Option) -> Option { 229 | let out = self.peek.and_then(f); 230 | if out.is_some() { 231 | self.next(); 232 | } 233 | out 234 | } 235 | 236 | fn next_if_eq(&mut self, expected: char) -> bool { 237 | if self.peek == Some(expected) { 238 | self.next(); 239 | true 240 | } else { 241 | false 242 | } 243 | } 244 | 245 | /// Parses one decimal number and returns whether or not it was 246 | /// terminated by `$`. 247 | fn parse_num(&mut self) -> Result<(usize, bool), Error> { 248 | let mut num: usize = 0; 249 | while matches!(self.peek, Some('0'..='9')) { 250 | num = num.checked_mul(10) 251 | .and_then(|num| { 252 | num.checked_add((self.next().unwrap() as u32 - '0' as u32) as usize) 253 | }) 254 | .ok_or(err!("width parameter value overflowed `usize`"))?; 255 | } 256 | 257 | let arg = self.next_if_eq('$'); 258 | Ok((num, arg)) 259 | } 260 | 261 | /// Parses a Rust identifier terminated by '$'. When calling this 262 | /// method, `self.peek.map_or(false, |c| c.is_xid_start())` must be 263 | /// true. 264 | fn parse_ident(&mut self) -> Result { 265 | let mut name = String::from(self.next().unwrap()); 266 | while self.peek.map_or(false, |c| c.is_xid_continue()) { 267 | name.push(self.next().unwrap()); 268 | } 269 | 270 | if !self.next_if_eq('$') { 271 | return Err(err!( 272 | "invalid format string specification: width/precision named parameter \ 273 | does not end with '$'. Note: try the `std` macros to get much better \ 274 | error reporting." 275 | )); 276 | } 277 | 278 | Ok(name) 279 | } 280 | } 281 | 282 | impl Iterator for Peek2<'_> { 283 | type Item = char; 284 | fn next(&mut self) -> Option { 285 | let out = self.peek.take(); 286 | if out.is_none() { 287 | return None; 288 | } 289 | 290 | self.peek = self.peek2; 291 | self.peek2 = self.rest.next(); 292 | 293 | out 294 | } 295 | } 296 | 297 | 298 | let mut it = Peek2::new(s); 299 | 300 | // Fill and align. The former can only exist if the latter also exists. 301 | let (fill, align) = if let Some(align) = it.next_if(Align::from_char) { 302 | (None, Some(align)) 303 | } else if let Some(align) = it.peek2.and_then(Align::from_char) { 304 | let fill = it.next().unwrap(); 305 | it.next().unwrap(); 306 | (Some(fill), Some(align)) 307 | } else { 308 | (None, None) 309 | }; 310 | 311 | // Simple flags. 312 | let sign = it.next_if(Sign::from_char); 313 | let alternate = it.next_if_eq('#'); 314 | let zero = if it.peek == Some('0') && it.peek2 != Some('$') { 315 | it.next().unwrap(); 316 | true 317 | } else { 318 | false 319 | }; 320 | 321 | // Width or early exit. 322 | let width = match it.peek { 323 | // Either a width constant (`8`) or referring to a positional 324 | // parameter (`2$`). 325 | Some('0'..='9') => { 326 | let (num, dollar) = it.parse_num()?; 327 | 328 | if dollar { 329 | Some(Width::Position(num)) 330 | } else { 331 | Some(Width::Constant(num)) 332 | } 333 | } 334 | 335 | // The "type" (e.g. `?`, `X`) determining the formatting trait. This 336 | // means we are done here. 337 | Some(c) if it.peek2.is_none() => { 338 | return Ok(Self { 339 | fill, 340 | align, 341 | sign, 342 | alternate, 343 | zero, 344 | width: None, 345 | precision: None, 346 | ty: Some(c), 347 | }); 348 | } 349 | 350 | // The start of a `width` named parameter (`foo$`). 351 | Some(c) if c.is_xid_start() => Some(Width::Name(it.parse_ident()?)), 352 | _ => None, 353 | }; 354 | 355 | // Precision starting with '.'. 356 | let precision = if it.next_if_eq('.') { 357 | if it.next_if_eq('*') { 358 | Some(Precision::Bundled) 359 | } else if matches!(it.peek, Some('0'..='9')) { 360 | let (num, dollar) = it.parse_num()?; 361 | 362 | if dollar { 363 | Some(Precision::Position(num)) 364 | } else { 365 | Some(Precision::Constant(num)) 366 | } 367 | } else { 368 | Some(Precision::Name(it.parse_ident()?)) 369 | } 370 | } else { 371 | None 372 | }; 373 | 374 | // Parse type char and make sure nothing else is left. 375 | let ty = it.next(); 376 | if let Some(c) = it.next() { 377 | return Err(err!( 378 | "expected end of format specification, but found '{}'. Note: use the std \ 379 | macros to get much better error reporting.", 380 | c, 381 | )); 382 | } 383 | 384 | Ok(Self { 385 | fill, 386 | align, 387 | sign, 388 | alternate, 389 | zero, 390 | width, 391 | precision, 392 | ty, 393 | }) 394 | } 395 | } 396 | 397 | impl Align { 398 | fn from_char(c: char) -> Option { 399 | match c { 400 | '<' => Some(Self::Left), 401 | '^' => Some(Self::Center), 402 | '>' => Some(Self::Right), 403 | _ => None, 404 | } 405 | } 406 | } 407 | 408 | impl Sign { 409 | fn from_char(c: char) -> Option { 410 | match c { 411 | '+' => Some(Self::Plus), 412 | '-' => Some(Self::Minus), 413 | _ => None, 414 | } 415 | } 416 | } 417 | -------------------------------------------------------------------------------- /macros/src/parse/mod.rs: -------------------------------------------------------------------------------- 1 | //! Parse the macro input into an intermediate representation. 2 | 3 | use proc_macro2::{ 4 | Span, TokenStream, Delimiter, TokenTree, Spacing, 5 | token_stream::IntoIter as TokenIterator, 6 | }; 7 | use std::{collections::HashMap, convert::TryFrom}; 8 | use crate::{ 9 | err::Error, 10 | ir::{Expr, WriteInput, FormatStr, FormatArgs}, 11 | }; 12 | 13 | mod fmt; 14 | mod style; 15 | 16 | 17 | /// Helper function to parse from a token stream. Makes sure the iterator is 18 | /// empty after `f` returns. 19 | pub(crate) fn parse(tokens: TokenStream, f: F) -> Result 20 | where 21 | F: FnOnce(&mut TokenIterator) -> Result, 22 | { 23 | let mut it = tokens.into_iter(); 24 | let out = f(&mut it)?; 25 | 26 | if let Some(tt) = it.next() { 27 | return Err(err!(tt.span(), "unexpected additional tokens")); 28 | } 29 | 30 | Ok(out) 31 | } 32 | 33 | /// Tries to parse a helper group (a group with `None` or `()` delimiter). 34 | /// 35 | /// These groups are inserted by the declarative macro wrappers in `bunt` to 36 | /// make parsing in `bunt-macros` easier. In particular, all expressions are 37 | /// wrapped in these group, allowing us to skip over them without having a Rust 38 | /// expression parser. 39 | fn expect_helper_group(tt: Option) -> Result<(TokenStream, Span), Error> { 40 | match tt { 41 | Some(TokenTree::Group(g)) 42 | if g.delimiter() == Delimiter::None || g.delimiter() == Delimiter::Parenthesis => 43 | { 44 | Ok((g.stream(), g.span())) 45 | } 46 | Some(TokenTree::Group(g)) => { 47 | Err(err!( 48 | g.span(), 49 | "expected none or () delimited group, but delimiter is {:?} (note: do not use \ 50 | the macros from `bunt-macros` directly, but only through `bunt`)", 51 | g.delimiter(), 52 | )) 53 | } 54 | Some(tt) => { 55 | Err(err!( 56 | tt.span(), 57 | "expected none or () delimited group, but found different token tree (note: do \ 58 | not use the macros from `bunt-macros` directly, but only through `bunt`)", 59 | )) 60 | } 61 | None => Err(err!("expected none or () delimited group, found EOF")), 62 | } 63 | } 64 | 65 | /// Tries to parse a string literal. 66 | pub(super) fn expect_str_literal(it: &mut TokenIterator) -> Result<(String, Span), Error> { 67 | let tt = it.next().ok_or(err!("expected string literal, found EOF"))?; 68 | let lit = litrs::StringLit::try_from(&tt) 69 | .map_err(|e| err!(tt.span(), "{}", e))?; 70 | 71 | Ok((lit.into_value().into_owned(), tt.span())) 72 | } 73 | 74 | impl WriteInput { 75 | pub(crate) fn parse(it: &mut TokenIterator) -> Result { 76 | let target = Expr::parse(it)?; 77 | let format_str = FormatStr::parse(it)?; 78 | let args = FormatArgs::parse(it)?; 79 | 80 | Ok(Self { target, format_str, args }) 81 | } 82 | } 83 | 84 | impl Expr { 85 | pub(crate) fn parse(it: &mut TokenIterator) -> Result { 86 | let (tokens, span) = expect_helper_group(it.next())?; 87 | Ok(Self { tokens, span }) 88 | } 89 | } 90 | 91 | impl FormatArgs { 92 | fn parse(it: &mut TokenIterator) -> Result { 93 | /// Checks if the token stream starting with `tt0` and `tt1` is a named 94 | /// argument. If so, returns the name of the argument, otherwise 95 | /// (positional argument) returns `None`. 96 | fn get_name(tt0: &Option, tt1: &Option) -> Option { 97 | if let (Some(TokenTree::Ident(name)), Some(TokenTree::Punct(punct))) = (tt0, tt1) { 98 | if punct.as_char() == '=' && punct.spacing() == Spacing::Alone { 99 | return Some(name.to_string()) 100 | } 101 | } 102 | 103 | None 104 | } 105 | 106 | let mut exprs = Vec::new(); 107 | let mut name_indices = HashMap::new(); 108 | let mut saw_named = false; 109 | 110 | // The remaining tokens should all be `None` delimited groups each 111 | // representing one argument. 112 | for arg_group in it { 113 | let (arg, span) = expect_helper_group(Some(arg_group))?; 114 | let mut it = arg.into_iter(); 115 | let tt0 = it.next(); 116 | let tt1 = it.next(); 117 | 118 | if let Some(name) = get_name(&tt0, &tt1) { 119 | saw_named = true; 120 | 121 | let expr = Expr { 122 | tokens: it.collect(), 123 | span, 124 | }; 125 | let index = exprs.len(); 126 | exprs.push(expr); 127 | name_indices.insert(name, index); 128 | } else { 129 | if saw_named { 130 | let e = err!(span, "positional argument after named arguments is not allowed"); 131 | return Err(e); 132 | } 133 | 134 | let expr = Expr { 135 | tokens: vec![tt0, tt1].into_iter().filter_map(|tt| tt).chain(it).collect(), 136 | span, 137 | }; 138 | exprs.push(expr); 139 | } 140 | 141 | } 142 | 143 | Ok(Self { exprs, name_indices }) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /macros/src/parse/style.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Span, TokenStream}; 2 | use crate::{ 3 | err::Error, 4 | ir::{Style, Color}, 5 | }; 6 | use super::{parse, expect_str_literal}; 7 | 8 | 9 | impl Style { 10 | /// Parses the style specifiction assuming the token stream contains a 11 | /// single string literal. 12 | pub(crate) fn parse_from_tokens(tokens: TokenStream) -> Result { 13 | let (s, span) = parse(tokens, expect_str_literal)?; 14 | Self::parse(&s, span) 15 | } 16 | 17 | /// Parses the style specification in `spec` (with `span`) and returns a token 18 | /// stream representing an expression constructing the corresponding `ColorSpec` 19 | /// value. 20 | pub(super) fn parse(spec: &str, span: Span) -> Result { 21 | let mut out = Self::default(); 22 | 23 | let mut previous_fg_color = None; 24 | let mut previous_bg_color = None; 25 | for fragment in spec.split('+').map(str::trim).filter(|s| !s.is_empty()) { 26 | let (fragment, is_bg) = match fragment.strip_prefix("bg:") { 27 | Some(color) => (color, true), 28 | None => (fragment, false), 29 | }; 30 | 31 | // Parse/obtain color if a color is specified. 32 | let color = match fragment { 33 | "black" => Some(Color::Black), 34 | "blue" => Some(Color::Blue), 35 | "green" => Some(Color::Green), 36 | "red" => Some(Color::Red), 37 | "cyan" => Some(Color::Cyan), 38 | "magenta" => Some(Color::Magenta), 39 | "yellow" => Some(Color::Yellow), 40 | "white" => Some(Color::White), 41 | 42 | fragment if fragment.starts_with('@') => { 43 | let ansi = &fragment[1..]; 44 | let ansi = ansi.parse::().map_err(|e| err!( 45 | span, 46 | "expected number between 0 and 255 for ANSI color code, found {} ({})", 47 | ansi, 48 | e, 49 | ))?; 50 | 51 | Some(Color::Ansi256(ansi)) 52 | }, 53 | 54 | hex if hex.starts_with('#') => { 55 | let hex = &hex[1..]; 56 | 57 | if hex.len() != 6 { 58 | let e = err!( 59 | span, 60 | "hex color code invalid: 6 digits expected, found {}", 61 | hex.len(), 62 | ); 63 | return Err(e); 64 | } 65 | 66 | let digits = hex.chars() 67 | .map(|c| { 68 | c.to_digit(16).ok_or_else(|| { 69 | err!(span, "hex color code invalid: {} is not a valid hex digit", c) 70 | }) 71 | }) 72 | .collect::, _>>()?; 73 | 74 | let r = (digits[0] * 16 + digits[1]) as u8; 75 | let g = (digits[2] * 16 + digits[3]) as u8; 76 | let b = (digits[4] * 16 + digits[5]) as u8; 77 | 78 | Some(Color::Rgb(r, g, b)) 79 | }, 80 | 81 | // TODO: Ansi256 colors 82 | _ => None, 83 | }; 84 | 85 | // Check for duplicate color definitions. 86 | let (previous_color, color_kind) = match is_bg { 87 | true => (&mut previous_bg_color, "background"), 88 | false => (&mut previous_fg_color, "foreground"), 89 | }; 90 | match (&color, *previous_color) { 91 | (Some(_), Some(old)) => { 92 | let e = err!( 93 | span, 94 | "found '{}' but the {} color was already specified as '{}'", 95 | fragment, 96 | color_kind, 97 | old, 98 | ); 99 | return Err(e); 100 | } 101 | (Some(_), None) => *previous_color = Some(fragment), 102 | _ => {} 103 | } 104 | 105 | macro_rules! set_attr { 106 | ($field:ident, $value:expr) => {{ 107 | if let Some(b) = out.$field { 108 | let field_s = stringify!($field); 109 | let old = if b { field_s.into() } else { format!("!{}", field_s) }; 110 | let new = if $value { field_s.into() } else { format!("!{}", field_s) }; 111 | let e = err!( 112 | span, 113 | "invalid style definition: found '{}', but '{}' was specified before", 114 | new, 115 | old, 116 | ); 117 | return Err(e); 118 | } 119 | 120 | out.$field = Some($value); 121 | }}; 122 | } 123 | 124 | // Obtain the final token stream for method call. 125 | match (is_bg, color, fragment) { 126 | (false, Some(color), _) => out.fg = Some(color), 127 | (true, Some(color), _) => out.bg = Some(color), 128 | (true, None, other) => { 129 | return Err(err!(span, "'{}' (following 'bg:') is not a valid color", other)); 130 | } 131 | 132 | (false, None, "bold") => set_attr!(bold, true), 133 | (false, None, "!bold") => set_attr!(bold, false), 134 | (false, None, "italic") => set_attr!(italic, true), 135 | (false, None, "!italic") => set_attr!(italic, false), 136 | (false, None, "dimmed") => set_attr!(dimmed, true), 137 | (false, None, "!dimmed") => set_attr!(dimmed, false), 138 | (false, None, "underline") => set_attr!(underline, true), 139 | (false, None, "!underline") => set_attr!(underline, false), 140 | (false, None, "intense") => set_attr!(intense, true), 141 | (false, None, "!intense") => set_attr!(intense, false), 142 | 143 | (false, None, other) => { 144 | return Err(err!(span, "invalid style spec fragment '{}'", other)); 145 | } 146 | } 147 | } 148 | 149 | Ok(out) 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /macros/src/tests.rs: -------------------------------------------------------------------------------- 1 | use crate::ir::{Align, Width, Sign, FormatSpec, Precision}; 2 | 3 | 4 | #[track_caller] 5 | fn check_format_spec(expected_str: &str, expected_parsed: FormatSpec) { 6 | let actual_parsed = FormatSpec::parse(expected_str) 7 | .expect(&format!("failed to parse format spec '{}'", expected_str)); 8 | 9 | if actual_parsed != expected_parsed { 10 | panic!( 11 | "result of parsing format spec '{}' unexpected.\ 12 | \n expected: {:?}\ 13 | \n actual: {:?}\n", 14 | expected_str, 15 | expected_parsed, 16 | actual_parsed, 17 | ); 18 | } 19 | 20 | let actual_string = actual_parsed.to_string(); 21 | if actual_string != expected_str { 22 | panic!( 23 | "result of stringifying '{:?}' unexpected.\ 24 | \n expected: {}\ 25 | \n actual: {}\n", 26 | actual_parsed, 27 | expected_str, 28 | actual_string, 29 | ); 30 | } 31 | } 32 | 33 | #[test] 34 | fn format_spec_simple() { 35 | check_format_spec("", FormatSpec::default()); 36 | check_format_spec("x", FormatSpec { 37 | ty: Some('x'), 38 | .. FormatSpec::default() 39 | }); 40 | check_format_spec("<", FormatSpec { 41 | align: Some(Align::Left), 42 | .. FormatSpec::default() 43 | }); 44 | check_format_spec("x^", FormatSpec { 45 | fill: Some('x'), 46 | align: Some(Align::Center), 47 | .. FormatSpec::default() 48 | }); 49 | check_format_spec("+", FormatSpec { 50 | sign: Some(Sign::Plus), 51 | .. FormatSpec::default() 52 | }); 53 | check_format_spec("#", FormatSpec { 54 | alternate: true, 55 | .. FormatSpec::default() 56 | }); 57 | check_format_spec("0", FormatSpec { 58 | zero: true, 59 | .. FormatSpec::default() 60 | }); 61 | } 62 | 63 | #[test] 64 | fn format_spec_width() { 65 | check_format_spec("5", FormatSpec { 66 | width: Some(Width::Constant(5)), 67 | .. FormatSpec::default() 68 | }); 69 | check_format_spec("0$", FormatSpec { 70 | width: Some(Width::Position(0)), 71 | .. FormatSpec::default() 72 | }); 73 | check_format_spec("2$", FormatSpec { 74 | width: Some(Width::Position(2)), 75 | .. FormatSpec::default() 76 | }); 77 | check_format_spec("peter$", FormatSpec { 78 | width: Some(Width::Name("peter".into())), 79 | .. FormatSpec::default() 80 | }); 81 | } 82 | 83 | #[test] 84 | fn format_spec_precision() { 85 | check_format_spec(".5", FormatSpec { 86 | precision: Some(Precision::Constant(5)), 87 | .. FormatSpec::default() 88 | }); 89 | check_format_spec(".2$", FormatSpec { 90 | precision: Some(Precision::Position(2)), 91 | .. FormatSpec::default() 92 | }); 93 | check_format_spec(".peter$", FormatSpec { 94 | precision: Some(Precision::Name("peter".into())), 95 | .. FormatSpec::default() 96 | }); 97 | check_format_spec(".*", FormatSpec { 98 | precision: Some(Precision::Bundled), 99 | .. FormatSpec::default() 100 | }); 101 | } 102 | 103 | #[test] 104 | fn format_spec_mixed() { 105 | check_format_spec("1.0", FormatSpec { 106 | width: Some(Width::Constant(1)), 107 | precision: Some(Precision::Constant(0)), 108 | .. FormatSpec::default() 109 | }); 110 | check_format_spec("1$.peter$", FormatSpec { 111 | width: Some(Width::Position(1)), 112 | precision: Some(Precision::Name("peter".into())), 113 | .. FormatSpec::default() 114 | }); 115 | check_format_spec("04", FormatSpec { 116 | zero: true, 117 | width: Some(Width::Constant(4)), 118 | .. FormatSpec::default() 119 | }); 120 | check_format_spec("-<5", FormatSpec { 121 | fill: Some('-'), 122 | align: Some(Align::Left), 123 | width: Some(Width::Constant(5)), 124 | .. FormatSpec::default() 125 | }); 126 | check_format_spec("00", FormatSpec { 127 | zero: true, 128 | width: Some(Width::Constant(0)), 129 | .. FormatSpec::default() 130 | }); 131 | check_format_spec("#010x", FormatSpec { 132 | alternate: true, 133 | zero: true, 134 | width: Some(Width::Constant(10)), 135 | ty: Some('x'), 136 | .. FormatSpec::default() 137 | }); 138 | 139 | check_format_spec("_>+#0x$.*?", FormatSpec { 140 | fill: Some('_'), 141 | align: Some(Align::Right), 142 | sign: Some(Sign::Plus), 143 | alternate: true, 144 | zero: true, 145 | width: Some(Width::Name("x".into())), 146 | precision: Some(Precision::Bundled), 147 | ty: Some('?'), 148 | }); 149 | } 150 | -------------------------------------------------------------------------------- /mine.zsh-theme: -------------------------------------------------------------------------------- 1 | PROMPT='%{$fg[cyan]%}%c/%{$reset_color%}$(git_prompt_info)' 2 | PROMPT+=" %(?:%{$fg_bold[green]%}➜ :%{$fg_bold[red]%}➜ )%{$reset_color%} " 3 | 4 | ZSH_THEME_GIT_PROMPT_PREFIX=" %{$fg[blue]%}(" 5 | ZSH_THEME_GIT_PROMPT_SUFFIX="%{$fg[blue]%})%{$reset_color%}" 6 | ZSH_THEME_GIT_PROMPT_DIRTY=" %{$fg[red]%}✖%{$reset_color%}" 7 | ZSH_THEME_GIT_PROMPT_CLEAN=" %{$fg[green]%}✔%{$reset_color%}" 8 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate offers a couple of macros to easily print colored and formatted 2 | //! text to a terminal. It is basically just a convenience API on top of 3 | //! [`termcolor`](https://crates.io/crates/termcolor). Thus, some understanding 4 | //! of `termcolor` is useful to use `bunt`. 5 | //! 6 | //! Mini example: 7 | //! 8 | //! ``` 9 | //! let ty = "u32"; 10 | //! bunt::println!("{$bold+red}error:{/$} invalid value for type `{[blue]}`", ty); 11 | //! ``` 12 | //! 13 | //! # Format string syntax 14 | //! 15 | //! The macros in this crate have almost the same syntax as the corresponding 16 | //! `std::fmt` macros: arguments are inserted with `{}` and braces are escaped 17 | //! with `{{` and `}}`. `bunt` has two additions to that syntax: 18 | //! 19 | //! ## Style tags 20 | //! 21 | //! With `{$style_spec}...{/$}`, you can apply a style to a section of your 22 | //! string (which can also contain arguments). The start tag `{$...}` contains 23 | //! the style specification, while the end tag is always `{/$}`. These tags can 24 | //! also be nested. 25 | //! 26 | //! ``` 27 | //! bunt::println!("normal color ... {$yellow}Yellow :){/$} ... normal color again"); 28 | //! bunt::println!("{$bold}This is bold. {$red}This is also red!{/$} Just bold again{/$}."); 29 | //! ``` 30 | //! 31 | //!Each opening tag needs a matching closing one and the other way around. 32 | //! 33 | //! ```compile_fail 34 | //! bunt::println!("{$red}unclosed tag :o"); 35 | //! ``` 36 | //! 37 | //! ```compile_fail 38 | //! bunt::println!("{$red}close it once{/$} and close it another time 🙁 {/$}"); 39 | //! ``` 40 | //! 41 | //! ## Styled arguments 42 | //! 43 | //! If you want to style an argument, you can use tags right before and after 44 | //! that argument. However, there is also a shorthand syntax: `{[style_spec] 45 | //! ...}`. You can still use the syntax for named arguments, positional 46 | //! arguments, width, fill/alignmen, precision, formatting traits and everything 47 | //! else from `std::fmt` after the `[...]`. 48 | //! 49 | //! ``` 50 | //! // Normal output via `Display`. Equivalent to `"value: {$green}{}{/$}"` 51 | //! bunt::println!("value: {[green]}", 27); 52 | //! 53 | //! // Output via `Debug`. All argument formatting syntax from `fmt` works 54 | //! // inside the braces, after the `[...]`. 55 | //! bunt::println!("value: {[green]:?}", vec![1, 2, 3]); 56 | //! 57 | //! // Named argument + precision specified: works. 58 | //! bunt::println!("value: {[green]foo:.5}", foo = 3.14); 59 | //! ``` 60 | //! 61 | //! ## Style specification 62 | //! 63 | //! `bunt` has the same capabilities as `termcolor`. See [`termcolor::Color`] 64 | //! and [`termcolor::ColorSpec`] for more information. The syntax for style 65 | //! specs in `bunt` is a simple list of fragments that are joined by `+`. 66 | //! Examples: 67 | //! 68 | //! - `red` 69 | //! - `#ff8030+bold` 70 | //! - `yellow+italic+intense` 71 | //! - `bg:white+blue+bold` 72 | //! 73 | //! Full list of allowed fragments: 74 | //! 75 | //! - Colors: 76 | //! - `black`, `blue`, `green`, `red`, `cyan`, `magenta`, `yellow`, `white` 77 | //! - RGB as hex string: `#rrggbb`, e.g. `#27ae60` 78 | //! - 8bit ANSI color codes: with `@` and a number between 0 and 255, e.g. `@197` 79 | //! - Background colors: same as colors but prefixed with `bg:`, e.g. `bg:blue` 80 | //! or `bg:#c0392b` 81 | //! - Attributes: 82 | //! - `bold` 83 | //! - `dimmed` 84 | //! - `italic` 85 | //! - `underline` 86 | //! - `intense` 87 | //! 88 | //! `bunt` macros make sure that your style spec makes sense (only one 89 | //! foreground/background color is allowed, duplicated attributes are not 90 | //! allowed). Invalid style specs result in a compile error. 91 | //! 92 | //! ```compile_fail 93 | //! bunt::println!("{$red+blue}what{/$}"); 94 | //! ``` 95 | //! 96 | //! ```compile_fail 97 | //! bunt::println!("{$bold+red+bold}you don't have to say it twice buddy{/$}"); 98 | //! ``` 99 | //! 100 | //! 101 | //! [`termcolor::Color`]: https://docs.rs/termcolor/1.1.0/termcolor/enum.Color.html 102 | //! [`termcolor::ColorSpec`]: https://docs.rs/termcolor/1.1.0/termcolor/struct.ColorSpec.html 103 | //! 104 | //! 105 | //! # Available macros 106 | //! 107 | //! - [`write`] and [`writeln`]: print to a `termcolor::WriteColor` instance. 108 | //! - [`print`] and [`println`]: print to stdout. 109 | //! - [`eprint`] and [`eprintln`]: print to stderr. 110 | //! - [`style`]: parses a format specification and returns the corresponding 111 | //! `termcolor::ColorSpec` value. 112 | //! 113 | //! # Color Choice 114 | //! 115 | //! In real applications, you usually want to give your users the choice of 116 | //! color usage, e.g. via a `--color` CLI argument. If it's sufficient for you 117 | //! to configure this globally, see [`set_stdout_color_choice`] and 118 | //! [`set_stderr_color_choice`]. If not, you have to use `write[ln]` and pass 119 | //! the stream explicitly. By default, `ColorChoice::Auto` is used. 120 | //! 121 | //! `termcolor` already handles the env var `NO_COLOR=1` when the color choice 122 | //! is `Auto`. But it does not automatically detect the presence of a terminal. 123 | //! You likely want that! See [here][1] for more details. 124 | //! 125 | //! [1]: https://docs.rs/termcolor/latest/termcolor/index.html#detecting-presence-of-a-terminal 126 | //! 127 | //! 128 | //! # Passing multiple format strings (`concat!` replacement) 129 | //! 130 | //! In many cases, users wish to call `concat!` and pass the result as format 131 | //! string to bunt's macros, e.g. `bunt::println!(concat!("foo", "bar"))`. This 132 | //! is mainly used if you want to write your own macro to wrap bunt's macros. 133 | //! Unfortunately, this is not easily possible as macros are expaned lazily. See 134 | //! [issue #15](https://github.com/LukasKalbertodt/bunt/issues/15) for more 135 | //! information. 136 | //! 137 | //! As a workaround for this fairly common use case, bunt allows passing an 138 | //! "array of format strings", like so: 139 | //! 140 | //! ``` 141 | //! bunt::println!(["foo ", "{[red]} bar"], 27); 142 | //! ``` 143 | //! 144 | //! All given strings will be concatenated by `bunt`. So the above code is 145 | //! equivalent to `bunt::println!("foo {[red]} bar", 27)`. 146 | //! 147 | //! For most users this feature is irrelevant. If possible, pass the format 148 | //! string as single string literal. 149 | //! 150 | 151 | #![deny(broken_intra_doc_links)] 152 | 153 | // Reexport of `termcolor`. This is mostly to be used by the code generated by 154 | // the macros. 155 | pub extern crate termcolor; 156 | 157 | // To consistently refer to the macros crate. 158 | #[doc(hidden)] 159 | pub extern crate bunt_macros; 160 | 161 | 162 | use std::sync::atomic::{AtomicU8, Ordering}; 163 | use termcolor::ColorChoice; 164 | 165 | 166 | /// Writes formatted data to a `termcolor::WriteColor` target. 167 | /// 168 | /// This is a more general version of `print` as you can specify the destination 169 | /// of the formatted data as first parameter. `write` also returns a `Result<(), 170 | /// std::io::Error>` which is `Err` in case writing to the target or setting the 171 | /// color fails. `print!` simply panics in that case. 172 | /// 173 | /// ``` 174 | /// use bunt::termcolor::{ColorChoice, StandardStream}; 175 | /// 176 | /// // Choosing a different color choice, just to show something `println` 177 | /// // can't do. 178 | /// let mut stdout = StandardStream::stdout(ColorChoice::Always); 179 | /// let result = bunt::write!(stdout, "{$red}bad error!{/$}"); 180 | /// 181 | /// if result.is_err() { 182 | /// // Writing to stdout failed... 183 | /// } 184 | /// ``` 185 | /// 186 | /// See crate-level docs for more information. 187 | // pub use bunt_macros::write; 188 | #[macro_export] 189 | macro_rules! write { 190 | ($target:expr, $format_str:literal $(, $arg:expr)* $(,)?) => { 191 | $crate::write!($target, [$format_str] $(, $arg )*) 192 | }; 193 | ($target:expr, [$($format_str:literal),+ $(,)?] $(, $arg:expr)* $(,)?) => { 194 | $crate::bunt_macros::write!( 195 | $target [$($format_str)+] $( $arg )* 196 | ) 197 | }; 198 | } 199 | 200 | /// Writes formatted data with newline to a `termcolor::WriteColor` target. 201 | /// 202 | /// Like [`write!`], but adds a newline (`\n`) at the end. 203 | /// 204 | /// ``` 205 | /// use bunt::termcolor::{ColorChoice, StandardStream}; 206 | /// 207 | /// // Choosing a different color choice, just to show something `println` 208 | /// // can't do. 209 | /// let mut stdout = StandardStream::stdout(ColorChoice::Always); 210 | /// let _ = bunt::writeln!(stdout, "{$red}bad error!{/$}"); 211 | /// ``` 212 | /// 213 | /// See crate-level docs for more information. 214 | #[macro_export] 215 | macro_rules! writeln { 216 | ($target:expr, $format_str:literal $(, $arg:expr)* $(,)?) => { 217 | $crate::writeln!($target, [$format_str] $(, $arg )*) 218 | }; 219 | ($target:expr, [$($format_str:literal),+ $(,)?] $(, $arg:expr)* $(,)?) => { 220 | $crate::bunt_macros::writeln!( 221 | $target [$($format_str)+] $( $arg )* 222 | ) 223 | }; 224 | ($target:expr $(,)?) => { 225 | $crate::writeln!($target, "") 226 | }; 227 | } 228 | 229 | /// Writes formatted data to stdout (with `ColorChoice::Auto`). 230 | /// 231 | /// This is like `write`, but always writes to 232 | /// `StandardStream::stdout(termcolor::ColorChoice::Auto)`. `print` also does 233 | /// not return a result, but instead panics if an error occurs writing to 234 | /// stdout. 235 | /// 236 | /// ``` 237 | /// bunt::print!("{$magenta}foo {[bold]} bar{/$}", 27); 238 | /// ``` 239 | /// 240 | /// See crate-level docs for more information. 241 | #[macro_export] 242 | macro_rules! print { 243 | ($format_str:literal $(, $arg:expr)* $(,)?) => { 244 | $crate::print!([$format_str] $(, $arg )*) 245 | }; 246 | ([$($format_str:literal),+ $(,)?] $(, $arg:expr)* $(,)?) => { 247 | $crate::bunt_macros::write!( 248 | ($crate::termcolor::StandardStream::stdout($crate::stdout_color_choice())) 249 | [$($format_str)+] $( $arg )* 250 | ).expect("failed to write to stdout in `bunt::print`") 251 | }; 252 | } 253 | 254 | /// Writes formatted data with newline to stdout (with `ColorChoice::Auto`). 255 | /// 256 | /// Like [`print!`], but adds a newline (`\n`) at the end. 257 | /// 258 | /// ``` 259 | /// bunt::println!("{$cyan}foo {[bold]} bar{/$}", true); 260 | /// ``` 261 | /// 262 | /// See crate-level docs for more information. 263 | #[macro_export] 264 | macro_rules! println { 265 | ($format_str:literal $(, $arg:expr)* $(,)?) => { 266 | $crate::println!([$format_str] $(, $arg )*) 267 | }; 268 | ([$($format_str:literal),+ $(,)?] $(, $arg:expr)* $(,)?) => { 269 | $crate::bunt_macros::writeln!( 270 | ($crate::termcolor::StandardStream::stdout($crate::stdout_color_choice())) 271 | [$($format_str)+] $( $arg )* 272 | ).expect("failed to write to stdout in `bunt::println`") 273 | }; 274 | () => { 275 | std::println!() 276 | }; 277 | } 278 | 279 | /// Writes formatted data to stderr (with `ColorChoice::Auto`). 280 | /// 281 | /// This is like `write`, but always writes to 282 | /// `StandardStream::stderr(termcolor::ColorChoice::Auto)`. `eprint` also does 283 | /// not return a result, but instead panics if an error occurs writing to 284 | /// stderr. 285 | /// 286 | /// ``` 287 | /// bunt::eprint!("{$magenta}foo {[bold]} bar{/$}", 27); 288 | /// ``` 289 | /// 290 | /// See crate-level docs for more information. 291 | #[macro_export] 292 | macro_rules! eprint { 293 | ($format_str:literal $(, $arg:expr)* $(,)?) => { 294 | $crate::eprint!([$format_str] $(, $arg )*) 295 | }; 296 | ([$($format_str:literal),+ $(,)?] $(, $arg:expr)* $(,)?) => { 297 | $crate::bunt_macros::write!( 298 | ($crate::termcolor::StandardStream::stderr($crate::stderr_color_choice())) 299 | [$($format_str)+] $( $arg )* 300 | ).expect("failed to write to stderr in `bunt::eprint`") 301 | }; 302 | } 303 | 304 | /// Writes formatted data with newline to stderr (with `ColorChoice::Auto`). 305 | /// 306 | /// Like [`eprint!`], but adds a newline (`\n`) at the end. 307 | /// 308 | /// ``` 309 | /// bunt::eprintln!("{$cyan}foo {[bold]} bar{/$}", true); 310 | /// ``` 311 | /// 312 | /// See crate-level docs for more information. 313 | #[macro_export] 314 | macro_rules! eprintln { 315 | ($format_str:literal $(, $arg:expr)* $(,)?) => { 316 | $crate::eprintln!([$format_str] $(, $arg )*) 317 | }; 318 | ([$($format_str:literal),+ $(,)?] $(, $arg:expr)* $(,)?) => { 319 | $crate::bunt_macros::writeln!( 320 | ($crate::termcolor::StandardStream::stderr($crate::stderr_color_choice())) 321 | [$($format_str)+] $( $arg )* 322 | ).expect("failed to write to stderr in `bunt::eprintln`") 323 | }; 324 | () => { 325 | std::eprintln!() 326 | }; 327 | } 328 | 329 | /// Parses the given style specification string and returns the corresponding 330 | /// `termcolor::ColorSpec` value. 331 | /// 332 | /// ``` 333 | /// use bunt::termcolor::{Color, ColorChoice, StandardStream, WriteColor}; 334 | /// 335 | /// let style = bunt::style!("red+bold+bg:yellow"); 336 | /// let mut stdout = StandardStream::stdout(ColorChoice::Auto); 337 | /// stdout.set_color(&style)?; 338 | /// 339 | /// assert_eq!(style.fg(), Some(&Color::Red)); 340 | /// assert_eq!(style.bg(), Some(&Color::Yellow)); 341 | /// assert!(style.bold()); 342 | /// assert!(!style.dimmed()); 343 | /// assert!(!style.italic()); 344 | /// assert!(!style.underline()); 345 | /// assert!(!style.intense()); 346 | /// # std::io::Result::Ok(()) 347 | /// ``` 348 | /// 349 | /// See crate-level docs for more information. 350 | pub use bunt_macros::style; 351 | 352 | 353 | static STDOUT_COLOR_CHOICE: AtomicU8 = AtomicU8::new(color_choice_to_u8(ColorChoice::Auto)); 354 | static STDERR_COLOR_CHOICE: AtomicU8 = AtomicU8::new(color_choice_to_u8(ColorChoice::Auto)); 355 | 356 | const fn color_choice_to_u8(c: ColorChoice) -> u8 { 357 | match c { 358 | ColorChoice::Always => 0, 359 | ColorChoice::AlwaysAnsi => 1, 360 | ColorChoice::Auto => 2, 361 | ColorChoice::Never => 3, 362 | } 363 | } 364 | 365 | fn u8_to_color_choice(v: u8) -> ColorChoice { 366 | match v { 367 | 0 => ColorChoice::Always, 368 | 1 => ColorChoice::AlwaysAnsi, 369 | 2 => ColorChoice::Auto, 370 | 3 => ColorChoice::Never, 371 | _ => unreachable!("invalid global color choice"), 372 | } 373 | } 374 | 375 | /// Returns the current global `ColorChoice` used by `print[ln]`. 376 | /// 377 | /// This is `ColorChoice::Auto` by default and can be changed with 378 | /// [`set_stdout_color_choice`]. If you need more control than a global 379 | /// setting, use `write[ln]` instead of `print[ln]` and pass the stream 380 | /// explicitly. 381 | pub fn stdout_color_choice() -> ColorChoice { 382 | u8_to_color_choice(STDOUT_COLOR_CHOICE.load(Ordering::SeqCst)) 383 | } 384 | 385 | /// Sets the global `ColorChoice` used by `print[ln]`. 386 | /// 387 | /// See [`stdout_color_choice`] for more information. 388 | pub fn set_stdout_color_choice(c: ColorChoice) { 389 | STDOUT_COLOR_CHOICE.store(color_choice_to_u8(c), Ordering::SeqCst); 390 | } 391 | 392 | /// Returns the current global `ColorChoice` used by `eprint[ln]`. 393 | /// 394 | /// This is `ColorChoice::Auto` by default and can be changed with 395 | /// [`set_stderr_color_choice`]. If you need more control than a global 396 | /// setting, use `write[ln]` instead of `eprint[ln]` and pass the stream 397 | /// explicitly. 398 | pub fn stderr_color_choice() -> ColorChoice { 399 | u8_to_color_choice(STDERR_COLOR_CHOICE.load(Ordering::SeqCst)) 400 | } 401 | 402 | /// Sets the global `ColorChoice` used by `eprint[ln]`. 403 | /// 404 | /// See [`stderr_color_choice`] for more information. 405 | pub fn set_stderr_color_choice(c: ColorChoice) { 406 | STDERR_COLOR_CHOICE.store(color_choice_to_u8(c), Ordering::SeqCst); 407 | } 408 | -------------------------------------------------------------------------------- /tests/main.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use bunt::{ 3 | write, writeln, print, println, eprint, eprintln, 4 | termcolor::Buffer, 5 | }; 6 | 7 | 8 | fn buf() -> Buffer { 9 | Buffer::ansi() 10 | } 11 | fn raw_str(buf: &Buffer) -> &str { 12 | std::str::from_utf8(buf.as_slice()).expect("test produced non-UTF8 string") 13 | } 14 | 15 | // Helper macro that checks the output of a `write!` invocation against an 16 | // expected string. 17 | macro_rules! check { 18 | ($expected:literal == $($t:tt)*) => {{ 19 | let mut buf = buf(); 20 | write!(buf, $($t)*).expect("failed to write to buffer in test"); 21 | let actual = raw_str(&buf); 22 | if $expected != actual { 23 | panic!( 24 | "incorrect output for `write!({})`:\n \ 25 | expected: {:?} ({})\n \ 26 | actual: {:?} ({})\n", 27 | stringify!($($t)*), 28 | $expected, 29 | $expected, 30 | actual, 31 | actual, 32 | ); 33 | } 34 | }}; 35 | } 36 | 37 | #[test] 38 | fn writeln() { 39 | let mut b = buf(); 40 | writeln!(b, "").unwrap(); 41 | assert_eq!(raw_str(&b), "\n"); 42 | 43 | let mut b = buf(); 44 | writeln!(b, "hello").unwrap(); 45 | assert_eq!(raw_str(&b), "hello\n"); 46 | 47 | let mut b = buf(); 48 | writeln!(b, "a {$red}b{/$}").unwrap(); 49 | assert_eq!(raw_str(&b), "a \x1b[0m\x1b[31mb\x1b[0m\n"); 50 | } 51 | 52 | #[test] 53 | fn no_move_buffer() { 54 | { 55 | let mut b = buf(); 56 | write!(b, "hi").unwrap(); 57 | drop(b); 58 | } 59 | { 60 | let mut b = buf(); 61 | write!(&mut b, "hi").unwrap(); 62 | drop(b); 63 | } 64 | 65 | write!(buf(), "hi").unwrap(); 66 | write!(&mut buf(), "hi").unwrap(); 67 | } 68 | 69 | #[test] 70 | fn no_move_args() { 71 | #[derive(Debug)] 72 | struct NoCopy; 73 | 74 | impl fmt::Display for NoCopy { 75 | fn fmt(&self, _: &mut fmt::Formatter) -> fmt::Result { 76 | Ok(()) 77 | } 78 | } 79 | 80 | { 81 | let string = "hi".to_string(); 82 | let no_copy = NoCopy; 83 | let mut b = buf(); 84 | write!(b, "a{[green]}b{:?}c", string, no_copy).unwrap(); 85 | assert_eq!(raw_str(&b), "a\x1b[0m\x1b[32mhi\x1b[0mbNoCopyc"); 86 | 87 | // We can still use the variables. 88 | drop(string); 89 | drop(no_copy); 90 | } 91 | { 92 | let s = "x".to_string(); 93 | let mut b = buf(); 94 | writeln!(b, "a{:?}b{}", s, s).unwrap(); 95 | drop(s); 96 | } 97 | 98 | // The `print[ln]` use termcolor and thus circumvent the stdout capture by 99 | // the Rust test harness. To avoid ugly test output, we print basically no 100 | // text. Only `println` actually emits one newline. 101 | { 102 | let x = NoCopy; 103 | print!("{}", x); 104 | drop(x); 105 | } 106 | { 107 | let x = NoCopy; 108 | println!("{}", x); 109 | drop(x); 110 | } 111 | } 112 | 113 | #[test] 114 | fn empty_ln() { 115 | // Just make sure they compile, we cannot easily capture and test their output. 116 | println!(); 117 | eprintln!(); 118 | 119 | 120 | let mut b = buf(); 121 | writeln!(b).unwrap(); 122 | writeln!(b,).unwrap(); 123 | assert_eq!(raw_str(&b), "\n\n"); 124 | } 125 | 126 | #[test] 127 | fn arg_referal() { 128 | check!("27" == "{peter}", peter = 27); 129 | check!("27 27" == "{0} {0}", 27); 130 | 131 | check!( 132 | "a p c b a m" == 133 | "{} {peter} {2} {} {0} {mary}", 'a', 'b', 'c', peter = 'p', mary = 'm' 134 | ); 135 | 136 | check!("3 5" == "{} {}", foo = 3, bar = 5); 137 | check!("3 7 5 3 5 7" == "{} {baz} {} {0} {bar} {}", foo = 3, bar = 5, baz = 7); 138 | } 139 | 140 | #[test] 141 | fn arg_referal_width() { 142 | check!("10|foo |true" == "{}|{:0$}|{}", 10, "foo", true); 143 | check!("10|foo |true" == "{}|{:3$}|{}", 10, "foo", true, 7); 144 | check!("10|foo |true" == "{}|{:wid$}|{}", 10, "foo", true, wid = 7); 145 | check!("10|foo |7" == "{}|{:wid$}|{wid}", 10, "foo", wid = 7); 146 | 147 | check!("bar| 5|5" == "{}|{1:1$}|{}", "bar", 5); 148 | check!("bar|anna |5" == "{}|{name:1$}|{}", "bar", 5, name = "anna"); 149 | check!("bar| 5|5" == "{}|{1:wid$}|{}", "bar", 5, wid = 9); 150 | check!("bar|anna |5" == "{}|{name:wid$}|{}", "bar", 5, wid = 9, name = "anna"); 151 | } 152 | 153 | #[test] 154 | fn arg_referal_precision() { 155 | check!("2|3.14|true" == "{}|{:.0$}|{}", 2, 3.1415926, true); 156 | check!("2|3.142|true" == "{}|{:.3$}|{}", 2, 3.1415926, true, 3); 157 | check!("2|3.142|true" == "{}|{:.prec$}|{}", 2, 3.1415926, true, prec = 3); 158 | check!("2|3.142|3" == "{}|{:.prec$}|{prec}", 2, 3.1415926, prec = 3); 159 | 160 | check!("bar|3.1|1" == "{}|{2:.1$}|{}", "bar", 1, 3.1415926); 161 | check!("bar|3.1|1" == "{}|{pi:.1$}|{}", "bar", 1, pi = 3.1415926); 162 | check!("bar|3.1|1" == "{}|{pi:.prec$}|{}", "bar", 1, pi = 3.1415926, prec = 1); 163 | check!("bar|3.1|1" == "{}|{2:.prec$}|{}", "bar", 1, 3.1415926, prec = 1); 164 | 165 | check!("foo|3.14|true" == "{}|{:.*}|{}", "foo", 2, 3.1415926, true); 166 | check!("3.1415926|3.14|foo" == "{}|{0:.*}|{}", 3.1415926, 2, "foo"); 167 | check!("true|3.14|foo" == "{}|{pi:.*}|{}", true, 2, "foo", pi = 3.1415926); 168 | } 169 | 170 | #[test] 171 | fn raw_strings() { 172 | check!("hello" == r"hello"); 173 | check!(r"a\n" == r"a\n"); 174 | } 175 | 176 | #[test] 177 | fn no_style() { 178 | check!("hello" == "hello"); 179 | check!("Foo 27 bar" == "Foo {} bar", 27); 180 | check!("x Foo 27 bar" == "{} Foo {} bar", 'x', 27); 181 | check!("Foo 8 barfren" == "Foo {} bar{}", 8, "fren"); 182 | 183 | check!( 184 | "a\nb\tc\rd\0e\x48f\u{50}g\u{228}h\u{fffe}i\u{1F923}j" == 185 | "a\nb\tc\rd\0e\x48f\u{50}g\u{228}h\u{fffe}i\u{1F923}j" 186 | ); 187 | check!( 188 | "abc\ 189 | def" == 190 | "abcdef" 191 | ); 192 | } 193 | 194 | #[test] 195 | fn simple_tags() { 196 | check!("a\x1b[0m\x1b[31mb\x1b[0mc" == "a{$red}b{/$}c"); 197 | check!("a\x1b[0m\x1b[33mbanana\x1b[0m" == "a{$yellow}banana{/$}"); 198 | check!("\x1b[0m\x1b[34mocean\x1b[0m is wet" == "{$blue}ocean{/$} is wet"); 199 | check!("\x1b[0m\x1b[1meverything\x1b[0m" == "{$bold}everything{/$}"); 200 | 201 | check!("foo\x1b[0m\x1b[1m\x1b[32mbar\x1b[0mbaz" == "foo{$bold+green}bar{/$}baz"); 202 | check!( 203 | "foo\x1b[0m\x1b[1m\x1b[32m\x1b[44mbar\x1b[0mbaz" == 204 | "foo{$bold+green+bg:blue}bar{/$}baz" 205 | ); 206 | check!( 207 | "foo\x1b[0m\x1b[1m\x1b[3m\x1b[38;5;10m\x1b[48;5;12mbar\x1b[0mbaz" == 208 | "foo{$bold+green+bg:blue+intense+italic}bar{/$}baz" 209 | ); 210 | } 211 | 212 | #[test] 213 | fn attributes() { 214 | check!("a\x1b[0m\x1b[1mb\x1b[0mc" == "a{$bold}b{/$}c"); 215 | check!("a\x1b[0m\x1b[2mb\x1b[0mc" == "a{$dimmed}b{/$}c"); 216 | check!("a\x1b[0m\x1b[3mb\x1b[0mc" == "a{$italic}b{/$}c"); 217 | check!("a\x1b[0m\x1b[4mb\x1b[0mc" == "a{$underline}b{/$}c"); 218 | } 219 | 220 | #[test] 221 | fn nested_tags() { 222 | check!( 223 | "a\x1b[0m\x1b[31mb\x1b[0m\x1b[1m\x1b[31mc\x1b[0m\x1b[31md\x1b[0me" == 224 | "a{$red}b{$bold}c{/$}d{/$}e" 225 | ); 226 | check!( 227 | "a\x1b[0m\x1b[31mb\x1b[0m\x1b[33mc\x1b[0m\x1b[31md\x1b[0me" == 228 | "a{$red}b{$yellow}c{/$}d{/$}e" 229 | ); 230 | check!( 231 | "a\x1b[0m\x1b[31mb\x1b[0m\x1b[1m\x1b[31mc\x1b[0m\x1b[1m\x1b[33md\x1b[0m\x1b[1m\x1b[31me\ 232 | \x1b[0m\x1b[31mf\x1b[0mg" == 233 | "a{$red}b{$bold}c{$yellow}d{/$}e{/$}f{/$}g" 234 | ); 235 | } 236 | 237 | #[test] 238 | fn colored_args() { 239 | check!("\x1b[0m\x1b[32m27\x1b[0m" == "{[green]}", 27); 240 | check!("a\x1b[0m\x1b[32m27\x1b[0m" == "a{[green]}", 27); 241 | check!("\x1b[0m\x1b[32m27\x1b[0mb" == "{[green]}b", 27); 242 | check!("a\x1b[0m\x1b[32m27\x1b[0mb" == "a{[green]}b", 27); 243 | 244 | check!("\x1b[0m\x1b[35mtrue\x1b[0m" == "{[magenta]:?}", true); 245 | check!("\x1b[0m\x1b[35m3f\x1b[0m" == "{[magenta]:x}", 0x3f); 246 | check!("\x1b[0m\x1b[35m3F\x1b[0m" == "{[magenta]:X}", 0x3f); 247 | check!("\x1b[0m\x1b[35m123\x1b[0m" == "{[magenta]:o}", 0o123); 248 | check!("\x1b[0m\x1b[35m101010\x1b[0m" == "{[magenta]:b}", 0b101010); 249 | check!("\x1b[0m\x1b[35m3.14e0\x1b[0m" == "{[magenta]:e}", 3.14); 250 | check!("\x1b[0m\x1b[35m3.14E0\x1b[0m" == "{[magenta]:E}", 3.14); 251 | 252 | check!( 253 | "a \x1b[0m\x1b[32m27\x1b[0m b \x1b[0m\x1b[31mtrue\x1b[0m c \x1b[0m\x1b[33mbanana\x1b[0m" == 254 | "a {[green]} b {[red]} c {[yellow]}", 27, true, "banana" 255 | ); 256 | check!( 257 | "a \x1b[0m\x1b[32m27\x1b[0m b \x1b[0m\x1b[1m3.14\x1b[0m c" == 258 | "a {[green]} b {[bold]} c", 27, 3.14 259 | ); 260 | } 261 | 262 | #[test] 263 | fn mixed_tag_args() { 264 | check!( 265 | "a \x1b[0m\x1b[1mb\x1b[0m\x1b[1m\x1b[32m27\x1b[0m\x1b[1mc\x1b[0md" == 266 | "a {$bold}b{[green]}c{/$}d", 27 267 | ); 268 | 269 | check!( 270 | "a \x1b[0m\x1b[33m...\x1b[0m\x1b[38;5;11m27\x1b[0m\x1b[33m...\x1b[0m\x1b[1m\x1b\ 271 | [33mb\x1b[0m\x1b[1m\x1b[32mtrue\x1b[0m\x1b[1m\x1b[33mc\x1b[0m\x1b[33m\x1b[0md" == 272 | "a {$yellow}...{[intense]}...{$bold}b{[green]}c{/$}{/$}d", 27, true 273 | ); 274 | } 275 | 276 | #[test] 277 | fn questionmark_in_argument() { 278 | fn foo(s: &str) -> Result { 279 | let mut b = buf(); 280 | let _ = bunt::writeln!(b, "Hello {[green]}", s.parse::()?); 281 | Ok(true) 282 | } 283 | 284 | assert!(foo("hi").is_err()); 285 | assert_eq!(foo("3"), Ok(true)); 286 | } 287 | 288 | #[test] 289 | fn io_error() { 290 | let mut empty = []; 291 | let res = bunt::write!(&mut empty[..], "hello"); 292 | assert!(res.is_err()); 293 | } 294 | 295 | #[test] 296 | fn set_color_error() { 297 | use std::io; 298 | 299 | struct Dummy; 300 | 301 | impl io::Write for Dummy { 302 | fn write(&mut self, buf: &[u8]) -> io::Result { 303 | Ok(buf.len()) 304 | } 305 | fn flush(&mut self) -> io::Result<()> { 306 | Ok(()) 307 | } 308 | } 309 | 310 | impl bunt::termcolor::WriteColor for Dummy { 311 | fn supports_color(&self) -> bool { 312 | true 313 | } 314 | fn set_color(&mut self, _: &bunt::termcolor::ColorSpec) -> io::Result<()> { 315 | Err(io::Error::new(io::ErrorKind::NotFound, "oops")) 316 | } 317 | fn reset(&mut self) -> io::Result<()> { 318 | Ok(()) 319 | } 320 | } 321 | 322 | let mut b = Dummy; 323 | assert!(bunt::write!(b, "hello").is_ok()); 324 | 325 | let res = bunt::write!(b, "{$green}colooor{/$}"); 326 | assert!(res.is_err()); 327 | assert_eq!(res.unwrap_err().kind(), io::ErrorKind::NotFound); 328 | } 329 | 330 | #[test] 331 | fn concat_fmt_strings() { 332 | check!("hello" == ["hello"]); 333 | check!("hello" == ["hello",]); 334 | check!("foobar" == ["foo", "bar"]); 335 | check!("foobar" == ["foo", "bar",]); 336 | check!("foo 27 bar" == ["foo ", "{} bar"], 27); 337 | check!("abtruecd" == ["a", "b", "{}", "c", "d"], true); 338 | 339 | check!( 340 | "a\nb\tc\rd\0e\x48f\u{50}g\u{228}h\u{fffe}i\u{1F923}j" == 341 | ["a\n", "b\tc\r", "d\0e\x48f\u{50}", "g\u{228}h\u{fffe}i", "\u{1F923}j"] 342 | ); 343 | } 344 | 345 | #[test] 346 | fn concat_fmt_strings_all_strings() { 347 | let mut b = buf(); 348 | let _ = write!(b, ["a", "{} b"], 27); 349 | let _ = writeln!(b, ["a", "{} b"], 27); 350 | print!(["a", "{} \r"], 27); 351 | eprint!(["a", "{} \r"], 27); 352 | println!(["", "{}"], ""); 353 | eprintln!(["", "{}"], ""); 354 | } 355 | 356 | #[test] 357 | fn ansi_colors() { 358 | check!("a\x1b[0m\x1b[38;5;197mb\x1b[0mc" == "a{$@197}b{/$}c"); 359 | check!("a\x1b[0m\x1b[38;5;8mb\x1b[0mc" == "a{$@8}b{/$}c"); 360 | check!("a\x1b[0m\x1b[38;5;8mb\x1b[0mc" == "a{$@08}b{/$}c"); 361 | } 362 | --------------------------------------------------------------------------------