├── .gitignore ├── .cargo └── config.toml ├── assets ├── screenshot.png ├── gifs │ └── complete.gif ├── logo.svg ├── real_curl_example.yaml └── cassettes │ └── complete.tape ├── typos.toml ├── rustfmt.toml ├── .github ├── dependabot.yml ├── codecov.yml └── workflows │ ├── release.yml │ ├── scheduled.yml.bkp │ ├── safety.yml │ ├── quickstartrs.yml.back │ ├── check.yml.nkp │ ├── test.yml.bkp │ └── ci.yaml ├── example_config.yml ├── deny.toml ├── src ├── lib.rs ├── main.rs ├── tstdin.rs ├── bars.rs ├── help.rs ├── event.rs ├── tui.rs ├── cb.rs ├── popup.rs ├── states.rs ├── input.rs ├── args.rs ├── container.rs ├── handler.rs └── app.rs ├── LICENSE ├── Cargo.toml ├── tests └── integration.rs ├── Makefile.toml ├── README.md ├── CHANGELOG.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | assets/shakespeare.txt 3 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [env] 2 | RUST_TEST_THREADS = "1" 3 | -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todoesverso/logss/HEAD/assets/screenshot.png -------------------------------------------------------------------------------- /assets/gifs/complete.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todoesverso/logss/HEAD/assets/gifs/complete.gif -------------------------------------------------------------------------------- /typos.toml: -------------------------------------------------------------------------------- 1 | # configuration for https://github.com/crate-ci/typos 2 | 3 | [default.extend-words] 4 | ratatui = "ratatui" 5 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # configuration for https://rust-lang.github.io/rustfmt/ 2 | group_imports = "StdExternalCrate" 3 | imports_granularity = "Crate" 4 | wrap_comments = true 5 | comment_width = 100 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: daily 7 | - package-ecosystem: cargo 8 | directory: / 9 | schedule: 10 | interval: daily 11 | -------------------------------------------------------------------------------- /assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | logss 4 | -------------------------------------------------------------------------------- /example_config.yml: -------------------------------------------------------------------------------- 1 | command: 2 | - cat 3 | - assets/shakespeare.txt 4 | vertical: true 5 | render: 26 6 | containers: 7 | - re: to 8 | trigger: echo $(date) >> /tmp/dates.txt 9 | timeout: 1 10 | - re: be 11 | trigger: echo '__line__' >> /tmp/match_lines.txt 12 | timeout: 1 13 | - re: or 14 | timeout: 1 15 | - re: not 16 | timeout: 1 17 | - re: to.*be 18 | timeout: 1 19 | output: output_path 20 | -------------------------------------------------------------------------------- /assets/real_curl_example.yaml: -------------------------------------------------------------------------------- 1 | command: 2 | - curl 3 | - -s 4 | - https://raw.githubusercontent.com/linuxacademy/content-elastic-log-samples/master/access.log 5 | render: 95 6 | # output: output_path/ 7 | # single: true 8 | threads: 1 9 | containers: 10 | - re: GET 11 | trigger: echo $(date) >> /tmp/get.log 12 | timeout: 4 13 | - re: \.css 14 | trigger: echo '__line__' >> /tmp/css.log 15 | timeout: 4 16 | - re: GET.*iPhone 17 | -------------------------------------------------------------------------------- /.github/codecov.yml: -------------------------------------------------------------------------------- 1 | # ref: https://docs.codecov.com/docs/codecovyml-reference 2 | coverage: 3 | # Hold ourselves to a high bar 4 | range: 85..100 5 | round: down 6 | precision: 1 7 | status: 8 | # ref: https://docs.codecov.com/docs/commit-status 9 | project: 10 | default: 11 | # Avoid false negatives 12 | threshold: 1% 13 | 14 | # Test files aren't important for coverage 15 | ignore: 16 | - "tests" 17 | 18 | # Make comments less noisy 19 | comment: 20 | layout: "files" 21 | require_changes: yes 22 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | # configuration for https://github.com/EmbarkStudios/cargo-deny 2 | 3 | [licenses] 4 | confidence-threshold = 0.8 5 | allow = [ 6 | "Apache-2.0", 7 | "BSD-2-Clause", 8 | "BSD-3-Clause", 9 | "ISC", 10 | "MIT", 11 | "Unicode-DFS-2016", 12 | "WTFPL", 13 | ] 14 | 15 | [advisories] 16 | unmaintained = "warn" 17 | yanked = "deny" 18 | 19 | [bans] 20 | multiple-versions = "allow" 21 | 22 | [sources] 23 | unknown-registry = "deny" 24 | unknown-git = "warn" 25 | allow-registry = ["https://github.com/rust-lang/crates.io-index"] 26 | -------------------------------------------------------------------------------- /assets/cassettes/complete.tape: -------------------------------------------------------------------------------- 1 | Output "gifs/complete.gif" 2 | Set Theme "OceanicMaterial" 3 | Set FontSize 12 4 | Set Width 824 5 | Set Height 600 6 | Show 7 | Type "cat real_curl_example.yaml" 8 | Enter 9 | Sleep 300ms 10 | Type "logss -f real_curl_example.yaml" 11 | Enter 12 | Sleep 500ms 13 | # Showcase Inputs 14 | Type "i" 15 | Type@200ms "image" 16 | Enter 17 | # Showcase VerticalView 18 | Type "v" 19 | Sleep 1s 20 | Type "v" 21 | Sleep 1s 22 | # Showcase Single 23 | Type "s" 24 | Sleep 2s 25 | Type "s" 26 | Sleep 1s 27 | # Showcase Barchart 28 | Type "b" 29 | Sleep 2s 30 | Type "b" 31 | Sleep 1s 32 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /// Application. 2 | pub mod app; 3 | 4 | /// Terminal events handler. 5 | pub mod event; 6 | 7 | /// Terminal user interface. 8 | pub mod tui; 9 | 10 | /// Event handler. 11 | pub mod handler; 12 | 13 | /// CircularBuffer. 14 | pub mod cb; 15 | 16 | /// Threaded Stdin. 17 | pub mod tstdin; 18 | 19 | /// Command line arguments 20 | pub mod args; 21 | 22 | /// Container matcher 23 | pub mod container; 24 | 25 | /// Help menu 26 | pub mod help; 27 | 28 | /// BarChart 29 | pub mod bars; 30 | 31 | /// State of app and containers 32 | pub mod states; 33 | 34 | /// Popup box 35 | pub mod popup; 36 | 37 | /// Input widget 38 | pub mod input; 39 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Release 3 | 4 | on: 5 | release: 6 | types: [created] 7 | 8 | jobs: 9 | release: 10 | name: release ${{ matrix.target }} 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | include: 16 | - target: x86_64-pc-windows-gnu 17 | archive: zip 18 | - target: x86_64-unknown-linux-musl 19 | archive: tar.gz tar.xz tar.zst 20 | - target: x86_64-apple-darwin 21 | archive: zip 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Compile and release 25 | uses: rust-build/rust-build.action@v1.4.5 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | with: 29 | RUSTTARGET: ${{ matrix.target }} 30 | ARCHIVE_TYPES: ${{ matrix.archive }} 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Victor Rosales 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "logss" 3 | description = "A simple command line tool that helps you visualize an input stream of text." 4 | version = "0.0.4" 5 | authors = ["Victor Rosales "] 6 | edition = "2021" 7 | publish = true 8 | repository = "https://github.com/todoesverso/logss" 9 | keywords = ["terminal", "logs", "cli", "split"] 10 | categories = ["visualization", "command-line-utilities", "parsing"] 11 | license = "MIT" 12 | rust-version = "1.74.0" 13 | exclude = [ 14 | "assets/*", 15 | ".github", 16 | "Makefile.toml", 17 | "CONTRIBUTING.md", 18 | "*.log", 19 | "tags", 20 | ] 21 | readme = "README.md" 22 | 23 | [badges] 24 | 25 | [dependencies] 26 | crossterm = "0.29.0" 27 | is-terminal = "0.4.16" 28 | unicode-width = "0.2.0" 29 | pico-args = "0.5.0" 30 | ratatui = "0.29.0" 31 | regex = "1.12.2" 32 | serde_yaml = "0.9.33" 33 | serde = { version = "1.0.228", features = ["derive"] } 34 | proc-macro2 = "1.0.101" 35 | slug = "0.1.6" 36 | anyhow = "1.0.100" 37 | threadpool = "1.8.1" 38 | wait-timeout = "0.2.1" 39 | 40 | [profile.release] 41 | codegen-units = 1 42 | lto = "fat" 43 | opt-level = 3 44 | strip = true 45 | 46 | [dev-dependencies] 47 | assert_cmd = "2.0.10" 48 | predicates = "3.0.3" 49 | -------------------------------------------------------------------------------- /.github/workflows/scheduled.yml.bkp: -------------------------------------------------------------------------------- 1 | permissions: 2 | contents: read 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | schedule: 8 | - cron: '7 7 * * *' 9 | name: rolling 10 | jobs: 11 | # https://twitter.com/mycoliza/status/1571295690063753218 12 | nightly: 13 | runs-on: ubuntu-latest 14 | name: ubuntu / nightly 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | submodules: true 19 | - name: Install nightly 20 | uses: dtolnay/rust-toolchain@nightly 21 | - name: cargo generate-lockfile 22 | if: hashFiles('Cargo.lock') == '' 23 | run: cargo generate-lockfile 24 | - name: cargo test --locked 25 | run: cargo test --locked --all-features --all-targets 26 | # https://twitter.com/alcuadrado/status/1571291687837732873 27 | update: 28 | runs-on: ubuntu-latest 29 | name: ubuntu / beta / updated 30 | # There's no point running this if no Cargo.lock was checked in in the 31 | # first place, since we'd just redo what happened in the regular test job. 32 | # Unfortunately, hashFiles only works in if on steps, so we reepeat it. 33 | # if: hashFiles('Cargo.lock') != '' 34 | steps: 35 | - uses: actions/checkout@v4 36 | with: 37 | submodules: true 38 | - name: Install beta 39 | if: hashFiles('Cargo.lock') != '' 40 | uses: dtolnay/rust-toolchain@beta 41 | - name: cargo update 42 | if: hashFiles('Cargo.lock') != '' 43 | run: cargo update 44 | - name: cargo test 45 | if: hashFiles('Cargo.lock') != '' 46 | run: cargo test --locked --all-features --all-targets 47 | env: 48 | RUSTFLAGS: -D deprecated 49 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use anyhow::Result; 4 | use is_terminal::IsTerminal; 5 | use logss::{ 6 | app::App, 7 | args::parse_args, 8 | event::{Event, EventHandler}, 9 | handler::handle_key_events, 10 | tui::Tui, 11 | }; 12 | use ratatui::{backend::CrosstermBackend, Terminal}; 13 | 14 | fn main() -> Result<()> { 15 | let args = parse_args(); 16 | let render_speed = args.render.unwrap_or(100); 17 | 18 | if args.command.is_none() && std::io::stdin().is_terminal() { 19 | eprintln!("No command provided and no data piped."); 20 | eprintln!("Please pipe some data to this command. Exiting."); 21 | std::process::exit(1); 22 | } 23 | 24 | // Create an application. 25 | let mut app = App::new(Some(args)); 26 | // First we try to start app so that it can fail and we do not mess with the console 27 | app.init()?; 28 | 29 | // Initialize the terminal user interface. 30 | let backend = CrosstermBackend::new(io::stderr()); 31 | let terminal = Terminal::new(backend)?; 32 | let events = EventHandler::new(render_speed); 33 | let mut tui = Tui::new(terminal, events); 34 | tui.init()?; 35 | 36 | // Start the main loop. 37 | while app.is_running() { 38 | // Render the user interface. 39 | tui.draw(&mut app)?; 40 | // Handle events. 41 | match tui.events.next()? { 42 | Event::Tick => app.tick(), 43 | Event::Key(key_event) => handle_key_events(key_event, &mut app)?, 44 | Event::Mouse(_) => {} 45 | Event::Resize(_, _) => {} 46 | } 47 | } 48 | 49 | // Exit the user interface. 50 | tui.exit()?; 51 | Ok(()) 52 | } 53 | #[cfg(test)] 54 | mod tests { 55 | //use super::*; 56 | 57 | // Unit test for main function 58 | #[test] 59 | fn test_main() { 60 | //let result = main(); 61 | //assert!(result.is_ok()); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /.github/workflows/safety.yml: -------------------------------------------------------------------------------- 1 | permissions: 2 | contents: read 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | # Spend CI time only on latest ref: https://github.com/jonhoo/rust-ci-conf/pull/5 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 10 | cancel-in-progress: true 11 | name: safety 12 | jobs: 13 | sanitizers: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | submodules: true 19 | - name: Install nightly 20 | uses: dtolnay/rust-toolchain@nightly 21 | - run: | 22 | # to get the symbolizer for debug symbol resolution 23 | sudo apt install llvm 24 | # to fix buggy leak analyzer: 25 | # https://github.com/japaric/rust-san#unrealiable-leaksanitizer 26 | # ensure there's a profile.dev section 27 | if ! grep -qE '^[ \t]*[profile.dev]' Cargo.toml; then 28 | echo >> Cargo.toml 29 | echo '[profile.dev]' >> Cargo.toml 30 | fi 31 | # remove pre-existing opt-levels in profile.dev 32 | sed -i '/^\s*\[profile.dev\]/,/^\s*\[/ {/^\s*opt-level/d}' Cargo.toml 33 | # now set opt-level to 1 34 | sed -i '/^\s*\[profile.dev\]/a opt-level = 1' Cargo.toml 35 | cat Cargo.toml 36 | name: Enable debug symbols 37 | - name: cargo test -Zsanitizer=address 38 | # only --lib --tests b/c of https://github.com/rust-lang/rust/issues/53945 39 | run: cargo test --lib --tests --all-features --target x86_64-unknown-linux-gnu 40 | env: 41 | ASAN_OPTIONS: "detect_odr_violation=0:detect_leaks=0" 42 | RUSTFLAGS: "-Z sanitizer=address" 43 | - name: cargo test -Zsanitizer=leak 44 | if: always() 45 | run: cargo test --all-features --target x86_64-unknown-linux-gnu 46 | env: 47 | LSAN_OPTIONS: "suppressions=lsan-suppressions.txt" 48 | RUSTFLAGS: "-Z sanitizer=leak" 49 | -------------------------------------------------------------------------------- /src/tstdin.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::{stdin, BufRead, BufReader, Error}, 3 | process::{Command, Stdio}, 4 | sync::{mpsc, mpsc::Sender}, 5 | thread, 6 | }; 7 | 8 | use anyhow::Result; 9 | 10 | #[derive(Debug)] 11 | pub struct StdinHandler { 12 | receiver: mpsc::Receiver, 13 | pub sender: mpsc::Sender, 14 | } 15 | 16 | impl Default for StdinHandler { 17 | fn default() -> Self { 18 | Self::new() 19 | } 20 | } 21 | 22 | impl StdinHandler { 23 | pub fn new() -> Self { 24 | let (sender, receiver) = mpsc::channel(); 25 | Self { receiver, sender } 26 | } 27 | 28 | pub fn init(&self, cmd: Option>) -> Result<()> { 29 | let sender = self.sender.clone(); 30 | match cmd { 31 | Some(inner_cmd) => { 32 | let child = Command::new(&inner_cmd[0]) 33 | .args(&inner_cmd[1..]) 34 | .stderr(Stdio::null()) 35 | .stdout(Stdio::piped()) 36 | .spawn()?; 37 | 38 | let stdout = child 39 | .stdout 40 | .ok_or_else(|| Error::other("Failed to run command"))?; 41 | let reader = BufReader::new(stdout); 42 | read_lines_and_send(reader, sender); 43 | } 44 | // If no command set then we are being pipped 45 | None => { 46 | let stdin = stdin(); 47 | let reader = BufReader::new(stdin); 48 | 49 | read_lines_and_send(reader, sender); 50 | } 51 | } 52 | 53 | Ok(()) 54 | } 55 | 56 | pub fn recv(&self) -> Result { 57 | self.receiver.recv() 58 | } 59 | 60 | pub fn try_recv(&self) -> Result { 61 | self.receiver.try_recv() 62 | } 63 | } 64 | 65 | fn read_lines_and_send(mut reader: R, sender: Sender) 66 | where 67 | R: BufRead + Send + 'static, 68 | { 69 | let mut line = String::new(); 70 | thread::spawn(move || loop { 71 | match reader.read_line(&mut line) { 72 | Ok(len) => { 73 | if len == 0 { 74 | break; 75 | } else { 76 | sender.send(line.clone()).ok(); 77 | } 78 | } 79 | Err(e) => { 80 | sender.send(e.to_string()).ok(); 81 | break; 82 | } 83 | } 84 | line.clear(); 85 | }); 86 | } 87 | -------------------------------------------------------------------------------- /src/bars.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{ 2 | style::{Color, Style}, 3 | text::Line, 4 | widgets::{Bar, BarChart, BarGroup, Block, Borders}, 5 | Frame, 6 | }; 7 | 8 | use crate::{ 9 | app::App, 10 | container::Container, 11 | popup::{centered_rect, render_bar_chart_popup}, 12 | }; 13 | 14 | pub fn render_bar_chart(frame: &mut Frame, app: &App) { 15 | let bargroup = create_groups(app); 16 | let rect = centered_rect(50, 50, frame.area()); 17 | let containers_count = app.containers.len() as u16; 18 | let bar_width = (rect.width - (containers_count)) / containers_count; 19 | let corrected_bw = if bar_width * containers_count + containers_count == rect.width { 20 | bar_width - 1 21 | } else { 22 | bar_width 23 | }; 24 | let style = Style::default().fg(Color::White).bg(Color::Black); 25 | let title = "Counts"; 26 | let barchart = BarChart::default() 27 | .block(create_block(title)) 28 | .data(bargroup) 29 | .bar_gap(1) 30 | .bar_width(corrected_bw) 31 | .value_style(Style::default().fg(Color::Black)) 32 | .style(style); 33 | render_bar_chart_popup(frame, barchart, (50, 50)); 34 | } 35 | 36 | fn create_block(title: &str) -> Block<'_> { 37 | Block::default().title(title).borders(Borders::ALL) 38 | } 39 | 40 | fn create_groups<'a>(app: &'a App) -> BarGroup<'a> { 41 | let bars: Vec = app.containers.iter().map(create_bar).collect(); 42 | BarGroup::default().bars(&bars) 43 | } 44 | 45 | fn create_bar<'a>(c: &'a Container) -> Bar<'a> { 46 | Bar::default() 47 | .value(c.get_count()) 48 | .style(Style::default().fg(c.state.color)) 49 | .value_style(Style::default().fg(c.state.color)) 50 | .label(Line::from(c.text.clone())) 51 | } 52 | 53 | #[cfg(test)] 54 | mod tests { 55 | use super::*; 56 | 57 | #[test] 58 | fn test_create_block() { 59 | let block = create_block("block"); 60 | let block_base = Block::default().title("block").borders(Borders::ALL); 61 | assert_eq!(block_base, block); 62 | } 63 | 64 | #[test] 65 | fn test_create_bar() { 66 | let container = Container::new("test".to_string(), None, 1, 1, 1); 67 | let bar = create_bar(&container); 68 | let base_bar = Bar::default() 69 | .value(0) 70 | .style(Style::default().fg(Color::Red)) 71 | .value_style(Style::default().fg(Color::Red)) 72 | .label(Line::from("test")); 73 | 74 | assert_eq!(base_bar, bar); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /.github/workflows/quickstartrs.yml.back: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: Continuous integration 4 | 5 | jobs: 6 | check: 7 | name: Check 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: actions-rs/toolchain@v1.0.7 12 | with: 13 | profile: minimal 14 | toolchain: stable 15 | override: true 16 | - uses: actions-rs/cargo@v1.0.3 17 | with: 18 | command: check 19 | 20 | test: 21 | name: Test Suite 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v3 25 | - uses: actions-rs/toolchain@v1.0.7 26 | with: 27 | profile: minimal 28 | toolchain: stable 29 | override: true 30 | - name: Rust Cache 31 | uses: Swatinem/rust-cache@v2.2.1 32 | - uses: actions-rs/cargo@v1.0.3 33 | with: 34 | command: test 35 | 36 | fmt: 37 | name: Rustfmt 38 | runs-on: ubuntu-latest 39 | steps: 40 | - uses: actions/checkout@v3 41 | - uses: actions-rs/toolchain@v1.0.7 42 | with: 43 | profile: minimal 44 | toolchain: stable 45 | override: true 46 | - run: rustup component add rustfmt 47 | - uses: actions-rs/cargo@v1.0.3 48 | with: 49 | command: fmt 50 | args: --all -- --check 51 | 52 | clippy: 53 | name: Clippy 54 | runs-on: ubuntu-latest 55 | steps: 56 | - uses: actions/checkout@v3 57 | - uses: actions-rs/toolchain@v1.0.7 58 | with: 59 | toolchain: nightly 60 | components: clippy 61 | override: true 62 | - uses: actions-rs/clippy-check@v1.0.7 63 | with: 64 | token: ${{ secrets.GITHUB_TOKEN }} 65 | args: --all-features 66 | 67 | coverage: 68 | name: Coverage 69 | runs-on: ubuntu-latest 70 | env: 71 | CARGO_TERM_COLOR: always 72 | steps: 73 | - uses: actions/checkout@v3 74 | - name: Install Rust 75 | run: rustup update stable 76 | - name: Install cargo-llvm-cov 77 | uses: taiki-e/install-action@cargo-llvm-cov 78 | - name: Generate code coverage 79 | run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info 80 | - name: Upload coverage to Codecov 81 | uses: codecov/codecov-action@v3 82 | with: 83 | token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos 84 | files: lcov.info 85 | fail_ci_if_error: true 86 | -------------------------------------------------------------------------------- /src/help.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{ 2 | style::{Color, Style}, 3 | text::{Line, Span}, 4 | Frame, 5 | }; 6 | 7 | use crate::popup::render_popup; 8 | 9 | pub fn render_help(frame: &mut Frame) { 10 | let help_text = vec![ 11 | Line::from(Span::styled( 12 | "h - toggles help popup", 13 | Style::default(), 14 | )), 15 | Line::from(Span::styled( 16 | "b - toggles BarChart popup", 17 | Style::default(), 18 | )), 19 | Line::from(Span::styled( 20 | "w - toggles text wrapping", 21 | Style::default(), 22 | )), 23 | Line::from(Span::styled( 24 | "i|/ - input new container (Enter/Esc)", 25 | Style::default(), 26 | )), 27 | Line::from(Span::styled( 28 | "p|Space - toggles scrolling", 29 | Style::default(), 30 | )), 31 | Line::from(Span::styled("v - toggles vertical", Style::default())), 32 | Line::from(Span::styled( 33 | "* - toggles between containers and raw input", 34 | Style::default(), 35 | )), 36 | Line::from(Span::styled( 37 | "s - toggles between containers and single input", 38 | Style::default(), 39 | )), 40 | Line::from(Span::styled( 41 | "1-9 - toggles zoom to specific container", 42 | Style::default(), 43 | )), 44 | Line::from(Span::styled( 45 | "Alt+1-9 - removes specific container", 46 | Style::default(), 47 | )), 48 | Line::from(Span::styled( 49 | "F1/9 - toggles hide/show for container", 50 | Style::default(), 51 | )), 52 | Line::from(Span::styled("Up/Down - Scrolls lines", Style::default())), 53 | Line::from(Span::styled( 54 | "c - continues autoscroll", 55 | Style::default(), 56 | )), 57 | Line::from(Span::styled( 58 | "Esc - exits the program", 59 | Style::default().bg(Color::Red), 60 | )), 61 | ]; 62 | render_popup(frame, "Help", &help_text, (50, 50)); 63 | } 64 | 65 | #[cfg(test)] 66 | mod tests { 67 | use ratatui::{backend::TestBackend, Terminal}; 68 | 69 | use super::*; 70 | 71 | #[test] 72 | fn test_render_help() { 73 | let backend = TestBackend::new(40, 40); 74 | let mut terminal = Terminal::new(backend).unwrap(); 75 | let res = terminal 76 | .draw(|f| { 77 | render_help(f); 78 | }) 79 | .is_ok(); 80 | assert!(res); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /tests/integration.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use assert_cmd::Command; 4 | use predicates::prelude::*; 5 | 6 | #[test] 7 | fn non_valid_arg() { 8 | let mut cmd = Command::cargo_bin("logss").unwrap(); 9 | cmd.arg("-e").arg("--non-valid-arg"); 10 | 11 | cmd.assert() 12 | .stdout(predicate::str::is_empty()) 13 | .stderr(predicate::str::contains("Error: non valid arguments")); 14 | } 15 | 16 | #[test] 17 | fn show_help() { 18 | let mut cmd = Command::cargo_bin("logss").unwrap(); 19 | cmd.arg("-h"); 20 | 21 | cmd.assert() 22 | .success() 23 | .stdout(predicate::str::contains( 24 | "Simple CLI command to display logs in a user-friendly way 25 | 26 | Usage: logss [OPTIONS] 27 | 28 | Options: 29 | -c Specify substrings (regex patterns) 30 | -e Exit on empty input [default: false] 31 | -s Start in single view mode [default: false] 32 | -C Get input from a command 33 | -f Input configuration file (overrides CLI arguments) 34 | -o Specify the output path for matched patterns 35 | -r Define render speed in milliseconds [default: 100] 36 | -t Number of threads per container for triggers [default: 1] 37 | -V Start in vertical view mode 38 | -h Print help 39 | ", 40 | )) 41 | .stderr(predicate::str::is_empty()); 42 | } 43 | 44 | #[test] 45 | #[ignore] 46 | fn simple_piped_run() { 47 | let mut cmd = Command::cargo_bin("logss").unwrap(); 48 | let c_path = Path::new("README.md"); 49 | cmd.pipe_stdin(c_path).unwrap(); 50 | cmd.arg("-e") 51 | .arg("-c") 52 | .arg("version") 53 | .arg("-c") 54 | .arg("package") 55 | .arg("-r") 56 | .arg("25"); 57 | 58 | cmd.assert() 59 | .success() 60 | .stderr(predicate::str::contains("package")) 61 | .stderr(predicate::str::contains("version")) 62 | .stderr(predicate::str::contains("name").not()) 63 | .stdout(predicate::str::is_empty()); 64 | } 65 | 66 | #[test] 67 | #[ignore] 68 | fn simple_command_run() { 69 | let mut cmd = Command::cargo_bin("logss").unwrap(); 70 | cmd.arg("-e") 71 | .arg("-c") 72 | .arg("version") 73 | .arg("-c") 74 | .arg("package") 75 | .arg("-C") 76 | .arg("cat Cargo.toml") 77 | .arg("-r") 78 | .arg("25"); 79 | 80 | cmd.assert() 81 | .success() 82 | .stderr(predicate::str::contains("package")) 83 | .stderr(predicate::str::contains("version")) 84 | .stderr(predicate::str::contains("name").not()) 85 | .stdout(predicate::str::is_empty()); 86 | } 87 | -------------------------------------------------------------------------------- /.github/workflows/check.yml.nkp: -------------------------------------------------------------------------------- 1 | permissions: 2 | contents: read 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | name: check 8 | jobs: 9 | fmt: 10 | runs-on: ubuntu-latest 11 | name: stable / fmt 12 | steps: 13 | - uses: actions/checkout@v4 14 | with: 15 | submodules: true 16 | - name: Install stable 17 | uses: dtolnay/rust-toolchain@stable 18 | with: 19 | components: rustfmt 20 | - name: cargo fmt --check 21 | run: cargo fmt --check 22 | clippy: 23 | runs-on: ubuntu-latest 24 | name: ${{ matrix.toolchain }} / clippy 25 | permissions: 26 | contents: read 27 | checks: write 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | toolchain: [stable, beta] 32 | steps: 33 | - uses: actions/checkout@v4 34 | with: 35 | submodules: true 36 | - name: Install ${{ matrix.toolchain }} 37 | uses: dtolnay/rust-toolchain@master 38 | with: 39 | toolchain: ${{ matrix.toolchain }} 40 | components: clippy 41 | - name: cargo clippy 42 | uses: actions-rs/clippy-check@v1 43 | with: 44 | token: ${{ secrets.GITHUB_TOKEN }} 45 | doc: 46 | runs-on: ubuntu-latest 47 | name: nightly / doc 48 | steps: 49 | - uses: actions/checkout@v4 50 | with: 51 | submodules: true 52 | - name: Install nightly 53 | uses: dtolnay/rust-toolchain@nightly 54 | - name: cargo doc 55 | run: cargo doc --no-deps --all-features 56 | env: 57 | RUSTDOCFLAGS: --cfg docsrs 58 | hack: 59 | runs-on: ubuntu-latest 60 | name: ubuntu / stable / features 61 | steps: 62 | - uses: actions/checkout@v4 63 | with: 64 | submodules: true 65 | - name: Install stable 66 | uses: dtolnay/rust-toolchain@stable 67 | - name: cargo install cargo-hack 68 | uses: taiki-e/install-action@cargo-hack 69 | - name: cargo hack 70 | run: cargo hack --feature-powerset check --lib --tests 71 | msrv: 72 | runs-on: ubuntu-latest 73 | # we use a matrix here just because env can't be used in job names 74 | # https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability 75 | strategy: 76 | matrix: 77 | msrv: [1.67.0] # crates-index 78 | name: ubuntu / ${{ matrix.msrv }} 79 | steps: 80 | - uses: actions/checkout@v4 81 | with: 82 | submodules: true 83 | - name: Install ${{ matrix.msrv }} 84 | uses: dtolnay/rust-toolchain@master 85 | with: 86 | toolchain: ${{ matrix.msrv }} 87 | - name: cargo +${{ matrix.msrv }} check 88 | run: cargo check 89 | -------------------------------------------------------------------------------- /Makefile.toml: -------------------------------------------------------------------------------- 1 | # configuration for https://github.com/sagiegurari/cargo-make 2 | 3 | [config] 4 | skip_core_tasks = true 5 | 6 | [env] 7 | # all features except the backend ones 8 | ALL_FEATURES = "all-widgets,macros,serde" 9 | 10 | # Windows does not support building termion, so this avoids the build failure by providing two 11 | # sets of flags, one for Windows and one for other platforms. 12 | # Windows: --features=all-widgets,macros,serde,crossterm,termwiz 13 | # Other: --all-features 14 | ALL_FEATURES_FLAG = { source = "${CARGO_MAKE_RUST_TARGET_OS}", default_value = "--all-features", mapping = { "windows" = "--features=all-widgets,macros,serde,crossterm,termwiz" } } 15 | 16 | [tasks.default] 17 | alias = "ci" 18 | 19 | [tasks.ci] 20 | description = "Run continuous integration tasks" 21 | dependencies = ["lint-style", "clippy", "check", "test"] 22 | 23 | [tasks.lint-style] 24 | description = "Lint code style (formatting, typos, docs)" 25 | dependencies = ["lint-format", "lint-typos", "lint-docs"] 26 | 27 | [tasks.lint-format] 28 | description = "Lint code formatting" 29 | toolchain = "nightly" 30 | command = "cargo" 31 | args = ["fmt", "--all", "--check"] 32 | 33 | [tasks.format] 34 | description = "Fix code formatting" 35 | toolchain = "nightly" 36 | command = "cargo" 37 | args = ["fmt", "--all"] 38 | 39 | [tasks.lint-typos] 40 | description = "Run typo checks" 41 | install_crate = { crate_name = "typos-cli", binary = "typos", test_arg = "--version" } 42 | command = "typos" 43 | 44 | [tasks.lint-docs] 45 | description = "Check documentation for errors and warnings" 46 | toolchain = "nightly" 47 | command = "cargo" 48 | args = [ 49 | "rustdoc", 50 | "--", 51 | "-Zunstable-options", 52 | "--check", 53 | "-Dwarnings", 54 | ] 55 | 56 | [tasks.check] 57 | description = "Check code for errors and warnings" 58 | command = "cargo" 59 | args = [ 60 | "check", 61 | "--all-targets", 62 | ] 63 | 64 | [tasks.build] 65 | description = "Compile the project" 66 | command = "cargo" 67 | args = [ 68 | "build", 69 | "--all-targets", 70 | ] 71 | 72 | [tasks.clippy] 73 | description = "Run Clippy for linting" 74 | command = "cargo" 75 | args = [ 76 | "clippy", 77 | "--all-targets", 78 | "--tests", 79 | "--benches", 80 | "--", 81 | "-D", 82 | "warnings", 83 | ] 84 | 85 | [tasks.test] 86 | description = "Run tests" 87 | dependencies = ["test-doc"] 88 | command = "cargo" 89 | args = [ 90 | "test", 91 | "--all-targets", 92 | "--locked", 93 | "--all-features" 94 | ] 95 | 96 | [tasks.test-doc] 97 | description = "Run documentation tests" 98 | command = "cargo" 99 | args = ["test", "--doc", "--no-default-features"] 100 | 101 | [tasks.coverage] 102 | description = "Generate code coverage report" 103 | command = "cargo" 104 | args = [ 105 | "llvm-cov", 106 | "--lcov", 107 | "--output-path", 108 | "target/lcov.info", 109 | "--no-default-features", 110 | "${ALL_FEATURES_FLAG}", 111 | ] 112 | -------------------------------------------------------------------------------- /src/event.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | sync::mpsc, 3 | thread, 4 | time::{Duration, Instant}, 5 | }; 6 | 7 | use anyhow::Result; 8 | use crossterm::event::{self, Event as CrosstermEvent, KeyEvent, MouseEvent}; 9 | 10 | /// Terminal events. 11 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 12 | pub enum Event { 13 | /// Terminal tick. 14 | Tick, 15 | /// Key press. 16 | Key(KeyEvent), 17 | /// Mouse click/scroll. 18 | Mouse(MouseEvent), 19 | /// Terminal resize. 20 | Resize(u16, u16), 21 | } 22 | 23 | /// Terminal event handler. 24 | #[derive(Debug)] 25 | pub struct EventHandler { 26 | /// Event receiver channel. 27 | receiver: mpsc::Receiver, 28 | sender: mpsc::Sender, 29 | tick_rate: u64, 30 | } 31 | 32 | impl EventHandler { 33 | /// Constructs a new instance of [`EventHandler`]. 34 | pub fn new(tick_rate: u64) -> Self { 35 | let (sender, receiver) = mpsc::channel(); 36 | 37 | Self { 38 | receiver, 39 | sender, 40 | tick_rate, 41 | } 42 | } 43 | 44 | pub fn init(&self) { 45 | let tick_rate = Duration::from_millis(self.tick_rate); 46 | let sender = self.sender.clone(); 47 | thread::spawn(move || { 48 | let mut last_tick = Instant::now(); 49 | loop { 50 | let timeout = tick_rate 51 | .checked_sub(last_tick.elapsed()) 52 | .unwrap_or(tick_rate); 53 | 54 | if event::poll(timeout).expect("no events available") { 55 | match event::read().expect("unable to read event") { 56 | CrosstermEvent::Key(e) => sender.send(Event::Key(e)), 57 | CrosstermEvent::Mouse(e) => sender.send(Event::Mouse(e)), 58 | CrosstermEvent::Resize(w, h) => sender.send(Event::Resize(w, h)), 59 | _ => Ok(()), 60 | } 61 | .expect("failed to send terminal event") 62 | } 63 | 64 | if last_tick.elapsed() >= tick_rate { 65 | sender.send(Event::Tick).ok(); 66 | last_tick = Instant::now(); 67 | } 68 | } 69 | }); 70 | } 71 | 72 | /// Receive the next event from the handler thread. 73 | /// 74 | /// This function will always block the current thread if 75 | /// there is no data available and it's possible for more data to be sent. 76 | pub fn next(&self) -> Result { 77 | Ok(self.receiver.recv()?) 78 | } 79 | } 80 | 81 | #[cfg(test)] 82 | mod tests { 83 | use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; 84 | 85 | use super::*; 86 | #[test] 87 | fn new() { 88 | let event = EventHandler::new(1); 89 | event.sender.send(Event::Tick).unwrap(); 90 | assert_eq!(event.next().unwrap(), Event::Tick); 91 | let key = KeyEvent::new(KeyCode::Char('1'), KeyModifiers::NONE); 92 | event.sender.send(Event::Key(key)).unwrap(); 93 | assert_eq!(event.next().unwrap(), Event::Key(key)); 94 | } 95 | #[test] 96 | fn init() { 97 | // just call it and expect not to panic 98 | let event = EventHandler::new(1); 99 | event.init(); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/tui.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use anyhow::Result; 4 | use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}; 5 | use ratatui::{backend::Backend, Terminal}; 6 | 7 | use crate::{app::App, event::EventHandler}; 8 | 9 | /// Representation of a terminal user interface. 10 | /// 11 | /// It is responsible for setting up the terminal, 12 | /// initializing the interface and handling the draw events. 13 | #[derive(Debug)] 14 | pub struct Tui { 15 | /// Interface to the Terminal. 16 | terminal: Terminal, 17 | /// Terminal event handler. 18 | pub events: EventHandler, 19 | } 20 | 21 | impl Tui { 22 | /// Constructs a new instance of [`Tui`]. 23 | pub fn new(terminal: Terminal, events: EventHandler) -> Self { 24 | Self { terminal, events } 25 | } 26 | 27 | /// Initializes the terminal interface. 28 | /// 29 | /// It enables the raw mode and sets terminal properties. 30 | pub fn init(&mut self) -> Result<()> { 31 | terminal::enable_raw_mode()?; 32 | crossterm::execute!(io::stderr(), EnterAlternateScreen)?; 33 | self.terminal.hide_cursor()?; 34 | self.terminal.clear()?; 35 | self.events.init(); 36 | Ok(()) 37 | } 38 | 39 | /// [`Draw`] the terminal interface by [`rendering`] the widgets. 40 | /// 41 | /// [`Draw`]: ratatui::Terminal::draw 42 | /// [`rendering`]: crate::app::App::render 43 | pub fn draw(&mut self, app: &mut App) -> Result<()> { 44 | self.terminal.draw(|frame| app.render(frame))?; 45 | Ok(()) 46 | } 47 | 48 | /// Exits the terminal interface. 49 | /// 50 | /// It disables the raw mode and reverts back the terminal properties. 51 | pub fn exit(&mut self) -> Result<()> { 52 | terminal::disable_raw_mode()?; 53 | crossterm::execute!(io::stderr(), LeaveAlternateScreen)?; 54 | self.terminal.show_cursor()?; 55 | Ok(()) 56 | } 57 | } 58 | 59 | #[cfg(test)] 60 | mod tests { 61 | use ratatui::{ 62 | backend::TestBackend, 63 | buffer::Buffer, 64 | style::{Color, Modifier, Style}, 65 | Terminal, 66 | }; 67 | 68 | use super::*; 69 | use crate::args::parse_args; 70 | 71 | #[test] 72 | fn new() { 73 | let backend = TestBackend::new(13, 10); 74 | let terminal = Terminal::new(backend).unwrap(); 75 | let events = EventHandler::new(1); 76 | let mut app = App::new(Some(parse_args())); 77 | app.raw_buffer.state.color = Color::White; 78 | 79 | let mut tui = Tui::new(terminal, events); 80 | //tui.init().unwrap(); // This fails in github tests 81 | tui.draw(&mut app).unwrap(); 82 | let mut expected = Buffer::with_lines(vec![ 83 | "┌[0]'.*' (0)┐", 84 | "│ │", 85 | "│ │", 86 | "│ │", 87 | "│ │", 88 | "│ │", 89 | "│ │", 90 | "│ │", 91 | "│ │", 92 | "└───────────┘", 93 | ]); 94 | let bolds = 1..=11; 95 | for x in 0..=12 { 96 | for y in 0..=9 { 97 | if bolds.contains(&x) && y == 0 { 98 | expected[(x, y)].set_style(Style::default().add_modifier(Modifier::BOLD)); 99 | } 100 | expected[(x, y)].set_fg(Color::White); 101 | expected[(x, y)].set_bg(Color::Black); 102 | } 103 | } 104 | 105 | tui.terminal.backend().assert_buffer(&expected); 106 | tui.exit().unwrap(); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/cb.rs: -------------------------------------------------------------------------------- 1 | /// Generic CircularBuffer where oldest element are overwritten 2 | /// by the new ones. 3 | /// 4 | /// Usage example: 5 | /// ``` 6 | /// extern crate logss; 7 | /// use logss::cb; 8 | /// let mut cb:cb::CircularBuffer = cb::CircularBuffer::new(3); 9 | /// cb.push(1); 10 | /// cb.push(2); 11 | /// cb.push(3); 12 | /// cb.push(4); 13 | /// assert_eq!(cb.buffer, vec![4, 2, 3]); 14 | /// 15 | /// // Notice the order that `ordered_clone` returns 16 | /// let cb2 = cb.ordered_clone(); 17 | /// assert_eq!(cb2.buffer, vec![2, 3, 4]); 18 | /// ``` 19 | #[derive(Debug)] 20 | pub struct CircularBuffer { 21 | /// Actual buffer 22 | pub buffer: Vec, 23 | /// index that keeps track of the writes 24 | write_index: usize, 25 | } 26 | 27 | impl CircularBuffer 28 | where 29 | T: Clone, 30 | { 31 | /// Constructs a new instance of [`CircularBuffer`]. 32 | pub fn new(capacity: usize) -> Self { 33 | Self { 34 | buffer: Vec::with_capacity(capacity), 35 | write_index: 0, 36 | } 37 | } 38 | 39 | /// Returns the buffer capacity 40 | pub fn capacity(&self) -> usize { 41 | self.buffer.capacity() 42 | } 43 | 44 | /// Returns the buffer length 45 | pub fn len(&self) -> usize { 46 | self.buffer.len() 47 | } 48 | 49 | /// Returns ttue if the buffer is empty 50 | pub fn is_empty(&self) -> bool { 51 | self.buffer.is_empty() 52 | } 53 | 54 | /// Push an element into the buffer 55 | pub fn push(&mut self, element: T) { 56 | let capacity = self.capacity(); 57 | let len = self.len(); 58 | 59 | if len < capacity { 60 | self.buffer.push(element); 61 | } else { 62 | self.buffer[self.write_index % capacity] = element; 63 | } 64 | 65 | self.write_index += 1; 66 | } 67 | 68 | /// Clones and returns a new instance of [`CircularBuffer`] in the write order 69 | pub fn ordered_clone(&self) -> Self { 70 | let capacity = self.capacity(); 71 | let len = self.len(); 72 | let mut cb = Self::new(capacity); 73 | 74 | if len < capacity { 75 | cb.buffer.clone_from(&self.buffer); 76 | } else { 77 | let index_position = self.write_index % capacity; 78 | cb.buffer.extend_from_slice(&self.buffer[index_position..]); 79 | cb.buffer.extend_from_slice(&self.buffer[..index_position]); 80 | } 81 | 82 | cb 83 | } 84 | 85 | pub fn reset(&mut self) { 86 | self.buffer.clear(); 87 | self.write_index = 0; 88 | } 89 | } 90 | 91 | #[cfg(test)] 92 | mod tests { 93 | use super::*; 94 | #[test] 95 | fn circular_buffer() { 96 | let mut cb: CircularBuffer = CircularBuffer::new(3); 97 | assert_eq!(cb.capacity(), 3); 98 | assert_eq!(cb.len(), 0); 99 | assert!(cb.is_empty()); 100 | 101 | cb.push(1); 102 | assert_eq!(cb.buffer, vec![1]); 103 | assert_eq!(cb.len(), 1); 104 | assert!(!cb.is_empty()); 105 | 106 | cb.push(2); 107 | cb.push(3); 108 | assert_eq!(cb.buffer, vec![1, 2, 3]); 109 | assert_eq!(cb.len(), 3); 110 | 111 | cb.push(4); 112 | assert_eq!(cb.buffer, vec![4, 2, 3]); 113 | assert_eq!(cb.len(), 3); 114 | } 115 | 116 | #[test] 117 | fn circular_buffer_clone() { 118 | let mut cb: CircularBuffer = CircularBuffer::new(3); 119 | cb.push(1); 120 | cb.push(2); 121 | cb.push(3); 122 | cb.push(4); 123 | 124 | let cb2 = cb.ordered_clone(); 125 | assert_eq!(cb2.buffer, vec![2, 3, 4]); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/popup.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{ 2 | layout::{Constraint, Direction, Layout, Rect}, 3 | prelude::style::{Color, Style}, 4 | text::Line, 5 | widgets::{BarChart, Block, Borders, Clear, Paragraph}, 6 | Frame, 7 | }; 8 | 9 | pub fn render_popup(frame: &mut Frame<'_>, title: &str, text: &[Line], percent_area: (u16, u16)) { 10 | let size = frame.area(); 11 | let block = Block::default().title(title).borders(Borders::ALL); 12 | let style = Style::default().fg(Color::White).bg(Color::Black); 13 | let paragraph = Paragraph::new(text.to_owned()).block(block).style(style); 14 | let area = centered_rect(percent_area.0, percent_area.1, size); 15 | 16 | frame.render_widget(Clear, area); // this clears out the background 17 | frame.render_widget(paragraph, area); 18 | } 19 | 20 | pub fn render_bar_chart_popup(frame: &mut Frame<'_>, barchart: BarChart, percent_area: (u16, u16)) { 21 | let size = frame.area(); 22 | let area = centered_rect(percent_area.0, percent_area.1, size); 23 | 24 | frame.render_widget(Clear, area); // this clears out the background 25 | frame.render_widget(barchart, area); 26 | } 27 | 28 | /// helper function to create a centered rect using up certain percentage of the available rect `r` 29 | pub fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { 30 | let popup_layout = Layout::default() 31 | .direction(Direction::Vertical) 32 | .constraints( 33 | [ 34 | Constraint::Percentage((100 - percent_y) / 2), 35 | Constraint::Percentage(percent_y), 36 | Constraint::Percentage((100 - percent_y) / 2), 37 | ] 38 | .as_ref(), 39 | ) 40 | .split(r); 41 | 42 | Layout::default() 43 | .direction(Direction::Horizontal) 44 | .constraints( 45 | [ 46 | Constraint::Percentage((100 - percent_x) / 2), 47 | Constraint::Percentage(percent_x), 48 | Constraint::Percentage((100 - percent_x) / 2), 49 | ] 50 | .as_ref(), 51 | ) 52 | .split(popup_layout[1])[1] 53 | } 54 | 55 | #[cfg(test)] 56 | mod tests { 57 | use ratatui::{ 58 | backend::TestBackend, buffer::Buffer, layout::Rect, style::Style, text::Span, Terminal, 59 | }; 60 | 61 | use super::*; 62 | #[test] 63 | fn test_centered_rect() { 64 | let rect = Rect::new(0, 0, 100, 100); 65 | let new_rect = centered_rect(50, 50, rect); 66 | let expected_rect = Rect::new(25, 25, 50, 50); 67 | assert_eq!(new_rect, expected_rect); 68 | } 69 | 70 | #[test] 71 | fn test_render_popup() { 72 | let backend = TestBackend::new(14, 15); 73 | let mut terminal = Terminal::new(backend).unwrap(); 74 | let text = vec![Line::from(Span::styled("text", Style::default()))]; 75 | terminal 76 | .draw(|f| { 77 | render_popup(f, "coso", &text, (50, 50)); 78 | }) 79 | .unwrap(); 80 | let mut expected = Buffer::with_lines(vec![ 81 | " ", 82 | " ", 83 | " ", 84 | " ", 85 | " ┌coso─┐ ", 86 | " │text │ ", 87 | " │ │ ", 88 | " │ │ ", 89 | " │ │ ", 90 | " │ │ ", 91 | " └─────┘ ", 92 | " ", 93 | " ", 94 | " ", 95 | " ", 96 | ]); 97 | for x in 4..=10 { 98 | for y in 4..=10 { 99 | expected[(x, y)].set_fg(Color::White); 100 | expected[(x, y)].set_bg(Color::Black); 101 | } 102 | } 103 | 104 | terminal.backend().assert_buffer(&expected); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/states.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{ 2 | layout::Direction, 3 | style::{Color, Style}, 4 | }; 5 | 6 | #[derive(Debug, Eq, PartialEq)] 7 | pub enum Views { 8 | RawBuffer, 9 | SingleBuffer, 10 | Containers, 11 | Zoom, 12 | Remove, 13 | } 14 | 15 | #[derive(Debug, Eq, PartialEq)] 16 | pub enum ScrollDirection { 17 | UP, 18 | DOWN, 19 | NONE, 20 | } 21 | 22 | #[derive(Debug, Eq, PartialEq)] 23 | pub struct AppState { 24 | pub running: bool, 25 | pub paused: bool, 26 | pub show: Views, 27 | pub wrap: bool, 28 | pub help: bool, 29 | pub barchart: bool, 30 | pub show_input: bool, 31 | pub zoom_id: Option, 32 | pub scroll_direction: ScrollDirection, 33 | pub direction: Direction, 34 | } 35 | 36 | impl Default for AppState { 37 | fn default() -> Self { 38 | Self { 39 | running: false, 40 | paused: false, 41 | wrap: false, 42 | show: Views::Containers, 43 | direction: Direction::Vertical, 44 | help: false, 45 | barchart: false, 46 | show_input: false, 47 | zoom_id: None, 48 | scroll_direction: ScrollDirection::NONE, 49 | } 50 | } 51 | } 52 | 53 | impl AppState { 54 | pub fn hide_show_input(&mut self) { 55 | self.show_input = false; 56 | } 57 | 58 | pub const fn is_running(&self) -> bool { 59 | self.running 60 | } 61 | 62 | pub const fn show_input(&self) -> bool { 63 | self.show_input 64 | } 65 | 66 | pub fn stop(&mut self) { 67 | self.running = false; 68 | } 69 | 70 | pub fn pause(&mut self) { 71 | self.paused = true; 72 | } 73 | 74 | pub fn unpause(&mut self) { 75 | self.paused = false; 76 | } 77 | 78 | pub fn flip_pause(&mut self) { 79 | self.paused = !self.paused; 80 | } 81 | 82 | pub fn flip_wrap(&mut self) { 83 | self.wrap = !self.wrap; 84 | } 85 | 86 | pub fn flip_help(&mut self) { 87 | self.help = !self.help; 88 | } 89 | 90 | pub fn flip_barchart(&mut self) { 91 | self.barchart = !self.barchart; 92 | } 93 | 94 | pub fn flip_show_input(&mut self) { 95 | self.show_input = !self.show_input; 96 | } 97 | 98 | pub fn scroll_up(&mut self) { 99 | self.pause(); 100 | self.scroll_direction = ScrollDirection::UP; 101 | } 102 | 103 | pub fn scroll_down(&mut self) { 104 | self.pause(); 105 | self.scroll_direction = ScrollDirection::DOWN; 106 | } 107 | 108 | pub fn flip_direction(&mut self) { 109 | if self.direction == Direction::Vertical { 110 | self.direction = Direction::Horizontal; 111 | } else { 112 | self.direction = Direction::Vertical; 113 | } 114 | } 115 | } 116 | 117 | #[derive(Debug, Eq, PartialEq)] 118 | pub struct ContainerState { 119 | pub paused: bool, 120 | pub hide: bool, 121 | pub wrap: bool, 122 | pub scroll: u16, 123 | pub count: u64, 124 | pub color: Color, 125 | pub style: Style, 126 | } 127 | 128 | impl Default for ContainerState { 129 | fn default() -> Self { 130 | Self { 131 | paused: false, 132 | hide: false, 133 | wrap: false, 134 | scroll: 0, 135 | count: 0, 136 | color: Color::Red, 137 | style: Style::default().fg(Color::White).bg(Color::Black), 138 | } 139 | } 140 | } 141 | 142 | #[cfg(test)] 143 | mod tests { 144 | use super::*; 145 | 146 | #[test] 147 | fn init_container_state() { 148 | let cs = ContainerState::default(); 149 | assert!(!cs.paused); 150 | assert!(!cs.wrap); 151 | assert_eq!(cs.scroll, 0); 152 | assert_eq!(cs.color, Color::Red); 153 | assert_eq!(cs.style, Style::default().fg(Color::White).bg(Color::Black)); 154 | } 155 | 156 | #[test] 157 | fn test_app_state() { 158 | let appstate = AppState::default(); 159 | assert!(!appstate.wrap); 160 | assert!(!appstate.paused); 161 | assert!(!appstate.running); 162 | assert_eq!(appstate.show, Views::Containers); 163 | assert_eq!(appstate.direction, Direction::Vertical); 164 | assert!(!appstate.help); 165 | assert!(!appstate.show_input); 166 | assert_eq!(appstate.zoom_id, None); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /.github/workflows/test.yml.bkp: -------------------------------------------------------------------------------- 1 | permissions: 2 | contents: read 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | name: test 8 | jobs: 9 | required: 10 | runs-on: ubuntu-latest 11 | name: ubuntu / ${{ matrix.toolchain }} 12 | strategy: 13 | matrix: 14 | toolchain: [stable, beta] 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | submodules: true 19 | - name: Install ${{ matrix.toolchain }} 20 | uses: dtolnay/rust-toolchain@master 21 | with: 22 | toolchain: ${{ matrix.toolchain }} 23 | - name: cargo generate-lockfile 24 | if: hashFiles('Cargo.lock') == '' 25 | run: cargo generate-lockfile 26 | - name: Restore cached target/ 27 | id: target-cache-restore 28 | uses: actions/cache/restore@v3 29 | with: 30 | path: | 31 | target 32 | /home/runner/.cargo 33 | key: ${{ matrix.toolchain }}-target 34 | # https://twitter.com/jonhoo/status/1571290371124260865 35 | - name: cargo test --locked 36 | run: cargo test --locked --all-features --all-targets 37 | - name: Save cached target/ 38 | id: target-cache-save 39 | uses: actions/cache/save@v3 40 | with: 41 | path: | 42 | target 43 | /home/runner/.cargo 44 | key: ${{ steps.target-cache-restore.outputs.cache-primary-key }} 45 | # minimal: 46 | # runs-on: ubuntu-latest 47 | # name: ubuntu / stable / minimal-versions 48 | # steps: 49 | # - uses: actions/checkout@v4 50 | # with: 51 | # submodules: true 52 | # - name: Install stable 53 | # uses: dtolnay/rust-toolchain@stable 54 | # - name: Install nightly for -Zminimal-versions 55 | # uses: dtolnay/rust-toolchain@nightly 56 | # - name: rustup default stable 57 | # run: rustup default stable 58 | # - name: cargo update -Zminimal-versions 59 | # run: cargo +nightly update -Zminimal-versions 60 | # - name: cargo test 61 | # run: cargo test --locked --all-features --all-targets 62 | os-check: 63 | runs-on: ${{ matrix.os }} 64 | name: ${{ matrix.os }} / stable 65 | strategy: 66 | fail-fast: false 67 | matrix: 68 | os: [macos-latest, windows-latest] 69 | steps: 70 | - run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append 71 | if: runner.os == 'Windows' 72 | - run: vcpkg install openssl:x64-windows-static-md 73 | if: runner.os == 'Windows' 74 | - uses: actions/checkout@v4 75 | with: 76 | submodules: true 77 | - name: Install stable 78 | uses: dtolnay/rust-toolchain@stable 79 | - name: cargo generate-lockfile 80 | if: hashFiles('Cargo.lock') == '' 81 | run: cargo generate-lockfile 82 | - name: cargo test 83 | run: cargo test --locked --all-features --all-targets 84 | coverage: 85 | runs-on: ubuntu-latest 86 | name: ubuntu / stable / coverage 87 | steps: 88 | - uses: actions/checkout@v4 89 | with: 90 | submodules: true 91 | - name: Install stable 92 | uses: dtolnay/rust-toolchain@stable 93 | with: 94 | components: llvm-tools-preview 95 | - name: cargo install cargo-llvm-cov 96 | uses: taiki-e/install-action@cargo-llvm-cov 97 | - name: cargo generate-lockfile 98 | if: hashFiles('Cargo.lock') == '' 99 | run: cargo generate-lockfile 100 | - name: Restore cached target/ 101 | id: target-cache-restore 102 | uses: actions/cache/restore@v3 103 | with: 104 | path: | 105 | target 106 | /home/runner/.cargo 107 | key: coverage-target 108 | - name: cargo llvm-cov clean 109 | run: cargo llvm-cov clean --workspace 110 | - name: cargo llvm-cov 111 | run: cargo llvm-cov --locked --all-features --no-report --release 112 | - name: Save cached target/ 113 | id: target-cache-save 114 | uses: actions/cache/save@v3 115 | with: 116 | path: | 117 | target 118 | /home/runner/.cargo 119 | key: ${{ steps.target-cache-restore.outputs.cache-primary-key }} 120 | - name: cargo llvm-cov report 121 | run: cargo llvm-cov report --release --lcov --output-path lcov.info 122 | - name: Upload to codecov.io 123 | uses: codecov/codecov-action@v3 124 | with: 125 | fail_ci_if_error: true 126 | token: ${{ secrets.CODECOV_TOKEN }} 127 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | logss 4 |
5 |
6 |

7 | 8 |
logs splitter
9 |

A simple command line tool that helps you visualize an input stream of text.

10 | 11 | ![screenshot](./assets/gifs/complete.gif) 12 | 13 |

14 | 15 | 16 | 17 | 18 | 19 |

20 | 21 |

22 | Key Features • 23 | Usage • 24 | Installation • 25 | Download • 26 | Roadmap • 27 | License 28 |

29 | 30 | ## Key Features 31 | 32 | * Select render/stream speed 33 | * Automatic color assigned to each string match 34 | * Vertical and Horizontal view 35 | * Pause and continue stream 36 | * Scroll Up/Down 37 | * Delete containers on runtime 38 | * Add new containers on runtime 39 | * Dedicated container for raw stream 40 | * Toggle line wrapping 41 | * Zoom into a specific container 42 | * Containers Show/Hide 43 | * Support for regexp 44 | * Support for configuration file 45 | * Support for explicit command (no need to pipe into it) 46 | * Send all matched lines to dedicated files 47 | * Consolidated view with highlighted items 48 | * Simple BarChart popup with counts 49 | * Support to trigger shell commands (thru 'bin/sh') fir each match 50 | * The line matched can be replaced in the command to execute (__line__) 51 | * Timeout for each trigger 52 | * Configurable number of threads for each container 53 | 54 | 55 | ## Usage 56 | 57 | ```sh 58 | $ logss -h 59 | Simple CLI command to display logs in a user-friendly way 60 | 61 | Usage: logss [OPTIONS] 62 | 63 | Options: 64 | -c Specify substrings (regex patterns) 65 | -e Exit on empty input [default: false] 66 | -s Start in single view mode [default: false] 67 | -C Get input from a command 68 | -f Input configuration file (overrides CLI arguments) 69 | -o Specify the output path for matched patterns 70 | -r Define render speed in milliseconds [default: 100] 71 | -t Number of threads per container for triggers [default: 1] 72 | -V Start in vertical view mode 73 | -h Print help 74 | 75 | $ cat shakespeare.txt | logss -c to -c be -c or,'echo or_found >> /tmp/or.log',1 -c 'in.*of' 76 | $ # The containers can be a simple '-c ' or '-c , , ' 77 | $ cat real_curl_example.yaml 78 | command: 79 | - curl 80 | - -s 81 | - https://raw.githubusercontent.com/linuxacademy/content-elastic-log-samples/master/access.log 82 | render: 75 83 | containers: 84 | - re: GET 85 | trigger: echo $(date) >> /tmp/get.log 86 | timeout: 4 87 | - re: "404" 88 | trigger: echo __line__ >> /tmp/404.log 89 | timeout: 4 90 | - ".*ERROR|error.*" 91 | $ logss -f real_curl_example.yaml 92 | ``` 93 | 94 | ## Installation 95 | 96 | So far only available in crates.io. 97 | 98 | ```shell 99 | cargo install logss 100 | ``` 101 | 102 | If cargo is not a possibility then download pre compiled binaries from the [download](#download) section. 103 | 104 | ### Arch Linux (AUR) 105 | 106 | You can install `logss` from the [AUR](https://aur.archlinux.org/packages/logss) with using an [AUR helper](https://wiki.archlinux.org/title/AUR_helpers). 107 | 108 | ```shell 109 | paru -S logss 110 | ``` 111 | 112 | ### eget 113 | You can install the pre build binaries using [eget](https://github.com/zyedidia/eget) 114 | 115 | ```shell 116 | eget todoesverso/logss 117 | ``` 118 | 119 | ## Download 120 | 121 | Pre compiled binaries for several platforms can be downloaded from the [release](https://github.com/todoesverso/logss/releases) section. 122 | 123 | ## Roadmap 124 | 125 | This is just a personal project intended to learn Rust, so things move slowly. 126 | 127 | This is a list of things I plan to do: 128 | 129 | * Add documentation (the rust way) 130 | * Refactoring (as I learn more Rust things) 131 | * Tests 132 | * Smart timestamp highlights 133 | * ... whatever I can think of when I am using it 134 | 135 | ## License 136 | 137 | MIT 138 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | # Allows you to run this workflow manually from the Actions tab 5 | workflow_dispatch: 6 | push: 7 | branches: 8 | - main 9 | pull_request: 10 | branches: 11 | - main 12 | merge_group: 13 | 14 | # ensure that the workflow is only triggered once per PR, subsequent pushes to the PR will cancel 15 | # and restart the workflow. See https://docs.github.com/en/actions/using-jobs/using-concurrency 16 | concurrency: 17 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 18 | cancel-in-progress: true 19 | 20 | env: 21 | # don't install husky hooks during CI as they are only needed for for pre-push 22 | CARGO_HUSKY_DONT_INSTALL_HOOKS: true 23 | 24 | # lint, clippy and coveraget jobs are intentionally early in the workflow to catch simple 25 | # formatting, typos, and missing tests as early as possible. This allows us to fix these and 26 | # resubmit the PR without having to wait for the comprehensive matrix of tests to complete. 27 | jobs: 28 | lint: 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout 32 | if: github.event_name != 'pull_request' 33 | uses: actions/checkout@v4 34 | - name: Checkout 35 | if: github.event_name == 'pull_request' 36 | uses: actions/checkout@v4 37 | with: 38 | ref: ${{ github.event.pull_request.head.sha }} 39 | - name: Install Rust nightly 40 | uses: dtolnay/rust-toolchain@nightly 41 | with: 42 | components: rustfmt 43 | - name: Install cargo-make 44 | uses: taiki-e/install-action@cargo-make 45 | - name: Check formatting 46 | run: cargo make lint-format 47 | - name: Check documentation 48 | run: cargo make lint-docs 49 | - name: Check conventional commits 50 | uses: crate-ci/committed@master 51 | with: 52 | args: "-vv" 53 | commits: HEAD 54 | - name: Check typos 55 | uses: crate-ci/typos@master 56 | - name: Lint dependencies 57 | uses: EmbarkStudios/cargo-deny-action@v1 58 | 59 | clippy: 60 | runs-on: ubuntu-latest 61 | steps: 62 | - name: Checkout 63 | uses: actions/checkout@v4 64 | - name: Install Rust stable 65 | uses: dtolnay/rust-toolchain@stable 66 | with: 67 | components: clippy 68 | - name: Install cargo-make 69 | uses: taiki-e/install-action@cargo-make 70 | - name: Run cargo make clippy-all 71 | run: cargo make clippy 72 | 73 | coverage: 74 | runs-on: ubuntu-latest 75 | steps: 76 | - name: Checkout 77 | uses: actions/checkout@v4 78 | - name: Install Rust stable 79 | uses: dtolnay/rust-toolchain@stable 80 | with: 81 | components: llvm-tools 82 | - name: Install cargo-llvm-cov and cargo-make 83 | uses: taiki-e/install-action@v2 84 | with: 85 | tool: cargo-llvm-cov,cargo-make 86 | - name: Generate coverage 87 | run: cargo make coverage 88 | - name: Upload to codecov.io 89 | uses: codecov/codecov-action@v3 90 | with: 91 | token: ${{ secrets.CODECOV_TOKEN }} 92 | fail_ci_if_error: true 93 | 94 | check: 95 | strategy: 96 | fail-fast: false 97 | matrix: 98 | os: [ubuntu-latest, windows-latest, macos-latest] 99 | toolchain: ["1.75.0", "stable"] 100 | runs-on: ${{ matrix.os }} 101 | steps: 102 | - name: Checkout 103 | uses: actions/checkout@v4 104 | - name: Install Rust {{ matrix.toolchain }} 105 | uses: dtolnay/rust-toolchain@master 106 | with: 107 | toolchain: ${{ matrix.toolchain }} 108 | - name: Install cargo-make 109 | uses: taiki-e/install-action@cargo-make 110 | - name: Run cargo make check 111 | run: cargo make check 112 | env: 113 | RUST_BACKTRACE: full 114 | 115 | test-doc: 116 | strategy: 117 | fail-fast: false 118 | matrix: 119 | os: [ubuntu-latest, windows-latest, macos-latest] 120 | runs-on: ${{ matrix.os }} 121 | steps: 122 | - name: Checkout 123 | uses: actions/checkout@v4 124 | - name: Install Rust stable 125 | uses: dtolnay/rust-toolchain@stable 126 | - name: Install cargo-make 127 | uses: taiki-e/install-action@cargo-make 128 | - name: Test docs 129 | run: cargo make test-doc 130 | env: 131 | RUST_BACKTRACE: full 132 | 133 | test: 134 | strategy: 135 | fail-fast: false 136 | matrix: 137 | os: [ubuntu-latest, windows-latest, macos-latest] 138 | toolchain: ["1.75.0", "stable"] 139 | runs-on: ${{ matrix.os }} 140 | steps: 141 | - name: Checkout 142 | uses: actions/checkout@v4 143 | - name: Install Rust ${{ matrix.toolchain }}} 144 | uses: dtolnay/rust-toolchain@master 145 | with: 146 | toolchain: ${{ matrix.toolchain }} 147 | - name: Install cargo-make 148 | uses: taiki-e/install-action@cargo-make 149 | - name: Test 150 | run: cargo make test 151 | env: 152 | RUST_BACKTRACE: full 153 | -------------------------------------------------------------------------------- /src/input.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{ 2 | layout::Position, 3 | style::Style, 4 | text::{Line, Span}, 5 | Frame, 6 | }; 7 | use regex::Regex; 8 | use unicode_width::UnicodeWidthStr; 9 | 10 | use crate::popup::{centered_rect, render_popup}; 11 | 12 | #[derive(Debug, Default)] 13 | pub struct Input { 14 | /// Current value of the input box 15 | pub input: String, 16 | } 17 | 18 | impl Input { 19 | pub fn new() -> Self { 20 | Self::default() 21 | } 22 | 23 | pub fn render(&self, frame: &mut Frame) { 24 | let pos = (40, 8); 25 | let area = centered_rect(pos.0, pos.1, frame.area()); 26 | let text = vec![Line::from(Span::styled( 27 | self.input.clone(), 28 | Style::default(), 29 | ))]; 30 | 31 | let title = if self.is_valid() { 32 | "Input" 33 | } else { 34 | "Input (non valid regexp)" 35 | }; 36 | let position = Position::new(area.x + self.input.width() as u16 + 1, area.y + 1); 37 | frame.set_cursor_position(position); 38 | render_popup(frame, title, &text, (pos.0, pos.1)); 39 | } 40 | 41 | pub fn reset(&mut self) { 42 | self.input = String::new(); 43 | } 44 | 45 | pub fn push(&mut self, ch: char) { 46 | self.input.push(ch); 47 | } 48 | 49 | pub fn pop(&mut self) { 50 | self.input.pop(); 51 | } 52 | 53 | pub fn inner_clone(&self) -> String { 54 | self.input.clone() 55 | } 56 | 57 | pub fn is_valid(&self) -> bool { 58 | Regex::new(&self.input).is_ok() 59 | } 60 | } 61 | 62 | #[cfg(test)] 63 | mod tests { 64 | use ratatui::{backend::TestBackend, buffer::Buffer, prelude::Color, Terminal}; 65 | 66 | use super::*; 67 | 68 | #[test] 69 | fn simple_full_test() { 70 | let mut input = Input::new(); 71 | assert_eq!(input.input, String::new()); 72 | 73 | input.push('a'); 74 | assert_eq!(input.input, "a"); 75 | input.push('b'); 76 | assert_eq!(input.input, "ab"); 77 | input.pop(); 78 | assert_eq!(input.input, "a"); 79 | input.reset(); 80 | assert_eq!(input.input, String::new()); 81 | } 82 | 83 | #[test] 84 | fn test_render_input() { 85 | let mut input = Input::new(); 86 | input.push('a'); 87 | input.push('b'); 88 | let backend = TestBackend::new(20, 37); 89 | let mut terminal = Terminal::new(backend).unwrap(); 90 | terminal.draw(|f| input.render(f)).unwrap(); 91 | let mut expected = Buffer::with_lines(vec![ 92 | " ", 93 | " ", 94 | " ", 95 | " ", 96 | " ", 97 | " ", 98 | " ", 99 | " ", 100 | " ", 101 | " ", 102 | " ", 103 | " ", 104 | " ", 105 | " ", 106 | " ", 107 | " ", 108 | " ", 109 | " ┌Input─┐ ", 110 | " │ab │ ", 111 | " └──────┘ ", 112 | " ", 113 | " ", 114 | " ", 115 | " ", 116 | " ", 117 | " ", 118 | " ", 119 | " ", 120 | " ", 121 | " ", 122 | " ", 123 | " ", 124 | " ", 125 | " ", 126 | " ", 127 | " ", 128 | " ", 129 | ]); 130 | 131 | for x in 6..=13 { 132 | for y in 17..=19 { 133 | expected[(x, y)].set_fg(Color::White); 134 | expected[(x, y)].set_bg(Color::Black); 135 | } 136 | } 137 | 138 | terminal.backend().assert_buffer(&expected); 139 | } 140 | 141 | #[test] 142 | fn test_render_non_valid_input() { 143 | let mut input = Input::new(); 144 | input.push('['); 145 | let backend = TestBackend::new(65, 37); 146 | let mut terminal = Terminal::new(backend).unwrap(); 147 | terminal.draw(|f| input.render(f)).unwrap(); 148 | let mut expected = Buffer::with_lines(vec![ 149 | " ", 150 | " ", 151 | " ", 152 | " ", 153 | " ", 154 | " ", 155 | " ", 156 | " ", 157 | " ", 158 | " ", 159 | " ", 160 | " ", 161 | " ", 162 | " ", 163 | " ", 164 | " ", 165 | " ", 166 | " ┌Input (non valid regexp)┐ ", 167 | " │[ │ ", 168 | " └────────────────────────┘ ", 169 | " ", 170 | " ", 171 | " ", 172 | " ", 173 | " ", 174 | " ", 175 | " ", 176 | " ", 177 | " ", 178 | " ", 179 | " ", 180 | " ", 181 | " ", 182 | " ", 183 | " ", 184 | " ", 185 | " ", 186 | ]); 187 | for x in 20..=45 { 188 | for y in 17..=19 { 189 | expected[(x, y)].set_fg(Color::White); 190 | expected[(x, y)].set_bg(Color::Black); 191 | } 192 | } 193 | 194 | terminal.backend().assert_buffer(&expected); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/args.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::{remove_file, OpenOptions}, 3 | str::FromStr, 4 | }; 5 | 6 | use pico_args; 7 | use regex::Regex; 8 | use serde::{Deserialize, Serialize}; 9 | use serde_yaml; 10 | 11 | const HELP: &str = "\ 12 | Simple CLI command to display logs in a user-friendly way 13 | 14 | Usage: logss [OPTIONS] 15 | 16 | Options: 17 | -c Specify substrings (regex patterns) 18 | -e Exit on empty input [default: false] 19 | -s Start in single view mode [default: false] 20 | -C Get input from a command 21 | -f Input configuration file (overrides CLI arguments) 22 | -o Specify the output path for matched patterns 23 | -r Define render speed in milliseconds [default: 100] 24 | -t Number of threads per container for triggers [default: 1] 25 | -V Start in vertical view mode 26 | -h Print help 27 | "; 28 | 29 | #[derive(Debug, Serialize, Deserialize, PartialEq)] 30 | pub struct LocalContainer { 31 | pub re: String, 32 | pub trigger: Option, 33 | pub timeout: Option, 34 | } 35 | 36 | impl FromStr for LocalContainer { 37 | type Err = &'static str; 38 | 39 | fn from_str(s: &str) -> Result { 40 | let parts: Vec<&str> = s.trim().split(',').collect(); 41 | 42 | if parts.len() > 3 { 43 | return Err("Expected not more than 2 comma-separated parts"); 44 | } 45 | 46 | let re = parts[0].trim().to_string(); 47 | let trigger = if parts.len() < 2 || parts[1].trim().is_empty() { 48 | None 49 | } else { 50 | Some(parts[1].trim().to_string()) 51 | }; 52 | let timeout = if parts.len() < 3 || parts[2].trim().is_empty() { 53 | Some(1) 54 | } else { 55 | let timeout: u64 = parts[2].trim().parse().unwrap_or(1); 56 | Some(timeout) 57 | }; 58 | 59 | Ok(LocalContainer { 60 | re, 61 | trigger, 62 | timeout, 63 | }) 64 | } 65 | } 66 | 67 | #[derive(Debug, Serialize, Deserialize)] 68 | pub struct Args { 69 | pub containers: Vec, 70 | pub exit: Option, 71 | pub vertical: Option, 72 | pub single: Option, 73 | pub render: Option, 74 | pub threads: Option, 75 | pub command: Option>, 76 | pub output: Option, 77 | pub config_file: Option, 78 | } 79 | 80 | pub fn parse_args() -> Args { 81 | match parser() { 82 | Ok(v) => v, 83 | Err(e) => { 84 | eprintln!("Error: {}.", e); 85 | std::process::exit(1); 86 | } 87 | } 88 | } 89 | 90 | fn parser() -> Result> { 91 | let mut pargs = pico_args::Arguments::from_env(); 92 | 93 | // Help has a higher priority and should be handled separately. 94 | if pargs.contains(["-h", "--help"]) { 95 | print!("{}", HELP); 96 | std::process::exit(0); 97 | } 98 | 99 | let mut args = Args { 100 | containers: pargs.values_from_str("-c")?, 101 | command: pargs.opt_value_from_fn("-C", parse_cmd)?, 102 | config_file: pargs.opt_value_from_os_str("-f", parse_path)?, 103 | output: pargs.opt_value_from_os_str("-o", validate_path)?, 104 | exit: pargs.contains("-e").then_some(true), 105 | single: pargs.contains("-s").then_some(true), 106 | vertical: pargs.contains("-V").then_some(true), 107 | render: pargs 108 | .opt_value_from_fn("-r", render_in_range)? 109 | .unwrap_or(Some(100)), 110 | threads: pargs 111 | .opt_value_from_fn("-t", render_in_range)? 112 | .unwrap_or(Some(4)), 113 | }; 114 | 115 | let render = args.render; 116 | 117 | if !validate_regex(&args.containers) { 118 | std::process::exit(1); 119 | } 120 | 121 | // It's up to the caller what to do with the remaining arguments. 122 | let remaining = pargs.finish(); 123 | if !remaining.is_empty() { 124 | eprintln!("Error: non valid arguments: {:?}.", remaining); 125 | } 126 | 127 | if let Some(config_file) = args.config_file { 128 | args = parse_yaml(config_file)?; 129 | } 130 | 131 | if args.render.is_none() { 132 | args.render = render; 133 | } 134 | 135 | Ok(args) 136 | } 137 | 138 | fn parse_yaml(config_file: std::path::PathBuf) -> Result> { 139 | let f = std::fs::File::open(config_file)?; 140 | let scrape_config: Args = serde_yaml::from_reader(f)?; 141 | Ok(scrape_config) 142 | } 143 | 144 | fn validate_regex(containers: &Vec) -> bool { 145 | for c in containers { 146 | if Regex::new(&c.re).is_err() { 147 | eprintln!("Error: Failed to parse regexp '{c:?}'."); 148 | return false; 149 | } 150 | } 151 | true 152 | } 153 | 154 | fn parse_path(s: &std::ffi::OsStr) -> Result { 155 | Ok(s.into()) 156 | } 157 | 158 | fn validate_path(s: &std::ffi::OsStr) -> Result { 159 | let path: std::path::PathBuf = s.into(); 160 | if !path.is_dir() { 161 | return Err(format!("{} is not a valid path", path.display())); 162 | } 163 | /* TODO: re write once you learn some real rust 164 | * Not proud of this but is the simplest way I found to test 165 | * write permissions in a directory 166 | */ 167 | let test_file_name = format!("{}/.logss", path.to_string_lossy()); 168 | 169 | let a = OpenOptions::new() 170 | .append(true) 171 | .create(true) 172 | .open(&test_file_name); 173 | 174 | match a { 175 | Ok(_) => { 176 | remove_file(test_file_name).expect("Failed to delete sentinel file"); 177 | Ok(path) 178 | } 179 | Err(error) => Err(error.to_string()), 180 | } 181 | } 182 | 183 | fn parse_cmd(s: &str) -> Result, Box> { 184 | Ok(s.split_whitespace().map(str::to_string).collect()) 185 | } 186 | 187 | fn render_in_range(s: &str) -> Result, String> { 188 | let render: u64 = s 189 | .parse() 190 | .map_err(|_| format!("`{s}` isn't a valid number"))?; 191 | 192 | Ok(Some(render)) 193 | } 194 | 195 | #[cfg(test)] 196 | mod tests { 197 | #[cfg(not(target_os = "windows"))] 198 | use std::os::unix::fs::DirBuilderExt; 199 | use std::{ 200 | ffi::OsStr, 201 | fs::{remove_dir_all, DirBuilder}, 202 | path::PathBuf, 203 | }; 204 | 205 | use super::*; 206 | #[test] 207 | fn test_render_in_range() { 208 | assert_eq!(render_in_range("30"), Ok(Some(30))); 209 | } 210 | 211 | #[test] 212 | fn test_validate_regex() { 213 | let c = vec![LocalContainer { 214 | re: "a".to_string(), 215 | trigger: None, 216 | timeout: None, 217 | }]; 218 | assert!(validate_regex(&c)); 219 | 220 | let c = vec![LocalContainer { 221 | re: "*".to_string(), 222 | trigger: None, 223 | timeout: None, 224 | }]; 225 | assert!(!validate_regex(&c)); 226 | } 227 | 228 | #[test] 229 | fn test_validate_path_non_valid() { 230 | let resp = Err("non_valid_path is not a valid path".to_string()); 231 | assert_eq!(validate_path(OsStr::new("non_valid_path")), resp); 232 | } 233 | 234 | #[test] 235 | #[cfg(not(target_os = "windows"))] 236 | fn test_validate_path_no_perm() { 237 | let _ = remove_dir_all("test-sarasa"); 238 | let mut dir = DirBuilder::new(); 239 | dir.mode(0o444); 240 | dir.recursive(true).create("test-sarasa").unwrap(); 241 | let resp = Err("Permission denied (os error 13)".to_string()); 242 | assert_eq!(validate_path(OsStr::new("test-sarasa")), resp); 243 | let _ = remove_dir_all("test-sarasa"); 244 | } 245 | 246 | #[test] 247 | fn test_validate_path() { 248 | let _ = remove_dir_all("test-sarasa"); 249 | let path = PathBuf::from("test-sarasa"); 250 | let mut dir = DirBuilder::new(); 251 | dir.recursive(true).create("test-sarasa").unwrap(); 252 | assert_eq!(validate_path(OsStr::new("test-sarasa")), Ok(path)); 253 | let _ = remove_dir_all("test-sarasa"); 254 | } 255 | 256 | #[test] 257 | fn test_parse_yaml() { 258 | let path = std::path::PathBuf::from("example_config.yml"); 259 | let args = parse_yaml(path).unwrap(); 260 | assert_eq!(args.render.unwrap(), 26); 261 | assert_eq!( 262 | args.containers, 263 | vec![ 264 | LocalContainer { 265 | re: "to".to_string(), 266 | trigger: Some("echo $(date) >> /tmp/dates.txt".to_string()), 267 | timeout: Some(1), 268 | }, 269 | LocalContainer { 270 | re: "be".to_string(), 271 | trigger: Some("echo '__line__' >> /tmp/match_lines.txt".to_string()), 272 | timeout: Some(1), 273 | }, 274 | LocalContainer { 275 | re: "or".to_string(), 276 | trigger: None, 277 | timeout: Some(1), 278 | }, 279 | LocalContainer { 280 | re: "not".to_string(), 281 | trigger: None, 282 | timeout: Some(1), 283 | }, 284 | LocalContainer { 285 | re: "to.*be".to_string(), 286 | trigger: None, 287 | timeout: Some(1), 288 | }, 289 | ] 290 | ); 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /src/container.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::{File, OpenOptions}, 3 | io::Write, 4 | path::{Path, PathBuf}, 5 | process::Command, 6 | time::Duration, 7 | }; 8 | 9 | use anyhow::Result; 10 | use ratatui::{ 11 | layout::Rect, 12 | style::{Color, Modifier, Style}, 13 | text::{Line, Span}, 14 | widgets::{Block, Borders, Paragraph, Wrap}, 15 | Frame, 16 | }; 17 | use regex::Regex; 18 | use slug; 19 | use threadpool::ThreadPool; 20 | use wait_timeout::ChildExt; 21 | 22 | use crate::{ 23 | cb::CircularBuffer, 24 | states::{ContainerState, ScrollDirection}, 25 | }; 26 | 27 | pub const CONTAINER_BUFFER: usize = 1024; 28 | pub const CONTAINERS_MAX: u8 = 10; 29 | pub const CONTAINER_COLORS: [ratatui::style::Color; 10] = [ 30 | Color::Red, 31 | Color::Blue, 32 | Color::Cyan, 33 | Color::Green, 34 | Color::Yellow, 35 | Color::LightYellow, 36 | Color::Magenta, 37 | Color::LightMagenta, 38 | Color::Gray, 39 | Color::DarkGray, 40 | ]; 41 | 42 | #[derive(Debug)] 43 | pub struct Container<'a> { 44 | /// matching text 45 | pub text: String, 46 | pub re: Regex, 47 | /// circular buffer with matching lines 48 | pub cb: CircularBuffer>, 49 | pub id: u8, 50 | pub state: ContainerState, 51 | pub file: Option, 52 | pub trigger: Option, 53 | pub timeout: u64, 54 | pub thread_pool: Option, 55 | } 56 | 57 | impl<'a> Container<'a> { 58 | pub fn new( 59 | text: String, 60 | trigger: Option, 61 | timeout: u64, 62 | threads: u64, 63 | buffersize: usize, 64 | ) -> Self { 65 | let re = Regex::new(&text).unwrap(); 66 | let thread_pool = if threads > 0 { 67 | Some(ThreadPool::new(threads as usize)) 68 | } else { 69 | None 70 | }; 71 | Self { 72 | text: text.clone(), 73 | re, 74 | cb: CircularBuffer::new(buffersize), 75 | id: 0, 76 | state: ContainerState::default(), 77 | file: None, 78 | trigger, 79 | timeout, 80 | thread_pool, 81 | } 82 | } 83 | 84 | pub fn new_clean(text: &str) -> Self { 85 | let re = Regex::new(text).unwrap(); 86 | Self { 87 | text: text.to_string(), 88 | re, 89 | cb: CircularBuffer::new(CONTAINER_BUFFER), 90 | id: 0, 91 | state: ContainerState::default(), 92 | file: None, 93 | trigger: None, 94 | timeout: 1, 95 | thread_pool: None, 96 | } 97 | } 98 | 99 | pub fn set_output_path(&mut self, output_path: PathBuf) -> Result<()> { 100 | let file_name = format!( 101 | "{}/{}.txt", 102 | output_path.to_string_lossy(), 103 | slug::slugify(self.text.clone()) 104 | ); 105 | let file_path = Path::new(&file_name); 106 | self.file = Some( 107 | OpenOptions::new() 108 | .append(true) 109 | .create(true) 110 | .open(file_path)?, 111 | ); 112 | 113 | Ok(()) 114 | } 115 | 116 | fn process_line(&self, line: &str) -> Option> { 117 | // TODO: maybe add smart time coloration? 118 | if let Some(mat) = self.re.find(line) { 119 | let start = mat.start(); 120 | let end = mat.end(); 121 | 122 | return Some(Line::from(vec![ 123 | Span::from(line[0..start].to_string()), 124 | Span::styled( 125 | line[start..end].to_string(), 126 | Style::default().fg(self.state.color), 127 | ), 128 | Span::from(line[end..].to_string()), 129 | ])); 130 | } 131 | None 132 | } 133 | 134 | pub fn push(&mut self, element: Line<'a>) { 135 | self.state.count += 1; 136 | let _ = &self.cb.push(element); 137 | } 138 | 139 | pub fn proc_and_push_line(&mut self, line: &str) -> Option> { 140 | let processed_line = self.process_line(line); 141 | if let Some(processed_line_clone) = processed_line.clone() { 142 | self.push(processed_line_clone); 143 | } 144 | if let Some(file) = &mut self.file { 145 | file.write_all(line.as_bytes()) 146 | .expect("Failed to write file"); 147 | file.flush().expect("Failed to flush"); 148 | } 149 | if self.trigger.is_some() && self.thread_pool.is_some() { 150 | let cmd = self.trigger.clone().unwrap().replace("__line__", line); 151 | let mut child = Command::new("sh").arg("-c").arg(cmd).spawn().unwrap(); 152 | let timeout = Duration::from_secs(self.timeout); 153 | self.thread_pool.as_ref().unwrap().execute(move || { 154 | let _status_code = match child.wait_timeout(timeout).unwrap() { 155 | Some(status) => status.code(), 156 | None => { 157 | // child hasn't exited yet 158 | child.kill().unwrap(); 159 | child.wait().unwrap().code() 160 | } 161 | }; 162 | }); 163 | } 164 | 165 | processed_line 166 | } 167 | 168 | pub fn update_scroll(&mut self, visible_lines: usize, scroll: &ScrollDirection) { 169 | let total_lines = self.cb.len(); 170 | 171 | // If we have less lines in the buffer than visible lines then do nothing 172 | if total_lines < visible_lines { 173 | return; 174 | } 175 | 176 | let max_scroll = (total_lines - visible_lines) as u16; 177 | 178 | if !self.state.paused { 179 | // This ensures automatic scrolling 180 | self.state.scroll = max_scroll; 181 | } else { 182 | match scroll { 183 | ScrollDirection::NONE => (), 184 | ScrollDirection::UP => { 185 | if self.state.scroll <= max_scroll { 186 | self.state.scroll += 1; 187 | } 188 | } 189 | ScrollDirection::DOWN => { 190 | if self.state.scroll - 1 > 0 { 191 | self.state.scroll -= 1; 192 | } 193 | } 194 | } 195 | } 196 | } 197 | 198 | pub fn render(&self, frame: &mut Frame, area: Rect) { 199 | if self.state.hide { 200 | return; 201 | } 202 | let title = format!("[{}]'{}' ({})", self.id, self.text, self.state.count); 203 | let block = create_block(&title, self.state.color, self.state.paused); 204 | let mut paragraph = Paragraph::new(self.cb.ordered_clone().buffer.clone()) 205 | .block(block) 206 | .style(self.state.style) 207 | .scroll((self.state.scroll, 0)); 208 | if self.state.wrap { 209 | paragraph = paragraph.wrap(Wrap { trim: false }); 210 | } 211 | 212 | frame.render_widget(paragraph, area); 213 | } 214 | 215 | pub fn get_count(&self) -> u64 { 216 | self.state.count 217 | } 218 | 219 | pub fn reset(&mut self) { 220 | self.cb.reset(); 221 | } 222 | } 223 | 224 | fn create_block(title: &str, color: Color, paused: bool) -> Block<'_> { 225 | let modifier = if paused { 226 | Modifier::BOLD | Modifier::SLOW_BLINK | Modifier::UNDERLINED 227 | } else { 228 | Modifier::BOLD 229 | }; 230 | Block::default().borders(Borders::ALL).title(Span::styled( 231 | title, 232 | Style::default().add_modifier(modifier).fg(color), 233 | )) 234 | } 235 | 236 | #[cfg(test)] 237 | mod tests { 238 | use super::*; 239 | 240 | #[test] 241 | fn test_create_block() { 242 | let block = create_block("sarasa", Color::Red, false); 243 | let expected = Block::default().borders(Borders::ALL).title(Span::styled( 244 | "sarasa", 245 | Style::default().add_modifier(Modifier::BOLD).fg(Color::Red), 246 | )); 247 | assert_eq!(block, expected); 248 | 249 | let block = create_block("coso", Color::Blue, true); 250 | let expected = Block::default().borders(Borders::ALL).title(Span::styled( 251 | "coso", 252 | Style::default() 253 | .add_modifier(Modifier::BOLD | Modifier::SLOW_BLINK | Modifier::UNDERLINED) 254 | .fg(Color::Blue), 255 | )); 256 | assert_eq!(block, expected); 257 | } 258 | 259 | #[test] 260 | fn test_container_new() { 261 | let container = Container::new("key".to_string(), None, 1, 0, 2); 262 | assert_eq!(container.id, 0); 263 | assert_eq!(container.text, "key"); 264 | assert_eq!(container.cb.len(), 0); 265 | assert_eq!(container.cb.capacity(), 2); 266 | assert_eq!(container.state, ContainerState::default()); 267 | } 268 | 269 | #[test] 270 | fn test_set_output_path() { 271 | let _ = std::fs::remove_dir_all("test-sarasa"); 272 | let mut container = Container::new("key".to_string(), None, 1, 0, 2); 273 | let path = std::path::PathBuf::from("test-sarasa"); 274 | let mut dir = std::fs::DirBuilder::new(); 275 | dir.recursive(true).create("test-sarasa").unwrap(); 276 | assert!(container.set_output_path(path).is_ok()); 277 | let _ = std::fs::remove_dir_all("test-sarasa"); 278 | } 279 | 280 | #[test] 281 | fn process_line() { 282 | let container = Container::new("stringtomatch".to_string(), None, 1, 0, 2); 283 | let span = container.process_line("this line should not be proc"); 284 | assert_eq!(span, None); 285 | let span = container.process_line("stringtomatch this line should be proc"); 286 | let expected_span = Some(Line::from(vec![ 287 | Span::from("".to_string()), 288 | Span::styled("stringtomatch".to_string(), Style::default().fg(Color::Red)), 289 | Span::from(" this line should be proc".to_string()), 290 | ])); 291 | assert_eq!(span, expected_span); 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /src/handler.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; 3 | 4 | use crate::app::App; 5 | 6 | /// Handles the key events and updates the state of [`App`]. 7 | pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> Result<()> { 8 | if app.show_input() { 9 | app.update_input(key_event.code); 10 | } else { 11 | match key_event.code { 12 | // exit application on ESC 13 | KeyCode::Esc => app.stop(), 14 | // exit application on Ctrl-D 15 | KeyCode::Char('d') | KeyCode::Char('D') => { 16 | if key_event.modifiers == KeyModifiers::CONTROL { 17 | app.stop(); 18 | } 19 | } 20 | KeyCode::Char('*') => app.flip_raw_view(), 21 | KeyCode::Char('s') => app.flip_single_view(), 22 | KeyCode::Char('i') | KeyCode::Char('/') => app.flip_show_input(), 23 | KeyCode::Char('h') => app.flip_help(), 24 | KeyCode::Char('b') => app.flip_barchart(), 25 | KeyCode::Char('w') => app.flip_wrap(), 26 | KeyCode::Char('p') | KeyCode::Char(' ') => app.flip_pause(), 27 | KeyCode::Char('v') => app.flip_direction(), 28 | KeyCode::Char('1') => view_helper(app, 1, key_event), 29 | KeyCode::F(1) => app.hide_view(1), 30 | KeyCode::Char('2') => view_helper(app, 2, key_event), 31 | KeyCode::F(2) => app.hide_view(2), 32 | KeyCode::Char('3') => view_helper(app, 3, key_event), 33 | KeyCode::F(3) => app.hide_view(3), 34 | KeyCode::Char('4') => view_helper(app, 4, key_event), 35 | KeyCode::F(4) => app.hide_view(4), 36 | KeyCode::Char('5') => view_helper(app, 5, key_event), 37 | KeyCode::F(5) => app.hide_view(5), 38 | KeyCode::Char('6') => view_helper(app, 6, key_event), 39 | KeyCode::F(6) => app.hide_view(6), 40 | KeyCode::Char('7') => view_helper(app, 7, key_event), 41 | KeyCode::F(7) => app.hide_view(7), 42 | KeyCode::Char('8') => view_helper(app, 8, key_event), 43 | KeyCode::F(8) => app.hide_view(8), 44 | KeyCode::Char('9') => view_helper(app, 9, key_event), 45 | KeyCode::F(9) => app.hide_view(9), 46 | KeyCode::Up => { 47 | if key_event.kind == KeyEventKind::Press { 48 | app.scroll_up(); 49 | } 50 | } 51 | KeyCode::Down => { 52 | if key_event.kind == KeyEventKind::Press { 53 | app.scroll_down(); 54 | } 55 | } 56 | KeyCode::Char('c') => { 57 | app.unpause(); 58 | if key_event.modifiers == KeyModifiers::CONTROL { 59 | app.stop(); 60 | } 61 | } 62 | _ => {} 63 | } 64 | } 65 | Ok(()) 66 | } 67 | 68 | fn view_helper(app: &mut App, id: u8, key_event: KeyEvent) { 69 | match key_event.modifiers { 70 | KeyModifiers::ALT => app.remove_view(id), 71 | KeyModifiers::NONE => app.zoom_into(id), 72 | _ => {} 73 | } 74 | } 75 | 76 | #[cfg(test)] 77 | mod tests { 78 | use std::char::from_digit; 79 | 80 | use ratatui::layout::Direction; 81 | 82 | use super::*; 83 | use crate::states::Views; 84 | 85 | #[test] 86 | fn stop() { 87 | let mut app = App::default(); 88 | app.add_container("1"); 89 | app.add_container("2"); 90 | assert_eq!(app.containers.len(), 2); 91 | 92 | // Test stopping 93 | app.state.running = true; 94 | assert!(app.is_running()); 95 | let key = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE); 96 | handle_key_events(key, &mut app).ok(); 97 | assert!(!app.is_running()); 98 | 99 | app.state.running = true; 100 | let key = KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE); 101 | handle_key_events(key, &mut app).ok(); 102 | assert!(app.is_running()); 103 | 104 | app.state.running = true; 105 | let key = KeyEvent::new(KeyCode::Char('D'), KeyModifiers::NONE); 106 | handle_key_events(key, &mut app).ok(); 107 | assert!(app.is_running()); 108 | 109 | app.state.running = true; 110 | let key = KeyEvent::new(KeyCode::Char('d'), KeyModifiers::CONTROL); 111 | handle_key_events(key, &mut app).ok(); 112 | assert!(!app.is_running()); 113 | 114 | app.state.running = true; 115 | let key = KeyEvent::new(KeyCode::Char('D'), KeyModifiers::CONTROL); 116 | handle_key_events(key, &mut app).ok(); 117 | assert!(!app.is_running()); 118 | } 119 | 120 | #[test] 121 | fn flip_raw() { 122 | let mut app = App::default(); 123 | app.add_container("3"); 124 | assert_eq!(app.containers.len(), 1); 125 | assert_eq!(app.state.show, Views::Containers); 126 | let key = KeyEvent::new(KeyCode::Char('*'), KeyModifiers::NONE); 127 | handle_key_events(key, &mut app).ok(); 128 | assert_eq!(app.state.show, Views::RawBuffer); 129 | handle_key_events(key, &mut app).ok(); 130 | assert_eq!(app.state.show, Views::Containers); 131 | } 132 | 133 | #[test] 134 | fn flip_single() { 135 | let mut app = App::default(); 136 | app.add_container("3"); 137 | assert_eq!(app.containers.len(), 1); 138 | assert_eq!(app.state.show, Views::Containers); 139 | let key = KeyEvent::new(KeyCode::Char('s'), KeyModifiers::NONE); 140 | handle_key_events(key, &mut app).ok(); 141 | assert_eq!(app.state.show, Views::SingleBuffer); 142 | handle_key_events(key, &mut app).ok(); 143 | assert_eq!(app.state.show, Views::Containers); 144 | } 145 | 146 | #[test] 147 | fn flip_show_input() { 148 | let mut app = App::default(); 149 | assert!(!app.state.show_input); 150 | let key = KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE); 151 | handle_key_events(key, &mut app).ok(); 152 | assert!(app.state.show_input); 153 | let key = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE); 154 | handle_key_events(key, &mut app).ok(); 155 | assert!(!app.state.show_input); 156 | } 157 | 158 | #[test] 159 | fn flip_help() { 160 | let mut app = App::default(); 161 | assert!(!app.state.help); 162 | let key = KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE); 163 | handle_key_events(key, &mut app).ok(); 164 | assert!(app.state.help); 165 | let key = KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE); 166 | handle_key_events(key, &mut app).ok(); 167 | assert!(!app.state.help); 168 | } 169 | 170 | #[test] 171 | fn flip_wrap() { 172 | let mut app = App::default(); 173 | assert!(!app.state.wrap); 174 | let key = KeyEvent::new(KeyCode::Char('w'), KeyModifiers::NONE); 175 | handle_key_events(key, &mut app).ok(); 176 | assert!(app.state.wrap); 177 | let key = KeyEvent::new(KeyCode::Char('w'), KeyModifiers::NONE); 178 | handle_key_events(key, &mut app).ok(); 179 | assert!(!app.state.help); 180 | } 181 | 182 | #[test] 183 | fn flip_pause() { 184 | let mut app = App::default(); 185 | assert!(!app.state.paused); 186 | let key = KeyEvent::new(KeyCode::Char('p'), KeyModifiers::NONE); 187 | handle_key_events(key, &mut app).ok(); 188 | assert!(app.state.paused); 189 | let key = KeyEvent::new(KeyCode::Char('p'), KeyModifiers::NONE); 190 | handle_key_events(key, &mut app).ok(); 191 | assert!(!app.state.help); 192 | } 193 | 194 | #[test] 195 | fn flip_direction() { 196 | let mut app = App::default(); 197 | assert_eq!(app.state.direction, Direction::Vertical); 198 | let key = KeyEvent::new(KeyCode::Char('v'), KeyModifiers::NONE); 199 | handle_key_events(key, &mut app).ok(); 200 | assert_eq!(app.state.direction, Direction::Horizontal); 201 | let key = KeyEvent::new(KeyCode::Char('v'), KeyModifiers::NONE); 202 | handle_key_events(key, &mut app).ok(); 203 | assert_eq!(app.state.direction, Direction::Vertical); 204 | } 205 | 206 | #[test] 207 | fn scroll_up_down_continue() { 208 | let mut app = App::default(); 209 | assert!(!app.state.paused); 210 | let mut key = KeyEvent::new(KeyCode::Up, KeyModifiers::NONE); 211 | key.kind = KeyEventKind::Press; 212 | handle_key_events(key, &mut app).ok(); 213 | assert!(app.state.paused); 214 | let mut key = KeyEvent::new(KeyCode::Up, KeyModifiers::NONE); 215 | key.kind = KeyEventKind::Press; 216 | handle_key_events(key, &mut app).ok(); 217 | assert!(app.state.paused); 218 | let mut key = KeyEvent::new(KeyCode::Down, KeyModifiers::NONE); 219 | key.kind = KeyEventKind::Press; 220 | handle_key_events(key, &mut app).ok(); 221 | assert!(app.state.paused); 222 | let key = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE); 223 | handle_key_events(key, &mut app).ok(); 224 | assert!(!app.state.paused); 225 | } 226 | 227 | #[test] 228 | fn container_number() { 229 | for i in 1..9_u8 { 230 | let mut app = App::default(); 231 | for a in 1..9_u8 { 232 | app.add_container(&a.to_string()); 233 | } 234 | assert_eq!(app.state.show, Views::Containers); 235 | assert_eq!(app.state.zoom_id, None); 236 | let key = KeyEvent::new( 237 | KeyCode::Char(from_digit(i as u32, 10).unwrap()), 238 | KeyModifiers::NONE, 239 | ); 240 | handle_key_events(key, &mut app).ok(); 241 | assert_eq!(app.state.show, Views::Zoom); 242 | assert_eq!(app.state.zoom_id, Some(i)); 243 | // Flip 244 | handle_key_events(key, &mut app).ok(); 245 | assert_eq!(app.state.show, Views::Containers); 246 | assert_eq!(app.state.zoom_id, None); 247 | // remove 248 | let key = KeyEvent::new( 249 | KeyCode::Char(from_digit(i as u32, 10).unwrap()), 250 | KeyModifiers::ALT, 251 | ); 252 | handle_key_events(key, &mut app).ok(); 253 | assert_eq!(app.state.show, Views::Remove); 254 | assert_eq!(app.state.zoom_id, Some(i)); 255 | } 256 | } 257 | 258 | #[test] 259 | fn container_hide() { 260 | let mut app = App::default(); 261 | for i in 1..9_u8 { 262 | app.add_container(&i.to_string()); 263 | assert!(!app.containers[(i - 1) as usize].state.hide); 264 | let key = KeyEvent::new(KeyCode::F(i), KeyModifiers::NONE); 265 | handle_key_events(key, &mut app).ok(); 266 | assert!(app.containers[(i - 1) as usize].state.hide); 267 | } 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # cffe7a2..v0.0.1 (2023-10-19) 2 | 3 | - Bump actions-rs/cargo from 1.0.1 to 1.0.3 4 | - Merge pull request #20 from todoesverso/dependabot/github_actions/actions-rs/cargo-1.0.3 5 | - Merge pull request #22 from todoesverso/fix_stdin_cmd 6 | - Merge pull request #23 from todoesverso/ci 7 | - Bump crossterm from 0.25.0 to 0.26.1 8 | - Merge pull request #24 from todoesverso/dependabot/cargo/crossterm-0.26.1 9 | - Bump serde from 1.0.160 to 1.0.162 10 | - Merge pull request #25 from todoesverso/dependabot/cargo/serde-1.0.162 11 | - Merge pull request #26 from todoesverso/todoesverso-patch-1 12 | - Bump serde from 1.0.162 to 1.0.163 13 | - Merge pull request #27 from todoesverso/dependabot/cargo/serde-1.0.163 14 | - Bump regex from 1.8.1 to 1.8.3 15 | - Merge pull request #29 from todoesverso/dependabot/cargo/regex-1.8.3 16 | - Bump regex from 1.8.3 to 1.8.4 17 | - Merge pull request #31 from todoesverso/dependabot/cargo/regex-1.8.4 18 | - Bump serde from 1.0.163 to 1.0.164 19 | - Merge pull request #32 from todoesverso/dependabot/cargo/serde-1.0.164 20 | - Bump regex from 1.8.4 to 1.9.0 21 | - Merge pull request #35 from todoesverso/dependabot/cargo/regex-1.9.0 22 | - Bump is-terminal from 0.4.8 to 0.4.9 23 | - Bump serde from 1.0.166 to 1.0.167 24 | - Merge pull request #37 from todoesverso/dependabot/cargo/serde-1.0.167 25 | - Merge pull request #36 from todoesverso/dependabot/cargo/is-terminal-0.4.9 26 | - Bump regex from 1.9.0 to 1.9.1 27 | - Bump serde from 1.0.167 to 1.0.171 28 | - Merge pull request #40 from todoesverso/dependabot/cargo/serde-1.0.171 29 | - Merge pull request #38 from todoesverso/dependabot/cargo/regex-1.9.1 30 | - Bump proc-macro2 from 1.0.63 to 1.0.64 31 | - Merge pull request #39 from todoesverso/dependabot/cargo/proc-macro2-1.0.64 32 | - Pedantic cargo (#58) 33 | - [feature] Dump matches to files (#64) 34 | - Add single consolidated view (#66) 35 | - Bump actions/checkout from 3 to 4 (#68) 36 | - Bump regex from 1.9.4 to 1.9.5 (#67) 37 | - Bump proc-macro2 from 1.0.66 to 1.0.67 (#71) 38 | - Bump predicates from 3.0.3 to 3.0.4 (#72) 39 | - Bump unicode-width from 0.1.10 to 0.1.11 (#73) 40 | - Several refactors (#81) 41 | - Test new ci (#85) 42 | - Follow ratatui book (#88) 43 | 44 | ### Notes 45 | 46 | - Bumps [actions-rs/cargo](https://github.com/actions-rs/cargo) from 1.0.1 to 1.0.3. 47 | [Release notes](https://github.com/actions-rs/cargo/releases) 48 | [Changelog](https://github.com/actions-rs/cargo/blob/master/CHANGELOG.md) 49 | [Commits](https://github.com/actions-rs/cargo/compare/v1.0.1...v1.0.3) 50 | 51 | -- 52 | updated-dependencies: 53 | dependency-name: actions-rs/cargo 54 | dependency-type: direct:production 55 | update-type: version-update:semver-patch 56 | ... 57 | 58 | Signed-off-by: dependabot[bot] 59 | 60 | - Bump actions-rs/cargo from 1.0.1 to 1.0.3 61 | 62 | - Explicit command now is read in a separate thread 63 | 64 | - Test @jonhoo's CI 65 | 66 | - Bumps [crossterm](https://github.com/crossterm-rs/crossterm) from 0.25.0 to 0.26.1. 67 | [Release notes](https://github.com/crossterm-rs/crossterm/releases) 68 | [Changelog](https://github.com/crossterm-rs/crossterm/blob/master/CHANGELOG.md) 69 | [Commits](https://github.com/crossterm-rs/crossterm/compare/0.25...0.26.1) 70 | 71 | -- 72 | updated-dependencies: 73 | dependency-name: crossterm 74 | dependency-type: direct:production 75 | update-type: version-update:semver-minor 76 | ... 77 | 78 | Signed-off-by: dependabot[bot] 79 | 80 | - Bump crossterm from 0.25.0 to 0.26.1 81 | 82 | - Bumps [serde](https://github.com/serde-rs/serde) from 1.0.160 to 1.0.162. 83 | [Release notes](https://github.com/serde-rs/serde/releases) 84 | [Commits](https://github.com/serde-rs/serde/compare/v1.0.160...1.0.162) 85 | 86 | -- 87 | updated-dependencies: 88 | dependency-name: serde 89 | dependency-type: direct:production 90 | update-type: version-update:semver-patch 91 | ... 92 | 93 | Signed-off-by: dependabot[bot] 94 | 95 | - Bump serde from 1.0.160 to 1.0.162 96 | 97 | - Update README.md 98 | 99 | - Bumps [serde](https://github.com/serde-rs/serde) from 1.0.162 to 1.0.163. 100 | [Release notes](https://github.com/serde-rs/serde/releases) 101 | [Commits](https://github.com/serde-rs/serde/compare/v1.0.162...v1.0.163) 102 | 103 | -- 104 | updated-dependencies: 105 | dependency-name: serde 106 | dependency-type: direct:production 107 | update-type: version-update:semver-patch 108 | ... 109 | 110 | Signed-off-by: dependabot[bot] 111 | 112 | - Bump serde from 1.0.162 to 1.0.163 113 | 114 | - Bumps [regex](https://github.com/rust-lang/regex) from 1.8.1 to 1.8.3. 115 | [Release notes](https://github.com/rust-lang/regex/releases) 116 | [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md) 117 | [Commits](https://github.com/rust-lang/regex/compare/1.8.1...1.8.3) 118 | 119 | -- 120 | updated-dependencies: 121 | dependency-name: regex 122 | dependency-type: direct:production 123 | update-type: version-update:semver-patch 124 | ... 125 | 126 | Signed-off-by: dependabot[bot] 127 | 128 | - Bump regex from 1.8.1 to 1.8.3 129 | 130 | - Bumps [regex](https://github.com/rust-lang/regex) from 1.8.3 to 1.8.4. 131 | [Release notes](https://github.com/rust-lang/regex/releases) 132 | [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md) 133 | [Commits](https://github.com/rust-lang/regex/compare/1.8.3...1.8.4) 134 | 135 | -- 136 | updated-dependencies: 137 | dependency-name: regex 138 | dependency-type: direct:production 139 | update-type: version-update:semver-patch 140 | ... 141 | 142 | Signed-off-by: dependabot[bot] 143 | 144 | - Bump regex from 1.8.3 to 1.8.4 145 | 146 | - Bumps [serde](https://github.com/serde-rs/serde) from 1.0.163 to 1.0.164. 147 | [Release notes](https://github.com/serde-rs/serde/releases) 148 | [Commits](https://github.com/serde-rs/serde/compare/v1.0.163...v1.0.164) 149 | 150 | -- 151 | updated-dependencies: 152 | dependency-name: serde 153 | dependency-type: direct:production 154 | update-type: version-update:semver-patch 155 | ... 156 | 157 | Signed-off-by: dependabot[bot] 158 | 159 | - Bump serde from 1.0.163 to 1.0.164 160 | 161 | - Bumps [regex](https://github.com/rust-lang/regex) from 1.8.4 to 1.9.0. 162 | [Release notes](https://github.com/rust-lang/regex/releases) 163 | [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md) 164 | [Commits](https://github.com/rust-lang/regex/compare/1.8.4...1.9.0) 165 | 166 | -- 167 | updated-dependencies: 168 | dependency-name: regex 169 | dependency-type: direct:production 170 | update-type: version-update:semver-minor 171 | ... 172 | 173 | Signed-off-by: dependabot[bot] 174 | 175 | - Bump regex from 1.8.4 to 1.9.0 176 | 177 | - Bumps [is-terminal](https://github.com/sunfishcode/is-terminal) from 0.4.8 to 0.4.9. 178 | [Commits](https://github.com/sunfishcode/is-terminal/compare/v0.4.8...v0.4.9) 179 | 180 | -- 181 | updated-dependencies: 182 | dependency-name: is-terminal 183 | dependency-type: direct:production 184 | update-type: version-update:semver-patch 185 | ... 186 | 187 | Signed-off-by: dependabot[bot] 188 | 189 | - Bumps [serde](https://github.com/serde-rs/serde) from 1.0.166 to 1.0.167. 190 | [Release notes](https://github.com/serde-rs/serde/releases) 191 | [Commits](https://github.com/serde-rs/serde/compare/v1.0.166...v1.0.167) 192 | 193 | -- 194 | updated-dependencies: 195 | dependency-name: serde 196 | dependency-type: direct:production 197 | update-type: version-update:semver-patch 198 | ... 199 | 200 | Signed-off-by: dependabot[bot] 201 | 202 | - Bump serde from 1.0.166 to 1.0.167 203 | 204 | - Bump is-terminal from 0.4.8 to 0.4.9 205 | 206 | - Bumps [regex](https://github.com/rust-lang/regex) from 1.9.0 to 1.9.1. 207 | [Release notes](https://github.com/rust-lang/regex/releases) 208 | [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md) 209 | [Commits](https://github.com/rust-lang/regex/compare/1.9.0...1.9.1) 210 | 211 | -- 212 | updated-dependencies: 213 | dependency-name: regex 214 | dependency-type: direct:production 215 | update-type: version-update:semver-patch 216 | ... 217 | 218 | Signed-off-by: dependabot[bot] 219 | 220 | - Bumps [serde](https://github.com/serde-rs/serde) from 1.0.167 to 1.0.171. 221 | [Release notes](https://github.com/serde-rs/serde/releases) 222 | [Commits](https://github.com/serde-rs/serde/compare/v1.0.167...v1.0.171) 223 | 224 | -- 225 | updated-dependencies: 226 | dependency-name: serde 227 | dependency-type: direct:production 228 | update-type: version-update:semver-patch 229 | ... 230 | 231 | Signed-off-by: dependabot[bot] 232 | 233 | - Bump serde from 1.0.167 to 1.0.171 234 | 235 | - Bump regex from 1.9.0 to 1.9.1 236 | 237 | - Bumps [proc-macro2](https://github.com/dtolnay/proc-macro2) from 1.0.63 to 1.0.64. 238 | [Release notes](https://github.com/dtolnay/proc-macro2/releases) 239 | [Commits](https://github.com/dtolnay/proc-macro2/compare/1.0.63...1.0.64) 240 | 241 | -- 242 | updated-dependencies: 243 | dependency-name: proc-macro2 244 | dependency-type: direct:production 245 | update-type: version-update:semver-patch 246 | ... 247 | 248 | Signed-off-by: dependabot[bot] 249 | 250 | - Bump proc-macro2 from 1.0.63 to 1.0.64 251 | 252 | - * fix some cargo pedantic things 253 | 254 | * More pedantic 255 | 256 | * More macro info 257 | 258 | * Fix prev 259 | 260 | * Disable minimal version tests 261 | 262 | * Update min version 263 | 264 | - * Dump match to files first draft 265 | 266 | * Skip test on windows 267 | 268 | * fmt 269 | 270 | * Skip test on windows 271 | 272 | * Skip test on windows 273 | 274 | * Minor refactor 275 | 276 | - * Add single consolidated view 277 | 278 | * Added some tests 279 | 280 | * Added some tests 281 | 282 | - Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. 283 | [Release notes](https://github.com/actions/checkout/releases) 284 | [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) 285 | [Commits](https://github.com/actions/checkout/compare/v3...v4) 286 | 287 | -- 288 | updated-dependencies: 289 | dependency-name: actions/checkout 290 | dependency-type: direct:production 291 | update-type: version-update:semver-major 292 | ... 293 | 294 | Signed-off-by: dependabot[bot] 295 | Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 296 | 297 | - Bumps [regex](https://github.com/rust-lang/regex) from 1.9.4 to 1.9.5. 298 | [Release notes](https://github.com/rust-lang/regex/releases) 299 | [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md) 300 | [Commits](https://github.com/rust-lang/regex/compare/1.9.4...1.9.5) 301 | 302 | -- 303 | updated-dependencies: 304 | dependency-name: regex 305 | dependency-type: direct:production 306 | update-type: version-update:semver-patch 307 | ... 308 | 309 | Signed-off-by: dependabot[bot] 310 | Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 311 | 312 | - Bumps [proc-macro2](https://github.com/dtolnay/proc-macro2) from 1.0.66 to 1.0.67. 313 | [Release notes](https://github.com/dtolnay/proc-macro2/releases) 314 | [Commits](https://github.com/dtolnay/proc-macro2/compare/1.0.66...1.0.67) 315 | 316 | -- 317 | updated-dependencies: 318 | dependency-name: proc-macro2 319 | dependency-type: direct:production 320 | update-type: version-update:semver-patch 321 | ... 322 | 323 | Signed-off-by: dependabot[bot] 324 | Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 325 | 326 | - Bumps [predicates](https://github.com/assert-rs/predicates-rs) from 3.0.3 to 3.0.4. 327 | [Changelog](https://github.com/assert-rs/predicates-rs/blob/master/CHANGELOG.md) 328 | [Commits](https://github.com/assert-rs/predicates-rs/compare/v3.0.3...v3.0.4) 329 | 330 | -- 331 | updated-dependencies: 332 | dependency-name: predicates 333 | dependency-type: direct:production 334 | update-type: version-update:semver-patch 335 | ... 336 | 337 | Signed-off-by: dependabot[bot] 338 | Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 339 | 340 | - Bumps [unicode-width](https://github.com/unicode-rs/unicode-width) from 0.1.10 to 0.1.11. 341 | [Commits](https://github.com/unicode-rs/unicode-width/compare/v0.1.10...v0.1.11) 342 | 343 | -- 344 | updated-dependencies: 345 | dependency-name: unicode-width 346 | dependency-type: direct:production 347 | update-type: version-update:semver-patch 348 | ... 349 | 350 | Signed-off-by: dependabot[bot] 351 | Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 352 | 353 | - * Several refactors 354 | 355 | * Use the same CI than ratatui 356 | 357 | * Test new ci 358 | 359 | * Test new ci 360 | 361 | * Test new ci 362 | 363 | * Test new ci 364 | 365 | * Test new ci 366 | 367 | * Test new ci 368 | 369 | * Test new ci 370 | 371 | - * Test new ci 372 | 373 | * Test new ci 374 | 375 | * Test new ci 376 | 377 | * Add simple tests 378 | 379 | - * Use anyhow 380 | 381 | * implement deref for appstate 382 | 383 | * Background colors and doc 384 | # v0.0.1..v0.0.2 (2023-12-29) 385 | 386 | - Add installation section (#90) 387 | - Bump actions/checkout from 3 to 4 (#87) 388 | - Bump ratatui and others (#95) 389 | - Bump ratatui from 0.24.0 to 0.25.0 (#100) 390 | - Bump zerocopy from 0.7.11 to 0.7.31 (#99) 391 | - Bump proc-macro2 from 1.0.69 to 1.0.70 (#98) 392 | 393 | ### Notes 394 | 395 | - * Add installation section 396 | 397 | * Fix link 398 | 399 | - Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. 400 | [Release notes](https://github.com/actions/checkout/releases) 401 | [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) 402 | [Commits](https://github.com/actions/checkout/compare/v3...v4) 403 | 404 | -- 405 | updated-dependencies: 406 | dependency-name: actions/checkout 407 | dependency-type: direct:production 408 | update-type: version-update:semver-major 409 | ... 410 | 411 | Signed-off-by: dependabot[bot] 412 | Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 413 | 414 | - * Bump ratatui and others 415 | 416 | * Revert predicate version bump 417 | 418 | * Bump minor rust version to match ratatui reqs 419 | 420 | - Bumps [ratatui](https://github.com/ratatui-org/ratatui) from 0.24.0 to 0.25.0. 421 | [Release notes](https://github.com/ratatui-org/ratatui/releases) 422 | [Changelog](https://github.com/ratatui-org/ratatui/blob/main/CHANGELOG.md) 423 | [Commits](https://github.com/ratatui-org/ratatui/compare/v0.24.0...v0.25.0) 424 | 425 | -- 426 | updated-dependencies: 427 | dependency-name: ratatui 428 | dependency-type: direct:production 429 | update-type: version-update:semver-minor 430 | ... 431 | 432 | Signed-off-by: dependabot[bot] 433 | Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 434 | 435 | - Bumps [zerocopy](https://github.com/google/zerocopy) from 0.7.11 to 0.7.31. 436 | [Release notes](https://github.com/google/zerocopy/releases) 437 | [Changelog](https://github.com/google/zerocopy/blob/main/CHANGELOG.md) 438 | [Commits](https://github.com/google/zerocopy/compare/v0.7.11...v0.7.31) 439 | 440 | -- 441 | updated-dependencies: 442 | dependency-name: zerocopy 443 | dependency-type: indirect 444 | ... 445 | 446 | Signed-off-by: dependabot[bot] 447 | Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 448 | 449 | - Bumps [proc-macro2](https://github.com/dtolnay/proc-macro2) from 1.0.69 to 1.0.70. 450 | [Release notes](https://github.com/dtolnay/proc-macro2/releases) 451 | [Commits](https://github.com/dtolnay/proc-macro2/compare/1.0.69...1.0.70) 452 | 453 | -- 454 | updated-dependencies: 455 | dependency-name: proc-macro2 456 | dependency-type: direct:production 457 | update-type: version-update:semver-patch 458 | ... 459 | 460 | Signed-off-by: dependabot[bot] 461 | Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 462 | -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | ops::{Deref, DerefMut}, 3 | sync::mpsc::TryRecvError, 4 | }; 5 | 6 | use anyhow::Result; 7 | use crossterm::event::KeyCode; 8 | use ratatui::{ 9 | layout::{Constraint, Direction, Layout, Rect}, 10 | text::Line, 11 | Frame, 12 | }; 13 | use threadpool::ThreadPool; 14 | 15 | use crate::{ 16 | args::{parse_args, Args}, 17 | bars::render_bar_chart, 18 | container::{Container, CONTAINERS_MAX, CONTAINER_BUFFER, CONTAINER_COLORS}, 19 | help::render_help, 20 | input::Input, 21 | states::{AppState, ScrollDirection, Views}, 22 | tstdin::StdinHandler, 23 | }; 24 | 25 | /// Application. 26 | /// 27 | /// This is the main application. 28 | #[derive(Debug)] 29 | pub struct App<'a> { 30 | pub containers: Vec>, 31 | pub state: AppState, 32 | pub input: Input, 33 | stdin: StdinHandler, 34 | pub raw_buffer: Container<'a>, 35 | pub single_buffer: Container<'a>, 36 | thread_pool: ThreadPool, 37 | args: Args, 38 | } 39 | 40 | impl Deref for App<'_> { 41 | type Target = AppState; 42 | 43 | fn deref(&self) -> &AppState { 44 | &self.state 45 | } 46 | } 47 | 48 | impl DerefMut for App<'_> { 49 | fn deref_mut(&mut self) -> &mut AppState { 50 | &mut self.state 51 | } 52 | } 53 | 54 | impl Default for App<'_> { 55 | fn default() -> Self { 56 | Self { 57 | stdin: StdinHandler::new(), 58 | args: parse_args(), 59 | input: Input::default(), 60 | raw_buffer: Container::new_clean(".*"), 61 | single_buffer: Container::new_clean("single"), 62 | containers: Vec::new(), 63 | state: AppState::default(), 64 | thread_pool: ThreadPool::new(4), 65 | } 66 | } 67 | } 68 | 69 | impl App<'_> { 70 | /// Constructs a new instance of [`App`]. 71 | pub fn new(args: Option) -> Self { 72 | let mut ret = Self::default(); 73 | let mut threads: u64 = 1; 74 | if let Some(args_inner) = args { 75 | ret.args = args_inner; 76 | if ret.args.vertical.is_some() { 77 | ret.state.direction = Direction::Horizontal; 78 | } 79 | if ret.args.single.is_some() { 80 | ret.state.show = Views::SingleBuffer; 81 | } 82 | 83 | threads = ret.args.threads.unwrap_or(1); 84 | ret.thread_pool = ThreadPool::new(threads as usize); 85 | } 86 | 87 | // Let 0 for raw_buffer 88 | for (id, c) in (1_u8..).zip(ret.args.containers.iter()) { 89 | let mut con = Container::new( 90 | c.re.clone(), 91 | c.trigger.clone(), 92 | c.timeout.unwrap_or(1), 93 | threads, 94 | CONTAINER_BUFFER, 95 | ); 96 | if let Some(output_path) = ret.args.output.clone() { 97 | con.set_output_path(output_path).ok(); 98 | } 99 | con.state.color = CONTAINER_COLORS[(id - 1) as usize]; 100 | con.id = id; 101 | ret.containers.push(con); 102 | } 103 | if ret.containers.is_empty() { 104 | ret.state.show = Views::RawBuffer; 105 | } 106 | ret 107 | } 108 | 109 | pub fn init(&mut self) -> Result<()> { 110 | self.state.running = true; 111 | self.stdin.init(self.args.command.clone())?; 112 | Ok(()) 113 | } 114 | 115 | pub fn update_input(&mut self, key_code: KeyCode) { 116 | match key_code { 117 | KeyCode::Enter => { 118 | if self.add_input_as_container() { 119 | self.hide_show_input(); 120 | self.state.show = Views::Containers; 121 | } 122 | } 123 | KeyCode::Char(c) => { 124 | self.input.push(c); 125 | } 126 | KeyCode::Backspace => { 127 | self.input.pop(); 128 | } 129 | KeyCode::Esc => { 130 | self.hide_show_input(); 131 | } 132 | _ => {} 133 | } 134 | } 135 | 136 | pub fn add_input_as_container(&mut self) -> bool { 137 | let is_valid = self.input.is_valid(); 138 | if is_valid { 139 | self.add_container(&self.input.inner_clone()); 140 | self.input.reset(); 141 | } 142 | is_valid 143 | } 144 | 145 | pub fn add_container(&mut self, text: &str) { 146 | let first_free_id = self.get_free_ids(); 147 | let mut con = Container::new(text.to_string(), None, 1, 1, CONTAINER_BUFFER); 148 | if let Some(output_path) = self.args.output.clone() { 149 | con.set_output_path(output_path).ok(); 150 | } 151 | if let Some(inner_id) = first_free_id.first() { 152 | con.state.color = CONTAINER_COLORS[(inner_id - 1) as usize]; 153 | con.id = *inner_id; 154 | self.containers.push(con); 155 | } 156 | } 157 | 158 | pub fn zoom_into(&mut self, id: u8) { 159 | if !self.containers.iter().map(|c| c.id).any(|x| x == id) { 160 | return; 161 | } 162 | 163 | if self.state.show == Views::Zoom { 164 | self.state.show = Views::Containers; 165 | self.state.zoom_id = None; 166 | } else { 167 | self.state.show = Views::Zoom; 168 | self.state.zoom_id = Some(id); 169 | } 170 | } 171 | 172 | pub fn remove_view(&mut self, id: u8) { 173 | if !self.containers.iter().map(|c| c.id).any(|x| x == id) { 174 | return; 175 | } 176 | self.state.show = Views::Remove; 177 | self.state.zoom_id = Some(id); 178 | } 179 | 180 | pub fn hide_view(&mut self, id: u8) { 181 | if !self.containers.iter().map(|c| c.id).any(|x| x == id) { 182 | return; 183 | } 184 | for container in self.containers.iter_mut() { 185 | if container.id == id { 186 | container.state.hide = !container.state.hide; 187 | } 188 | } 189 | } 190 | 191 | pub fn flip_raw_view(&mut self) { 192 | if !self.containers.is_empty() { 193 | if self.state.show == Views::RawBuffer { 194 | self.state.show = Views::Containers; 195 | } else { 196 | self.state.show = Views::RawBuffer; 197 | } 198 | } 199 | } 200 | 201 | pub fn flip_single_view(&mut self) { 202 | if !self.containers.is_empty() { 203 | if self.state.show == Views::SingleBuffer { 204 | self.state.show = Views::Containers; 205 | } else { 206 | self.state.show = Views::SingleBuffer; 207 | } 208 | } 209 | } 210 | 211 | /// Handles the tick event of the terminal. 212 | pub fn tick(&mut self) { 213 | self.get_stdin(); 214 | } 215 | 216 | fn handle_containers_with_line(&mut self, line: &str) { 217 | for c in self.containers.iter_mut() { 218 | if c.re.is_match(line) { 219 | let ret = c.proc_and_push_line(line); 220 | if let Some(l) = ret { 221 | self.single_buffer.cb.push(l.to_owned()); 222 | } 223 | } 224 | } 225 | } 226 | 227 | fn get_stdin(&mut self) { 228 | match self.stdin.try_recv() { 229 | Ok(line) => { 230 | // save all lines to a raw buffer 231 | if !self.state.paused { 232 | self.raw_buffer.cb.push(Line::from(line.clone())); 233 | self.handle_containers_with_line(&line); 234 | } 235 | } 236 | Err(TryRecvError::Disconnected) => self.stop(), 237 | Err(TryRecvError::Empty) if self.args.exit.unwrap_or_default() => { 238 | self.stop(); 239 | } 240 | _ => {} 241 | } 242 | } 243 | 244 | fn get_free_ids(&self) -> Vec { 245 | let used_ids: Vec = self.containers.iter().map(|c| c.id).collect(); 246 | let mut free_ids: Vec = Vec::new(); 247 | 248 | for id in 1_u8..CONTAINERS_MAX { 249 | if !used_ids.contains(&id) { 250 | free_ids.push(id); 251 | } 252 | } 253 | 254 | free_ids 255 | } 256 | 257 | fn get_layout_blocks(&self, size: Rect) -> Vec { 258 | let mut constr: Vec = vec![]; 259 | let show_cont = self.containers.iter().filter(|c| !c.state.hide).count(); 260 | for _ in 0..show_cont { 261 | constr.push(Constraint::Ratio(1, show_cont as u32)); 262 | } 263 | let ret = Layout::default() 264 | .direction(self.state.direction) 265 | .constraints(constr) 266 | .split(size); 267 | 268 | ret.to_vec() 269 | } 270 | 271 | fn render_containers(&mut self, frame: &mut Frame) { 272 | let blocks = self.get_layout_blocks(frame.area()); 273 | 274 | for (i, container) in self.containers.iter().filter(|c| !c.state.hide).enumerate() { 275 | container.render(frame, blocks[i]); 276 | } 277 | } 278 | 279 | fn update_containers(&mut self, frame_rect: Rect) { 280 | match self.state.show { 281 | Views::RawBuffer => { 282 | // Raw buffer 283 | let container = &mut self.raw_buffer; 284 | container.state.paused = self.state.paused; 285 | container.state.wrap = self.state.wrap; 286 | container.update_scroll(frame_rect.height as usize, &self.state.scroll_direction); 287 | } 288 | Views::SingleBuffer => { 289 | // Single buffer 290 | let container = &mut self.single_buffer; 291 | container.state.paused = self.state.paused; 292 | container.state.wrap = self.state.wrap; 293 | container.update_scroll(frame_rect.height as usize, &self.state.scroll_direction); 294 | } 295 | _ => (), 296 | } 297 | let blocks = self.get_layout_blocks(frame_rect); 298 | let mut area; 299 | 300 | // General containers 301 | for (i, container) in self 302 | .containers 303 | .iter_mut() 304 | .filter(|c| !c.state.hide) 305 | .enumerate() 306 | { 307 | if self.state.show == Views::Zoom { 308 | area = frame_rect.height; 309 | } else { 310 | area = blocks[i].height; 311 | } 312 | container.state.paused = self.state.paused; 313 | container.state.wrap = self.state.wrap; 314 | container.update_scroll(area as usize, &self.state.scroll_direction); 315 | } 316 | 317 | // Reset scroll direction so that scroll is done on each key press 318 | self.state.scroll_direction = ScrollDirection::NONE; 319 | } 320 | 321 | fn render_raw(&mut self, frame: &mut Frame) { 322 | let container = &self.raw_buffer; 323 | container.render(frame, frame.area()); 324 | } 325 | 326 | fn render_single(&mut self, frame: &mut Frame) { 327 | let container = &self.single_buffer; 328 | container.render(frame, frame.area()); 329 | } 330 | 331 | fn render_id(&mut self, frame: &mut Frame, id: u8) { 332 | for container in self.containers.iter() { 333 | if container.id == id { 334 | container.render(frame, frame.area()); 335 | } 336 | } 337 | } 338 | 339 | fn remove_id(&mut self, id: u8) { 340 | if let Some(index) = self.containers.iter().position(|c| c.id == id) { 341 | self.containers[index].reset(); 342 | self.containers.swap_remove(index); 343 | } 344 | self.containers.sort_by_key(|container| container.id); 345 | } 346 | 347 | fn render_help(&self, frame: &mut Frame) { 348 | if self.state.help { 349 | render_help(frame); 350 | } 351 | } 352 | 353 | fn render_bar_chart(&self, frame: &mut Frame) { 354 | if self.state.barchart { 355 | render_bar_chart(frame, self); 356 | } 357 | } 358 | 359 | fn render_input(&self, frame: &mut Frame) { 360 | if self.state.show_input { 361 | self.input.render(frame); 362 | } 363 | } 364 | 365 | /// Renders the user interface widgets. 366 | pub fn render(&mut self, frame: &mut Frame) { 367 | self.update_containers(frame.area()); 368 | match self.state.show { 369 | Views::Containers => self.render_containers(frame), 370 | Views::RawBuffer => self.render_raw(frame), 371 | Views::SingleBuffer => self.render_single(frame), 372 | Views::Zoom => { 373 | if let Some(id) = self.state.zoom_id { 374 | self.render_id(frame, id); 375 | } 376 | } 377 | Views::Remove => { 378 | if let Some(id) = self.state.zoom_id { 379 | self.remove_id(id); 380 | if self.containers.is_empty() { 381 | self.state.show = Views::RawBuffer; 382 | self.render_raw(frame); 383 | } else { 384 | self.state.show = Views::Containers; 385 | self.state.zoom_id = None; 386 | self.render_containers(frame); 387 | } 388 | } 389 | } 390 | } 391 | // Popups need to go at the bottom 392 | self.render_help(frame); 393 | self.render_bar_chart(frame); 394 | self.render_input(frame); 395 | } 396 | } 397 | 398 | #[cfg(test)] 399 | mod tests { 400 | use ratatui::{ 401 | backend::TestBackend, 402 | buffer::Buffer, 403 | style::{Color, Modifier, Style}, 404 | Terminal, 405 | }; 406 | 407 | use super::*; 408 | use crate::args::LocalContainer; 409 | 410 | #[test] 411 | fn test_new() { 412 | let mut app = App::new(None); 413 | 414 | // Running 415 | assert!(!app.is_running()); 416 | app.init().unwrap(); 417 | assert!(app.is_running()); 418 | app.stop(); 419 | assert!(!app.is_running()); 420 | 421 | // Direction 422 | assert_eq!(app.state.direction, Direction::Vertical); 423 | app.flip_direction(); 424 | assert_eq!(app.state.direction, Direction::Horizontal); 425 | 426 | // Containers 427 | assert_eq!(app.containers.len(), 0); 428 | app.add_container("text"); 429 | assert_eq!(app.containers.len(), 1); 430 | app.add_container("text2"); 431 | assert_eq!(app.containers.len(), 2); 432 | 433 | let mut args = parse_args(); 434 | args.containers = vec![ 435 | LocalContainer { 436 | re: "a".to_string(), 437 | trigger: None, 438 | timeout: None, 439 | }, 440 | LocalContainer { 441 | re: "b".to_string(), 442 | trigger: None, 443 | timeout: None, 444 | }, 445 | ]; 446 | let app = App::new(Some(args)); 447 | assert_eq!(app.containers.len(), 2); 448 | } 449 | 450 | #[test] 451 | fn input() { 452 | // New all clean 453 | let mut app = App::new(None); 454 | assert_eq!(app.containers.len(), 0); 455 | assert_eq!(app.input.input, "".to_string()); 456 | // Add a char 457 | app.update_input(KeyCode::Char('a')); 458 | assert_eq!(app.input.input, "a".to_string()); 459 | // Remove the char 460 | app.update_input(KeyCode::Backspace); 461 | assert_eq!(app.input.input, "".to_string()); 462 | // Re add the char 463 | app.update_input(KeyCode::Char('a')); 464 | assert_eq!(app.containers.len(), 0); 465 | app.update_input(KeyCode::Enter); 466 | // Enter the input 467 | assert!(!app.show_input()); 468 | assert_eq!(app.input.input, "".to_string()); 469 | assert_eq!(app.containers.len(), 1); 470 | } 471 | 472 | #[test] 473 | fn zoom_into() { 474 | let mut app = App::new(None); 475 | assert_eq!(app.containers.len(), 0); 476 | app.add_container("text"); 477 | app.add_container("text2"); 478 | app.add_container("text3"); 479 | assert_eq!(app.containers.len(), 3); 480 | assert_eq!(app.state.show, Views::RawBuffer); 481 | assert_eq!(app.state.zoom_id, None); 482 | 483 | // Zoom in 484 | app.zoom_into(1); 485 | assert_eq!(app.state.show, Views::Zoom); 486 | assert_eq!(app.state.zoom_id, Some(1)); 487 | 488 | // Zoom out 489 | app.zoom_into(1); 490 | assert_eq!(app.state.zoom_id, None); 491 | assert_eq!(app.state.show, Views::Containers); 492 | } 493 | 494 | #[test] 495 | fn get_stdin() { 496 | let mut app = App::new(None); 497 | app.add_container("a"); 498 | let c = app.containers.first().unwrap(); 499 | assert!(c.cb.is_empty()); 500 | assert!(app.raw_buffer.cb.is_empty()); 501 | assert_eq!(app.raw_buffer.cb.len(), 0); 502 | app.init().unwrap(); 503 | app.stdin.sender.send("abc".to_string()).unwrap(); 504 | app.tick(); 505 | 506 | app.stdin.sender.send("def".to_string()).unwrap(); 507 | app.tick(); 508 | 509 | let c = app.containers.first().unwrap(); 510 | assert!(!c.cb.is_empty()); 511 | assert_eq!(c.cb.len(), 1); 512 | assert!(!app.raw_buffer.cb.is_empty()); 513 | assert_eq!(app.raw_buffer.cb.len(), 2); 514 | } 515 | 516 | #[test] 517 | fn get_layout_blocks() { 518 | let mut app = App::new(None); 519 | app.add_container("a"); 520 | let rect = Rect::new(0, 0, 10, 10); 521 | let lb = app.get_layout_blocks(rect); 522 | assert_eq!(lb, vec![rect]); 523 | app.add_container("b"); 524 | let lb = app.get_layout_blocks(rect); 525 | let expected_blocks = vec![Rect::new(0, 0, 10, 5), Rect::new(0, 5, 10, 5)]; 526 | assert_eq!(lb, expected_blocks); 527 | } 528 | 529 | #[test] 530 | fn render_containers() { 531 | let mut app = App::new(None); 532 | app.add_container("a"); 533 | app.add_container("b"); 534 | for c in app.containers.iter_mut() { 535 | c.state.color = Color::White; 536 | } 537 | let backend = TestBackend::new(16, 14); 538 | let mut terminal = Terminal::new(backend).unwrap(); 539 | terminal 540 | .draw(|f| { 541 | app.render_containers(f); 542 | }) 543 | .unwrap(); 544 | let mut expected = Buffer::with_lines(vec![ 545 | "┌[1]'a' (0)────┐", 546 | "│ │", 547 | "│ │", 548 | "│ │", 549 | "│ │", 550 | "│ │", 551 | "└──────────────┘", 552 | "┌[2]'b' (0)────┐", 553 | "│ │", 554 | "│ │", 555 | "│ │", 556 | "│ │", 557 | "│ │", 558 | "└──────────────┘", 559 | ]); 560 | let bolds = 1..=10; 561 | for x in 0..=15 { 562 | for y in 0..=13 { 563 | if bolds.contains(&x) && (y == 0 || y == 7) { 564 | expected[(x, y)].set_style(Style::default().add_modifier(Modifier::BOLD)); 565 | } 566 | expected[(x, y)].set_fg(Color::White); 567 | expected[(x, y)].set_bg(Color::Black); 568 | } 569 | } 570 | terminal.backend().assert_buffer(&expected); 571 | } 572 | 573 | #[test] 574 | fn render_id() { 575 | let mut app = App::new(None); 576 | app.add_container("a"); 577 | app.add_container("b"); 578 | for c in app.containers.iter_mut() { 579 | c.state.color = Color::White; 580 | } 581 | app.zoom_into(1); 582 | let backend = TestBackend::new(16, 14); 583 | let mut terminal = Terminal::new(backend).unwrap(); 584 | terminal 585 | .draw(|f| { 586 | app.render(f); 587 | }) 588 | .unwrap(); 589 | let mut expected = Buffer::with_lines(vec![ 590 | "┌[1]'a' (0)────┐", 591 | "│ │", 592 | "│ │", 593 | "│ │", 594 | "│ │", 595 | "│ │", 596 | "│ │", 597 | "│ │", 598 | "│ │", 599 | "│ │", 600 | "│ │", 601 | "│ │", 602 | "│ │", 603 | "└──────────────┘", 604 | ]); 605 | let bolds = 1..=10; 606 | for x in 0..=15 { 607 | for y in 0..=13 { 608 | if bolds.contains(&x) && (y == 0) { 609 | expected[(x, y)].set_style(Style::default().add_modifier(Modifier::BOLD)); 610 | } 611 | expected[(x, y)].set_fg(Color::White); 612 | expected[(x, y)].set_bg(Color::Black); 613 | } 614 | } 615 | terminal.backend().assert_buffer(&expected); 616 | app.zoom_into(1); 617 | app.zoom_into(2); 618 | terminal 619 | .draw(|f| { 620 | app.render(f); 621 | }) 622 | .unwrap(); 623 | let mut expected = Buffer::with_lines(vec![ 624 | "┌[2]'b' (0)────┐", 625 | "│ │", 626 | "│ │", 627 | "│ │", 628 | "│ │", 629 | "│ │", 630 | "│ │", 631 | "│ │", 632 | "│ │", 633 | "│ │", 634 | "│ │", 635 | "│ │", 636 | "│ │", 637 | "└──────────────┘", 638 | ]); 639 | let bolds = 1..=10; 640 | for x in 0..=15 { 641 | for y in 0..=13 { 642 | if bolds.contains(&x) && (y == 0) { 643 | expected[(x, y)].set_style(Style::default().add_modifier(Modifier::BOLD)); 644 | } 645 | expected[(x, y)].set_fg(Color::White); 646 | expected[(x, y)].set_bg(Color::Black); 647 | } 648 | } 649 | terminal.backend().assert_buffer(&expected); 650 | } 651 | 652 | #[test] 653 | fn render_single_view() { 654 | let mut app = App::new(None); 655 | app.add_container("a"); 656 | for c in app.containers.iter_mut() { 657 | c.state.color = Color::White; 658 | } 659 | app.state.show = Views::SingleBuffer; 660 | let backend = TestBackend::new(17, 14); 661 | let mut terminal = Terminal::new(backend).unwrap(); 662 | terminal 663 | .draw(|f| { 664 | app.render(f); 665 | }) 666 | .unwrap(); 667 | let mut expected = Buffer::with_lines(vec![ 668 | "┌[0]'single' (0)┐", 669 | "│ │", 670 | "│ │", 671 | "│ │", 672 | "│ │", 673 | "│ │", 674 | "│ │", 675 | "│ │", 676 | "│ │", 677 | "│ │", 678 | "│ │", 679 | "│ │", 680 | "│ │", 681 | "└───────────────┘", 682 | ]); 683 | 684 | let bolds = 1..=15; 685 | for x in 0..=16 { 686 | for y in 0..=13 { 687 | if bolds.contains(&x) && (y == 0) { 688 | let st = Style::default().add_modifier(Modifier::BOLD); 689 | expected[(x, y)].set_style(st); 690 | expected[(x, y)].set_fg(Color::Red); 691 | } else { 692 | expected[(x, y)].set_fg(Color::White); 693 | } 694 | expected[(x, y)].set_bg(Color::Black); 695 | } 696 | } 697 | dbg!(&expected); 698 | terminal.backend().assert_buffer(&expected); 699 | } 700 | 701 | #[test] 702 | fn flip_single_view() { 703 | let mut app = App::new(None); 704 | app.add_container("a"); 705 | assert_eq!(app.state.show, Views::RawBuffer); 706 | app.flip_single_view(); 707 | assert_eq!(app.state.show, Views::SingleBuffer); 708 | } 709 | 710 | #[test] 711 | fn remove_view() { 712 | let mut app = App::new(None); 713 | app.add_container("a"); 714 | app.add_container("b"); 715 | for c in app.containers.iter_mut() { 716 | c.state.color = Color::White; 717 | } 718 | app.flip_raw_view(); 719 | app.remove_view(1); 720 | let backend = TestBackend::new(16, 14); 721 | let mut terminal = Terminal::new(backend).unwrap(); 722 | terminal 723 | .draw(|f| { 724 | app.render(f); 725 | }) 726 | .unwrap(); 727 | let mut expected = Buffer::with_lines(vec![ 728 | "┌[2]'b' (0)────┐", 729 | "│ │", 730 | "│ │", 731 | "│ │", 732 | "│ │", 733 | "│ │", 734 | "│ │", 735 | "│ │", 736 | "│ │", 737 | "│ │", 738 | "│ │", 739 | "│ │", 740 | "│ │", 741 | "└──────────────┘", 742 | ]); 743 | let bolds = 1..=10; 744 | for x in 0..=15 { 745 | for y in 0..=13 { 746 | if bolds.contains(&x) && (y == 0) { 747 | expected[(x, y)].set_style(Style::default().add_modifier(Modifier::BOLD)); 748 | } 749 | expected[(x, y)].set_fg(Color::White); 750 | expected[(x, y)].set_bg(Color::Black); 751 | } 752 | } 753 | terminal.backend().assert_buffer(&expected); 754 | } 755 | 756 | #[test] 757 | fn hide_view() { 758 | let mut app = App::new(None); 759 | app.add_container("a"); 760 | app.add_container("b"); 761 | for c in app.containers.iter_mut() { 762 | c.state.color = Color::White; 763 | } 764 | app.flip_raw_view(); 765 | app.hide_view(1); 766 | let backend = TestBackend::new(16, 14); 767 | let mut terminal = Terminal::new(backend).unwrap(); 768 | terminal 769 | .draw(|f| { 770 | app.render(f); 771 | }) 772 | .unwrap(); 773 | let mut expected = Buffer::with_lines(vec![ 774 | "┌[2]'b' (0)────┐", 775 | "│ │", 776 | "│ │", 777 | "│ │", 778 | "│ │", 779 | "│ │", 780 | "│ │", 781 | "│ │", 782 | "│ │", 783 | "│ │", 784 | "│ │", 785 | "│ │", 786 | "│ │", 787 | "└──────────────┘", 788 | ]); 789 | let bolds = 1..=10; 790 | for x in 0..=15 { 791 | for y in 0..=13 { 792 | if bolds.contains(&x) && (y == 0) { 793 | expected[(x, y)].set_style(Style::default().add_modifier(Modifier::BOLD)); 794 | } 795 | expected[(x, y)].set_fg(Color::White); 796 | expected[(x, y)].set_bg(Color::Black); 797 | } 798 | } 799 | terminal.backend().assert_buffer(&expected); 800 | 801 | app.hide_view(1); 802 | terminal 803 | .draw(|f| { 804 | app.render(f); 805 | }) 806 | .unwrap(); 807 | let mut expected = Buffer::with_lines(vec![ 808 | "┌[1]'a' (0)────┐", 809 | "│ │", 810 | "│ │", 811 | "│ │", 812 | "│ │", 813 | "│ │", 814 | "└──────────────┘", 815 | "┌[2]'b' (0)────┐", 816 | "│ │", 817 | "│ │", 818 | "│ │", 819 | "│ │", 820 | "│ │", 821 | "└──────────────┘", 822 | ]); 823 | let bolds = 1..=10; 824 | for x in 0..=15 { 825 | for y in 0..=13 { 826 | if bolds.contains(&x) && (y == 0 || y == 7) { 827 | expected[(x, y)].set_style(Style::default().add_modifier(Modifier::BOLD)); 828 | } 829 | expected[(x, y)].set_fg(Color::White); 830 | expected[(x, y)].set_bg(Color::Black); 831 | } 832 | } 833 | terminal.backend().assert_buffer(&expected); 834 | } 835 | 836 | #[test] 837 | fn update_containers() { 838 | let mut app = App::new(None); 839 | let rect = Rect::new(0, 0, 10, 10); 840 | app.add_container("a"); 841 | app.add_container("b"); 842 | 843 | for c in app.containers.iter() { 844 | assert!(!c.state.paused); 845 | assert!(!c.state.wrap); 846 | assert_eq!(c.state.scroll, 0); 847 | } 848 | 849 | app.init().unwrap(); 850 | for _ in 0..=128 { 851 | app.stdin.sender.send("abc".to_string()).unwrap(); 852 | app.tick(); 853 | } 854 | 855 | // Change the app state 856 | app.state.paused = true; 857 | app.state.wrap = true; 858 | app.update_containers(rect); 859 | 860 | for c in app.containers.iter() { 861 | assert_eq!(c.state.paused, app.state.paused); 862 | assert_eq!(c.state.wrap, app.state.wrap); 863 | } 864 | app.update_containers(rect); 865 | } 866 | } 867 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "ahash" 7 | version = "0.8.11" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" 10 | dependencies = [ 11 | "cfg-if", 12 | "once_cell", 13 | "version_check", 14 | "zerocopy", 15 | ] 16 | 17 | [[package]] 18 | name = "aho-corasick" 19 | version = "1.1.3" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 22 | dependencies = [ 23 | "memchr", 24 | ] 25 | 26 | [[package]] 27 | name = "allocator-api2" 28 | version = "0.2.18" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" 31 | 32 | [[package]] 33 | name = "anstyle" 34 | version = "1.0.7" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" 37 | 38 | [[package]] 39 | name = "anyhow" 40 | version = "1.0.100" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" 43 | 44 | [[package]] 45 | name = "assert_cmd" 46 | version = "2.0.14" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "ed72493ac66d5804837f480ab3766c72bdfab91a65e565fc54fa9e42db0073a8" 49 | dependencies = [ 50 | "anstyle", 51 | "bstr", 52 | "doc-comment", 53 | "predicates", 54 | "predicates-core", 55 | "predicates-tree", 56 | "wait-timeout", 57 | ] 58 | 59 | [[package]] 60 | name = "autocfg" 61 | version = "1.3.0" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 64 | 65 | [[package]] 66 | name = "bitflags" 67 | version = "2.9.4" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" 70 | 71 | [[package]] 72 | name = "bstr" 73 | version = "1.9.1" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" 76 | dependencies = [ 77 | "memchr", 78 | "regex-automata", 79 | "serde", 80 | ] 81 | 82 | [[package]] 83 | name = "bumpalo" 84 | version = "3.16.0" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 87 | 88 | [[package]] 89 | name = "cassowary" 90 | version = "0.3.0" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" 93 | 94 | [[package]] 95 | name = "castaway" 96 | version = "0.2.4" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" 99 | dependencies = [ 100 | "rustversion", 101 | ] 102 | 103 | [[package]] 104 | name = "cfg-if" 105 | version = "1.0.0" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 108 | 109 | [[package]] 110 | name = "compact_str" 111 | version = "0.8.1" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" 114 | dependencies = [ 115 | "castaway", 116 | "cfg-if", 117 | "itoa", 118 | "rustversion", 119 | "ryu", 120 | "static_assertions", 121 | ] 122 | 123 | [[package]] 124 | name = "convert_case" 125 | version = "0.7.1" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" 128 | dependencies = [ 129 | "unicode-segmentation", 130 | ] 131 | 132 | [[package]] 133 | name = "crossterm" 134 | version = "0.28.1" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" 137 | dependencies = [ 138 | "bitflags", 139 | "crossterm_winapi", 140 | "mio", 141 | "parking_lot", 142 | "rustix 0.38.44", 143 | "signal-hook", 144 | "signal-hook-mio", 145 | "winapi", 146 | ] 147 | 148 | [[package]] 149 | name = "crossterm" 150 | version = "0.29.0" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" 153 | dependencies = [ 154 | "bitflags", 155 | "crossterm_winapi", 156 | "derive_more", 157 | "document-features", 158 | "mio", 159 | "parking_lot", 160 | "rustix 1.1.2", 161 | "signal-hook", 162 | "signal-hook-mio", 163 | "winapi", 164 | ] 165 | 166 | [[package]] 167 | name = "crossterm_winapi" 168 | version = "0.9.1" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 171 | dependencies = [ 172 | "winapi", 173 | ] 174 | 175 | [[package]] 176 | name = "darling" 177 | version = "0.20.11" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" 180 | dependencies = [ 181 | "darling_core", 182 | "darling_macro", 183 | ] 184 | 185 | [[package]] 186 | name = "darling_core" 187 | version = "0.20.11" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" 190 | dependencies = [ 191 | "fnv", 192 | "ident_case", 193 | "proc-macro2", 194 | "quote", 195 | "strsim", 196 | "syn", 197 | ] 198 | 199 | [[package]] 200 | name = "darling_macro" 201 | version = "0.20.11" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" 204 | dependencies = [ 205 | "darling_core", 206 | "quote", 207 | "syn", 208 | ] 209 | 210 | [[package]] 211 | name = "derive_more" 212 | version = "2.0.1" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" 215 | dependencies = [ 216 | "derive_more-impl", 217 | ] 218 | 219 | [[package]] 220 | name = "derive_more-impl" 221 | version = "2.0.1" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" 224 | dependencies = [ 225 | "convert_case", 226 | "proc-macro2", 227 | "quote", 228 | "syn", 229 | ] 230 | 231 | [[package]] 232 | name = "deunicode" 233 | version = "1.6.0" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "339544cc9e2c4dc3fc7149fd630c5f22263a4fdf18a98afd0075784968b5cf00" 236 | 237 | [[package]] 238 | name = "difflib" 239 | version = "0.4.0" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" 242 | 243 | [[package]] 244 | name = "doc-comment" 245 | version = "0.3.3" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" 248 | 249 | [[package]] 250 | name = "document-features" 251 | version = "0.2.11" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" 254 | dependencies = [ 255 | "litrs", 256 | ] 257 | 258 | [[package]] 259 | name = "either" 260 | version = "1.12.0" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" 263 | 264 | [[package]] 265 | name = "equivalent" 266 | version = "1.0.1" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 269 | 270 | [[package]] 271 | name = "errno" 272 | version = "0.3.14" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" 275 | dependencies = [ 276 | "libc", 277 | "windows-sys", 278 | ] 279 | 280 | [[package]] 281 | name = "float-cmp" 282 | version = "0.9.0" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" 285 | dependencies = [ 286 | "num-traits", 287 | ] 288 | 289 | [[package]] 290 | name = "fnv" 291 | version = "1.0.7" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 294 | 295 | [[package]] 296 | name = "hashbrown" 297 | version = "0.14.5" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 300 | dependencies = [ 301 | "ahash", 302 | "allocator-api2", 303 | ] 304 | 305 | [[package]] 306 | name = "heck" 307 | version = "0.5.0" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 310 | 311 | [[package]] 312 | name = "hermit-abi" 313 | version = "0.3.9" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 316 | 317 | [[package]] 318 | name = "hermit-abi" 319 | version = "0.5.2" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" 322 | 323 | [[package]] 324 | name = "ident_case" 325 | version = "1.0.1" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 328 | 329 | [[package]] 330 | name = "indexmap" 331 | version = "2.2.6" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" 334 | dependencies = [ 335 | "equivalent", 336 | "hashbrown", 337 | ] 338 | 339 | [[package]] 340 | name = "indoc" 341 | version = "2.0.6" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" 344 | 345 | [[package]] 346 | name = "instability" 347 | version = "0.3.9" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "435d80800b936787d62688c927b6490e887c7ef5ff9ce922c6c6050fca75eb9a" 350 | dependencies = [ 351 | "darling", 352 | "indoc", 353 | "proc-macro2", 354 | "quote", 355 | "syn", 356 | ] 357 | 358 | [[package]] 359 | name = "is-terminal" 360 | version = "0.4.16" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" 363 | dependencies = [ 364 | "hermit-abi 0.5.2", 365 | "libc", 366 | "windows-sys", 367 | ] 368 | 369 | [[package]] 370 | name = "itertools" 371 | version = "0.12.1" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" 374 | dependencies = [ 375 | "either", 376 | ] 377 | 378 | [[package]] 379 | name = "itertools" 380 | version = "0.13.0" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 383 | dependencies = [ 384 | "either", 385 | ] 386 | 387 | [[package]] 388 | name = "itoa" 389 | version = "1.0.11" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 392 | 393 | [[package]] 394 | name = "libc" 395 | version = "0.2.177" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" 398 | 399 | [[package]] 400 | name = "linux-raw-sys" 401 | version = "0.4.15" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 404 | 405 | [[package]] 406 | name = "linux-raw-sys" 407 | version = "0.11.0" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" 410 | 411 | [[package]] 412 | name = "litrs" 413 | version = "0.4.2" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" 416 | 417 | [[package]] 418 | name = "lock_api" 419 | version = "0.4.12" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 422 | dependencies = [ 423 | "autocfg", 424 | "scopeguard", 425 | ] 426 | 427 | [[package]] 428 | name = "log" 429 | version = "0.4.21" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" 432 | 433 | [[package]] 434 | name = "logss" 435 | version = "0.0.4" 436 | dependencies = [ 437 | "anyhow", 438 | "assert_cmd", 439 | "crossterm 0.29.0", 440 | "is-terminal", 441 | "pico-args", 442 | "predicates", 443 | "proc-macro2", 444 | "ratatui", 445 | "regex", 446 | "serde", 447 | "serde_yaml", 448 | "slug", 449 | "threadpool", 450 | "unicode-width 0.2.0", 451 | "wait-timeout", 452 | ] 453 | 454 | [[package]] 455 | name = "lru" 456 | version = "0.12.3" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" 459 | dependencies = [ 460 | "hashbrown", 461 | ] 462 | 463 | [[package]] 464 | name = "memchr" 465 | version = "2.7.3" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "6d0d8b92cd8358e8d229c11df9358decae64d137c5be540952c5ca7b25aea768" 468 | 469 | [[package]] 470 | name = "mio" 471 | version = "1.0.4" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" 474 | dependencies = [ 475 | "libc", 476 | "log", 477 | "wasi", 478 | "windows-sys", 479 | ] 480 | 481 | [[package]] 482 | name = "normalize-line-endings" 483 | version = "0.3.0" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" 486 | 487 | [[package]] 488 | name = "num-traits" 489 | version = "0.2.19" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 492 | dependencies = [ 493 | "autocfg", 494 | ] 495 | 496 | [[package]] 497 | name = "num_cpus" 498 | version = "1.16.0" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" 501 | dependencies = [ 502 | "hermit-abi 0.3.9", 503 | "libc", 504 | ] 505 | 506 | [[package]] 507 | name = "once_cell" 508 | version = "1.19.0" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 511 | 512 | [[package]] 513 | name = "parking_lot" 514 | version = "0.12.3" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 517 | dependencies = [ 518 | "lock_api", 519 | "parking_lot_core", 520 | ] 521 | 522 | [[package]] 523 | name = "parking_lot_core" 524 | version = "0.9.10" 525 | source = "registry+https://github.com/rust-lang/crates.io-index" 526 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 527 | dependencies = [ 528 | "cfg-if", 529 | "libc", 530 | "redox_syscall", 531 | "smallvec", 532 | "windows-targets", 533 | ] 534 | 535 | [[package]] 536 | name = "paste" 537 | version = "1.0.15" 538 | source = "registry+https://github.com/rust-lang/crates.io-index" 539 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 540 | 541 | [[package]] 542 | name = "pico-args" 543 | version = "0.5.0" 544 | source = "registry+https://github.com/rust-lang/crates.io-index" 545 | checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" 546 | 547 | [[package]] 548 | name = "predicates" 549 | version = "3.1.0" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "68b87bfd4605926cdfefc1c3b5f8fe560e3feca9d5552cf68c466d3d8236c7e8" 552 | dependencies = [ 553 | "anstyle", 554 | "difflib", 555 | "float-cmp", 556 | "normalize-line-endings", 557 | "predicates-core", 558 | "regex", 559 | ] 560 | 561 | [[package]] 562 | name = "predicates-core" 563 | version = "1.0.6" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" 566 | 567 | [[package]] 568 | name = "predicates-tree" 569 | version = "1.0.9" 570 | source = "registry+https://github.com/rust-lang/crates.io-index" 571 | checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" 572 | dependencies = [ 573 | "predicates-core", 574 | "termtree", 575 | ] 576 | 577 | [[package]] 578 | name = "proc-macro2" 579 | version = "1.0.101" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" 582 | dependencies = [ 583 | "unicode-ident", 584 | ] 585 | 586 | [[package]] 587 | name = "quote" 588 | version = "1.0.41" 589 | source = "registry+https://github.com/rust-lang/crates.io-index" 590 | checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" 591 | dependencies = [ 592 | "proc-macro2", 593 | ] 594 | 595 | [[package]] 596 | name = "ratatui" 597 | version = "0.29.0" 598 | source = "registry+https://github.com/rust-lang/crates.io-index" 599 | checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" 600 | dependencies = [ 601 | "bitflags", 602 | "cassowary", 603 | "compact_str", 604 | "crossterm 0.28.1", 605 | "indoc", 606 | "instability", 607 | "itertools 0.13.0", 608 | "lru", 609 | "paste", 610 | "strum", 611 | "unicode-segmentation", 612 | "unicode-truncate", 613 | "unicode-width 0.2.0", 614 | ] 615 | 616 | [[package]] 617 | name = "redox_syscall" 618 | version = "0.5.1" 619 | source = "registry+https://github.com/rust-lang/crates.io-index" 620 | checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" 621 | dependencies = [ 622 | "bitflags", 623 | ] 624 | 625 | [[package]] 626 | name = "regex" 627 | version = "1.12.2" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" 630 | dependencies = [ 631 | "aho-corasick", 632 | "memchr", 633 | "regex-automata", 634 | "regex-syntax", 635 | ] 636 | 637 | [[package]] 638 | name = "regex-automata" 639 | version = "0.4.13" 640 | source = "registry+https://github.com/rust-lang/crates.io-index" 641 | checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" 642 | dependencies = [ 643 | "aho-corasick", 644 | "memchr", 645 | "regex-syntax", 646 | ] 647 | 648 | [[package]] 649 | name = "regex-syntax" 650 | version = "0.8.8" 651 | source = "registry+https://github.com/rust-lang/crates.io-index" 652 | checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" 653 | 654 | [[package]] 655 | name = "rustix" 656 | version = "0.38.44" 657 | source = "registry+https://github.com/rust-lang/crates.io-index" 658 | checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" 659 | dependencies = [ 660 | "bitflags", 661 | "errno", 662 | "libc", 663 | "linux-raw-sys 0.4.15", 664 | "windows-sys", 665 | ] 666 | 667 | [[package]] 668 | name = "rustix" 669 | version = "1.1.2" 670 | source = "registry+https://github.com/rust-lang/crates.io-index" 671 | checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" 672 | dependencies = [ 673 | "bitflags", 674 | "errno", 675 | "libc", 676 | "linux-raw-sys 0.11.0", 677 | "windows-sys", 678 | ] 679 | 680 | [[package]] 681 | name = "rustversion" 682 | version = "1.0.17" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" 685 | 686 | [[package]] 687 | name = "ryu" 688 | version = "1.0.18" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 691 | 692 | [[package]] 693 | name = "scopeguard" 694 | version = "1.2.0" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 697 | 698 | [[package]] 699 | name = "serde" 700 | version = "1.0.228" 701 | source = "registry+https://github.com/rust-lang/crates.io-index" 702 | checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 703 | dependencies = [ 704 | "serde_core", 705 | "serde_derive", 706 | ] 707 | 708 | [[package]] 709 | name = "serde_core" 710 | version = "1.0.228" 711 | source = "registry+https://github.com/rust-lang/crates.io-index" 712 | checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 713 | dependencies = [ 714 | "serde_derive", 715 | ] 716 | 717 | [[package]] 718 | name = "serde_derive" 719 | version = "1.0.228" 720 | source = "registry+https://github.com/rust-lang/crates.io-index" 721 | checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 722 | dependencies = [ 723 | "proc-macro2", 724 | "quote", 725 | "syn", 726 | ] 727 | 728 | [[package]] 729 | name = "serde_yaml" 730 | version = "0.9.34+deprecated" 731 | source = "registry+https://github.com/rust-lang/crates.io-index" 732 | checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" 733 | dependencies = [ 734 | "indexmap", 735 | "itoa", 736 | "ryu", 737 | "serde", 738 | "unsafe-libyaml", 739 | ] 740 | 741 | [[package]] 742 | name = "signal-hook" 743 | version = "0.3.17" 744 | source = "registry+https://github.com/rust-lang/crates.io-index" 745 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 746 | dependencies = [ 747 | "libc", 748 | "signal-hook-registry", 749 | ] 750 | 751 | [[package]] 752 | name = "signal-hook-mio" 753 | version = "0.2.4" 754 | source = "registry+https://github.com/rust-lang/crates.io-index" 755 | checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" 756 | dependencies = [ 757 | "libc", 758 | "mio", 759 | "signal-hook", 760 | ] 761 | 762 | [[package]] 763 | name = "signal-hook-registry" 764 | version = "1.4.2" 765 | source = "registry+https://github.com/rust-lang/crates.io-index" 766 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 767 | dependencies = [ 768 | "libc", 769 | ] 770 | 771 | [[package]] 772 | name = "slug" 773 | version = "0.1.6" 774 | source = "registry+https://github.com/rust-lang/crates.io-index" 775 | checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724" 776 | dependencies = [ 777 | "deunicode", 778 | "wasm-bindgen", 779 | ] 780 | 781 | [[package]] 782 | name = "smallvec" 783 | version = "1.13.2" 784 | source = "registry+https://github.com/rust-lang/crates.io-index" 785 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 786 | 787 | [[package]] 788 | name = "static_assertions" 789 | version = "1.1.0" 790 | source = "registry+https://github.com/rust-lang/crates.io-index" 791 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 792 | 793 | [[package]] 794 | name = "strsim" 795 | version = "0.11.1" 796 | source = "registry+https://github.com/rust-lang/crates.io-index" 797 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 798 | 799 | [[package]] 800 | name = "strum" 801 | version = "0.26.3" 802 | source = "registry+https://github.com/rust-lang/crates.io-index" 803 | checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" 804 | dependencies = [ 805 | "strum_macros", 806 | ] 807 | 808 | [[package]] 809 | name = "strum_macros" 810 | version = "0.26.4" 811 | source = "registry+https://github.com/rust-lang/crates.io-index" 812 | checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" 813 | dependencies = [ 814 | "heck", 815 | "proc-macro2", 816 | "quote", 817 | "rustversion", 818 | "syn", 819 | ] 820 | 821 | [[package]] 822 | name = "syn" 823 | version = "2.0.106" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" 826 | dependencies = [ 827 | "proc-macro2", 828 | "quote", 829 | "unicode-ident", 830 | ] 831 | 832 | [[package]] 833 | name = "termtree" 834 | version = "0.4.1" 835 | source = "registry+https://github.com/rust-lang/crates.io-index" 836 | checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" 837 | 838 | [[package]] 839 | name = "threadpool" 840 | version = "1.8.1" 841 | source = "registry+https://github.com/rust-lang/crates.io-index" 842 | checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" 843 | dependencies = [ 844 | "num_cpus", 845 | ] 846 | 847 | [[package]] 848 | name = "unicode-ident" 849 | version = "1.0.12" 850 | source = "registry+https://github.com/rust-lang/crates.io-index" 851 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 852 | 853 | [[package]] 854 | name = "unicode-segmentation" 855 | version = "1.11.0" 856 | source = "registry+https://github.com/rust-lang/crates.io-index" 857 | checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" 858 | 859 | [[package]] 860 | name = "unicode-truncate" 861 | version = "1.0.0" 862 | source = "registry+https://github.com/rust-lang/crates.io-index" 863 | checksum = "5a5fbabedabe362c618c714dbefda9927b5afc8e2a8102f47f081089a9019226" 864 | dependencies = [ 865 | "itertools 0.12.1", 866 | "unicode-width 0.1.13", 867 | ] 868 | 869 | [[package]] 870 | name = "unicode-width" 871 | version = "0.1.13" 872 | source = "registry+https://github.com/rust-lang/crates.io-index" 873 | checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" 874 | 875 | [[package]] 876 | name = "unicode-width" 877 | version = "0.2.0" 878 | source = "registry+https://github.com/rust-lang/crates.io-index" 879 | checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 880 | 881 | [[package]] 882 | name = "unsafe-libyaml" 883 | version = "0.2.11" 884 | source = "registry+https://github.com/rust-lang/crates.io-index" 885 | checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" 886 | 887 | [[package]] 888 | name = "version_check" 889 | version = "0.9.4" 890 | source = "registry+https://github.com/rust-lang/crates.io-index" 891 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 892 | 893 | [[package]] 894 | name = "wait-timeout" 895 | version = "0.2.1" 896 | source = "registry+https://github.com/rust-lang/crates.io-index" 897 | checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" 898 | dependencies = [ 899 | "libc", 900 | ] 901 | 902 | [[package]] 903 | name = "wasi" 904 | version = "0.11.0+wasi-snapshot-preview1" 905 | source = "registry+https://github.com/rust-lang/crates.io-index" 906 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 907 | 908 | [[package]] 909 | name = "wasm-bindgen" 910 | version = "0.2.92" 911 | source = "registry+https://github.com/rust-lang/crates.io-index" 912 | checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" 913 | dependencies = [ 914 | "cfg-if", 915 | "wasm-bindgen-macro", 916 | ] 917 | 918 | [[package]] 919 | name = "wasm-bindgen-backend" 920 | version = "0.2.92" 921 | source = "registry+https://github.com/rust-lang/crates.io-index" 922 | checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" 923 | dependencies = [ 924 | "bumpalo", 925 | "log", 926 | "once_cell", 927 | "proc-macro2", 928 | "quote", 929 | "syn", 930 | "wasm-bindgen-shared", 931 | ] 932 | 933 | [[package]] 934 | name = "wasm-bindgen-macro" 935 | version = "0.2.92" 936 | source = "registry+https://github.com/rust-lang/crates.io-index" 937 | checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" 938 | dependencies = [ 939 | "quote", 940 | "wasm-bindgen-macro-support", 941 | ] 942 | 943 | [[package]] 944 | name = "wasm-bindgen-macro-support" 945 | version = "0.2.92" 946 | source = "registry+https://github.com/rust-lang/crates.io-index" 947 | checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" 948 | dependencies = [ 949 | "proc-macro2", 950 | "quote", 951 | "syn", 952 | "wasm-bindgen-backend", 953 | "wasm-bindgen-shared", 954 | ] 955 | 956 | [[package]] 957 | name = "wasm-bindgen-shared" 958 | version = "0.2.92" 959 | source = "registry+https://github.com/rust-lang/crates.io-index" 960 | checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" 961 | 962 | [[package]] 963 | name = "winapi" 964 | version = "0.3.9" 965 | source = "registry+https://github.com/rust-lang/crates.io-index" 966 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 967 | dependencies = [ 968 | "winapi-i686-pc-windows-gnu", 969 | "winapi-x86_64-pc-windows-gnu", 970 | ] 971 | 972 | [[package]] 973 | name = "winapi-i686-pc-windows-gnu" 974 | version = "0.4.0" 975 | source = "registry+https://github.com/rust-lang/crates.io-index" 976 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 977 | 978 | [[package]] 979 | name = "winapi-x86_64-pc-windows-gnu" 980 | version = "0.4.0" 981 | source = "registry+https://github.com/rust-lang/crates.io-index" 982 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 983 | 984 | [[package]] 985 | name = "windows-sys" 986 | version = "0.59.0" 987 | source = "registry+https://github.com/rust-lang/crates.io-index" 988 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 989 | dependencies = [ 990 | "windows-targets", 991 | ] 992 | 993 | [[package]] 994 | name = "windows-targets" 995 | version = "0.52.6" 996 | source = "registry+https://github.com/rust-lang/crates.io-index" 997 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 998 | dependencies = [ 999 | "windows_aarch64_gnullvm", 1000 | "windows_aarch64_msvc", 1001 | "windows_i686_gnu", 1002 | "windows_i686_gnullvm", 1003 | "windows_i686_msvc", 1004 | "windows_x86_64_gnu", 1005 | "windows_x86_64_gnullvm", 1006 | "windows_x86_64_msvc", 1007 | ] 1008 | 1009 | [[package]] 1010 | name = "windows_aarch64_gnullvm" 1011 | version = "0.52.6" 1012 | source = "registry+https://github.com/rust-lang/crates.io-index" 1013 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1014 | 1015 | [[package]] 1016 | name = "windows_aarch64_msvc" 1017 | version = "0.52.6" 1018 | source = "registry+https://github.com/rust-lang/crates.io-index" 1019 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1020 | 1021 | [[package]] 1022 | name = "windows_i686_gnu" 1023 | version = "0.52.6" 1024 | source = "registry+https://github.com/rust-lang/crates.io-index" 1025 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1026 | 1027 | [[package]] 1028 | name = "windows_i686_gnullvm" 1029 | version = "0.52.6" 1030 | source = "registry+https://github.com/rust-lang/crates.io-index" 1031 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1032 | 1033 | [[package]] 1034 | name = "windows_i686_msvc" 1035 | version = "0.52.6" 1036 | source = "registry+https://github.com/rust-lang/crates.io-index" 1037 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1038 | 1039 | [[package]] 1040 | name = "windows_x86_64_gnu" 1041 | version = "0.52.6" 1042 | source = "registry+https://github.com/rust-lang/crates.io-index" 1043 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1044 | 1045 | [[package]] 1046 | name = "windows_x86_64_gnullvm" 1047 | version = "0.52.6" 1048 | source = "registry+https://github.com/rust-lang/crates.io-index" 1049 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1050 | 1051 | [[package]] 1052 | name = "windows_x86_64_msvc" 1053 | version = "0.52.6" 1054 | source = "registry+https://github.com/rust-lang/crates.io-index" 1055 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1056 | 1057 | [[package]] 1058 | name = "zerocopy" 1059 | version = "0.7.34" 1060 | source = "registry+https://github.com/rust-lang/crates.io-index" 1061 | checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" 1062 | dependencies = [ 1063 | "zerocopy-derive", 1064 | ] 1065 | 1066 | [[package]] 1067 | name = "zerocopy-derive" 1068 | version = "0.7.34" 1069 | source = "registry+https://github.com/rust-lang/crates.io-index" 1070 | checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" 1071 | dependencies = [ 1072 | "proc-macro2", 1073 | "quote", 1074 | "syn", 1075 | ] 1076 | --------------------------------------------------------------------------------