├── .github └── workflows │ ├── check.yaml │ ├── pr-review.yaml │ └── publish.yaml ├── .gitignore ├── .vscode └── launch.json ├── Cargo.toml ├── LICENSE.txt ├── README.md ├── check ├── examples ├── demo.gif └── demo.rs ├── src ├── lib.rs ├── symbols.rs └── widgets │ ├── mod.rs │ └── throbber.rs └── tools ├── check_readme.sh ├── publish_crate.sh └── readme_lib.diff /.github/workflows/check.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | branches: [main] 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | 11 | permissions: 12 | checks: write 13 | contents: read 14 | pull-requests: write 15 | 16 | jobs: 17 | lint: 18 | timeout-minutes: 10 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: actions/cache@v4 23 | with: 24 | path: ~/.cargo/ 25 | key: ${{ runner.os }}-cargo-lint 26 | - name: Install cargo-audit 27 | run: cargo install cargo-audit 28 | - name: Audit 29 | run: cargo audit 30 | - name: Format 31 | run: cargo fmt --check --verbose 32 | - name: Lint 33 | run: cargo clippy 34 | - name: Documentation 35 | run: cargo doc 36 | - name: Install cargo-license 37 | run: cargo install cargo-license 38 | - name: Check README 39 | run: tools/check_readme.sh 40 | 41 | build: 42 | needs: [lint] 43 | timeout-minutes: 10 44 | runs-on: ${{ matrix.os }} 45 | strategy: 46 | matrix: 47 | rust: 48 | - 1.74.0 # MSRV 49 | - beta 50 | - nightly 51 | os: 52 | - macos-latest 53 | - ubuntu-latest 54 | - windows-latest 55 | steps: 56 | - uses: actions/checkout@v4 57 | - uses: actions/cache@v4 58 | with: 59 | path: | 60 | ~/.cargo/ 61 | target 62 | key: ${{ runner.os}}-cargo-build-${{ matrix.rust }} 63 | - name: Setup Rust 64 | run: | 65 | rustup toolchain install --profile minimal ${{ matrix.rust }} 66 | rustup override set ${{ matrix.rust }} 67 | - name: Build 68 | run: cargo check --verbose --release 69 | - name: Test 70 | run: cargo test --verbose 71 | continue-on-error: ${{ matrix.rust == 'nightly' }} 72 | -------------------------------------------------------------------------------- /.github/workflows/pr-review.yaml: -------------------------------------------------------------------------------- 1 | name: PR Review 2 | on: 3 | pull_request: 4 | types: 5 | - opened 6 | - reopened 7 | - synchronize 8 | - ready_for_review # Draft Pull RequestからDraftが外れたら起動 9 | jobs: 10 | approve: 11 | if: | 12 | github.event.pull_request.user.login == github.repository_owner 13 | && ! github.event.pull_request.draft 14 | runs-on: ubuntu-latest 15 | permissions: 16 | pull-requests: write 17 | steps: 18 | # Pull RequestをApproveする 19 | - uses: hmarr/auto-approve-action@v4 20 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | workflow_run: 4 | workflows: 5 | - .github/workflows/check.yaml 6 | types: [completed] 7 | branches: [main] 8 | paths: [Cargo.toml] # No effect so far. 9 | 10 | env: 11 | CARGO_TERM_COLOR: always 12 | 13 | permissions: 14 | contents: write 15 | 16 | jobs: 17 | publish: 18 | timeout-minutes: 10 19 | runs-on: ubuntu-latest 20 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 21 | steps: 22 | - uses: actions/checkout@v4 23 | with: 24 | fetch-depth: 2 25 | - run: ./tools/publish_crate.sh 26 | env: 27 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | 4 | # IDEs 5 | 6 | /.idea -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "launch", 10 | "name": "Debug unit tests in library 'throbber-widgets-tui'", 11 | "cargo": { 12 | "args": [ 13 | "test", 14 | "--no-run", 15 | "--lib", 16 | "--package=throbber-widgets-tui" 17 | ], 18 | "filter": { 19 | "name": "throbber-widgets-tui", 20 | "kind": "lib" 21 | } 22 | }, 23 | "args": [], 24 | "cwd": "${workspaceFolder}" 25 | }, 26 | { 27 | "type": "lldb", 28 | "request": "launch", 29 | "name": "Debug example 'demo'", 30 | "cargo": { 31 | "args": [ 32 | "build", 33 | "--example=demo", 34 | "--package=throbber-widgets-tui" 35 | ], 36 | "filter": { 37 | "name": "demo", 38 | "kind": "example" 39 | } 40 | }, 41 | "args": [], 42 | "cwd": "${workspaceFolder}" 43 | }, 44 | { 45 | "type": "lldb", 46 | "request": "launch", 47 | "name": "Debug unit tests in example 'demo'", 48 | "cargo": { 49 | "args": [ 50 | "test", 51 | "--no-run", 52 | "--example=demo", 53 | "--package=throbber-widgets-tui" 54 | ], 55 | "filter": { 56 | "name": "demo", 57 | "kind": "example" 58 | } 59 | }, 60 | "args": [], 61 | "cwd": "${workspaceFolder}" 62 | } 63 | ] 64 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "throbber-widgets-tui" 3 | #note: After updating the dependent crate, do +0.1.0 4 | version = "0.8.0" 5 | edition = "2021" 6 | rust-version = "1.74.0" 7 | authors = ["arkbig"] 8 | description = "This is a ratatui widget that displays throbber." 9 | readme = "README.md" 10 | documentation = "https://docs.rs/throbber-widgets-tui/latest/throbber_widgets_tui/" 11 | repository = "https://github.com/arkbig/throbber-widgets-tui" 12 | license = "Zlib" 13 | keywords = ["tui", "widgets"] 14 | categories = ["command-line-utilities"] 15 | 16 | [dependencies] 17 | rand = "0.8.5" 18 | ratatui = { version = "0.29.0", default-features = false } 19 | 20 | [dev-dependencies] 21 | ratatui = "0.29.0" 22 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | zlib License 2 | 3 | Copyright (c) 2022 arkbig 4 | 5 | This software is provided 'as-is', without any express or implied 6 | warranty. In no event will the authors be held liable for any damages 7 | arising from the use of this software. 8 | 9 | Permission is granted to anyone to use this software for any purpose, 10 | including commercial applications, and to alter it and redistribute it 11 | freely, subject to the following restrictions: 12 | 13 | 1. The origin of this software must not be misrepresented; you must not 14 | claim that you wrote the original software. If you use this software 15 | in a product, an acknowledgment in the product documentation would be 16 | appreciated but is not required. 17 | 2. Altered source versions must be plainly marked as such, and must not be 18 | misrepresented as being the original software. 19 | 3. This notice may not be removed or altered from any source distribution. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Throbber widget of [ratatui] 2 | 3 | [ratatui]: https://github.com/ratatui-org/ratatui 4 | [tui-rs]: https://github.com/fdehau/tui-rs 5 | 6 | > **_NOTE:_** If you want to use [tui-rs] instead of [ratatui], please use 0.4.1 or older version. 7 | 8 | `throbber-widgets-tui` is a [ratatui] widget that displays throbber. 9 | 10 | A throbber may also be called: 11 | 12 | - activity indicator 13 | - indeterminate progress bar 14 | - loading icon 15 | - spinner 16 | - ぐるぐる(guru guru) 17 | 18 | ## Demo 19 | 20 | ![Demo Animation](./examples/demo.gif) 21 | 22 | The demo shown in the gif can be run with all available symbols. 23 | 24 | ```sh 25 | cargo run --example demo --release 26 | ``` 27 | 28 | ## Features 29 | 30 | - Render throbber 31 | - With label 32 | - Random or specified step, also negative is possible. 33 | 34 | ## Getting Started 35 | 36 | MSRV: `throbber-widgets-tui` requires rustc 1.74.0 or newer. 37 | 38 | ```sh 39 | cargo add throbber-widgets-tui 40 | ``` 41 | 42 | Example code: 43 | 44 | ```rust 45 | // : 46 | // : 47 | struct App { 48 | throbber_state: throbber_widgets_tui::ThrobberState, 49 | } 50 | impl App { 51 | fn on_tick(&mut self) { 52 | self.throbber_state.calc_next(); 53 | } 54 | } 55 | // : 56 | // : 57 | fn ui(f: &mut ratatui::Frame, app: &mut App) { 58 | let chunks = ratatui::layout::Layout::default() 59 | .direction(ratatui::layout::Direction::Horizontal) 60 | .margin(1) 61 | .constraints( 62 | [ 63 | ratatui::layout::Constraint::Percentage(50), 64 | ratatui::layout::Constraint::Percentage(50), 65 | ] 66 | .as_ref(), 67 | ) 68 | .split(f.area()); 69 | 70 | // Simple random step 71 | let simple = throbber_widgets_tui::Throbber::default(); 72 | f.render_widget(simple, chunks[0]); 73 | 74 | // Set full with state 75 | let full = throbber_widgets_tui::Throbber::default() 76 | .label("Running...") 77 | .style(ratatui::style::Style::default().fg(ratatui::style::Color::Cyan)) 78 | .throbber_style(ratatui::style::Style::default().fg(ratatui::style::Color::Red).add_modifier(ratatui::style::Modifier::BOLD)) 79 | .throbber_set(throbber_widgets_tui::CLOCK) 80 | .use_type(throbber_widgets_tui::WhichUse::Spin); 81 | f.render_stateful_widget(full, chunks[1], &mut app.throbber_state); 82 | } 83 | ``` 84 | 85 | ## Apps using throbber-widgets-tui 86 | 87 | - [mntime](https://github.com/arkbig/mntime): Execute "m" commands "n" times to calculate mean of usage time and memory. As an alternative to "time", "gnu-time" is used internally. 88 | 89 | ## Dependencies (By default) 90 | 91 | Direct dependencies crates: 92 | 93 | ```sh 94 | cargo license --direct-deps-only --avoid-build-deps --avoid-dev-deps | awk -F ":" 'BEGIN {printf "|License|crate|\n|-|-|\n"} {printf "|%s|%s|\n", $1, $2}' 95 | ``` 96 | 97 | |License|crate| 98 | |-|-| 99 | |Apache-2.0 OR MIT (1)| rand| 100 | |MIT (1)| ratatui| 101 | |Zlib (1)| throbber-widgets-tui| 102 | 103 | Chain dependencies crates: 104 | 105 | ```sh 106 | cargo license --avoid-build-deps --avoid-dev-deps | awk -F ":" 'BEGIN {printf "|License|crate|\n|-|-|\n"} {printf "|%s|%s|\n", $1, $2}' 107 | ``` 108 | 109 | |License|crate| 110 | |-|-| 111 | |(MIT OR Apache-2.0) AND Unicode-DFS-2016 (1)| unicode-ident| 112 | |Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT (3)| linux-raw-sys, rustix, wasi| 113 | |Apache-2.0 OR BSD-2-Clause OR MIT (2)| zerocopy, zerocopy-derive| 114 | |Apache-2.0 OR BSL-1.0 (1)| ryu| 115 | |Apache-2.0 OR MIT (51)| allocator-api2, bitflags, cassowary, cfg-if, either, equivalent, errno, getrandom, hashbrown, heck, hermit-abi, indoc, itertools, itoa, libc, lock_api, log, parking_lot, parking_lot_core, paste, ppv-lite86, proc-macro2, quote, rand, rand_chacha, rand_core, rustversion, scopeguard, signal-hook, signal-hook-mio, signal-hook-registry, smallvec, static_assertions, syn, unicode-segmentation, unicode-truncate, unicode-width, unicode-width, winapi, winapi-i686-pc-windows-gnu, winapi-x86_64-pc-windows-gnu, windows-sys, windows-targets, windows_aarch64_gnullvm, windows_aarch64_msvc, windows_i686_gnu, windows_i686_gnullvm, windows_i686_msvc, windows_x86_64_gnu, windows_x86_64_gnullvm, windows_x86_64_msvc| 116 | |MIT (11)| castaway, compact_str, crossterm, crossterm_winapi, instability, lru, mio, ratatui, redox_syscall, strum, strum_macros| 117 | |MIT OR Unlicense (1)| byteorder| 118 | |Zlib (2)| foldhash, throbber-widgets-tui| 119 | 120 | ## License 121 | 122 | This repository's license is zlib. Please feel free to use this, but no warranty. 123 | -------------------------------------------------------------------------------- /check: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -eux 3 | 4 | ROOTDIR=$(cd "$(dirname "$0")" && pwd) 5 | 6 | "$ROOTDIR/tools/check_readme.sh" 7 | cargo audit 8 | cargo fmt 9 | cargo clippy 10 | cargo test 11 | cargo doc 12 | cargo run --example demo 13 | -------------------------------------------------------------------------------- /examples/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arkbig/throbber-widgets-tui/d125ce0e8f650b0dd1bfddb06d252f62a4dc420c/examples/demo.gif -------------------------------------------------------------------------------- /examples/demo.rs: -------------------------------------------------------------------------------- 1 | #[derive(Default)] 2 | struct App { 3 | states: Vec, 4 | } 5 | 6 | impl App { 7 | fn on_tick(&mut self) { 8 | self.states.iter_mut().for_each(|state| state.calc_next()); 9 | } 10 | } 11 | 12 | fn main() -> std::io::Result<()> { 13 | // setup terminal 14 | let mut terminal = ratatui::init(); 15 | 16 | // create app and run it 17 | let tick_rate = std::time::Duration::from_millis(250); 18 | let app = App::default(); 19 | let res = run_app(&mut terminal, app, tick_rate); 20 | 21 | // restore terminal 22 | ratatui::restore(); 23 | 24 | res 25 | } 26 | 27 | fn run_app( 28 | terminal: &mut ratatui::Terminal, 29 | mut app: App, 30 | tick_rate: std::time::Duration, 31 | ) -> std::io::Result<()> { 32 | let mut last_tick = std::time::Instant::now(); 33 | loop { 34 | terminal.draw(|f| ui(f, &mut app))?; 35 | 36 | let timeout = tick_rate 37 | .checked_sub(last_tick.elapsed()) 38 | .unwrap_or_else(|| std::time::Duration::from_secs(0)); 39 | if ratatui::crossterm::event::poll(timeout)? { 40 | if let ratatui::crossterm::event::Event::Key(key) = ratatui::crossterm::event::read()? { 41 | if let ratatui::crossterm::event::KeyCode::Char('q') = key.code { 42 | return Ok(()); 43 | } 44 | } 45 | } 46 | if last_tick.elapsed() >= tick_rate { 47 | app.on_tick(); 48 | last_tick = std::time::Instant::now(); 49 | } 50 | } 51 | } 52 | fn ui(f: &mut ratatui::Frame, app: &mut App) { 53 | let all_sets = [ 54 | ("ASCII", throbber_widgets_tui::ASCII), 55 | ("BOX_DRAWING", throbber_widgets_tui::BOX_DRAWING), 56 | ("ARROW", throbber_widgets_tui::ARROW), 57 | ("DOUBLE_ARROW", throbber_widgets_tui::DOUBLE_ARROW), 58 | ("VERTICAL_BLOCK", throbber_widgets_tui::VERTICAL_BLOCK), 59 | ("HORIZONTAL_BLOCK", throbber_widgets_tui::HORIZONTAL_BLOCK), 60 | ("QUADRANT_BLOCK", throbber_widgets_tui::QUADRANT_BLOCK), 61 | ( 62 | "QUADRANT_BLOCK_CRACK", 63 | throbber_widgets_tui::QUADRANT_BLOCK_CRACK, 64 | ), 65 | ("WHITE_SQUARE", throbber_widgets_tui::WHITE_SQUARE), 66 | ("WHITE_CIRCLE", throbber_widgets_tui::WHITE_CIRCLE), 67 | ("BLACK_CIRCLE", throbber_widgets_tui::BLACK_CIRCLE), 68 | ("CLOCK", throbber_widgets_tui::CLOCK), 69 | ("BRAILLE_ONE", throbber_widgets_tui::BRAILLE_ONE), 70 | ("BRAILLE_SIX", throbber_widgets_tui::BRAILLE_SIX), 71 | ("BRAILLE_EIGHT", throbber_widgets_tui::BRAILLE_EIGHT), 72 | ("BRAILLE_DOUBLE", throbber_widgets_tui::BRAILLE_DOUBLE), 73 | ( 74 | "BRAILLE_SIX_DOUBLE", 75 | throbber_widgets_tui::BRAILLE_SIX_DOUBLE, 76 | ), 77 | ( 78 | "BRAILLE_EIGHT_DOUBLE", 79 | throbber_widgets_tui::BRAILLE_EIGHT_DOUBLE, 80 | ), 81 | ("OGHAM_A", throbber_widgets_tui::OGHAM_A), 82 | ("OGHAM_B", throbber_widgets_tui::OGHAM_B), 83 | ("OGHAM_C", throbber_widgets_tui::OGHAM_C), 84 | ("PARENTHESIS", throbber_widgets_tui::PARENTHESIS), 85 | ("CANADIAN", throbber_widgets_tui::CANADIAN), 86 | ]; 87 | let horizontal_num = 4; 88 | // why +1? because the first line is for title default throbber. 89 | let vertical_num = 1 + (all_sets.len() + horizontal_num - 1) / horizontal_num; 90 | let verticals = ratatui::layout::Layout::default() 91 | .direction(ratatui::layout::Direction::Vertical) 92 | .constraints(&vec![ratatui::layout::Constraint::Length(1); vertical_num]) 93 | .split(f.area()); 94 | let default_throbber = throbber_widgets_tui::Throbber::default() 95 | .label("Press q to exit. This line is a default throbber (random step). The followings are incremental step.") 96 | .style(ratatui::style::Style::default().fg(ratatui::style::Color::Yellow)); 97 | f.render_widget(default_throbber, verticals[0]); 98 | 99 | let mut chunks: Option<_> = None; 100 | for (i, kvp) in all_sets.iter().enumerate() { 101 | let (name, set) = kvp; 102 | let row = i / horizontal_num; 103 | let col = i % horizontal_num; 104 | if col == 0 { 105 | chunks = Some( 106 | ratatui::layout::Layout::default() 107 | .direction(ratatui::layout::Direction::Horizontal) 108 | .constraints(&vec![ 109 | ratatui::layout::Constraint::Percentage( 110 | 100 / (horizontal_num as u16) 111 | ); 112 | horizontal_num 113 | ]) 114 | .split(verticals[row + 1]), 115 | ); 116 | } 117 | if app.states.len() <= i { 118 | app.states 119 | .push(throbber_widgets_tui::ThrobberState::default()); 120 | } 121 | let throbber = throbber_widgets_tui::Throbber::default() 122 | .label(name.to_string()) 123 | .throbber_set(set.clone()); 124 | f.render_stateful_widget(throbber, chunks.clone().unwrap()[col], &mut app.states[i]); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | # Throbber widget of [ratatui] 3 | 4 | [ratatui]: https://github.com/ratatui-org/ratatui 5 | [tui-rs]: https://github.com/fdehau/tui-rs 6 | 7 | > **_NOTE:_** If you want to use [tui-rs] instead of [ratatui], please use 0.4.1 or older version. 8 | 9 | `throbber-widgets-tui` is a [ratatui] widget that displays throbber. 10 | 11 | A throbber may also be called: 12 | 13 | - activity indicator 14 | - indeterminate progress bar 15 | - loading icon 16 | - spinner 17 | - guru guru 18 | 19 | ## Demo 20 | 21 | ![Demo Animation](https://raw.githubusercontent.com/arkbig/throbber-widgets-tui/main/examples/demo.gif) 22 | 23 | The demo shown in the gif can be run with all available symbols. 24 | 25 | ```sh 26 | cargo run --example demo --release 27 | ``` 28 | 29 | ## Features 30 | 31 | - Render throbber 32 | - With label 33 | - Random or specified step, also negative is possible. 34 | 35 | ## Getting Started 36 | 37 | MSRV: `throbber-widgets-tui` requires rustc 1.74.0 or newer. 38 | 39 | ```sh 40 | cargo add throbber-widgets-tui 41 | ``` 42 | 43 | Example code: 44 | 45 | ```rust 46 | // : 47 | // : 48 | struct App { 49 | throbber_state: throbber_widgets_tui::ThrobberState, 50 | } 51 | impl App { 52 | fn on_tick(&mut self) { 53 | self.throbber_state.calc_next(); 54 | } 55 | } 56 | // : 57 | // : 58 | fn ui(f: &mut ratatui::Frame, app: &mut App) { 59 | let chunks = ratatui::layout::Layout::default() 60 | .direction(ratatui::layout::Direction::Horizontal) 61 | .margin(1) 62 | .constraints( 63 | [ 64 | ratatui::layout::Constraint::Percentage(50), 65 | ratatui::layout::Constraint::Percentage(50), 66 | ] 67 | .as_ref(), 68 | ) 69 | .split(f.area()); 70 | 71 | // Simple random step 72 | let simple = throbber_widgets_tui::Throbber::default(); 73 | f.render_widget(simple, chunks[0]); 74 | 75 | // Set full with state 76 | let full = throbber_widgets_tui::Throbber::default() 77 | .label("Running...") 78 | .style(ratatui::style::Style::default().fg(ratatui::style::Color::Cyan)) 79 | .throbber_style(ratatui::style::Style::default().fg(ratatui::style::Color::Red).add_modifier(ratatui::style::Modifier::BOLD)) 80 | .throbber_set(throbber_widgets_tui::CLOCK) 81 | .use_type(throbber_widgets_tui::WhichUse::Spin); 82 | f.render_stateful_widget(full, chunks[1], &mut app.throbber_state); 83 | } 84 | ``` 85 | 86 | ## Apps using throbber-widgets-tui 87 | 88 | - [mntime](https://github.com/arkbig/mntime): Execute "m" commands "n" times to calculate mean of usage time and memory. As an alternative to "time", "gnu-time" is used internally. 89 | 90 | ## Dependencies (By default) 91 | 92 | Direct dependencies crates: 93 | 94 | |License|crate| 95 | |-|-| 96 | |Apache-2.0 OR MIT (1)| rand| 97 | |MIT (1)| ratatui| 98 | |Zlib (1)| throbber-widgets-tui| 99 | 100 | Chain dependencies crates: 101 | 102 | |License|crate| 103 | |-|-| 104 | |(MIT OR Apache-2.0) AND Unicode-DFS-2016 (1)| unicode-ident| 105 | |Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT (3)| linux-raw-sys, rustix, wasi| 106 | |Apache-2.0 OR BSD-2-Clause OR MIT (2)| zerocopy, zerocopy-derive| 107 | |Apache-2.0 OR BSL-1.0 (1)| ryu| 108 | |Apache-2.0 OR MIT (51)| allocator-api2, bitflags, cassowary, cfg-if, either, equivalent, errno, getrandom, hashbrown, heck, hermit-abi, indoc, itertools, itoa, libc, lock_api, log, parking_lot, parking_lot_core, paste, ppv-lite86, proc-macro2, quote, rand, rand_chacha, rand_core, rustversion, scopeguard, signal-hook, signal-hook-mio, signal-hook-registry, smallvec, static_assertions, syn, unicode-segmentation, unicode-truncate, unicode-width, unicode-width, winapi, winapi-i686-pc-windows-gnu, winapi-x86_64-pc-windows-gnu, windows-sys, windows-targets, windows_aarch64_gnullvm, windows_aarch64_msvc, windows_i686_gnu, windows_i686_gnullvm, windows_i686_msvc, windows_x86_64_gnu, windows_x86_64_gnullvm, windows_x86_64_msvc| 109 | |MIT (11)| castaway, compact_str, crossterm, crossterm_winapi, instability, lru, mio, ratatui, redox_syscall, strum, strum_macros| 110 | |MIT OR Unlicense (1)| byteorder| 111 | |Zlib (2)| foldhash, throbber-widgets-tui| 112 | 113 | ## License 114 | 115 | This repository's license is zlib. Please feel free to use this, but no warranty. 116 | */ 117 | 118 | pub mod symbols; 119 | pub mod widgets; 120 | 121 | pub use self::symbols::throbber::*; 122 | pub use self::widgets::*; 123 | -------------------------------------------------------------------------------- /src/symbols.rs: -------------------------------------------------------------------------------- 1 | pub mod throbber { 2 | /// A set of symbols to be rendered by throbber. 3 | #[derive(Debug, Clone)] 4 | pub struct Set { 5 | pub full: &'static str, 6 | pub empty: &'static str, 7 | pub symbols: &'static [&'static str], 8 | } 9 | 10 | /// Rendering object. 11 | /// 12 | /// If Spin is specified, ThrobberState.index is used. 13 | #[derive(Debug, Clone)] 14 | pub enum WhichUse { 15 | Full, 16 | Empty, 17 | Spin, 18 | } 19 | 20 | /// ["|", "/", "-", "\\"] 21 | pub const ASCII: Set = Set { 22 | full: "*", 23 | empty: " ", 24 | symbols: &["|", "/", "-", "\\"], 25 | }; 26 | 27 | /// ["│", "╱", "─", "╲"] 28 | pub const BOX_DRAWING: Set = Set { 29 | full: "┼", 30 | empty: " ", 31 | symbols: &["│", "╱", "─", "╲"], 32 | }; 33 | 34 | /// ["↑", "↗", "→", "↘", "↓", "↙", "←", "↖"] 35 | pub const ARROW: Set = Set { 36 | full: "↔", 37 | empty: " ", 38 | symbols: &["↑", "↗", "→", "↘", "↓", "↙", "←", "↖"], 39 | }; 40 | 41 | /// ["⇑", "⇗", "⇒", "⇘", "⇓", "⇙", "⇐", "⇖"] 42 | pub const DOUBLE_ARROW: Set = Set { 43 | full: "⇔", 44 | empty: " ", 45 | symbols: &["⇑", "⇗", "⇒", "⇘", "⇓", "⇙", "⇐", "⇖"], 46 | }; 47 | 48 | /// ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"] 49 | pub const VERTICAL_BLOCK: Set = Set { 50 | full: "█", 51 | empty: " ", 52 | symbols: &["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"], 53 | }; 54 | 55 | /// ["▏", "▎", "▍", "▌", "▋", "▊", "▉", "█"] 56 | pub const HORIZONTAL_BLOCK: Set = Set { 57 | full: "█", 58 | empty: " ", 59 | symbols: &["▏", "▎", "▍", "▌", "▋", "▊", "▉", "█"], 60 | }; 61 | 62 | /// ["▝", "▗", "▖", "▘"] 63 | pub const QUADRANT_BLOCK: Set = Set { 64 | full: "█", 65 | empty: " ", 66 | symbols: &["▝", "▗", "▖", "▘"], 67 | }; 68 | 69 | /// ["▙", "▛", "▜", "▟"] 70 | pub const QUADRANT_BLOCK_CRACK: Set = Set { 71 | full: "█", 72 | empty: " ", 73 | symbols: &["▙", "▛", "▜", "▟"], 74 | }; 75 | 76 | /// ["◳", "◲", "◱", "◰"] 77 | pub const WHITE_SQUARE: Set = Set { 78 | full: "⊞", 79 | empty: " ", 80 | symbols: &["◳", "◲", "◱", "◰"], 81 | }; 82 | 83 | /// ["◷", "◶", "◵", "◴"] 84 | pub const WHITE_CIRCLE: Set = Set { 85 | full: "⊕", 86 | empty: " ", 87 | symbols: &["◷", "◶", "◵", "◴"], 88 | }; 89 | 90 | /// ["◑", "◒", "◐", "◓"] 91 | pub const BLACK_CIRCLE: Set = Set { 92 | full: "●", 93 | empty: " ", 94 | symbols: &["◑", "◒", "◐", "◓"], 95 | }; 96 | 97 | /// ["🕛", "🕧", "🕐", "🕜", "🕑", ..., "🕚", "🕦"] 98 | pub const CLOCK: Set = Set { 99 | full: "🕛", 100 | empty: " ", 101 | symbols: &[ 102 | "🕛", "🕧", "🕐", "🕜", "🕑", "🕝", "🕒", "🕞", "🕓", "🕟", "🕔", "🕠", "🕕", "🕡", 103 | "🕖", "🕢", "🕗", "🕣", "🕘", "🕤", "🕙", "🕥", "🕚", "🕦", 104 | ], 105 | }; 106 | 107 | /// ["⠈", "⠐", "⠠", "⠄", "⠂", "⠁"] 108 | pub const BRAILLE_ONE: Set = Set { 109 | full: "⠿", 110 | empty: " ", 111 | symbols: &["⠈", "⠐", "⠠", "⠄", "⠂", "⠁"], 112 | }; 113 | 114 | /// ["⠘", "⠰", "⠤", "⠆", "⠃", "⠉"] 115 | pub const BRAILLE_DOUBLE: Set = Set { 116 | full: "⠿", 117 | empty: " ", 118 | symbols: &["⠘", "⠰", "⠤", "⠆", "⠃", "⠉"], 119 | }; 120 | 121 | /// ["⠷", "⠯", "⠟", "⠻", "⠽", "⠾"] 122 | pub const BRAILLE_SIX: Set = Set { 123 | full: "⠿", 124 | empty: " ", 125 | symbols: &["⠷", "⠯", "⠟", "⠻", "⠽", "⠾"], 126 | }; 127 | 128 | /// ["⠧", "⠏", "⠛", "⠹", "⠼", "⠶"] 129 | pub const BRAILLE_SIX_DOUBLE: Set = Set { 130 | full: "⠿", 131 | empty: " ", 132 | symbols: &["⠧", "⠏", "⠛", "⠹", "⠼", "⠶"], 133 | }; 134 | 135 | /// ["⣷", "⣯", "⣟", "⡿", "⢿", "⣻", "⣽", "⣾"] 136 | pub const BRAILLE_EIGHT: Set = Set { 137 | full: "⣿", 138 | empty: " ", 139 | symbols: &["⣷", "⣯", "⣟", "⡿", "⢿", "⣻", "⣽", "⣾"], 140 | }; 141 | 142 | /// ["⣧", "⣏", "⡟", "⠿", "⢻", "⣹", "⣼", "⣶"] 143 | pub const BRAILLE_EIGHT_DOUBLE: Set = Set { 144 | full: "⣿", 145 | empty: " ", 146 | symbols: &["⣧", "⣏", "⡟", "⠿", "⢻", "⣹", "⣼", "⣶"], 147 | }; 148 | 149 | /// [" ", "ᚐ", "ᚑ", "ᚒ", "ᚓ", "ᚔ"] 150 | pub const OGHAM_A: Set = Set { 151 | full: "ᚔ", 152 | empty: " ", 153 | symbols: &[" ", "ᚐ", "ᚑ", "ᚒ", "ᚓ", "ᚔ"], 154 | }; 155 | 156 | /// [" ", "ᚁ", "ᚂ", "ᚃ", "ᚄ", "ᚅ"] 157 | pub const OGHAM_B: Set = Set { 158 | full: "ᚅ", 159 | empty: " ", 160 | symbols: &[" ", "ᚁ", "ᚂ", "ᚃ", "ᚄ", "ᚅ"], 161 | }; 162 | 163 | /// [" ", "ᚆ", "ᚇ", "ᚈ", "ᚉ", "ᚊ"] 164 | pub const OGHAM_C: Set = Set { 165 | full: "ᚊ", 166 | empty: " ", 167 | symbols: &[" ", "ᚆ", "ᚇ", "ᚈ", "ᚉ", "ᚊ"], 168 | }; 169 | 170 | /// ["⎛", "⎜", "⎝", "⎞", "⎟", "⎠"] 171 | pub const PARENTHESIS: Set = Set { 172 | full: "∫", 173 | empty: " ", 174 | symbols: &["⎛", "⎜", "⎝", "⎞", "⎟", "⎠"], 175 | }; 176 | 177 | /// ["ᔐ", "ᯇ", "ᔑ", "ᯇ"] 178 | pub const CANADIAN: Set = Set { 179 | full: "ᦟ", 180 | empty: " ", 181 | symbols: &["ᔐ", "ᯇ", "ᔑ", "ᯇ"], 182 | }; 183 | } 184 | -------------------------------------------------------------------------------- /src/widgets/mod.rs: -------------------------------------------------------------------------------- 1 | mod throbber; 2 | 3 | pub use self::throbber::Throbber; 4 | pub use self::throbber::ThrobberState; 5 | -------------------------------------------------------------------------------- /src/widgets/throbber.rs: -------------------------------------------------------------------------------- 1 | use rand::Rng as _; 2 | 3 | /// State to be used for Throbber render. 4 | #[derive(Debug, Clone, Default)] 5 | pub struct ThrobberState { 6 | /// Index of Set.symbols used when Spin is specified for WhichUse. 7 | /// 8 | /// If out of range, it is normalized at render time. 9 | index: i8, 10 | } 11 | 12 | impl ThrobberState { 13 | /// Get a index. 14 | pub fn index(&self) -> i8 { 15 | self.index 16 | } 17 | 18 | /// Increase index. 19 | /// 20 | /// # Examples: 21 | /// ``` 22 | /// let mut throbber_state = throbber_widgets_tui::ThrobberState::default(); 23 | /// assert_eq!(throbber_state.index(), 0); 24 | /// throbber_state.calc_next(); 25 | /// assert_eq!(throbber_state.index(), 1); 26 | /// ``` 27 | pub fn calc_next(&mut self) { 28 | self.calc_step(1); 29 | } 30 | 31 | /// Calculate the index by specifying step. 32 | /// 33 | /// Negative numbers can also be specified for step. 34 | /// 35 | /// If step is 0, the index is determined at random. 36 | /// 37 | /// # Examples: 38 | /// ``` 39 | /// let mut throbber_state = throbber_widgets_tui::ThrobberState::default(); 40 | /// assert_eq!(throbber_state.index(), 0); 41 | /// throbber_state.calc_step(2); 42 | /// assert_eq!(throbber_state.index(), 2); 43 | /// throbber_state.calc_step(-3); 44 | /// assert_eq!(throbber_state.index(), -1); 45 | /// throbber_state.calc_step(0); // random 46 | /// assert!((std::i8::MIN..=std::i8::MAX).contains(&throbber_state.index())) 47 | /// ``` 48 | pub fn calc_step(&mut self, step: i8) { 49 | self.index = if step == 0 { 50 | let mut rng = rand::thread_rng(); 51 | rng.gen() 52 | } else { 53 | self.index.checked_add(step).unwrap_or(0) 54 | } 55 | } 56 | 57 | /// Set the index to the range of throbber_set.symbols.len(). 58 | /// 59 | /// This is called from render function automatically. 60 | /// 61 | /// # Examples: 62 | /// ``` 63 | /// let mut throbber_state = throbber_widgets_tui::ThrobberState::default(); 64 | /// let throbber = throbber_widgets_tui::Throbber::default(); 65 | /// let len = 6; //throbber.throbber_set.symbols.len() as i8; 66 | /// 67 | /// throbber_state.normalize(&throbber); 68 | /// assert_eq!(throbber_state.index(), 0); 69 | /// 70 | /// throbber_state.calc_step(len + 2); 71 | /// assert_eq!(throbber_state.index(), len + 2); 72 | /// throbber_state.normalize(&throbber); 73 | /// assert_eq!(throbber_state.index(), 2); 74 | /// 75 | /// // Negative numbers are indexed from backward 76 | /// throbber_state.calc_step(-3 - len); 77 | /// assert_eq!(throbber_state.index(), -1 - len); 78 | /// throbber_state.normalize(&throbber); 79 | /// assert_eq!(throbber_state.index(), len - 1); 80 | /// ``` 81 | pub fn normalize(&mut self, throbber: &Throbber) { 82 | let len = throbber.throbber_set.symbols.len() as i8; 83 | if len <= 0 { 84 | //ng but it's not used, so it stays. 85 | } else { 86 | self.index %= len; 87 | if self.index < 0 { 88 | // Negative numbers are indexed from the tail 89 | self.index += len; 90 | } 91 | } 92 | } 93 | } 94 | 95 | /// A compact widget to display a throbber. 96 | /// 97 | /// A throbber may also be called: 98 | /// - activity indicator 99 | /// - indeterminate progress bar 100 | /// - loading icon 101 | /// - spinner 102 | /// - guru guru 103 | /// 104 | /// # Examples: 105 | /// 106 | /// ``` 107 | /// let throbber = throbber_widgets_tui::Throbber::default() 108 | /// .throbber_style(ratatui::style::Style::default().fg(ratatui::style::Color::White).bg(ratatui::style::Color::Black)) 109 | /// .label("NOW LOADING..."); 110 | /// // frame.render_widget(throbber, chunks[0]); 111 | /// let throbber_state = throbber_widgets_tui::ThrobberState::default(); 112 | /// // frame.render_stateful_widget(throbber, chunks[0], &mut throbber_state); 113 | /// ``` 114 | #[derive(Debug, Clone)] 115 | pub struct Throbber<'a> { 116 | label: Option>, 117 | style: ratatui::style::Style, 118 | throbber_style: ratatui::style::Style, 119 | throbber_set: crate::symbols::throbber::Set, 120 | use_type: crate::symbols::throbber::WhichUse, 121 | } 122 | 123 | impl<'a> Default for Throbber<'a> { 124 | fn default() -> Self { 125 | Self { 126 | label: None, 127 | style: ratatui::style::Style::default(), 128 | throbber_style: ratatui::style::Style::default(), 129 | throbber_set: crate::symbols::throbber::BRAILLE_SIX, 130 | use_type: crate::symbols::throbber::WhichUse::Spin, 131 | } 132 | } 133 | } 134 | 135 | impl<'a> Throbber<'a> { 136 | pub fn label(mut self, label: T) -> Self 137 | where 138 | T: Into>, 139 | { 140 | self.label = Some(label.into()); 141 | self 142 | } 143 | 144 | pub fn style(mut self, style: ratatui::style::Style) -> Self { 145 | self.style = style; 146 | self 147 | } 148 | 149 | pub fn throbber_style(mut self, style: ratatui::style::Style) -> Self { 150 | self.throbber_style = style; 151 | self 152 | } 153 | 154 | pub fn throbber_set(mut self, set: crate::symbols::throbber::Set) -> Self { 155 | self.throbber_set = set; 156 | self 157 | } 158 | 159 | pub fn use_type(mut self, use_type: crate::symbols::throbber::WhichUse) -> Self { 160 | self.use_type = use_type; 161 | self 162 | } 163 | 164 | /// Convert symbol only to Span with state. 165 | pub fn to_symbol_span(&self, state: &ThrobberState) -> ratatui::text::Span<'a> { 166 | let symbol = match self.use_type { 167 | crate::symbols::throbber::WhichUse::Full => self.throbber_set.full, 168 | crate::symbols::throbber::WhichUse::Empty => self.throbber_set.empty, 169 | crate::symbols::throbber::WhichUse::Spin => { 170 | let mut state = state.clone(); 171 | state.normalize(self); 172 | let len = self.throbber_set.symbols.len() as i8; 173 | if 0 <= state.index && state.index < len { 174 | self.throbber_set.symbols[state.index as usize] 175 | } else { 176 | self.throbber_set.empty 177 | } 178 | } 179 | }; 180 | let symbol_span = ratatui::text::Span::styled(format!("{} ", symbol), self.style) 181 | .patch_style(self.throbber_style); 182 | symbol_span 183 | } 184 | 185 | /// Convert symbol and label to Line with state. 186 | pub fn to_line(&self, state: &ThrobberState) -> ratatui::text::Line<'a> { 187 | let mut line = ratatui::text::Line::default().style(self.style); 188 | line.spans.push(self.to_symbol_span(state)); 189 | if let Some(label) = &self.label.clone() { 190 | line.spans.push(label.clone()); 191 | } 192 | line 193 | } 194 | } 195 | 196 | impl<'a> ratatui::widgets::Widget for Throbber<'a> { 197 | /// Render random step symbols. 198 | fn render(self, area: ratatui::layout::Rect, buf: &mut ratatui::buffer::Buffer) { 199 | let mut state = ThrobberState::default(); 200 | state.calc_step(0); 201 | ratatui::widgets::StatefulWidget::render(self, area, buf, &mut state); 202 | } 203 | } 204 | 205 | impl<'a> ratatui::widgets::StatefulWidget for Throbber<'a> { 206 | type State = ThrobberState; 207 | 208 | /// Render specified index symbols. 209 | fn render( 210 | self, 211 | area: ratatui::layout::Rect, 212 | buf: &mut ratatui::buffer::Buffer, 213 | state: &mut Self::State, 214 | ) { 215 | buf.set_style(area, self.style); 216 | 217 | let throbber_area = area; 218 | if throbber_area.height < 1 { 219 | return; 220 | } 221 | 222 | // render a symbol. 223 | let symbol = match self.use_type { 224 | crate::symbols::throbber::WhichUse::Full => self.throbber_set.full, 225 | crate::symbols::throbber::WhichUse::Empty => self.throbber_set.empty, 226 | crate::symbols::throbber::WhichUse::Spin => { 227 | state.normalize(&self); 228 | let len = self.throbber_set.symbols.len() as i8; 229 | if 0 <= state.index && state.index < len { 230 | self.throbber_set.symbols[state.index as usize] 231 | } else { 232 | self.throbber_set.empty 233 | } 234 | } 235 | }; 236 | let symbol_span = ratatui::text::Span::styled(format!("{} ", symbol), self.throbber_style); 237 | let (col, row) = buf.set_span( 238 | throbber_area.left(), 239 | throbber_area.top(), 240 | &symbol_span, 241 | symbol_span.width() as u16, 242 | ); 243 | 244 | // render a label. 245 | if let Some(label) = self.label { 246 | if throbber_area.right() <= col { 247 | return; 248 | } 249 | buf.set_span(col, row, &label, label.width() as u16); 250 | } 251 | } 252 | } 253 | 254 | /// Convert symbol only to Span without state(mostly random index). 255 | /// 256 | /// If you want to specify a state, use `Throbber::to_symbol_span()`. 257 | impl<'a> From> for ratatui::text::Span<'a> { 258 | fn from(throbber: Throbber<'a>) -> ratatui::text::Span<'a> { 259 | let mut state = ThrobberState::default(); 260 | state.calc_step(0); 261 | throbber.to_symbol_span(&state) 262 | } 263 | } 264 | 265 | /// Convert symbol and label to Line without state(mostly random index). 266 | /// 267 | /// If you want to specify a state, use `Throbber::to_line()`. 268 | impl<'a> From> for ratatui::text::Line<'a> { 269 | fn from(throbber: Throbber<'a>) -> ratatui::text::Line<'a> { 270 | let mut state = ThrobberState::default(); 271 | state.calc_step(0); 272 | throbber.to_line(&state) 273 | } 274 | } 275 | 276 | #[cfg(test)] 277 | mod tests { 278 | use super::*; 279 | #[test] 280 | fn throbber_state_calc_step() { 281 | let mut throbber_state = ThrobberState::default(); 282 | assert_eq!(throbber_state.index(), 0); 283 | 284 | // random test 285 | // The probability of failure is not zero, but it is as low as possible. 286 | let mut difference = false; 287 | for _ in 0..100 { 288 | throbber_state.calc_step(0); 289 | assert!((std::i8::MIN..=std::i8::MAX).contains(&throbber_state.index())); 290 | 291 | if 0 != throbber_state.index() { 292 | difference = true; 293 | } 294 | } 295 | assert!(difference); 296 | } 297 | 298 | #[test] 299 | fn throbber_state_normalize() { 300 | let mut throbber_state = ThrobberState::default(); 301 | let throbber = Throbber::default(); 302 | let len = throbber.throbber_set.symbols.len() as i8; 303 | let max = len - 1; 304 | 305 | // check upper 306 | throbber_state.calc_step(max); 307 | throbber_state.normalize(&throbber); 308 | assert_eq!(throbber_state.index(), max); 309 | 310 | // check overflow 311 | throbber_state.calc_next(); 312 | throbber_state.normalize(&throbber); 313 | assert_eq!(throbber_state.index(), 0); 314 | 315 | // check underflow 316 | throbber_state.calc_step(-1); 317 | throbber_state.normalize(&throbber); 318 | assert_eq!(throbber_state.index(), max); 319 | 320 | // check negative out of range 321 | throbber_state.calc_step(len * -2); 322 | throbber_state.normalize(&throbber); 323 | assert_eq!(throbber_state.index(), max); 324 | } 325 | 326 | #[test] 327 | fn throbber_converts_to_span() { 328 | let throbber = Throbber::default().use_type(crate::symbols::throbber::WhichUse::Full); 329 | let span: ratatui::text::Span = throbber.into(); 330 | assert_eq!(span.content, "⠿ "); 331 | } 332 | 333 | #[test] 334 | fn throbber_converts_to_line() { 335 | let throbber = Throbber::default().use_type(crate::symbols::throbber::WhichUse::Full); 336 | let line: ratatui::text::Line = throbber.into(); 337 | assert_eq!(line.spans[0].content, "⠿ "); 338 | } 339 | 340 | #[test] 341 | fn throbber_reaches_upper_limit_step_resets_to_zero() { 342 | let mut throbber_state = ThrobberState::default(); 343 | 344 | for _ in 0..i8::MAX { 345 | throbber_state.calc_next(); 346 | } 347 | throbber_state.calc_next(); 348 | assert!(throbber_state.index() != i8::MAX); 349 | } 350 | } 351 | -------------------------------------------------------------------------------- /tools/check_readme.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | #set -eux 3 | set -eu 4 | 5 | ROOTDIR=$(cd "$(dirname "$0")"/.. && pwd) 6 | cd "$ROOTDIR" 7 | 8 | echo Check MSRV: 9 | cargo_toml_msrv=$(cargo read-manifest | jq -r .rust_version) 10 | readme_msrv=$(grep "MSRV" README.md) 11 | echo "$readme_msrv" | grep "$cargo_toml_msrv" 12 | 13 | echo Check dependencies: 14 | cargo license --color never --direct-deps-only --avoid-build-deps --avoid-dev-deps | awk -F ":" '{printf "|%s|%s|\n", $1, $2}' >temp.tmp 15 | cargo license --color never --avoid-build-deps --avoid-dev-deps | awk -F ":" '{printf "|%s|%s|\n", $1, $2}' >>temp.tmp 16 | grep -f temp.tmp README.md | diff temp.tmp - 17 | rm temp.tmp 18 | 19 | echo Check content between lib.rs doc comment: 20 | awk '/\/\*!/,/\*\//' "src/lib.rs" | \diff README.md - --old-line-format='> %L' --new-line-format='< %L' --unchanged-line-format='' | diff - tools/readme_lib.diff 21 | -------------------------------------------------------------------------------- /tools/publish_crate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # set -eux 3 | set -eu 4 | 5 | ROOTDIR=$(cd "$(dirname "$0")"/.. && pwd) 6 | cd "$ROOTDIR" 7 | 8 | crate_name=$(cargo read-manifest | jq -r ".name") 9 | crate_version=$(cargo read-manifest | jq -r ".version") 10 | 11 | already_exists_tag=$(gh api "repos/arkbig/$crate_name/tags" | jq "any(.[]; .name==\"v$crate_version\")") 12 | if $already_exists_tag; then 13 | echo "Already exists tag v$crate_version" 14 | exit 15 | fi 16 | 17 | git tag "v$crate_version" 18 | git push origin "v$crate_version" 19 | 20 | cargo publish 21 | -------------------------------------------------------------------------------- /tools/readme_lib.diff: -------------------------------------------------------------------------------- 1 | < /*! 2 | > - ぐるぐる(guru guru) 3 | < - guru guru 4 | > ![Demo Animation](./examples/demo.gif) 5 | < ![Demo Animation](https://raw.githubusercontent.com/arkbig/throbber-widgets-tui/main/examples/demo.gif) 6 | > ```sh 7 | > cargo license --direct-deps-only --avoid-build-deps --avoid-dev-deps | awk -F ":" 'BEGIN {printf "|License|crate|\n|-|-|\n"} {printf "|%s|%s|\n", $1, $2}' 8 | > ``` 9 | > 10 | > ```sh 11 | > cargo license --avoid-build-deps --avoid-dev-deps | awk -F ":" 'BEGIN {printf "|License|crate|\n|-|-|\n"} {printf "|%s|%s|\n", $1, $2}' 12 | > ``` 13 | > 14 | < */ 15 | --------------------------------------------------------------------------------