├── .editorconfig ├── .github └── workflows │ └── test_and_push.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── assets ├── README.md ├── config.yml ├── demo.gif └── demo.yml ├── examples ├── bottom.rs ├── fullscreen.rs ├── simple.rs ├── vertical.rs └── vertical_right.rs ├── scripts ├── ci-build.sh ├── deploy-badges.sh └── shields-from-tests.jq ├── src ├── bar.rs ├── error.rs ├── lib.rs └── panel.rs └── tests ├── end2end.rs └── snapshots ├── end2end__end2end_add_at.snap ├── end2end__end2end_add_at_action_change_tab.snap ├── end2end__end2end_add_at_panel.snap ├── end2end__end2end_panel_smoke.snap ├── end2end__end2end_remove_active.snap ├── end2end__end2end_remove_inactive.snap ├── end2end__end2end_swap.snap ├── end2end__end2end_switch.snap ├── end2end__end2end_vertical_left.snap ├── end2end__end2end_vertical_left_with_action_change_tab.snap ├── end2end__end2end_vertical_right.snap └── end2end__puppet_screen.snap /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 4 11 | 12 | [Makefile] 13 | indent_style = tab 14 | 15 | [{*.json,*.yml}] 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.github/workflows/test_and_push.yml: -------------------------------------------------------------------------------- 1 | name: "test_and_badge" 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - main 8 | 9 | jobs: 10 | test_and_badge: 11 | name: "Test and Badge" 12 | runs-on: "ubuntu-latest" 13 | steps: 14 | - name: "Prepare" 15 | run: | 16 | sudo apt-get update 17 | sudo apt-get install -y curl git 18 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y 19 | source $HOME/.cargo/env 20 | rustup toolchain install nightly 21 | git clone https://github.com/deinstapel/cursive-tabs.git repo 22 | 23 | - name: "Build and Test" 24 | run: | 25 | cd repo 26 | RUST_CHAIN=stable ./scripts/ci-build.sh 27 | RUST_CHAIN=nightly ./scripts/ci-build.sh 28 | 29 | - name: "Deploy Badge" 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | run: | 33 | cd repo 34 | ./scripts/deploy-badges.sh 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | node_modules 5 | *.temp 6 | .org 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## version 0.6.0 2 | - Removal of tab ids, this release replace the usage of the internal generic key used in `cursive-tabs` with the `cursive` native `NamedView`, this implies that only `NamedView`s can be added now to tabs. 3 | 4 | Any code adding views will need to be migrated. This migration can easily be done by using the `cursive::view::Nameable` trait, a small example on how to do this is shown below. 5 | 6 | ```rust 7 | // old style add 8 | tabs.add_tab(42, TextView::new("Crabs! 🦀")); 9 | 10 | // new add 11 | tabs.add(TextView::new("Crabs! 🦀").with_name("42")); 12 | ``` 13 | 14 | Notable differences between both styles is that only strings are taken as identifier, for most use cases this will be perfectly fine. 15 | Other use cases requiring the usage of more complex structures can use the `format!` macro to attain a `String` value. 16 | 17 | ```rust 18 | tabs.add(TextView::new("Crabs! 🦀").with_name(format!("{}", my_super_key)); 19 | ``` 20 | 21 | - Many small changes were required to allow for a more `cursive` like interface, mainly to do with other methods receiving identifiers to remove, or switch views, all methods operating on keys now receive `&str` 22 | 23 | Affected Methods: 24 | ```rust 25 | pub fn remove_tab(&mut self, id: &str) -> Result<(),()> 26 | pub fn swap_tabs(&mut self, fst: &str, snd: &str) 27 | pub fn add_tab_at(&mut self, view: NamedView, pos: usize) 28 | pub fn with_tab_at(mut self, view: NamedView, pos: usize) -> Self 29 | pub fn add_tab(&mut self, view: NamedView) 30 | pub fn with_tab(mut self, view: NamedView) -> Self 31 | pub fn with_active_tab(mut self, id: &str) -> Result 32 | pub fn set_active_tab(&mut self, id: &str) -> Result<(), ()> 33 | ``` 34 | ## version 0.5.0 35 | 36 | - Change `remove_tab` and `swap_tab` to take references to values instead of values itself 37 | - Change trait requirements to less specific `Clone` instead of `Copy` 38 | - `with_active_tab` will now return also in error cases `self` encapsulated in the `Result` 39 | 40 | ## version 0.3.0 41 | 42 | - Addition of `set_bar_placement` and `with_bar_placement` to control the position of the bar inside of the `TabPanel` 43 | - Change `HAlign` to `Align` to represent all directions. Possible values are `Start`, `Center` and `End` 44 | - New examples can be found that show the updated usage 45 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cursive-tabs" 3 | version = "0.8.0" 4 | authors = ["Fin Christensen ", "Johannes Wünsche "] 5 | edition = "2018" 6 | description = "Tabs for gyscos/cursive views" 7 | license = "BSD-3-Clause" 8 | readme = "README.md" 9 | keywords = ["cursive", "tui", "terminal", "tabs", "view"] 10 | repository = "https://github.com/deinstapel/cursive-tabs" 11 | documentation = "https://docs.rs/cursive-tabs" 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | cursive_core = "0.4" 17 | crossbeam = "0.8" 18 | log = "0.4" 19 | num = "0.4" 20 | 21 | [dev-dependencies] 22 | serde_json = "1.0.74" 23 | cursive = "0.21.0" 24 | insta = "1.10.0" 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, deinstapel 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Welcome to cursive-tabs 👋

2 |

3 | 4 | stable build 5 | 6 | 7 | nightly build 8 | 9 | 10 | crates.io 11 | 12 | 13 | Docs.rs 14 | 15 | 16 | GitHub 17 | 18 | 19 | 20 | 21 | 22 | PRs Welcome 23 | 24 |
25 | A tab view wrapper for 26 | gyscos/cursive 27 | views 28 |

29 | 30 | --- 31 | 32 | > This project is work-in-progress 33 | 34 | This project provides a wrapper view to be able to easily handle multiple tabs that can be switched to at any time without having to change the order of the views for [gyscos/cursive](https://github.com/gyscos/cursive) views. 35 | 36 | ## How does it look like? `demo` [![terminalizer](https://img.shields.io/badge/GIF-terminalizer-blueviolet.svg)](https://github.com/faressoft/terminalizer) 37 | 38 |
39 | Expand to view 40 | tabs demo 41 |
42 | 43 | ## Usage 44 | 45 | Simply add to your `Cargo.toml` 46 | 47 | ```toml 48 | [dependencies] 49 | cursive-tabs = "^0" 50 | ``` 51 | 52 | ### Creating a `TabPanel` 53 | 54 | The easiest way to use this crate is by creating a `TabPanel` and add your views to it. 55 | In the `TabPanel` included is a bar that shows all tabs and allows to switch between them by clicking the desired tab. 56 | Below it is the `TabView` showing the current tab. 57 | 58 | It can be created by simply calling new on `TabPanel` and views and customize it as you want, have a look at the [documentation](https://docs.rs/cursive-tabs) to see all options. 59 | 60 | ```rust 61 | use cursive::views::TextView; 62 | use cursive_tabs::TabPanel; 63 | use cursive::view::Nameable; 64 | 65 | let mut siv = cursive::default(); 66 | 67 | //Create your panel and add tabs 68 | let mut panel = TabPanel::new() 69 | .with_tab(TextView::new("This is the first view!").with_name("First")) 70 | .with_tab(TextView::new("This is the second view!").with_name("Second")); 71 | siv.add_layer(panel); 72 | siv.run(); 73 | ``` 74 | 75 | ### Creating a `TabView` 76 | 77 | This crate also provides a struct `TabView` you can use to add tabs and switch between them, if you do not need a separate bar to switch and e.g. want to switch programmatically. 78 | 79 | The `TabView` can also be used to create your own Panel/Bar if you want to design your cursive environment a different way. 80 | 81 | ```rust 82 | use cursive::{views::TextView}; 83 | use cursive_tabs::TabView; 84 | use cursive::view::Nameable; 85 | 86 | let mut siv = cursive::default(); 87 | let tabs = TabView::new().with_tab(TextView::new("Our first tab!").with_name("0")); 88 | // We can continue to add as many tabs as we want! 89 | 90 | siv.add_layer(tabs); 91 | siv.run(); 92 | ``` 93 | 94 | Look into the [documentation](https://docs.rs/cursive-tabs) for more examples and a detailed explanation. 95 | 96 | ### Creating your own Panel :hammer::construction: 97 | 98 | When you create a `TabBar` it will more or less look similar to the view e.g. also used in the example. To customize it you then need to create a view, creating a `TabBar` and a `TabView` events between them can be exchanged e.g. with channels. 99 | Channels have been chosen in this case by us, because they provide the easiest way to communicate between to instances of views in cursive. 100 | 101 | To make these channels work you have to create two separate channels transmitting both keys, once for the direction from the bar to the tab view, transmitting keys that have been selected by e.g. buttons, and the other from the tab view to the bar. 102 | 103 | An example for such a button would look like this. 104 | ```rust 105 | let button_tx_clone = button_tx.clone(); 106 | let button = Button::new_raw(format!(" {} ", key), move |_| { 107 | match button_tx_clone.send(key) { 108 | Ok(_) => {} 109 | Err(err) => { 110 | debug!("button could not send key: {:?}", err); 111 | } 112 | } 113 | }); 114 | ``` 115 | 116 | To make the `TabView` respond to messages over this channel pass the receiving end to the tab view via the method `set_bar_rx`. 117 | 118 | The other direction can be set by passing the Sender to `TabView` via the method `set_active_key_tx`. In this channel the currently active is send everytime a switch between tabs occurs. You can use this to register switches in your tab bar. 119 | 120 | The rest is depending on how you want to style your panel, but if you have anymore questions or problems have a look at the source of the provided `TabPanel`. 121 | 122 | ## Troubleshooting 123 | 124 | If you find any bugs/unexpected behaviour or you have a proposition for future changes open an issue describing the current behaviour and what you expected. 125 | 126 | ## Development [![cargo test](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fdeinstapel.github.io%2Fcursive-tabs%2Fcargo-test.json)](https://github.com/deinstapel/cursive-tabs/actions) 127 | 128 | ### Running the tests 129 | 130 | #### Running all test suites 131 | 132 | Just run 133 | 134 | ``` 135 | $ cargo test 136 | ``` 137 | 138 | to execute all available tests. 139 | 140 | #### Investigating failed tests 141 | 142 | In case some test fails with your changes, you can use the `cargo-insta` tool to investigate the test case. 143 | 144 | To install 145 | ``` 146 | $ cargo install cargo-insta 147 | ``` 148 | 149 | and to run the tests and investigate all failing tests interactively. 150 | 151 | ``` 152 | $ cargo insta review 153 | ``` 154 | 155 | 156 | Any changes between the expected and received screen will be then displayed. 157 | 158 | #### shields.io endpoints 159 | 160 | [shields.io](https://shields.io) endpoints are generated inside the `./target/shields` folder. They are used in this README. 161 | 162 | ### Public API naming 163 | 164 | The current public API of this crate is not consistent with [RFC 344](https://github.com/rust-lang/rfcs/pull/344). This is due to `cursive` itself not being 165 | consistent with `RFC 344`. This crate tries to implement a smooth user experience for cursive 166 | users. Therefore, the `cursive` naming convention was adapted. When `cursive` upstream converts 167 | their API to a `RFC 344` consistent naming scheme, this crate will adapt to the changes. 168 | 169 | ## Authors 170 | 171 | **Fin Christensen** 172 | 173 | > [:octocat: `@fin-ger`](https://github.com/fin-ger) 174 | > [:elephant: `@fin_ger@weirder.earth`](https://weirder.earth/@fin_ger) 175 | > [:bird: `@fin_ger_github`](https://twitter.com/fin_ger_github) 176 | 177 |
178 | 179 | **Johannes Wünsche** 180 | 181 | > [:octocat: `@jwuensche`](https://github.com/jwuensche) 182 | > [:elephant: `@fredowald@mastodon.social`](https://mastodon.social/web/accounts/843376) 183 | > [:bird: `@Fredowald`](https://twitter.com/fredowald) 184 | 185 | ## Show your support 186 | 187 | Give a :star: if this project helped you! 188 | -------------------------------------------------------------------------------- /assets/README.md: -------------------------------------------------------------------------------- 1 | # Creating a new GIF 2 | 3 | ## Recording 4 | 5 | Inside a `80x24` terminal record it using 6 | 7 | ``` 8 | $ cargo build --example simple 9 | $ cd assets 10 | $ terminalizer record --config ./config.yml demo 11 | ``` 12 | 13 | ## Rendering 14 | 15 | ``` 16 | $ terminalizer render demo.yml -o demo.gif 17 | ``` 18 | 19 | ## Optimizing 20 | 21 | ``` 22 | $ gifsicle --colors 24 -O3 demo.gif -o demo.gif 23 | ``` 24 | -------------------------------------------------------------------------------- /assets/config.yml: -------------------------------------------------------------------------------- 1 | # Specify a command to be executed 2 | # like `/bin/bash -l`, `ls`, or any other commands 3 | # the default is bash for Linux 4 | # or powershell.exe for Windows 5 | command: ../target/debug/examples/simple 6 | 7 | # Specify the current working directory path 8 | # the default is the current working directory path 9 | cwd: null 10 | 11 | # Export additional ENV variables 12 | env: 13 | recording: true 14 | 15 | # Explicitly set the number of columns 16 | # or use `auto` to take the current 17 | # number of columns of your shell 18 | cols: 80 19 | 20 | # Explicitly set the number of rows 21 | # or use `auto` to take the current 22 | # number of rows of your shell 23 | rows: 24 24 | 25 | # Amount of times to repeat GIF 26 | # If value is -1, play once 27 | # If value is 0, loop indefinitely 28 | # If value is a positive number, loop n times 29 | repeat: 0 30 | 31 | # Quality 32 | # 1 - 100 33 | quality: 100 34 | 35 | # Delay between frames in ms 36 | # If the value is `auto` use the actual recording delays 37 | frameDelay: auto 38 | 39 | # Maximum delay between frames in ms 40 | # Ignored if the `frameDelay` isn't set to `auto` 41 | # Set to `auto` to prevent limiting the max idle time 42 | maxIdleTime: 1000 43 | 44 | # The surrounding frame box 45 | # The `type` can be null, window, floating, or solid` 46 | # To hide the title use the value null 47 | # Don't forget to add a backgroundColor style with a null as type 48 | frameBox: 49 | type: solid 50 | title: null 51 | style: 52 | border: 0px black solid 53 | # boxShadow: none 54 | # margin: 0px 55 | 56 | # Add a watermark image to the rendered gif 57 | # You need to specify an absolute path for 58 | # the image on your machine or a URL, and you can also 59 | # add your own CSS styles 60 | watermark: 61 | imagePath: null 62 | style: 63 | position: absolute 64 | right: 15px 65 | bottom: 15px 66 | width: 100px 67 | opacity: 0.9 68 | 69 | # Cursor style can be one of 70 | # `block`, `underline`, or `bar` 71 | cursorStyle: block 72 | 73 | # Font family 74 | # You can use any font that is installed on your machine 75 | # in CSS-like syntax 76 | fontFamily: "Roboto Mono" 77 | 78 | # The size of the font 79 | fontSize: 12 80 | 81 | # The height of lines 82 | lineHeight: 1 83 | 84 | # The spacing between letters 85 | letterSpacing: 0 86 | 87 | # Theme 88 | theme: 89 | background: "transparent" 90 | foreground: "#afafaf" 91 | cursor: "#c7c7c7" 92 | black: "#232628" 93 | red: "#fc4384" 94 | green: "#b3e33b" 95 | yellow: "#ffa727" 96 | blue: "#003366" 97 | magenta: "#ae89fe" 98 | cyan: "#708387" 99 | white: "#d5d5d0" 100 | brightBlack: "#626566" 101 | brightRed: "#ff7fac" 102 | brightGreen: "#c8ed71" 103 | brightYellow: "#ebdf86" 104 | brightBlue: "#75dff2" 105 | brightMagenta: "#ae89fe" 106 | brightCyan: "#b1c6ca" 107 | brightWhite: "#f9f9f4" 108 | -------------------------------------------------------------------------------- /assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deinstapel/cursive-tabs/12998c24702f3e537946235db3a372e1bce3809c/assets/demo.gif -------------------------------------------------------------------------------- /assets/demo.yml: -------------------------------------------------------------------------------- 1 | # The configurations that used for the recording, feel free to edit them 2 | config: 3 | 4 | # Specify a command to be executed 5 | # like `/bin/bash -l`, `ls`, or any other commands 6 | # the default is bash for Linux 7 | # or powershell.exe for Windows 8 | command: ../target/debug/examples/simple 9 | 10 | # Specify the current working directory path 11 | # the default is the current working directory path 12 | cwd: null 13 | 14 | # Export additional ENV variables 15 | env: 16 | recording: true 17 | 18 | # Explicitly set the number of columns 19 | # or use `auto` to take the current 20 | # number of columns of your shell 21 | cols: 80 22 | 23 | # Explicitly set the number of rows 24 | # or use `auto` to take the current 25 | # number of rows of your shell 26 | rows: 24 27 | 28 | # Amount of times to repeat GIF 29 | # If value is -1, play once 30 | # If value is 0, loop indefinitely 31 | # If value is a positive number, loop n times 32 | repeat: 0 33 | 34 | # Quality 35 | # 1 - 100 36 | quality: 100 37 | 38 | # Delay between frames in ms 39 | # If the value is `auto` use the actual recording delays 40 | frameDelay: auto 41 | 42 | # Maximum delay between frames in ms 43 | # Ignored if the `frameDelay` isn't set to `auto` 44 | # Set to `auto` to prevent limiting the max idle time 45 | maxIdleTime: 1000 46 | 47 | # The surrounding frame box 48 | # The `type` can be null, window, floating, or solid` 49 | # To hide the title use the value null 50 | # Don't forget to add a backgroundColor style with a null as type 51 | frameBox: 52 | type: solid 53 | title: null 54 | style: 55 | border: 0px black solid 56 | # boxShadow: none 57 | # margin: 0px 58 | 59 | # Add a watermark image to the rendered gif 60 | # You need to specify an absolute path for 61 | # the image on your machine or a URL, and you can also 62 | # add your own CSS styles 63 | watermark: 64 | imagePath: null 65 | style: 66 | position: absolute 67 | right: 15px 68 | bottom: 15px 69 | width: 100px 70 | opacity: 0.9 71 | 72 | # Cursor style can be one of 73 | # `block`, `underline`, or `bar` 74 | cursorStyle: block 75 | 76 | # Font family 77 | # You can use any font that is installed on your machine 78 | # in CSS-like syntax 79 | fontFamily: "Roboto Mono" 80 | 81 | # The size of the font 82 | fontSize: 12 83 | 84 | # The height of lines 85 | lineHeight: 1 86 | 87 | # The spacing between letters 88 | letterSpacing: 0 89 | 90 | # Theme 91 | theme: 92 | background: "transparent" 93 | foreground: "#afafaf" 94 | cursor: "#c7c7c7" 95 | black: "#232628" 96 | red: "#fc4384" 97 | green: "#b3e33b" 98 | yellow: "#ffa727" 99 | blue: "#003366" 100 | magenta: "#ae89fe" 101 | cyan: "#708387" 102 | white: "#d5d5d0" 103 | brightBlack: "#626566" 104 | brightRed: "#8b0000" 105 | brightGreen: "#ffa727" 106 | brightYellow: "#ffa727" 107 | brightBlue: "#75dff2" 108 | brightMagenta: "#ae89fe" 109 | brightCyan: "#b1c6ca" 110 | brightWhite: "#f9f9f4" 111 | 112 | # Records, feel free to edit them 113 | records: 114 | - delay: 413 115 | content: "\e[?1049h\e[22;0;0t\e[1;24r\e(B\e[m\e[4l\e[?7h\e[?1h\e=\e[?1006;1000h\e[39;49m\e[?25l\e[?1002h\e[39;49m\e[34m\e[44m\e[H\e[2J\e[19d\e[J\e[H\e[K\n\e[K\n\e[K\n\e[K\n\e[K\n\e[K\e[7;15H\e[1K \e[30m\e[47m┌───────────────────────────┨\e(B\e[0;1m\e[31m\e[47m 0 \e(B\e[0m\e[30m\e[47m┃\e(B\e[0;1m\e[33m\e[47m 1 \e(B\e[0m\e[30m\e[47m│\e(B\e[0;1m\e[33m\e[47m 2 \e(B\e[0m\e[30m\e[47m│\e(B\e[0;1m\e[33m\e[47m 3 \e(B\e[0m\e[30m\e[47m│\e(B\e[0;1m\e[37m\e[41m 4 \e(B\e[0m\e[30m\e[47m├┐\e[34m\e[44m\e[K\e[8;15H\e[1K \e[30m\e[47m│With using the TabPanel you get a TabView and │\e[30m\e[40m \e[34m\e[44m\e[K\e[9;15H\e[1K \e[30m\e[47m│TabBar, preconfigured for you to use! │\e[30m\e[40m \e[34m\e[44m\e[K\e[10;15H\e[1K \e[30m\e[47m│Simply create it with:\e[26X\e[10;65H│\e[30m\e[40m \e[34m\e[44m\e[K\e[11;15H\e[1K \e[30m\e[47m│\e[48X\e[11;65H│\e[30m\e[40m \e[34m\e[44m\e[K\e[12;15H\e[1K \e[30m\e[47m│`cursive_tabs::TabPanel::new()`\e[17X\e[12;65H│\e[30m\e[40m \e[34m\e[44m\e[K\e[13;15H\e[1K \e[30m\e[47m│\e[48X\e[13;65H│\e[30m\e[40m \e[34m\e[44m\e[K\e[14;15H\e[1K \e[30m\e[47m│\e[48X\e[14;65H│\e[30m\e[40m \e[34m\e[44m\e[K\e[15;15H\e[1K \e[30m\e[47m│\e[48X\e[15;65H│\e[30m\e[40m \e[34m\e[44m\e[K\e[16;15H\e[1K \e[30m\e[47m└────────────────────────────────────────────────┘\e[30m\e[40m \e[34m\e[44m\e[K\e[17;15H\e[1K \e[30m\e[47m\e[38X\e[17;66H\e[30m\e[40m \e[34m\e[44m\e[K\e[18;16H\e[1K \e[30m\e[40m\e[50X\e[18;67H\e[34m\e[44m\e[K\e[17;28H\e[39;49m\e(B\e[m" 116 | - delay: 1050 117 | content: "\e[7;57H\e(B\e[0;1m\e[37m\e[41m 3 \e(B\e[0m\e[30m\e[47m│\e(B\e[0;1m\e[33m\e[47m 4 \e[17;28H\e[39;49m\e(B\e[m" 118 | - delay: 153 119 | content: "\e[7;53H\e(B\e[0;1m\e[37m\e[41m 2 \e(B\e[0m\e[30m\e[47m│\e(B\e[0;1m\e[33m\e[47m 3 \e[17;28H\e[39;49m\e(B\e[m" 120 | - delay: 154 121 | content: "\e[7;49H\e(B\e[0;1m\e[37m\e[41m 1 \e(B\e[0m\e[30m\e[47m│\e(B\e[0;1m\e[33m\e[47m 2 \e[17;28H\e[39;49m\e(B\e[m" 122 | - delay: 666 123 | content: "\e[7;44H\e[30m\e[47m┤\e(B\e[0;1m\e[33m\e[47m 0 \e(B\e[0m\e[30m\e[47m┃\e(B\e[0;1m\e[37m\e[41m 1 \e(B\e[0m\e[30m\e[47m┃\e[8;17HYou then can add views and configure your panel.\e[9;17H\e[37X\n\e[22X\n\n\e[31X\e[17;28H\e[39;49m\e(B\e[m" 124 | - delay: 1000 125 | content: "\e[7;49H\e(B\e[0;1m\e[31m\e[47m 1 \e(B\e[0m\e[30m\e[47m┃\e(B\e[0;1m\e[37m\e[41m 2 \e[17;28H\e[39;49m\e(B\e[m" 126 | - delay: 1000 127 | content: "\e[7;48H\e[30m\e[47m│\e(B\e[0;1m\e[33m\e[47m 1 \e(B\e[0m\e[30m\e[47m┃\e(B\e[0;1m\e[37m\e[41m 2 \e(B\e[0m\e[30m\e[47m┃\e[8;17HOfcourse you can also use the provided TabView \e[9;17Hwithout the panel, simply create it with:\e[11;17H`cursive_tabs::TabView::new()`\e[17;28H\e[39;49m\e(B\e[m" 128 | - delay: 1000 129 | content: "\e[7;53H\e(B\e[0;1m\e[31m\e[47m 2 \e(B\e[0m\e[30m\e[47m┃\e(B\e[0;1m\e[37m\e[41m 3 \e[17;28H\e[39;49m\e(B\e[m" 130 | - delay: 1000 131 | content: "\e[7;52H\e[30m\e[47m│\e(B\e[0;1m\e[33m\e[47m 2 \e(B\e[0m\e[30m\e[47m┃\e(B\e[0;1m\e[37m\e[41m 3 \e(B\e[0m\e[30m\e[47m┃\e[8;17HAll you have to do is add:\e[20X\e[9;17H\e[41X\ncursive-tabs = \"0^\"\e[11;17H\e[30X\nto your Cargo.toml!\e[17;28H\e[39;49m\e(B\e[m" 132 | - delay: 1000 133 | content: "\e[7;57H\e(B\e[0;1m\e[31m\e[47m 3 \e(B\e[0m\e[30m\e[47m┃\e(B\e[0;1m\e[37m\e[41m 4 \e[17;28H\e[39;49m\e(B\e[m" 134 | - delay: 1000 135 | content: "\e[7;56H\e[30m\e[47m│\e(B\e[0;1m\e[33m\e[47m 3 \e(B\e[0m\e[30m\e[47m┃\e(B\e[0;1m\e[37m\e[41m 4 \e(B\e[0m\e[30m\e[47m┠\e[8;17H\e[26X\n \e(B\e[0;7m\e[34m\e[47m \e[10;17H\e(B\e[0m\e[30m\e[47m \e(B\e[0;7m\e[34m\e[47m \e[11;19H \e[12;17H\e(B\e[0m\e[30m\e[47m \e(B\e[0;7m\e[34m\e[47m \e[13;19H \e[14;19H \e[17;28H\e[39;49m\e(B\e[m" 136 | - delay: 1000 137 | content: "\e[7;61H\e(B\e[0;1m\e[31m\e[47m 4 \e[9;19H\e(B\e[0m\e[34m\e[47m_\e[17;28H\e[39;49m\e(B\e[m" 138 | - delay: 50 139 | content: "\e[9;19H\e(B\e[0;7m\e[34m\e[47mH\e(B\e[0m\e[34m\e[47m_\e[17;28H\e[39;49m\e(B\e[m" 140 | - delay: 50 141 | content: "\e[9;20H\e(B\e[0;7m\e[34m\e[47ma\e(B\e[0m\e[34m\e[47m_\e[17;28H\e[39;49m\e(B\e[m" 142 | - delay: 50 143 | content: "\e[9;21H\e(B\e[0;7m\e[34m\e[47mv\e(B\e[0m\e[34m\e[47m_\e[17;28H\e[39;49m\e(B\e[m" 144 | - delay: 50 145 | content: "\e[9;22H\e(B\e[0;7m\e[34m\e[47me\e(B\e[0m\e[34m\e[47m_\e[17;28H\e[39;49m\e(B\e[m" 146 | - delay: 50 147 | content: "\e[9;23H\e(B\e[0;7m\e[34m\e[47m \e(B\e[0m\e[34m\e[47m_\e[17;28H\e[39;49m\e(B\e[m" 148 | - delay: 50 149 | content: "\e[9;24H\e(B\e[0;7m\e[34m\e[47mf\e(B\e[0m\e[34m\e[47m_\e[17;28H\e[39;49m\e(B\e[m" 150 | - delay: 50 151 | content: "\e[9;25H\e(B\e[0;7m\e[34m\e[47mu\e(B\e[0m\e[34m\e[47m_\e[17;28H\e[39;49m\e(B\e[m" 152 | - delay: 50 153 | content: "\e[9;26H\e(B\e[0;7m\e[34m\e[47mn\e(B\e[0m\e[34m\e[47m_\e[17;28H\e[39;49m\e(B\e[m" 154 | - delay: 50 155 | content: "\e[9;27H\e(B\e[0;7m\e[34m\e[47m!\e(B\e[0m\e[34m\e[47m_\e[17;28H\e[39;49m\e(B\e[m" 156 | - delay: 2204 157 | content: "\e[?1002l\e[?1006;1000l\e[24;1H\e[?12l\e[?25h\e[?1049l\e[23;0;0t\r\e[?1l\e>" 158 | -------------------------------------------------------------------------------- /examples/bottom.rs: -------------------------------------------------------------------------------- 1 | use cursive::view::{Nameable, Resizable}; 2 | use cursive::views::{Button, LinearLayout, PaddedView, TextArea, TextView}; 3 | use cursive_tabs::{Align, Placement, TabPanel}; 4 | 5 | const TAB_0: &str = 6 | "With using the TabPanel you get a TabView and TabBar, preconfigured for you to use! 7 | Simply create it with: 8 | 9 | `cursive_tabs::TabPanel::new()`"; 10 | 11 | const TAB_1: &str = "You then can add views and configure your panel."; 12 | 13 | const TAB_2: &str = 14 | "Ofcourse you can also use the provided TabView without the panel, simply create it with: 15 | 16 | `cursive_tabs::TabView::new()`"; 17 | 18 | const TAB_3: &str = "All you have to do is add: 19 | 20 | cursive-tabs = \"^0\" 21 | 22 | to your Cargo.toml! 23 | "; 24 | 25 | fn main() { 26 | let mut siv = cursive::default(); 27 | let panel = TabPanel::new() 28 | .with_tab(TextView::new(TAB_0).with_name("0")) 29 | .with_tab(TextView::new(TAB_1).with_name("1")) 30 | .with_tab(TextView::new(TAB_2).with_name("2")) 31 | .with_tab(TextView::new(TAB_3).with_name("3")) 32 | .with_tab(PaddedView::lrtb(2, 2, 1, 1, TextArea::new()).with_name("4")) 33 | .with_bar_alignment(Align::End) 34 | .with_bar_placement(Placement::HorizontalBottom) 35 | .with_active_tab("0") 36 | .unwrap_or_else(|_| { 37 | panic!("Could not set the first tab as active tab! This is probably an issue with the implementation in the lib. Please report!"); 38 | }); 39 | 40 | siv.add_layer( 41 | LinearLayout::vertical() 42 | .child(panel.with_name("Tabs").fixed_size((50, 10))) 43 | .child( 44 | LinearLayout::horizontal() 45 | .child(Button::new("Prev", |siv| { 46 | let mut tabs: cursive::views::ViewRef = 47 | siv.find_name("Tabs").expect("id not found"); 48 | tabs.prev(); 49 | })) 50 | .child(Button::new("Next", |siv| { 51 | let mut tabs: cursive::views::ViewRef = 52 | siv.find_name("Tabs").expect("id not found"); 53 | tabs.next(); 54 | })) 55 | .child(Button::new("Switch", |siv| { 56 | let mut tabs: cursive::views::ViewRef = 57 | siv.find_name("Tabs").expect("id not found"); 58 | tabs.swap_tabs("1", "2"); 59 | })), 60 | ), 61 | ); 62 | 63 | siv.add_global_callback('q', |siv| siv.quit()); 64 | 65 | siv.run(); 66 | } 67 | -------------------------------------------------------------------------------- /examples/fullscreen.rs: -------------------------------------------------------------------------------- 1 | use cursive::view::{Nameable, Resizable}; 2 | use cursive::views::{Button, LinearLayout, PaddedView, TextArea, TextView}; 3 | use cursive_tabs::{Align, TabPanel}; 4 | 5 | const TAB_0: &str = 6 | "With using the TabPanel you get a TabView and TabBar, preconfigured for you to use! 7 | Simply create it with: 8 | 9 | `cursive_tabs::TabPanel::new()`"; 10 | 11 | const TAB_1: &str = "You then can add views and configure your panel."; 12 | 13 | const TAB_2: &str = 14 | "Ofcourse you can also use the provided TabView without the panel, simply create it with: 15 | 16 | `cursive_tabs::TabView::new()`"; 17 | 18 | const TAB_3: &str = "All you have to do is add: 19 | 20 | cursive-tabs = \"^0\" 21 | 22 | to your Cargo.toml! 23 | "; 24 | 25 | fn main() { 26 | let mut siv = cursive::default(); 27 | let panel = TabPanel::new() 28 | .with_tab(TextView::new(TAB_0).with_name("0")) 29 | .with_tab(TextView::new(TAB_1).with_name("1")) 30 | .with_tab(TextView::new(TAB_2).with_name("2")) 31 | .with_tab(TextView::new(TAB_3).with_name("3")) 32 | .with_tab(PaddedView::lrtb(2, 2, 1, 1, TextArea::new()).with_name("4")) 33 | .with_bar_alignment(Align::End) 34 | .with_active_tab("0") 35 | .unwrap_or_else(|_| { 36 | panic!("Could not set the first tab as active tab! This is probably an issue with the implementation in the lib. Please report!"); 37 | }); 38 | 39 | siv.add_fullscreen_layer( 40 | LinearLayout::vertical() 41 | .child(panel.with_name("Tabs").full_screen()) 42 | .child( 43 | LinearLayout::horizontal() 44 | .child(Button::new("Prev", |siv| { 45 | let mut tabs: cursive::views::ViewRef = 46 | siv.find_name("Tabs").expect("id not found"); 47 | tabs.prev(); 48 | })) 49 | .child(Button::new("Next", |siv| { 50 | let mut tabs: cursive::views::ViewRef = 51 | siv.find_name("Tabs").expect("id not found"); 52 | tabs.next(); 53 | })), 54 | ), 55 | ); 56 | 57 | siv.add_global_callback('q', |siv| siv.quit()); 58 | 59 | siv.run(); 60 | } 61 | -------------------------------------------------------------------------------- /examples/simple.rs: -------------------------------------------------------------------------------- 1 | use cursive::view::{Nameable, Resizable}; 2 | use cursive::views::{Button, LinearLayout, NamedView, PaddedView, TextArea, TextView}; 3 | use cursive_tabs::{Align, TabPanel}; 4 | 5 | const TAB_0: &str = 6 | "With using the TabPanel you get a TabView and TabBar, preconfigured for you to use! 7 | Simply create it with: 8 | 9 | `cursive_tabs::TabPanel::new()`"; 10 | 11 | const TAB_1: &str = "You then can add views and configure your panel."; 12 | 13 | const TAB_2: &str = 14 | "Ofcourse you can also use the provided TabView without the panel, simply create it with: 15 | 16 | `cursive_tabs::TabView::new()`"; 17 | 18 | const TAB_3: &str = "All you have to do is add: 19 | 20 | cursive-tabs = \"^0\" 21 | 22 | to your Cargo.toml! 23 | "; 24 | 25 | fn main() { 26 | let mut siv = cursive::default(); 27 | let mut panel = TabPanel::new() 28 | .with_tab(TextView::new(TAB_0).with_name("0")) 29 | .with_tab(TextView::new(TAB_1).with_name("1")) 30 | .with_tab(TextView::new(TAB_2).with_name("2")) 31 | .with_tab(TextView::new(TAB_3).with_name("3")) 32 | .with_tab(PaddedView::lrtb(2, 2, 1, 1, TextArea::new()).with_name("4")) 33 | .with_bar_alignment(Align::End); 34 | 35 | let view = panel 36 | .active_view_mut() 37 | .unwrap() 38 | .downcast_mut::>>() 39 | .unwrap(); 40 | view.get_mut() 41 | .get_inner_mut() 42 | .set_content("This is additional text, set after the creation of the view!"); 43 | panel.set_active_tab("0").expect("View not found"); 44 | 45 | siv.add_layer( 46 | LinearLayout::vertical() 47 | .child(panel.with_name("Tabs").fixed_size((50, 10))) 48 | .child( 49 | LinearLayout::horizontal() 50 | .child(Button::new("Prev", |siv| { 51 | let mut tabs: cursive::views::ViewRef = 52 | siv.find_name("Tabs").expect("id not found"); 53 | tabs.prev(); 54 | })) 55 | .child(Button::new("Next", |siv| { 56 | let mut tabs: cursive::views::ViewRef = 57 | siv.find_name("Tabs").expect("id not found"); 58 | tabs.next(); 59 | })) 60 | .child(Button::new("Switch", |siv| { 61 | let mut tabs: cursive::views::ViewRef = 62 | siv.find_name("Tabs").expect("id not found"); 63 | tabs.swap_tabs("1", "2"); 64 | })), 65 | ), 66 | ); 67 | 68 | siv.add_global_callback('q', |siv| siv.quit()); 69 | 70 | siv.run(); 71 | } 72 | -------------------------------------------------------------------------------- /examples/vertical.rs: -------------------------------------------------------------------------------- 1 | use cursive::view::{Nameable, Resizable}; 2 | use cursive::views::{Button, LinearLayout, PaddedView, TextArea, TextView}; 3 | use cursive_tabs::{Align, Placement, TabPanel}; 4 | 5 | const TAB_0: &str = 6 | "With using the TabPanel you get a TabView and TabBar, preconfigured for you to use! 7 | Simply create it with: 8 | 9 | `cursive_tabs::TabPanel::new()`"; 10 | 11 | const TAB_1: &str = "You then can add views and configure your panel."; 12 | 13 | const TAB_2: &str = 14 | "Ofcourse you can also use the provided TabView without the panel, simply create it with: 15 | 16 | `cursive_tabs::TabView::new()`"; 17 | 18 | const TAB_3: &str = "All you have to do is add: 19 | 20 | cursive-tabs = \"^0\" 21 | 22 | to your Cargo.toml! 23 | "; 24 | 25 | fn main() { 26 | let mut siv = cursive::default(); 27 | let panel = TabPanel::new() 28 | .with_tab(TextView::new(TAB_0).with_name("0")) 29 | .with_tab(TextView::new(TAB_1).with_name("1")) 30 | .with_tab(TextView::new(TAB_2).with_name("2")) 31 | .with_tab(TextView::new(TAB_3).with_name("3")) 32 | .with_tab(PaddedView::lrtb(2, 2, 1, 1, TextArea::new()).with_name("4")) 33 | .with_bar_alignment(Align::End) 34 | .with_bar_placement(Placement::VerticalLeft) 35 | .with_active_tab("0") 36 | .unwrap_or_else(|_| { 37 | panic!("Could not set the first tab as active tab! This is probably an issue with the implementation in the lib. Please report!"); 38 | }); 39 | 40 | siv.add_layer( 41 | LinearLayout::vertical() 42 | .child(panel.with_name("Tabs").fixed_size((50, 30))) 43 | .child( 44 | LinearLayout::horizontal() 45 | .child(Button::new("Prev", |siv| { 46 | let mut tabs: cursive::views::ViewRef = 47 | siv.find_name("Tabs").expect("id not found"); 48 | tabs.prev(); 49 | })) 50 | .child(Button::new("Next", |siv| { 51 | let mut tabs: cursive::views::ViewRef = 52 | siv.find_name("Tabs").expect("id not found"); 53 | tabs.next(); 54 | })) 55 | .child(Button::new("Switch", |siv| { 56 | let mut tabs: cursive::views::ViewRef = 57 | siv.find_name("Tabs").expect("id not found"); 58 | tabs.swap_tabs("1", "2"); 59 | })), 60 | ), 61 | ); 62 | 63 | siv.add_global_callback('q', |siv| siv.quit()); 64 | 65 | siv.run(); 66 | } 67 | -------------------------------------------------------------------------------- /examples/vertical_right.rs: -------------------------------------------------------------------------------- 1 | use cursive::view::{Nameable, Resizable}; 2 | use cursive::views::{Button, LinearLayout, PaddedView, TextArea, TextView}; 3 | use cursive_tabs::{Align, Placement, TabPanel}; 4 | 5 | const TAB_0: &str = 6 | "With using the TabPanel you get a TabView and TabBar, preconfigured for you to use! 7 | Simply create it with: 8 | 9 | `cursive_tabs::TabPanel::new()`"; 10 | 11 | const TAB_1: &str = "You then can add views and configure your panel."; 12 | 13 | const TAB_2: &str = 14 | "Ofcourse you can also use the provided TabView without the panel, simply create it with: 15 | 16 | `cursive_tabs::TabView::new()`"; 17 | 18 | const TAB_3: &str = "All you have to do is add: 19 | 20 | cursive-tabs = \"^0\" 21 | 22 | to your Cargo.toml! 23 | "; 24 | 25 | fn main() { 26 | let mut siv = cursive::default(); 27 | let panel = TabPanel::new() 28 | .with_tab(TextView::new(TAB_0).with_name("0")) 29 | .with_tab(TextView::new(TAB_1).with_name("1")) 30 | .with_tab(TextView::new(TAB_2).with_name("2")) 31 | .with_tab(TextView::new(TAB_3).with_name("3")) 32 | .with_tab(PaddedView::lrtb(2, 2, 1, 1, TextArea::new()).with_name("4")) 33 | .with_bar_alignment(Align::End) 34 | .with_bar_placement(Placement::VerticalRight) 35 | .with_active_tab("0") 36 | .unwrap_or_else(|_| { panic!("Could not set the first tab as active tab! This is probably an issue with the implementation in the lib. Please report!"); 37 | }); 38 | 39 | siv.add_layer( 40 | LinearLayout::vertical() 41 | .child(panel.with_name("Tabs").fixed_size((50, 30))) 42 | .child( 43 | LinearLayout::horizontal() 44 | .child(Button::new("Prev", |siv| { 45 | let mut tabs: cursive::views::ViewRef = 46 | siv.find_name("Tabs").expect("id not found"); 47 | tabs.prev(); 48 | })) 49 | .child(Button::new("Next", |siv| { 50 | let mut tabs: cursive::views::ViewRef = 51 | siv.find_name("Tabs").expect("id not found"); 52 | tabs.next(); 53 | })) 54 | .child(Button::new("Switch", |siv| { 55 | let mut tabs: cursive::views::ViewRef = 56 | siv.find_name("Tabs").expect("id not found"); 57 | tabs.swap_tabs("1", "2"); 58 | })), 59 | ), 60 | ); 61 | 62 | siv.add_global_callback('q', |siv| siv.quit()); 63 | 64 | siv.run(); 65 | } 66 | -------------------------------------------------------------------------------- /scripts/ci-build.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | die() { 4 | printf "\e[31:1mError: %s\e[0m\n" "$1" >&2 5 | exit 1 6 | } 7 | 8 | if [ -z "$RUST_CHAIN" ] 9 | then 10 | die "RUST_CHAIN environment variable is not set! RUST_CHAIN={stable,nightly}" 11 | fi 12 | 13 | ( 14 | cd "$(git rev-parse --show-toplevel)" || die "cannot find project root" 15 | 16 | # Badges! 17 | mkdir -p ./target/shields 18 | if cargo "+${RUST_CHAIN}" --color=always build --all-targets; then 19 | cat < "./target/shields/$RUST_CHAIN-build.json" 20 | { 21 | "color": "brightgreen", 22 | "isError": true, 23 | "label": "$RUST_CHAIN build", 24 | "message": "passing", 25 | "schemaVersion": 1 26 | } 27 | EOF 28 | else 29 | PRV_EXIT=$? 30 | cat < "./target/shields/$RUST_CHAIN-build.json" 31 | { 32 | "color": "red", 33 | "isError": true, 34 | "label": "$RUST_CHAIN build", 35 | "message": "failed", 36 | "schemaVersion": 1 37 | } 38 | EOF 39 | exit $PRV_EXIT 40 | fi 41 | 42 | cargo "+${RUST_CHAIN}" --color=always test --no-fail-fast 43 | exitcode=$? 44 | 45 | # create badge for `cargo test` 46 | cargo "+${RUST_CHAIN}" test --no-fail-fast -- -Z unstable-options --format json | \ 47 | jq -s -f ./scripts/shields-from-tests.jq > ./target/shields/cargo-test.json 48 | 49 | exit $exitcode 50 | ) 51 | -------------------------------------------------------------------------------- /scripts/deploy-badges.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | die() { 4 | printf "\e[31:1mError: %s\e[0m\n" "$1" >&2 5 | exit 1 6 | } 7 | 8 | if [ -z "$GITHUB_ACTOR" ] 9 | then 10 | die "the GITHUB_ACTOR environment variable is not set" 11 | fi 12 | 13 | if [ -z "$GITHUB_TOKEN" ] 14 | then 15 | die "the GITHUB_TOKEN environment variable is not set" 16 | fi 17 | 18 | if [ -z "$GITHUB_REPOSITORY" ] 19 | then 20 | die "the GITHUB_REPOSITORY environment variable is not set" 21 | fi 22 | 23 | ( 24 | cd "$(git rev-parse --show-toplevel)/target/shields" || die "cannot find project root!" 25 | repo="https://${GITHUB_ACTOR}:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" 26 | tmp_dir=$(mktemp -d -t cursive-multiplex-deploy-XXXXXXXX) 27 | 28 | git config --global user.email "runner@ci" 29 | git config --global user.name "Github CI Runner" 30 | try=0 31 | while :; do 32 | if ! git clone --branch gh-pages "$repo" "$tmp_dir" 33 | then 34 | ( 35 | cd "$tmp_dir" || die "failed to enter temporary directory" 36 | git init 37 | git remote add origin "$repo" 38 | git checkout -b gh-pages 39 | ) 40 | fi 41 | 42 | cp -ar ./* "$tmp_dir" 43 | 44 | ( 45 | cd "$tmp_dir" || die "failed to enter temporary directory" 46 | git add -A 47 | git commit -m "Github CI badge deployment" 48 | git push origin gh-pages:gh-pages 49 | ) 50 | 51 | result=$? 52 | if [ "$result" -eq 0 ] || [ "$try" -ge 5 ] 53 | then 54 | break 55 | fi 56 | 57 | try=$((try + 1)) 58 | done 59 | 60 | rm -rf "$tmp_dir" 61 | 62 | ) 63 | -------------------------------------------------------------------------------- /scripts/shields-from-tests.jq: -------------------------------------------------------------------------------- 1 | { 2 | # filter for "passed" and "test_count" in input objects with `"type": "suite"` 3 | # and accumulate stats from all tests 4 | "passed": map(select(.type == "suite" and has("passed")) | .passed) | add, 5 | "total": map(select(.type == "suite" and has("test_count")) | .test_count) | add 6 | } | . + { 7 | # calculate ratio of passed tests 8 | "factor": (.passed / .total) 9 | } | { 10 | # calculate color from test factor 11 | "color": ( 12 | if .factor < 0.33 then 13 | "red" 14 | elif .factor < 0.66 then 15 | "orange" 16 | elif .factor < 1.0 then 17 | "yellow" 18 | else 19 | "brightgreen" 20 | end 21 | ), 22 | "isError": true, 23 | "label": "cargo test", 24 | # interpolate the shield label 25 | "message": "\(.passed) / \(.total) tests", 26 | "schemaVersion": 1 27 | } 28 | -------------------------------------------------------------------------------- /src/bar.rs: -------------------------------------------------------------------------------- 1 | use crossbeam::channel::{Receiver, Sender}; 2 | use cursive::event::{Event, EventResult, Key, MouseButton, MouseEvent}; 3 | use cursive::theme::{Effect, PaletteColor}; 4 | use cursive::view::{View, ViewWrapper}; 5 | use cursive::views::Button; 6 | use cursive::{wrap_impl, Printer, Vec2}; 7 | use log::debug; 8 | 9 | use crate::panel::{Align, Placement}; 10 | 11 | /// Trait which defines which basic action a tab bar should be able to handle 12 | pub trait Bar { 13 | fn add_button(&mut self, tx: Sender, key: &str); 14 | fn remove_button(&mut self, key: &str); 15 | fn swap_button(&mut self, left: &str, right: &str); 16 | fn add_button_at(&mut self, tx: Sender, key: &str, pos: usize); 17 | } 18 | 19 | // Quick Wrapper around Views to be able to set their positon 20 | struct PositionWrap { 21 | view: T, 22 | pub pos: Vec2, 23 | pub key: String, 24 | } 25 | 26 | impl ViewWrapper for PositionWrap { 27 | wrap_impl!(self.view: T); 28 | } 29 | 30 | impl PositionWrap { 31 | pub fn new(view: T, key: String) -> Self { 32 | Self { 33 | view, 34 | pos: Vec2::zero(), 35 | key, 36 | } 37 | } 38 | } 39 | 40 | pub struct TabBar { 41 | children: Vec>, 42 | bar_size: Vec2, 43 | align: Align, 44 | last_rendered_size: Vec2, 45 | // List of accumulated sizes of prev buttons 46 | sizes: Vec, 47 | placement: Placement, 48 | cursor: Option, 49 | active: Option, 50 | rx: Receiver, 51 | invalidated: bool, 52 | } 53 | 54 | impl TabBar { 55 | pub fn new(rx: Receiver) -> Self { 56 | Self { 57 | children: Vec::new(), 58 | sizes: Vec::new(), 59 | cursor: None, 60 | active: None, 61 | align: Align::Start, 62 | placement: Placement::HorizontalTop, 63 | bar_size: Vec2::zero(), 64 | last_rendered_size: Vec2::zero(), 65 | rx, 66 | invalidated: true, 67 | } 68 | } 69 | 70 | pub fn with_alignment(mut self, align: Align) -> Self { 71 | self.align = align; 72 | self.invalidated = true; 73 | self 74 | } 75 | 76 | pub fn set_alignment(&mut self, align: Align) { 77 | self.align = align; 78 | self.invalidated = true; 79 | } 80 | 81 | pub fn with_placement(mut self, placement: Placement) -> Self { 82 | self.placement = placement; 83 | self.invalidated = true; 84 | self 85 | } 86 | 87 | pub fn set_placement(&mut self, placement: Placement) { 88 | self.placement = placement; 89 | self.invalidated = true; 90 | } 91 | 92 | fn decrement_idx(&mut self) -> EventResult { 93 | if let Some(index) = self.cursor { 94 | if index > 0 { 95 | self.cursor = Some(index - 1); 96 | self.invalidated = true; 97 | EventResult::Consumed(None) 98 | } else { 99 | EventResult::Ignored 100 | } 101 | } else { 102 | EventResult::Ignored 103 | } 104 | } 105 | 106 | fn increment_idx(&mut self) -> EventResult { 107 | if let Some(index) = self.cursor { 108 | if (index + 1) < self.children.len() { 109 | self.cursor = Some(index + 1); 110 | self.invalidated = true; 111 | EventResult::Consumed(None) 112 | } else { 113 | EventResult::Ignored 114 | } 115 | } else { 116 | EventResult::Ignored 117 | } 118 | } 119 | } 120 | 121 | impl Bar for TabBar { 122 | fn add_button(&mut self, tx: Sender, key: &str) { 123 | let k = key.to_owned(); 124 | let button = Button::new_raw(format!(" {} ", key), move |_| { 125 | debug!("send {}", k); 126 | match tx.send(k.clone()) { 127 | Ok(_) => {} 128 | Err(err) => { 129 | debug!("button could not send key: {:?}", err); 130 | } 131 | } 132 | }); 133 | self.children 134 | .push(PositionWrap::new(button, key.to_owned())); 135 | self.cursor = Some(self.children.len() - 1); 136 | self.active = Some(self.children.len() - 1); 137 | self.invalidated = true; 138 | } 139 | 140 | fn remove_button(&mut self, key: &str) { 141 | if let Some(pos) = self 142 | .children 143 | .iter() 144 | .enumerate() 145 | .filter_map( 146 | |(pos, button)| { 147 | if button.key == *key { 148 | Some(pos) 149 | } else { 150 | None 151 | } 152 | }, 153 | ) 154 | .next() 155 | { 156 | if let Some(idx) = self.cursor { 157 | if idx == pos { 158 | self.cursor = None; 159 | self.active = None; 160 | } 161 | } 162 | self.children.remove(pos); 163 | } 164 | self.invalidated = true; 165 | } 166 | 167 | fn swap_button(&mut self, first: &str, second: &str) { 168 | let pos: Vec = self 169 | .children 170 | .iter() 171 | .enumerate() 172 | .filter_map(|(pos, button)| { 173 | if button.key == *first || button.key == *second { 174 | Some(pos) 175 | } else { 176 | None 177 | } 178 | }) 179 | .collect(); 180 | if let [pos1, pos2] = pos[..] { 181 | let child2 = self.children.remove(pos2); 182 | let child1 = self.children.remove(pos1); 183 | self.children.insert(pos1, child2); 184 | self.children.insert(pos2, child1); 185 | } 186 | self.invalidated = true; 187 | } 188 | 189 | fn add_button_at(&mut self, tx: Sender, key: &str, pos: usize) { 190 | let k = key.to_owned(); 191 | let button = Button::new_raw(format!(" {} ", key), move |_| { 192 | debug!("send {}", k); 193 | match tx.send(k.clone()) { 194 | Ok(_) => {} 195 | Err(err) => { 196 | debug!("button could not send key: {:?}", err); 197 | } 198 | } 199 | }); 200 | self.cursor = Some(pos); 201 | self.active = Some(pos); 202 | self.children 203 | .insert(pos, PositionWrap::new(button, key.to_owned())); 204 | self.invalidated = true; 205 | } 206 | } 207 | 208 | impl View for TabBar { 209 | fn draw(&self, printer: &Printer) { 210 | match self.placement { 211 | Placement::HorizontalBottom | Placement::HorizontalTop => { 212 | // First draw the complete horizontal line 213 | printer.print_hline((0, 0), printer.size.x, "─"); 214 | // Spacing for padding & crop end 215 | let inner_printer = printer 216 | // Alignment 217 | .offset(( 218 | self.align 219 | .get_offset(self.bar_size.x + self.children.len() + 1, printer.size.x), 220 | 0, 221 | )); 222 | for (idx, child) in self.children.iter().enumerate() { 223 | // There is no chainable api... 224 | let mut rel_sizes = self.sizes.clone(); 225 | rel_sizes.truncate(idx); 226 | let mut print = inner_printer 227 | .offset( 228 | rel_sizes 229 | .iter() 230 | .fold(Vec2::new(0, 0), |acc, x| acc.stack_horizontal(x)) 231 | .keep_x(), 232 | ) 233 | // Spacing for first character 234 | .offset((idx, 0)) 235 | .cropped({ 236 | if idx == 0 || idx == self.children.len() - 1 { 237 | self.sizes[idx].stack_horizontal(&Vec2::new(2, 1)) 238 | } else { 239 | self.sizes[idx].stack_horizontal(&Vec2::new(1, 1)) 240 | } 241 | }); 242 | let mut theme = printer.theme.clone(); 243 | 244 | if !self.active.map_or(false, |active| idx == active) { 245 | let color = theme.palette[PaletteColor::TitleSecondary]; 246 | theme.palette[PaletteColor::Primary] = color; 247 | } else { 248 | let color = theme.palette[PaletteColor::TitlePrimary]; 249 | theme.palette[PaletteColor::Primary] = color; 250 | } 251 | 252 | if let Some(focus) = self.cursor { 253 | print = print.focused(focus == idx); 254 | } 255 | 256 | print.with_theme(&theme, |printer| { 257 | if idx > 0 { 258 | if self.active.map_or(false, |active| idx == active) 259 | || self.active.map_or(false, |active| active == (idx - 1)) 260 | { 261 | printer.print((0, 0), "┃") 262 | } else { 263 | printer.print((0, 0), "│"); 264 | } 265 | } else if self.active.map_or(false, |active| idx == active) { 266 | printer.print((0, 0), "┨") 267 | } else { 268 | printer.print((0, 0), "┤"); 269 | } 270 | printer.with_effect(Effect::Bold, |printer| { 271 | child.draw(&printer.offset((1, 0))) 272 | }); 273 | if idx == self.children.len() - 1 { 274 | if self.active.map_or(false, |active| idx == active) { 275 | printer.offset((1, 0)).print(self.sizes[idx].keep_x(), "┠"); 276 | } else { 277 | printer.offset((1, 0)).print(self.sizes[idx].keep_x(), "├"); 278 | } 279 | } 280 | }); 281 | } 282 | } 283 | Placement::VerticalLeft | Placement::VerticalRight => { 284 | // First draw the complete vertical line 285 | let horizontal_offset = match self.placement { 286 | Placement::VerticalLeft => printer.size.x - 1, 287 | _ => 0, 288 | }; 289 | printer.print_vline((horizontal_offset, 0), printer.size.y, "│"); 290 | // Spacing for padding & crop end 291 | let inner_printer = printer 292 | // Alignment 293 | .offset(( 294 | 0, 295 | self.align 296 | .get_offset(self.bar_size.y + self.children.len() + 1, printer.size.y), 297 | )); 298 | for (idx, child) in self.children.iter().enumerate() { 299 | // There is no chainable api... 300 | let mut rel_sizes = self.sizes.clone(); 301 | rel_sizes.truncate(idx); 302 | let mut print = inner_printer 303 | // Move the printer to the position of the child, respecting the height of all previous ones 304 | .offset( 305 | rel_sizes 306 | .iter() 307 | .fold(Vec2::new(0, 0), |acc, x| acc.stack_vertical(x)) 308 | .keep_y(), 309 | ) 310 | // Spacing for first character of the current one and all previous ones 311 | .offset((0, idx)) 312 | // Offset so that the right side when aligned to the left is on the panel border 313 | .offset(( 314 | if self.placement == Placement::VerticalLeft { 315 | self.bar_size.x - self.sizes[idx].x 316 | } else { 317 | 0 318 | }, 319 | 0, 320 | )) 321 | // Crop to size including the delimiters 322 | .cropped({ 323 | if idx == 0 || idx == self.children.len() - 1 { 324 | self.sizes[idx].stack_vertical(&Vec2::new(1, 2)) 325 | } else { 326 | self.sizes[idx].stack_vertical(&Vec2::new(1, 1)) 327 | } 328 | }); 329 | let mut theme = printer.theme.clone(); 330 | 331 | if !self.active.map_or(false, |active| idx == active) { 332 | let color = theme.palette[PaletteColor::TitleSecondary]; 333 | theme.palette[PaletteColor::Primary] = color; 334 | } else { 335 | let color = theme.palette[PaletteColor::TitlePrimary]; 336 | theme.palette[PaletteColor::Primary] = color; 337 | } 338 | 339 | if let Some(focus) = self.cursor { 340 | print = print.focused(focus == idx); 341 | } 342 | print.with_theme(&theme, |printer| { 343 | if idx > 0 { 344 | if self.active.map_or(false, |active| idx == active) 345 | || self.active.map_or(false, |active| active == (idx - 1)) 346 | { 347 | printer.print_hline((0, 0), printer.size.x, "━"); 348 | } else { 349 | printer.print_hline((0, 0), printer.size.x, "─"); 350 | } 351 | } else if self.active.map_or(false, |active| idx == active) { 352 | printer.print_hline((0, 0), printer.size.x, "━"); 353 | printer.print((horizontal_offset, 0), "┷") 354 | } else { 355 | printer.print_hline((0, 0), printer.size.x, "─"); 356 | printer.print((horizontal_offset, 0), "┴"); 357 | } 358 | printer.with_effect(Effect::Bold, |printer| { 359 | child.draw(&printer.offset((0, 1))) 360 | }); 361 | if idx == self.children.len() - 1 { 362 | let (delim, connector) = 363 | if self.active.map_or(false, |active| idx == active) { 364 | ("━", "┯") 365 | } else { 366 | ("─", "┬") 367 | }; 368 | printer.print_hline((0, printer.size.y - 1), printer.size.x, delim); 369 | printer.print( 370 | self.sizes[idx].keep_y() + Vec2::new(horizontal_offset, 1), 371 | connector, 372 | ); 373 | } 374 | }); 375 | } 376 | } 377 | } 378 | } 379 | 380 | fn layout(&mut self, vec: Vec2) { 381 | self.invalidated = false; 382 | for (child, size) in self.children.iter_mut().zip(self.sizes.iter()) { 383 | child.layout(*size); 384 | } 385 | self.last_rendered_size = vec; 386 | } 387 | 388 | fn needs_relayout(&self) -> bool { 389 | self.invalidated 390 | } 391 | 392 | fn required_size(&mut self, cst: Vec2) -> Vec2 { 393 | while self.rx.len() > 1 { 394 | // Discard old messages 395 | // This may happen if more than one view gets added to before the event loop of cursive gets started, resulting 396 | // in an incorrect start state 397 | match self.rx.try_recv() { 398 | Ok(_) => debug!("Got too many requests dropping some..."), 399 | Err(e) => debug!("Other side got dropped {:?}, ignoring this error", e), 400 | } 401 | } 402 | if let Ok(new_active) = self.rx.try_recv() { 403 | self.invalidated = true; 404 | for (idx, child) in self.children.iter().enumerate() { 405 | if new_active == child.key { 406 | self.active = Some(idx); 407 | } 408 | } 409 | } 410 | self.sizes.clear(); 411 | let sizes = &mut self.sizes; 412 | let placement = self.placement; 413 | if self.children.is_empty() { 414 | return Vec2::new(1, 1); 415 | } 416 | let total_size = self 417 | .children 418 | .iter_mut() 419 | .fold(Vec2::zero(), |mut acc, child| { 420 | let size = child.required_size(cst); 421 | match placement { 422 | Placement::HorizontalBottom | Placement::HorizontalTop => { 423 | acc = acc.stack_horizontal(&size); 424 | } 425 | Placement::VerticalLeft | Placement::VerticalRight => { 426 | acc = acc.stack_vertical(&size); 427 | } 428 | } 429 | child.pos = acc; 430 | sizes.push(size); 431 | acc 432 | }); 433 | // Total size of bar 434 | self.bar_size = total_size; 435 | // Return max width and maximum height of child 436 | // We need the max size of every side here so try again 437 | match self.placement { 438 | Placement::HorizontalTop | Placement::HorizontalBottom => { 439 | (total_size.x * 2, total_size.y).into() 440 | } 441 | Placement::VerticalLeft | Placement::VerticalRight => { 442 | (total_size.x, total_size.y * 2).into() 443 | } 444 | } 445 | } 446 | 447 | fn on_event(&mut self, evt: Event) -> EventResult { 448 | if let Event::Mouse { 449 | offset, 450 | position, 451 | event, 452 | } = evt 453 | { 454 | for (idx, child) in self.children.iter().peekable().enumerate() { 455 | if position.checked_sub(offset).is_some() 456 | && (match self.placement { 457 | Placement::HorizontalBottom | Placement::HorizontalTop => { 458 | child.pos 459 | + Vec2::new(idx + 1, 0) 460 | + Vec2::new( 461 | self.align.get_offset( 462 | // Length of buttons and delimiting characters 463 | self.bar_size.x + self.children.len() + 1, 464 | self.last_rendered_size.x, 465 | ), 466 | 0, 467 | ) 468 | } 469 | Placement::VerticalLeft | Placement::VerticalRight => { 470 | child.pos 471 | + Vec2::new(0, idx + 1) 472 | + Vec2::new( 473 | 0, 474 | self.align.get_offset( 475 | // Length of buttons and delimiting characters 476 | self.bar_size.y + self.children.len() + 1, 477 | self.last_rendered_size.y, 478 | ), 479 | ) 480 | } 481 | }) 482 | .fits(position - offset) 483 | { 484 | if let MouseEvent::Release(MouseButton::Left) = event { 485 | self.invalidated = true; 486 | self.cursor = Some(idx); 487 | return self.children[idx].on_event(Event::Key(Key::Enter)); 488 | } 489 | } 490 | } 491 | } 492 | 493 | if let Some(focus) = self.cursor { 494 | let pos = self.children[focus].pos; 495 | 496 | if let EventResult::Consumed(any) = self.children[focus].on_event(evt.relativized(pos)) 497 | { 498 | self.invalidated = true; 499 | return EventResult::Consumed(any); 500 | } 501 | } 502 | 503 | match evt { 504 | Event::Key(Key::Left) 505 | if self.placement == Placement::HorizontalBottom 506 | || self.placement == Placement::HorizontalTop => 507 | { 508 | self.decrement_idx() 509 | } 510 | Event::Key(Key::Up) 511 | if self.placement == Placement::VerticalLeft 512 | || self.placement == Placement::VerticalRight => 513 | { 514 | self.decrement_idx() 515 | } 516 | Event::Key(Key::Right) 517 | if self.placement == Placement::HorizontalBottom 518 | || self.placement == Placement::HorizontalTop => 519 | { 520 | self.increment_idx() 521 | } 522 | Event::Key(Key::Down) 523 | if self.placement == Placement::VerticalLeft 524 | || self.placement == Placement::VerticalRight => 525 | { 526 | self.increment_idx() 527 | } 528 | _ => EventResult::Ignored, 529 | } 530 | } 531 | } 532 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone, Eq, PartialEq)] 2 | pub struct IdNotFound { 3 | pub id: String, 4 | } 5 | 6 | impl std::error::Error for IdNotFound {} 7 | 8 | impl std::fmt::Display for IdNotFound { 9 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 10 | write!(f, "Id not found: {}", self.id) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate provides a tabbing view for 2 | //! [gyscos/cursive](https://github.com/gyscos/cursive) views. It is build to 3 | //! be as simple as possible. 4 | //! 5 | //! The behaviour is oriented to be similar to [`StackView`](https://docs.rs/cursive/0.13.0/cursive/views/struct.StackView.html) of cursive, but with the advantage of selectively displaying 6 | //! views without needing to delete foremost one. 7 | //! 8 | //! # Example 9 | //! All you need to do to create a new `TabView` is: 10 | //! ``` 11 | //! # use cursive::{view::Nameable, views::{TextView, Dialog}}; 12 | //! # use cursive_tabs::TabView; 13 | //! # let mut siv = cursive::default(); 14 | //! let mut tabs = TabView::new(); 15 | //! # // That is all what is needed to display an empty TabView, but of course 16 | //! # // you can add your own tabs now and switch them around as you want! 17 | //! # tabs.add_tab(TextView::new("Our first view!").with_name("First")); 18 | //! # siv.add_layer(Dialog::around(tabs)); 19 | //! # // When your done setting run cursive 20 | //! # // siv.run(); 21 | //! ``` 22 | //! You can then use the provided methods to modify the content of the `TabView` 23 | //! Consuming and non-consuming are both provided. 24 | //! 25 | //! # Full Example 26 | //! ``` 27 | //! use cursive::{view::Nameable, views::{TextView, Dialog}}; 28 | //! use cursive_tabs::TabView; 29 | //! 30 | //! let mut siv = cursive::default(); 31 | //! let mut tabs = TabView::new(); 32 | //! // That is all what is needed to display an empty TabView, but of course 33 | //! // you can add your own tabs now and switch them around as you want! 34 | //! tabs.add_tab(TextView::new("Our first view!").with_name("First")); 35 | //! siv.add_layer(Dialog::around(tabs)); 36 | //! // When your done setting run cursive 37 | //! // siv.run(); 38 | //! ``` 39 | extern crate cursive_core as cursive; 40 | 41 | use crossbeam::channel::{Receiver, Sender}; 42 | use cursive::direction::Direction; 43 | use cursive::event::{AnyCb, Event, EventResult}; 44 | use cursive::view::{CannotFocus, Selector, View, ViewNotFound}; 45 | use cursive::views::NamedView; 46 | use cursive::{Printer, Rect, Vec2}; 47 | use log::debug; 48 | use std::collections::HashMap; 49 | 50 | mod bar; 51 | mod error; 52 | mod panel; 53 | 54 | // Reexports 55 | use bar::{Bar, TabBar}; 56 | pub use panel::{Align, Placement, TabPanel}; 57 | /// Main struct which manages views 58 | pub struct TabView { 59 | current_id: Option, 60 | // Version 0.6 changes this to only contain NamedViews, in the map this remains the same type though 61 | // as NamedViews cannot be sized properly due to their enclosed view trait object 62 | map: HashMap>, 63 | key_order: Vec, 64 | bar_rx: Option>, 65 | active_key_tx: Option>, 66 | invalidated: bool, 67 | } 68 | 69 | impl Default for TabView { 70 | fn default() -> Self { 71 | Self::new() 72 | } 73 | } 74 | 75 | impl TabView { 76 | /// Returns a new TabView 77 | /// # Example 78 | /// ``` 79 | /// # use cursive::{view::Nameable, views::{TextView, Dialog}}; 80 | /// # use cursive_tabs::TabView; 81 | /// # let mut siv = cursive::default(); 82 | /// let mut tabs = TabView::new(); 83 | /// # // That is all what is needed to display an empty TabView, but of course 84 | /// # // you can add your own tabs now and switch them around as you want! 85 | /// # tabs.add_tab(TextView::new("Our first view!").with_name("First")); 86 | /// # siv.add_layer(Dialog::around(tabs)); 87 | /// # // When your done setting run cursive 88 | /// # // siv.run(); 89 | /// ``` 90 | pub fn new() -> Self { 91 | Self { 92 | current_id: None, 93 | map: HashMap::new(), 94 | key_order: Vec::new(), 95 | bar_rx: None, 96 | active_key_tx: None, 97 | invalidated: true, 98 | } 99 | } 100 | 101 | /// Returns the currently active tab Id. 102 | pub fn active_tab(&self) -> Option<&str> { 103 | self.current_id.as_deref() 104 | } 105 | 106 | /// Returns a reference to the underlying view. 107 | pub fn active_view(&self) -> Option<&dyn View> { 108 | self.active_tab() 109 | .and_then(|k| self.map.get(k).map(|v| &**v)) 110 | } 111 | 112 | /// Returns a mutable reference to the underlying view. 113 | pub fn active_view_mut(&mut self) -> Option<&mut dyn View> { 114 | if let Some(k) = self.current_id.as_ref() { 115 | self.map.get_mut(k).map(|v| &mut **v) 116 | } else { 117 | None 118 | } 119 | } 120 | 121 | pub fn views(&self) -> Vec<&dyn View> { 122 | self.map.values().map(|v| &**v).collect() 123 | } 124 | 125 | // Mutable references to all mutable views. 126 | pub fn views_mut(&mut self) -> Vec<&mut dyn View> { 127 | self.map.values_mut().map(|v| &mut **v).collect() 128 | } 129 | 130 | /// Set the currently active (visible) tab. 131 | /// If the tab id is not known, an error is returned and no action is performed. 132 | pub fn set_active_tab(&mut self, id: &str) -> Result<(), error::IdNotFound> { 133 | if self.map.contains_key(id) { 134 | if let Some(sender) = &self.active_key_tx { 135 | match sender.send(id.to_owned()) { 136 | Ok(_) => {} 137 | Err(e) => debug!( 138 | "error occured while trying to send new active key to sender: {}", 139 | e 140 | ), 141 | } 142 | } 143 | self.current_id = Some(id.to_owned()); 144 | self.invalidated = true; 145 | Ok(()) 146 | } else { 147 | Err(error::IdNotFound { id: id.to_owned() }) 148 | } 149 | } 150 | 151 | /// Set the currently active (visible) tab. 152 | /// If the tab id is not known, an error is returned and no action is performed. 153 | /// 154 | /// This is the consumable variant. 155 | pub fn with_active_tab(mut self, id: &str) -> Result { 156 | match self.set_active_tab(id) { 157 | Ok(_) => Ok(self), 158 | Err(_) => Err(self), 159 | } 160 | } 161 | 162 | /// Add a new tab to the tab view. 163 | /// The new tab will be set active and will be the visible tab for this tab view. 164 | pub fn add_tab(&mut self, view: NamedView) { 165 | let id = view.name().to_owned(); 166 | self.map.insert(id.clone(), Box::new(view)); 167 | self.key_order.push(id.clone()); 168 | self.current_id = Some(id); 169 | } 170 | 171 | /// Add a new tab to the tab view. 172 | /// The new tab will be set active and will be the visible tab for this tab view. 173 | /// 174 | /// This is the consumable variant. 175 | pub fn with_tab(mut self, view: NamedView) -> Self { 176 | self.add_tab(view); 177 | self 178 | } 179 | 180 | /// Add a new tab at a given position. 181 | /// The new tab will be set active and will be the visible tab for this tab view. 182 | /// 183 | /// This is designed to not fail, if the given position is greater than the number of current tabs, it simply will be appended. 184 | pub fn add_tab_at(&mut self, view: NamedView, pos: usize) { 185 | let id = view.name().to_owned(); 186 | self.map.insert(id.clone(), Box::new(view)); 187 | if let Some(sender) = &self.active_key_tx { 188 | match sender.send(id.clone()) { 189 | Ok(_) => {} 190 | Err(send_err) => debug!( 191 | "Could not send new key to receiver in TabBar, has it been dropped? {}", 192 | send_err 193 | ), 194 | } 195 | } 196 | self.current_id = Some(id.clone()); 197 | if self.key_order.len() > pos { 198 | self.key_order.insert(pos, id) 199 | } else { 200 | self.key_order.push(id); 201 | } 202 | } 203 | 204 | /// Add a new tab at a given position. 205 | /// The new tab will be set active and will be the visible tab for this tab view. 206 | /// 207 | /// It is designed to be fail-safe, if the given position is greater than the number of current tabs, it simply will be appended. 208 | /// 209 | /// This is the consumable variant. 210 | pub fn with_tab_at(mut self, view: NamedView, pos: usize) -> Self { 211 | self.add_tab_at(view, pos); 212 | self 213 | } 214 | 215 | /// Swap the tabs position. 216 | /// If one of the given key cannot be found, then no operation is performed. 217 | pub fn swap_tabs(&mut self, fst: &str, snd: &str) { 218 | let mut fst_pos: Option = None; 219 | let mut snd_pos: Option = None; 220 | for (pos, key) in self.tab_order().into_iter().enumerate() { 221 | match key { 222 | val if val == *fst => fst_pos = Some(pos), 223 | val if val == *snd => snd_pos = Some(pos), 224 | _ => {} 225 | } 226 | } 227 | if let (Some(fst_pos), Some(snd_pos)) = (fst_pos, snd_pos) { 228 | if let Some(cur) = self.current_id.as_ref() { 229 | if self.active_key_tx.is_some() && (fst == cur || snd == cur) { 230 | self.active_key_tx 231 | .as_mut() 232 | .unwrap() 233 | .send(cur.to_owned()) 234 | .expect("Sending failed."); 235 | } 236 | } 237 | self.key_order.swap(fst_pos, snd_pos); 238 | } 239 | } 240 | 241 | /// Removes a tab with the given id from the `TabView`. 242 | /// If the removed tab is active at the moment, the `TabView` will unfocus it and 243 | /// the focus needs to be set manually afterwards, or a new view has to be inserted. 244 | pub fn remove_tab(&mut self, id: &str) -> Result<(), error::IdNotFound> { 245 | if self.map.remove(id).is_some() { 246 | if let Some(key) = &self.current_id { 247 | if key == id { 248 | // Current id no longer valid 249 | self.current_id = None; 250 | } 251 | } 252 | // remove_key experimental 253 | self.key_order.retain(|k| k != id); 254 | self.invalidated = true; 255 | Ok(()) 256 | } else { 257 | Err(error::IdNotFound { id: id.to_owned() }) 258 | } 259 | } 260 | 261 | /// Returns the current order of keys in a vector. 262 | /// When you're implementing your own tab bar, be aware that this is the current 263 | /// tab bar and is only a copy of the original order, modification will not be 264 | /// transferred and future updates in the original not displayed. 265 | pub fn tab_order(&self) -> Vec { 266 | self.key_order.clone() 267 | } 268 | 269 | // Returns the index of the key, length of the vector if the key is not included 270 | // This can be done with out sorting 271 | fn index_key(cur_key: &str, key_order: &[String]) -> usize { 272 | for (idx, key) in key_order.iter().enumerate() { 273 | if *key == *cur_key { 274 | return idx; 275 | } 276 | } 277 | key_order.len() 278 | } 279 | 280 | /// Set the active tab to the next tab in order. 281 | pub fn next(&mut self) { 282 | if let Some(cur_key) = &self.current_id { 283 | let idx = (Self::index_key(&cur_key, &self.key_order) + 1) % self.key_order.len(); 284 | 285 | let key = &self.key_order[idx].clone(); 286 | self.set_active_tab(key) 287 | .expect("Key content changed during operation, this should not happen"); 288 | } 289 | } 290 | 291 | /// Set the active tab to the previous tab in order. 292 | pub fn prev(&mut self) { 293 | if let Some(cur_key) = self.current_id.as_ref().cloned() { 294 | let idx_key = Self::index_key(&cur_key, &self.key_order); 295 | let idx = (self.key_order.len() + idx_key - 1) % self.key_order.len(); 296 | 297 | let key = &self.key_order[idx].clone(); 298 | self.set_active_tab(key) 299 | .expect("Key content changed during operation, this should not happen"); 300 | } 301 | } 302 | 303 | /// Set the receiver for keys to be changed to 304 | pub fn set_bar_rx(&mut self, rx: Receiver) { 305 | self.bar_rx = Some(rx); 306 | } 307 | 308 | /// Set the sender for the key switched to 309 | pub fn set_active_key_tx(&mut self, tx: Sender) { 310 | self.active_key_tx = Some(tx); 311 | } 312 | } 313 | 314 | impl View for TabView { 315 | fn draw(&self, printer: &Printer) { 316 | if let Some(key) = &self.current_id { 317 | if let Some(view) = self.map.get(key) { 318 | view.draw(printer); 319 | } 320 | } 321 | } 322 | 323 | fn layout(&mut self, size: Vec2) { 324 | self.invalidated = false; 325 | if let Some(key) = &self.current_id { 326 | if let Some(view) = self.map.get_mut(key) { 327 | view.layout(size); 328 | } 329 | } 330 | } 331 | 332 | fn required_size(&mut self, req: Vec2) -> Vec2 { 333 | if let Some(rx) = &self.bar_rx { 334 | if let Ok(evt) = rx.try_recv() { 335 | match self.set_active_tab(&evt) { 336 | Ok(_) => {} 337 | Err(err) => debug!("could not accept tab bar event: {:?}", err), 338 | } 339 | } 340 | } 341 | if let Some(key) = &self.current_id { 342 | if let Some(view) = self.map.get_mut(key) { 343 | view.required_size(req) 344 | } else { 345 | (1, 1).into() 346 | } 347 | } else { 348 | (1, 1).into() 349 | } 350 | } 351 | 352 | fn on_event(&mut self, evt: Event) -> EventResult { 353 | if let Some(key) = &self.current_id { 354 | if let Some(view) = self.map.get_mut(key) { 355 | view.on_event(evt) 356 | } else { 357 | EventResult::Ignored 358 | } 359 | } else { 360 | EventResult::Ignored 361 | } 362 | } 363 | 364 | fn take_focus(&mut self, src: Direction) -> Result { 365 | if let Some(key) = &self.current_id { 366 | if let Some(view) = self.map.get_mut(key) { 367 | view.take_focus(src) 368 | } else { 369 | Err(CannotFocus) 370 | } 371 | } else { 372 | Err(CannotFocus) 373 | } 374 | } 375 | 376 | fn call_on_any<'a>(&mut self, slt: &Selector, cb: AnyCb<'a>) { 377 | for (_, view) in self.map.iter_mut() { 378 | view.call_on_any(slt, cb); 379 | } 380 | } 381 | 382 | fn focus_view(&mut self, slt: &Selector) -> Result { 383 | if let Some(key) = &self.current_id { 384 | if let Some(view) = self.map.get_mut(key) { 385 | view.focus_view(slt) 386 | } else { 387 | Err(ViewNotFound) 388 | } 389 | } else { 390 | Err(ViewNotFound) 391 | } 392 | } 393 | 394 | fn needs_relayout(&self) -> bool { 395 | self.invalidated || { 396 | if let Some(key) = &self.current_id { 397 | if let Some(view) = self.map.get(key) { 398 | view.needs_relayout() 399 | } else { 400 | false 401 | } 402 | } else { 403 | false 404 | } 405 | } 406 | } 407 | 408 | fn important_area(&self, size: Vec2) -> Rect { 409 | if let Some(key) = &self.current_id { 410 | if let Some(view) = self.map.get(key) { 411 | view.important_area(size) 412 | } else { 413 | Rect::from_point((1, 1)) 414 | } 415 | } else { 416 | Rect::from_point((1, 1)) 417 | } 418 | } 419 | } 420 | 421 | #[cfg(test)] 422 | mod test { 423 | use super::TabView; 424 | use cursive::{traits::Nameable, views::DummyView}; 425 | 426 | #[test] 427 | fn smoke() { 428 | let _ = TabView::new(); 429 | } 430 | 431 | #[test] 432 | fn insert() { 433 | let mut tabs = TabView::new().with_tab(DummyView {}.with_name("0")); 434 | tabs.add_tab(DummyView {}.with_name("1")); 435 | } 436 | 437 | #[test] 438 | fn switch() { 439 | let mut tabs = TabView::new(); 440 | tabs.add_tab(DummyView {}.with_name("0")); 441 | tabs.add_tab(DummyView {}.with_name("1")); 442 | assert_eq!(tabs.active_tab().expect("Id not correct"), "1"); 443 | tabs.set_active_tab("0").expect("Id not taken"); 444 | assert_eq!(tabs.active_tab().expect("Id not correct"), "0"); 445 | } 446 | 447 | #[test] 448 | fn remove() { 449 | let mut tabs = TabView::new(); 450 | tabs.add_tab(DummyView {}.with_name("0")); 451 | tabs.add_tab(DummyView {}.with_name("1")); 452 | assert_eq!(tabs.remove_tab("1"), Ok(())); 453 | assert!(tabs.active_tab().is_none()); 454 | } 455 | } 456 | -------------------------------------------------------------------------------- /src/panel.rs: -------------------------------------------------------------------------------- 1 | use crossbeam::channel::{unbounded, Sender}; 2 | use cursive::direction::{Absolute, Direction}; 3 | use cursive::event::{AnyCb, Event, EventResult, Key}; 4 | use cursive::view::{CannotFocus, Selector, View, ViewNotFound}; 5 | use cursive::views::NamedView; 6 | use cursive::{Printer, Vec2}; 7 | use log::debug; 8 | use num::clamp; 9 | 10 | use crate::error; 11 | use crate::Bar; 12 | use crate::TabBar; 13 | use crate::TabView; 14 | 15 | #[derive(Clone, Copy, Debug)] 16 | pub enum Align { 17 | Start, 18 | Center, 19 | End, 20 | } 21 | 22 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 23 | pub enum Placement { 24 | VerticalLeft, 25 | VerticalRight, 26 | HorizontalTop, 27 | HorizontalBottom, 28 | } 29 | 30 | impl Align { 31 | pub fn get_offset(self, content: usize, container: usize) -> usize { 32 | if container < content { 33 | 0 34 | } else { 35 | match self { 36 | Align::Start => 0, 37 | Align::Center => (container - content) / 2, 38 | Align::End => container - content, 39 | } 40 | } 41 | } 42 | } 43 | 44 | /// The `TabPanel` is an ease of use wrapper around a `TabView` and its `TabBar`. 45 | /// Additionally the TabBar in the Panel can be horizontally aligned, by default it is set to be left aligned. 46 | /// 47 | /// # Example 48 | /// ``` 49 | /// use cursive_tabs::{Align, TabPanel}; 50 | /// use cursive::views::TextView; 51 | /// use cursive::view::Nameable; 52 | /// 53 | /// let mut tabs = TabPanel::new() 54 | /// .with_tab(TextView::new("First").with_name("First")) 55 | /// .with_tab(TextView::new("Second").with_name("Second")) 56 | /// .with_bar_alignment(Align::Center); 57 | /// ``` 58 | /// 59 | /// A TabView is also usable separately, so if you prefer the tabs without the TabBar and Panel around have a look at `TabView`. 60 | pub struct TabPanel { 61 | bar: TabBar, 62 | bar_size: Vec2, 63 | tab_size: Vec2, 64 | tx: Sender, 65 | tabs: TabView, 66 | bar_focused: bool, 67 | bar_align: Align, 68 | bar_placement: Placement, 69 | } 70 | 71 | impl Default for TabPanel { 72 | fn default() -> Self { 73 | Self::new() 74 | } 75 | } 76 | 77 | impl TabPanel { 78 | /// Returns a new instance of a TabPanel. 79 | /// Alignment is set by default to left, to change this use `set_bar_alignment` to change to any other `HAlign` provided by `cursive`. 80 | pub fn new() -> Self { 81 | let mut tabs = TabView::new(); 82 | let (tx, rx) = unbounded(); 83 | let (active_tx, active_rx) = unbounded(); 84 | tabs.set_bar_rx(rx); 85 | tabs.set_active_key_tx(active_tx); 86 | Self { 87 | bar: TabBar::new(active_rx) 88 | .with_placement(Placement::HorizontalTop) 89 | .with_alignment(Align::Start), 90 | bar_size: Vec2::new(1, 1), 91 | tab_size: Vec2::new(1, 1), 92 | tabs, 93 | tx, 94 | bar_focused: true, 95 | bar_align: Align::Start, 96 | bar_placement: Placement::HorizontalTop, 97 | } 98 | } 99 | 100 | /// Returns the current active tab of the `TabView`. 101 | /// Note: Calls `active_tab` on the enclosed `TabView`. 102 | pub fn active_tab(&self) -> Option<&str> { 103 | self.tabs.active_tab() 104 | } 105 | 106 | /// Returns a reference to the underlying view. 107 | pub fn active_view(&self) -> Option<&dyn View> { 108 | self.tabs.active_view() 109 | } 110 | 111 | /// Returns a mutable reference to the underlying view. 112 | pub fn active_view_mut(&mut self) -> Option<&mut dyn View> { 113 | self.tabs.active_view_mut() 114 | } 115 | 116 | pub fn views(&self) -> Vec<&dyn View> { 117 | self.tabs.views() 118 | } 119 | 120 | pub fn views_mut(&mut self) -> Vec<&mut dyn View> { 121 | self.tabs.views_mut() 122 | } 123 | 124 | /// Non-consuming variant to set the active tab in the `TabView`. 125 | /// Note: Calls `set_active_tab` on the enclosed `TabView`. 126 | pub fn set_active_tab(&mut self, id: &str) -> Result<(), error::IdNotFound> { 127 | self.tabs.set_active_tab(id) 128 | } 129 | 130 | /// Consuming & Chainable variant to set the active tab in the `TabView`. 131 | /// Note: Calls `set_active_tab` on the enclosed `TabView`. 132 | /// 133 | pub fn with_active_tab(mut self, id: &str) -> Result { 134 | match self.tabs.set_active_tab(id) { 135 | Ok(_) => Ok(self), 136 | Err(_) => Err(self), 137 | } 138 | } 139 | 140 | /// Non-consuming variant to add new tabs to the `TabView`. 141 | /// Note: Calls `add_tab` on the enclosed `TabView`. 142 | pub fn add_tab(&mut self, view: NamedView) { 143 | let id = view.name(); 144 | self.bar.add_button(self.tx.clone(), id); 145 | self.tabs.add_tab(view); 146 | } 147 | 148 | /// Consuming & Chainable variant to add a new tab. 149 | /// Note: Calls `add_tab` on the enclosed `TabView`. 150 | pub fn with_tab(mut self, view: NamedView) -> Self { 151 | let id = view.name(); 152 | self.bar.add_button(self.tx.clone(), id); 153 | self.tabs.add_tab(view); 154 | self 155 | } 156 | 157 | /// Swaps the given tab keys. 158 | /// If at least one of them cannot be found then no operation is performed 159 | pub fn swap_tabs(&mut self, fst: &str, snd: &str) { 160 | self.tabs.swap_tabs(fst, snd); 161 | self.bar.swap_button(fst, snd); 162 | } 163 | 164 | /// Non-consuming variant to add new tabs to the `TabView` at a certain position. 165 | /// It is fail-safe, if the postion is greater than the amount of tabs, it is appended to the end. 166 | /// Note: Calls `add_tab_at` on the enclosed `TabView`. 167 | pub fn add_tab_at(&mut self, view: NamedView, pos: usize) { 168 | let id = view.name(); 169 | self.bar.add_button_at(self.tx.clone(), id, pos); 170 | self.tabs.add_tab_at(view, pos); 171 | } 172 | 173 | /// Consuming & Chainable variant to add a new tab at a certain position. 174 | /// It is fail-safe, if the postion is greater than the amount of tabs, it is appended to the end. 175 | /// Note: Calls `add_tab_at` on the enclosed `TabView`. 176 | pub fn with_tab_at(mut self, view: NamedView, pos: usize) -> Self { 177 | let id = view.name(); 178 | self.bar.add_button_at(self.tx.clone(), id, pos); 179 | self.tabs.add_tab_at(view, pos); 180 | self 181 | } 182 | 183 | /// Remove a tab of the enclosed `TabView`. 184 | pub fn remove_tab(&mut self, id: &str) -> Result<(), error::IdNotFound> { 185 | self.bar.remove_button(id); 186 | self.tabs.remove_tab(id) 187 | } 188 | 189 | /// Proceeds to the next view in order of addition. 190 | pub fn next(&mut self) { 191 | self.tabs.next() 192 | } 193 | 194 | /// Go back to the previous view in order of addition. 195 | pub fn prev(&mut self) { 196 | self.tabs.prev() 197 | } 198 | 199 | /// Consumable & Chainable variant to set the bar alignment. 200 | pub fn with_bar_alignment(mut self, align: Align) -> Self { 201 | self.set_bar_alignment(align); 202 | 203 | self 204 | } 205 | 206 | /// Non-consuming variant to set the bar alignment. 207 | pub fn set_bar_alignment(&mut self, align: Align) { 208 | self.bar_align = align; 209 | self.bar.set_alignment(align); 210 | } 211 | 212 | pub fn with_bar_placement(mut self, placement: Placement) -> Self { 213 | self.set_bar_placement(placement); 214 | self 215 | } 216 | 217 | pub fn set_bar_placement(&mut self, placement: Placement) { 218 | self.bar_placement = placement; 219 | self.bar.set_placement(placement); 220 | } 221 | 222 | /// Returns the current order of tabs as an Vector with the keys of the views. 223 | pub fn tab_order(&self) -> Vec { 224 | self.tabs.tab_order() 225 | } 226 | 227 | // Print lines corresponding to the current placement 228 | fn draw_outer_panel(&self, printer: &Printer) { 229 | match self.bar_placement { 230 | Placement::HorizontalTop => { 231 | // Side bars 232 | printer.print_vline((0, 0), printer.size.y, "│"); 233 | printer.print_vline((printer.size.x - 1, 0), printer.size.y, "│"); 234 | // Bottom line 235 | printer.print_hline((0, printer.size.y - 1), printer.size.x, "─"); 236 | 237 | printer.print((0, self.bar_size.y - 1), "┌"); 238 | printer.print((printer.size.x - 1, self.bar_size.y - 1), "┐"); 239 | printer.print((0, printer.size.y - 1), "└"); 240 | printer.print((printer.size.x - 1, printer.size.y - 1), "┘"); 241 | } 242 | Placement::HorizontalBottom => { 243 | // Side bars 244 | printer.print_vline((0, 0), printer.size.y, "│"); 245 | printer.print_vline((printer.size.x - 1, 0), printer.size.y, "│"); 246 | // Top line 247 | let lowest = clamp(printer.size.y - self.bar_size.y, 0, printer.size.y - 1); 248 | printer.print_hline((0, 0), printer.size.x, "─"); 249 | printer.print((0, 0), "┌"); 250 | printer.print((printer.size.x - 1, 0), "┐"); 251 | printer.print((0, lowest), "└"); 252 | printer.print((printer.size.x - 1, lowest), "┘"); 253 | } 254 | Placement::VerticalLeft => { 255 | // Side bar 256 | printer.print_vline((printer.size.x - 1, 0), printer.size.y, "│"); 257 | // Top lines 258 | printer.print_hline((self.bar_size.x - 1, 0), printer.size.x, "─"); 259 | printer.print_hline( 260 | (self.bar_size.x - 1, printer.size.y - 1), 261 | printer.size.x, 262 | "─", 263 | ); 264 | printer.print((self.bar_size.x - 1, 0), "┌"); 265 | printer.print((printer.size.x - 1, 0), "┐"); 266 | printer.print((self.bar_size.x - 1, printer.size.y - 1), "└"); 267 | printer.print((printer.size.x - 1, printer.size.y - 1), "┘"); 268 | } 269 | Placement::VerticalRight => { 270 | // Side bar 271 | printer.print_vline((0, 0), printer.size.y, "│"); 272 | // Top lines 273 | printer.print_hline((0, 0), printer.size.x, "─"); 274 | // Line draws too far here, needs to be overwritten with blanks 275 | printer.print_hline((0, printer.size.y - 1), printer.size.x, "─"); 276 | 277 | let right = clamp(printer.size.x - self.bar_size.x, 0, printer.size.x - 1); 278 | printer.print((0, 0), "┌"); 279 | printer.print((right, 0), "┐"); 280 | printer.print_hline((right + 1, 0), printer.size.x, " "); 281 | printer.print((0, printer.size.y - 1), "└"); 282 | printer.print((right, printer.size.y - 1), "┘"); 283 | printer.print_hline((right + 1, printer.size.y - 1), printer.size.x, " "); 284 | } 285 | } 286 | } 287 | 288 | fn on_event_focused(&mut self, evt: Event) -> EventResult { 289 | match self.bar.on_event(evt.relativized(match self.bar_placement { 290 | Placement::HorizontalTop | Placement::VerticalLeft => Vec2::new(0, 0), 291 | Placement::HorizontalBottom => self.tab_size.keep_y() + Vec2::new(0, 1), 292 | Placement::VerticalRight => self.tab_size.keep_x() + Vec2::new(1, 0), 293 | })) { 294 | EventResult::Consumed(cb) => EventResult::Consumed(cb), 295 | EventResult::Ignored => match evt { 296 | Event::Key(Key::Down) if self.bar_placement == Placement::HorizontalTop => { 297 | if let Ok(result) = self.tabs.take_focus(Direction::up()) { 298 | self.bar_focused = false; 299 | result.and(EventResult::consumed()) 300 | } else { 301 | EventResult::Ignored 302 | } 303 | } 304 | Event::Key(Key::Up) if self.bar_placement == Placement::HorizontalBottom => { 305 | if let Ok(result) = self.tabs.take_focus(Direction::down()) { 306 | self.bar_focused = false; 307 | result.and(EventResult::consumed()) 308 | } else { 309 | EventResult::Ignored 310 | } 311 | } 312 | Event::Key(Key::Left) if self.bar_placement == Placement::VerticalRight => { 313 | if let Ok(result) = self.tabs.take_focus(Direction::right()) { 314 | self.bar_focused = false; 315 | result.and(EventResult::consumed()) 316 | } else { 317 | EventResult::Ignored 318 | } 319 | } 320 | Event::Key(Key::Right) if self.bar_placement == Placement::VerticalLeft => { 321 | if let Ok(result) = self.tabs.take_focus(Direction::left()) { 322 | self.bar_focused = false; 323 | result.and(EventResult::consumed()) 324 | } else { 325 | EventResult::Ignored 326 | } 327 | } 328 | _ => EventResult::Ignored, 329 | }, 330 | } 331 | } 332 | 333 | fn on_event_unfocused(&mut self, evt: Event) -> EventResult { 334 | match self 335 | .tabs 336 | .on_event(evt.relativized(match self.bar_placement { 337 | Placement::HorizontalTop => Vec2::new(1, self.bar_size.y), 338 | Placement::VerticalLeft => Vec2::new(self.bar_size.x, 1), 339 | Placement::HorizontalBottom | Placement::VerticalRight => Vec2::new(1, 1), 340 | })) { 341 | EventResult::Consumed(cb) => EventResult::Consumed(cb), 342 | EventResult::Ignored => match evt { 343 | Event::Key(Key::Up) if self.bar_placement == Placement::HorizontalTop => { 344 | self.bar_focused = true; 345 | EventResult::Consumed(None) 346 | } 347 | Event::Key(Key::Down) if self.bar_placement == Placement::HorizontalBottom => { 348 | self.bar_focused = true; 349 | EventResult::Consumed(None) 350 | } 351 | Event::Key(Key::Left) if self.bar_placement == Placement::VerticalLeft => { 352 | self.bar_focused = true; 353 | EventResult::Consumed(None) 354 | } 355 | Event::Key(Key::Right) if self.bar_placement == Placement::VerticalRight => { 356 | self.bar_focused = true; 357 | EventResult::Consumed(None) 358 | } 359 | _ => EventResult::Ignored, 360 | }, 361 | } 362 | } 363 | 364 | fn check_focus_grab(&mut self, event: &Event) -> EventResult { 365 | if let Event::Mouse { 366 | offset, 367 | position, 368 | event, 369 | } = *event 370 | { 371 | debug!( 372 | "mouse event: offset: {:?} , position: {:?}", 373 | offset, position 374 | ); 375 | if !event.grabs_focus() { 376 | return EventResult::Ignored; 377 | } 378 | 379 | match self.bar_placement { 380 | Placement::VerticalRight | Placement::HorizontalBottom => { 381 | if position > offset && self.tab_size.fits(position - offset) { 382 | if let Ok(res) = self.tabs.take_focus(Direction::none()) { 383 | self.bar_focused = false; 384 | return res; 385 | } 386 | } else { 387 | self.bar_focused = true; 388 | } 389 | } 390 | Placement::HorizontalTop | Placement::VerticalLeft => { 391 | // Here we want conceptually position >= offset, which is what Vec2::fits does. 392 | // (The actual >= means strictly > or strictly equal, which is not _quite_ what we want in 2D.) 393 | if position.fits(offset) 394 | && (self.bar_size - Vec2::new(1, 1)).fits(position - offset) 395 | { 396 | self.bar_focused = true; 397 | } else if let Ok(res) = self.tabs.take_focus(Direction::none()) { 398 | self.bar_focused = false; 399 | return res; 400 | } 401 | } 402 | } 403 | } 404 | EventResult::Ignored 405 | } 406 | } 407 | 408 | impl View for TabPanel { 409 | fn draw(&self, printer: &Printer) { 410 | self.draw_outer_panel(printer); 411 | let printer_bar = printer 412 | .offset(match self.bar_placement { 413 | Placement::HorizontalTop => (1, 0), 414 | Placement::HorizontalBottom => ( 415 | 1, 416 | clamp(printer.size.y - self.bar_size.y, 0, printer.size.y - 1), 417 | ), 418 | Placement::VerticalLeft => (0, 1), 419 | Placement::VerticalRight => ( 420 | clamp(printer.size.x - self.bar_size.x, 0, printer.size.x - 1), 421 | 1, 422 | ), 423 | }) 424 | .cropped(match self.bar_placement { 425 | Placement::HorizontalTop | Placement::HorizontalBottom => { 426 | (printer.size.x - 2, self.bar_size.y) 427 | } 428 | Placement::VerticalRight | Placement::VerticalLeft => { 429 | (self.bar_size.x, printer.size.y - 2) 430 | } 431 | }) 432 | .focused(self.bar_focused); 433 | let printer_tab = printer 434 | .offset(match self.bar_placement { 435 | Placement::VerticalLeft => (self.bar_size.x, 1), 436 | Placement::VerticalRight => (1, 1), 437 | Placement::HorizontalBottom => (1, 1), 438 | Placement::HorizontalTop => (1, self.bar_size.y), 439 | }) 440 | // Inner area 441 | .cropped(match self.bar_placement { 442 | Placement::VerticalLeft | Placement::VerticalRight => { 443 | (printer.size.x - self.bar_size.x - 1, printer.size.y - 2) 444 | } 445 | Placement::HorizontalBottom | Placement::HorizontalTop => { 446 | (printer.size.x - 2, printer.size.y - self.bar_size.y - 1) 447 | } 448 | }) 449 | .focused(!self.bar_focused); 450 | self.bar.draw(&printer_bar); 451 | self.tabs.draw(&printer_tab); 452 | } 453 | 454 | fn layout(&mut self, vec: Vec2) { 455 | self.bar.layout(match self.bar_placement { 456 | Placement::VerticalRight | Placement::VerticalLeft => { 457 | Vec2::new(self.bar_size.x, vec.y - 2) 458 | } 459 | Placement::HorizontalBottom | Placement::HorizontalTop => { 460 | Vec2::new(vec.x - 2, self.bar_size.y) 461 | } 462 | }); 463 | self.tabs.layout(match self.bar_placement { 464 | Placement::VerticalRight | Placement::VerticalLeft => { 465 | self.tab_size = Vec2::new(vec.x - self.bar_size.x - 1, vec.y - 2); 466 | self.tab_size 467 | } 468 | Placement::HorizontalBottom | Placement::HorizontalTop => { 469 | self.tab_size = Vec2::new(vec.x - 2, vec.y - self.bar_size.y - 1); 470 | self.tab_size 471 | } 472 | }); 473 | } 474 | 475 | fn needs_relayout(&self) -> bool { 476 | self.bar.needs_relayout() || self.tabs.needs_relayout() 477 | } 478 | 479 | fn required_size(&mut self, cst: Vec2) -> Vec2 { 480 | let tab_size = self.tabs.required_size(cst); 481 | self.bar_size = self.bar.required_size(cst); 482 | match self.bar_placement { 483 | Placement::HorizontalTop | Placement::HorizontalBottom => self 484 | .bar_size 485 | .stack_vertical(&tab_size) 486 | .stack_vertical(&Vec2::new(tab_size.x + 2, 1)), 487 | Placement::VerticalLeft | Placement::VerticalRight => self 488 | .bar_size 489 | .stack_horizontal(&tab_size) 490 | .stack_vertical(&Vec2::new(1, tab_size.y + 2)), 491 | } 492 | } 493 | 494 | fn on_event(&mut self, evt: Event) -> EventResult { 495 | let result = self.check_focus_grab(&evt); 496 | 497 | result.and(if self.bar_focused { 498 | self.on_event_focused(evt) 499 | } else { 500 | self.on_event_unfocused(evt) 501 | }) 502 | } 503 | 504 | fn take_focus(&mut self, d: Direction) -> Result { 505 | let tabs_take_focus = |panel: &mut TabPanel, d: Direction| { 506 | let result = panel.tabs.take_focus(d); 507 | 508 | if result.is_ok() { 509 | panel.bar_focused = false; 510 | } else { 511 | panel.bar_focused = true; 512 | } 513 | 514 | result 515 | }; 516 | 517 | let mut result = Ok(EventResult::consumed()); 518 | 519 | match self.bar_placement { 520 | Placement::HorizontalBottom => match d { 521 | Direction::Abs(Absolute::Up) => { 522 | result = tabs_take_focus(self, d); 523 | } 524 | Direction::Abs(Absolute::Left) | Direction::Abs(Absolute::Right) => { 525 | if !self.bar_focused { 526 | result = tabs_take_focus(self, d); 527 | } 528 | } 529 | Direction::Abs(Absolute::Down) => { 530 | self.bar_focused = true; 531 | } 532 | _ => (), 533 | }, 534 | Placement::HorizontalTop => match d { 535 | Direction::Abs(Absolute::Down) => { 536 | result = tabs_take_focus(self, d); 537 | } 538 | Direction::Abs(Absolute::Left) | Direction::Abs(Absolute::Right) => { 539 | if !self.bar_focused { 540 | result = tabs_take_focus(self, d); 541 | } 542 | } 543 | Direction::Abs(Absolute::Up) => { 544 | self.bar_focused = true; 545 | } 546 | _ => (), 547 | }, 548 | Placement::VerticalLeft => match d { 549 | Direction::Abs(Absolute::Right) => { 550 | result = tabs_take_focus(self, d); 551 | } 552 | Direction::Abs(Absolute::Up) | Direction::Abs(Absolute::Down) => { 553 | if !self.bar_focused { 554 | result = tabs_take_focus(self, d); 555 | } 556 | } 557 | Direction::Abs(Absolute::Left) => self.bar_focused = true, 558 | _ => {} 559 | }, 560 | Placement::VerticalRight => match d { 561 | Direction::Abs(Absolute::Left) => { 562 | result = tabs_take_focus(self, d); 563 | } 564 | Direction::Abs(Absolute::Up) | Direction::Abs(Absolute::Down) => { 565 | if !self.bar_focused { 566 | result = tabs_take_focus(self, d) 567 | } 568 | } 569 | Direction::Abs(Absolute::Right) => self.bar_focused = true, 570 | _ => {} 571 | }, 572 | } 573 | 574 | return Ok(result.unwrap_or(EventResult::Ignored)); 575 | } 576 | 577 | fn focus_view(&mut self, slt: &Selector) -> Result { 578 | self.tabs.focus_view(slt) 579 | } 580 | 581 | fn call_on_any<'a>(&mut self, slt: &Selector, cb: AnyCb<'a>) { 582 | self.bar.call_on_any(slt, cb); 583 | self.tabs.call_on_any(slt, cb); 584 | } 585 | } 586 | -------------------------------------------------------------------------------- /tests/end2end.rs: -------------------------------------------------------------------------------- 1 | use crossbeam::channel::{Receiver, Sender}; 2 | use cursive::backends::puppet::observed::ObservedScreen; 3 | use cursive::backends::puppet::Backend; 4 | use cursive::event::{Event, Key}; 5 | use cursive::view::Nameable; 6 | use cursive::views::TextView; 7 | use cursive::Vec2; 8 | use cursive_tabs::{Align, Placement, TabPanel, TabView}; 9 | use insta::assert_display_snapshot; 10 | 11 | fn setup_test_environment(cb: F) -> (Receiver, Sender>) 12 | where 13 | F: FnOnce(&mut cursive::Cursive), 14 | { 15 | let backend = Backend::init(Some(Vec2::new(80, 24))); 16 | let frames = backend.stream(); 17 | let input = backend.input(); 18 | let mut siv = cursive::Cursive::new().into_runner(backend); 19 | cb(&mut siv); 20 | input 21 | .send(Some(Event::Refresh)) 22 | .expect("Refresh not accepted, backend not valid"); 23 | siv.step(); 24 | (frames, input) 25 | } 26 | 27 | struct TestCursive { 28 | siv: cursive::CursiveRunner, 29 | frames: Receiver, 30 | input: Sender>, 31 | } 32 | 33 | impl TestCursive { 34 | fn new(cb: F) -> Self 35 | where 36 | F: FnOnce(&mut cursive::Cursive), 37 | { 38 | let backend = Backend::init(Some(Vec2::new(80, 24))); 39 | let frames = backend.stream(); 40 | let input = backend.input(); 41 | let mut siv = cursive::Cursive::new().into_runner(backend); 42 | cb(&mut siv); 43 | input 44 | .send(Some(Event::Refresh)) 45 | .expect("Refresh not accepted, backend not valid"); 46 | siv.step(); 47 | Self { siv, frames, input } 48 | } 49 | fn _call_on(&mut self, cb: F) 50 | where 51 | F: FnOnce(&mut cursive::Cursive), 52 | { 53 | cb(&mut self.siv); 54 | } 55 | 56 | fn input(&mut self, event: Event) { 57 | self.input 58 | .send(Some(event)) 59 | .expect("Refresh not accepted, backend could not react"); 60 | self.step(); 61 | } 62 | 63 | fn step(&mut self) { 64 | self.input 65 | .send(Some(Event::Refresh)) 66 | .expect("Refresh not accepted, backend could not react"); 67 | self.siv.step(); 68 | } 69 | 70 | fn last_screen(&mut self) -> ObservedScreen { 71 | self.frames.try_iter().last().unwrap() 72 | } 73 | } 74 | 75 | #[test] 76 | fn test_puppet_screen() { 77 | let (frames, _) = setup_test_environment(|siv: &mut cursive::Cursive| { 78 | siv.add_fullscreen_layer(TextView::new( 79 | "This is a smoke test for the puppet cursive backend.", 80 | )) 81 | }); 82 | assert_display_snapshot!(frames.try_iter().last().unwrap()) 83 | } 84 | 85 | #[test] 86 | fn end2end_add_at() { 87 | let (frames, _) = setup_test_environment(|siv: &mut cursive::Cursive| { 88 | let tabs = TabView::new() 89 | .with_tab_at(TextView::new("Third").with_name("0"), 0) 90 | .with_tab_at(TextView::new("First").with_name("1"), 0) 91 | .with_tab_at(TextView::new("Second").with_name("2"), 1); 92 | siv.add_layer(tabs); 93 | }); 94 | assert_display_snapshot!(frames.try_iter().last().unwrap()); 95 | } 96 | 97 | #[test] 98 | fn end2end_add_at_action_change_tab() { 99 | let mut tsiv = TestCursive::new(|siv: &mut cursive::Cursive| { 100 | let tabs = TabView::new() 101 | .with_tab_at(TextView::new("Third").with_name("0"), 0) 102 | .with_tab_at(TextView::new("First").with_name("1"), 0) 103 | .with_tab_at(TextView::new("Second").with_name("2"), 1); 104 | siv.add_layer(tabs); 105 | }); 106 | tsiv.input(Event::Key(Key::Up)); 107 | assert_display_snapshot!(tsiv.last_screen()); 108 | } 109 | 110 | #[test] 111 | fn end2end_add_at_panel() { 112 | let (frames, _) = setup_test_environment(|siv: &mut cursive::Cursive| { 113 | let tabs = TabPanel::new() 114 | .with_tab(TextView::new("Pshhhh").with_name("Stonks")) 115 | .with_tab_at(TextView::new("Fooooo").with_name("So"), 0) 116 | .with_tab_at(TextView::new("Ahhhhh").with_name("Much"), 1) 117 | .with_bar_alignment(Align::Center); 118 | siv.add_layer(tabs); 119 | }); 120 | assert_display_snapshot!(frames.try_iter().last().unwrap()); 121 | } 122 | 123 | #[test] 124 | fn end2end_panel_smoke() { 125 | let (frames, _) = setup_test_environment(|siv: &mut cursive::Cursive| { 126 | let tabs = TabPanel::new() 127 | .with_tab(TextView::new("Pshhhh").with_name("Stronk test")) 128 | .with_active_tab("Stronk test") 129 | .unwrap_or_else(|_| panic!("Setting active tab has failed")) 130 | .with_bar_alignment(Align::Center); 131 | siv.add_layer(tabs); 132 | }); 133 | assert_display_snapshot!(frames.try_iter().last().unwrap()); 134 | } 135 | 136 | #[test] 137 | fn end2end_remove_active() { 138 | let (frames, _) = setup_test_environment(|siv: &mut cursive::Cursive| { 139 | let mut tabs = TabView::new() 140 | .with_tab(TextView::new("First").with_name("0")) 141 | .with_tab(TextView::new("Second").with_name("1")); 142 | tabs.remove_tab("1").expect("Removal of active tab failed"); 143 | siv.add_layer(tabs); 144 | }); 145 | assert_display_snapshot!(frames.try_iter().last().unwrap()); 146 | } 147 | 148 | #[test] 149 | fn end2end_remove_inactive() { 150 | let (frames, _) = setup_test_environment(|siv: &mut cursive::Cursive| { 151 | let mut tabs = TabView::new() 152 | .with_tab(TextView::new("First").with_name("0")) 153 | .with_tab(TextView::new("Second").with_name("1")); 154 | tabs.remove_tab("0").expect("Removal failed."); 155 | siv.add_layer(tabs); 156 | }); 157 | assert_display_snapshot!(frames.try_iter().last().unwrap()); 158 | } 159 | 160 | #[test] 161 | fn end2end_swap() { 162 | let (frames, _) = setup_test_environment(|siv: &mut cursive::Cursive| { 163 | let mut tabs = TabPanel::new() 164 | .with_tab(TextView::new("Pshhhh").with_name("Stonks")) 165 | .with_tab_at(TextView::new("Fooooo").with_name("So"), 0) 166 | .with_tab_at(TextView::new("Ahhhhh").with_name("Much"), 1) 167 | .with_bar_alignment(Align::Center); 168 | tabs.swap_tabs("So", "Stonks"); 169 | siv.add_layer(tabs); 170 | }); 171 | assert_display_snapshot!(frames.try_iter().last().unwrap()); 172 | } 173 | 174 | #[test] 175 | fn end2end_switch() { 176 | let (frames, _) = setup_test_environment(|siv: &mut cursive::Cursive| { 177 | let tabs = TabView::new() 178 | .with_tab(TextView::new("First").with_name("0")) 179 | .with_tab(TextView::new("Second").with_name("1")) 180 | .with_active_tab("0") 181 | .unwrap_or_else(|_| panic!("Setting active tab has failed")); 182 | siv.add_layer(tabs); 183 | }); 184 | assert_display_snapshot!(frames.try_iter().last().unwrap()); 185 | } 186 | 187 | #[test] 188 | fn end2end_vertical_left() { 189 | let (frames, _) = setup_test_environment(|siv: &mut cursive::Cursive| { 190 | let tabs = TabPanel::new() 191 | .with_tab(TextView::new("Pshhhh").with_name("Stronk test")) 192 | .with_tab(TextView::new("Pshhhh").with_name("Stronker test")) 193 | .with_active_tab("Stronk test") 194 | .unwrap_or_else(|_| panic!("Setting active tab has failed")) 195 | .with_bar_alignment(Align::Center) 196 | .with_bar_placement(Placement::VerticalLeft); 197 | siv.add_layer(tabs); 198 | }); 199 | assert_display_snapshot!(frames.try_iter().last().unwrap()); 200 | } 201 | 202 | #[test] 203 | fn end2end_vertical_left_with_action_change_tab() { 204 | let mut tsiv = TestCursive::new(|siv: &mut cursive::Cursive| { 205 | let tabs = TabPanel::new() 206 | .with_tab(TextView::new("Pshhhh").with_name("Stronk test")) 207 | .with_tab(TextView::new("Pshhhh").with_name("Stronker test")) 208 | .with_active_tab("Stronk test") 209 | .unwrap_or_else(|_| panic!("Setting active tab has failed")) 210 | .with_bar_alignment(Align::Center) 211 | .with_bar_placement(Placement::VerticalLeft); 212 | siv.add_layer(tabs); 213 | }); 214 | tsiv.input(Event::Key(Key::Up)); 215 | assert_display_snapshot!(tsiv.last_screen()); 216 | } 217 | 218 | #[test] 219 | fn end2end_vertical_right() { 220 | let (frames, _) = setup_test_environment(|siv: &mut cursive::Cursive| { 221 | let tabs = TabPanel::new() 222 | .with_tab(TextView::new("Pshhhh").with_name("Stronk test")) 223 | .with_active_tab("Stronk test") 224 | .unwrap_or_else(|_| panic!("Setting active tab has failed")) 225 | .with_bar_alignment(Align::Center) 226 | .with_bar_placement(Placement::VerticalRight); 227 | siv.add_layer(tabs); 228 | }); 229 | assert_display_snapshot!(frames.try_iter().last().unwrap()); 230 | } 231 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__end2end_add_at.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: frames.try_iter().last().unwrap() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0 | 8 | 1 | 9 | 2 | 10 | 3 | 11 | 4 | 12 | 5 | 13 | 6 | 14 | 7 | 15 | 8 | 16 | 9 | 17 | 0 | 18 | 1 Second | 19 | 2 | 20 | 3 | 21 | 4 | 22 | 5 | 23 | 6 | 24 | 7 | 25 | 8 | 26 | 9 | 27 | 0 | 28 | 1 | 29 | 2 | 30 | 3 | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__end2end_add_at_action_change_tab.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | assertion_line: 107 4 | expression: tsiv.last_screen() 5 | --- 6 | captured piece: 7 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 8 | 0 | 9 | 1 | 10 | 2 | 11 | 3 | 12 | 4 | 13 | 5 | 14 | 6 | 15 | 7 | 16 | 8 | 17 | 9 | 18 | 0 | 19 | 1 Second | 20 | 2 | 21 | 3 | 22 | 4 | 23 | 5 | 24 | 6 | 25 | 7 | 26 | 8 | 27 | 9 | 28 | 0 | 29 | 1 | 30 | 2 | 31 | 3 | 32 | x--------------------------------------------------------------------------------x 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__end2end_add_at_panel.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: frames.try_iter().last().unwrap() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0 | 8 | 1 | 9 | 2 | 10 | 3 | 11 | 4 | 12 | 5 | 13 | 6 | 14 | 7 | 15 | 8 | 16 | 9 | 17 | 0 ┌──────┤ So ┃ Much ┃ Stonks ├──────┐ | 18 | 1 │Ahhhhh │ | 19 | 2 └──────────────────────────────────┘ | 20 | 3 | 21 | 4 | 22 | 5 | 23 | 6 | 24 | 7 | 25 | 8 | 26 | 9 | 27 | 0 | 28 | 1 | 29 | 2 | 30 | 3 | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__end2end_panel_smoke.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: frames.try_iter().last().unwrap() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0 | 8 | 1 | 9 | 2 | 10 | 3 | 11 | 4 | 12 | 5 | 13 | 6 | 14 | 7 | 15 | 8 | 16 | 9 | 17 | 0 ┌────┨ Stronk test ┠─────┐ | 18 | 1 │Pshhhh │ | 19 | 2 └────────────────────────┘ | 20 | 3 | 21 | 4 | 22 | 5 | 23 | 6 | 24 | 7 | 25 | 8 | 26 | 9 | 27 | 0 | 28 | 1 | 29 | 2 | 30 | 3 | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__end2end_remove_active.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: frames.try_iter().last().unwrap() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0 | 8 | 1 | 9 | 2 | 10 | 3 | 11 | 4 | 12 | 5 | 13 | 6 | 14 | 7 | 15 | 8 | 16 | 9 | 17 | 0 | 18 | 1 | 19 | 2 | 20 | 3 | 21 | 4 | 22 | 5 | 23 | 6 | 24 | 7 | 25 | 8 | 26 | 9 | 27 | 0 | 28 | 1 | 29 | 2 | 30 | 3 | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__end2end_remove_inactive.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: frames.try_iter().last().unwrap() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0 | 8 | 1 | 9 | 2 | 10 | 3 | 11 | 4 | 12 | 5 | 13 | 6 | 14 | 7 | 15 | 8 | 16 | 9 | 17 | 0 | 18 | 1 Second | 19 | 2 | 20 | 3 | 21 | 4 | 22 | 5 | 23 | 6 | 24 | 7 | 25 | 8 | 26 | 9 | 27 | 0 | 28 | 1 | 29 | 2 | 30 | 3 | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__end2end_swap.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: frames.try_iter().last().unwrap() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0 | 8 | 1 | 9 | 2 | 10 | 3 | 11 | 4 | 12 | 5 | 13 | 6 | 14 | 7 | 15 | 8 | 16 | 9 | 17 | 0 ┌──────┤ Stonks ┃ Much ┃ So ├──────┐ | 18 | 1 │Ahhhhh │ | 19 | 2 └──────────────────────────────────┘ | 20 | 3 | 21 | 4 | 22 | 5 | 23 | 6 | 24 | 7 | 25 | 8 | 26 | 9 | 27 | 0 | 28 | 1 | 29 | 2 | 30 | 3 | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__end2end_switch.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: frames.try_iter().last().unwrap() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0 | 8 | 1 | 9 | 2 | 10 | 3 | 11 | 4 | 12 | 5 | 13 | 6 | 14 | 7 | 15 | 8 | 16 | 9 | 17 | 0 | 18 | 1 First | 19 | 2 | 20 | 3 | 21 | 4 | 22 | 5 | 23 | 6 | 24 | 7 | 25 | 8 | 26 | 9 | 27 | 0 | 28 | 1 | 29 | 2 | 30 | 3 | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__end2end_vertical_left.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: frames.try_iter().last().unwrap() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0 | 8 | 1 | 9 | 2 | 10 | 3 | 11 | 4 | 12 | 5 | 13 | 6 | 14 | 7 | 15 | 8 ┌─────┐ | 16 | 9 ━━━━━━━━━━━━━Pshhh│ | 17 | 0 Stronk test h │ | 18 | 1 ━━━━━━━━━━━━━━━ │ | 19 | 2 Stronker test │ | 20 | 3 ──────────────┬ │ | 21 | 4 └─────┘ | 22 | 5 | 23 | 6 | 24 | 7 | 25 | 8 | 26 | 9 | 27 | 0 | 28 | 1 | 29 | 2 | 30 | 3 | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__end2end_vertical_left_with_action_change_tab.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | assertion_line: 215 4 | expression: tsiv.last_screen() 5 | --- 6 | captured piece: 7 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 8 | 0 | 9 | 1 | 10 | 2 | 11 | 3 | 12 | 4 | 13 | 5 | 14 | 6 | 15 | 7 | 16 | 8 ┌─────┐ | 17 | 9 ━━━━━━━━━━━━━Pshhh│ | 18 | 0 Stronk test h │ | 19 | 1 ━━━━━━━━━━━━━━━ │ | 20 | 2 Stronker test │ | 21 | 3 ──────────────┬ │ | 22 | 4 └─────┘ | 23 | 5 | 24 | 6 | 25 | 7 | 26 | 8 | 27 | 9 | 28 | 0 | 29 | 1 | 30 | 2 | 31 | 3 | 32 | x--------------------------------------------------------------------------------x 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__end2end_vertical_right.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: frames.try_iter().last().unwrap() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0 | 8 | 1 | 9 | 2 | 10 | 3 | 11 | 4 | 12 | 5 | 13 | 6 | 14 | 7 | 15 | 8 | 16 | 9 ┌─────┐ | 17 | 0 │Pshhh┷━━━━━━━━━━━━ | 18 | 1 │h Stronk test | 19 | 2 │ ┯━━━━━━━━━━━━ | 20 | 3 └─────┘ | 21 | 4 | 22 | 5 | 23 | 6 | 24 | 7 | 25 | 8 | 26 | 9 | 27 | 0 | 28 | 1 | 29 | 2 | 30 | 3 | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__puppet_screen.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: frames.try_iter().last().unwrap() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0This is a smoke test for the puppet cursive backend. | 8 | 1 | 9 | 2 | 10 | 3 | 11 | 4 | 12 | 5 | 13 | 6 | 14 | 7 | 15 | 8 | 16 | 9 | 17 | 0 | 18 | 1 | 19 | 2 | 20 | 3 | 21 | 4 | 22 | 5 | 23 | 6 | 24 | 7 | 25 | 8 | 26 | 9 | 27 | 0 | 28 | 1 | 29 | 2 | 30 | 3 | 31 | x--------------------------------------------------------------------------------x 32 | 33 | --------------------------------------------------------------------------------