├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── rust.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE ├── README.md ├── cursive-core ├── Cargo.toml ├── LICENSE ├── README.md └── src │ ├── align.rs │ ├── backend.rs │ ├── buffer.rs │ ├── builder.rs │ ├── builder │ └── resolvable.rs │ ├── cursive_root.rs │ ├── cursive_run.rs │ ├── direction.rs │ ├── div.rs │ ├── dump.rs │ ├── event.rs │ ├── lib.rs │ ├── logger.rs │ ├── menu.rs │ ├── printer.rs │ ├── rect.rs │ ├── style │ ├── border_style.rs │ ├── color.rs │ ├── color_pair.rs │ ├── color_style.rs │ ├── effect.rs │ ├── gradient │ │ └── mod.rs │ ├── mod.rs │ ├── palette.rs │ └── style_types.rs │ ├── theme.rs │ ├── traits.rs │ ├── utils │ ├── counter.rs │ ├── immutify.rs │ ├── lines │ │ ├── mod.rs │ │ ├── simple │ │ │ ├── lines_iterator.rs │ │ │ ├── mod.rs │ │ │ ├── row.rs │ │ │ └── tests.rs │ │ └── spans │ │ │ ├── chunk.rs │ │ │ ├── chunk_iterator.rs │ │ │ ├── lines_iterator.rs │ │ │ ├── mod.rs │ │ │ ├── prefix.rs │ │ │ ├── row.rs │ │ │ ├── segment.rs │ │ │ ├── segment_merge_iterator.rs │ │ │ └── tests.rs │ ├── markup │ │ ├── ansi.rs │ │ ├── cursup.rs │ │ ├── gradient.rs │ │ ├── markdown.rs │ │ └── mod.rs │ ├── mod.rs │ ├── reader.rs │ └── span.rs │ ├── vec.rs │ ├── view │ ├── any.rs │ ├── finder.rs │ ├── into_boxed_view.rs │ ├── margins.rs │ ├── mod.rs │ ├── nameable.rs │ ├── position.rs │ ├── resizable.rs │ ├── scroll │ │ ├── core.rs │ │ ├── mod.rs │ │ └── raw.rs │ ├── scroll_base.rs │ ├── scrollable.rs │ ├── size_cache.rs │ ├── size_constraint.rs │ ├── view_path.rs │ ├── view_trait.rs │ └── view_wrapper.rs │ ├── views │ ├── boxed_view.rs │ ├── button.rs │ ├── canvas.rs │ ├── checkbox.rs │ ├── circular_focus.rs │ ├── debug_view.rs │ ├── dialog.rs │ ├── dummy.rs │ ├── edit_view.rs │ ├── enableable_view.rs │ ├── fixed_layout.rs │ ├── focus_tracker.rs │ ├── gradient_view.rs │ ├── hideable_view.rs │ ├── last_size_view.rs │ ├── layer.rs │ ├── linear_layout.rs │ ├── list_view.rs │ ├── menu_popup.rs │ ├── menubar.rs │ ├── mod.rs │ ├── named_view.rs │ ├── on_event_view.rs │ ├── on_layout_view.rs │ ├── padded_view.rs │ ├── panel.rs │ ├── progress_bar.rs │ ├── radio.rs │ ├── resized_view.rs │ ├── screens_view.rs │ ├── scroll_view.rs │ ├── select_view.rs │ ├── shadow_view.rs │ ├── slider_view.rs │ ├── stack_view.rs │ ├── text_area.rs │ ├── text_view.rs │ ├── themed_view.rs │ └── tracked_view.rs │ ├── with.rs │ └── xy.rs ├── cursive-macros ├── Cargo.toml ├── LICENSE ├── Readme.md └── src │ ├── builder │ ├── blueprint.rs │ ├── callback_helper.rs │ ├── dummy_mod.rs │ ├── mod.rs │ └── real_mod.rs │ └── lib.rs ├── cursive-syntect ├── Cargo.toml ├── LICENSE ├── README.md ├── examples │ └── parse.rs └── src │ └── lib.rs ├── cursive ├── Cargo.toml ├── LICENSE ├── README.md ├── examples │ ├── Readme.md │ ├── advanced_user_data.rs │ ├── ansi.rs │ ├── assets │ │ ├── cities.txt │ │ ├── lorem.txt │ │ └── style.toml │ ├── autocomplete.rs │ ├── builder.rs │ ├── builder.yaml │ ├── colored_text.rs │ ├── colors.rs │ ├── ctrl_c.rs │ ├── debug_console.rs │ ├── dialog.rs │ ├── edit.rs │ ├── fixed_layout.rs │ ├── focus.rs │ ├── gradient.rs │ ├── hello_world.rs │ ├── key_codes.rs │ ├── label-view.yaml │ ├── linear.rs │ ├── list_view.rs │ ├── logs.rs │ ├── lorem.rs │ ├── markup.rs │ ├── menubar.rs │ ├── menubar_styles.rs │ ├── mines │ │ ├── Readme.md │ │ ├── board │ │ │ ├── mod.rs │ │ │ ├── model.rs │ │ │ └── view.rs │ │ └── main.rs │ ├── mutation.rs │ ├── panels.rs │ ├── pause.rs │ ├── position.rs │ ├── progress.rs │ ├── radio.rs │ ├── refcell_view.rs │ ├── scroll.rs │ ├── select.rs │ ├── select_test.rs │ ├── slider.rs │ ├── status.rs │ ├── status_bar_ext.rs │ ├── stopwatch.rs │ ├── tcp_server.rs │ ├── terminal_default.rs │ ├── text_area.rs │ ├── text_with_ansi_codes.txt │ ├── theme.rs │ ├── theme_editor.rs │ ├── theme_manual.rs │ ├── themed_view.rs │ ├── user_data.rs │ ├── view.yaml │ ├── vpv.rs │ ├── vspace.yaml │ └── window_title.rs └── src │ ├── backends │ ├── blt.rs │ ├── crossterm.rs │ ├── curses │ │ ├── mod.rs │ │ ├── n.rs │ │ └── pan.rs │ ├── mod.rs │ ├── puppet │ │ ├── mod.rs │ │ ├── observed.rs │ │ ├── observed_screen_view.rs │ │ └── static_values.rs │ ├── resize.rs │ └── termion.rs │ ├── cursive_ext.rs │ ├── cursive_runnable.rs │ ├── lib.rs │ └── utf8.rs ├── doc ├── cursive_cursive.png ├── cursive_example.png ├── test.txt ├── tutorial_1.md ├── tutorial_1.png ├── tutorial_2.md ├── tutorial_2.png ├── tutorial_3.md └── tutorial_3.png ├── examples ├── rust-analyzer.json ├── rustfmt.toml └── shell.nix /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Try to provide "simple" (if possible) code to reproduce the bug. If you can't simplify your code, a complete code is better than a simple, but incomplete one. 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Screenshots** 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | **Environment** 23 | * Operating system used 24 | * Backend used: ncurses (the default one), pancurses, crossterm, ... 25 | * Current locale (run `locale` in a terminal) 26 | * Cursive version (from crates.io, from git, ...) 27 | 28 | **Additional context** 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | TERM: xterm 12 | 13 | jobs: 14 | build: 15 | 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v1 20 | - name: Check 21 | # We run checks with all backend features to make sure they compile, but don't actually build them as they require external libs. 22 | run: cargo check --features "builder blt-backend ncurses-backend pancurses-backend termion-backend crossterm-backend markdown toml ansi" 23 | - name: Build 24 | run: cargo build --features "builder toml markdown ansi termion-backend crossterm-backend" --no-default-features --verbose 25 | - name: Run tests 26 | run: > 27 | cargo test --features "builder toml markdown ansi termion-backend crossterm-backend" --no-default-features --verbose && 28 | cargo test --example select_test 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | *.swp 3 | Cargo.lock 4 | tags 5 | .ctags 6 | *.bk 7 | TODO.txt 8 | *.rustfmt 9 | .idea 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | cache: cargo 3 | rust: 4 | - stable 5 | - nightly 6 | script: 7 | - cd cursive 8 | - cargo check --all-features 9 | - cargo build --verbose --features "markdown pancurses-backend termion-backend crossterm-backend" 10 | - cargo test --verbose --features "markdown pancurses-backend termion-backend crossterm-backend" 11 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | *TLDR*: Don't be a dick. Do we really need to be told that? 4 | 5 | ## Our Pledge 6 | 7 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 8 | 9 | ## Our Standards 10 | 11 | Examples of behavior that contributes to creating a positive environment include: 12 | 13 | * Being yay! 14 | * Using welcoming and inclusive language 15 | * Being respectful of differing viewpoints and experiences 16 | * Gracefully accepting constructive criticism 17 | * Focusing on what is best for the community 18 | * Showing empathy towards other community members 19 | 20 | Examples of unacceptable behavior by participants include: 21 | 22 | * Being boooh :( 23 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 24 | * Trolling, insulting/derogatory comments, and personal or political attacks 25 | * Public or private harassment 26 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 27 | * Other conduct which could reasonably be considered inappropriate in a professional setting 28 | 29 | ## Our Responsibilities 30 | 31 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 32 | 33 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 34 | 35 | ## Scope 36 | 37 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 38 | 39 | ## Enforcement 40 | 41 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at alexandre.bury@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 42 | 43 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 44 | 45 | ## Attribution 46 | 47 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]. 48 | 49 | It is a completely generic piece of text that should surprise no-one and may only remind people what it means to be a human being. But it ticks a box on github's TODO list. 50 | 51 | [homepage]: http://contributor-covenant.org 52 | [version]: http://contributor-covenant.org/version/1/4/ 53 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Thank you so much for helping on cursive! 2 | 3 | Here are a few guidelines to help make sure your contribution goes as smoothly as possible. 4 | 5 | ## You found a bug 6 | 7 | This is still a young library, and bugs are sure to be hiding everywhere. If you found one, great! 8 | 9 | * Mention the operating system you are using. If you are manually selecting a backend (for instance termion), please mention this as well. 10 | * Try to provide "simple" (if possible) code to reproduce the bug. If you can't simplify your code, a complete code is better than a simple, but incomplete one. 11 | * If relevant, you can post a screenshot describing the problem. 12 | 13 | ## Improvement idea 14 | 15 | If you have an idea you think would make this library better, we're all ears! 16 | 17 | * Describe your use-case: what do you want to achieve with this improvement? 18 | 19 | ## Pull requests 20 | 21 | If you feel like digging in the code, thank you again! We're very grateful for your work. 22 | 23 | * Format your code with rustfmt. 24 | * Make sure all tests are passing - it's very easy to forget a backend! If you start a Pull Request, travis will check all backends for you. 25 | * Only use stable rust features. This library should compile on rust stable. 26 | * Do one thing at a time. Split a large change into a series of smaller, standalone changes. Don't start a refactor while adding a feature. 27 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = ["cursive", "cursive-core", "cursive-syntect", "cursive-macros"] 4 | resolver = "2" 5 | 6 | [profile.dev] 7 | incremental = false 8 | 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Alexandre Bury 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /cursive-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Alexandre Bury "] 3 | categories = ["command-line-interface", "gui"] 4 | description = "Core components for the Cursive TUI" 5 | documentation = "https://docs.rs/cursive" 6 | keywords = ["ncurses", "TUI", "UI"] 7 | license = "MIT" 8 | name = "cursive_core" 9 | readme = "README.md" 10 | repository = "https://github.com/gyscos/cursive" 11 | version = "0.4.6" 12 | edition = "2021" 13 | rust-version = "1.70" 14 | include = ["src/**/*", "LICENSE", "README.md"] 15 | 16 | [package.metadata.docs.rs] 17 | all-features = true 18 | 19 | [dependencies] 20 | enum-map = "2.0" 21 | enumset = "1.1.4" # EnumSet::empty() became `const` in 1.1.4 22 | log = "0.4" 23 | unicode-segmentation = "1" 24 | unicode-width = "0.2" 25 | xi-unicode = "0.3" 26 | crossbeam-channel = "0.5" 27 | lazy_static = "1" 28 | ahash = "0.8" 29 | serde_json = "1.0.85" 30 | parking_lot = { version = "0.12.1", features = ["arc_lock"] } 31 | compact_str = "0.8.0" 32 | 33 | [dependencies.cursive-macros] 34 | path = "../cursive-macros" 35 | version = "0.1.0" 36 | 37 | [dependencies.inventory] 38 | version = "0.3.1" 39 | optional = true 40 | 41 | [dependencies.ansi-parser] 42 | version = "0.9.0" 43 | optional = true 44 | 45 | [dependencies.time] 46 | version = "0.3" 47 | features = ["local-offset", "formatting"] 48 | 49 | [dependencies.toml] 50 | optional = true 51 | version = "0.8" 52 | 53 | [dependencies.num] 54 | default-features = false 55 | version = "0.4" 56 | 57 | [dependencies.pulldown-cmark] 58 | default-features = false 59 | optional = true 60 | version = "0.12" 61 | 62 | [features] 63 | default = [] 64 | doc-cfg = [] # Add doc-centric features 65 | builder = ["dep:inventory", "cursive-macros/builder"] # Enable building views from configs 66 | markdown = ["dep:pulldown-cmark"] # Enables a markdown-to-styled string parser 67 | ansi = ["dep:ansi-parser"] # Enables an ansi-to-styled string parser 68 | 69 | [lib] 70 | name = "cursive_core" 71 | 72 | [dev-dependencies] 73 | serde_yaml = "0.9.11" 74 | -------------------------------------------------------------------------------- /cursive-core/LICENSE: -------------------------------------------------------------------------------- 1 | ../LICENSE -------------------------------------------------------------------------------- /cursive-core/README.md: -------------------------------------------------------------------------------- 1 | # Cursive-core 2 | 3 | This crate is where most of cursive is defined, except for the backends. 4 | 5 | Third-party libraries are encouraged to depend on this instead of `cursive`, as it should have fewer semver breaking updates. 6 | -------------------------------------------------------------------------------- /cursive-core/src/div.rs: -------------------------------------------------------------------------------- 1 | use num::Num; 2 | 3 | /// Integer division that rounds up. 4 | pub fn div_up(p: T, q: T) -> T 5 | where 6 | T: Num + Clone, 7 | { 8 | let d = p.clone() / q.clone(); 9 | 10 | if p % q == T::zero() { 11 | d 12 | } else { 13 | T::one() + d 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /cursive-core/src/dump.rs: -------------------------------------------------------------------------------- 1 | use crate::{theme::Theme, views, Cursive}; 2 | use crossbeam_channel::{Receiver, Sender}; 3 | use std::any::Any; 4 | use std::num::NonZeroU32; 5 | 6 | type Callback = dyn FnOnce(&mut Cursive) + Send; 7 | 8 | /// Represents a dump of everything from a `Cursive` instance. 9 | /// 10 | /// See [`Cursive::dump()`](../cursive.html#method.dump) 11 | pub struct Dump { 12 | pub(crate) cb_sink: Sender>, 13 | pub(crate) cb_source: Receiver>, 14 | 15 | pub(crate) fps: Option, 16 | 17 | pub(crate) menubar: views::Menubar, 18 | pub(crate) root_view: views::OnEventView>, 19 | 20 | pub(crate) theme: Theme, 21 | 22 | pub(crate) user_data: Box, 23 | } 24 | -------------------------------------------------------------------------------- /cursive-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Cursive-core 2 | //! 3 | //! This library defines the core components for the Cursive TUI. 4 | //! 5 | //! The main purpose of `cursive-core` is to write third-party libraries to work with Cursive. 6 | //! 7 | //! If you are building an end-user application, then [`cursive`] is probably what you want. 8 | //! 9 | //! [`cursive`]: https://docs.rs/cursive 10 | #![deny(missing_docs)] 11 | #![cfg_attr(feature = "doc-cfg", feature(doc_cfg))] 12 | 13 | macro_rules! new_default( 14 | ($c:ident<$t:ident>) => { 15 | impl<$t> Default for $c<$t> { 16 | fn default() -> Self { 17 | Self::new() 18 | } 19 | } 20 | }; 21 | ($c:ident) => { 22 | impl Default for $c { 23 | fn default() -> Self { 24 | Self::new() 25 | } 26 | } 27 | }; 28 | ($c:ident<$t:ident: Default>) => { 29 | impl <$t> Default for $c<$t> 30 | where $t: Default { 31 | fn default() -> Self { 32 | Self::new($t::default()) 33 | } 34 | } 35 | }; 36 | ); 37 | 38 | /// Re-export crates used in the public API 39 | pub mod reexports { 40 | pub use ahash; 41 | pub use crossbeam_channel; 42 | pub use enumset; 43 | pub use log; 44 | pub use time; 45 | 46 | #[cfg(feature = "toml")] 47 | pub use toml; 48 | 49 | #[cfg(feature = "ansi")] 50 | pub use ansi_parser; 51 | 52 | pub use serde_json; 53 | } 54 | 55 | // use crate as cursive; 56 | 57 | pub use cursive_macros::{blueprint, callback_helpers}; 58 | 59 | #[macro_use] 60 | pub mod utils; 61 | #[macro_use] 62 | pub mod view; 63 | #[macro_use] 64 | pub mod views; 65 | 66 | pub mod align; 67 | pub mod backend; 68 | pub mod direction; 69 | pub mod event; 70 | pub mod logger; 71 | pub mod menu; 72 | pub mod style; 73 | pub mod theme; 74 | pub mod traits; 75 | pub mod vec; 76 | 77 | #[cfg(feature = "builder")] 78 | pub use inventory::submit; 79 | 80 | #[macro_use] 81 | pub mod builder; 82 | 83 | pub mod buffer; 84 | mod cursive_root; 85 | mod cursive_run; 86 | mod dump; 87 | mod printer; 88 | mod rect; 89 | mod with; 90 | mod xy; 91 | 92 | mod div; 93 | 94 | pub use self::cursive_root::{CbSink, Cursive, ScreenId}; 95 | pub use self::cursive_run::CursiveRunner; 96 | pub use self::dump::Dump; 97 | pub use self::printer::Printer; 98 | pub use self::rect::Rect; 99 | pub use self::vec::Vec2; 100 | pub use self::view::View; 101 | pub use self::with::With; 102 | pub use self::xy::XY; 103 | -------------------------------------------------------------------------------- /cursive-core/src/style/border_style.rs: -------------------------------------------------------------------------------- 1 | use enum_map::Enum; 2 | 3 | use std::ops::Deref; 4 | 5 | /// Specifies how some borders should be drawn. 6 | /// 7 | /// Borders are used around Dialogs, select popups, and panels. 8 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Enum)] 9 | pub enum BorderStyle { 10 | /// Simple borders. 11 | Simple, 12 | /// Outset borders with a simple 3d effect. 13 | Outset, 14 | /// No borders. 15 | None, 16 | } 17 | 18 | impl BorderStyle { 19 | /// Returns an iterator on all possible border styles. 20 | pub fn all() -> impl Iterator { 21 | (0..Self::LENGTH).map(Self::from_usize) 22 | } 23 | } 24 | 25 | impl> From for BorderStyle { 26 | fn from(s: S) -> Self { 27 | if &*s == "simple" { 28 | BorderStyle::Simple 29 | } else if &*s == "outset" { 30 | BorderStyle::Outset 31 | } else { 32 | BorderStyle::None 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /cursive-core/src/style/color_pair.rs: -------------------------------------------------------------------------------- 1 | use super::Color; 2 | 3 | /// Combines a front and back color. 4 | #[derive(Clone, Copy, Default, Debug, PartialEq, Eq, Hash)] 5 | pub struct ColorPair { 6 | /// Color used for the foreground. 7 | pub front: Color, 8 | 9 | /// Color used for the background. 10 | pub back: Color, 11 | } 12 | 13 | impl ColorPair { 14 | /// Return an inverted color pair. 15 | /// 16 | /// With swapped front and back color. 17 | #[must_use] 18 | pub const fn invert(self) -> Self { 19 | ColorPair { 20 | front: self.back, 21 | back: self.front, 22 | } 23 | } 24 | 25 | /// Return a color with `TerminalDefault` as front and back. 26 | pub const fn terminal_default() -> Self { 27 | Self { 28 | front: Color::TerminalDefault, 29 | back: Color::TerminalDefault, 30 | } 31 | } 32 | 33 | /// Creates a new color pair from color IDs. 34 | #[must_use] 35 | pub const fn from_256colors(front: u8, back: u8) -> Self { 36 | Self { 37 | front: Color::from_256colors(front), 38 | back: Color::from_256colors(back), 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /cursive-core/src/traits.rs: -------------------------------------------------------------------------------- 1 | //! Commonly used traits bundled for easy import. 2 | //! 3 | //! This module provides an easy way to import some traits. 4 | //! 5 | //! # Examples 6 | //! 7 | //! ``` 8 | //! use cursive_core::traits::*; 9 | //! ``` 10 | 11 | #[doc(no_inline)] 12 | pub use crate::view::{Finder, Nameable, Resizable, Scrollable, View}; 13 | 14 | #[doc(no_inline)] 15 | pub use crate::With; 16 | -------------------------------------------------------------------------------- /cursive-core/src/utils/counter.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::{AtomicUsize, Ordering}; 2 | use std::sync::Arc; 3 | 4 | /// Atomic counter used by [`ProgressBar`]. 5 | /// 6 | /// [`ProgressBar`]: crate::views::ProgressBar 7 | #[derive(Clone, Debug)] 8 | pub struct Counter(pub Arc); 9 | 10 | impl Counter { 11 | /// Creates a new `Counter` starting with the given value. 12 | pub fn new(value: usize) -> Self { 13 | Counter(Arc::new(AtomicUsize::new(value))) 14 | } 15 | 16 | /// Retrieves the current progress value. 17 | pub fn get(&self) -> usize { 18 | self.0.load(Ordering::Relaxed) 19 | } 20 | 21 | /// Sets the current progress value. 22 | pub fn set(&self, value: usize) { 23 | self.0.store(value, Ordering::Relaxed); 24 | } 25 | 26 | /// Increase the current progress by `ticks`. 27 | pub fn tick(&self, ticks: usize) { 28 | self.0.fetch_add(ticks, Ordering::Relaxed); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /cursive-core/src/utils/lines/mod.rs: -------------------------------------------------------------------------------- 1 | //! Compute rows of text with a width constraint. 2 | 3 | pub mod simple; 4 | pub mod spans; 5 | -------------------------------------------------------------------------------- /cursive-core/src/utils/lines/simple/lines_iterator.rs: -------------------------------------------------------------------------------- 1 | use super::Row; 2 | use crate::utils::lines::spans; 3 | use crate::utils::span::{IndexedSpan, SpannedText}; 4 | 5 | /// Generates rows of text in constrained width. 6 | /// 7 | /// Given a long text and a width constraint, it iterates over 8 | /// substrings of the text, each within the constraint. 9 | pub struct LinesIterator<'a> { 10 | iter: spans::LinesIterator>, 11 | 12 | /// Available width. Don't output lines wider than that. 13 | width: usize, 14 | } 15 | 16 | struct DummySpannedText<'a> { 17 | content: &'a str, 18 | attrs: Vec>, 19 | } 20 | 21 | impl<'a> DummySpannedText<'a> { 22 | fn new(content: &'a str) -> Self { 23 | let attrs = vec![IndexedSpan::simple_borrowed(content, ())]; 24 | DummySpannedText { content, attrs } 25 | } 26 | } 27 | 28 | impl SpannedText for DummySpannedText<'_> { 29 | type S = IndexedSpan<()>; 30 | 31 | fn source(&self) -> &str { 32 | self.content 33 | } 34 | 35 | fn spans(&self) -> &[IndexedSpan<()>] { 36 | &self.attrs 37 | } 38 | } 39 | 40 | impl<'a> LinesIterator<'a> { 41 | /// Returns a new `LinesIterator` on `content`. 42 | /// 43 | /// Yields rows of `width` cells or less. 44 | pub fn new(content: &'a str, width: usize) -> Self { 45 | let iter = spans::LinesIterator::new(DummySpannedText::new(content), width); 46 | LinesIterator { iter, width } 47 | } 48 | 49 | /// Leave a blank cell at the end of lines. 50 | /// 51 | /// Unless a word had to be truncated, in which case 52 | /// it takes the entire width. 53 | #[must_use] 54 | pub fn show_spaces(self) -> Self { 55 | let iter = self.iter.show_spaces(); 56 | let width = self.width; 57 | LinesIterator { iter, width } 58 | } 59 | } 60 | 61 | impl Iterator for LinesIterator<'_> { 62 | type Item = Row; 63 | 64 | fn next(&mut self) -> Option { 65 | let row = self.iter.next()?; 66 | 67 | let start = row.segments.first()?.start; 68 | let end = row.segments.last()?.end; 69 | 70 | let spans::Row { 71 | width, is_wrapped, .. 72 | } = row; 73 | 74 | Some(Row { 75 | start, 76 | end, 77 | width, 78 | is_wrapped, 79 | }) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /cursive-core/src/utils/lines/simple/row.rs: -------------------------------------------------------------------------------- 1 | use crate::With; 2 | 3 | /// Represents a row of text within a `String`. 4 | /// 5 | /// A row is made of offsets into a parent `String`. 6 | /// The corresponding substring should take `width` cells when printed. 7 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 8 | pub struct Row { 9 | /// Beginning of the row in the parent `String`. 10 | pub start: usize, 11 | /// End of the row (excluded) 12 | pub end: usize, 13 | 14 | /// Width of the row, in cells. 15 | pub width: usize, 16 | 17 | /// Whether or not this text was wrapped onto the next line 18 | pub is_wrapped: bool, 19 | } 20 | 21 | impl Row { 22 | /// Shift a row start and end by `offset`. 23 | pub fn shift(&mut self, offset: usize) { 24 | self.start += offset; 25 | self.end += offset; 26 | } 27 | 28 | /// Shift a row start and end by `offset`. 29 | /// 30 | /// Chainable variant; 31 | #[must_use] 32 | pub fn shifted(self, offset: usize) -> Self { 33 | self.with(|s| s.shift(offset)) 34 | } 35 | 36 | /// Shift back a row start and end by `offset`. 37 | pub fn rev_shift(&mut self, offset: usize) { 38 | self.start -= offset; 39 | self.end -= offset; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /cursive-core/src/utils/lines/simple/tests.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn test_prefix() { 3 | use super::prefix; 4 | 5 | assert_eq!(prefix(" abra ".split(' '), 5, " ").length, 5); 6 | assert_eq!(prefix("abra a".split(' '), 5, " ").length, 4); 7 | assert_eq!(prefix("a a br".split(' '), 5, " ").length, 3); 8 | } 9 | 10 | #[test] 11 | fn test_lines() { 12 | use super::make_lines; 13 | 14 | let content = "This is a line.\n\nThis is a second line."; 15 | let rows = make_lines(content, 30); 16 | 17 | assert_eq!(rows.len(), 3); 18 | } 19 | -------------------------------------------------------------------------------- /cursive-core/src/utils/lines/spans/chunk.rs: -------------------------------------------------------------------------------- 1 | use super::segment::Segment; 2 | 3 | /// Non-splittable piece of text. 4 | /// 5 | /// It is made of a list of segments of text. 6 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 7 | pub struct Chunk { 8 | /// Total width of this chunk. 9 | pub width: usize, 10 | 11 | /// This is the segments this chunk contains. 12 | pub segments: Vec, 13 | 14 | /// Hard stops are non-optional line breaks (newlines). 15 | pub hard_stop: bool, 16 | 17 | /// If a chunk of text ends in a space, it can be compressed a bit. 18 | /// 19 | /// (We can omit the space if it would result in a perfect fit.) 20 | /// 21 | /// This only matches literally the ' ' byte. 22 | pub ends_with_space: bool, 23 | } 24 | 25 | impl Chunk { 26 | /// Remove some text from the front. 27 | /// 28 | /// We're given the length (number of bytes) and the width. 29 | pub fn remove_front(&mut self, mut to_remove: ChunkPart) { 30 | // Remove something from each segment until we've removed enough. 31 | for segment in &mut self.segments { 32 | if to_remove.length <= segment.end - segment.start { 33 | // This segment is bigger than what we need to remove 34 | // So just trim the prefix and stop there. 35 | segment.start += to_remove.length; 36 | segment.width -= to_remove.width; 37 | self.width -= to_remove.width; 38 | break; 39 | } else { 40 | // This segment is too small, so it'll disappear entirely. 41 | to_remove.length -= segment.end - segment.start; 42 | to_remove.width -= segment.width; 43 | self.width -= segment.width; 44 | 45 | // Empty this segment 46 | segment.start = segment.end; 47 | segment.width = 0; 48 | } 49 | } 50 | } 51 | 52 | /// Remove the last character from this chunk. 53 | /// 54 | /// Usually done to remove a trailing space/newline. 55 | pub fn remove_last_char(&mut self) { 56 | // We remove the last char in 2 situations: 57 | // * Trailing space. 58 | // * Trailing newline. 59 | // Only in the first case does this affect width. 60 | // (Because newlines have 0 width) 61 | 62 | if self.ends_with_space { 63 | // Only reduce the width if the last char was a space. 64 | // Otherwise it's a newline, and we don't want to reduce 65 | // that. 66 | self.width -= 1; 67 | } 68 | 69 | // Is the last segment empty after trimming it? 70 | // If yes, just drop it. 71 | let last_empty = { 72 | let last = self.segments.last_mut().unwrap(); 73 | if self.ends_with_space { 74 | last.end -= 1; 75 | last.width -= 1; 76 | } 77 | last.start == last.end 78 | }; 79 | 80 | if last_empty { 81 | // self.segments.pop().unwrap(); 82 | } 83 | } 84 | } 85 | 86 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)] 87 | /// Describes a part of a chunk. 88 | /// 89 | /// Includes both length and width to ease some computations. 90 | /// 91 | /// This is used to represent how much of a chunk we've already processed. 92 | pub struct ChunkPart { 93 | pub width: usize, 94 | pub length: usize, 95 | } 96 | -------------------------------------------------------------------------------- /cursive-core/src/utils/lines/spans/mod.rs: -------------------------------------------------------------------------------- 1 | //! Compute lines on multiple spans of text. 2 | //! 3 | //! The input is a list of consecutive text spans. 4 | //! 5 | //! Computed rows will include a list of span segments. 6 | //! Each segment include the source span ID, and start/end byte offsets. 7 | mod chunk; 8 | mod chunk_iterator; 9 | mod lines_iterator; 10 | mod prefix; 11 | mod row; 12 | mod segment; 13 | mod segment_merge_iterator; 14 | 15 | #[cfg(test)] 16 | mod tests; 17 | 18 | pub use self::lines_iterator::LinesIterator; 19 | pub use self::row::Row; 20 | pub use self::segment::Segment; 21 | -------------------------------------------------------------------------------- /cursive-core/src/utils/lines/spans/prefix.rs: -------------------------------------------------------------------------------- 1 | use super::chunk::{Chunk, ChunkPart}; 2 | use std::iter::Peekable; 3 | 4 | /// Concatenates chunks as long as they fit in the given width. 5 | pub fn prefix(tokens: &mut Peekable, width: usize, offset: &mut ChunkPart) -> Vec 6 | where 7 | I: Iterator, 8 | { 9 | let mut available = width; 10 | let mut chunks = Vec::new(); 11 | 12 | // Accumulate chunks until it doesn't fit. 13 | loop { 14 | // Look at the next chunk and see if it would fit. 15 | let result = { 16 | let next_chunk = match tokens.peek() { 17 | None => break, 18 | Some(chunk) => chunk, 19 | }; 20 | 21 | // When considering if the chunk fits, remember that we may 22 | // already have processed part of it. 23 | // So (chunk - width) fits available 24 | // if chunks fits (available + width) 25 | consider_chunk(available + offset.width, next_chunk) 26 | }; 27 | 28 | match result { 29 | ChunkFitResult::Fits => { 30 | // It fits! Add it and move to the next one. 31 | let mut chunk = tokens.next().unwrap(); 32 | // Remember to strip the prefix, in case we took some earlier. 33 | chunk.remove_front(*offset); 34 | // And reset out offset. 35 | offset.length = 0; 36 | offset.width = 0; 37 | 38 | available -= chunk.width; 39 | chunks.push(chunk); 40 | continue; 41 | } 42 | ChunkFitResult::FitsBarely => { 43 | // That's it, it's the last one and we're off. 44 | let mut chunk = tokens.next().unwrap(); 45 | chunk.remove_front(*offset); 46 | offset.length = 0; 47 | offset.width = 0; 48 | 49 | // We know we need to remove the last character. 50 | // Because it's either: 51 | // * A hard stop: there is a newline 52 | // * A compressed chunk: it ends with a space 53 | chunk.remove_last_char(); 54 | chunks.push(chunk); 55 | // No need to update `available`, 56 | // as we're ending the line anyway. 57 | break; 58 | } 59 | ChunkFitResult::DoesNotFit => { 60 | break; 61 | } 62 | } 63 | } 64 | 65 | chunks 66 | } 67 | 68 | /// Result of a fitness test 69 | /// 70 | /// Describes how well a chunk fits in the available space. 71 | enum ChunkFitResult { 72 | /// This chunk can fit as-is 73 | Fits, 74 | 75 | /// This chunk fits, but it'll be the last one. 76 | /// Additionally, its last char may need to be removed. 77 | FitsBarely, 78 | 79 | /// This chunk doesn't fit. Don't even. 80 | DoesNotFit, 81 | } 82 | 83 | /// Look at a chunk, and decide how it could fit. 84 | fn consider_chunk(available: usize, chunk: &Chunk) -> ChunkFitResult { 85 | if chunk.width <= available { 86 | // We fits. No question about it. 87 | if chunk.hard_stop { 88 | // Still, we have to stop here. 89 | // And possibly trim a newline. 90 | ChunkFitResult::FitsBarely 91 | } else { 92 | // Nothing special here. 93 | ChunkFitResult::Fits 94 | } 95 | } else if chunk.width == available + 1 { 96 | // We're just SLIGHTLY too big! 97 | // Can we just pop something? 98 | if chunk.ends_with_space { 99 | // Yay! 100 | ChunkFitResult::FitsBarely 101 | } else { 102 | // Noo( 103 | ChunkFitResult::DoesNotFit 104 | } 105 | } else { 106 | // Can't bargain with me. 107 | ChunkFitResult::DoesNotFit 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /cursive-core/src/utils/lines/spans/row.rs: -------------------------------------------------------------------------------- 1 | use super::Segment; 2 | use crate::utils::span::{IndexedCow, Span, SpannedStr}; 3 | 4 | /// A list of segments representing a row of text 5 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 6 | pub struct Row { 7 | /// List of segments 8 | pub segments: Vec, 9 | /// Total width for this row 10 | pub width: usize, 11 | /// Whether or not this text was wrapped onto the next line 12 | pub is_wrapped: bool, 13 | } 14 | 15 | impl Row { 16 | /// Resolve the row indices into string slices and attributes. 17 | pub fn resolve<'a, T, S>(&self, source: S) -> Vec> 18 | where 19 | S: Into>, 20 | { 21 | let source = source.into(); 22 | 23 | self.segments 24 | .iter() 25 | .map(|seg| seg.resolve(&source)) 26 | .filter(|span| !span.content.is_empty()) 27 | .collect() 28 | } 29 | 30 | /// Resolve the row indices into string slices and attributes. 31 | pub fn resolve_stream<'a, 'b, T, S>(&'b self, source: S) -> impl Iterator> 32 | where 33 | S: Into>, 34 | T: 'a, 35 | 'b: 'a, 36 | { 37 | let source = source.into(); 38 | 39 | self.segments 40 | .iter() 41 | .map(move |seg| seg.resolve(&source)) 42 | .filter(|span| !span.content.is_empty()) 43 | } 44 | 45 | /// Returns indices in the source string, if possible. 46 | /// 47 | /// Returns overall `(start, end)`, or `None` if the segments are owned. 48 | pub fn overall_indices(&self, spans: &[S]) -> Option<(usize, usize)> 49 | where 50 | S: AsRef, 51 | { 52 | let (start, _) = self.segments.first()?.source_indices(spans)?; 53 | let (_, end) = self.segments.last()?.source_indices(spans)?; 54 | 55 | Some((start, end)) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /cursive-core/src/utils/lines/spans/segment.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::span::{IndexedCow, Span, SpannedStr, SpannedText}; 2 | 3 | /// Refers to a part of a span 4 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 5 | pub struct Segment { 6 | /// ID of the span this segment refers to 7 | pub span_id: usize, 8 | 9 | /// Beginning of this segment within the span (included) 10 | pub start: usize, 11 | /// End of this segment within the span (excluded) 12 | pub end: usize, 13 | 14 | /// Width of this segment 15 | pub width: usize, 16 | } 17 | 18 | impl Segment { 19 | /// Resolve this segment to a string slice and an attribute. 20 | pub fn resolve<'a, T>(&self, source: &SpannedStr<'a, T>) -> Span<'a, T> { 21 | let span = &source.spans_raw()[self.span_id]; 22 | 23 | let content = span.content.resolve(source.source()); 24 | let content = &content[self.start..self.end]; 25 | 26 | Span { 27 | content, 28 | attr: &span.attr, 29 | width: self.width, 30 | } 31 | } 32 | 33 | /// Resolves this segment to plain text. 34 | pub fn resolve_plain<'a, S>(&self, source: &'a S) -> &'a str 35 | where 36 | S: SpannedText, 37 | { 38 | let span = &source.spans()[self.span_id]; 39 | 40 | let content = span.as_ref().resolve(source.source()); 41 | 42 | &content[self.start..self.end] 43 | } 44 | 45 | /// Returns indices in the source string, if possible. 46 | /// 47 | /// Returns `(start, end)`, or `None` if the target span is an owned string. 48 | pub fn source_indices(&self, spans: &[S]) -> Option<(usize, usize)> 49 | where 50 | S: AsRef, 51 | { 52 | let span = spans[self.span_id].as_ref(); 53 | 54 | if let IndexedCow::Borrowed { start, .. } = *span { 55 | Some((self.start + start, self.end + start)) 56 | } else { 57 | None 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /cursive-core/src/utils/lines/spans/segment_merge_iterator.rs: -------------------------------------------------------------------------------- 1 | use super::Segment; 2 | 3 | pub struct SegmentMergeIterator { 4 | current: Option, 5 | inner: I, 6 | } 7 | 8 | impl SegmentMergeIterator { 9 | pub fn new(inner: I) -> Self { 10 | SegmentMergeIterator { 11 | inner, 12 | current: None, 13 | } 14 | } 15 | } 16 | 17 | impl Iterator for SegmentMergeIterator 18 | where 19 | I: Iterator, 20 | { 21 | type Item = Segment; 22 | 23 | fn next(&mut self) -> Option { 24 | // Always work on an segment. 25 | if self.current.is_none() { 26 | self.current = self.inner.next(); 27 | self.current?; 28 | } 29 | 30 | // Keep growing our current segment until we find something else. 31 | loop { 32 | match self.inner.next() { 33 | None => return self.current.take(), 34 | Some(next) => { 35 | if next.span_id == self.current.unwrap().span_id { 36 | let current = self.current.as_mut().unwrap(); 37 | current.end = next.end; 38 | current.width += next.width; 39 | } else { 40 | let current = self.current.take(); 41 | self.current = Some(next); 42 | return current; 43 | } 44 | } 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /cursive-core/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | //! Toolbox to make text layout easier. 2 | 3 | mod counter; 4 | #[macro_use] 5 | mod immutify; 6 | pub mod lines; 7 | pub mod markup; 8 | mod reader; 9 | pub mod span; 10 | 11 | pub use self::counter::Counter; 12 | pub use self::reader::ProgressReader; 13 | -------------------------------------------------------------------------------- /cursive-core/src/utils/reader.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::Counter; 2 | use std::io::{self, Read}; 3 | 4 | /// Wrapper around a `Read` that reports the progress made. 5 | /// 6 | /// Used to monitor a file downloading or other slow IO task 7 | /// in a progress bar. 8 | /// 9 | /// # Examples 10 | /// 11 | /// ```rust,no_run 12 | /// use cursive_core::utils::{Counter, ProgressReader}; 13 | /// use std::io::Read; 14 | /// 15 | /// // Read a file and report the progress 16 | /// let file = std::fs::File::open("large_file").unwrap(); 17 | /// let counter = Counter::new(0); 18 | /// let mut reader = ProgressReader::new(counter.clone(), file); 19 | /// 20 | /// std::thread::spawn(move || { 21 | /// // Left as an exercise: use an AtomicBool for a stop condition! 22 | /// loop { 23 | /// let progress = counter.get(); 24 | /// println!("Read {} bytes so far", progress); 25 | /// } 26 | /// }); 27 | /// 28 | /// // As we read data, the counter will be updated and the control thread 29 | /// // will monitor the progress. 30 | /// let mut buffer = Vec::new(); 31 | /// reader.read_to_end(&mut buffer).unwrap(); 32 | /// ``` 33 | #[derive(Clone, Debug)] 34 | pub struct ProgressReader { 35 | reader: R, 36 | counter: Counter, 37 | } 38 | 39 | impl ProgressReader { 40 | /// Creates a new `ProgressReader` around `reader`. 41 | /// 42 | /// `counter` will be updated with the number of bytes read. 43 | /// 44 | /// You should make sure the progress bar knows how 45 | /// many bytes should be received. 46 | pub fn new(counter: Counter, reader: R) -> Self { 47 | ProgressReader { reader, counter } 48 | } 49 | 50 | /// Unwraps this `ProgressReader`, returning the reader and counter. 51 | pub fn deconstruct(self) -> (R, Counter) { 52 | (self.reader, self.counter) 53 | } 54 | } 55 | 56 | impl Read for ProgressReader { 57 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 58 | let result = self.reader.read(buf)?; 59 | self.counter.tick(result); 60 | Ok(result) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /cursive-core/src/view/any.rs: -------------------------------------------------------------------------------- 1 | use crate::view::View; 2 | use std::any::Any; 3 | 4 | /// A view that can be downcasted to its concrete type. 5 | /// 6 | /// This trait is automatically implemented for any `T: View`. 7 | pub trait AnyView { 8 | /// Downcast self to a `Any`. 9 | fn as_any(&self) -> &dyn Any; 10 | 11 | /// Downcast self to a mutable `Any`. 12 | fn as_any_mut(&mut self) -> &mut dyn Any; 13 | 14 | /// Returns a boxed any from a boxed self. 15 | /// 16 | /// Can be used before `Box::downcast()`. 17 | /// 18 | /// # Examples 19 | /// 20 | /// ```rust 21 | /// # use cursive_core::views::TextView; 22 | /// # use cursive_core::view::View; 23 | /// let boxed: Box = Box::new(TextView::new("text")); 24 | /// let text: Box = boxed.as_boxed_any().downcast().unwrap(); 25 | /// ``` 26 | fn as_boxed_any(self: Box) -> Box; 27 | } 28 | 29 | impl AnyView for T { 30 | /// Downcast self to a `Any`. 31 | fn as_any(&self) -> &dyn Any { 32 | self 33 | } 34 | 35 | /// Downcast self to a mutable `Any`. 36 | fn as_any_mut(&mut self) -> &mut dyn Any { 37 | self 38 | } 39 | 40 | fn as_boxed_any(self: Box) -> Box { 41 | self 42 | } 43 | } 44 | 45 | impl dyn AnyView { 46 | /// Attempts to downcast `self` to a concrete type. 47 | pub fn downcast_ref(&self) -> Option<&T> { 48 | self.as_any().downcast_ref() 49 | } 50 | 51 | /// Attempts to downcast `self` to a concrete type. 52 | pub fn downcast_mut(&mut self) -> Option<&mut T> { 53 | self.as_any_mut().downcast_mut() 54 | } 55 | 56 | /// Attempts to downcast `Box` to a concrete type. 57 | pub fn downcast(self: Box) -> Result, Box> { 58 | // Do the check here + unwrap, so the error 59 | // value is `Self` and not `dyn Any`. 60 | if self.as_any().is::() { 61 | Ok(self.as_boxed_any().downcast().unwrap()) 62 | } else { 63 | Err(self) 64 | } 65 | } 66 | 67 | /// Checks if this view is of type `T`. 68 | pub fn is(&mut self) -> bool { 69 | self.as_any().is::() 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /cursive-core/src/view/finder.rs: -------------------------------------------------------------------------------- 1 | use crate::view::{View, ViewWrapper}; 2 | use crate::views::{BoxedView, NamedView, ViewRef}; 3 | 4 | /// Provides `call_on` to views. 5 | /// 6 | /// This trait is mostly a wrapper around [`View::call_on_any`]. 7 | /// 8 | /// It provides a nicer interface to find a view when you know its type. 9 | pub trait Finder { 10 | /// Runs a callback on the view identified by `sel`. 11 | /// 12 | /// If the view is found, return the result of `callback`. 13 | /// 14 | /// If the view is not found, or if it is not of the asked type, 15 | /// it returns `None`. 16 | fn call_on(&mut self, sel: &Selector, callback: F) -> Option 17 | where 18 | V: View, 19 | F: FnOnce(&mut V) -> R, 20 | { 21 | let mut callback = Some(callback); 22 | let mut result = None; 23 | self.call_on_all(sel, |v: &mut V| { 24 | if let Some(callback) = callback.take() { 25 | result = Some(callback(v)); 26 | } 27 | }); 28 | result 29 | } 30 | 31 | /// Runs a callback on all views identified by `sel`. 32 | /// 33 | /// Useful if you have multiple views of the same type with the same name. 34 | fn call_on_all(&mut self, sel: &Selector, callback: F) 35 | where 36 | V: View, 37 | F: FnMut(&mut V); 38 | 39 | /// Convenient method to use `call_on` with a `view::Selector::Name`. 40 | fn call_on_name(&mut self, name: &str, callback: F) -> Option 41 | where 42 | V: View, 43 | F: FnOnce(&mut V) -> R, 44 | { 45 | self.call_on(&Selector::Name(name), callback) 46 | } 47 | 48 | /// Convenient method to find a view wrapped in an [`NamedView`]. 49 | fn find_name(&mut self, name: &str) -> Option> 50 | where 51 | V: View, 52 | { 53 | self.call_on_name(name, NamedView::::get_mut) 54 | } 55 | } 56 | 57 | impl Finder for T { 58 | fn call_on_all(&mut self, sel: &Selector, mut callback: F) 59 | where 60 | V: View, 61 | F: FnMut(&mut V), 62 | { 63 | self.call_on_any(sel, &mut |v: &mut dyn View| { 64 | if let Some(v) = v.downcast_mut::() { 65 | // Allow to select the view directly. 66 | callback(v); 67 | } else if let Some(v) = v.downcast_mut::>() { 68 | // In most cases we will actually find the wrapper. 69 | v.with_view_mut(&mut callback); 70 | } else if let Some(v) = v.downcast_mut::>() { 71 | v.with_view_mut(|v: &mut BoxedView| { 72 | if let Some(v) = v.get_mut() { 73 | callback(v); 74 | } 75 | }); 76 | } 77 | }); 78 | } 79 | } 80 | 81 | /// Selects a single view (if any) in the tree. 82 | #[non_exhaustive] 83 | pub enum Selector<'a> { 84 | /// Selects a view from its name. 85 | Name(&'a str), 86 | } 87 | -------------------------------------------------------------------------------- /cursive-core/src/view/into_boxed_view.rs: -------------------------------------------------------------------------------- 1 | use crate::view::View; 2 | 3 | /// Represents a type that can be made into a `Box`. 4 | pub trait IntoBoxedView { 5 | /// Returns a `Box`. 6 | fn into_boxed_view(self) -> Box; 7 | } 8 | 9 | impl IntoBoxedView for T 10 | where 11 | T: View, 12 | { 13 | fn into_boxed_view(self) -> Box { 14 | Box::new(self) 15 | } 16 | } 17 | 18 | impl IntoBoxedView for Box { 19 | fn into_boxed_view(self) -> Box { 20 | self 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /cursive-core/src/view/margins.rs: -------------------------------------------------------------------------------- 1 | use crate::Vec2; 2 | use std::ops::{Add, Div, Mul, Sub}; 3 | 4 | /// Four values representing each direction. 5 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] 6 | pub struct Margins { 7 | /// Left margin 8 | pub left: usize, 9 | /// Right margin 10 | pub right: usize, 11 | /// Top margin 12 | pub top: usize, 13 | /// Bottom margin 14 | pub bottom: usize, 15 | } 16 | 17 | impl Margins { 18 | /// Creates a new `Margins` object with zero margins. 19 | pub fn zeroes() -> Self { 20 | Self::lrtb(0, 0, 0, 0) 21 | } 22 | 23 | /// Creates a new `Margins` object from the Left, Right, Top, Bottom fields. 24 | pub fn lrtb(left: usize, right: usize, top: usize, bottom: usize) -> Self { 25 | Margins { 26 | left, 27 | right, 28 | top, 29 | bottom, 30 | } 31 | } 32 | 33 | /// Creates a new `Margins` object from the Left, Top, Right, Bottom fields. 34 | pub fn ltrb(left_top: Vec2, right_bottom: Vec2) -> Self { 35 | Self::lrtb(left_top.x, right_bottom.x, left_top.y, right_bottom.y) 36 | } 37 | 38 | /// Creates a new `Margins` object from the Top, Right, Bottom, Left fields. 39 | pub fn trbl(top: usize, right: usize, bottom: usize, left: usize) -> Self { 40 | Self::lrtb(left, right, top, bottom) 41 | } 42 | 43 | /// Creates a new `Margins` object from the Left and Right fields. 44 | /// 45 | /// Top and Bottom will be 0. 46 | pub fn lr(left: usize, right: usize) -> Self { 47 | Self::lrtb(left, right, 0, 0) 48 | } 49 | 50 | /// Creates a new `Margins` object from the Top and Bottom fields. 51 | /// 52 | /// Left and Right will be 0. 53 | pub fn tb(top: usize, bottom: usize) -> Self { 54 | Self::lrtb(0, 0, top, bottom) 55 | } 56 | 57 | /// Returns left + right. 58 | pub fn horizontal(&self) -> usize { 59 | self.left + self.right 60 | } 61 | 62 | /// Returns top + bottom. 63 | pub fn vertical(&self) -> usize { 64 | self.top + self.bottom 65 | } 66 | 67 | /// Returns (left+right, top+bottom). 68 | pub fn combined(&self) -> Vec2 { 69 | Vec2::new(self.horizontal(), self.vertical()) 70 | } 71 | 72 | /// Returns (left, top). 73 | pub fn top_left(&self) -> Vec2 { 74 | Vec2::new(self.left, self.top) 75 | } 76 | 77 | /// Returns (right, bottom). 78 | pub fn bot_right(&self) -> Vec2 { 79 | Vec2::new(self.right, self.bottom) 80 | } 81 | } 82 | 83 | impl Add for Margins { 84 | type Output = Margins; 85 | 86 | fn add(self, other: Margins) -> Margins { 87 | Margins { 88 | left: self.left + other.left, 89 | right: self.right + other.right, 90 | top: self.top + other.top, 91 | bottom: self.bottom + other.bottom, 92 | } 93 | } 94 | } 95 | 96 | impl Sub for Margins { 97 | type Output = Margins; 98 | 99 | fn sub(self, other: Margins) -> Margins { 100 | Margins { 101 | left: self.left - other.left, 102 | right: self.right - other.right, 103 | top: self.top - other.top, 104 | bottom: self.bottom - other.bottom, 105 | } 106 | } 107 | } 108 | 109 | impl Div for Margins { 110 | type Output = Margins; 111 | 112 | fn div(self, other: usize) -> Margins { 113 | Margins { 114 | left: self.left / other, 115 | right: self.right / other, 116 | top: self.top / other, 117 | bottom: self.bottom / other, 118 | } 119 | } 120 | } 121 | 122 | impl Mul for Margins { 123 | type Output = Margins; 124 | 125 | fn mul(self, other: usize) -> Margins { 126 | Margins { 127 | left: self.left * other, 128 | right: self.right * other, 129 | top: self.top * other, 130 | bottom: self.bottom * other, 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /cursive-core/src/view/nameable.rs: -------------------------------------------------------------------------------- 1 | use crate::view::View; 2 | use crate::views::NamedView; 3 | 4 | /// Makes a view wrappable in an [`NamedView`]. 5 | /// 6 | /// [`NamedView`]: ../views/struct.NamedView.html 7 | pub trait Nameable: View + Sized { 8 | /// Wraps this view into an `NamedView` with the given id. 9 | /// 10 | /// This is just a shortcut for `NamedView::new(id, self)` 11 | /// 12 | /// You can use the given id to find the view in the layout tree. 13 | /// 14 | /// # Examples 15 | /// 16 | /// ```rust 17 | /// # use cursive_core::Cursive; 18 | /// # use cursive_core::views::TextView; 19 | /// # use cursive_core::view::Resizable; 20 | /// use cursive_core::view::Nameable; 21 | /// 22 | /// let mut siv = Cursive::new(); 23 | /// siv.add_layer(TextView::new("foo").with_name("text").fixed_width(10)); 24 | /// 25 | /// // You could call this from an event callback 26 | /// siv.call_on_name("text", |view: &mut TextView| { 27 | /// view.set_content("New content!"); 28 | /// }); 29 | /// ``` 30 | /// 31 | /// # Notes 32 | /// 33 | /// You should call this directly on the view you want to retrieve later, 34 | /// before other wrappers like [`fixed_width`]. Otherwise, you would be 35 | /// retrieving a [`ResizedView`]! 36 | /// 37 | /// [`fixed_width`]: crate::view::Resizable::fixed_width 38 | /// [`ResizedView`]: crate::views::ResizedView 39 | fn with_name>(self, name: S) -> NamedView { 40 | NamedView::new(name, self) 41 | } 42 | } 43 | 44 | /// Any `View` implements this trait. 45 | impl Nameable for T {} 46 | -------------------------------------------------------------------------------- /cursive-core/src/view/position.rs: -------------------------------------------------------------------------------- 1 | use crate::Vec2; 2 | use crate::XY; 3 | use std::cmp::min; 4 | 5 | /// Location of the view on screen 6 | pub type Position = XY; 7 | 8 | impl Position { 9 | /// Returns a position centered on both axis. 10 | pub fn center() -> Self { 11 | Position::new(Offset::Center, Offset::Center) 12 | } 13 | 14 | /// Returns a position absolute on both axis. 15 | pub fn absolute>(offset: T) -> Self { 16 | let offset = offset.into(); 17 | Position::new(Offset::Absolute(offset.x), Offset::Absolute(offset.y)) 18 | } 19 | 20 | /// Returns a position relative to the parent on both axis. 21 | pub fn parent>>(offset: T) -> Self { 22 | let offset = offset.into(); 23 | Position::new(Offset::Parent(offset.x), Offset::Parent(offset.y)) 24 | } 25 | 26 | /// Computes the offset required to draw a view. 27 | /// 28 | /// When drawing a view with `size` in a container with `available`, 29 | /// and a parent with the absolute coordinates `parent`, drawing the 30 | /// child with its top-left corner at the returned coordinates will 31 | /// position him appropriately. 32 | pub fn compute_offset(&self, size: S, available: A, parent: P) -> Vec2 33 | where 34 | S: Into, 35 | A: Into, 36 | P: Into, 37 | { 38 | let available = available.into(); 39 | let size = size.into(); 40 | let parent = parent.into(); 41 | 42 | Vec2::new( 43 | self.x.compute_offset(size.x, available.x, parent.x), 44 | self.y.compute_offset(size.y, available.y, parent.y), 45 | ) 46 | } 47 | } 48 | 49 | /// Single-dimensional offset policy. 50 | #[derive(PartialEq, Eq, Debug, Clone, Copy, Hash)] 51 | pub enum Offset { 52 | /// In the center of the screen 53 | Center, 54 | /// Place top-left corner at the given absolute coordinates 55 | Absolute(usize), 56 | 57 | /// Offset from the previous layer's top-left corner. 58 | /// 59 | /// If this is the first layer, behaves like `Absolute`. 60 | Parent(isize), 61 | } 62 | 63 | impl Offset { 64 | /// Computes a single-dimension offset required to draw a view. 65 | pub fn compute_offset(&self, size: usize, available: usize, parent: usize) -> usize { 66 | if size > available { 67 | 0 68 | } else { 69 | match *self { 70 | Offset::Center => (available - size) / 2, 71 | Offset::Absolute(offset) => min(offset, available - size), 72 | Offset::Parent(offset) => { 73 | min((parent as isize + offset) as usize, available - size) 74 | } 75 | } 76 | } 77 | } 78 | } 79 | 80 | #[cfg(test)] 81 | mod tests { 82 | 83 | use super::Position; 84 | use crate::Vec2; 85 | 86 | #[test] 87 | fn test_center() { 88 | let c = Position::center(); 89 | assert_eq!(Vec2::new(2, 1), c.compute_offset((1, 1), (5, 3), (0, 0))); 90 | assert_eq!(Vec2::new(2, 0), c.compute_offset((1, 3), (5, 3), (0, 0))); 91 | assert_eq!(Vec2::new(1, 1), c.compute_offset((3, 1), (5, 3), (0, 0))); 92 | assert_eq!(Vec2::new(0, 1), c.compute_offset((5, 1), (5, 3), (0, 0))); 93 | assert_eq!(Vec2::new(0, 0), c.compute_offset((5, 3), (5, 3), (0, 0))); 94 | assert_eq!(Vec2::new(0, 0), c.compute_offset((5, 3), (3, 1), (0, 0))); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /cursive-core/src/view/resizable.rs: -------------------------------------------------------------------------------- 1 | use crate::view::{SizeConstraint, View}; 2 | use crate::views::ResizedView; 3 | use crate::Vec2; 4 | 5 | /// Makes a view wrappable in a [`ResizedView`]. 6 | /// 7 | /// [`ResizedView`]: ../views/struct.ResizedView.html 8 | pub trait Resizable: View + Sized { 9 | /// Wraps `self` in a `ResizedView` with the given size constraints. 10 | fn resized(self, width: SizeConstraint, height: SizeConstraint) -> ResizedView { 11 | ResizedView::new(width, height, self) 12 | } 13 | 14 | /// Wraps `self` into a fixed-size `ResizedView`. 15 | fn fixed_size>(self, size: S) -> ResizedView { 16 | ResizedView::with_fixed_size(size, self) 17 | } 18 | 19 | /// Wraps `self` into a fixed-width `ResizedView`. 20 | fn fixed_width(self, width: usize) -> ResizedView { 21 | ResizedView::with_fixed_width(width, self) 22 | } 23 | 24 | /// Wraps `self` into a fixed-width `ResizedView`. 25 | fn fixed_height(self, height: usize) -> ResizedView { 26 | ResizedView::with_fixed_height(height, self) 27 | } 28 | 29 | /// Wraps `self` into a full-screen `ResizedView`. 30 | fn full_screen(self) -> ResizedView { 31 | ResizedView::with_full_screen(self) 32 | } 33 | 34 | /// Wraps `self` into a full-width `ResizedView`. 35 | fn full_width(self) -> ResizedView { 36 | ResizedView::with_full_width(self) 37 | } 38 | 39 | /// Wraps `self` into a full-height `ResizedView`. 40 | fn full_height(self) -> ResizedView { 41 | ResizedView::with_full_height(self) 42 | } 43 | 44 | /// Wraps `self` into a limited-size `ResizedView`. 45 | fn max_size>(self, size: S) -> ResizedView { 46 | ResizedView::with_max_size(size, self) 47 | } 48 | 49 | /// Wraps `self` into a limited-width `ResizedView`. 50 | fn max_width(self, max_width: usize) -> ResizedView { 51 | ResizedView::with_max_width(max_width, self) 52 | } 53 | 54 | /// Wraps `self` into a limited-height `ResizedView`. 55 | fn max_height(self, max_height: usize) -> ResizedView { 56 | ResizedView::with_max_height(max_height, self) 57 | } 58 | 59 | /// Wraps `self` into a `ResizedView` at least sized `size`. 60 | fn min_size>(self, size: S) -> ResizedView { 61 | ResizedView::with_min_size(size, self) 62 | } 63 | 64 | /// Wraps `self` in a `ResizedView` at least `min_width` wide. 65 | fn min_width(self, min_width: usize) -> ResizedView { 66 | ResizedView::with_min_width(min_width, self) 67 | } 68 | 69 | /// Wraps `self` in a `ResizedView` at least `min_height` tall. 70 | fn min_height(self, min_height: usize) -> ResizedView { 71 | ResizedView::with_min_height(min_height, self) 72 | } 73 | } 74 | 75 | impl Resizable for T {} 76 | -------------------------------------------------------------------------------- /cursive-core/src/view/scrollable.rs: -------------------------------------------------------------------------------- 1 | use crate::view::View; 2 | use crate::views::ScrollView; 3 | 4 | /// Makes a view wrappable in a [`ScrollView`]. 5 | /// 6 | /// [`ScrollView`]: crate::views::ScrollView 7 | pub trait Scrollable: View + Sized { 8 | /// Wraps `self` in a `ScrollView`. 9 | fn scrollable(self) -> ScrollView { 10 | ScrollView::new(self) 11 | } 12 | } 13 | 14 | impl Scrollable for T {} 15 | -------------------------------------------------------------------------------- /cursive-core/src/view/size_cache.rs: -------------------------------------------------------------------------------- 1 | use crate::Vec2; 2 | use crate::XY; 3 | 4 | /// Cache around a one-dimensional layout result. 5 | /// 6 | /// This is not a View, but something to help you if you create your own Views. 7 | #[derive(Eq, PartialEq, Debug, Clone, Copy)] 8 | pub struct SizeCache { 9 | /// Cached value 10 | pub value: usize, 11 | /// `true` if the last size was constrained. 12 | /// 13 | /// If unconstrained, any request larger than this value 14 | /// would return the same size. 15 | pub constrained: bool, 16 | 17 | /// Extra field. 18 | pub extra: T, 19 | } 20 | 21 | impl SizeCache<()> { 22 | /// Creates a new sized cache 23 | pub fn new(value: usize, constrained: bool) -> Self { 24 | SizeCache { 25 | value, 26 | constrained, 27 | extra: (), 28 | } 29 | } 30 | 31 | /// Creates a new bi-dimensional cache. 32 | /// 33 | /// It will stay valid for the same request, and compatible ones. 34 | /// 35 | /// A compatible request is one where, for each axis, either: 36 | /// 37 | /// * the request is equal to the cached size, or 38 | /// * the request is larger than the cached size and the cache is 39 | /// unconstrained 40 | /// 41 | /// Notes: 42 | /// 43 | /// * `size` must fit inside `req`. 44 | /// * for each dimension, `constrained = (size == req)` 45 | pub fn build(size: Vec2, req: Vec2) -> XY { 46 | size.zip_map(req, |size, req| SizeCache::new(size, size >= req)) 47 | } 48 | } 49 | 50 | impl SizeCache { 51 | /// Creates a new sized cache 52 | pub fn new_extra(value: usize, constrained: bool, extra: T) -> Self { 53 | Self { 54 | value, 55 | constrained, 56 | extra, 57 | } 58 | } 59 | 60 | /// Creates a new bi-dimensional cache. 61 | /// 62 | /// Similar to `build()`, but includes the extra field. 63 | pub fn build_extra(size: Vec2, req: Vec2, extra: XY) -> XY { 64 | XY::zip3(size, req, extra) 65 | .map(|(size, req, extra)| SizeCache::new_extra(size, size >= req, extra)) 66 | } 67 | 68 | /// Returns `true` if `self` is still valid for the given `request`. 69 | pub fn accept(self, request: usize) -> bool { 70 | match (request, self.value) { 71 | // Request a smaller size than last time? Hell no! 72 | (r, v) if r < v => false, 73 | // Request exactly what we had last time? Sure! 74 | (r, v) if r == v => true, 75 | // Request more than we had last time? Maybe? 76 | _ => !self.constrained, 77 | } 78 | } 79 | 80 | /// Returns the value in the cache. 81 | pub fn value(self) -> usize { 82 | self.value 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /cursive-core/src/view/size_constraint.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::min; 2 | 3 | /// Single-dimensional constraint on a view size. 4 | /// 5 | /// This describes a possible behaviour for a [`ResizedView`]. 6 | /// 7 | /// [`ResizedView`]: crate::views::ResizedView 8 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 9 | pub enum SizeConstraint { 10 | /// No constraint imposed, the child view's response is used. 11 | Free, 12 | /// Tries to take all available space, no matter what the child needs. 13 | Full, 14 | /// Always return the included size, no matter what the child needs. 15 | Fixed(usize), 16 | /// Returns the minimum of the included value and the child view's size. 17 | AtMost(usize), 18 | /// Returns the maximum of the included value and the child view's size. 19 | AtLeast(usize), 20 | } 21 | 22 | impl SizeConstraint { 23 | /// Returns the size to be given to the child. 24 | /// 25 | /// When `available` is offered to the `ResizedView`. 26 | pub fn available(self, available: usize) -> usize { 27 | match self { 28 | SizeConstraint::Free | SizeConstraint::Full | SizeConstraint::AtLeast(_) => available, 29 | // If the available space is too small, always give in. 30 | SizeConstraint::Fixed(value) | SizeConstraint::AtMost(value) => min(value, available), 31 | } 32 | } 33 | 34 | /// Returns the size the child view should actually use. 35 | /// 36 | /// When it said it wanted `result`. 37 | pub fn result(self, (result, available): (usize, usize)) -> usize { 38 | match self { 39 | SizeConstraint::AtLeast(value) if result < value => value, /* max(result, value) */ 40 | SizeConstraint::AtMost(value) if result > value => value, /* min(result, value) */ 41 | SizeConstraint::Fixed(value) => value, 42 | // Explanation required: why return result if result > available? 43 | SizeConstraint::Full if available > result => available, 44 | _ => result, 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /cursive-core/src/view/view_path.rs: -------------------------------------------------------------------------------- 1 | /// Represents a path to a single view in the layout. 2 | pub struct ViewPath { 3 | /// List of turns to make on decision nodes when descending the view tree. 4 | /// Simple nodes (with one fixed child) are skipped. 5 | pub path: Vec, 6 | } 7 | 8 | new_default!(ViewPath); 9 | 10 | impl ViewPath { 11 | /// Creates a new empty path. 12 | pub fn new() -> Self { 13 | ViewPath { path: Vec::new() } 14 | } 15 | 16 | /// Creates a path from the given item. 17 | pub fn from(path: T) -> Self { 18 | path.to_path() 19 | } 20 | } 21 | 22 | /// Generic trait for elements that can be converted into a `ViewPath`. 23 | pub trait ToPath { 24 | /// Creates a path from the element. 25 | fn to_path(self) -> ViewPath; 26 | } 27 | 28 | impl<'a> ToPath for &'a [usize] { 29 | fn to_path(self) -> ViewPath { 30 | ViewPath { 31 | path: self.to_owned(), 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /cursive-core/src/views/boxed_view.rs: -------------------------------------------------------------------------------- 1 | use crate::view::{IntoBoxedView, View, ViewWrapper}; 2 | use std::ops::{Deref, DerefMut}; 3 | 4 | /// A boxed `View`. 5 | /// 6 | /// It derefs to the wrapped view. 7 | pub struct BoxedView { 8 | view: Box, 9 | } 10 | 11 | impl BoxedView { 12 | /// Creates a new `BoxedView` around the given boxed view. 13 | pub fn new(view: Box) -> Self { 14 | BoxedView { view } 15 | } 16 | 17 | /// Returns a reference to the inner view. 18 | /// 19 | /// Returns `None` if the inner view is not actually type `V`. 20 | pub fn get(&self) -> Option<&V> { 21 | self.view.downcast_ref() 22 | } 23 | 24 | /// Returns a mutable reference to the inner view. 25 | /// 26 | /// Returns `None` if the inner view is not actually type `V`. 27 | pub fn get_mut(&mut self) -> Option<&mut V> { 28 | self.view.downcast_mut() 29 | } 30 | 31 | /// Box the given view 32 | pub fn boxed(view: T) -> Self 33 | where 34 | T: IntoBoxedView, 35 | { 36 | BoxedView::new(view.into_boxed_view()) 37 | } 38 | 39 | /// Returns the inner boxed view. 40 | pub fn unwrap(self) -> Box { 41 | self.view 42 | } 43 | } 44 | 45 | impl Deref for BoxedView { 46 | type Target = dyn View; 47 | 48 | fn deref(&self) -> &dyn View { 49 | &*self.view 50 | } 51 | } 52 | 53 | impl DerefMut for BoxedView { 54 | fn deref_mut(&mut self) -> &mut dyn View { 55 | &mut *self.view 56 | } 57 | } 58 | 59 | impl ViewWrapper for BoxedView { 60 | type V = dyn View; 61 | 62 | fn with_view(&self, f: F) -> Option 63 | where 64 | F: FnOnce(&Self::V) -> R, 65 | { 66 | Some(f(&*self.view)) 67 | } 68 | 69 | fn with_view_mut(&mut self, f: F) -> Option 70 | where 71 | F: FnOnce(&mut Self::V) -> R, 72 | { 73 | Some(f(&mut *self.view)) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /cursive-core/src/views/debug_view.rs: -------------------------------------------------------------------------------- 1 | use crate::logger; 2 | use crate::style; 3 | use crate::view::View; 4 | use crate::Printer; 5 | use crate::Vec2; 6 | 7 | use unicode_width::UnicodeWidthStr; 8 | 9 | /// View used for debugging, showing logs. 10 | pub struct DebugView { 11 | // TODO: wrap log lines if needed, and save the line splits here. 12 | } 13 | 14 | impl DebugView { 15 | /// Creates a new DebugView. 16 | pub fn new() -> Self { 17 | DebugView {} 18 | } 19 | } 20 | 21 | impl Default for DebugView { 22 | fn default() -> Self { 23 | Self::new() 24 | } 25 | } 26 | 27 | impl View for DebugView { 28 | fn draw(&self, printer: &Printer) { 29 | let logs = logger::LOGS.lock().unwrap(); 30 | // Only print the last logs, so skip what doesn't fit 31 | let skipped = logs.len().saturating_sub(printer.size.y); 32 | 33 | let format = 34 | time::format_description::parse("[hour]:[minute]:[second].[subsecond digits:3]") 35 | .unwrap(); 36 | 37 | for (i, record) in logs.iter().skip(skipped).enumerate() { 38 | // TODO: Apply style to message? (Ex: errors in bold?) 39 | // TODO: customizable time format? (24h/AM-PM) 40 | let formatted = record 41 | .time 42 | .format(&format) 43 | .unwrap_or_else(|_| String::new()); 44 | printer.print( 45 | (0, i), 46 | &format!("{} | [ ] {}", formatted, record.message), 47 | ); 48 | let color = match record.level { 49 | log::Level::Error => style::BaseColor::Red.dark(), 50 | log::Level::Warn => style::BaseColor::Yellow.dark(), 51 | log::Level::Info => style::BaseColor::Black.light(), 52 | log::Level::Debug => style::BaseColor::Green.dark(), 53 | log::Level::Trace => style::BaseColor::Blue.dark(), 54 | }; 55 | printer.with_color(color.into(), |printer| { 56 | printer.print((16, i), &format!("{:5}", record.level)) 57 | }); 58 | } 59 | } 60 | 61 | fn required_size(&mut self, _constraint: Vec2) -> Vec2 { 62 | // TODO: read the logs, and compute the required size to print it. 63 | let logs = logger::LOGS.lock().unwrap(); 64 | 65 | let level_width = 8; // Width of "[ERROR] " 66 | let time_width = 16; // Width of "23:59:59.123 | " 67 | 68 | // The longest line sets the width 69 | let w = logs 70 | .iter() 71 | .map(|record| record.message.width() + level_width + time_width) 72 | .max() 73 | .unwrap_or(1); 74 | let h = logs.len(); 75 | 76 | Vec2::new(w, h) 77 | } 78 | 79 | fn layout(&mut self, _size: Vec2) { 80 | // Uh? 81 | } 82 | } 83 | 84 | crate::manual_blueprint!(DebugView, |_, _| { Ok(DebugView::new()) }); 85 | -------------------------------------------------------------------------------- /cursive-core/src/views/dummy.rs: -------------------------------------------------------------------------------- 1 | use crate::view::View; 2 | use crate::Printer; 3 | 4 | /// Dummy view. 5 | /// 6 | /// Doesn't print anything. Minimal size is (1,1). 7 | #[derive(Default, Debug, Clone, Copy)] 8 | pub struct DummyView; 9 | 10 | impl DummyView { 11 | /// Create a new `DummyView`. 12 | pub fn new() -> Self { 13 | DummyView 14 | } 15 | } 16 | 17 | impl View for DummyView { 18 | fn draw(&self, _: &Printer) {} 19 | 20 | fn needs_relayout(&self) -> bool { 21 | false 22 | } 23 | } 24 | 25 | #[crate::blueprint(DummyView::new())] 26 | struct Blueprint; 27 | 28 | // crate::manual_blueprint!(DummyView, |_config, _context| { Ok(DummyView) }); 29 | -------------------------------------------------------------------------------- /cursive-core/src/views/enableable_view.rs: -------------------------------------------------------------------------------- 1 | use crate::event::{Event, EventResult}; 2 | use crate::view::{View, ViewWrapper}; 3 | use crate::Printer; 4 | 5 | /// Wrapper around another view that can be enabled/disabled at will. 6 | /// 7 | /// When disabled, all child views will be disabled and will stop receiving events. 8 | /// 9 | /// # Examples 10 | /// 11 | /// ``` 12 | /// use cursive_core::traits::Nameable; 13 | /// use cursive_core::views::{Button, Checkbox, EnableableView, LinearLayout}; 14 | /// use cursive_core::Cursive; 15 | /// 16 | /// let mut siv = Cursive::new(); 17 | /// 18 | /// siv.add_layer( 19 | /// LinearLayout::vertical() 20 | /// .child(EnableableView::new(Checkbox::new()).with_name("my_view")) 21 | /// .child(Button::new("Toggle", |s| { 22 | /// s.call_on_name("my_view", |v: &mut EnableableView| { 23 | /// // This will disable (or re-enable) the checkbox, preventing the user from 24 | /// // interacting with it. 25 | /// v.set_enabled(!v.is_enabled()); 26 | /// }); 27 | /// })), 28 | /// ); 29 | /// ``` 30 | pub struct EnableableView { 31 | view: V, 32 | enabled: bool, 33 | } 34 | 35 | new_default!(EnableableView); 36 | 37 | impl EnableableView { 38 | /// Creates a new `EnableableView` around `view`. 39 | /// 40 | /// It will be enabled by default. 41 | pub fn new(view: V) -> Self { 42 | EnableableView { 43 | view, 44 | enabled: true, 45 | } 46 | } 47 | 48 | impl_enabled!(self.enabled); 49 | inner_getters!(self.view: V); 50 | } 51 | 52 | impl ViewWrapper for EnableableView { 53 | wrap_impl!(self.view: V); 54 | 55 | fn wrap_on_event(&mut self, event: Event) -> EventResult { 56 | if self.enabled { 57 | self.view.on_event(event) 58 | } else { 59 | EventResult::Ignored 60 | } 61 | } 62 | 63 | fn wrap_draw(&self, printer: &Printer) { 64 | self.view.draw(&printer.enabled(self.enabled)); 65 | } 66 | } 67 | 68 | crate::manual_blueprint!(with enableable, |config, context| { 69 | let enabled = context.resolve_or(&config["enabled"], true)?; 70 | 71 | Ok(move |view| EnableableView::new(view).with_enabled(enabled)) 72 | }); 73 | -------------------------------------------------------------------------------- /cursive-core/src/views/focus_tracker.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | direction::Direction, 3 | event::{Event, EventResult}, 4 | view::{CannotFocus, View, ViewWrapper}, 5 | With, 6 | }; 7 | 8 | /// Detects focus events for a view. 9 | pub struct FocusTracker { 10 | view: T, 11 | on_focus_lost: Box EventResult + Send + Sync>, 12 | on_focus: Box EventResult + Send + Sync>, 13 | } 14 | 15 | impl FocusTracker { 16 | /// Wraps a view in a new `FocusTracker`. 17 | pub fn new(view: T) -> Self { 18 | FocusTracker { 19 | view, 20 | on_focus_lost: Box::new(|_| EventResult::Ignored), 21 | on_focus: Box::new(|_| EventResult::Ignored), 22 | } 23 | } 24 | 25 | /// Sets a callback to be run when the focus is gained. 26 | #[must_use] 27 | pub fn on_focus(self, f: F) -> Self 28 | where 29 | F: 'static + FnMut(&mut T) -> EventResult + Send + Sync, 30 | { 31 | self.with(|s| s.set_on_focus(f)) 32 | } 33 | 34 | /// Sets a callback to be run when the focus is gained. 35 | #[crate::callback_helpers] 36 | pub fn set_on_focus(&mut self, f: F) 37 | where 38 | F: 'static + FnMut(&mut T) -> EventResult + Send + Sync, 39 | { 40 | self.on_focus = Box::new(f); 41 | } 42 | 43 | /// Sets a callback to be run when the focus is lost. 44 | #[must_use] 45 | pub fn on_focus_lost(self, f: F) -> Self 46 | where 47 | F: 'static + FnMut(&mut T) -> EventResult + Send + Sync, 48 | { 49 | self.with(|s| s.set_on_focus_lost(f)) 50 | } 51 | 52 | /// Sets a callback to be run when the focus is lost. 53 | #[crate::callback_helpers] 54 | pub fn set_on_focus_lost(&mut self, f: F) 55 | where 56 | F: 'static + FnMut(&mut T) -> EventResult + Send + Sync, 57 | { 58 | self.on_focus_lost = Box::new(f); 59 | } 60 | 61 | inner_getters!(self.view: T); 62 | } 63 | 64 | impl ViewWrapper for FocusTracker { 65 | wrap_impl!(self.view: T); 66 | 67 | fn wrap_take_focus(&mut self, source: Direction) -> Result { 68 | match self.view.take_focus(source) { 69 | Ok(res) => Ok(res.and((self.on_focus)(&mut self.view))), 70 | Err(CannotFocus) => Err(CannotFocus), 71 | } 72 | } 73 | 74 | fn wrap_on_event(&mut self, event: Event) -> EventResult { 75 | let res = if let Event::FocusLost = event { 76 | (self.on_focus_lost)(&mut self.view) 77 | } else { 78 | EventResult::Ignored 79 | }; 80 | res.and(self.view.on_event(event)) 81 | } 82 | } 83 | 84 | #[crate::blueprint(FocusTracker::new(view))] 85 | struct Blueprint { 86 | view: crate::views::BoxedView, 87 | 88 | on_focus: Option<_>, 89 | 90 | on_focus_lost: Option<_>, 91 | } 92 | 93 | crate::manual_blueprint!(with focus_tracker, |config, context| { 94 | let on_focus = context.resolve(&config["on_focus"])?; 95 | let on_focus_lost = context.resolve(&config["on_focus_lost"])?; 96 | 97 | Ok(move |view| { 98 | let mut tracker = FocusTracker::new(view); 99 | 100 | if let Some(on_focus) = on_focus { 101 | tracker.set_on_focus_cb(on_focus); 102 | } 103 | 104 | if let Some(on_focus_lost) = on_focus_lost { 105 | tracker.set_on_focus_lost_cb(on_focus_lost); 106 | } 107 | 108 | tracker 109 | }) 110 | }); 111 | -------------------------------------------------------------------------------- /cursive-core/src/views/hideable_view.rs: -------------------------------------------------------------------------------- 1 | use crate::event::AnyCb; 2 | use crate::view::{Selector, View, ViewWrapper}; 3 | use crate::Vec2; 4 | use crate::With; 5 | 6 | /// Wrapper around another view that can be hidden at will. 7 | /// 8 | /// By default, it simply forwards all calls to the inner view. 9 | /// 10 | /// When hidden (with `HideableView::hide()`), it will appear as a zero-sized 11 | /// invisible view, will not take focus and will not accept input. 12 | /// 13 | /// It can be made visible again with `HideableView::unhide()`. 14 | pub struct HideableView { 15 | view: V, 16 | visible: bool, 17 | invalidated: bool, 18 | } 19 | 20 | new_default!(HideableView); 21 | 22 | impl HideableView { 23 | /// Creates a new HideableView around `view`. 24 | /// 25 | /// It will be visible by default. 26 | pub fn new(view: V) -> Self { 27 | HideableView { 28 | view, 29 | visible: true, 30 | invalidated: true, 31 | } 32 | } 33 | 34 | /// Sets the visibility for this view. 35 | pub fn set_visible(&mut self, visible: bool) { 36 | self.visible = visible; 37 | self.invalidate(); 38 | } 39 | 40 | /// Sets the visibility for this view to `false`. 41 | pub fn hide(&mut self) { 42 | self.set_visible(false); 43 | } 44 | 45 | /// Sets the visibility for this view to `true`. 46 | pub fn unhide(&mut self) { 47 | self.set_visible(true); 48 | } 49 | 50 | /// Sets the visibility for this view to `false`. 51 | /// 52 | /// Chainable variant. 53 | #[must_use] 54 | pub fn hidden(self) -> Self { 55 | self.with(Self::hide) 56 | } 57 | 58 | /// Sets the visibility for this view to flag value. 59 | /// Useful when creating views needs to be decided at runtime. 60 | /// Chainable variant. 61 | #[must_use] 62 | pub fn visible(self, flag: bool) -> Self { 63 | self.with(|s| s.set_visible(flag)) 64 | } 65 | 66 | /// Returns `true` if the wrapped view is going to be visible. 67 | pub fn is_visible(&self) -> bool { 68 | self.visible 69 | } 70 | 71 | fn invalidate(&mut self) { 72 | self.invalidated = true; 73 | } 74 | 75 | inner_getters!(self.view: V); 76 | } 77 | 78 | impl ViewWrapper for HideableView { 79 | type V = V; 80 | 81 | fn with_view(&self, f: F) -> Option 82 | where 83 | F: FnOnce(&Self::V) -> R, 84 | { 85 | if self.visible { 86 | Some(f(&self.view)) 87 | } else { 88 | None 89 | } 90 | } 91 | 92 | fn with_view_mut(&mut self, f: F) -> Option 93 | where 94 | F: FnOnce(&mut Self::V) -> R, 95 | { 96 | if self.visible { 97 | Some(f(&mut self.view)) 98 | } else { 99 | None 100 | } 101 | } 102 | 103 | fn wrap_call_on_any(&mut self, selector: &Selector, callback: AnyCb) { 104 | // We always run callbacks, even when invisible. 105 | self.view.call_on_any(selector, callback) 106 | } 107 | 108 | fn into_inner(self) -> Result 109 | where 110 | Self: Sized, 111 | Self::V: Sized, 112 | { 113 | Ok(self.view) 114 | } 115 | 116 | fn wrap_layout(&mut self, size: Vec2) { 117 | self.invalidated = false; 118 | self.with_view_mut(|v| v.layout(size)); 119 | } 120 | 121 | fn wrap_needs_relayout(&self) -> bool { 122 | self.invalidated || (self.visible && self.view.needs_relayout()) 123 | } 124 | } 125 | 126 | #[crate::blueprint(HideableView::new(view))] 127 | struct Blueprint { 128 | view: crate::views::BoxedView, 129 | visible: Option, 130 | } 131 | 132 | crate::manual_blueprint!(with hideable, |config, context| { 133 | let visible: Option = context.resolve(&config["visible"])?; 134 | 135 | Ok(move |view| HideableView::new(view).visible(visible.unwrap_or(true))) 136 | }); 137 | -------------------------------------------------------------------------------- /cursive-core/src/views/last_size_view.rs: -------------------------------------------------------------------------------- 1 | use crate::view::View; 2 | use crate::view::ViewWrapper; 3 | use crate::Vec2; 4 | 5 | /// Wrapper around a view that remembers its size. 6 | pub struct LastSizeView { 7 | /// Wrapped view. 8 | pub view: T, 9 | /// Cached size from the last layout() call. 10 | pub size: Vec2, 11 | } 12 | 13 | new_default!(LastSizeView); 14 | 15 | impl LastSizeView { 16 | /// Wraps the given view. 17 | pub fn new(view: T) -> Self { 18 | LastSizeView { 19 | view, 20 | size: Vec2::zero(), 21 | } 22 | } 23 | 24 | inner_getters!(self.view: T); 25 | } 26 | 27 | impl ViewWrapper for LastSizeView { 28 | wrap_impl!(self.view: T); 29 | 30 | fn wrap_layout(&mut self, size: Vec2) { 31 | self.size = size; 32 | self.view.layout(size); 33 | } 34 | } 35 | 36 | #[crate::blueprint(LastSizeView::new(view))] 37 | struct Blueprint { 38 | view: crate::views::BoxedView, 39 | } 40 | 41 | crate::manual_blueprint!(with last_size, |_, _| Ok(LastSizeView::new)); 42 | -------------------------------------------------------------------------------- /cursive-core/src/views/layer.rs: -------------------------------------------------------------------------------- 1 | use crate::style::ColorStyle; 2 | use crate::view::{View, ViewWrapper}; 3 | use crate::Printer; 4 | 5 | /// Wrapper view that fills the background. 6 | /// 7 | /// This is mostly used as layer in the [`StackView`]. 8 | /// 9 | /// [`StackView`]: crate::views::StackView 10 | #[derive(Debug)] 11 | pub struct Layer { 12 | view: T, 13 | color: ColorStyle, 14 | } 15 | 16 | new_default!(Layer); 17 | 18 | impl Layer { 19 | /// Wraps the given view. 20 | pub fn new(view: T) -> Self { 21 | Self::with_color(view, ColorStyle::view()) 22 | } 23 | 24 | /// Wraps the given view with a custom background color. 25 | pub fn with_color(view: T, color: ColorStyle) -> Self { 26 | Layer { view, color } 27 | } 28 | 29 | /// Gets the current color. 30 | pub fn color(&self) -> ColorStyle { 31 | self.color 32 | } 33 | 34 | /// Sets the background color. 35 | pub fn set_color(&mut self, color: ColorStyle) { 36 | self.color = color; 37 | } 38 | 39 | inner_getters!(self.view: T); 40 | } 41 | 42 | impl ViewWrapper for Layer { 43 | wrap_impl!(self.view: T); 44 | 45 | fn wrap_draw(&self, printer: &Printer) { 46 | printer.with_color(self.color, |printer| { 47 | for y in 0..printer.size.y { 48 | printer.print_hline((0, y), printer.size.x, " "); 49 | } 50 | self.view.draw(printer); 51 | }); 52 | } 53 | } 54 | 55 | #[crate::blueprint(Layer::new(view))] 56 | struct Blueprint { 57 | view: crate::views::BoxedView, 58 | color: Option, 59 | } 60 | 61 | crate::manual_blueprint!(with layer, |config, context| { 62 | let color = match config { 63 | crate::builder::Config::Null => None, 64 | config => Some(context.resolve(config)?), 65 | }; 66 | Ok(move |view| { 67 | let mut layer = Layer::new(view); 68 | 69 | if let Some(color) = color { 70 | layer.set_color(color); 71 | } 72 | 73 | layer 74 | }) 75 | }); 76 | -------------------------------------------------------------------------------- /cursive-core/src/views/mod.rs: -------------------------------------------------------------------------------- 1 | //! Various views to use when creating the layout. 2 | 3 | /// A macro to help with creating toggleable views. 4 | /// 5 | /// # Examples 6 | /// 7 | /// ``` 8 | /// struct MyView { 9 | /// enabled: bool, 10 | /// } 11 | /// 12 | /// impl MyView { 13 | /// cursive_core::impl_enabled!(self.enabled); 14 | /// } 15 | /// 16 | /// let view = MyView { enabled: true }; 17 | /// assert!(view.is_enabled()); 18 | /// ``` 19 | #[macro_export] 20 | macro_rules! impl_enabled { 21 | (self.$x:ident) => { 22 | /// Disables this view. 23 | /// 24 | /// A disabled view cannot be selected. 25 | pub fn disable(&mut self) { 26 | self.$x = false; 27 | } 28 | 29 | /// Disables this view. 30 | /// 31 | /// Chainable variant. 32 | #[must_use] 33 | pub fn disabled(self) -> Self { 34 | use $crate::traits::With as _; 35 | self.with(Self::disable) 36 | } 37 | 38 | /// Re-enables this view. 39 | pub fn enable(&mut self) { 40 | self.$x = true; 41 | } 42 | 43 | /// Enable or disable this view. 44 | pub fn set_enabled(&mut self, enabled: bool) { 45 | self.$x = enabled; 46 | } 47 | 48 | /// Enable or disable this view. 49 | /// 50 | /// Chainable variant. 51 | #[must_use] 52 | pub fn with_enabled(mut self, is_enabled: bool) -> Self { 53 | self.set_enabled(is_enabled); 54 | self 55 | } 56 | 57 | /// Returns `true` if this view is enabled. 58 | pub fn is_enabled(&self) -> bool { 59 | self.$x 60 | } 61 | }; 62 | } 63 | 64 | mod boxed_view; 65 | mod button; 66 | mod canvas; 67 | mod checkbox; 68 | mod circular_focus; 69 | mod debug_view; 70 | mod dialog; 71 | mod dummy; 72 | mod edit_view; 73 | mod enableable_view; 74 | mod fixed_layout; 75 | mod focus_tracker; 76 | mod gradient_view; 77 | mod hideable_view; 78 | mod last_size_view; 79 | mod layer; 80 | mod linear_layout; 81 | mod list_view; 82 | mod menu_popup; 83 | mod menubar; 84 | mod named_view; 85 | mod on_event_view; 86 | mod on_layout_view; 87 | mod padded_view; 88 | mod panel; 89 | mod progress_bar; 90 | mod radio; 91 | mod resized_view; 92 | mod screens_view; 93 | mod scroll_view; 94 | mod select_view; 95 | mod shadow_view; 96 | mod slider_view; 97 | pub mod stack_view; 98 | mod text_area; 99 | mod text_view; 100 | mod themed_view; 101 | mod tracked_view; 102 | 103 | pub use self::{ 104 | boxed_view::BoxedView, 105 | button::Button, 106 | canvas::Canvas, 107 | checkbox::Checkbox, 108 | circular_focus::CircularFocus, 109 | debug_view::DebugView, 110 | dialog::{Dialog, DialogFocus}, 111 | dummy::DummyView, 112 | edit_view::EditView, 113 | enableable_view::EnableableView, 114 | fixed_layout::FixedLayout, 115 | focus_tracker::FocusTracker, 116 | gradient_view::GradientView, 117 | hideable_view::HideableView, 118 | last_size_view::LastSizeView, 119 | layer::Layer, 120 | linear_layout::LinearLayout, 121 | list_view::{ListChild, ListView}, 122 | menu_popup::MenuPopup, 123 | menubar::Menubar, 124 | named_view::{NamedView, ViewRef}, 125 | on_event_view::OnEventView, 126 | on_layout_view::OnLayoutView, 127 | padded_view::PaddedView, 128 | panel::Panel, 129 | progress_bar::ProgressBar, 130 | radio::{RadioButton, RadioGroup}, 131 | resized_view::ResizedView, 132 | screens_view::ScreensView, 133 | scroll_view::ScrollView, 134 | select_view::SelectView, 135 | shadow_view::ShadowView, 136 | slider_view::SliderView, 137 | stack_view::{LayerPosition, StackView}, 138 | text_area::TextArea, 139 | text_view::{TextContent, TextContentRef, TextView}, 140 | themed_view::ThemedView, 141 | tracked_view::TrackedView, 142 | }; 143 | -------------------------------------------------------------------------------- /cursive-core/src/views/on_layout_view.rs: -------------------------------------------------------------------------------- 1 | use crate::{view::ViewWrapper, Vec2, View}; 2 | 3 | type Callback = dyn FnMut(&mut V, Vec2) + Send + Sync; 4 | 5 | /// View wrapper overriding the `View::layout` method. 6 | pub struct OnLayoutView { 7 | view: V, 8 | on_layout: Box>, 9 | } 10 | 11 | impl OnLayoutView { 12 | /// Wraps a view in an `OnLayoutView`. 13 | /// 14 | /// Will run the given closure for layout _instead_ of the one from `view`. 15 | /// 16 | /// ```rust 17 | /// use cursive_core::{ 18 | /// views::{OnLayoutView, TextView}, 19 | /// View, 20 | /// }; 21 | /// 22 | /// let view = TextView::new("foo"); 23 | /// 24 | /// // Here we just run the innver view's layout. 25 | /// OnLayoutView::new(view, |v, s| v.layout(s)); 26 | /// ``` 27 | pub fn new(view: V, on_layout: F) -> Self 28 | where 29 | F: FnMut(&mut V, Vec2) + 'static + Send + Sync, 30 | { 31 | let on_layout = Box::new(on_layout); 32 | OnLayoutView { view, on_layout } 33 | } 34 | 35 | /// Wraps a view in an `OnLayoutView`. 36 | /// 37 | /// This is a shortcut for `Self::new(view, V::layout)` 38 | /// 39 | /// You can change it later with `set_on_layout`. 40 | pub fn wrap(view: V) -> Self 41 | where 42 | V: View, 43 | { 44 | Self::new(view, V::layout) 45 | } 46 | 47 | /// Replaces the callback to run. 48 | #[crate::callback_helpers] 49 | pub fn set_on_layout(&mut self, on_layout: F) 50 | where 51 | F: FnMut(&mut V, Vec2) + 'static + Send + Sync, 52 | { 53 | self.on_layout = Box::new(on_layout); 54 | } 55 | 56 | inner_getters!(self.view: V); 57 | } 58 | 59 | impl ViewWrapper for OnLayoutView { 60 | wrap_impl!(self.view: V); 61 | 62 | fn wrap_layout(&mut self, size: Vec2) { 63 | (self.on_layout)(&mut self.view, size); 64 | } 65 | } 66 | 67 | #[crate::blueprint(OnLayoutView::wrap(view))] 68 | struct Blueprint { 69 | view: crate::views::BoxedView, 70 | 71 | on_layout: Option<_>, 72 | } 73 | 74 | crate::manual_blueprint!(with on_layout, |config, context| { 75 | let callback = context.resolve(config)?; 76 | Ok(move |view| { 77 | let mut view = OnLayoutView::wrap(view); 78 | if let Some(callback) = callback { 79 | view.set_on_layout_cb(callback); 80 | } 81 | view 82 | }) 83 | }); 84 | -------------------------------------------------------------------------------- /cursive-core/src/views/padded_view.rs: -------------------------------------------------------------------------------- 1 | use crate::event::{Event, EventResult}; 2 | use crate::rect::Rect; 3 | use crate::view::{Margins, View, ViewWrapper}; 4 | use crate::Printer; 5 | use crate::Vec2; 6 | 7 | /// Adds padding to another view. 8 | /// 9 | /// This view wraps another view and adds some padding. 10 | /// 11 | /// The wrapped view will see a reduced space available. 12 | /// 13 | /// # Examples 14 | /// 15 | /// ```rust 16 | /// # use cursive_core::views::{TextView, PaddedView}; 17 | /// // Adds 2 columns of padding to the left and to the right. 18 | /// 19 | /// // lrtb = Left, Right, Top, Bottom 20 | /// let view = PaddedView::lrtb(2, 2, 0, 0, TextView::new("Padded text")); 21 | /// ``` 22 | pub struct PaddedView { 23 | view: V, 24 | margins: Margins, 25 | } 26 | 27 | impl PaddedView { 28 | /// Wraps `view` in a new `PaddedView` with the given margins. 29 | pub fn new(margins: Margins, view: V) -> Self { 30 | PaddedView { view, margins } 31 | } 32 | 33 | /// Wraps `view` in a new `PaddedView` with the given margins. 34 | pub fn lrtb(left: usize, right: usize, top: usize, bottom: usize, view: V) -> Self { 35 | Self::new(Margins::lrtb(left, right, top, bottom), view) 36 | } 37 | 38 | /// Sets the margins for this view. 39 | pub fn set_margins(&mut self, margins: Margins) { 40 | // TODO: invalidate? wrap_needs_relayout? 41 | self.margins = margins; 42 | } 43 | 44 | inner_getters!(self.view: V); 45 | } 46 | 47 | impl ViewWrapper for PaddedView { 48 | wrap_impl!(self.view: V); 49 | 50 | fn wrap_required_size(&mut self, req: Vec2) -> Vec2 { 51 | let margins = self.margins.combined(); 52 | self.view.required_size(req.saturating_sub(margins)) + margins 53 | } 54 | 55 | fn wrap_layout(&mut self, size: Vec2) { 56 | let margins = self.margins.combined(); 57 | self.view.layout(size.saturating_sub(margins)); 58 | } 59 | 60 | fn wrap_on_event(&mut self, event: Event) -> EventResult { 61 | let padding = self.margins.top_left(); 62 | self.view.on_event(event.relativized(padding)) 63 | } 64 | 65 | fn wrap_draw(&self, printer: &Printer) { 66 | let top_left = self.margins.top_left(); 67 | let bot_right = self.margins.bot_right(); 68 | let printer = &printer.offset(top_left).shrinked(bot_right); 69 | self.view.draw(printer); 70 | } 71 | 72 | fn wrap_important_area(&self, view_size: Vec2) -> Rect { 73 | let inner_size = view_size.saturating_sub(self.margins.combined()); 74 | self.view.important_area(inner_size) + self.margins.top_left() 75 | } 76 | } 77 | 78 | #[crate::blueprint(PaddedView::new(margins, view))] 79 | struct Blueprint { 80 | margins: Margins, 81 | view: crate::views::BoxedView, 82 | } 83 | 84 | crate::manual_blueprint!(with padding, |config, context| { 85 | let margins = context.resolve(config)?; 86 | Ok(move |view| PaddedView::new(margins, view)) 87 | }); 88 | -------------------------------------------------------------------------------- /cursive-core/src/views/shadow_view.rs: -------------------------------------------------------------------------------- 1 | use crate::event::{Event, EventResult}; 2 | use crate::rect::Rect; 3 | use crate::style::PaletteStyle; 4 | use crate::view::{View, ViewWrapper}; 5 | use crate::Printer; 6 | use crate::Vec2; 7 | 8 | /// Wrapper view that adds a shadow. 9 | /// 10 | /// It reserves a 1 pixel border on each side. 11 | pub struct ShadowView { 12 | view: T, 13 | top_padding: bool, 14 | left_padding: bool, 15 | // TODO: invalidate if we change the padding? wrap_needs_relayout? 16 | } 17 | 18 | new_default!(ShadowView); 19 | 20 | impl ShadowView { 21 | /// Wraps the given view. 22 | pub fn new(view: T) -> Self { 23 | ShadowView { 24 | view, 25 | top_padding: true, 26 | left_padding: true, 27 | } 28 | } 29 | 30 | /// Return the total padding for this view (include both sides) 31 | fn padding(&self) -> Vec2 { 32 | // We always need (1, 1) for the shadow. 33 | self.top_left_padding() + (1, 1) 34 | } 35 | 36 | fn top_left_padding(&self) -> Vec2 { 37 | Vec2::new(self.left_padding as usize, self.top_padding as usize) 38 | } 39 | 40 | /// If set, adds an empty column to the left of the view. 41 | /// 42 | /// Default to true. 43 | #[must_use] 44 | pub fn left_padding(mut self, value: bool) -> Self { 45 | self.left_padding = value; 46 | self 47 | } 48 | 49 | /// If set, adds an empty row at the top of the view. 50 | /// 51 | /// Default to true. 52 | #[must_use] 53 | pub fn top_padding(mut self, value: bool) -> Self { 54 | self.top_padding = value; 55 | self 56 | } 57 | 58 | inner_getters!(self.view: T); 59 | } 60 | 61 | impl ViewWrapper for ShadowView { 62 | wrap_impl!(self.view: T); 63 | 64 | fn wrap_required_size(&mut self, req: Vec2) -> Vec2 { 65 | // Make sure req >= offset 66 | let offset = self.padding(); 67 | self.view.required_size(req.saturating_sub(offset)) + offset 68 | } 69 | 70 | fn wrap_layout(&mut self, size: Vec2) { 71 | let offset = self.padding(); 72 | self.view.layout(size.saturating_sub(offset)); 73 | } 74 | 75 | fn wrap_on_event(&mut self, event: Event) -> EventResult { 76 | let padding = self.top_left_padding(); 77 | self.view.on_event(event.relativized(padding)) 78 | } 79 | 80 | fn wrap_draw(&self, printer: &Printer) { 81 | if printer.size.y <= self.top_padding as usize 82 | || printer.size.x <= self.left_padding as usize 83 | { 84 | // Nothing to do if there's no place to draw. 85 | return; 86 | } 87 | 88 | // Skip the first row/column 89 | let offset = Vec2::new(self.left_padding as usize, self.top_padding as usize); 90 | let printer = &printer.offset(offset); 91 | if printer.theme.shadow { 92 | let h = printer.size.y; 93 | let w = printer.size.x; 94 | 95 | if h == 0 || w == 0 { 96 | return; 97 | } 98 | 99 | printer.with_style(PaletteStyle::Shadow, |printer| { 100 | printer.print_hline((1, h - 1), w - 1, " "); 101 | printer.print_vline((w - 1, 1), h - 1, " "); 102 | }); 103 | } 104 | 105 | // Draw the view background 106 | let printer = printer.shrinked((1, 1)); 107 | self.view.draw(&printer); 108 | } 109 | 110 | fn wrap_important_area(&self, view_size: Vec2) -> Rect { 111 | self.view 112 | .important_area(view_size.saturating_sub(self.padding())) 113 | + self.top_left_padding() 114 | } 115 | } 116 | 117 | #[crate::blueprint(ShadowView::new(view))] 118 | struct Blueprint { 119 | view: crate::views::BoxedView, 120 | } 121 | 122 | crate::manual_blueprint!(with shadow, |_, _| Ok(ShadowView::new)); 123 | -------------------------------------------------------------------------------- /cursive-core/src/views/themed_view.rs: -------------------------------------------------------------------------------- 1 | use crate::view::{View, ViewWrapper}; 2 | 3 | /// Applies a theme to the wrapped view. 4 | pub struct ThemedView { 5 | theme: crate::theme::Theme, 6 | view: T, 7 | } 8 | 9 | impl ThemedView { 10 | /// Wrap the given view with a theme. 11 | pub fn new(theme: crate::theme::Theme, view: T) -> Self { 12 | ThemedView { theme, view } 13 | } 14 | 15 | /// Retrieve the wrapped theme. 16 | pub fn get_theme(&self) -> &crate::theme::Theme { 17 | &self.theme 18 | } 19 | 20 | /// Sets a new theme for the wrapped view. 21 | pub fn set_theme(&mut self, theme: crate::theme::Theme) { 22 | self.theme = theme; 23 | } 24 | 25 | inner_getters!(self.view: T); 26 | } 27 | 28 | impl ViewWrapper for ThemedView { 29 | wrap_impl!(self.view: T); 30 | 31 | fn wrap_draw(&self, printer: &crate::Printer) { 32 | // Hack: We need to re-apply the View (+Primary) style. 33 | // 34 | // InheritParent would not be enough because it re-uses the previous _concrete color_ 35 | // (after the theme is applied), so it would not pick up the theme new colors. 36 | // Ideally we would need to know the previous _StyleType_ (before the theme is applied), 37 | // but that's not easy for now. 38 | printer 39 | .theme(&self.theme) 40 | .with_style(crate::style::PaletteStyle::View, |printer| { 41 | self.view.draw(printer); 42 | }); 43 | } 44 | } 45 | 46 | #[crate::blueprint(ThemedView::new(theme, view))] 47 | struct Blueprint { 48 | view: crate::views::BoxedView, 49 | theme: crate::theme::Theme, 50 | } 51 | 52 | crate::manual_blueprint!(with theme, |config, context| { 53 | let theme = context.resolve(config)?; 54 | Ok(move |view| ThemedView::new(theme, view)) 55 | }); 56 | -------------------------------------------------------------------------------- /cursive-core/src/views/tracked_view.rs: -------------------------------------------------------------------------------- 1 | use crate::view::{View, ViewWrapper}; 2 | use crate::Printer; 3 | use crate::Vec2; 4 | use parking_lot::Mutex; 5 | 6 | /// Wrapper around a view that remembers its position. 7 | pub struct TrackedView { 8 | /// Wrapped view. 9 | pub view: T, 10 | /// Last position the view was located. 11 | offset: Mutex, 12 | } 13 | 14 | new_default!(TrackedView); 15 | 16 | impl TrackedView { 17 | /// Return the last offset at which the view was drawn. 18 | pub fn offset(&self) -> Vec2 { 19 | *self.offset.lock() 20 | } 21 | 22 | /// Creates a new `TrackedView` around `view`. 23 | pub fn new(view: T) -> Self { 24 | TrackedView { 25 | view, 26 | offset: Mutex::new(Vec2::zero()), 27 | } 28 | } 29 | 30 | inner_getters!(self.view: T); 31 | } 32 | 33 | impl ViewWrapper for TrackedView { 34 | wrap_impl!(self.view: T); 35 | 36 | fn wrap_draw(&self, printer: &Printer) { 37 | *self.offset.lock() = printer.offset; 38 | self.view.draw(printer); 39 | } 40 | } 41 | 42 | #[crate::blueprint(TrackedView::new(view))] 43 | struct Blueprint { 44 | view: crate::views::BoxedView, 45 | } 46 | 47 | crate::manual_blueprint!(with tracked, |_, _| Ok(TrackedView::new)); 48 | -------------------------------------------------------------------------------- /cursive-core/src/with.rs: -------------------------------------------------------------------------------- 1 | /// Generic trait to enable chainable API 2 | pub trait With: Sized { 3 | /// Calls the given closure and return the result. 4 | /// 5 | /// Used to chainify wrapper constructors. 6 | fn wrap_with U>(self, f: F) -> U { 7 | f(self) 8 | } 9 | 10 | /// Calls the given closure on `self`. 11 | #[must_use] 12 | fn with(mut self, f: F) -> Self { 13 | f(&mut self); 14 | self 15 | } 16 | 17 | /// Calls the given closure on `self`. 18 | fn try_with(mut self, f: F) -> Result 19 | where 20 | F: FnOnce(&mut Self) -> Result<(), E>, 21 | { 22 | f(&mut self)?; 23 | Ok(self) 24 | } 25 | 26 | /// Calls the given closure if `condition == true`. 27 | #[must_use] 28 | fn with_if(mut self, condition: bool, f: F) -> Self 29 | where 30 | F: FnOnce(&mut Self), 31 | { 32 | if condition { 33 | f(&mut self); 34 | } 35 | self 36 | } 37 | } 38 | 39 | impl With for T {} 40 | -------------------------------------------------------------------------------- /cursive-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Alexandre Bury "] 3 | categories = ["command-line-interface", "gui"] 4 | description = "Proc-macros for the cursive TUI library." 5 | documentation = "https://docs.rs/cursive-macros" 6 | edition = "2021" 7 | keywords = ["ncurses", "TUI", "UI"] 8 | license = "MIT" 9 | name = "cursive-macros" 10 | readme = "Readme.md" 11 | repository = "https://github.com/gyscos/cursive" 12 | version = "0.1.0" 13 | include = ["src/**/*", "/LICENSE"] 14 | 15 | [lib] 16 | proc-macro = true 17 | 18 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 19 | 20 | [dependencies] 21 | find-crate = { optional = true, version = "0.6.3" } 22 | proc-macro2 = "1.0.47" 23 | quote = {version = "1.0.21", optional = true } 24 | syn = { version = "2", features = ["full", "extra-traits"], optional = true } 25 | 26 | 27 | [features] 28 | builder = ["find-crate", "syn", "quote"] 29 | -------------------------------------------------------------------------------- /cursive-macros/LICENSE: -------------------------------------------------------------------------------- 1 | ../LICENSE -------------------------------------------------------------------------------- /cursive-macros/Readme.md: -------------------------------------------------------------------------------- 1 | # cursive-macros 2 | 3 | This crate defines procedural macros for use with cursive. 4 | 5 | You probably don't need to use this directly; instead, look at the re-exported macros in `cursive` or `cursive-core`. 6 | -------------------------------------------------------------------------------- /cursive-macros/src/builder/dummy_mod.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | 3 | // When the builder feature is disabled, just remove the entire thing. 4 | pub fn blueprint(_: TokenStream, _: TokenStream) -> TokenStream { 5 | TokenStream::new() 6 | } 7 | 8 | // Just return the annotated function unchanged. 9 | pub fn callback_helpers(item: TokenStream) -> TokenStream { 10 | item 11 | } 12 | -------------------------------------------------------------------------------- /cursive-macros/src/builder/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "builder")] 2 | include!("real_mod.rs"); 3 | 4 | #[cfg(not(feature = "builder"))] 5 | include!("dummy_mod.rs"); 6 | -------------------------------------------------------------------------------- /cursive-macros/src/builder/real_mod.rs: -------------------------------------------------------------------------------- 1 | mod blueprint; 2 | mod callback_helper; 3 | 4 | pub use blueprint::blueprint; 5 | pub use callback_helper::callback_helpers; 6 | -------------------------------------------------------------------------------- /cursive-syntect/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cursive-syntect" 3 | readme = "README.md" 4 | authors = ["Alexandre Bury "] 5 | description = "Highlight text with syntect as cursive styled text." 6 | documentation = "https://docs.rs/cursive-syntect" 7 | keywords = ["cursive", "TUI", "syntect"] 8 | repository = "https://github.com/gyscos/cursive" 9 | license = "MIT" 10 | version = "0.2.0" 11 | edition = "2021" 12 | include = ["src/lib.rs", "LICENSE", "README.md"] 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [dependencies] 17 | cursive_core = { path = "../cursive-core", version= "0.4.0"} 18 | unicode-width = "0.2" 19 | 20 | [dependencies.syntect] 21 | version = "5.0.0" 22 | # default-features = false 23 | features = ["parsing"] 24 | 25 | [dev-dependencies] 26 | cursive = { path = "../cursive", version = "0.21.0" } 27 | 28 | [dev-dependencies.syntect] 29 | version = "5.0.0" 30 | features = ["regex-onig", "metadata"] 31 | 32 | [features] 33 | # Using this means that we don't build with "default-features = false", which is unfortunate. 34 | #default = ["regex-onig"] 35 | #regex-onig = ["syntect/regex-onig"] 36 | #regex-fancy = ["syntect/regex-fancy"] 37 | -------------------------------------------------------------------------------- /cursive-syntect/LICENSE: -------------------------------------------------------------------------------- 1 | ../LICENSE -------------------------------------------------------------------------------- /cursive-syntect/README.md: -------------------------------------------------------------------------------- 1 | # cursive-syntect 2 | 3 | [![crates.io](https://img.shields.io/crates/v/cursive-syntect.svg)](https://crates.io/crates/cursive-syntect) 4 | [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) 5 | [![Gitter chat](https://badges.gitter.im/gyscos/cursive.png)](https://gitter.im/cursive-rs/cursive) 6 | 7 | This is a thin adapter to style a string for cursive using a highlighter from syntect. 8 | 9 | See the example for usage. 10 | 11 | syntect example 12 | 13 | ```toml 14 | [dependencies] 15 | cursive-syntect = "0.2" 16 | ``` 17 | -------------------------------------------------------------------------------- /cursive-syntect/examples/parse.rs: -------------------------------------------------------------------------------- 1 | use cursive::traits::*; 2 | use syntect::{highlighting::ThemeSet, parsing::SyntaxSet}; 3 | 4 | use std::rc::Rc; 5 | 6 | struct State { 7 | syntax_set: SyntaxSet, 8 | themes: ThemeSet, 9 | } 10 | 11 | fn main() { 12 | let mut siv = cursive::default(); 13 | 14 | // Load syntect syntax, theme, ... 15 | let syntax_set = SyntaxSet::load_defaults_newlines(); 16 | let themes = ThemeSet::load_defaults(); 17 | 18 | siv.set_user_data(Rc::new(State { syntax_set, themes })); 19 | 20 | // Use it as a single view. 21 | siv.add_fullscreen_layer( 22 | cursive::views::TextView::new("") 23 | .with_name("content") 24 | .scrollable() 25 | .full_screen(), 26 | ); 27 | 28 | siv.with_theme(|t| { 29 | t.shadow = false; 30 | }); 31 | 32 | apply_theme(&mut siv, "InspiredGitHub"); 33 | 34 | siv.add_global_callback('q', |s| s.quit()); 35 | 36 | siv.add_global_callback('t', |s| { 37 | let theme_names: Vec<_> = s 38 | .with_user_data(|s: &mut Rc| s.themes.themes.keys().cloned().collect()) 39 | .unwrap(); 40 | 41 | s.add_layer( 42 | cursive::views::OnEventView::new( 43 | cursive::views::Dialog::new() 44 | .title("Select a theme") 45 | .content( 46 | cursive::views::SelectView::new() 47 | .with_all_str(theme_names) 48 | .on_submit(|s, theme_name| { 49 | apply_theme(s, theme_name); 50 | s.pop_layer(); 51 | }), 52 | ), 53 | ) 54 | .on_event(cursive::event::Key::Esc, |s| { 55 | s.pop_layer(); 56 | }), 57 | ); 58 | }); 59 | 60 | siv.add_layer(cursive::views::Dialog::info( 61 | r"This is a syntect example. 62 | 63 | This very file is printed here. 64 | 65 | Press T to change the theme. 66 | Press Q to quit.", 67 | )); 68 | 69 | siv.run(); 70 | } 71 | 72 | fn apply_theme(siv: &mut cursive::Cursive, theme_name: &str) { 73 | let state = siv 74 | .with_user_data(|s: &mut Rc| Rc::clone(s)) 75 | .unwrap(); 76 | 77 | let theme = &state.themes.themes[theme_name]; 78 | let syntax = state.syntax_set.find_syntax_by_token("rs").unwrap(); 79 | let mut highlighter = syntect::easy::HighlightLines::new(syntax, theme); 80 | 81 | // Apply some settings from the theme to cursive's own theme. 82 | siv.with_theme(|t| { 83 | if let Some(background) = theme 84 | .settings 85 | .background 86 | .map(cursive_syntect::translate_color) 87 | { 88 | t.palette[cursive::theme::PaletteColor::Background] = background; 89 | t.palette[cursive::theme::PaletteColor::View] = background; 90 | } 91 | if let Some(foreground) = theme 92 | .settings 93 | .foreground 94 | .map(cursive_syntect::translate_color) 95 | { 96 | t.palette[cursive::theme::PaletteColor::Primary] = foreground; 97 | t.palette[cursive::theme::PaletteColor::TitlePrimary] = foreground; 98 | } 99 | 100 | if let Some(highlight) = theme 101 | .settings 102 | .highlight 103 | .map(cursive_syntect::translate_color) 104 | { 105 | t.palette[cursive::theme::PaletteColor::Highlight] = highlight; 106 | } 107 | }); 108 | 109 | // Read some content somewhere 110 | let content = include_str!("parse.rs"); 111 | 112 | // Parse the content and highlight it 113 | let styled = cursive_syntect::parse(content, &mut highlighter, &state.syntax_set).unwrap(); 114 | 115 | siv.call_on_name("content", |t: &mut cursive::views::TextView| { 116 | t.set_content(styled); 117 | }) 118 | .unwrap(); 119 | } 120 | -------------------------------------------------------------------------------- /cursive-syntect/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Parse text using a [`syntect`] highlighter. 2 | //! 3 | //! The [`parse()`] function can be used to generate a StyledString using a 4 | //! highlighter and a syntax set. 5 | //! 6 | //! [`syntect`]: https://docs.rs/syntect 7 | #![deny(missing_docs)] 8 | 9 | use cursive_core::style; 10 | use cursive_core::utils::markup::{StyledIndexedSpan, StyledString}; 11 | use cursive_core::utils::span::IndexedCow; 12 | 13 | use unicode_width::UnicodeWidthStr; 14 | 15 | /// Translate a syntect font style into a set of cursive effects. 16 | pub fn translate_effects(font_style: syntect::highlighting::FontStyle) -> style::Effects { 17 | let mut effects = style::Effects::empty(); 18 | 19 | for &(style, effect) in &[ 20 | (syntect::highlighting::FontStyle::BOLD, style::Effect::Bold), 21 | ( 22 | syntect::highlighting::FontStyle::UNDERLINE, 23 | style::Effect::Underline, 24 | ), 25 | ( 26 | syntect::highlighting::FontStyle::ITALIC, 27 | style::Effect::Italic, 28 | ), 29 | ] { 30 | if font_style.contains(style) { 31 | effects.insert(effect); 32 | } 33 | } 34 | 35 | effects 36 | } 37 | 38 | /// Translate a syntect color into a cursive color. 39 | pub fn translate_color(color: syntect::highlighting::Color) -> style::Color { 40 | style::Color::Rgb(color.r, color.g, color.b) 41 | } 42 | 43 | /// Translate a syntect style into a cursive style. 44 | pub fn translate_style(style: syntect::highlighting::Style) -> style::Style { 45 | let front = translate_color(style.foreground); 46 | let back = translate_color(style.background); 47 | 48 | style::Style { 49 | color: (front, back).into(), 50 | effects: translate_effects(style.font_style), 51 | } 52 | } 53 | 54 | /// Parse text using a syntect highlighter. 55 | pub fn parse>( 56 | input: S, 57 | highlighter: &mut syntect::easy::HighlightLines, 58 | syntax_set: &syntect::parsing::SyntaxSet, 59 | ) -> Result { 60 | let input = input.into(); 61 | let mut spans = Vec::new(); 62 | 63 | for line in input.split_inclusive('\n') { 64 | for (style, text) in highlighter.highlight_line(line, syntax_set)? { 65 | spans.push(StyledIndexedSpan { 66 | content: IndexedCow::from_str(text, &input), 67 | attr: translate_style(style), 68 | width: text.width(), 69 | }); 70 | } 71 | } 72 | 73 | Ok(StyledString::with_spans(input, spans)) 74 | } 75 | -------------------------------------------------------------------------------- /cursive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Alexandre Bury "] 3 | categories = ["command-line-interface", "gui"] 4 | description = "A TUI (Text User Interface) library focused on ease-of-use." 5 | documentation = "https://docs.rs/cursive" 6 | edition = "2021" 7 | keywords = ["ncurses", "TUI", "UI"] 8 | license = "MIT" 9 | name = "cursive" 10 | readme = "README.md" 11 | repository = "https://github.com/gyscos/cursive" 12 | version = "0.21.1" 13 | include = ["src/**/*", "LICENSE", "README.md"] 14 | 15 | [package.metadata.docs.rs] 16 | features = [ 17 | "doc-cfg", 18 | "ansi", "toml", "markdown", 19 | "builder", 20 | "termion-backend", 21 | "crossterm-backend", 22 | "pancurses-backend", 23 | ] 24 | 25 | [dependencies] 26 | cursive_core = { path = "../cursive-core", version= "0.4.0"} 27 | crossbeam-channel = "0.5" 28 | cfg-if = "1" 29 | unicode-segmentation = "1" 30 | unicode-width = "0.2" 31 | lazy_static = "1" 32 | libc = "0.2" 33 | maplit = { version = "1.0", optional = true } 34 | log = "0.4" 35 | ahash = "0.8" 36 | 37 | [dependencies.bear-lib-terminal] 38 | optional = true 39 | version = "2" 40 | 41 | [dependencies.ncurses] 42 | features = ["wide"] 43 | optional = true 44 | version = "6.0.1" 45 | 46 | [dependencies.pancurses] 47 | features = ["wide"] 48 | optional = true 49 | version = "0.17" 50 | 51 | [dependencies.termion] 52 | optional = true 53 | version = "4" 54 | 55 | [dependencies.crossterm] 56 | optional = true 57 | version = "0.28.1" 58 | 59 | [features] 60 | doc-cfg = ["cursive_core/doc-cfg"] # Enable doc_cfg, a nightly-only doc feature. 61 | builder = ["cursive_core/builder"] # Enable the builder module to build views from config blobs. 62 | blt-backend = ["dep:bear-lib-terminal"] # Enable the BearLibTerminal backend. Requires BLT to be installed separately. 63 | default = ["crossterm-backend"] # Defaults to crossterm, supported on windows, linux and macos. 64 | ncurses-backend = ["dep:ncurses", "dep:maplit"] # Enable the ncurses backend. 65 | pancurses-backend = ["dep:pancurses", "dep:maplit"] # Enable the pancurses backend. 66 | termion-backend = ["dep:termion"] # Enable the termion backend. 67 | crossterm-backend = ["dep:crossterm"] # Enable the crossterm backend. 68 | markdown = ["cursive_core/markdown"] # Allows parsing StyledString from markdown text. 69 | ansi = ["cursive_core/ansi"] # Allows parsing StyledString from ANSI-marked up text. 70 | toml = ["cursive_core/toml"] # Allows parsing themes from toml. 71 | 72 | [lib] 73 | name = "cursive" 74 | 75 | [target.'cfg(unix)'.dependencies] 76 | signal-hook = "0.3" 77 | 78 | [[example]] 79 | name = "theme" 80 | required-features = ["toml"] 81 | 82 | [[example]] 83 | name = "ansi" 84 | required-features = ["ansi"] 85 | 86 | [[example]] 87 | name = "colored_text" 88 | required-features = ["ansi", "crossterm-backend"] 89 | 90 | [[example]] 91 | name = "builder" 92 | required-features = ["builder"] 93 | 94 | [dev-dependencies] 95 | rand = "0.8" 96 | pretty-bytes = "0.2" 97 | serde_json = "1.0.85" 98 | serde_yaml = "0.9.13" 99 | -------------------------------------------------------------------------------- /cursive/LICENSE: -------------------------------------------------------------------------------- 1 | ../LICENSE -------------------------------------------------------------------------------- /cursive/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /cursive/examples/Readme.md: -------------------------------------------------------------------------------- 1 | # Cursive Examples 2 | 3 | Here are example programs using Cursive to help you getting familiar with the 4 | various aspects of the library. 5 | 6 | To run an example, use `cargo run --example EXAMPLE_NAME`. 7 | 8 | To use a specific cursive backend, you can do, for example: 9 | 10 | ``` 11 | cargo run --example EXAMPLE_NAME --features crossterm-backend --no-default-features 12 | ``` 13 | 14 | ## [`hello_world`](./hello_world.rs) 15 | 16 | Simplest example possible, it will show you the starting point of a basic 17 | Cursive application. 18 | 19 | ## [`dialog`](./dialog.rs) 20 | 21 | This example wraps the text in a `Dialog` view, showing the basic idea of view 22 | composition. 23 | 24 | ## [`lorem`](./lorem.rs) 25 | 26 | This example loads a large text file to show scrolling behaviour. It also 27 | includes greek and japanese characters to show non-ascii support. 28 | 29 | ## [`edit`](./edit.rs) 30 | 31 | Here we have an `EditView` to get input from the user, and use that input in 32 | the next view. It shows how to identify a view with an name and refer to it 33 | later. 34 | 35 | ## [`mutation`](./mutation.rs) 36 | 37 | This example modifies the content of an existing view. 38 | 39 | ## [`linear`](./linear.rs) 40 | 41 | This example uses a `LinearView` to put multiple views side-by-side. 42 | 43 | ## [`menubar`](./menubar.rs) 44 | 45 | Here we learn how to create a menubar at the top of the screen, and populate 46 | it with static and dynamic entried. 47 | 48 | ## [`menubar_styles`](./menubar_styles.rs) 49 | 50 | Example of using StyledString in menubar lables. 51 | 52 | ## [`logs`](./logs.rs) 53 | 54 | This example defines a custom view to display asynchronous input from a 55 | channel. 56 | 57 | ## [`key_codes`](./key_codes.rs) 58 | 59 | This example uses a custom view to print any input received. Can be used as a 60 | debugging tool to see what input the application is receiving. 61 | 62 | ## [`select`](./select.rs) 63 | 64 | This example uses a `SelectView` to have the user pick a city from a long list. 65 | 66 | ## [`list_view`](./list_view.rs) 67 | 68 | This shows a use of a `ListView`, used to build simple forms. 69 | 70 | ## [`text_area`](./text_area.rs) 71 | 72 | This example uses a `TextArea`, where the user can input a block of text. 73 | 74 | ## [`markup`](./markup.rs) 75 | 76 | This example prints a text with markup decorations. 77 | 78 | ## [`theme`](./theme.rs) 79 | 80 | This loads a theme file at runtime to change default colors. 81 | 82 | ## [`theme_manual`](./theme_manual.rs) 83 | 84 | Instead of loading a theme file, this manually sets various theme settings. 85 | 86 | ## [`terminal_default`](./terminal_default.rs) 87 | 88 | This example shows the effect of the `Color::TerminalDefault` setting. 89 | 90 | ## [`colors`](./colors.rs) 91 | 92 | This example draws a colorful square to show off true color support. 93 | 94 | ## [`colored_text`](./colored_text.rs) 95 | 96 | This example showcasing various methods to color and remove text styles, highlighting the limitations of Crossterm's raw ANSI output. 97 | 98 | ## [`refcell_view`](./refcell_view.rs) 99 | 100 | Here we show how to access multiple views concurrently through their name. 101 | 102 | ## [`progress`](./progress.rs) 103 | 104 | This shows how to send information from an asynchronous task (like a download 105 | or slow computation) to update a progress bar. 106 | 107 | ## [`radio`](./radio.rs) 108 | 109 | This shows how to use `RadioGroup` and `RadioButton`. 110 | 111 | ## [`slider`](./slider.rs) 112 | 113 | This is a demonstration of the `SliderView`. 114 | 115 | ## [`mines`](./mines) (**Work in progress**) 116 | 117 | A larger example showing an implementation of minesweeper. 118 | 119 | ## [`window_title`](./window_title.rs) 120 | 121 | This shows how to change the terminal window title. 122 | -------------------------------------------------------------------------------- /cursive/examples/ansi.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | // Start with some text content that includes ANSI codes. 3 | // Often this could be the output of another command meant for humans. 4 | let content = include_str!("text_with_ansi_codes.txt").trim(); 5 | 6 | // Parse the content as ANSI-decorated text. 7 | let styled = cursive::utils::markup::ansi::parse(content); 8 | 9 | // Just give this to `TextView` 10 | let text_view = cursive::views::TextView::new(styled); 11 | 12 | // And make a minimal app around that. 13 | let mut siv = cursive::default(); 14 | siv.add_layer(text_view); 15 | siv.run(); 16 | } 17 | -------------------------------------------------------------------------------- /cursive/examples/assets/cities.txt: -------------------------------------------------------------------------------- 1 | Abidjan 2 | Abu Dhabi 3 | Abuja 4 | Accra 5 | Adamstown 6 | Addis Ababa 7 | Algiers 8 | Alofi 9 | Amman 10 | Amsterdam 11 | Andorra la Vella 12 | Ankara 13 | Antananarivo 14 | Apia 15 | Arbil 16 | Ashgabat 17 | Asmara 18 | Astana 19 | Asunción 20 | Athens 21 | Avarua 22 | Baghdad 23 | Baku 24 | Bamako 25 | Bandar Seri Begawan 26 | Bangkok 27 | Bangui 28 | Banjul 29 | Basseterre 30 | Beijing 31 | 北京 32 | Beirut 33 | Belgrade 34 | Belmopan 35 | Berlin 36 | Bern 37 | Bishkek 38 | Bissau 39 | Bogotá 40 | Brasília 41 | Bratislava 42 | Brazzaville 43 | Bridgetown 44 | Brussels 45 | Bucharest 46 | Budapest 47 | Buenos Aires 48 | Bujumbura 49 | Cairo 50 | Canberra 51 | Caracas 52 | Cardiff 53 | Castries 54 | Cayenne 55 | Charlotte Amalie 56 | Chișinău 57 | Cockburn Town 58 | Colombo 59 | Conakry 60 | Copenhagen 61 | Dakar 62 | Damascus 63 | Dhaka 64 | Dili 65 | Djibouti 66 | Dodoma 67 | Doha 68 | Douglas 69 | Dublin 70 | Dushanbe 71 | Edinburgh 72 | Fort-de-France 73 | Freetown 74 | Funafuti 75 | Funchal 76 | Gaborone 77 | Garoowe 78 | Gaza 79 | Georgetown 80 | Georgetown 81 | George Town 82 | Gibraltar 83 | Grozny 84 | Guatemala City 85 | Hagatna 86 | Hamilton 87 | Hanoi 88 | Harare 89 | Hargeisa 90 | Havana 91 | Helsinki 92 | Honiara 93 | Islamabad 94 | Jakarta 95 | Jamestown 96 | Jerusalem 97 | Jerusalem 98 | Kabul 99 | Kampala 100 | Kathmandu 101 | Khartoum 102 | Kiev 103 | Kigali 104 | Kilinochchi 105 | Kingston 106 | Kingston 107 | Kingstown 108 | Kinshasa 109 | Kuala Lumpur 110 | Kuwait City 111 | La Paz 112 | Las Palmas 113 | Libreville 114 | Lilongwe 115 | Lima 116 | Lisbon 117 | Ljubljana 118 | Lomé 119 | London 120 | Luanda 121 | Lusaka 122 | Luxembourg 123 | Madrid 124 | Majuro 125 | Malabo 126 | Malé 127 | Mamoudzou 128 | Managua 129 | Manama 130 | Manila 131 | Maputo 132 | Maseru 133 | Mata-Utu 134 | Mbabane 135 | Melekeok 136 | Mexico City 137 | Minsk 138 | Mogadishu 139 | Monaco 140 | Monrovia 141 | Montevideo 142 | Moroni 143 | Moscow 144 | Muscat 145 | Nairobi 146 | Nassau 147 | Naypyidaw 148 | N'Djamena 149 | New Delhi 150 | Niamey 151 | Nicosia 152 | none 153 | Nouakchott 154 | Nouméa 155 | Nuku'alofa 156 | Nuuk 157 | Oranjestad 158 | Oslo 159 | Ottawa 160 | Ouagadougou 161 | Pago Pago 162 | Palikir 163 | Panama City 164 | Papeete 165 | Paramaribo 166 | Paris 167 | Phnom Penh 168 | Plymouth 169 | Podgorica 170 | Ponta Delgada 171 | Port-au-Prince 172 | Port Louis 173 | Port Moresby 174 | Port of Spain 175 | Porto-Novo 176 | Port Vila 177 | Prague 178 | Praia 179 | Pretoria 180 | Priština 181 | Putrajaya 182 | Pyongyang 183 | Quito 184 | Rabat 185 | Ramallah 186 | Reykjavík 187 | Riga 188 | Riyadh 189 | Road Town 190 | Rome 191 | Roseau 192 | Saint-Denis 193 | Saint Helier 194 | Saint-Pierre 195 | Saipan 196 | Sanaa 197 | San Juan 198 | San Marino 199 | San Salvador 200 | Santiago 201 | Sant José 202 | Santo Domingo 203 | São Tomé 204 | Sarajevo 205 | Seoul 206 | Singapore 207 | Skopje 208 | Sofia 209 | Stanley 210 | Stepanakert 211 | St. George's 212 | St. John's 213 | Stockholm 214 | St Peter Port 215 | Sucre 216 | Sukhumi 217 | Suva 218 | Taipei 219 | Tallinn 220 | Tarawa 221 | Tashkent 222 | Tbilisi 223 | Tegucigalpa 224 | Tehran 225 | The Settlement 226 | The Valley 227 | Thimphu 228 | Tirana 229 | Tiraspol 230 | Tokyo 231 | 東京 232 | Tórshavn 233 | Tripoli 234 | Tskhinvali 235 | Tunis 236 | Ulaanbaatar 237 | Vaduz 238 | Valletta 239 | Valparaíso 240 | Vatican City 241 | Victoria 242 | Vienna 243 | Vientiane 244 | Vilnius 245 | Vitoria-Gasteiz 246 | Warsaw 247 | Washington, D.C. 248 | Wellington 249 | West Island 250 | Willemstad 251 | Windhoek 252 | Yamoussoukro 253 | Yaoundé 254 | Yaren 255 | Yerevan 256 | Zagreb 257 | -------------------------------------------------------------------------------- /cursive/examples/assets/style.toml: -------------------------------------------------------------------------------- 1 | # Every field in a theme file is optional. 2 | 3 | shadow = false 4 | borders = "outset" # Alternatives are "none" and "simple" 5 | 6 | # Base colors are red, green, blue, 7 | # cyan, magenta, yellow, white and black. 8 | [colors] 9 | # There are 3 ways to select a color: 10 | # - The 16 base colors are selected by name: 11 | # "blue", "light red", "magenta", ... 12 | # - Low-resolution colors use 3 characters, each <= 5: 13 | # "541", "003", ... 14 | # - Full-resolution colors start with '#' and can be 3 or 6 hex digits: 15 | # "#1A6", "#123456", ... 16 | 17 | # If the value is an array, the first valid 18 | # and supported color will be used. 19 | background = ["#cdf6cd", "454", "magenta"] 20 | 21 | # If the terminal doesn't support custom color (like the linux TTY), 22 | # non-base colors will be skipped. 23 | shadow = ["#222288", "blue"] 24 | view = "111" 25 | 26 | # An array with a single value has the same effect as a simple value. 27 | primary = ["white"] 28 | secondary = "#EEEEEE" 29 | tertiary = "#252521" 30 | 31 | # Hex values can use lower or uppercase. 32 | # (base color MUST be lowercase) 33 | title_primary = ["BLUE", "yellow"] # `BLUE` will be skipped. 34 | title_secondary = "#ffff55" 35 | 36 | # Lower precision values can use only 3 digits. 37 | highlight = "#F88" 38 | highlight_inactive = "#5555FF" 39 | 40 | [styles.highlight] 41 | effects = [] 42 | front = "red" 43 | back = "inherit_parent" 44 | -------------------------------------------------------------------------------- /cursive/examples/autocomplete.rs: -------------------------------------------------------------------------------- 1 | use cursive::align::HAlign; 2 | use cursive::traits::Scrollable; 3 | use cursive::view::{Nameable, Resizable}; 4 | use cursive::views::{Dialog, EditView, LinearLayout, SelectView, TextView}; 5 | use cursive::Cursive; 6 | 7 | // This example shows a way to implement a (Google-like) autocomplete search box. 8 | // Try entering "tok"! 9 | 10 | static CITIES: &str = include_str!("assets/cities.txt"); 11 | 12 | fn main() { 13 | let mut siv = cursive::default(); 14 | 15 | siv.add_layer( 16 | Dialog::around( 17 | LinearLayout::vertical() 18 | // the query box is on the top 19 | .child( 20 | EditView::new() 21 | // update results every time the query changes 22 | .on_edit(on_edit) 23 | // submit the focused (first) item of the matches 24 | .on_submit(on_submit) 25 | .with_name("query"), 26 | ) 27 | // search results below the input 28 | .child( 29 | SelectView::new() 30 | // shows all cities by default 31 | .with_all_str(CITIES.lines()) 32 | // Sets the callback for when "Enter" is pressed. 33 | .on_submit(show_next_window) 34 | // Center the text horizontally 35 | .h_align(HAlign::Center) 36 | .with_name("matches") 37 | .scrollable(), 38 | ) 39 | .fixed_height(10), 40 | ) 41 | .button("Quit", Cursive::quit) 42 | .title("Where are you from?"), 43 | ); 44 | 45 | siv.run(); 46 | } 47 | 48 | // Update results according to the query 49 | fn on_edit(siv: &mut Cursive, query: &str, _cursor: usize) { 50 | let matches = search_fn(CITIES.lines(), query); 51 | // Update the `matches` view with the filtered array of cities 52 | siv.call_on_name("matches", |v: &mut SelectView| { 53 | v.clear(); 54 | v.add_all_str(matches); 55 | }); 56 | } 57 | 58 | // Filter cities with names containing query string. You can implement your own logic here! 59 | fn search_fn<'a, 'b, T: std::iter::IntoIterator>( 60 | items: T, 61 | query: &'b str, 62 | ) -> Vec<&'a str> { 63 | items 64 | .into_iter() 65 | .filter(|&item| { 66 | let item = item.to_lowercase(); 67 | let query = query.to_lowercase(); 68 | item.contains(&query) 69 | }) 70 | .collect() 71 | } 72 | 73 | fn on_submit(siv: &mut Cursive, query: &str) { 74 | let matches = siv.find_name::("matches").unwrap(); 75 | if matches.is_empty() { 76 | // not all people live in big cities. If none of the cities in the list matches, use the value of the query. 77 | show_next_window(siv, query); 78 | } else { 79 | // pressing "Enter" without moving the focus into the `matches` view will submit the first match result 80 | let city = &*matches.selection().unwrap(); 81 | show_next_window(siv, city); 82 | }; 83 | } 84 | 85 | fn show_next_window(siv: &mut Cursive, city: &str) { 86 | siv.pop_layer(); 87 | let text = format!("{city} is a great city!"); 88 | siv.add_layer(Dialog::around(TextView::new(text)).button("Quit", |s| s.quit())); 89 | } 90 | -------------------------------------------------------------------------------- /cursive/examples/builder.rs: -------------------------------------------------------------------------------- 1 | use cursive::views::{BoxedView, Button, EditView, Panel, TextView}; 2 | 3 | // This is how we can define some global blueprints. 4 | // Here, we define a blueprint from a template. 5 | cursive::manual_blueprint!(LabeledField from { 6 | // We just need to return a cursive::builder::Config 7 | // (in practice, a serde_json::Value). 8 | // 9 | // Here we parse yaml but any other serde-supported language would work. 10 | serde_yaml::from_str(include_str!("label-view.yaml")).unwrap() 11 | }); 12 | 13 | cursive::manual_blueprint!(VSpace from { 14 | // Another similar blueprint. 15 | // 16 | // Here we embed the template in the binary with `include_str!`, 17 | // but it'd be possible as well to dynamically read a file, 18 | // load from network, ... 19 | // 20 | // Note that this code only runs when this blueprint is actually called 21 | // (when a `VSpace` view is requested). 22 | serde_yaml::from_str(include_str!("vspace.yaml")).unwrap() 23 | }); 24 | 25 | // We can also define blueprint that build arbitrary views. 26 | cursive::manual_blueprint!(Titled, |config, context| { 27 | // Manual blueprints just need to return something that implements `View`. 28 | 29 | // Fetch a string from the config 30 | let title: String = context.resolve(&config["title"])?; 31 | 32 | // Build a view from the other field 33 | let child = context.build(&config["child"])?; 34 | 35 | // And return some view 36 | Ok(Panel::new(child).title(title)) 37 | }); 38 | 39 | // Or we can use a declarative blueprint definition 40 | #[cursive::blueprint(Panel::new(child), name = "WithTitle")] 41 | struct Blueprint { 42 | // Some fields are used in the initialization expression above. 43 | child: BoxedView, 44 | 45 | // Additional fields use `set_*` setters. 46 | title: String, 47 | } 48 | 49 | fn main() { 50 | cursive::logger::init(); 51 | 52 | // We will build a view from a template (possibly written by another team) 53 | let mut context = cursive::builder::Context::new(); 54 | 55 | // The only thing we need to know are the variables it expects. 56 | // 57 | // In our case, it's a title string, and two callbacks. 58 | context.store("title", String::from("Config-driven layout example")); 59 | 60 | // Callbacks are tricky to store and need the exact closure type. 61 | // Here we use a helper function, `on_edit_cb`, to wrap a closure in the proper type. 62 | context.store("on_edit", EditView::on_edit_cb(on_edit_callback)); 63 | 64 | // Each callback-taking function has a matching helper. 65 | context.store("randomize", Button::new_cb(randomize)); 66 | 67 | // Load the template - here it's a yaml file. 68 | const CONFIG: &str = include_str!("builder.yaml"); 69 | let config = serde_yaml::from_str(CONFIG).unwrap(); 70 | 71 | // And build the view 72 | let view = context.build(&config).unwrap_or_else(|e| { 73 | panic!("{e:#?}"); 74 | }); 75 | 76 | let mut siv = cursive::default(); 77 | siv.add_global_callback('~', cursive::Cursive::toggle_debug_console); 78 | siv.screen_mut().add_transparent_layer(view); 79 | siv.run(); 80 | } 81 | 82 | fn randomize(s: &mut cursive::Cursive) { 83 | let cb = s 84 | .call_on_name("edit", |e: &mut EditView| e.set_content("Not so random!")) 85 | .unwrap(); 86 | cb(s); 87 | } 88 | 89 | // Just a regular callback for EditView::on_edit 90 | fn on_edit_callback(siv: &mut cursive::Cursive, text: &str, cursor: usize) { 91 | siv.call_on_name("status", |v: &mut TextView| { 92 | let spaces: String = " ".repeat(cursor + "You wrote `".len()); 93 | v.set_content(format!("You wrote `{text}`\n{spaces}^")); 94 | }) 95 | .unwrap(); 96 | } 97 | -------------------------------------------------------------------------------- /cursive/examples/builder.yaml: -------------------------------------------------------------------------------- 1 | # This is the config for a small app 2 | # The top-level view will be a dialog. 3 | Dialog: 4 | # Here we use the `title` variable. 5 | # Users of this template will have to specify a value for that before rendering it. 6 | title: 7 | # We can use some of the builtin functions 8 | $concat: 9 | - "Title: " 10 | - $title 11 | content: 12 | LinearLayout: 13 | children: 14 | # Some view can take a single value as parameter. 15 | - TextView: 16 | $cursup: /blue+underline{Fancy} /bold+red{prompt} 17 | # Some view don't even need any parameter at all. 18 | - DummyView 19 | # This is a custom blueprint defined in our example. 20 | - Titled: 21 | title: Edit area 22 | child: 23 | # This is a custom view defined using a separate template file. 24 | LabeledField: 25 | label: "Please write:" 26 | on_edit: $on_edit 27 | name: edit 28 | - Button: 29 | label: Randomize 30 | callback: $randomize 31 | # This is another one of our custom views 32 | - VSpace: 2 33 | # Give the TextView a name so we can update it 34 | - TextView: 35 | # Every view can have a `with` field with a list of wrappers. 36 | with: 37 | # Here we only give this view a name: "status". 38 | - name: status 39 | with: 40 | # Other examples of wrappers. 41 | - padding: 1 42 | # Some wrappers don't take any parameter. 43 | - full_width 44 | - fixed_height: 11 45 | buttons: 46 | - Info: 47 | $EditView.with_content: 48 | # This special callback fetches the content from a named EditView, 49 | # then runs another callback with the result. 50 | name: edit 51 | callback: 52 | # In the callback, `$content` will be set to the EditView's content. 53 | $Dialog.info: 54 | # We can combine functions to prepare a markup string, then parse it. 55 | $cursup: 56 | $concat: 57 | - "This is an example of template-driven callback.\nThe current content is: `" 58 | - "/bold{" 59 | - $content 60 | - "}" 61 | - "`." 62 | # This is another builtin callback 63 | - Exit: $Cursive.quit 64 | with: 65 | - circular_focus: left_right 66 | - layer 67 | - shadow 68 | - padding: 69 | # Some wrappers take a full object as parameter. 70 | left: 2 71 | right: 2 72 | top: 1 73 | bottom: 1 74 | - gradient: 75 | angled: 76 | angle_deg: 45 77 | gradient: 78 | - "#A0A0A0" 79 | - "#505050" 80 | -------------------------------------------------------------------------------- /cursive/examples/colored_text.rs: -------------------------------------------------------------------------------- 1 | use crossterm::style::Stylize; 2 | use cursive_core::style::BaseColor::{Black, Red}; 3 | use cursive_core::style::ColorStyle; 4 | use cursive_core::theme::Style; 5 | use cursive_core::utils::markup::{cursup, StyledString}; 6 | use cursive_core::views::{Dialog, LinearLayout, TextView}; 7 | 8 | fn main() { 9 | // Coloring text. 10 | let crossterm_colored = crossterm_coloring_into_ansi("Crossterm colored text."); 11 | 12 | let cursive_parsed_ansi = cursive_parse_ansi("Parsed ANSI"); 13 | 14 | let cursive_single_span = single_style_span("Cursive StyledString::single_span"); 15 | 16 | let cursup_markup = cursup_markup("/blue+bold{Cursup} /yellow+bold{markup}"); 17 | 18 | // Removing styles. 19 | 20 | let cleared_by_iterating: String = iterating_spans(&cursive_parsed_ansi); 21 | 22 | let cleared_by_canonicalize: String = canonicalize_clear(&cursive_single_span); 23 | 24 | // Minimal application for text output 25 | let mut siv = cursive::default(); 26 | siv.add_layer( 27 | Dialog::new() 28 | .content( 29 | LinearLayout::vertical() 30 | .child(TextView::new(crossterm_colored)) 31 | .child(TextView::new(cursive_parsed_ansi)) 32 | .child(TextView::new(cursive_single_span)) 33 | .child(TextView::new(cursup_markup)) 34 | .child(TextView::new(cleared_by_iterating)) 35 | .child(TextView::new(cleared_by_canonicalize)), 36 | ) 37 | .button("Quit!", |s| s.quit()), 38 | ); 39 | siv.run(); 40 | } 41 | 42 | // Crossterm function for text styling and casting it to a string. 43 | // Results in raw text with ANSI codes. 44 | fn crossterm_coloring_into_ansi(str: &str) -> String { 45 | str.red().to_string() 46 | } 47 | 48 | // Parsing ansi into StyledString 49 | // https://docs.rs/cursive/latest/cursive/utils/markup/ansi/fn.parse.html 50 | fn cursive_parse_ansi(str: &str) -> StyledString { 51 | cursive::utils::markup::ansi::parse(str.red().to_string()) 52 | } 53 | 54 | // Building cursive-native StyledString with StyledString::single_span (single style for entire string) 55 | // https://docs.rs/cursive/latest/cursive/utils/markup/type.StyledString.html#method.single_span 56 | fn single_style_span(str: &str) -> StyledString { 57 | StyledString::single_span( 58 | str, 59 | Style { 60 | effects: Default::default(), 61 | color: ColorStyle::new(Red, Black), 62 | }, 63 | ) 64 | } 65 | 66 | // Cursup, a simple markup language. 67 | // https://docs.rs/cursive/latest/cursive/utils/markup/cursup/index.html 68 | fn cursup_markup(str: &str) -> StyledString { 69 | cursup::parse(str) 70 | } 71 | 72 | // Iterating on the spans, and accumulating the span text content, ignoring the styles. 73 | fn iterating_spans(str: &StyledString) -> String { 74 | str.spans().map(|span| span.content).collect() 75 | } 76 | 77 | // Use compact or canonicalize, which rebuilds the source to only include visible text (no markup), and then get the source. 78 | // Note that this technically modifies the StyledString, and requires mutable access. 79 | // https://docs.rs/cursive/latest/cursive/utils/span/struct.SpannedString.html#method.canonicalize 80 | // https://docs.rs/cursive/latest/cursive/utils/span/struct.SpannedString.html#method.compact 81 | fn canonicalize_clear(str: &StyledString) -> String { 82 | let mut canonicalize_clear = str.clone(); 83 | canonicalize_clear.canonicalize(); 84 | canonicalize_clear.source().to_string() 85 | } 86 | -------------------------------------------------------------------------------- /cursive/examples/colors.rs: -------------------------------------------------------------------------------- 1 | use cursive::style::{Color, ColorStyle}; 2 | use cursive::view::Resizable; 3 | use cursive::views::Canvas; 4 | use cursive::Printer; 5 | 6 | // This example will draw a colored square with a gradient. 7 | // 8 | // We'll use a Canvas, which lets us only define a draw method. 9 | // 10 | // We will combine 2 gradients: one for the background, 11 | // and one for the foreground. 12 | // 13 | // Note: color reproduction is not as good on all backends. 14 | // termion can do full 16M true colors, but ncurses is currently limited to 15 | // 256 colors. 16 | 17 | fn main() { 18 | // Start as usual 19 | let mut siv = cursive::default(); 20 | siv.add_global_callback('q', |s| s.quit()); 21 | 22 | // Canvas lets us easily override any method. 23 | // Canvas can have states, but we don't need any here, so we use `()`. 24 | siv.add_layer(Canvas::new(()).with_draw(draw).fixed_size((20, 10))); 25 | 26 | siv.run(); 27 | } 28 | 29 | /// Method used to draw the cube. 30 | /// 31 | /// This takes as input the Canvas state and a printer. 32 | fn draw(_: &(), p: &Printer) { 33 | // We use the view size to calibrate the color 34 | let x_max = p.size.x as u8; 35 | let y_max = p.size.y as u8; 36 | 37 | // Print each cell individually 38 | for x in 0..x_max { 39 | for y in 0..y_max { 40 | // We'll use a different style for each cell 41 | let style = ColorStyle::new( 42 | front_color(x, y, x_max, y_max), 43 | back_color(x, y, x_max, y_max), 44 | ); 45 | 46 | p.with_color(style, |printer| { 47 | printer.print((x, y), "+"); 48 | }); 49 | } 50 | } 51 | } 52 | 53 | // Gradient for the front color 54 | fn front_color(x: u8, y: u8, x_max: u8, y_max: u8) -> Color { 55 | // We return a full 24-bits RGB color, but some backends 56 | // will project it to a 256-colors palette. 57 | Color::Rgb( 58 | x * (255 / x_max), 59 | y * (255 / y_max), 60 | (x + 2 * y) * (255 / (x_max + 2 * y_max)), 61 | ) 62 | } 63 | 64 | // Gradient for the background color 65 | fn back_color(x: u8, y: u8, x_max: u8, y_max: u8) -> Color { 66 | // Let's try to have a gradient in a different direction than the front color. 67 | Color::Rgb( 68 | 128 + (2 * y_max + x - 2 * y) * (128 / (x_max + 2 * y_max)), 69 | 255 - y * (255 / y_max), 70 | 255 - x * (255 / x_max), 71 | ) 72 | } 73 | -------------------------------------------------------------------------------- /cursive/examples/ctrl_c.rs: -------------------------------------------------------------------------------- 1 | use cursive::views; 2 | 3 | fn main() { 4 | let mut siv = cursive::default(); 5 | 6 | siv.clear_global_callbacks(cursive::event::Event::CtrlChar('c')); 7 | 8 | siv.set_on_pre_event(cursive::event::Event::CtrlChar('c'), |s| { 9 | s.add_layer( 10 | views::Dialog::text("Do you want to quit?") 11 | .button("Yes", |s| s.quit()) 12 | .button("No", |s| { 13 | s.pop_layer(); 14 | }), 15 | ); 16 | }); 17 | 18 | siv.add_layer(views::Dialog::text("Try pressing Ctrl-C!")); 19 | 20 | siv.run(); 21 | } 22 | -------------------------------------------------------------------------------- /cursive/examples/debug_console.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | // Initialize the cursive logger. 3 | cursive::logger::init(); 4 | 5 | // Use some logging macros from the `log` crate. 6 | log::error!("Something serious probably happened!"); 7 | log::warn!("Or did it?"); 8 | log::debug!("Logger initialized."); 9 | log::info!("Starting!"); 10 | 11 | let mut siv = cursive::default(); 12 | siv.add_layer(cursive::views::Dialog::text( 13 | "Press ~ to open the console.\nPress l to generate logs.\nPress q to quit.", 14 | )); 15 | siv.add_global_callback('q', cursive::Cursive::quit); 16 | siv.add_global_callback('~', cursive::Cursive::toggle_debug_console); 17 | 18 | siv.add_global_callback('l', |_| log::trace!("Wooo")); 19 | 20 | siv.run(); 21 | } 22 | -------------------------------------------------------------------------------- /cursive/examples/dialog.rs: -------------------------------------------------------------------------------- 1 | use cursive::{ 2 | views::{CircularFocus, Dialog, TextView}, 3 | With as _, 4 | }; 5 | 6 | fn main() { 7 | // Creates the cursive root - required for every application. 8 | let mut siv = cursive::default(); 9 | 10 | // Creates a dialog with a single "Quit" button 11 | siv.add_layer( 12 | // Most views can be configured in a chainable way 13 | Dialog::around(TextView::new("Hello Dialog!")) 14 | .title("Cursive") 15 | .button("Foo", |_s| ()) 16 | .button("Quit", |s| s.quit()) 17 | .wrap_with(CircularFocus::new) 18 | .wrap_tab(), 19 | ); 20 | 21 | // Starts the event loop. 22 | siv.run(); 23 | } 24 | -------------------------------------------------------------------------------- /cursive/examples/edit.rs: -------------------------------------------------------------------------------- 1 | use cursive::traits::*; 2 | use cursive::views::{Dialog, EditView, TextView}; 3 | use cursive::Cursive; 4 | 5 | fn main() { 6 | let mut siv = cursive::default(); 7 | 8 | // Create a dialog with an edit text and a button. 9 | // The user can either hit the button, 10 | // or press Enter on the edit text. 11 | siv.add_layer( 12 | Dialog::new() 13 | .title("Enter your name") 14 | // Padding is (left, right, top, bottom) 15 | .padding_lrtb(1, 1, 1, 0) 16 | .content( 17 | EditView::new() 18 | // Call `show_popup` when the user presses `Enter` 19 | .on_submit(show_popup) 20 | // Give the `EditView` a name so we can refer to it later. 21 | .with_name("name") 22 | // Wrap this in a `ResizedView` with a fixed width. 23 | // Do this _after_ `with_name` or the name will point to the 24 | // `ResizedView` instead of `EditView`! 25 | .fixed_width(20), 26 | ) 27 | .button("Ok", |s| { 28 | // This will run the given closure, *ONLY* if a view with the 29 | // correct type and the given name is found. 30 | let name = s 31 | .call_on_name("name", |view: &mut EditView| { 32 | // We can return content from the closure! 33 | view.get_content() 34 | }) 35 | .unwrap(); 36 | 37 | // Run the next step 38 | show_popup(s, &name); 39 | }), 40 | ); 41 | 42 | siv.run(); 43 | } 44 | 45 | // This will replace the current layer with a new popup. 46 | // If the name is empty, we'll show an error message instead. 47 | fn show_popup(s: &mut Cursive, name: &str) { 48 | if name.is_empty() { 49 | // Try again as many times as we need! 50 | s.add_layer(Dialog::info("Please enter a name!")); 51 | } else { 52 | let content = format!("Hello {name}!"); 53 | // Remove the initial popup 54 | s.pop_layer(); 55 | // And put a new one instead 56 | s.add_layer(Dialog::around(TextView::new(content)).button("Quit", |s| s.quit())); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /cursive/examples/fixed_layout.rs: -------------------------------------------------------------------------------- 1 | use cursive::{ 2 | views::{Button, FixedLayout, TextView}, 3 | Rect, 4 | }; 5 | fn main() { 6 | let mut siv = cursive::default(); 7 | 8 | siv.add_layer( 9 | cursive::views::Dialog::around( 10 | FixedLayout::new() 11 | .child(Rect::from_size((0, 0), (1, 1)), TextView::new("/")) 12 | .child(Rect::from_size((14, 0), (1, 1)), TextView::new(r"\")) 13 | .child(Rect::from_size((0, 2), (1, 1)), TextView::new(r"\")) 14 | .child(Rect::from_size((14, 2), (1, 1)), TextView::new("/")) 15 | .child( 16 | Rect::from_size((2, 1), (11, 1)), 17 | Button::new("Click me!", |s| s.quit()), 18 | ), 19 | ) 20 | .button("Quit", |s| s.quit()), 21 | ); 22 | 23 | siv.run(); 24 | } 25 | -------------------------------------------------------------------------------- /cursive/examples/focus.rs: -------------------------------------------------------------------------------- 1 | use cursive::traits::*; 2 | 3 | fn main() { 4 | let mut siv = cursive::default(); 5 | 6 | siv.add_layer( 7 | cursive::views::Dialog::new().content( 8 | cursive::views::LinearLayout::vertical() 9 | .child(cursive::views::TextView::new("Focused").with_name("text")) 10 | .child( 11 | cursive::views::EditView::new() 12 | .wrap_with(cursive::views::FocusTracker::new) 13 | .on_focus(|_| { 14 | cursive::event::EventResult::with_cb(|s| { 15 | s.call_on_name("text", |v: &mut cursive::views::TextView| { 16 | v.set_content("Focused"); 17 | }); 18 | }) 19 | }) 20 | .on_focus_lost(|_| { 21 | cursive::event::EventResult::with_cb(|s| { 22 | s.call_on_name("text", |v: &mut cursive::views::TextView| { 23 | v.set_content("Focus lost"); 24 | }); 25 | }) 26 | }), 27 | ) 28 | .child(cursive::views::Button::new("Quit", |s| s.quit())) 29 | .fixed_width(20), 30 | ), 31 | ); 32 | 33 | siv.run(); 34 | } 35 | -------------------------------------------------------------------------------- /cursive/examples/gradient.rs: -------------------------------------------------------------------------------- 1 | use cursive::event::EventResult; 2 | use cursive::style::{ 3 | gradient::{Angled, Bilinear, Linear, Radial}, 4 | Rgb, 5 | }; 6 | use cursive::traits::*; 7 | use cursive::utils::markup::gradient; 8 | use cursive::views::{Dialog, GradientView, OnEventView, PaddedView, TextView}; 9 | use cursive::XY; 10 | 11 | fn main() { 12 | let mut siv = cursive::default(); 13 | 14 | let text = "So many colors! So little time! Let's go through them all!"; 15 | let text = gradient::decorate_front(text, (Rgb::black(), Rgb::white())); 16 | let text = gradient::decorate_back(text, Linear::rainbow()); 17 | 18 | // Add a simple view 19 | siv.add_layer(Dialog::new().content(TextView::new(text)).button( 20 | gradient::decorate_back("Moar", Linear::rainbow()), 21 | show_more, 22 | )); 23 | 24 | // Run the event loop 25 | siv.run(); 26 | } 27 | 28 | fn show_more(c: &mut cursive::Cursive) { 29 | let dialog = Dialog::new() 30 | .button("Moar", show_more_2) 31 | .fixed_size((40, 20)); 32 | 33 | let interpolator = Radial { 34 | center: XY::new(-0.1, -0.1), 35 | gradient: Linear::evenly_spaced(&[Rgb::from(0xFFFFFF), Rgb::from(0x000000)]), 36 | }; 37 | 38 | c.pop_layer(); 39 | c.add_layer(GradientView::new(dialog, interpolator)); 40 | } 41 | 42 | fn show_more_2(c: &mut cursive::Cursive) { 43 | let dialog = Dialog::new() 44 | .content(PaddedView::lrtb( 45 | 0, 46 | 0, 47 | 8, 48 | 0, 49 | TextView::new("Press Q or E to rotate the gradient"), 50 | )) 51 | .button("Moar", show_more_3) 52 | .fixed_size((40, 20)); 53 | 54 | let interpolator = Angled { 55 | angle_rad: 0f32, 56 | gradient: Linear::evenly_spaced(&[Rgb::from(0xFFFFFF), Rgb::from(0x000000)]), 57 | }; 58 | c.pop_layer(); 59 | c.add_layer( 60 | OnEventView::new(GradientView::new(dialog, interpolator)) 61 | .on_event_inner('q', |g, _| { 62 | g.interpolator_mut().angle_rad += 0.1; 63 | Some(EventResult::Consumed(None)) 64 | }) 65 | .on_event_inner('e', |g, _| { 66 | g.interpolator_mut().angle_rad -= 0.1; 67 | Some(EventResult::Consumed(None)) 68 | }), 69 | ); 70 | } 71 | 72 | fn show_more_3(c: &mut cursive::Cursive) { 73 | let dialog = Dialog::new() 74 | .button("Quit", |s| s.quit()) 75 | .fixed_size((40, 20)); 76 | 77 | let interpolator = Bilinear { 78 | top_left: Rgb::new(255, 0, 0).as_f32(), 79 | top_right: Rgb::new(255, 255, 0).as_f32(), 80 | bottom_left: Rgb::new(127, 255, 255).as_f32(), 81 | bottom_right: Rgb::new(0, 0, 0).as_f32(), 82 | }; 83 | c.pop_layer(); 84 | c.add_layer(GradientView::new(dialog, interpolator)); 85 | } 86 | -------------------------------------------------------------------------------- /cursive/examples/hello_world.rs: -------------------------------------------------------------------------------- 1 | use cursive::views::TextView; 2 | use cursive::Cursive; 3 | 4 | fn main() { 5 | let mut siv = cursive::default(); 6 | 7 | // We can quit by pressing `q` 8 | siv.add_global_callback('q', Cursive::quit); 9 | 10 | // Add a simple view 11 | siv.add_layer(TextView::new( 12 | "Hello World!\n\ 13 | Press q to quit the application.", 14 | )); 15 | 16 | // Run the event loop 17 | siv.run(); 18 | } 19 | -------------------------------------------------------------------------------- /cursive/examples/key_codes.rs: -------------------------------------------------------------------------------- 1 | use cursive::event::{Event, EventResult}; 2 | use cursive::traits::*; 3 | use cursive::Printer; 4 | 5 | // This example define a custom view that prints any event it receives. 6 | // This is a handy way to check the input received by cursive. 7 | 8 | fn main() { 9 | let mut siv = cursive::default(); 10 | siv.add_layer(KeyCodeView::new(10).full_width().fixed_height(10)); 11 | 12 | siv.run(); 13 | } 14 | 15 | // Our view will have a small history of the last events. 16 | struct KeyCodeView { 17 | history: Vec, 18 | size: usize, 19 | } 20 | 21 | impl KeyCodeView { 22 | fn new(size: usize) -> Self { 23 | KeyCodeView { 24 | history: Vec::new(), 25 | size, 26 | } 27 | } 28 | } 29 | 30 | // Let's implement the `View` trait. 31 | // `View` contains many methods, but only a few are required. 32 | impl View for KeyCodeView { 33 | fn draw(&self, printer: &Printer) { 34 | // We simply draw every event from the history. 35 | for (y, line) in self.history.iter().enumerate() { 36 | printer.print((0, y), line); 37 | } 38 | } 39 | 40 | fn on_event(&mut self, event: Event) -> EventResult { 41 | // Each line will be a debug-format of the event. 42 | let line = format!("{event:?}"); 43 | self.history.push(line); 44 | 45 | // Keep a fixed-sized history. 46 | while self.history.len() > self.size { 47 | self.history.remove(0); 48 | } 49 | 50 | // No need to return any callback. 51 | EventResult::Consumed(None) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /cursive/examples/label-view.yaml: -------------------------------------------------------------------------------- 1 | # Definition for a labeled view 2 | LinearLayout: 3 | orientation: horizontal 4 | children: 5 | # We can use variables here - they will be filled from entries in the config. 6 | - TextView: $label 7 | - DummyView 8 | - EditView: 9 | on_edit: $on_edit 10 | with: 11 | - name: $name 12 | - full_width 13 | -------------------------------------------------------------------------------- /cursive/examples/linear.rs: -------------------------------------------------------------------------------- 1 | use cursive::align::HAlign; 2 | use cursive::traits::*; 3 | use cursive::views::{Dialog, DummyView, LinearLayout, TextView}; 4 | 5 | // This example uses a LinearLayout to stick multiple views next to each other. 6 | 7 | fn main() { 8 | let mut siv = cursive::default(); 9 | 10 | // Some description text. We want it to be long, but not _too_ long. 11 | let text = "This is a very simple example of linear layout. Two views \ 12 | are present, a short title above, and this text. The text \ 13 | has a fixed width, and the title is centered horizontally."; 14 | 15 | // We'll create a dialog with a TextView serving as a title 16 | siv.add_layer( 17 | Dialog::around( 18 | LinearLayout::vertical() 19 | .child(TextView::new("Title").h_align(HAlign::Center)) 20 | // Use a DummyView as spacer 21 | .child(DummyView.fixed_height(1)) 22 | // Disabling scrollable means the view cannot shrink. 23 | .child(TextView::new(text)) 24 | // The other views will share the remaining space. 25 | .child(TextView::new(text).scrollable()) 26 | .child(TextView::new(text).scrollable()) 27 | .child(TextView::new(text).scrollable()) 28 | .fixed_width(30), 29 | ) 30 | .button("Quit", |s| s.quit()) 31 | .h_align(HAlign::Center), 32 | ); 33 | 34 | siv.run(); 35 | } 36 | -------------------------------------------------------------------------------- /cursive/examples/list_view.rs: -------------------------------------------------------------------------------- 1 | use cursive::{ 2 | traits::*, 3 | views::{Checkbox, Dialog, EditView, LinearLayout, ListView, SelectView, TextArea, TextView}, 4 | }; 5 | 6 | // This example uses a ListView. 7 | // 8 | // ListView can be used to build forms, with a list of inputs. 9 | 10 | fn main() { 11 | let mut siv = cursive::default(); 12 | 13 | siv.add_layer( 14 | Dialog::new() 15 | .title("Please fill out this form") 16 | .button("Ok", |s| s.quit()) 17 | .content( 18 | ListView::new() 19 | // Each child is a single-line view with a label 20 | .child("Name", EditView::new().fixed_width(10)) 21 | .child("Presentation", TextArea::new().min_height(4)) 22 | .child( 23 | "Receive spam?", 24 | Checkbox::new().on_change(|s, checked| { 25 | // Enable/Disable the next field depending on this checkbox 26 | for name in &["email1", "email2"] { 27 | s.call_on_name(name, |view: &mut EditView| { 28 | view.set_enabled(checked) 29 | }); 30 | if checked { 31 | s.focus_name("email1").unwrap(); 32 | } 33 | } 34 | }), 35 | ) 36 | .child( 37 | "Email", 38 | // Each child must have a height of 1 line, 39 | // but we can still combine multiple views! 40 | LinearLayout::horizontal() 41 | .child( 42 | EditView::new() 43 | .disabled() 44 | .with_name("email1") 45 | .fixed_width(15), 46 | ) 47 | .child(TextView::new("@")) 48 | .child( 49 | EditView::new() 50 | .disabled() 51 | .with_name("email2") 52 | .fixed_width(10), 53 | ), 54 | ) 55 | // Delimiter currently are just a blank line 56 | .delimiter() 57 | .child( 58 | "Age", 59 | // Popup-mode SelectView are small enough to fit here 60 | SelectView::new() 61 | .popup() 62 | .item_str("0-18") 63 | .item_str("19-30") 64 | .item_str("31-40") 65 | .item_str("41+"), 66 | ) 67 | .with(|list| { 68 | // We can also add children procedurally 69 | for i in 0..50 { 70 | list.add_child( 71 | &format!("Item {i}"), 72 | EditView::new(), 73 | ); 74 | } 75 | }) 76 | .scrollable(), 77 | ), 78 | ); 79 | 80 | siv.run(); 81 | } 82 | -------------------------------------------------------------------------------- /cursive/examples/logs.rs: -------------------------------------------------------------------------------- 1 | use crossbeam_channel::{unbounded, Receiver, Sender}; 2 | use cursive::traits::*; 3 | use cursive::Vec2; 4 | use cursive::{Cursive, Printer}; 5 | use std::collections::VecDeque; 6 | use std::thread; 7 | use std::time::Duration; 8 | 9 | // This example will print a stream of logs generated from a separate thread. 10 | // 11 | // We will use a custom view using a channel to receive data asynchronously. 12 | 13 | fn main() { 14 | // As usual, create the Cursive root 15 | let mut siv = cursive::default(); 16 | 17 | let cb_sink = siv.cb_sink().clone(); 18 | 19 | // We want to refresh the page even when no input is given. 20 | siv.add_global_callback('q', |s| s.quit()); 21 | 22 | // A channel will communicate data from our running task to the UI. 23 | let (tx, rx) = unbounded(); 24 | 25 | // Generate data in a separate thread. 26 | thread::spawn(move || { 27 | generate_logs(&tx, cb_sink); 28 | }); 29 | 30 | // And sets the view to read from the other end of the channel. 31 | siv.add_layer(BufferView::new(200, rx).full_screen()); 32 | 33 | siv.run(); 34 | } 35 | 36 | // We will only simulate log generation here. 37 | // In real life, this may come from a running task, a separate process, ... 38 | fn generate_logs(tx: &Sender, cb_sink: cursive::CbSink) { 39 | let mut i = 1; 40 | loop { 41 | let line = format!("Interesting log line {i}"); 42 | i += 1; 43 | // The send will fail when the other side is dropped. 44 | // (When the application ends). 45 | if tx.send(line).is_err() { 46 | return; 47 | } 48 | cb_sink.send(Box::new(Cursive::noop)).unwrap(); 49 | thread::sleep(Duration::from_millis(30)); 50 | } 51 | } 52 | 53 | // Let's define a buffer view, that shows the last lines from a stream. 54 | struct BufferView { 55 | // We'll use a ring buffer 56 | buffer: VecDeque, 57 | // Receiving end of the stream 58 | rx: Receiver, 59 | } 60 | 61 | impl BufferView { 62 | // Creates a new view with the given buffer size 63 | fn new(size: usize, rx: Receiver) -> Self { 64 | let mut buffer = VecDeque::new(); 65 | buffer.resize(size, String::new()); 66 | BufferView { buffer, rx } 67 | } 68 | 69 | // Reads available data from the stream into the buffer 70 | fn update(&mut self) { 71 | // Add each available line to the end of the buffer. 72 | while let Ok(line) = self.rx.try_recv() { 73 | self.buffer.push_back(line); 74 | self.buffer.pop_front(); 75 | } 76 | } 77 | } 78 | 79 | impl View for BufferView { 80 | fn layout(&mut self, _: Vec2) { 81 | // Before drawing, we'll want to update the buffer 82 | self.update(); 83 | } 84 | 85 | fn draw(&self, printer: &Printer) { 86 | // Print the end of the buffer 87 | for (i, line) in self.buffer.iter().rev().take(printer.size.y).enumerate() { 88 | printer.print((0, printer.size.y - 1 - i), line); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /cursive/examples/lorem.rs: -------------------------------------------------------------------------------- 1 | use cursive::{ 2 | align::HAlign, 3 | event::{EventResult, Key}, 4 | traits::With, 5 | view::{scroll::Scroller, Scrollable}, 6 | views::{Dialog, OnEventView, Panel, TextView}, 7 | }; 8 | 9 | fn main() { 10 | // Read some long text from a file. 11 | let content = include_str!("assets/lorem.txt"); 12 | 13 | let mut siv = cursive::default(); 14 | 15 | // We can quit by pressing q 16 | siv.add_global_callback('q', |s| s.quit()); 17 | 18 | // The text is too long to fit on a line, so the view will wrap lines, 19 | // and will adapt to the terminal size. 20 | siv.add_fullscreen_layer( 21 | Dialog::around(Panel::new( 22 | TextView::new(content) 23 | .scrollable() 24 | .wrap_with(OnEventView::new) 25 | .on_pre_event_inner(Key::PageUp, |v, _| { 26 | let scroller = v.get_scroller_mut(); 27 | if scroller.can_scroll_up() { 28 | scroller.scroll_up( 29 | scroller.last_outer_size().y.saturating_sub(1), 30 | ); 31 | } 32 | Some(EventResult::Consumed(None)) 33 | }) 34 | .on_pre_event_inner(Key::PageDown, |v, _| { 35 | let scroller = v.get_scroller_mut(); 36 | if scroller.can_scroll_down() { 37 | scroller.scroll_down( 38 | scroller.last_outer_size().y.saturating_sub(1), 39 | ); 40 | } 41 | Some(EventResult::Consumed(None)) 42 | }), 43 | )) 44 | .title("Unicode and wide-character support") 45 | // This is the alignment for the button 46 | .h_align(HAlign::Center) 47 | .button("Quit", |s| s.quit()), 48 | ); 49 | // Show a popup on top of the view. 50 | siv.add_layer(Dialog::info( 51 | "Try resizing the terminal!\n(Press 'q' to \ 52 | quit when you're done.)", 53 | )); 54 | 55 | siv.run(); 56 | } 57 | -------------------------------------------------------------------------------- /cursive/examples/markup.rs: -------------------------------------------------------------------------------- 1 | use cursive::style::BaseColor; 2 | use cursive::style::Color; 3 | use cursive::style::Effect; 4 | use cursive::style::Style; 5 | use cursive::utils::markup::StyledString; 6 | use cursive::views::{Dialog, TextView}; 7 | 8 | fn main() { 9 | let mut siv = cursive::default(); 10 | 11 | let mut styled = StyledString::plain("Isn't "); 12 | styled.append(StyledString::styled("that ", Color::Dark(BaseColor::Red))); 13 | styled.append(StyledString::styled( 14 | "cool?", 15 | Style::from(Color::Light(BaseColor::Blue)).combine(Effect::Bold), 16 | )); 17 | 18 | // TextView can natively accept StyledString. 19 | siv.add_layer(Dialog::around(TextView::new(styled)).button("Hell yeah!", |s| s.quit())); 20 | 21 | siv.run(); 22 | } 23 | -------------------------------------------------------------------------------- /cursive/examples/menubar.rs: -------------------------------------------------------------------------------- 1 | use cursive::{event::Key, menu, traits::*, views::Dialog}; 2 | use std::sync::atomic::{AtomicUsize, Ordering}; 3 | 4 | // This examples shows how to configure and use a menubar at the top of the 5 | // application. 6 | 7 | fn main() { 8 | let mut siv = cursive::default(); 9 | 10 | // We'll use a counter to name new files. 11 | let counter = AtomicUsize::new(1); 12 | 13 | // The menubar is a list of (label, menu tree) pairs. 14 | siv.menubar() 15 | // We add a new "File" tree 16 | .add_subtree( 17 | "File", 18 | menu::Tree::new() 19 | // Trees are made of leaves, with are directly actionable... 20 | .leaf("New", move |s| { 21 | // Here we use the counter to add an entry 22 | // in the list of "Recent" items. 23 | let i = counter.fetch_add(1, Ordering::Relaxed); 24 | let filename = format!("New {i}"); 25 | s.menubar() 26 | .find_subtree("File") 27 | .unwrap() 28 | .find_subtree("Recent") 29 | .unwrap() 30 | .insert_leaf(0, filename, |_| ()); 31 | 32 | s.add_layer(Dialog::info("New file!")); 33 | }) 34 | // ... and of sub-trees, which open up when selected. 35 | .subtree( 36 | "Recent", 37 | // The `.with()` method can help when running loops 38 | // within builder patterns. 39 | menu::Tree::new().with(|tree| { 40 | for i in 1..100 { 41 | // We don't actually do anything here, 42 | // but you could! 43 | tree.add_item(menu::Item::leaf(format!("Item {i}"), |_| ()).with(|s| { 44 | if i % 5 == 0 { s.disable(); } 45 | })) 46 | } 47 | }), 48 | ) 49 | // Delimiter are simple lines between items, 50 | // and cannot be selected. 51 | .delimiter() 52 | .with(|tree| { 53 | for i in 1..10 { 54 | tree.add_leaf(format!("Option {i}"), |_| ()); 55 | } 56 | }), 57 | ) 58 | .add_subtree( 59 | "Help", 60 | menu::Tree::new() 61 | .subtree( 62 | "Help", 63 | menu::Tree::new() 64 | .leaf("General", |s| { 65 | s.add_layer(Dialog::info("Help message!")) 66 | }) 67 | .leaf("Online", |s| { 68 | let text = "Google it yourself!\n\ 69 | Kids, these days..."; 70 | s.add_layer(Dialog::info(text)) 71 | }), 72 | ) 73 | .leaf("About", |s| { 74 | s.add_layer(Dialog::info("Cursive v0.0.0")) 75 | }), 76 | ) 77 | .add_delimiter() 78 | .add_leaf("Quit", |s| s.quit()); 79 | 80 | // When `autohide` is on (default), the menu only appears when active. 81 | // Turning it off will leave the menu always visible. 82 | // Try uncommenting this line! 83 | 84 | // siv.set_autohide_menu(false); 85 | 86 | siv.add_global_callback(Key::Esc, |s| s.select_menubar()); 87 | 88 | siv.add_layer(Dialog::text("Hit to show the menu!")); 89 | 90 | siv.run(); 91 | } 92 | -------------------------------------------------------------------------------- /cursive/examples/menubar_styles.rs: -------------------------------------------------------------------------------- 1 | use cursive::style::{BaseColor, Color, Effect, Style}; 2 | use cursive::utils::markup::StyledString; 3 | use cursive::{menu, traits::*}; 4 | 5 | fn main() { 6 | let mut siv = cursive::default(); 7 | 8 | let mut styles_label = StyledString::plain(""); 9 | styles_label.append(StyledString::styled("S", Color::Dark(BaseColor::Red))); 10 | styles_label.append(StyledString::styled("t", Color::Dark(BaseColor::Green))); 11 | styles_label.append(StyledString::styled("y", Color::Dark(BaseColor::Yellow))); 12 | styles_label.append(StyledString::styled("l", Color::Dark(BaseColor::Blue))); 13 | styles_label.append(StyledString::styled("e", Color::Dark(BaseColor::Magenta))); 14 | styles_label.append(StyledString::styled("s", Color::Dark(BaseColor::Cyan))); 15 | 16 | let quit_label = StyledString::styled( 17 | "Quit", 18 | Style::from(Color::Dark(BaseColor::Red)).combine(Effect::Bold), 19 | ); 20 | 21 | let sub_item_labels = vec![ 22 | StyledString::styled("Black", Color::Dark(BaseColor::Black)), 23 | StyledString::styled("Red", Color::Dark(BaseColor::Red)), 24 | StyledString::styled("Green", Color::Dark(BaseColor::Green)), 25 | StyledString::styled("Yellow", Color::Dark(BaseColor::Yellow)), 26 | StyledString::styled("Blue", Color::Dark(BaseColor::Blue)), 27 | StyledString::styled("Magenta", Color::Dark(BaseColor::Magenta)), 28 | StyledString::styled("Cyan", Color::Dark(BaseColor::Cyan)), 29 | StyledString::styled("White", Color::Dark(BaseColor::White)), 30 | StyledString::styled("Light Black", Color::Light(BaseColor::Black)), 31 | StyledString::styled("Light Red", Color::Light(BaseColor::Red)), 32 | StyledString::styled("Light Green", Color::Light(BaseColor::Green)), 33 | StyledString::styled("Light Yellow", Color::Light(BaseColor::Yellow)), 34 | StyledString::styled("Light Blue", Color::Light(BaseColor::Blue)), 35 | StyledString::styled("Light Magenta", Color::Light(BaseColor::Magenta)), 36 | StyledString::styled("Light Cyan", Color::Light(BaseColor::Cyan)), 37 | StyledString::styled("Light White", Color::Light(BaseColor::White)), 38 | ]; 39 | 40 | siv.menubar() 41 | .add_subtree( 42 | styles_label, 43 | menu::Tree::new().with(|tree| { 44 | for label in &sub_item_labels { 45 | tree.add_leaf(label.clone(), |_| {}); 46 | } 47 | }), 48 | ) 49 | .add_delimiter() 50 | .add_leaf(quit_label, |s| s.quit()); 51 | 52 | siv.set_autohide_menu(false); 53 | 54 | siv.run(); 55 | } 56 | -------------------------------------------------------------------------------- /cursive/examples/mines/Readme.md: -------------------------------------------------------------------------------- 1 | This is a slightly larger example, showing an implementation of Minesweeper. 2 | 3 | *This is a work in progress; many features are not implemented yet.* 4 | -------------------------------------------------------------------------------- /cursive/examples/mines/board/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod model; 2 | pub mod view; 3 | -------------------------------------------------------------------------------- /cursive/examples/mines/main.rs: -------------------------------------------------------------------------------- 1 | mod board; 2 | 3 | use crate::board::model::{self}; 4 | use board::view::BoardView; 5 | use cursive::{ 6 | views::{Button, Dialog, LinearLayout, Panel, SelectView}, 7 | Cursive, Vec2, 8 | }; 9 | use cursive_core::traits::Nameable; 10 | 11 | fn main() { 12 | let mut siv = cursive::default(); 13 | 14 | siv.add_layer( 15 | Dialog::new() 16 | .title("Minesweeper") 17 | .padding_lrtb(2, 2, 1, 1) 18 | .content( 19 | LinearLayout::vertical() 20 | .child(Button::new_raw(" New game ", show_options)) 21 | .child(Button::new_raw(" Controls ", show_controls)) 22 | .child(Button::new_raw(" Scores ", show_scores)) 23 | .child(Button::new_raw(" Exit ", |s| s.quit())), 24 | ), 25 | ); 26 | 27 | siv.run(); 28 | } 29 | 30 | fn show_options(siv: &mut Cursive) { 31 | siv.add_layer( 32 | Dialog::new() 33 | .title("Select difficulty") 34 | .content( 35 | SelectView::new() 36 | .item( 37 | "Easy: 8x8, 10 mines", 38 | model::Options { 39 | size: Vec2::new(8, 8), 40 | mines: 10, 41 | }, 42 | ) 43 | .item( 44 | "Medium: 16x16, 40 mines", 45 | model::Options { 46 | size: Vec2::new(16, 16), 47 | mines: 40, 48 | }, 49 | ) 50 | .item( 51 | "Difficult: 24x24, 99 mines", 52 | model::Options { 53 | size: Vec2::new(24, 24), 54 | mines: 99, 55 | }, 56 | ) 57 | .on_submit(|s, option| { 58 | s.pop_layer(); 59 | new_game(s, *option); 60 | }), 61 | ) 62 | .dismiss_button("Back"), 63 | ); 64 | } 65 | 66 | fn show_controls(s: &mut Cursive) { 67 | s.add_layer( 68 | Dialog::info( 69 | "Controls: 70 | Reveal cell: left click 71 | Mark as mine: right-click 72 | Reveal nearby unmarked cells: middle-click", 73 | ) 74 | .title("Controls"), 75 | ) 76 | } 77 | 78 | fn show_scores(s: &mut Cursive) { 79 | s.add_layer(Dialog::info("Not yet!").title("Scores")) 80 | } 81 | 82 | fn new_game(siv: &mut Cursive, options: model::Options) { 83 | let dialog = Dialog::new() 84 | .title("Minesweeper") 85 | .content( 86 | LinearLayout::horizontal() 87 | .child(Panel::new(BoardView::new(options).with_name("board"))), 88 | ) 89 | .button("Quit game", |s| { 90 | s.pop_layer(); 91 | }) 92 | .with_name("game"); 93 | 94 | siv.add_layer(dialog); 95 | } 96 | -------------------------------------------------------------------------------- /cursive/examples/mutation.rs: -------------------------------------------------------------------------------- 1 | use cursive::traits::*; 2 | use cursive::view::{Offset, Position}; 3 | use cursive::views::{Dialog, OnEventView, TextView}; 4 | use cursive::Cursive; 5 | 6 | // This example modifies a view after creation. 7 | 8 | fn main() { 9 | let mut siv = cursive::default(); 10 | 11 | let content = "Press Q to quit the application.\n\nPress P to open the \ 12 | popup."; 13 | 14 | siv.add_global_callback('q', |s| s.quit()); 15 | 16 | // Let's wrap the view to give it a recognizable name, so we can look for it. 17 | // We add the P callback on the textview only (and not globally), 18 | // so that we can't call it when the popup is already visible. 19 | siv.add_layer( 20 | OnEventView::new(TextView::new(content).with_name("text")).on_event('p', show_popup), 21 | ); 22 | 23 | siv.run(); 24 | } 25 | 26 | fn show_popup(siv: &mut Cursive) { 27 | // Let's center the popup horizontally, but offset it down a few rows, 28 | // so the user can see both the popup and the view underneath. 29 | siv.screen_mut().add_layer_at( 30 | Position::new(Offset::Center, Offset::Parent(5)), 31 | Dialog::around(TextView::new("Tak!")) 32 | .button("Change", |s| { 33 | // Look for a view tagged "text". 34 | // We _know_ it's there, so unwrap it. 35 | s.call_on_name("text", |view: &mut TextView| { 36 | let content = reverse(view.get_content().source()); 37 | view.set_content(content); 38 | }); 39 | }) 40 | .dismiss_button("Ok"), 41 | ); 42 | } 43 | 44 | // This just reverses each character 45 | // 46 | // Note: it would be more correct to iterate on graphemes instead. 47 | // Check the unicode_segmentation crate! 48 | fn reverse(text: &str) -> String { 49 | text.chars().rev().collect() 50 | } 51 | -------------------------------------------------------------------------------- /cursive/examples/panels.rs: -------------------------------------------------------------------------------- 1 | use cursive::traits::*; 2 | use cursive::views::{Button, LinearLayout, Panel}; 3 | 4 | fn main() { 5 | let mut siv = cursive::default(); 6 | 7 | siv.add_layer( 8 | LinearLayout::vertical() 9 | .child( 10 | Panel::new(Button::new("Quit", |s| s.quit())) 11 | .title("Panel 1") 12 | .fixed_width(20), 13 | ) 14 | .child( 15 | Panel::new(Button::new("Quit", |s| s.quit())) 16 | .title("Panel 2") 17 | .fixed_width(15), 18 | ) 19 | .child( 20 | Panel::new(Button::new("Quit", |s| s.quit())) 21 | .title("Panel 3") 22 | .fixed_width(10), 23 | ) 24 | .child( 25 | Panel::new(Button::new("Quit", |s| s.quit())) 26 | .title("Panel 4") 27 | .fixed_width(5), 28 | ), 29 | ); 30 | 31 | siv.run(); 32 | } 33 | -------------------------------------------------------------------------------- /cursive/examples/pause.rs: -------------------------------------------------------------------------------- 1 | use cursive::{self, views}; 2 | 3 | fn main() { 4 | let mut siv = cursive::default(); 5 | 6 | siv.add_layer(views::Dialog::text("Please write your message.").button("Ok", |s| s.quit())); 7 | 8 | siv.run(); 9 | // At this point the terminal is cleaned up. 10 | // We can write to stdout like any CLI program. 11 | // You could also start $EDITOR, or run other commands. 12 | 13 | println!("Enter your message here:"); 14 | 15 | let mut line = String::new(); 16 | std::io::stdin().read_line(&mut line).unwrap(); 17 | 18 | // And we can start another event loop later on. 19 | siv.add_layer( 20 | views::Dialog::text(format!("Your message was:\n{line}")).button("I guess?", |s| s.quit()), 21 | ); 22 | siv.run(); 23 | } 24 | -------------------------------------------------------------------------------- /cursive/examples/position.rs: -------------------------------------------------------------------------------- 1 | use cursive::{ 2 | view::Position, 3 | views::{LayerPosition, TextView}, 4 | Cursive, 5 | }; 6 | 7 | /// Moves top layer by the specified amount 8 | fn move_top(c: &mut Cursive, x_in: isize, y_in: isize) { 9 | // Step 1. Get the current position of the layer. 10 | let s = c.screen_mut(); 11 | let l = LayerPosition::FromFront(0); 12 | 13 | // Step 2. add the specified amount 14 | let pos = s 15 | .layer_offset(LayerPosition::FromFront(0)) 16 | .unwrap() 17 | .saturating_add((x_in, y_in)); 18 | 19 | // convert the new x and y into a position 20 | let p = Position::absolute(pos); 21 | 22 | // Step 3. Apply the new position 23 | s.reposition_layer(l, p); 24 | } 25 | 26 | fn main() { 27 | let mut siv = cursive::default(); 28 | 29 | // We can quit by pressing `q` 30 | siv.add_global_callback('q', Cursive::quit); 31 | // Next Gen FPS Controls. 32 | siv.add_global_callback('w', |s| move_top(s, 0, -1)); 33 | siv.add_global_callback('a', |s| move_top(s, -1, 0)); 34 | siv.add_global_callback('s', |s| move_top(s, 0, 1)); 35 | siv.add_global_callback('d', |s| move_top(s, 1, 0)); 36 | 37 | // Add window to fly around. 38 | siv.add_layer(TextView::new( 39 | "Press w,a,s,d to move the window.\n\ 40 | Press q to quit the application.", 41 | )); 42 | 43 | // Run the event loop 44 | siv.run(); 45 | } 46 | -------------------------------------------------------------------------------- /cursive/examples/radio.rs: -------------------------------------------------------------------------------- 1 | use cursive::views::{Dialog, DummyView, LinearLayout, RadioButton, RadioGroup}; 2 | 3 | // This example uses radio buttons. 4 | #[derive(Debug)] 5 | enum Flavor { 6 | Vanilla, 7 | Strawberry, 8 | Chocolate, 9 | } 10 | 11 | fn main() { 12 | let mut siv = cursive::default(); 13 | 14 | // We need to pre-create the groups for our RadioButtons. 15 | let mut color_group: RadioGroup = RadioGroup::new(); 16 | let mut size_group: RadioGroup = RadioGroup::new(); 17 | // The last group will be global 18 | 19 | siv.add_layer( 20 | Dialog::new() 21 | .title("Make your selection") 22 | // We'll have two columns side-by-side 23 | .content( 24 | LinearLayout::horizontal() 25 | .child( 26 | LinearLayout::vertical() 27 | // The color group uses the label itself as stored value 28 | // By default, the first item is selected. 29 | .child(color_group.button_str("Red")) 30 | .child(color_group.button_str("Green")) 31 | .child(color_group.button_str("Blue")), 32 | ) 33 | // A DummyView is used as a spacer 34 | .child(DummyView) 35 | .child( 36 | LinearLayout::vertical() 37 | // For the size, we store a number separately 38 | .child(size_group.button(5, "Small")) 39 | // The initial selection can also be overridden 40 | .child(size_group.button(15, "Medium").selected()) 41 | // The large size is out of stock, sorry! 42 | .child(size_group.button(25, "Large").disabled()), 43 | ) 44 | .child(DummyView) 45 | .child(LinearLayout::vertical() 46 | .child(RadioButton::global("flavor", Flavor::Vanilla, "Vanilla")) 47 | .child(RadioButton::global("flavor", Flavor::Strawberry, "Strawberry")) 48 | .child(RadioButton::global("flavor", Flavor::Chocolate, "Chocolate")) 49 | ) 50 | ) 51 | .button("Ok", move |s| { 52 | // We retrieve the stored value for both group. 53 | let color = color_group.selection(); 54 | let size = size_group.selection(); 55 | let flavor = RadioGroup::::with_global("flavor", |group| group.selection()); 56 | 57 | s.pop_layer(); 58 | // And we simply print the result. 59 | let text = format!("Color: {color}\nSize: {size}cm\nFlavor: {flavor:?}"); 60 | s.add_layer(Dialog::text(text).button("Ok", |s| s.quit())); 61 | }), 62 | ); 63 | 64 | siv.run(); 65 | } 66 | -------------------------------------------------------------------------------- /cursive/examples/refcell_view.rs: -------------------------------------------------------------------------------- 1 | use cursive::view::{Nameable, Resizable}; 2 | use cursive::views::{Dialog, EditView, LinearLayout, TextView}; 3 | use cursive::Cursive; 4 | 5 | // This example shows a way to access multiple views at the same time. 6 | 7 | fn main() { 8 | let mut siv = cursive::default(); 9 | 10 | // Create a dialog with 2 edit fields, and a text view. 11 | // The text view indicates when the 2 fields content match. 12 | siv.add_layer( 13 | Dialog::around( 14 | LinearLayout::vertical() 15 | .child(EditView::new().on_edit(on_edit).with_name("1")) 16 | .child(EditView::new().on_edit(on_edit).with_name("2")) 17 | .child(TextView::new("match").with_name("match")) 18 | .fixed_width(10), 19 | ) 20 | .button("Quit", Cursive::quit), 21 | ); 22 | 23 | siv.run(); 24 | } 25 | 26 | // Compare the content of the two edit views, 27 | // and update the TextView accordingly. 28 | // 29 | // We'll ignore the `content` and `cursor` arguments, 30 | // and directly retrieve the content from the `Cursive` root. 31 | fn on_edit(siv: &mut Cursive, _content: &str, _cursor: usize) { 32 | // Get handles for each view. 33 | let edit_1 = siv.find_name::("1").unwrap(); 34 | let edit_2 = siv.find_name::("2").unwrap(); 35 | 36 | // Directly compare references to edit_1 and edit_2. 37 | let matches = edit_1.get_content() == edit_2.get_content(); 38 | 39 | siv.call_on_name("match", |v: &mut TextView| { 40 | v.set_content(if matches { "match" } else { "no match" }) 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /cursive/examples/scroll.rs: -------------------------------------------------------------------------------- 1 | use cursive::traits::Resizable; 2 | use cursive::view::Scrollable; 3 | use cursive::views::{Button, Canvas, Dialog, LinearLayout}; 4 | use cursive::Printer; 5 | 6 | fn main() { 7 | let mut siv = cursive::default(); 8 | 9 | siv.add_layer( 10 | Dialog::around( 11 | LinearLayout::vertical() 12 | .child(Button::new("Foo", |s| s.add_layer(Dialog::info("Ah")))) 13 | .child(Canvas::new(()).with_draw(draw).fixed_size((120, 40))) 14 | .child(Button::new("Bar", |s| s.add_layer(Dialog::info("Uh")))) 15 | .scrollable() 16 | .scroll_x(true), 17 | ) 18 | .fixed_size((60, 30)), 19 | ); 20 | 21 | siv.add_global_callback('q', |s| s.quit()); 22 | 23 | siv.run(); 24 | } 25 | 26 | fn draw(_: &(), p: &Printer) { 27 | for x in 0..p.size.x { 28 | for y in 0..p.size.y { 29 | let c = (x + 6 * y) % 10; 30 | p.print((x, y), &format!("{c}")); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /cursive/examples/select.rs: -------------------------------------------------------------------------------- 1 | use cursive::align::HAlign; 2 | use cursive::event::EventResult; 3 | use cursive::traits::*; 4 | use cursive::views::{Dialog, OnEventView, SelectView, TextView}; 5 | use cursive::Cursive; 6 | 7 | // We'll use a SelectView here. 8 | // 9 | // A SelectView is a scrollable list of items, from which the user can select 10 | // one. 11 | 12 | fn main() { 13 | let mut select = SelectView::new() 14 | // Center the text horizontally 15 | .h_align(HAlign::Center) 16 | // Use keyboard to jump to the pressed letters 17 | .autojump(); 18 | 19 | // Read the list of cities from separate file, and fill the view with it. 20 | // (We include the file at compile-time to avoid runtime read errors.) 21 | let content = include_str!("assets/cities.txt"); 22 | select.add_all_str(content.lines()); 23 | 24 | // Sets the callback for when "Enter" is pressed. 25 | select.set_on_submit(show_next_window); 26 | 27 | // Let's override the `j` and `k` keys for navigation 28 | let select = OnEventView::new(select) 29 | .on_pre_event_inner('k', |s, _| { 30 | let cb = s.select_up(1); 31 | Some(EventResult::Consumed(Some(cb))) 32 | }) 33 | .on_pre_event_inner('j', |s, _| { 34 | let cb = s.select_down(1); 35 | Some(EventResult::Consumed(Some(cb))) 36 | }); 37 | 38 | let mut siv = cursive::default(); 39 | 40 | // Let's add a ResizedView to keep the list at a reasonable size 41 | // (it can scroll anyway). 42 | siv.add_layer( 43 | Dialog::around(select.scrollable().fixed_size((20, 10))).title("Where are you from?"), 44 | ); 45 | 46 | siv.run(); 47 | } 48 | 49 | // Let's put the callback in a separate function to keep it clean, 50 | // but it's not required. 51 | fn show_next_window(siv: &mut Cursive, city: &str) { 52 | siv.pop_layer(); 53 | let text = format!("{city} is a great city!"); 54 | siv.add_layer(Dialog::around(TextView::new(text)).button("Quit", |s| s.quit())); 55 | } 56 | -------------------------------------------------------------------------------- /cursive/examples/slider.rs: -------------------------------------------------------------------------------- 1 | use cursive::traits::*; 2 | use cursive::views::{Dialog, SliderView}; 3 | use cursive::Cursive; 4 | 5 | fn main() { 6 | let mut siv = cursive::default(); 7 | 8 | siv.add_global_callback('q', |s| s.quit()); 9 | 10 | // Let's add a simple slider in a dialog. 11 | // Moving the slider will update the dialog's title. 12 | // And pressing "Enter" will show a new dialog. 13 | siv.add_layer( 14 | Dialog::around( 15 | // We give the number of steps in the constructor 16 | SliderView::horizontal(15) 17 | // Sets the initial value 18 | .value(7) 19 | .on_change(|s, v| { 20 | let title = format!("{v: ^5}"); 21 | s.call_on_name("dialog", |view: &mut Dialog| { 22 | view.set_title(title) 23 | }); 24 | }) 25 | .on_enter(|s, v| { 26 | s.pop_layer(); 27 | s.add_layer( 28 | Dialog::text(format!("Lucky number {v}!")) 29 | .button("Ok", Cursive::quit), 30 | ); 31 | }), 32 | ) 33 | .title(" 7 ") 34 | .with_name("dialog"), 35 | ); 36 | 37 | siv.run(); 38 | } 39 | -------------------------------------------------------------------------------- /cursive/examples/status.rs: -------------------------------------------------------------------------------- 1 | //! This example creates a status bar at the bottom of the screen. 2 | 3 | use cursive::{ 4 | style::BaseColor, 5 | traits::{Nameable as _, Resizable as _}, 6 | utils::markup::StyledString, 7 | view::View as _, 8 | views::{Dialog, FixedLayout, Layer, OnLayoutView, TextView}, 9 | {Rect, Vec2}, 10 | }; 11 | 12 | fn main() { 13 | let mut siv = cursive::default(); 14 | 15 | // To build the status bar, we use a full-screen transparent layer. 16 | // We use FixedLayout (with OnLayoutView) to manually position our 17 | // TextView at the bottom of the screen. 18 | siv.screen_mut().add_transparent_layer( 19 | OnLayoutView::new( 20 | FixedLayout::new().child( 21 | Rect::from_point(Vec2::zero()), 22 | Layer::new(TextView::new("Status: unknown").with_name("status")).full_width(), 23 | ), 24 | |layout, size| { 25 | // We could also keep the status bar at the top instead. 26 | layout.set_child_position(0, Rect::from_size((0, size.y - 1), (size.x, 1))); 27 | layout.layout(size); 28 | }, 29 | ) 30 | .full_screen(), 31 | ); 32 | 33 | // We'll add a single dialog with a Quit button, and another button 34 | // that changes the status. 35 | siv.add_layer( 36 | Dialog::new() 37 | .title("Status bar example") 38 | .button("Change status", |s| { 39 | s.call_on_name("status", |text: &mut TextView| { 40 | // Flip the current situation. 41 | let nominal = !text.get_content().source().contains("nominal"); 42 | 43 | text.set_content(make_message(nominal)); 44 | }) 45 | .unwrap(); 46 | }) 47 | .button("Quit", |s| s.quit()), 48 | ); 49 | 50 | siv.run(); 51 | } 52 | 53 | /// Prepare a colorful message based on the status. 54 | fn make_message(nominal: bool) -> StyledString { 55 | let mut status = StyledString::plain("Status: "); 56 | 57 | if nominal { 58 | status.append_styled("nominal", BaseColor::Green.dark()); 59 | } else { 60 | status.append_styled("error", BaseColor::Red.dark()); 61 | } 62 | 63 | status 64 | } 65 | -------------------------------------------------------------------------------- /cursive/examples/status_bar_ext.rs: -------------------------------------------------------------------------------- 1 | //! Show a status bar line at the bottom of the screen. 2 | //! 3 | //! This example show how to create a `StatusBarExt` trait extension. 4 | //! A trait extension is a way to combine new functionality and an 5 | //! existing struct, and a way to improve implementation encapsulation. 6 | //! 7 | //! By Joel Parker Henderson (joel@joelparkerhenderson.com) 8 | 9 | use cursive::{ 10 | style::Style, 11 | utils::span::SpannedString, 12 | view::{Nameable, Resizable, View}, 13 | views::{FixedLayout, Layer, OnLayoutView, TextContent, TextContentRef, TextView}, 14 | Cursive, Rect, Vec2, 15 | }; 16 | 17 | pub trait StatusBarExt { 18 | fn status_bar(&mut self, content: impl Into>) -> TextContent; 19 | fn get_status_bar_content(&mut self) -> TextContentRef; 20 | fn set_status_bar_content(&mut self, content: impl Into>); 21 | } 22 | 23 | impl StatusBarExt for Cursive { 24 | /// Create a new status bar, set to the given content. 25 | fn status_bar(&mut self, content: impl Into>) -> TextContent { 26 | let text_content = TextContent::new(content); 27 | self.screen_mut().add_transparent_layer( 28 | OnLayoutView::new( 29 | FixedLayout::new().child( 30 | Rect::from_point(Vec2::zero()), 31 | Layer::new( 32 | TextView::new_with_content(text_content.clone()).with_name("status"), 33 | ) 34 | .full_width(), 35 | ), 36 | |layout, size| { 37 | let rect = Rect::from_size((0, size.y - 1), (size.x, 1)); 38 | layout.set_child_position(0, rect); 39 | layout.layout(size); 40 | }, 41 | ) 42 | .full_screen(), 43 | ); 44 | text_content 45 | } 46 | 47 | fn get_status_bar_content(&mut self) -> TextContentRef { 48 | self.call_on_name("status", |text_view: &mut TextView| text_view.get_content()) 49 | .expect("get_status") 50 | } 51 | 52 | fn set_status_bar_content(&mut self, content: impl Into>) { 53 | self.call_on_name("status", |text_view: &mut TextView| { 54 | text_view.set_content(content); 55 | }) 56 | .expect("set_status") 57 | } 58 | } 59 | 60 | pub fn main() { 61 | let mut siv = cursive::default(); 62 | siv.status_bar("Hello World"); 63 | siv.run(); 64 | } 65 | -------------------------------------------------------------------------------- /cursive/examples/stopwatch.rs: -------------------------------------------------------------------------------- 1 | //! A simple stopwatch implementation. 2 | //! Also check out [`clock-cli`](https://github.com/TianyiShi2001/cl!ock-cli-rs), 3 | //! which aims to implement a fully-fledged clock with stopwatch, countdown 4 | //! timer, and possibly more functionalities. 5 | 6 | use cursive::traits::{Nameable, Resizable}; 7 | use cursive::views::{Button, Canvas, Dialog, LinearLayout}; 8 | use std::time::{Duration, Instant}; 9 | 10 | fn main() { 11 | let mut siv = cursive::default(); 12 | 13 | siv.add_layer( 14 | Dialog::new() 15 | .title("Stopwatch") 16 | .content( 17 | LinearLayout::horizontal() 18 | .child( 19 | Canvas::new(Watch { 20 | last_started: Instant::now(), 21 | last_elapsed: Duration::default(), 22 | running: true, 23 | }) 24 | .with_draw(|s, printer| { 25 | printer.print((0, 1), &format!("{:.2?}", s.elapsed())); 26 | }) 27 | .with_name("stopwatch") 28 | .fixed_size((8, 3)), 29 | ) 30 | .child( 31 | LinearLayout::vertical() 32 | .child(Button::new("Start", run(Watch::start))) 33 | .child(Button::new("Pause", run(Watch::pause))) 34 | .child(Button::new("Stop", run(Watch::stop))), 35 | ), 36 | ) 37 | .button("Quit", |s| s.quit()) 38 | .h_align(cursive::align::HAlign::Center), 39 | ); 40 | 41 | siv.set_fps(20); 42 | 43 | siv.run(); 44 | } 45 | 46 | struct Watch { 47 | last_started: Instant, 48 | last_elapsed: Duration, 49 | running: bool, 50 | } 51 | 52 | impl Watch { 53 | fn start(&mut self) { 54 | if self.running { 55 | return; 56 | } 57 | self.running = true; 58 | self.last_started = Instant::now(); 59 | } 60 | 61 | fn elapsed(&self) -> Duration { 62 | self.last_elapsed 63 | + if self.running { 64 | Instant::now() - self.last_started 65 | } else { 66 | Duration::default() 67 | } 68 | } 69 | 70 | fn pause(&mut self) { 71 | self.last_elapsed = self.elapsed(); 72 | self.running = false; 73 | } 74 | 75 | fn stop(&mut self) { 76 | self.running = false; 77 | self.last_elapsed = Duration::default(); 78 | } 79 | } 80 | 81 | // Helper function to find the stopwatch view and run a closure on it. 82 | fn run(f: F) -> impl Fn(&mut cursive::Cursive) 83 | where 84 | F: Fn(&mut Watch), 85 | { 86 | move |s| { 87 | s.call_on_name("stopwatch", |c: &mut Canvas| { 88 | f(c.state_mut()); 89 | }); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /cursive/examples/terminal_default.rs: -------------------------------------------------------------------------------- 1 | use cursive::style::{Color, PaletteColor}; 2 | use cursive::theme::Theme; 3 | use cursive::views::TextView; 4 | use cursive::Cursive; 5 | 6 | // This example sets the background color to the terminal default. 7 | // 8 | // This way, it looks more natural. 9 | 10 | fn main() { 11 | let mut siv = cursive::default(); 12 | 13 | let theme = custom_theme_from_cursive(&siv); 14 | siv.set_theme(theme); 15 | 16 | // We can quit by pressing `q` 17 | siv.add_global_callback('q', Cursive::quit); 18 | 19 | siv.add_layer(TextView::new( 20 | "Hello World with default terminal background color!\n\ 21 | Press q to quit the application.", 22 | )); 23 | 24 | siv.run(); 25 | } 26 | 27 | fn custom_theme_from_cursive(siv: &Cursive) -> Theme { 28 | // We'll return the current theme with a small modification. 29 | let mut theme = siv.current_theme().clone(); 30 | 31 | theme.palette[PaletteColor::Background] = Color::TerminalDefault; 32 | 33 | theme 34 | } 35 | -------------------------------------------------------------------------------- /cursive/examples/text_area.rs: -------------------------------------------------------------------------------- 1 | use cursive::event::{Event, Key}; 2 | use cursive::traits::*; 3 | use cursive::views::{Dialog, EditView, OnEventView, TextArea}; 4 | use cursive::Cursive; 5 | 6 | fn main() { 7 | let mut siv = cursive::default(); 8 | 9 | // The main dialog will just have a textarea. 10 | // Its size expand automatically with the content. 11 | siv.add_layer( 12 | Dialog::new() 13 | .title("Describe your issue") 14 | .padding_lrtb(1, 1, 1, 0) 15 | .content(TextArea::new().with_name("text")) 16 | .button("Ok", Cursive::quit), 17 | ); 18 | 19 | // We'll add a find feature! 20 | siv.add_layer(Dialog::info("Hint: press Ctrl-F to find in text!")); 21 | 22 | siv.add_global_callback(Event::CtrlChar('f'), |s| { 23 | // When Ctrl-F is pressed, show the Find popup. 24 | // Pressing the Escape key will discard it. 25 | s.add_layer( 26 | OnEventView::new( 27 | Dialog::new() 28 | .title("Find") 29 | .content( 30 | EditView::new() 31 | .on_submit(find) 32 | .with_name("edit") 33 | .min_width(10), 34 | ) 35 | .button("Ok", |s| { 36 | let text = s 37 | .call_on_name("edit", |view: &mut EditView| view.get_content()) 38 | .unwrap(); 39 | find(s, &text); 40 | }) 41 | .dismiss_button("Cancel"), 42 | ) 43 | .on_event(Event::Key(Key::Esc), |s| { 44 | s.pop_layer(); 45 | }), 46 | ) 47 | }); 48 | 49 | siv.run(); 50 | } 51 | 52 | fn find(siv: &mut Cursive, text: &str) { 53 | // First, remove the find popup 54 | siv.pop_layer(); 55 | 56 | let res = siv.call_on_name("text", |v: &mut TextArea| { 57 | // Find the given text from the text area content 58 | // Possible improvement: search after the current cursor. 59 | if let Some(i) = v.get_content().find(text) { 60 | // If we found it, move the cursor 61 | v.set_cursor(i); 62 | Ok(()) 63 | } else { 64 | // Otherwise, return an error so we can show a warning. 65 | Err(()) 66 | } 67 | }); 68 | 69 | if let Some(Err(())) = res { 70 | // If we didn't find anything, tell the user! 71 | siv.add_layer(Dialog::info(format!("`{text}` not found"))); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /cursive/examples/text_with_ansi_codes.txt: -------------------------------------------------------------------------------- 1 | I love cursive and I cannot lie 2 | -------------------------------------------------------------------------------- /cursive/examples/theme.rs: -------------------------------------------------------------------------------- 1 | use cursive::views::{Dialog, TextView}; 2 | 3 | fn main() { 4 | let mut siv = cursive::default(); 5 | 6 | // You can load a theme from a file at runtime for fast development. 7 | siv.load_theme_file("examples/assets/style.toml").unwrap(); 8 | 9 | // Or you can directly load it from a string for easy deployment. 10 | siv.load_toml(include_str!("assets/style.toml")).unwrap(); 11 | 12 | siv.add_layer( 13 | Dialog::around(TextView::new( 14 | "This application uses a \ 15 | custom theme!", 16 | )) 17 | .title("Themed dialog") 18 | .button("Oh rly?", |_| ()) 19 | .button("Quit", |s| s.quit()), 20 | ); 21 | 22 | siv.run(); 23 | } 24 | -------------------------------------------------------------------------------- /cursive/examples/theme_manual.rs: -------------------------------------------------------------------------------- 1 | use cursive::style::{BorderStyle, Palette}; 2 | use cursive::traits::With; 3 | use cursive::views::{Dialog, EditView, LinearLayout, TextView}; 4 | use cursive::Cursive; 5 | 6 | fn main() { 7 | let mut siv = cursive::default(); 8 | 9 | // Start with a nicer theme than default 10 | siv.set_theme(cursive::theme::Theme { 11 | shadow: true, 12 | borders: BorderStyle::Simple, 13 | palette: Palette::retro().with(|palette| { 14 | use cursive::style::BaseColor::*; 15 | 16 | { 17 | // First, override some colors from the base palette. 18 | use cursive::style::Color::TerminalDefault; 19 | use cursive::style::PaletteColor::*; 20 | 21 | palette[Background] = TerminalDefault; 22 | palette[View] = TerminalDefault; 23 | palette[Primary] = White.dark(); 24 | palette[TitlePrimary] = Blue.light(); 25 | palette[Secondary] = Blue.light(); 26 | palette[Highlight] = Blue.dark(); 27 | } 28 | 29 | { 30 | // Then override some styles. 31 | use cursive::style::Effect::*; 32 | use cursive::style::PaletteStyle::*; 33 | use cursive::style::Style; 34 | palette[Highlight] = Style::from(Blue.light()).combine(Bold); 35 | palette[EditableTextCursor] = Style::secondary().combine(Reverse).combine(Underline) 36 | } 37 | }), 38 | }); 39 | 40 | let layout = LinearLayout::vertical() 41 | .child(TextView::new("This is a dynamic theme example!")) 42 | .child(EditView::new().content("Woo! colors!").filler(" ")); 43 | 44 | siv.add_layer( 45 | Dialog::around(layout) 46 | .title("Theme example") 47 | .button("Change", |s| { 48 | use cursive::style::BaseColor::*; 49 | use cursive::style::Color::TerminalDefault; 50 | use cursive::style::PaletteColor::*; 51 | // Change _something_ when the button is pressed. 52 | let mut theme = s.current_theme().clone(); 53 | 54 | theme.shadow = !theme.shadow; 55 | theme.borders = match theme.borders { 56 | BorderStyle::None => { 57 | theme.palette[View] = TerminalDefault; 58 | BorderStyle::Simple 59 | } 60 | _ => { 61 | theme.palette[View] = Black.light(); 62 | BorderStyle::None 63 | } 64 | }; 65 | 66 | s.set_theme(theme); 67 | }) 68 | .button("Quit", Cursive::quit), 69 | ); 70 | 71 | siv.run(); 72 | } 73 | -------------------------------------------------------------------------------- /cursive/examples/themed_view.rs: -------------------------------------------------------------------------------- 1 | use cursive::{views, With}; 2 | 3 | fn main() { 4 | let mut cursive = cursive::default(); 5 | 6 | cursive.add_layer( 7 | views::Dialog::text("Open a themed dialog?") 8 | .button("Open", show_dialog) 9 | .button("Quit", |s| s.quit()), 10 | ); 11 | 12 | cursive.run(); 13 | } 14 | 15 | fn show_dialog(s: &mut cursive::Cursive) { 16 | // Let's build a green theme 17 | let theme = s.current_theme().clone().with(|theme| { 18 | // Just for this function, import all color names for convenience. 19 | use cursive::style::{BaseColor::*, PaletteColor::*}; 20 | 21 | theme.palette[View] = Black.dark(); 22 | theme.palette[Primary] = Green.light(); 23 | theme.palette[TitlePrimary] = Green.light(); 24 | theme.palette[Highlight] = Green.dark(); 25 | theme.palette[HighlightText] = Green.light(); 26 | }); 27 | 28 | // We wrap the `Dialog` inside a `Layer` so it fills the entire view with the new `View` color. 29 | s.add_layer(views::ThemedView::new( 30 | theme, 31 | views::Layer::new(views::Dialog::info("Colors!").title("Themed Dialog")), 32 | )); 33 | } 34 | -------------------------------------------------------------------------------- /cursive/examples/user_data.rs: -------------------------------------------------------------------------------- 1 | //! This example shows the usage of user data. 2 | //! 3 | //! This lets you attach data to the main `Cursive` root, which can simplify 4 | //! communication in simple applications. 5 | //! 6 | //! `Cursive::set_user_data` is used to store or update the user data, while 7 | //! `Cursive::user_data` and `Cursive::with_user_data` can access it, if they 8 | //! know the exact type. 9 | use cursive::views::Dialog; 10 | use cursive::Cursive; 11 | 12 | struct Data { 13 | counter: u32, 14 | } 15 | 16 | fn main() { 17 | let mut siv = cursive::default(); 18 | 19 | // `Cursive::set_user_data` accepts any `T: Any`, which includes most 20 | // owned types 21 | siv.set_user_data(Data { counter: 0 }); 22 | 23 | siv.add_layer( 24 | Dialog::text("This uses some user data!") 25 | .title("User data example") 26 | .button("Increment", |s| { 27 | // `Cursive::with_user_data()` is an easy way to run a closure 28 | // on the data. 29 | s.with_user_data(|data: &mut Data| { 30 | data.counter += 1; 31 | }); 32 | }) 33 | .button("Show", |s| { 34 | // `Cursive::user_data()` returns a reference to the data. 35 | let value = s.user_data::().unwrap().counter; 36 | s.add_layer(Dialog::info(format!("Current value: {value}"))); 37 | }) 38 | .button("Quit", Cursive::quit), 39 | ); 40 | 41 | siv.run(); 42 | } 43 | -------------------------------------------------------------------------------- /cursive/examples/view.yaml: -------------------------------------------------------------------------------- 1 | # Definition for a labeled view 2 | LinearLayout: 3 | orientation: horizontal 4 | children: 5 | - TextView: $label 6 | - DummyView 7 | - EditView: 8 | on_edit: $on_edit 9 | with: 10 | - name: $name 11 | -------------------------------------------------------------------------------- /cursive/examples/vspace.yaml: -------------------------------------------------------------------------------- 1 | DummyView: 2 | with: 3 | # We can use $. to use the body itself of the config, rather than a field. 4 | - fixed_height: $. 5 | -------------------------------------------------------------------------------- /cursive/examples/window_title.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | let mut siv = cursive::default(); 3 | 4 | siv.add_layer( 5 | cursive::views::Dialog::new() 6 | .title("Write yourself a new title!") 7 | .content(cursive::views::EditView::new().on_edit(|s, content, _| { 8 | s.set_window_title(content); 9 | })) 10 | .button("Quit", |s| s.quit()), 11 | ); 12 | 13 | siv.run(); 14 | } 15 | -------------------------------------------------------------------------------- /cursive/src/backends/mod.rs: -------------------------------------------------------------------------------- 1 | //! Define backends using common libraries. 2 | //! 3 | //! Cursive doesn't print anything by itself: it delegates this job to a 4 | //! backend library, which handles all actual input and output. 5 | //! 6 | //! This module defines the [`Backend`] trait, as well as a few implementations 7 | //! using some common libraries. Each of those included backends needs a 8 | //! corresponding feature to be enabled. 9 | //! 10 | //! [`Backend`]: ../backend/trait.Backend.html 11 | #[cfg(unix)] 12 | mod resize; 13 | 14 | pub mod blt; 15 | pub mod crossterm; 16 | pub mod curses; 17 | pub mod puppet; 18 | pub mod termion; 19 | 20 | #[allow(dead_code)] 21 | fn boxed(e: impl std::error::Error + 'static) -> Box { 22 | Box::new(e) 23 | } 24 | 25 | /// Tries to initialize the default backend. 26 | /// 27 | /// Will use the first backend enabled from the list: 28 | /// * BearLibTerminal 29 | /// * Termion 30 | /// * Crossterm 31 | /// * Pancurses 32 | /// * Ncurses 33 | /// * Dummy 34 | pub fn try_default() -> Result, Box> 35 | { 36 | cfg_if::cfg_if! { 37 | if #[cfg(feature = "blt-backend")] { 38 | Ok(blt::Backend::init()) 39 | } else if #[cfg(feature = "termion-backend")] { 40 | termion::Backend::init().map_err(boxed) 41 | } else if #[cfg(feature = "pancurses-backend")] { 42 | curses::pan::Backend::init().map_err(boxed) 43 | } else if #[cfg(feature = "ncurses-backend")] { 44 | curses::n::Backend::init().map_err(boxed) 45 | } else if #[cfg(feature = "crossterm-backend")] { 46 | crossterm::Backend::init().map_err(boxed) 47 | } else { 48 | log::warn!("No built-it backend, falling back to Dummy backend."); 49 | Ok(cursive_core::backend::Dummy::init()) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /cursive/src/backends/puppet/observed_screen_view.rs: -------------------------------------------------------------------------------- 1 | //! View visualizing a captured PuppetBackend outputs 2 | use crate::backends::puppet::observed::ObservedCell; 3 | use crate::backends::puppet::observed::ObservedScreen; 4 | use crate::theme::ColorStyle; 5 | use crate::theme::ColorType; 6 | use crate::view::View; 7 | use crate::Printer; 8 | use crate::Vec2; 9 | 10 | /// A view that visualize observed screen 11 | pub struct ObservedScreenView { 12 | screen: ObservedScreen, 13 | } 14 | 15 | impl ObservedScreenView { 16 | /// Constructor 17 | pub fn new(obs: ObservedScreen) -> Self { 18 | ObservedScreenView { screen: obs } 19 | } 20 | } 21 | 22 | impl View for ObservedScreenView { 23 | fn draw(&self, printer: &Printer) { 24 | for x in 0..self.screen.size().x { 25 | for y in 0..self.screen.size().y { 26 | let pos = Vec2::new(x, y); 27 | let cell_op: &Option = &self.screen[pos]; 28 | if cell_op.is_none() { 29 | continue; 30 | } 31 | 32 | let cell = cell_op.as_ref().unwrap(); 33 | 34 | if cell.letter.is_continuation() { 35 | continue; 36 | } 37 | 38 | printer.with_effects(cell.style.effects, |printer| { 39 | let color_style = ColorStyle { 40 | front: ColorType::Color(cell.style.colors.front), 41 | back: ColorType::Color(cell.style.colors.back), 42 | }; 43 | 44 | printer.with_color(color_style, |printer| { 45 | printer.print(pos, &cell.letter.unwrap()); 46 | }); 47 | }); 48 | } 49 | } 50 | } 51 | 52 | fn required_size(&mut self, _: Vec2) -> Vec2 { 53 | self.screen.size() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /cursive/src/backends/puppet/static_values.rs: -------------------------------------------------------------------------------- 1 | /// Some default values to Puppet backend. 2 | use lazy_static::lazy_static; 3 | 4 | use crate::reexports::enumset::EnumSet; 5 | use crate::theme::ColorPair; 6 | use crate::theme::{Color, Effect}; 7 | use crate::Vec2; 8 | use crate::XY; 9 | 10 | use crate::backends::puppet::observed::*; 11 | 12 | lazy_static! { 13 | /// Default size for the puppet terminal. 14 | pub static ref DEFAULT_SIZE: Vec2 = XY:: { x: 120, y: 80 }; 15 | 16 | /// Default style for the puppet terminal. 17 | pub static ref DEFAULT_OBSERVED_STYLE: ObservedStyle = ObservedStyle { 18 | colors: ColorPair { 19 | front: Color::TerminalDefault, 20 | back: Color::TerminalDefault, 21 | }, 22 | effects: EnumSet::::empty(), 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /cursive/src/backends/resize.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::{AtomicBool, Ordering}; 2 | use std::sync::Arc; 3 | use std::thread; 4 | 5 | use crossbeam_channel::Sender; 6 | use signal_hook::iterator::Signals; 7 | 8 | /// This starts a new thread to listen for SIGWINCH signals 9 | #[allow(unused)] 10 | pub fn start_resize_thread(resize_sender: Sender<()>, resize_running: Arc) { 11 | let mut signals = Signals::new([libc::SIGWINCH]).unwrap(); 12 | thread::spawn(move || { 13 | // This thread will listen to SIGWINCH events and report them. 14 | while resize_running.load(Ordering::Relaxed) { 15 | // We know it will only contain SIGWINCH signals, so no need to check. 16 | if signals.wait().count() > 0 && resize_sender.send(()).is_err() { 17 | return; 18 | } 19 | } 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /cursive/src/utf8.rs: -------------------------------------------------------------------------------- 1 | use std::char::from_u32; 2 | 3 | /// Reads a potentially multi-bytes utf8 codepoint. 4 | /// 5 | /// Reads the given first byte, and uses the given 6 | /// function to get more if needed. 7 | /// 8 | /// Returns an error if the stream is invalid utf-8. 9 | #[allow(dead_code)] 10 | pub fn read_char(first: u8, next: F) -> Result 11 | where 12 | F: Fn() -> Option, 13 | { 14 | if first < 0x80 { 15 | return Ok(first as char); 16 | } 17 | 18 | // Number of leading 1s determines the number of bytes we'll have to read 19 | let n_bytes = match (!first).leading_zeros() { 20 | n @ 2..=6 => n as usize, 21 | 1 => return Err("First byte is continuation byte.".to_string()), 22 | 7..=8 => return Err("WTF is this byte??".to_string()), 23 | _ => unreachable!(), 24 | }; 25 | 26 | let mut res = 0_u32; 27 | 28 | // First, get the data - only the few last bits 29 | res |= u32::from(first & make_mask(7 - n_bytes)); 30 | 31 | // We already have one byte, now read the others. 32 | for _ in 1..n_bytes { 33 | let byte = next().ok_or_else(|| "Missing UTF-8 byte".to_string())?; 34 | if byte & 0xC0 != 0x80 { 35 | return Err(format!( 36 | "Found non-continuation byte after leading: `{byte}`", 37 | )); 38 | } 39 | // We have 6 fresh new bits to read, make room. 40 | res <<= 6; 41 | // 0x3F is 00111111, so we keep the last 6 bits 42 | res |= u32::from(byte & 0x3F); 43 | } 44 | 45 | // from_u32 could return an error if we gave it invalid utf-8. 46 | // But we're probably safe since we respected the rules when building it. 47 | Ok(from_u32(res).unwrap()) 48 | } 49 | 50 | // Returns a simple bitmask with n 1s to the right. 51 | #[allow(dead_code)] 52 | fn make_mask(n: usize) -> u8 { 53 | let mut r = 0_u8; 54 | for i in 0..n { 55 | r |= 1 << i; 56 | } 57 | r 58 | } 59 | -------------------------------------------------------------------------------- /doc/cursive_cursive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyscos/cursive/611eace2d63509c66762ccb2ffaf1e7eca56ac7c/doc/cursive_cursive.png -------------------------------------------------------------------------------- /doc/cursive_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyscos/cursive/611eace2d63509c66762ccb2ffaf1e7eca56ac7c/doc/cursive_example.png -------------------------------------------------------------------------------- /doc/test.txt: -------------------------------------------------------------------------------- 1 | 2 | ┌──┤ Cursive ├──┐ 3 | │ Hello Dialog! │ 4 | │ │ 5 | │ │ 6 | └───────────────┘ 7 | 8 | -------------------------------------------------------------------------------- /doc/tutorial_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyscos/cursive/611eace2d63509c66762ccb2ffaf1e7eca56ac7c/doc/tutorial_1.png -------------------------------------------------------------------------------- /doc/tutorial_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyscos/cursive/611eace2d63509c66762ccb2ffaf1e7eca56ac7c/doc/tutorial_2.png -------------------------------------------------------------------------------- /doc/tutorial_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyscos/cursive/611eace2d63509c66762ccb2ffaf1e7eca56ac7c/doc/tutorial_3.png -------------------------------------------------------------------------------- /examples: -------------------------------------------------------------------------------- 1 | cursive/examples -------------------------------------------------------------------------------- /rust-analyzer.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.cargo.features": "all", 3 | "rust-analyzer.cargo.buildScripts.enable": true, 4 | "rust-analyzer.procMacro.enable": true 5 | } 6 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | fn_params_layout = "Tall" 3 | format_code_in_doc_comments = true 4 | max_width = 100 5 | merge_derives = true 6 | reorder_imports = true 7 | reorder_modules = true 8 | use_field_init_shorthand = true 9 | use_try_shorthand = true 10 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {} }: 2 | 3 | pkgs.stdenv.mkDerivation { 4 | name = "cursive-env"; 5 | buildInputs = with pkgs; [ 6 | ncurses 7 | ]; 8 | 9 | RUST_BACKTRACE = 1; 10 | } 11 | --------------------------------------------------------------------------------