├── .gitignore ├── assets ├── demo.gif ├── README.md ├── config.yml └── demo.yml ├── .editorconfig ├── examples ├── simple.rs ├── builder.rs ├── rich.rs └── dynamic.rs ├── Cargo.toml ├── scripts ├── shields-from-tests.jq ├── ci-build.sh └── deploy-badges.sh ├── .github └── workflows │ └── test_and_push.yml ├── LICENSE ├── tests ├── snapshots │ ├── end2end__center.snap │ ├── end2end__align_center.snap │ ├── end2end__bottom_center.snap │ ├── end2end__bottom_left.snap │ ├── end2end__bottom_right.snap │ ├── end2end__center_left.snap │ ├── end2end__center_right.snap │ ├── end2end__top_center.snap │ ├── end2end__top_right.snap │ ├── end2end__align_bottom_left.snap │ ├── end2end__align_bottom_right.snap │ ├── end2end__align_center_left.snap │ ├── end2end__align_center_right.snap │ ├── end2end__align_top_center.snap │ ├── end2end__align_top_left.snap │ ├── end2end__align_top_right.snap │ ├── end2end__end2end_top_left.snap │ ├── end2end__top_left.snap │ ├── end2end__align_bottom_center.snap │ ├── end2end__exact_match_fill.snap │ ├── end2end__squeeze_underfill.snap │ ├── end2end__squeeze_underfill_center.snap │ ├── end2end__squeeze_underfill_center_left.snap │ ├── end2end__squeeze_underfill_top_center.snap │ ├── end2end__squeeze_underfill_top_left.snap │ ├── end2end__squeeze_underfill_top_right.snap │ ├── end2end__squeeze_underfill_bottom_center.snap │ ├── end2end__squeeze_underfill_bottom_left.snap │ ├── end2end__squeeze_underfill_bottom_right.snap │ └── end2end__squeeze_underfill_center_right.snap └── end2end.rs ├── README.md └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | node_modules 5 | *.temp 6 | .org 7 | -------------------------------------------------------------------------------- /assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deinstapel/cursive-aligned-view/HEAD/assets/demo.gif -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 dynamic 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 | -------------------------------------------------------------------------------- /examples/simple.rs: -------------------------------------------------------------------------------- 1 | use cursive::view::Resizable as _; 2 | use cursive::views::{DummyView, Panel}; 3 | use cursive::{Cursive, CursiveExt}; 4 | use cursive_aligned_view::Alignable as _; 5 | 6 | fn main() { 7 | let mut siv = Cursive::default(); 8 | 9 | let panel = Panel::new(DummyView) 10 | .title("Hello, world!") 11 | .fixed_width(20) 12 | .align_center() 13 | .full_screen(); 14 | 15 | siv.add_layer(panel); 16 | siv.run() 17 | } 18 | -------------------------------------------------------------------------------- /examples/builder.rs: -------------------------------------------------------------------------------- 1 | use cursive_aligned_view as _; // This needs to be imported to enable the blueprints. 2 | use serde_json::json; 3 | 4 | fn main() { 5 | // This is the same layout as the `simple` example, but using a builder config. 6 | let config = json! ({ 7 | "Panel": { 8 | "title": "Hello, world!", 9 | "view": "DummyView", 10 | "with": [ 11 | {"fixed_width": 20}, 12 | "align_center", 13 | "full_screen", 14 | ] 15 | }, 16 | }); 17 | 18 | let context = cursive::builder::Context::new(); 19 | 20 | let mut siv = cursive::default(); 21 | siv.add_layer(context.build(&config).unwrap()); 22 | siv.run(); 23 | } 24 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cursive-aligned-view" 3 | version = "0.7.0" 4 | authors = ["Fin Christensen ", "Johannes Wünsche "] 5 | edition = "2018" 6 | description = "A view wrapper for gyscos/cursive views which aligns child views" 7 | license = "BSD-3-Clause" 8 | readme = "README.md" 9 | keywords = ["cursive", "tui", "terminal", "align", "view"] 10 | repository = "https://github.com/deinstapel/cursive-aligned-view" 11 | documentation = "https://docs.rs/cursive-aligned-view" 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.4" 17 | 18 | [dev-dependencies] 19 | serde_json = "1.0.74" 20 | cursive = { version = "0.21.0", features = ["builder"] } 21 | crossbeam = "0.8.1" 22 | insta = "1.10.0" 23 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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-aligned-view.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 | -------------------------------------------------------------------------------- /examples/rich.rs: -------------------------------------------------------------------------------- 1 | use cursive::view::{Resizable, SizeConstraint}; 2 | use cursive::views::{DummyView, Panel, LinearLayout, TextView, Button}; 3 | use cursive::{Cursive, CursiveExt}; 4 | use cursive_aligned_view::Alignable; 5 | 6 | fn main() { 7 | let mut siv = Cursive::default(); 8 | 9 | let left = Panel::new(DummyView).title("Left panel"); 10 | let bottom = Panel::new(DummyView).title("Bottom panel"); 11 | let right_top = Panel::new(DummyView).title("Right top panel"); 12 | let right_bottom = Panel::new( 13 | LinearLayout::vertical() 14 | .child(TextView::new("Press this button to quit")) 15 | .child(Button::new("Quit", |s| s.quit())) 16 | .align_center() 17 | .resized(SizeConstraint::Free, SizeConstraint::Free) 18 | ).title("Right bottom panel"); 19 | 20 | let layout = LinearLayout::vertical().child( 21 | LinearLayout::horizontal().child(left).child( 22 | LinearLayout::vertical().child(right_top).child(right_bottom) 23 | ) 24 | ).child(bottom); 25 | 26 | siv.add_fullscreen_layer(layout); 27 | siv.run(); 28 | } 29 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__center.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 ┌┤ Hello, world! ├─┐ | 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__align_center.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 ┌┤ Hello, world! ├─┐ | 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__bottom_center.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 ┌┤ Hello, world! ├─┐ | 29 | 2 │ │ | 30 | 3 └──────────────────┘ | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__bottom_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 | 17 | 0 | 18 | 1 | 19 | 2 | 20 | 3 | 21 | 4 | 22 | 5 | 23 | 6 | 24 | 7 | 25 | 8 | 26 | 9 | 27 | 0 | 28 | 1┌┤ Hello, world! ├─┐ | 29 | 2│ │ | 30 | 3└──────────────────┘ | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__bottom_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 | 18 | 1 | 19 | 2 | 20 | 3 | 21 | 4 | 22 | 5 | 23 | 6 | 24 | 7 | 25 | 8 | 26 | 9 | 27 | 0 | 28 | 1 ┌┤ Hello, world! ├─┐| 29 | 2 │ │| 30 | 3 └──────────────────┘| 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__center_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 | 17 | 0┌┤ Hello, world! ├─┐ | 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__center_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 ┌┤ Hello, world! ├─┐| 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__top_center.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: frames.try_iter().last().unwrap() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0 ┌┤ Hello, world! ├─┐ | 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__top_right.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: frames.try_iter().last().unwrap() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0 ┌┤ Hello, world! ├─┐| 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__align_bottom_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 | 17 | 0 | 18 | 1 | 19 | 2 | 20 | 3 | 21 | 4 | 22 | 5 | 23 | 6 | 24 | 7 | 25 | 8 | 26 | 9 | 27 | 0 | 28 | 1┌┤ Hello, world! ├─┐ | 29 | 2│ │ | 30 | 3└──────────────────┘ | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__align_bottom_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 | 18 | 1 | 19 | 2 | 20 | 3 | 21 | 4 | 22 | 5 | 23 | 6 | 24 | 7 | 25 | 8 | 26 | 9 | 27 | 0 | 28 | 1 ┌┤ Hello, world! ├─┐| 29 | 2 │ │| 30 | 3 └──────────────────┘| 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__align_center_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 | 17 | 0┌┤ Hello, world! ├─┐ | 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__align_center_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 ┌┤ Hello, world! ├─┐| 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__align_top_center.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: frames.try_iter().last().unwrap() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0 ┌┤ Hello, world! ├─┐ | 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__align_top_left.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: frames.try_iter().last().unwrap() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0┌┤ Hello, world! ├─┐ | 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__align_top_right.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: frames.try_iter().last().unwrap() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0 ┌┤ Hello, world! ├─┐| 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_top_left.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: frames.try_iter().last().unwrap() 4 | --- 5 | captured piece: 6 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 7 | 0┌┤ Hello, world! ├─┐ | 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__top_left.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: frames.try_iter().last().unwrap() 4 | 5 | --- 6 | captured piece: 7 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 8 | 0┌┤ Hello, world! ├─┐ | 9 | 1│ │ | 10 | 2└──────────────────┘ | 11 | 3 | 12 | 4 | 13 | 5 | 14 | 6 | 15 | 7 | 16 | 8 | 17 | 9 | 18 | 0 | 19 | 1 | 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 | 34 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__align_bottom_center.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 ┌┤ Hello, world! ├─┐ | 29 | 2 │ │ | 30 | 3 └──────────────────┘ | 31 | x--------------------------------------------------------------------------------x 32 | 33 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__exact_match_fill.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: frames.try_iter().last().unwrap() 4 | 5 | --- 6 | captured piece: 7 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 8 | 0┌────────────────────┤ Hello, world! ├─────────────────────┐ | 9 | 1│A very long text that will reach the limit of some screens│ | 10 | 2└──────────────────────────────────────────────────────────┘ | 11 | 3 | 12 | 4 | 13 | 5 | 14 | 6 | 15 | 7 | 16 | 8 | 17 | 9 | 18 | 0 | 19 | 1 | 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 | 34 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__squeeze_underfill.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | expression: frames.try_iter().last().unwrap() 4 | 5 | --- 6 | captured piece: 7 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 8 | 0┌─────────────┐ | 9 | 1│A very long │ | 10 | 2└─────────────┘ | 11 | 3 | 12 | 4 | 13 | 5 | 14 | 6 | 15 | 7 | 16 | 8 | 17 | 9 | 18 | 0 | 19 | 1 | 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 | 34 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__squeeze_underfill_center.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | assertion_line: 94 4 | expression: frames.try_iter().last().unwrap() 5 | --- 6 | captured piece: 7 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 8 | 0┌┤ Hello, wo ├┐ | 9 | 1│A very long │ | 10 | 2└─────────────┘ | 11 | 3 | 12 | 4 | 13 | 5 | 14 | 6 | 15 | 7 | 16 | 8 | 17 | 9 | 18 | 0 | 19 | 1 | 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 | 34 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__squeeze_underfill_center_left.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | assertion_line: 80 4 | expression: frames.try_iter().last().unwrap() 5 | --- 6 | captured piece: 7 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 8 | 0┌┤ Hello, wo ├┐ | 9 | 1│A very long │ | 10 | 2└─────────────┘ | 11 | 3 | 12 | 4 | 13 | 5 | 14 | 6 | 15 | 7 | 16 | 8 | 17 | 9 | 18 | 0 | 19 | 1 | 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 | 34 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__squeeze_underfill_top_center.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | assertion_line: 52 4 | expression: frames.try_iter().last().unwrap() 5 | --- 6 | captured piece: 7 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 8 | 0┌┤ Hello, wo ├┐ | 9 | 1│A very long │ | 10 | 2└─────────────┘ | 11 | 3 | 12 | 4 | 13 | 5 | 14 | 6 | 15 | 7 | 16 | 8 | 17 | 9 | 18 | 0 | 19 | 1 | 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 | 34 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__squeeze_underfill_top_left.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | assertion_line: 38 4 | expression: frames.try_iter().last().unwrap() 5 | --- 6 | captured piece: 7 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 8 | 0┌┤ Hello, wo ├┐ | 9 | 1│A very long │ | 10 | 2└─────────────┘ | 11 | 3 | 12 | 4 | 13 | 5 | 14 | 6 | 15 | 7 | 16 | 8 | 17 | 9 | 18 | 0 | 19 | 1 | 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 | 34 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__squeeze_underfill_top_right.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | assertion_line: 66 4 | expression: frames.try_iter().last().unwrap() 5 | --- 6 | captured piece: 7 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 8 | 0┌┤ Hello, wo ├┐ | 9 | 1│A very long │ | 10 | 2└─────────────┘ | 11 | 3 | 12 | 4 | 13 | 5 | 14 | 6 | 15 | 7 | 16 | 8 | 17 | 9 | 18 | 0 | 19 | 1 | 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 | 34 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__squeeze_underfill_bottom_center.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | assertion_line: 136 4 | expression: frames.try_iter().last().unwrap() 5 | --- 6 | captured piece: 7 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 8 | 0┌┤ Hello, wo ├┐ | 9 | 1│A very long │ | 10 | 2└─────────────┘ | 11 | 3 | 12 | 4 | 13 | 5 | 14 | 6 | 15 | 7 | 16 | 8 | 17 | 9 | 18 | 0 | 19 | 1 | 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 | 34 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__squeeze_underfill_bottom_left.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | assertion_line: 122 4 | expression: frames.try_iter().last().unwrap() 5 | --- 6 | captured piece: 7 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 8 | 0┌┤ Hello, wo ├┐ | 9 | 1│A very long │ | 10 | 2└─────────────┘ | 11 | 3 | 12 | 4 | 13 | 5 | 14 | 6 | 15 | 7 | 16 | 8 | 17 | 9 | 18 | 0 | 19 | 1 | 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 | 34 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__squeeze_underfill_bottom_right.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | assertion_line: 150 4 | expression: frames.try_iter().last().unwrap() 5 | --- 6 | captured piece: 7 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 8 | 0┌┤ Hello, wo ├┐ | 9 | 1│A very long │ | 10 | 2└─────────────┘ | 11 | 3 | 12 | 4 | 13 | 5 | 14 | 6 | 15 | 7 | 16 | 8 | 17 | 9 | 18 | 0 | 19 | 1 | 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 | 34 | -------------------------------------------------------------------------------- /tests/snapshots/end2end__squeeze_underfill_center_right.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/end2end.rs 3 | assertion_line: 108 4 | expression: frames.try_iter().last().unwrap() 5 | --- 6 | captured piece: 7 | x01234567890123456789012345678901234567890123456789012345678901234567890123456789x 8 | 0┌┤ Hello, wo ├┐ | 9 | 1│A very long │ | 10 | 2└─────────────┘ | 11 | 3 | 12 | 4 | 13 | 5 | 14 | 6 | 15 | 7 | 16 | 8 | 17 | 9 | 18 | 0 | 19 | 1 | 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 | 34 | -------------------------------------------------------------------------------- /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/dynamic 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 | -------------------------------------------------------------------------------- /examples/dynamic.rs: -------------------------------------------------------------------------------- 1 | use cursive::view::{Nameable, Resizable}; 2 | use cursive::views::{DummyView, Panel, ResizedView}; 3 | use cursive::{Cursive, CursiveExt}; 4 | use cursive_aligned_view::{Alignable, AlignedView}; 5 | 6 | use std::time::Duration; 7 | 8 | type ChildView = ResizedView>; 9 | 10 | fn main() { 11 | let mut siv = Cursive::default(); 12 | 13 | let panel = Panel::new(DummyView) 14 | .title("Hello, world!") 15 | .fixed_width(20) 16 | .align_top_left() 17 | .with_name("panel") 18 | .full_screen(); 19 | 20 | let sink = siv.cb_sink().clone(); 21 | std::thread::spawn(move || { 22 | std::thread::sleep(Duration::from_secs(1)); 23 | sink.send(Box::new(|siv| { 24 | siv.call_on_name("panel", |view: &mut AlignedView| { 25 | view.set_top_center() 26 | }); 27 | })) 28 | .expect("alignment change failed"); 29 | 30 | std::thread::sleep(Duration::from_secs(1)); 31 | sink.send(Box::new(|siv| { 32 | siv.call_on_name("panel", |view: &mut AlignedView| { 33 | view.set_top_right() 34 | }); 35 | })) 36 | .expect("alignment change failed"); 37 | 38 | std::thread::sleep(Duration::from_secs(1)); 39 | sink.send(Box::new(|siv| { 40 | siv.call_on_name("panel", |view: &mut AlignedView| { 41 | view.set_center_left() 42 | }); 43 | })) 44 | .expect("alignment change failed"); 45 | 46 | std::thread::sleep(Duration::from_secs(1)); 47 | sink.send(Box::new(|siv| { 48 | siv.call_on_name("panel", |view: &mut AlignedView| { 49 | view.set_center() 50 | }); 51 | })) 52 | .expect("alignment change failed"); 53 | 54 | std::thread::sleep(Duration::from_secs(1)); 55 | sink.send(Box::new(|siv| { 56 | siv.call_on_name("panel", |view: &mut AlignedView| { 57 | view.set_center_right() 58 | }); 59 | })) 60 | .expect("alignment change failed"); 61 | 62 | std::thread::sleep(Duration::from_secs(1)); 63 | sink.send(Box::new(|siv| { 64 | siv.call_on_name("panel", |view: &mut AlignedView| { 65 | view.set_bottom_left() 66 | }); 67 | })) 68 | .expect("alignment change failed"); 69 | 70 | std::thread::sleep(Duration::from_secs(1)); 71 | sink.send(Box::new(|siv| { 72 | siv.call_on_name("panel", |view: &mut AlignedView| { 73 | view.set_bottom_center() 74 | }); 75 | })) 76 | .expect("alignment change failed"); 77 | 78 | std::thread::sleep(Duration::from_secs(1)); 79 | sink.send(Box::new(|siv| { 80 | siv.call_on_name("panel", |view: &mut AlignedView| { 81 | view.set_bottom_right() 82 | }); 83 | })) 84 | .expect("alignment change failed"); 85 | 86 | std::thread::sleep(Duration::from_secs(1)); 87 | sink.send(Box::new(|siv| { 88 | siv.quit(); 89 | })) 90 | .expect("alignment change failed"); 91 | }); 92 | 93 | siv.add_layer(panel); 94 | siv.run() 95 | } 96 | -------------------------------------------------------------------------------- /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/dynamic 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: "#ff7fac" 105 | brightGreen: "#c8ed71" 106 | brightYellow: "#ebdf86" 107 | brightBlue: "#75dff2" 108 | brightMagenta: "#ae89fe" 109 | brightCyan: "#b1c6ca" 110 | brightWhite: "#f9f9f4" 111 | 112 | # Records, feel free to edit them 113 | records: 114 | - delay: 396 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\n\n\n\e[30m\e[47m\e[J\e[H┌┤ \e[31m\e[47mHello, world!\e[30m\e[47m ├─┐\e[K\r\n│\e[18X\e[2;20H│\e[K\r\n└──────────────────┘\e[K\e[H\t\t\e[39;49m\e(B\e[m" 116 | - delay: 1022 117 | content: "\e[1;30H\e[30m\e[47m\e[1K ┌┤ \e[31m\e[47mHello, world!\e[30m\e[47m ├─┐\e[2;30H\e[1K │\e[2;50H│\e[3;30H\e[1K └──────────────────┘\e[1;47H\e[39;49m\e(B\e[m" 118 | - delay: 998 119 | content: "\e[1;60H\e[30m\e[47m\e[1K ┌┤ \e[31m\e[47mHello, world!\e[30m\e[47m ├─┐\e[2;60H\e[1K │\e[80G│\e[3;60H\e[1K └──────────────────┘\e[1;77H\e[39;49m\e(B\e[m" 120 | - delay: 998 121 | content: "\r\e[30m\e[47m\e[K\n\e[K\n\e[K\e[11d┌┤ \e[31m\e[47mHello, world!\e[30m\e[47m ├─┐\r\n│\e[12;20H│\r\n└──────────────────┘\e[11;17H\e[39;49m\e(B\e[m" 122 | - delay: 998 123 | content: "\e[11;30H\e[30m\e[47m\e[1K ┌┤ \e[31m\e[47mHello, world!\e[30m\e[47m ├─┐\e[12;30H\e[1K │\e[12;50H│\e[13;30H\e[1K └──────────────────┘\e[11;47H\e[39;49m\e(B\e[m" 124 | - delay: 998 125 | content: "\e[11;60H\e[30m\e[47m\e[1K ┌┤ \e[31m\e[47mHello, world!\e[30m\e[47m ├─┐\e[12;60H\e[1K │\e[80G│\e[13;60H\e[1K └──────────────────┘\e[11;77H\e[39;49m\e(B\e[m" 126 | - delay: 997 127 | content: "\r\e[30m\e[47m\e[K\n\e[K\n\e[K\e[22d┌┤ \e[31m\e[47mHello, world!\e[30m\e[47m ├─┐\r\n│\e[23;20H│\r\n└──────────────────┘\e[22;17H\e[39;49m\e(B\e[m" 128 | - delay: 998 129 | content: "\e[22;30H\e[30m\e[47m\e[1K ┌┤ \e[31m\e[47mHello, world!\e[30m\e[47m ├─┐\e[23;30H\e[1K │\e[23;50H│\e[24;30H\e[1K └──────────────────┘\e[22;47H\e[39;49m\e(B\e[m" 130 | - delay: 1007 131 | content: "\e[22;60H\e[30m\e[47m\e[1K ┌┤ \e[31m\e[47mHello, world!\e[30m\e[47m ├─┐\e[23;60H\e[1K │\e[80G│\e[24;60H\e[1K └──────────────────\e[?7l┘\e[?7h\e[22;77H\e[39;49m\e(B\e[m" 132 | - delay: 987 133 | content: "\e[?1002l\e[?1006;1000l\e[24;1H\e[?12l\e[?25h\e[?1049l\e[23;0;0t\r\e[?1l\e>" 134 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Welcome to cursive-aligned-view 👋

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 view wrapper for 26 | gyscos/cursive 27 | views which aligns child views 28 |

29 | 30 | --- 31 | 32 | This project provides an `AlignedView` for [gyscos/cursive](https://github.com/gyscos/cursive) views which makes it possible to align the child view (center, left, right, top, bottom). The `AlignedView` uses the `required_size` reported by the child view and fills the rest of the available space with the views background color. 33 | 34 | ## How does it look like? `demo` [![terminalizer](https://img.shields.io/badge/GIF-terminalizer-blueviolet.svg)](https://github.com/faressoft/terminalizer) 35 | 36 |
37 | Expand to view 38 | aligned-view demo 39 |
40 | 41 | ## Usage 42 | 43 | Simply add to your `Cargo.toml` 44 | 45 | ```toml 46 | [dependencies] 47 | cursive-aligned-view = "^0" 48 | ``` 49 | 50 | ### Aligning a child view 51 | 52 | The easiest way to align a view is via the `Alignable` trait: 53 | 54 | ```rust 55 | use cursive_aligned_view::Alignable; 56 | 57 | let aligned = child_view.align_center(); 58 | ``` 59 | 60 | This is the preferred way as it is *chainable* and consistent with cursive's `Boxable` and `Identifiable` traits. 61 | 62 | As an alternative you can use the `AlignedView` constructors directly: 63 | 64 | ```rust 65 | use cursive_aligned_view::AlignedView; 66 | 67 | let aligned = AlignedView::with_center(child_view); 68 | ``` 69 | 70 | Look into the [documentation](https://docs.rs/cursive-aligned-view) for a detailed explanation on the API. 71 | 72 | ### Supported Alignments 73 | 74 | | Alignment | Construction method | 75 | |---------------|-----------------------| 76 | | top left | `align_top_left` | 77 | | top center | `align_top_center` | 78 | | top right | `align_top_right` | 79 | | center left | `align_center_left` | 80 | | center | `align_center` | 81 | | center right | `align_center_right` | 82 | | bottom left | `align_bottom_left` | 83 | | bottom center | `align_bottom_center` | 84 | | bottom right | `align_bottom_right` | 85 | 86 | ## Troubleshooting 87 | 88 | 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. 89 | 90 | ## Development [![cargo test](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fdeinstapel.github.io%2Fcursive-aligned-view%2Fcargo-test.json)](https://github.com/deinstapel/cursive-aligned-view/actions) 91 | 92 | ### Running the tests 93 | 94 | #### Preparing integration tests 95 | 96 | To perform the test it's advisable to install [`cargo-insta`](https://github.com/mitsuhiko/insta) in case your tests fail and you want to inspect the received output. 97 | ``` 98 | $ cargo install cargo-insta 99 | ``` 100 | 101 | You will also need insta to validate new tests you add or update older ones. 102 | 103 | #### Running all test suites 104 | 105 | Just run 106 | 107 | ``` 108 | $ cargo test 109 | ``` 110 | 111 | to execute all available tests. 112 | 113 | --- 114 | 115 | Or if you want to interactively inspect failed tests 116 | ``` 117 | $ cargo insta test 118 | ``` 119 | 120 | #### shields.io endpoints 121 | 122 | [shields.io](https://shields.io) endpoints are generated inside the `./target/shields` folder. They are used in this README. 123 | 124 | ## Authors 125 | 126 | **Fin Christensen** 127 | 128 | > [:octocat: `@fin-ger`](https://github.com/fin-ger) 129 | > [:elephant: `@fin_ger@weirder.earth`](https://weirder.earth/@fin_ger) 130 | > [:bird: `@fin_ger_github`](https://twitter.com/fin_ger_github) 131 | 132 |
133 | 134 | **Johannes Wünsche** 135 | 136 | > [:octocat: `@jwuensche`](https://github.com/jwuensche) 137 | > [:elephant: `@fredowald@mastodon.social`](https://mastodon.social/web/accounts/843376) 138 | > [:bird: `@Fredowald`](https://twitter.com/fredowald) 139 | 140 | ## Show your support 141 | 142 | Give a :star: if this project helped you! 143 | -------------------------------------------------------------------------------- /tests/end2end.rs: -------------------------------------------------------------------------------- 1 | use crossbeam::channel::{Receiver, Sender}; 2 | use cursive::backends::puppet::{observed::ObservedScreen, Backend}; 3 | use cursive::event::Event; 4 | use cursive::view::{Resizable, SizeConstraint}; 5 | use cursive::views::{DummyView, Panel, TextView}; 6 | use cursive::Vec2; 7 | use cursive_aligned_view::{Alignable, AlignedView}; 8 | use cursive_core::align::{Align, HAlign, VAlign}; 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 | #[test] 28 | fn squeeze_underfill_top_left() { 29 | let (frames, _) = setup_test_environment(|siv| { 30 | let panel = Panel::new(TextView::new( 31 | "A very long text that will reach the limit of some screens", 32 | )) 33 | .title("Hello, world!"); 34 | let aligned = AlignedView::with_top_left(panel) 35 | .resized(SizeConstraint::Fixed(15), SizeConstraint::Fixed(3)); 36 | siv.add_fullscreen_layer(aligned); 37 | }); 38 | assert_display_snapshot!(frames.try_iter().last().unwrap()); 39 | } 40 | 41 | #[test] 42 | fn squeeze_underfill_top_center() { 43 | let (frames, _) = setup_test_environment(|siv| { 44 | let panel = Panel::new(TextView::new( 45 | "A very long text that will reach the limit of some screens", 46 | )) 47 | .title("Hello, world!"); 48 | let aligned = AlignedView::with_top_center(panel) 49 | .resized(SizeConstraint::Fixed(15), SizeConstraint::Fixed(3)); 50 | siv.add_fullscreen_layer(aligned); 51 | }); 52 | assert_display_snapshot!(frames.try_iter().last().unwrap()); 53 | } 54 | 55 | #[test] 56 | fn squeeze_underfill_top_right() { 57 | let (frames, _) = setup_test_environment(|siv| { 58 | let panel = Panel::new(TextView::new( 59 | "A very long text that will reach the limit of some screens", 60 | )) 61 | .title("Hello, world!"); 62 | let aligned = AlignedView::with_top_right(panel) 63 | .resized(SizeConstraint::Fixed(15), SizeConstraint::Fixed(3)); 64 | siv.add_fullscreen_layer(aligned); 65 | }); 66 | assert_display_snapshot!(frames.try_iter().last().unwrap()); 67 | } 68 | 69 | #[test] 70 | fn squeeze_underfill_center_left() { 71 | let (frames, _) = setup_test_environment(|siv| { 72 | let panel = Panel::new(TextView::new( 73 | "A very long text that will reach the limit of some screens", 74 | )) 75 | .title("Hello, world!"); 76 | let aligned = AlignedView::with_center_left(panel) 77 | .resized(SizeConstraint::Fixed(15), SizeConstraint::Fixed(3)); 78 | siv.add_fullscreen_layer(aligned); 79 | }); 80 | assert_display_snapshot!(frames.try_iter().last().unwrap()); 81 | } 82 | 83 | #[test] 84 | fn squeeze_underfill_center() { 85 | let (frames, _) = setup_test_environment(|siv| { 86 | let panel = Panel::new(TextView::new( 87 | "A very long text that will reach the limit of some screens", 88 | )) 89 | .title("Hello, world!"); 90 | let aligned = AlignedView::with_center(panel) 91 | .resized(SizeConstraint::Fixed(15), SizeConstraint::Fixed(3)); 92 | siv.add_fullscreen_layer(aligned); 93 | }); 94 | assert_display_snapshot!(frames.try_iter().last().unwrap()); 95 | } 96 | 97 | #[test] 98 | fn squeeze_underfill_center_right() { 99 | let (frames, _) = setup_test_environment(|siv| { 100 | let panel = Panel::new(TextView::new( 101 | "A very long text that will reach the limit of some screens", 102 | )) 103 | .title("Hello, world!"); 104 | let aligned = AlignedView::with_center_right(panel) 105 | .resized(SizeConstraint::Fixed(15), SizeConstraint::Fixed(3)); 106 | siv.add_fullscreen_layer(aligned); 107 | }); 108 | assert_display_snapshot!(frames.try_iter().last().unwrap()); 109 | } 110 | 111 | #[test] 112 | fn squeeze_underfill_bottom_left() { 113 | let (frames, _) = setup_test_environment(|siv| { 114 | let panel = Panel::new(TextView::new( 115 | "A very long text that will reach the limit of some screens", 116 | )) 117 | .title("Hello, world!"); 118 | let aligned = AlignedView::with_bottom_left(panel) 119 | .resized(SizeConstraint::Fixed(15), SizeConstraint::Fixed(3)); 120 | siv.add_fullscreen_layer(aligned); 121 | }); 122 | assert_display_snapshot!(frames.try_iter().last().unwrap()); 123 | } 124 | 125 | #[test] 126 | fn squeeze_underfill_bottom_center() { 127 | let (frames, _) = setup_test_environment(|siv| { 128 | let panel = Panel::new(TextView::new( 129 | "A very long text that will reach the limit of some screens", 130 | )) 131 | .title("Hello, world!"); 132 | let aligned = AlignedView::with_bottom_center(panel) 133 | .resized(SizeConstraint::Fixed(15), SizeConstraint::Fixed(3)); 134 | siv.add_fullscreen_layer(aligned); 135 | }); 136 | assert_display_snapshot!(frames.try_iter().last().unwrap()); 137 | } 138 | 139 | #[test] 140 | fn squeeze_underfill_bottom_right() { 141 | let (frames, _) = setup_test_environment(|siv| { 142 | let panel = Panel::new(TextView::new( 143 | "A very long text that will reach the limit of some screens", 144 | )) 145 | .title("Hello, world!"); 146 | let aligned = AlignedView::with_bottom_right(panel) 147 | .resized(SizeConstraint::Fixed(15), SizeConstraint::Fixed(3)); 148 | siv.add_fullscreen_layer(aligned); 149 | }); 150 | assert_display_snapshot!(frames.try_iter().last().unwrap()); 151 | } 152 | 153 | #[test] 154 | fn exact_match_fill() { 155 | let (frames, _) = setup_test_environment(|siv| { 156 | let panel = Panel::new(TextView::new( 157 | "A very long text that will reach the limit of some screens", 158 | )) 159 | .title("Hello, world!"); 160 | let aligned = AlignedView::with_top_left(panel) 161 | .resized(SizeConstraint::Fixed(60), SizeConstraint::Fixed(3)); 162 | siv.add_fullscreen_layer(aligned); 163 | }); 164 | assert_display_snapshot!(frames.try_iter().last().unwrap()); 165 | } 166 | 167 | #[test] 168 | fn top_left() { 169 | let (frames, _) = setup_test_environment(|siv| { 170 | let panel = Panel::new(DummyView).title("Hello, world!").fixed_width(20); 171 | let aligned = 172 | AlignedView::with_top_left(panel).resized(SizeConstraint::Full, SizeConstraint::Full); 173 | siv.add_fullscreen_layer(aligned); 174 | }); 175 | assert_display_snapshot!(frames.try_iter().last().unwrap()); 176 | } 177 | #[test] 178 | fn top_center() { 179 | let (frames, _) = setup_test_environment(|siv| { 180 | let panel = Panel::new(DummyView).title("Hello, world!").fixed_width(20); 181 | let aligned = 182 | AlignedView::with_top_center(panel).resized(SizeConstraint::Full, SizeConstraint::Full); 183 | siv.add_fullscreen_layer(aligned); 184 | }); 185 | assert_display_snapshot!(frames.try_iter().last().unwrap()); 186 | } 187 | #[test] 188 | fn top_right() { 189 | let (frames, _) = setup_test_environment(|siv| { 190 | let panel = Panel::new(DummyView).title("Hello, world!").fixed_width(20); 191 | let aligned = 192 | AlignedView::with_top_right(panel).resized(SizeConstraint::Full, SizeConstraint::Full); 193 | siv.add_fullscreen_layer(aligned); 194 | }); 195 | assert_display_snapshot!(frames.try_iter().last().unwrap()); 196 | } 197 | #[test] 198 | fn center_left() { 199 | let (frames, _) = setup_test_environment(|siv| { 200 | let panel = Panel::new(DummyView).title("Hello, world!").fixed_width(20); 201 | let aligned = AlignedView::with_center_left(panel) 202 | .resized(SizeConstraint::Full, SizeConstraint::Full); 203 | siv.add_fullscreen_layer(aligned); 204 | }); 205 | assert_display_snapshot!(frames.try_iter().last().unwrap()); 206 | } 207 | #[test] 208 | fn center() { 209 | let (frames, _) = setup_test_environment(|siv| { 210 | let panel = Panel::new(DummyView).title("Hello, world!").fixed_width(20); 211 | let aligned = 212 | AlignedView::with_center(panel).resized(SizeConstraint::Full, SizeConstraint::Full); 213 | siv.add_fullscreen_layer(aligned); 214 | }); 215 | assert_display_snapshot!(frames.try_iter().last().unwrap()); 216 | } 217 | #[test] 218 | fn center_right() { 219 | let (frames, _) = setup_test_environment(|siv| { 220 | let panel = Panel::new(DummyView).title("Hello, world!").fixed_width(20); 221 | let aligned = AlignedView::with_center_right(panel) 222 | .resized(SizeConstraint::Full, SizeConstraint::Full); 223 | siv.add_fullscreen_layer(aligned); 224 | }); 225 | assert_display_snapshot!(frames.try_iter().last().unwrap()); 226 | } 227 | #[test] 228 | fn bottom_left() { 229 | let (frames, _) = setup_test_environment(|siv| { 230 | let panel = Panel::new(DummyView).title("Hello, world!").fixed_width(20); 231 | let aligned = AlignedView::with_bottom_left(panel) 232 | .resized(SizeConstraint::Full, SizeConstraint::Full); 233 | siv.add_fullscreen_layer(aligned); 234 | }); 235 | assert_display_snapshot!(frames.try_iter().last().unwrap()); 236 | } 237 | #[test] 238 | fn bottom_center() { 239 | let (frames, _) = setup_test_environment(|siv| { 240 | let panel = Panel::new(DummyView).title("Hello, world!").fixed_width(20); 241 | let aligned = AlignedView::with_bottom_center(panel) 242 | .resized(SizeConstraint::Full, SizeConstraint::Full); 243 | siv.add_fullscreen_layer(aligned); 244 | }); 245 | assert_display_snapshot!(frames.try_iter().last().unwrap()); 246 | } 247 | #[test] 248 | fn bottom_right() { 249 | let (frames, _) = setup_test_environment(|siv| { 250 | let panel = Panel::new(DummyView).title("Hello, world!").fixed_width(20); 251 | let aligned = AlignedView::with_bottom_right(panel) 252 | .resized(SizeConstraint::Full, SizeConstraint::Full); 253 | siv.add_fullscreen_layer(aligned); 254 | }); 255 | assert_display_snapshot!(frames.try_iter().last().unwrap()); 256 | } 257 | #[test] 258 | fn align_top_left() { 259 | let panel = Panel::new(DummyView) 260 | .title("Hello, world!") 261 | .align_top_left(); 262 | assert_eq!(*panel.alignment(), Align::top_left()) 263 | } 264 | #[test] 265 | fn align_top_center() { 266 | let panel = Panel::new(DummyView) 267 | .title("Hello, world!") 268 | .align_top_center(); 269 | assert_eq!(*panel.alignment(), Align::top_center()) 270 | } 271 | #[test] 272 | fn align_top_right() { 273 | let panel = Panel::new(DummyView) 274 | .title("Hello, world!") 275 | .align_top_right(); 276 | assert_eq!(*panel.alignment(), Align::top_right()) 277 | } 278 | #[test] 279 | fn align_center_left() { 280 | let panel = Panel::new(DummyView) 281 | .title("Hello, world!") 282 | .align_center_left(); 283 | assert_eq!(*panel.alignment(), Align::center_left()) 284 | } 285 | #[test] 286 | fn align_center() { 287 | let panel = Panel::new(DummyView).title("Hello, world!").align_center(); 288 | assert_eq!(*panel.alignment(), Align::center()) 289 | } 290 | #[test] 291 | fn align_center_right() { 292 | let panel = Panel::new(DummyView) 293 | .title("Hello, world!") 294 | .align_center_right(); 295 | assert_eq!(*panel.alignment(), Align::center_right()) 296 | } 297 | #[test] 298 | fn align_bottom_left() { 299 | let panel = Panel::new(DummyView) 300 | .title("Hello, world!") 301 | .align_bottom_left(); 302 | assert_eq!(*panel.alignment(), Align::bot_left()); 303 | } 304 | #[test] 305 | fn align_bottom_center() { 306 | let panel = Panel::new(DummyView) 307 | .title("Hello, world!") 308 | .align_bottom_center(); 309 | assert_eq!(*panel.alignment(), Align::bot_center()); 310 | } 311 | #[test] 312 | fn align_bottom_right() { 313 | let panel = Panel::new(DummyView) 314 | .title("Hello, world!") 315 | .align_bottom_right(); 316 | assert_eq!( 317 | *panel.alignment(), 318 | Align::new(HAlign::Right, VAlign::Bottom) 319 | ); 320 | } 321 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Align cursive views 2 | //! 3 | //! This crate provides an `AlignedView` for 4 | //! [gyscos/cursive](https://github.com/gyscos/cursive) views which makes it 5 | //! possible to align the child view (center, left, right, top, bottom). The 6 | //! `AlignedView` uses the `required_size` reported by the child view and fills 7 | //! the rest of the available space with the views background color. 8 | //! 9 | //! ## Aligning a child view 10 | //! 11 | //! The easiest way to align a view is via the `Alignable` trait: 12 | //! 13 | //! ```rust 14 | //! use cursive::{Cursive, CursiveExt}; 15 | //! use cursive::view::Resizable; 16 | //! use cursive::views::{Panel, DummyView}; 17 | //! use cursive_aligned_view::Alignable; 18 | //! 19 | //! fn main() { 20 | //! let mut siv = Cursive::default(); 21 | //! 22 | //! let panel = Panel::new(DummyView) 23 | //! .title("Hello, world!") 24 | //! .fixed_width(20) 25 | //! .align_center(); 26 | //! 27 | //! siv.add_fullscreen_layer(panel); 28 | //! // siv.run() 29 | //! } 30 | //! ``` 31 | //! 32 | //! This is the preferred way as it is *chainable* and consistent with cursive's 33 | //! `Resizable` and `Identifiable` traits. 34 | //! 35 | //! As an alternative you can use the `AlignedView` constructors directly: 36 | //! 37 | //! ```rust 38 | //! use cursive::{Cursive, CursiveExt}; 39 | //! use cursive::view::Resizable; 40 | //! use cursive::views::{Panel, DummyView}; 41 | //! use cursive_aligned_view::AlignedView; 42 | //! 43 | //! fn main() { 44 | //! let mut siv = Cursive::default(); 45 | //! 46 | //! let panel = Panel::new(DummyView) 47 | //! .title("Hello, world!") 48 | //! .fixed_width(20); 49 | //! let aligned = AlignedView::with_center(panel); 50 | //! 51 | //! siv.add_fullscreen_layer(aligned); 52 | //! // siv.run() 53 | //! } 54 | //! ``` 55 | //! 56 | //! ## Supported Alignments 57 | //! 58 | //! | Alignment | Construction method | 59 | //! |---------------|-----------------------| 60 | //! | top left | `align_top_left` | 61 | //! | top center | `align_top_center` | 62 | //! | top right | `align_top_right` | 63 | //! | center left | `align_center_left` | 64 | //! | center | `align_center` | 65 | //! | center right | `align_center_right` | 66 | //! | bottom left | `align_bottom_left` | 67 | //! | bottom center | `align_bottom_center` | 68 | //! | bottom right | `align_bottom_right` | 69 | 70 | use cursive_core::align::{Align, HAlign, VAlign}; 71 | use cursive_core::event::{Event, EventResult}; 72 | use cursive_core::view::{View, ViewWrapper}; 73 | use cursive_core::{Printer, Rect, Vec2}; 74 | 75 | /// Use this trait to extend all `cursive::view::View` instances to support 76 | /// the `align_...` methods. 77 | /// 78 | /// This trait provides *chainable* constructors for the `AlignedView`. 79 | /// 80 | /// # Usage Example 81 | /// 82 | /// ```rust 83 | /// use cursive::{Cursive, CursiveExt}; 84 | /// use cursive::view::Resizable; 85 | /// use cursive::views::{Panel, DummyView}; 86 | /// use cursive_aligned_view::Alignable; 87 | /// 88 | /// fn main() { 89 | /// let mut siv = Cursive::default(); 90 | /// 91 | /// let panel = Panel::new(DummyView) 92 | /// .title("Hello, world!") 93 | /// .fixed_width(20) 94 | /// .align_top_center(); // constructing `AlignedView` 95 | /// 96 | /// siv.add_fullscreen_layer(panel); 97 | /// // siv.run() 98 | /// } 99 | /// ``` 100 | pub trait Alignable: View + Sized { 101 | /// Align a child view at the top-left of the parent. 102 | fn align_top_left(self) -> AlignedView { 103 | AlignedView::with_top_left(self) 104 | } 105 | 106 | /// Align a child view at the top-center of the parent. 107 | fn align_top_center(self) -> AlignedView { 108 | AlignedView::with_top_center(self) 109 | } 110 | 111 | /// Align a child view at the top-right of the parent. 112 | fn align_top_right(self) -> AlignedView { 113 | AlignedView::with_top_right(self) 114 | } 115 | 116 | /// Align a child view at the center-left of the parent. 117 | fn align_center_left(self) -> AlignedView { 118 | AlignedView::with_center_left(self) 119 | } 120 | 121 | /// Align a child view at the center of the parent. 122 | fn align_center(self) -> AlignedView { 123 | AlignedView::with_center(self) 124 | } 125 | 126 | /// Align a child view at the center-right of the parent. 127 | fn align_center_right(self) -> AlignedView { 128 | AlignedView::with_center_right(self) 129 | } 130 | 131 | /// Align a child view at the bottom-left of the parent. 132 | fn align_bottom_left(self) -> AlignedView { 133 | AlignedView::with_bottom_left(self) 134 | } 135 | 136 | /// Align a child view at the bottom-center of the parent. 137 | fn align_bottom_center(self) -> AlignedView { 138 | AlignedView::with_bottom_center(self) 139 | } 140 | 141 | /// Align a child view at the bottom-right of the parent. 142 | fn align_bottom_right(self) -> AlignedView { 143 | AlignedView::with_bottom_right(self) 144 | } 145 | } 146 | 147 | impl Alignable for T {} 148 | 149 | /// This struct aligns a child view with a given alignment. 150 | /// 151 | /// The child view will have its minimum allowed size. Additionally, the child 152 | /// may get cropped if it is larger than the available size. 153 | /// 154 | /// The padded space around the child view is filled with the `View` color from 155 | /// cursive's color palette. 156 | /// 157 | /// # Usage 158 | /// 159 | /// The `AlignedView` may be used in 2 different ways: 160 | /// 161 | /// 1. Via the `Alignable` composition trait 162 | /// 2. Via normal constructors 163 | /// 164 | /// ## Using Alignable 165 | /// 166 | /// ```rust 167 | /// use cursive::{Cursive, CursiveExt}; 168 | /// use cursive::view::Resizable; 169 | /// use cursive::views::{Panel, DummyView}; 170 | /// use cursive_aligned_view::Alignable; 171 | /// 172 | /// fn main() { 173 | /// let mut siv = Cursive::default(); 174 | /// 175 | /// let panel = Panel::new(DummyView) 176 | /// .title("Hello, world!") 177 | /// .fixed_width(20) 178 | /// .align_top_center(); // `align_...` methods from `Alignable` 179 | /// 180 | /// siv.add_fullscreen_layer(panel); 181 | /// // siv.run() 182 | /// } 183 | /// ``` 184 | /// 185 | /// ## Constructors 186 | /// 187 | /// ```rust 188 | /// use cursive::{Cursive, CursiveExt}; 189 | /// use cursive::view::Resizable; 190 | /// use cursive::views::{Panel, DummyView}; 191 | /// use cursive_aligned_view::AlignedView; 192 | /// 193 | /// fn main() { 194 | /// let mut siv = Cursive::default(); 195 | /// 196 | /// let panel = Panel::new(DummyView) 197 | /// .title("Hello, world!") 198 | /// .fixed_width(20); 199 | /// let aligned = AlignedView::with_bottom_center(panel); // constructor 200 | /// 201 | /// siv.add_fullscreen_layer(aligned); 202 | /// // siv.run() 203 | /// } 204 | /// ``` 205 | pub struct AlignedView { 206 | view: T, 207 | alignment: Align, 208 | last_size: Vec2, 209 | offset: Vec2, 210 | needs_relayout: bool, 211 | } 212 | 213 | impl AlignedView { 214 | pub fn new(view: T, alignment: Align) -> Self { 215 | Self { 216 | view, 217 | alignment, 218 | last_size: Vec2::new(0, 0), 219 | offset: Vec2::new(0, 0), 220 | needs_relayout: false, 221 | } 222 | } 223 | 224 | /// Wrap a child view and align it at the top-left of the parent. 225 | pub fn with_top_left(view: T) -> Self { 226 | Self::new(view, Align::new(HAlign::Left, VAlign::Top)) 227 | } 228 | 229 | /// Wrap a child view and align it at the top-center of the parent. 230 | pub fn with_top_center(view: T) -> Self { 231 | Self::new(view, Align::new(HAlign::Center, VAlign::Top)) 232 | } 233 | 234 | /// Wrap a child view and align it at the top-right of the parent. 235 | pub fn with_top_right(view: T) -> Self { 236 | Self::new(view, Align::new(HAlign::Right, VAlign::Top)) 237 | } 238 | 239 | /// Wrap a child view and align it at the center-left of the parent. 240 | pub fn with_center_left(view: T) -> Self { 241 | Self::new(view, Align::new(HAlign::Left, VAlign::Center)) 242 | } 243 | 244 | /// Wrap a child view and align it at the center of the parent. 245 | pub fn with_center(view: T) -> Self { 246 | Self::new(view, Align::new(HAlign::Center, VAlign::Center)) 247 | } 248 | 249 | /// Wrap a child view and align it at the center-right of the parent. 250 | pub fn with_center_right(view: T) -> Self { 251 | Self::new(view, Align::new(HAlign::Right, VAlign::Center)) 252 | } 253 | 254 | /// Wrap a child view and align it at the bottom-left of the parent. 255 | pub fn with_bottom_left(view: T) -> Self { 256 | Self::new(view, Align::new(HAlign::Left, VAlign::Bottom)) 257 | } 258 | 259 | /// Wrap a child view and align it at the bottom-center of the parent. 260 | pub fn with_bottom_center(view: T) -> Self { 261 | Self::new(view, Align::new(HAlign::Center, VAlign::Bottom)) 262 | } 263 | 264 | /// Wrap a child view and align it at the bottom-right of the parent. 265 | pub fn with_bottom_right(view: T) -> Self { 266 | Self::new(view, Align::new(HAlign::Right, VAlign::Bottom)) 267 | } 268 | 269 | /// Set the alignment of this view to top-left. 270 | pub fn set_top_left(&mut self) { 271 | self.alignment = Align::new(HAlign::Left, VAlign::Top); 272 | self.needs_relayout = true; 273 | } 274 | 275 | /// Set the alignment of this view to top-center. 276 | pub fn set_top_center(&mut self) { 277 | self.alignment = Align::new(HAlign::Center, VAlign::Top); 278 | self.needs_relayout = true; 279 | } 280 | 281 | /// Set the alignment of this view to top-right. 282 | pub fn set_top_right(&mut self) { 283 | self.alignment = Align::new(HAlign::Right, VAlign::Top); 284 | self.needs_relayout = true; 285 | } 286 | 287 | /// Set the alignment of this view to center-left. 288 | pub fn set_center_left(&mut self) { 289 | self.alignment = Align::new(HAlign::Left, VAlign::Center); 290 | self.needs_relayout = true; 291 | } 292 | 293 | /// Set the alignment of this view to center. 294 | pub fn set_center(&mut self) { 295 | self.alignment = Align::new(HAlign::Center, VAlign::Center); 296 | self.needs_relayout = true; 297 | } 298 | 299 | /// Set the alignment of this view to center-right. 300 | pub fn set_center_right(&mut self) { 301 | self.alignment = Align::new(HAlign::Right, VAlign::Center); 302 | self.needs_relayout = true; 303 | } 304 | 305 | /// Set the alignment of this view to bottom-left. 306 | pub fn set_bottom_left(&mut self) { 307 | self.alignment = Align::new(HAlign::Left, VAlign::Bottom); 308 | self.needs_relayout = true; 309 | } 310 | 311 | /// Set the alignment of this view to bottom-center. 312 | pub fn set_bottom_center(&mut self) { 313 | self.alignment = Align::new(HAlign::Center, VAlign::Bottom); 314 | self.needs_relayout = true; 315 | } 316 | 317 | /// Set the alignment of this view to bottom-right. 318 | pub fn set_bottom_right(&mut self) { 319 | self.alignment = Align::new(HAlign::Right, VAlign::Bottom); 320 | self.needs_relayout = true; 321 | } 322 | 323 | /// Get the current alignment of this view. 324 | pub fn alignment(&self) -> &Align { 325 | &self.alignment 326 | } 327 | } 328 | 329 | impl ViewWrapper for AlignedView { 330 | cursive_core::wrap_impl!(self.view: T); 331 | 332 | fn wrap_draw(&self, printer: &Printer) { 333 | let offset_printer = printer.offset(self.offset).cropped(self.last_size); 334 | self.view.draw(&offset_printer); 335 | } 336 | 337 | fn wrap_layout(&mut self, size: Vec2) { 338 | self.offset = Vec2::new( 339 | self.alignment.h.get_offset(self.last_size.x, size.x), 340 | self.alignment.v.get_offset(self.last_size.y, size.y), 341 | ); 342 | 343 | let x = std::cmp::min(size.x, self.last_size.x); 344 | let y = std::cmp::min(size.y, self.last_size.y); 345 | 346 | self.view.layout(Vec2::new(x, y)); 347 | 348 | self.needs_relayout = false; 349 | } 350 | 351 | fn wrap_needs_relayout(&self) -> bool { 352 | self.needs_relayout || self.view.needs_relayout() 353 | } 354 | 355 | fn wrap_required_size(&mut self, constraint: Vec2) -> Vec2 { 356 | self.last_size = self.view.required_size(constraint); 357 | 358 | self.last_size 359 | } 360 | 361 | fn wrap_on_event(&mut self, ev: Event) -> EventResult { 362 | self.view.on_event(ev.relativized(self.offset)) 363 | } 364 | 365 | fn wrap_important_area(&self, _: Vec2) -> Rect { 366 | self.view.important_area(self.last_size) + self.offset 367 | } 368 | } 369 | 370 | #[cursive_core::blueprint(AlignedView::new(view, alignment))] 371 | struct Blueprint { 372 | view: cursive_core::views::BoxedView, 373 | alignment: Align, 374 | } 375 | 376 | cursive_core::manual_blueprint!(with align, |config, context| { 377 | let alignment = context.resolve(config)?; 378 | Ok(move |view| AlignedView::new(view, alignment)) 379 | }); 380 | 381 | cursive_core::manual_blueprint!(with align_top_left, |_config, _context| { 382 | Ok(|view| AlignedView::with_top_left(view)) 383 | }); 384 | 385 | cursive_core::manual_blueprint!(with align_top_center, |_config, _context| { 386 | Ok(|view| AlignedView::with_top_center(view)) 387 | }); 388 | 389 | cursive_core::manual_blueprint!(with align_top_right, |_config, _context| { 390 | Ok(|view| AlignedView::with_top_right(view)) 391 | }); 392 | 393 | cursive_core::manual_blueprint!(with align_center_left, |_config, _context| { 394 | Ok(|view| AlignedView::with_center_left(view)) 395 | }); 396 | 397 | cursive_core::manual_blueprint!(with align_center, |_config, _context| { 398 | Ok(|view| AlignedView::with_center(view)) 399 | }); 400 | 401 | cursive_core::manual_blueprint!(with align_center_right, |_config, _context| { 402 | Ok(|view| AlignedView::with_center_right(view)) 403 | }); 404 | 405 | cursive_core::manual_blueprint!(with align_bottom_left, |_config, _context| { 406 | Ok(|view| AlignedView::with_bottom_left(view)) 407 | }); 408 | 409 | cursive_core::manual_blueprint!(with align_bottom_center, |_config, _context| { 410 | Ok(|view| AlignedView::with_bottom_center(view)) 411 | }); 412 | 413 | cursive_core::manual_blueprint!(with align_bottom_right, |_config, _context| { 414 | Ok(|view| AlignedView::with_bottom_right(view)) 415 | }); 416 | --------------------------------------------------------------------------------