├── doc ├── demo.gif └── ztop.1 ├── .gitignore ├── release.toml ├── rustfmt.toml ├── src ├── event.rs ├── app │ ├── linux.rs │ └── freebsd.rs ├── main.rs └── app.rs ├── Cargo.toml ├── README.md ├── LICENSE ├── .cirrus.yml ├── CHANGELOG.md └── Cargo.lock /doc/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asomers/ztop/HEAD/doc/demo.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | **/*.orig 4 | **/*.rej 5 | **/*.diff 6 | **/.*.swp 7 | -------------------------------------------------------------------------------- /release.toml: -------------------------------------------------------------------------------- 1 | pre-release-replacements = [ 2 | { file="CHANGELOG.md", search="Unreleased", replace="{{version}}" }, 3 | { file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}" } 4 | ] 5 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | brace_style = "SameLineWhere" 2 | edition = "2021" 3 | format_strings = true 4 | group_imports = "StdExternalCrate" 5 | imports_granularity = "Crate" 6 | imports_layout = "HorizontalVertical" 7 | match_block_trailing_comma = false 8 | max_width = 80 9 | reorder_impl_items = true 10 | reorder_imports = true 11 | struct_field_align_threshold = 16 12 | use_field_init_shorthand = true 13 | # TODO: chained method calls https://github.com/rust-lang/rustfmt/issues/4306 14 | -------------------------------------------------------------------------------- /src/event.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use crossterm::event; 4 | 5 | #[derive(Debug)] 6 | pub enum Event { 7 | Key(event::KeyEvent), 8 | Mouse, 9 | Tick, 10 | Other, 11 | } 12 | 13 | /// Poll stdin for events with a timeout 14 | pub fn poll(tick_rate: &Duration) -> Option { 15 | if !event::poll(*tick_rate).unwrap() { 16 | Some(Event::Tick) 17 | } else { 18 | match event::read() { 19 | Ok(event::Event::Key(key)) => Some(Event::Key(key)), 20 | Ok(event::Event::Mouse(_)) => Some(Event::Mouse), 21 | Ok(_) => Some(Event::Other), 22 | e => panic!("Unhandled error {e:?}"), 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ztop" 3 | version = "0.3.0" 4 | edition = "2021" 5 | authors = ["Alan Somers "] 6 | license = "BSD-2-Clause" 7 | repository = "https://github.com/asomers/ztop" 8 | description = "Display ZFS datasets' I/O in real time" 9 | categories = ["command-line-utilities"] 10 | keywords = ["zfs"] 11 | include = ["src/**/*", "LICENSE", "README.md", "CHANGELOG.md"] 12 | 13 | [dependencies] 14 | cfg-if = "1.0" 15 | clap = { version = "4.5", features = ["derive"] } 16 | humanize-rs = "0.1.5" 17 | nix = { version = "0.27.0", default-features = false, features = ["time"] } 18 | sysctl = "0.5.0" 19 | crossterm = { version = "0.29.0", default-features = false , features = ["events"]} 20 | ratatui = { version = "0.30.0-alpha.5", default-features = false, features = ["crossterm", "unstable"] } 21 | 22 | [target.'cfg(target_os = "linux")'.dependencies] 23 | glob = "0.3" 24 | 25 | [dependencies.regex] 26 | version = "1.3" 27 | default-features = false 28 | # Disable the unicode feature, since dataset names are always ASCII 29 | features = [ "perf", "std" ] 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ztop 2 | 3 | Display ZFS datasets' I/O in real time 4 | 5 | [![Build Status](https://api.cirrus-ci.com/github/asomers/ztop.svg)](https://cirrus-ci.com/github/asomers/ztop) 6 | [![Crates.io](https://img.shields.io/crates/v/ztop.svg)](https://crates.io/crates/ztop) 7 | 8 | # Overview 9 | 10 | `ztop` is like `top`, but for ZFS datasets. It displays the real-time activity 11 | for datasets. The built-in `zpool iostat` can display real-time I/O statistics 12 | for pools, but until now there was no similar tool for datasets. 13 | 14 | # Platform support 15 | 16 | `ztop` works on FreeBSD 12 and later, and Linux. 17 | 18 | # Screenshot 19 | 20 | ![Screenshot 1](https://raw.githubusercontent.com/asomers/ztop/master/doc/demo.gif) 21 | 22 | # Minimum Supported Rust Version (MSRV) 23 | 24 | ztop does not guarantee any specific MSRV. Rather, it guarantees compatibility 25 | with the oldest rustc shipped in the package collection of each supported 26 | operating system. 27 | 28 | * https://www.freshports.org/lang/rust/ 29 | 30 | # License 31 | 32 | `ztop` is primarily distributed under the terms of the BSD 2-clause license. 33 | 34 | See LICENSE for details. 35 | 36 | # Sponsorship 37 | 38 | ztop is sponsored by Axcient, inc. 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Axcient, inc. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 10 | -------------------------------------------------------------------------------- /.cirrus.yml: -------------------------------------------------------------------------------- 1 | setup: &SETUP 2 | env: 3 | HOME: /tmp # cargo needs it 4 | RUST_BACKTRACE: full # Better info for debugging test failures. 5 | setup_script: 6 | - fetch https://sh.rustup.rs -o rustup.sh 7 | - sh rustup.sh -y --profile=minimal --default-toolchain ${VERSION}-x86_64-unknown-freebsd 8 | 9 | build: &BUILD 10 | cargo_cache: 11 | folder: $HOME/.cargo/registry 12 | fingerprint_script: cat Cargo.lock || echo "" 13 | build_script: 14 | - . $HOME/.cargo/env || true 15 | - cargo build --all 16 | test_script: 17 | - . $HOME/.cargo/env || true 18 | - cargo test --all 19 | 20 | task: 21 | env: 22 | VERSION: 1.85.0 23 | name: FreeBSD 13.5 MSRV 24 | freebsd_instance: 25 | image: freebsd-13-5-release-amd64 26 | << : *SETUP 27 | << : *BUILD 28 | before_cache_script: rm -rf $HOME/.cargo/registry/index 29 | 30 | task: 31 | name: FreeBSD 14.2 nightly 32 | env: 33 | VERSION: nightly 34 | freebsd_instance: 35 | image: freebsd-14-2-release-amd64-ufs 36 | << : *SETUP 37 | << : *BUILD 38 | clippy_script: 39 | - . $HOME/.cargo/env 40 | - rustup component add clippy 41 | - cargo clippy --all-features --all-targets -- -D warnings 42 | fmt_script: 43 | - . $HOME/.cargo/env 44 | - rustup component add rustfmt 45 | - cargo fmt --all -- --check --color=never 46 | audit_script: 47 | - . $HOME/.cargo/env 48 | # install ca_root_nss due to https://github.com/rustsec/rustsec/issues/1137 49 | - pkg install -y ca_root_nss cargo-audit 50 | - cargo audit 51 | # Test our minimal version spec 52 | minver_test_script: 53 | - . $HOME/.cargo/env 54 | - cargo update -Zdirect-minimal-versions 55 | - cargo check --all-targets 56 | before_cache_script: rm -rf $HOME/.cargo/registry/index 57 | 58 | task: 59 | name: Linux MSRV 60 | container: 61 | image: rust:1.85.0 62 | setup_script: 63 | - rustup component add rustfmt 64 | << : *BUILD 65 | before_cache_script: rm -rf $HOME/.cargo/registry/index 66 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.3.0] - 2025-02-23 9 | 10 | ### Fixed 11 | 12 | - Correctly reset terminal settings when quitting the application. 13 | (#[2fe9cd1](https://github.com/asomers/ztop/commit/2fe9cd17d041d4b02f0a9e79000c6c1a4bf58d06)) 14 | 15 | ### Changed 16 | 17 | - Changed the default sort order. By default, sort descending for numeric 18 | columns or ascending for dataset name. 19 | (#[56](https://github.com/asomers/gstat-rs/pull/56)) 20 | 21 | - Changed the `-d` switch to match the behavior of `zfs list -d`: A depth of 0 22 | means to display each pool, a depth of 1 means to display one dataset deeper, 23 | etc. 24 | (#[55](https://github.com/asomers/gstat-rs/pull/55)) 25 | 26 | - Tweaked colors for better visibility on some terminals. 27 | (#[48](https://github.com/asomers/gstat-rs/pull/48)) 28 | 29 | ## [0.2.3] - 2023-12-18 30 | 31 | ### Fixed 32 | 33 | - Removed dependency on unmaintained tui crate. 34 | ([RUSTSEC-2023-0049](https://rustsec.org/advisories/RUSTSEC-2023-0049)) 35 | Removed dependency on atty crate, fixing an unaligned read bug. 36 | ([RUSTSEC-2021-0145](https://rustsec.org/advisories/RUSTSEC-2021-0145)) 37 | (#[31](https://github.com/asomers/ztop/pull/31)) 38 | 39 | ## [0.2.2] - 2023-03-27 40 | 41 | ### Added 42 | 43 | - Added ZoL support. 44 | (#[26](https://github.com/asomers/ztop/pull/26)) 45 | 46 | ## [0.2.1] - 2022-09-27 47 | 48 | ### Fixed 49 | 50 | - Fixed annoying warnings on FreeBSD 14.0-CURRENT. 51 | (#[23](https://github.com/asomers/ztop/pull/23)) 52 | 53 | ## [0.2.0] - 2022-03-15 54 | 55 | ### Fixed 56 | 57 | - Fix sorting on the "kB/s r" and "kB/s w" columns with the -s option 58 | (#[18](https://github.com/asomers/ztop/pull/18)) 59 | 60 | - Don't crash if two different pools have objsets of the same ID that list 61 | adjacently in the sysctl tree. 62 | (#[15](https://github.com/asomers/ztop/pull/15)) 63 | 64 | ## [0.1.1] - 2021-08-13 65 | 66 | ### Fixed 67 | 68 | - Don't crash on FreeBSD 12.2 69 | (#[5](https://github.com/asomers/ztop/pull/5)) 70 | 71 | - Don't crash if no datasets are present 72 | (#[6](https://github.com/asomers/ztop/pull/6)) 73 | -------------------------------------------------------------------------------- /doc/ztop.1: -------------------------------------------------------------------------------- 1 | .\" Copyright (c) 2021 Axcient 2 | .\" All rights reserved. 3 | .\" 4 | .\" Redistribution and use in source and binary forms, with or without 5 | .\" modification, are permitted provided that the following conditions 6 | .\" are met: 7 | .\" 1. Redistributions of source code must retain the above copyright 8 | .\" notice, this list of conditions and the following disclaimer. 9 | .\" 2. Redistributions in binary form must reproduce the above copyright 10 | .\" notice, this list of conditions and the following disclaimer in the 11 | .\" documentation and/or other materials provided with the distribution. 12 | .\" 13 | .\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 14 | .\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | .\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 16 | .\" ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 17 | .\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | .\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 19 | .\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 20 | .\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 21 | .\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 22 | .\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 23 | .\" SUCH DAMAGE. 24 | .\" 25 | .\" $FreeBSD$ 26 | .\" 27 | .Dd July 26, 2021 28 | .Dt ZTOP 1 29 | .Os 30 | .Sh NAME 31 | .Nm ztop 32 | .Nd Display ZFS datasets' I/O in real time 33 | .Sh SYNOPSIS 34 | .Nm 35 | .Op Fl ahrV 36 | .Op Fl d Ar depth 37 | .Op Fl f Ar filter 38 | .Op Fl t Ar time 39 | .Op Fl S Ar key 40 | .Op Ar pool ... 41 | .Sh DESCRIPTION 42 | The 43 | .Nm 44 | utility can be used to monitor the performance of 45 | .Xr zfs 8 46 | datasets. 47 | .Pp 48 | The options are as follows: 49 | .Bl -tag -width indent 50 | .It Fl a , Fl Fl auto 51 | Do not display idle datasets. 52 | .It Fl c , Fl Fl children 53 | Include child datasets' statistics with their parents'. 54 | This is especially useful when combined with 55 | .Ar -d . 56 | .It Fl d , Fl Fl depth Ar depth 57 | Only display datasets up to the given depth. 58 | .It Fl f , Fl Fl filter Ar filter 59 | A regular expression that can be used to only show statistics for some 60 | datasets. 61 | Only datasets with the names matching 62 | .Ar filter 63 | will be displayed. 64 | The format of the regular expression is described at 65 | .Lk https://docs.rs/regex . 66 | .It Fl t , Fl Fl time Ar time 67 | Refresh the 68 | .Nm 69 | display every 70 | .Ar interval 71 | seconds. 72 | Suffixes like 73 | .Cm s , ms , 74 | and 75 | .Cm us 76 | are accepted. 77 | .It Fl r , Fl Fl reverse 78 | Reverse the sort order 79 | .It Fl s , Fl Fl column Ar column 80 | Sort the devices by 81 | .Ar column . 82 | The spelling of 83 | .Ar column 84 | should match the displayed column header. 85 | .El 86 | .Pp 87 | .Nm 88 | displays performance statistics for ZFS datasets. 89 | If one or more 90 | .Ar pool 91 | are specified, then only those pools' datasets will be displayed. 92 | .Sh INTERACTIVE COMMANDS 93 | These commands are currently recognized. 94 | .Bl -tag -width indent 95 | .It Ic + 96 | Sort by the next column to the right. 97 | .It Ic - 98 | Sort by the next column to the left. 99 | .It Ic < 100 | Halve the update interval. 101 | .It Ic > 102 | Double the update interval. 103 | .It Ic a 104 | Toggle auto mode. 105 | This has the same effect as the 106 | .Fl Fl auto 107 | command line option. 108 | .It Ic c 109 | Toggle children mode. 110 | This has the same effect as the 111 | .Fl Fl children 112 | command line option. 113 | .It Ic D 114 | Decrease the depth of displayed datasets. 115 | .It Ic d 116 | Increase the depth of displayed datasets. 117 | .It Ic f 118 | Display only datasets with the names matching a regular expression 119 | (prompt for filter expression). 120 | .It Ic F 121 | Remove dataset filter. 122 | .It Ic q 123 | Quit 124 | .It Ic r 125 | Toggle reverse sort. 126 | This has the same effect as the 127 | .Fl Fl reverse 128 | command line option. 129 | .El 130 | .Sh EXIT STATUS 131 | .Ex -std 132 | .Sh SEE ALSO 133 | .Xr zpool-iostat 8 134 | -------------------------------------------------------------------------------- /src/app/linux.rs: -------------------------------------------------------------------------------- 1 | // vim: tw=80 2 | 3 | #![warn(clippy::all, clippy::pedantic)] 4 | 5 | use std::{ 6 | error::Error, 7 | fs::File, 8 | io, 9 | io::BufRead, 10 | iter::{Flatten, Peekable}, 11 | }; 12 | 13 | use glob::{glob, Paths, Pattern}; 14 | 15 | use super::Snapshot; 16 | 17 | // Similar to sysctl::CtlValue, but only as many types as necessary. 18 | #[derive(Debug)] 19 | enum ObjsetValue { 20 | String(String), 21 | U64(u64), 22 | } 23 | 24 | fn parse_objset_row(row: &str) -> Option<(String, ObjsetValue)> { 25 | let mut fields = row.split_ascii_whitespace(); 26 | 27 | match (fields.next(), fields.next(), fields.next()) { 28 | (Some(name), Some(_), Some(value)) => { 29 | let field_name = (*name).to_string(); 30 | if field_name == "dataset_name" { 31 | Some((field_name, ObjsetValue::String((*value).to_string()))) 32 | } else { 33 | match value.parse::().ok() { 34 | Some(n) => Some((field_name, ObjsetValue::U64(n))), 35 | None => Some(( 36 | field_name, 37 | ObjsetValue::String((*value).to_string()), 38 | )), 39 | } 40 | } 41 | } 42 | _ => None, 43 | } 44 | } 45 | 46 | fn parse_objset(reader: R) -> io::Result { 47 | // The first line contains raw numeric data that we don't collect. 48 | // The second line contains column headers. Both these lines are skipped. 49 | let lines = reader.lines().skip(2); 50 | 51 | let mut snap = Snapshot::default(); 52 | 53 | for line in lines { 54 | let fields = parse_objset_row(&line?).expect("malformed objset row"); 55 | match fields.1 { 56 | ObjsetValue::String(name) => snap.name = name, 57 | ObjsetValue::U64(n) => match fields.0.as_str() { 58 | "nread" => snap.nread = n, 59 | "nunlinked" => snap.nunlinked = n, 60 | "nunlinks" => snap.nunlinks = n, 61 | "nwritten" => snap.nwritten = n, 62 | "reads" => snap.reads = n, 63 | "writes" => snap.writes = n, 64 | _ => (), 65 | }, 66 | } 67 | } 68 | Ok(snap) 69 | } 70 | 71 | /// Convenience implementation for use with glob's `PathBuf`'s 72 | impl TryFrom for Snapshot { 73 | type Error = io::Error; 74 | 75 | fn try_from(file: File) -> io::Result { 76 | parse_objset(io::BufReader::new(file)) 77 | } 78 | } 79 | 80 | /// Convenience implementation for simpler testing 81 | #[cfg(test)] 82 | impl TryFrom<&str> for Snapshot { 83 | type Error = io::Error; 84 | 85 | fn try_from(s: &str) -> io::Result { 86 | parse_objset(io::BufReader::new(s.as_bytes())) 87 | } 88 | } 89 | 90 | pub(super) struct SnapshotIter { 91 | inner: Peekable>, 92 | } 93 | 94 | impl SnapshotIter { 95 | // Clippy complains about unnecessary wraps, but the type signature is 96 | // retained to be consistent with FreeBSD implementation. 97 | #[allow(clippy::unnecessary_wraps, clippy::single_match_else)] 98 | pub(crate) fn new(pool: Option<&str>) -> Result> { 99 | let paths = match pool { 100 | Some(poolname) => { 101 | let poolpat = Pattern::escape(poolname); 102 | let mut paths = 103 | glob(&format!("/proc/spl/kstat/zfs/{poolpat}/objset-*"))? 104 | .flatten() 105 | .peekable(); 106 | if paths.peek().is_none() { 107 | eprintln!("Statistics not found for pool {poolname}"); 108 | std::process::exit(1); 109 | } 110 | paths 111 | } 112 | None => { 113 | let mut paths = glob("/proc/spl/kstat/zfs/*/objset-*")? 114 | .flatten() 115 | .peekable(); 116 | if paths.peek().is_none() { 117 | eprintln!("No pools found; ZFS module not loaded?"); 118 | std::process::exit(1); 119 | } 120 | paths 121 | } 122 | }; 123 | 124 | Ok(SnapshotIter { inner: paths }) 125 | } 126 | } 127 | 128 | impl Iterator for SnapshotIter { 129 | type Item = io::Result; 130 | 131 | fn next(&mut self) -> Option { 132 | self.inner.next().map(|glob_result| { 133 | let file = File::open(glob_result)?; 134 | Snapshot::try_from(file) 135 | }) 136 | } 137 | } 138 | 139 | #[cfg(test)] 140 | mod t { 141 | // While I normally agree that wildcard imports are bad, "use super::*" is 142 | // an exception. 143 | #[allow(clippy::wildcard_imports)] 144 | use super::*; 145 | 146 | const SAMPLE_OBJSET: &str = "28 1 0x01 7 2160 5156962179 648086076730177 147 | name type data 148 | dataset_name 7 rpool/ROOT/default 149 | writes 4 5 150 | nwritten 4 100 151 | reads 4 8 152 | nread 4 160 153 | nunlinks 4 7 154 | nunlinked 4 7 155 | "; 156 | 157 | #[test] 158 | fn objset_parsing() { 159 | let reader = io::BufReader::new(SAMPLE_OBJSET.as_bytes()); 160 | let snap = parse_objset(reader).unwrap(); 161 | assert_eq!("rpool/ROOT/default", snap.name.as_str()); 162 | assert_eq!(8, snap.reads); 163 | assert_eq!(5, snap.writes); 164 | assert_eq!(160, snap.nread); 165 | assert_eq!(7, snap.nunlinks); 166 | assert_eq!(7, snap.nunlinked); 167 | assert_eq!(100, snap.nwritten); 168 | } 169 | 170 | #[test] 171 | fn objset_try_from() { 172 | let snap = Snapshot::try_from(SAMPLE_OBJSET).unwrap(); 173 | assert_eq!("rpool/ROOT/default", snap.name.as_str()); 174 | assert_eq!(8, snap.reads); 175 | assert_eq!(5, snap.writes); 176 | assert_eq!(160, snap.nread); 177 | assert_eq!(7, snap.nunlinks); 178 | assert_eq!(7, snap.nunlinked); 179 | assert_eq!(100, snap.nwritten); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // vim: tw=80 2 | use std::{error::Error, io, time::Duration}; 3 | 4 | use clap::Parser; 5 | use crossterm::event::KeyCode; 6 | use ratatui::{ 7 | backend::CrosstermBackend, 8 | layout::{Constraint, Direction, Layout, Rect}, 9 | style::{Color, Modifier, Style}, 10 | widgets::{Block, Borders, Cell, Clear, Paragraph, Row, Table}, 11 | Terminal, 12 | }; 13 | use regex::Regex; 14 | 15 | mod app; 16 | use self::app::App; 17 | mod event; 18 | use self::event::Event; 19 | 20 | /// Display ZFS datasets' I/O in real time 21 | // TODO: shorten the help options so they fit on 80 columns. 22 | #[derive(Debug, Default, clap::Parser)] 23 | struct Cli { 24 | /// only display datasets that have some activity. 25 | #[clap(short = 'a', long = "auto", verbatim_doc_comment)] 26 | auto: bool, 27 | /// Include child datasets' stats with their parents'. 28 | #[clap(short = 'c', long = "children")] 29 | children: bool, 30 | /// display datasets no more than this many levels deep. 31 | #[clap(short = 'd', long = "depth")] 32 | depth: Option, 33 | /// only display datasets with names matching filter, as a regex. 34 | #[clap(short = 'f', value_parser = Regex::new, long = "filter")] 35 | filter: Option, 36 | /// display update interval, in seconds or with the specified unit 37 | #[clap(short = 't', value_parser = Cli::duration_from_str, long = "time")] 38 | time: Option, 39 | /// Reverse the sort 40 | #[clap(short = 'r', long = "reverse")] 41 | reverse: bool, 42 | /// Sort by the named column. The name should match the column header. 43 | #[clap(short = 's', long = "sort")] 44 | sort: Option, 45 | /// Display these pools and their children 46 | pools: Vec, 47 | } 48 | 49 | impl Cli { 50 | fn duration_from_str(s: &str) -> Result { 51 | if let Ok(fsecs) = s.parse::() { 52 | Ok(Duration::from_secs_f64(fsecs)) 53 | } else { 54 | // Must have units 55 | humanize_rs::duration::parse(s) 56 | } 57 | } 58 | } 59 | 60 | #[derive(Clone, Debug, Default)] 61 | pub struct FilterPopup { 62 | new_regex: String, 63 | } 64 | 65 | impl FilterPopup { 66 | pub fn on_enter(&mut self) -> Result { 67 | Regex::new(&self.new_regex) 68 | } 69 | 70 | pub fn on_backspace(&mut self) { 71 | self.new_regex.pop(); 72 | } 73 | 74 | pub fn on_char(&mut self, c: char) { 75 | self.new_regex.push(c); 76 | } 77 | } 78 | 79 | mod ui { 80 | use ratatui::Frame; 81 | 82 | use super::*; 83 | 84 | // helper function to create a one-line popup box 85 | fn popup_layout(x: u16, y: u16, r: Rect) -> Rect { 86 | let popup_layout = Layout::default() 87 | .direction(Direction::Vertical) 88 | .constraints( 89 | [ 90 | Constraint::Max(r.height.saturating_sub(y) / 2), 91 | Constraint::Length(y), 92 | Constraint::Max(r.height.saturating_sub(y) / 2), 93 | ] 94 | .as_ref(), 95 | ) 96 | .split(r); 97 | 98 | Layout::default() 99 | .direction(Direction::Horizontal) 100 | .constraints( 101 | [ 102 | Constraint::Max(r.width.saturating_sub(x) / 2), 103 | Constraint::Length(x), 104 | Constraint::Max(r.width.saturating_sub(x) / 2), 105 | ] 106 | .as_ref(), 107 | ) 108 | .split(popup_layout[1])[1] 109 | } 110 | 111 | pub fn draw(f: &mut Frame, app: &mut App) { 112 | let hstyle = Style::default() 113 | .fg(Color::LightYellow) 114 | .add_modifier(Modifier::BOLD); 115 | let sstyle = hstyle.add_modifier(Modifier::REVERSED); 116 | let hcells = [ 117 | Cell::from(" r/s"), 118 | Cell::from(" kB/s r"), 119 | Cell::from(" w/s"), 120 | Cell::from(" kB/s w"), 121 | Cell::from("unlink/s"), 122 | Cell::from("Dataset"), 123 | ] 124 | .into_iter() 125 | .enumerate() 126 | .map(|(i, cell)| { 127 | if Some(i) == app.sort_idx() { 128 | cell.style(sstyle) 129 | } else { 130 | cell.style(hstyle) 131 | } 132 | }); 133 | let header = Row::new(hcells).style(Style::default().bg(Color::Blue)); 134 | let rows = app 135 | .elements() 136 | .into_iter() 137 | .map(|elem| { 138 | Row::new([ 139 | Cell::from(format!("{:>6.0}", elem.ops_r)), 140 | Cell::from(format!("{:>7.0}", elem.r_s / 1024.0)), 141 | Cell::from(format!("{:>6.0}", elem.ops_w)), 142 | Cell::from(format!("{:>7.0}", elem.w_s / 1024.0)), 143 | Cell::from(format!("{:>8.0}", elem.ops_unlink)), 144 | Cell::from(elem.name), 145 | ]) 146 | }) 147 | .collect::>(); 148 | let widths = [ 149 | Constraint::Length(7), 150 | Constraint::Length(8), 151 | Constraint::Length(7), 152 | Constraint::Length(8), 153 | Constraint::Length(9), 154 | Constraint::Min(6), 155 | ]; 156 | let t = Table::new(rows, widths) 157 | .header(header) 158 | .block(Block::default()) 159 | .flex(ratatui::layout::Flex::Legacy); 160 | f.render_widget(t, f.area()); 161 | } 162 | 163 | #[rustfmt::skip] 164 | pub fn draw_filter(f: &mut Frame, app: &FilterPopup) { 165 | let area = popup_layout(40, 3, f.area()); 166 | let popup_box = Paragraph::new(app.new_regex.as_str()) 167 | .block( 168 | Block::default() 169 | .borders(Borders::ALL) 170 | .title("Filter regex") 171 | ); 172 | f.render_widget(Clear, area); 173 | f.render_widget(popup_box, area); 174 | } 175 | 176 | // Needs a &String argument to work with Option::as_ref 177 | #[allow(clippy::ptr_arg)] 178 | pub fn col_idx(col_name: &String) -> Option { 179 | match col_name.trim() { 180 | "r/s" => Some(0), 181 | "kB/s r" => Some(1), 182 | "w/s" => Some(2), 183 | "kB/s w" => Some(3), 184 | "unlink/s" => Some(4), 185 | "Dataset" => Some(5), 186 | _ => None, 187 | } 188 | } 189 | } 190 | 191 | // https://github.com/rust-lang/rust-clippy/issues/7483 192 | #[allow(clippy::or_fun_call)] 193 | fn main() -> Result<(), Box> { 194 | let cli: Cli = Cli::parse(); 195 | let mut editting_filter = false; 196 | let mut tick_rate = cli.time.unwrap_or(Duration::from_secs(1)); 197 | let col_idx = cli.sort.as_ref().map(ui::col_idx).unwrap_or(None); 198 | let mut app = App::new( 199 | cli.auto, 200 | cli.children, 201 | cli.pools, 202 | cli.depth, 203 | cli.filter, 204 | cli.reverse, 205 | col_idx, 206 | ); 207 | let mut filter_popup = FilterPopup::default(); 208 | let stdout = io::stdout(); 209 | crossterm::terminal::enable_raw_mode().unwrap(); 210 | 211 | let backend = CrosstermBackend::new(stdout); 212 | let mut terminal = Terminal::new(backend)?; 213 | 214 | terminal.clear()?; 215 | while !app.should_quit() { 216 | terminal.draw(|f| { 217 | ui::draw(f, &mut app); 218 | if editting_filter { 219 | ui::draw_filter(f, &filter_popup) 220 | } 221 | })?; 222 | 223 | match event::poll(&tick_rate) { 224 | Some(Event::Tick) => { 225 | app.on_tick(); 226 | } 227 | Some(Event::Key(kev)) => { 228 | match kev.code { 229 | KeyCode::Esc if editting_filter => { 230 | editting_filter = false; 231 | } 232 | KeyCode::Enter if editting_filter => { 233 | let filter = filter_popup.on_enter()?; 234 | app.set_filter(filter); 235 | editting_filter = false; 236 | } 237 | KeyCode::Backspace if editting_filter => { 238 | filter_popup.on_backspace(); 239 | } 240 | KeyCode::Char(c) if editting_filter => { 241 | filter_popup.on_char(c); 242 | } 243 | KeyCode::Char('+') => { 244 | app.on_plus(); 245 | } 246 | KeyCode::Char('-') => { 247 | app.on_minus(); 248 | } 249 | KeyCode::Char('<') => { 250 | tick_rate /= 2; 251 | } 252 | KeyCode::Char('>') => { 253 | tick_rate *= 2; 254 | } 255 | KeyCode::Char('a') => { 256 | app.on_a(); 257 | } 258 | KeyCode::Char('c') => { 259 | app.on_c()?; 260 | } 261 | KeyCode::Char('D') => { 262 | app.on_d(false); 263 | } 264 | KeyCode::Char('d') => { 265 | app.on_d(true); 266 | } 267 | KeyCode::Char('F') => { 268 | app.clear_filter(); 269 | } 270 | KeyCode::Char('f') => { 271 | editting_filter = true; 272 | } 273 | KeyCode::Char('q') => { 274 | app.on_q(); 275 | } 276 | KeyCode::Char('r') => { 277 | app.on_r(); 278 | } 279 | _ => { 280 | // Ignore unknown keys 281 | } 282 | } 283 | } 284 | None => { 285 | // stdin closed for some reason 286 | break; 287 | } 288 | _ => { 289 | // Ignore unknown events 290 | } 291 | } 292 | } 293 | terminal.set_cursor_position((0, crossterm::terminal::size()?.1 - 1))?; 294 | crossterm::terminal::disable_raw_mode().unwrap(); 295 | Ok(()) 296 | } 297 | -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | // vim: tw=80 2 | use std::{ 3 | collections::{btree_map, BTreeMap}, 4 | error::Error, 5 | mem, 6 | ops::AddAssign, 7 | }; 8 | 9 | use cfg_if::cfg_if; 10 | use nix::{ 11 | sys::time::TimeSpec, 12 | time::{clock_gettime, ClockId}, 13 | }; 14 | use regex::Regex; 15 | 16 | cfg_if! { 17 | if #[cfg(target_os = "freebsd")] { 18 | mod freebsd; 19 | use freebsd::SnapshotIter; 20 | const CLOCK_UPTIME: ClockId = ClockId::CLOCK_UPTIME; 21 | } else if #[cfg(target_os = "linux")] { 22 | mod linux; 23 | use linux::SnapshotIter; 24 | const CLOCK_UPTIME: ClockId = ClockId::CLOCK_BOOTTIME; 25 | } 26 | } 27 | 28 | /// A snapshot in time of a dataset's statistics. 29 | /// 30 | /// The various fields are not saved atomically, but ought to be close. 31 | #[derive(Clone, Debug, Default)] 32 | struct Snapshot { 33 | name: String, 34 | nunlinked: u64, 35 | nunlinks: u64, 36 | nread: u64, 37 | reads: u64, 38 | nwritten: u64, 39 | writes: u64, 40 | } 41 | 42 | impl Snapshot { 43 | fn compute(&self, prev: Option<&Self>, etime: f64) -> Element { 44 | if let Some(prev) = prev { 45 | Element { 46 | name: self.name.clone(), 47 | ops_r: (self.reads - prev.reads) as f64 / etime, 48 | r_s: (self.nread - prev.nread) as f64 / etime, 49 | ops_w: (self.writes - prev.writes) as f64 / etime, 50 | w_s: (self.nwritten - prev.nwritten) as f64 / etime, 51 | ops_unlink: (self.nunlinked - prev.nunlinked) as f64 / etime, 52 | } 53 | } else { 54 | Element { 55 | name: self.name.clone(), 56 | ops_r: self.reads as f64 / etime, 57 | r_s: self.nread as f64 / etime, 58 | ops_w: self.writes as f64 / etime, 59 | w_s: self.nwritten as f64 / etime, 60 | ops_unlink: self.nunlinked as f64 / etime, 61 | } 62 | } 63 | } 64 | 65 | /// Iterate through ZFS datasets, returning stats for each. 66 | /// 67 | /// Iterates through every dataset beneath each of the given pools, or 68 | /// through all datasets if no pool is supplied. 69 | pub fn iter(pool: Option<&str>) -> Result> { 70 | SnapshotIter::new(pool) 71 | } 72 | } 73 | 74 | impl AddAssign<&Self> for Snapshot { 75 | fn add_assign(&mut self, other: &Self) { 76 | assert!( 77 | other.name.starts_with(&self.name), 78 | "Why would you want to combine two unrelated datasets?" 79 | ); 80 | self.nunlinked += other.nunlinked; 81 | self.nunlinks += other.nunlinks; 82 | self.nread += other.nread; 83 | self.reads += other.reads; 84 | self.nwritten += other.nwritten; 85 | self.writes += other.writes; 86 | } 87 | } 88 | 89 | #[derive(Default)] 90 | struct DataSource { 91 | children: bool, 92 | prev: BTreeMap, 93 | prev_ts: Option, 94 | cur: BTreeMap, 95 | cur_ts: Option, 96 | pools: Vec, 97 | } 98 | 99 | impl DataSource { 100 | fn new(children: bool, pools: Vec) -> Self { 101 | DataSource { 102 | children, 103 | pools, 104 | ..Default::default() 105 | } 106 | } 107 | 108 | /// Iterate through all the datasets, returning current stats 109 | fn iter(&mut self) -> impl Iterator + '_ { 110 | let etime = if let Some(prev_ts) = self.prev_ts.as_ref() { 111 | let delta = *self.cur_ts.as_ref().unwrap() - *prev_ts; 112 | delta.tv_sec() as f64 + delta.tv_nsec() as f64 * 1e-9 113 | } else { 114 | let boottime = clock_gettime(CLOCK_UPTIME).unwrap(); 115 | boottime.tv_sec() as f64 + boottime.tv_nsec() as f64 * 1e-9 116 | }; 117 | DataSourceIter { 118 | inner_iter: self.cur.iter(), 119 | ds: self, 120 | etime, 121 | } 122 | } 123 | 124 | /// Iterate over all of the names of parent datasets of the argument 125 | fn with_parents(s: &str) -> impl Iterator { 126 | s.char_indices().filter_map(move |(idx, c)| { 127 | if c == '/' { 128 | Some(s.split_at(idx).0) 129 | } else if idx == s.len() - 1 { 130 | Some(s) 131 | } else { 132 | None 133 | } 134 | }) 135 | } 136 | 137 | fn refresh(&mut self) -> Result<(), Box> { 138 | let now = clock_gettime(ClockId::CLOCK_MONOTONIC)?; 139 | self.prev = mem::take(&mut self.cur); 140 | self.prev_ts = self.cur_ts.replace(now); 141 | if self.pools.is_empty() { 142 | for rss in Snapshot::iter(None).unwrap() { 143 | let ss = rss?; 144 | Self::upsert(&mut self.cur, ss, self.children); 145 | } 146 | } else { 147 | for pool in self.pools.iter() { 148 | for rss in Snapshot::iter(Some(pool)).unwrap() { 149 | let ss = rss?; 150 | Self::upsert(&mut self.cur, ss, self.children); 151 | } 152 | } 153 | } 154 | Ok(()) 155 | } 156 | 157 | fn toggle_children(&mut self) -> Result<(), Box> { 158 | self.children ^= true; 159 | // Wipe out previous statistics. The next refresh will report stats 160 | // since boot. 161 | self.refresh()?; 162 | mem::take(&mut self.prev); 163 | self.prev_ts = None; 164 | Ok(()) 165 | } 166 | 167 | /// Insert a snapshot into `cur`, and/or update it and its parents 168 | fn upsert( 169 | cur: &mut BTreeMap, 170 | ss: Snapshot, 171 | children: bool, 172 | ) { 173 | if children { 174 | for dsname in Self::with_parents(&ss.name) { 175 | match cur.entry(dsname.to_string()) { 176 | btree_map::Entry::Vacant(ve) => { 177 | if ss.name == dsname { 178 | ve.insert(ss.clone()); 179 | } else { 180 | let mut parent_ss = ss.clone(); 181 | parent_ss.name = dsname.to_string(); 182 | ve.insert(parent_ss); 183 | } 184 | } 185 | btree_map::Entry::Occupied(mut oe) => { 186 | *oe.get_mut() += &ss; 187 | } 188 | } 189 | } 190 | } else { 191 | match cur.entry(ss.name.clone()) { 192 | btree_map::Entry::Vacant(ve) => { 193 | ve.insert(ss); 194 | } 195 | btree_map::Entry::Occupied(mut oe) => { 196 | *oe.get_mut() += &ss; 197 | } 198 | } 199 | }; 200 | } 201 | } 202 | 203 | struct DataSourceIter<'a> { 204 | inner_iter: btree_map::Iter<'a, String, Snapshot>, 205 | ds: &'a DataSource, 206 | etime: f64, 207 | } 208 | 209 | impl Iterator for DataSourceIter<'_> { 210 | type Item = Element; 211 | 212 | fn next(&mut self) -> Option { 213 | self.inner_iter 214 | .next() 215 | .map(|(_, ss)| ss.compute(self.ds.prev.get(&ss.name), self.etime)) 216 | } 217 | } 218 | 219 | /// One thing to display in the table 220 | #[derive(Clone, Debug)] 221 | pub struct Element { 222 | pub name: String, 223 | /// Read IOPs 224 | pub ops_r: f64, 225 | /// Read B/s 226 | pub r_s: f64, 227 | /// Files unlinked per second 228 | pub ops_unlink: f64, 229 | /// Write IOPs 230 | pub ops_w: f64, 231 | /// Write B/s 232 | pub w_s: f64, 233 | } 234 | 235 | #[derive(Default)] 236 | pub struct App { 237 | auto: bool, 238 | data: DataSource, 239 | depth: Option, 240 | filter: Option, 241 | reverse: bool, 242 | should_quit: bool, 243 | /// 0-based index of the column to sort by, if any 244 | sort_idx: Option, 245 | } 246 | 247 | impl App { 248 | pub fn new( 249 | auto: bool, 250 | children: bool, 251 | pools: Vec, 252 | depth: Option, 253 | filter: Option, 254 | reverse: bool, 255 | sort_idx: Option, 256 | ) -> Self { 257 | let mut data = DataSource::new(children, pools); 258 | data.refresh().unwrap(); 259 | App { 260 | auto, 261 | data, 262 | depth, 263 | filter, 264 | reverse, 265 | sort_idx, 266 | ..Default::default() 267 | } 268 | } 269 | 270 | pub fn clear_filter(&mut self) { 271 | self.filter = None; 272 | } 273 | 274 | /// Return the elements that should be displayed, in order 275 | #[rustfmt::skip] 276 | pub fn elements(&mut self) -> Vec { 277 | let auto = self.auto; 278 | let depth = self.depth; 279 | let filter = &self.filter; 280 | let mut v = self.data.iter() 281 | .filter(move |elem| { 282 | if let Some(limit) = depth { 283 | let edepth = elem.name.split('/').count() - 1; 284 | edepth <= limit 285 | } else { 286 | true 287 | } 288 | }).filter(|elem| 289 | filter.as_ref() 290 | .map(|f| f.is_match(&elem.name)) 291 | .unwrap_or(true) 292 | ).filter(|elem| !auto || 293 | (elem.r_s + elem.w_s + elem.ops_unlink > 1.0) 294 | ).collect::>(); 295 | match (self.reverse, self.sort_idx) { 296 | (true, Some(0)) => v.sort_by(|x, y| x.ops_r.total_cmp(&y.ops_r)), 297 | (false, Some(0)) => v.sort_by(|x, y| y.ops_r.total_cmp(&x.ops_r)), 298 | (true, Some(1)) => v.sort_by(|x, y| x.r_s.total_cmp(&y.r_s)), 299 | (false, Some(1)) => v.sort_by(|x, y| y.r_s.total_cmp(&x.r_s)), 300 | (true, Some(2)) => v.sort_by(|x, y| x.ops_w.total_cmp(&y.ops_w)), 301 | (false, Some(2)) => v.sort_by(|x, y| y.ops_w.total_cmp(&x.ops_w)), 302 | (true, Some(3)) => v.sort_by(|x, y| x.w_s.total_cmp(&y.w_s)), 303 | (false, Some(3)) => v.sort_by(|x, y| y.w_s.total_cmp(&x.w_s)), 304 | (true, Some(4)) => v.sort_by(|x, y| 305 | x.ops_unlink.total_cmp(&y.ops_unlink)), 306 | (false, Some(4)) => v.sort_by(|x, y| 307 | y.ops_unlink.total_cmp(&x.ops_unlink)), 308 | (false, Some(5)) => v.sort_by(|x, y| x.name.cmp(&y.name)), 309 | (true, Some(5)) => v.sort_by(|x, y| y.name.cmp(&x.name)), 310 | _ => () 311 | } 312 | v 313 | } 314 | 315 | pub fn on_a(&mut self) { 316 | self.auto ^= true; 317 | } 318 | 319 | pub fn on_c(&mut self) -> Result<(), Box> { 320 | self.data.toggle_children() 321 | } 322 | 323 | pub fn on_d(&mut self, more_depth: bool) { 324 | self.depth = if more_depth { 325 | match self.depth { 326 | None => Some(1), 327 | Some(x) => Some(x + 1), 328 | } 329 | } else { 330 | match self.depth { 331 | None => Some(0), 332 | Some(x) => Some(x.saturating_sub(1)), 333 | } 334 | } 335 | } 336 | 337 | pub fn on_minus(&mut self) { 338 | self.sort_idx = match self.sort_idx { 339 | Some(0) => None, 340 | Some(old) => Some(old - 1), 341 | None => Some(5), 342 | } 343 | } 344 | 345 | pub fn on_plus(&mut self) { 346 | self.sort_idx = match self.sort_idx { 347 | Some(old) if old >= 5 => None, 348 | Some(old) => Some(old + 1), 349 | None => Some(0), 350 | } 351 | } 352 | 353 | pub fn on_q(&mut self) { 354 | self.should_quit = true; 355 | } 356 | 357 | pub fn on_r(&mut self) { 358 | self.reverse ^= true; 359 | } 360 | 361 | pub fn on_tick(&mut self) { 362 | self.data.refresh().unwrap(); 363 | } 364 | 365 | pub fn set_filter(&mut self, filter: Regex) { 366 | self.filter = Some(filter); 367 | } 368 | 369 | pub fn should_quit(&self) -> bool { 370 | self.should_quit 371 | } 372 | 373 | pub fn sort_idx(&self) -> Option { 374 | self.sort_idx 375 | } 376 | } 377 | 378 | #[cfg(test)] 379 | mod t { 380 | mod with_parents { 381 | use super::super::*; 382 | 383 | /// The empty string is not a valid dataset, but make sure nothing bad 384 | /// happens anyway 385 | #[test] 386 | fn empty() { 387 | let ds = ""; 388 | let mut actual = DataSource::with_parents(ds); 389 | assert!(actual.next().is_none()); 390 | } 391 | 392 | #[test] 393 | fn pool() { 394 | let ds = "zroot"; 395 | let expected = ["zroot"]; 396 | let actual = DataSource::with_parents(ds).collect::>(); 397 | assert_eq!(&expected[..], &actual[..]); 398 | } 399 | 400 | #[test] 401 | fn one_level() { 402 | let ds = "zroot/ROOT"; 403 | let expected = ["zroot", "zroot/ROOT"]; 404 | let actual = DataSource::with_parents(ds).collect::>(); 405 | assert_eq!(&expected[..], &actual[..]); 406 | } 407 | 408 | #[test] 409 | fn two_levels() { 410 | let ds = "zroot/ROOT/13.0-RELEASE"; 411 | let expected = ["zroot", "zroot/ROOT", "zroot/ROOT/13.0-RELEASE"]; 412 | let actual = DataSource::with_parents(ds).collect::>(); 413 | assert_eq!(&expected[..], &actual[..]); 414 | } 415 | } 416 | } 417 | -------------------------------------------------------------------------------- /src/app/freebsd.rs: -------------------------------------------------------------------------------- 1 | // vim: tw=80 2 | use std::{error::Error, mem}; 3 | 4 | use cfg_if::cfg_if; 5 | use sysctl::{Ctl, CtlIter, CtlValue, Sysctl, SysctlError}; 6 | 7 | use super::Snapshot; 8 | 9 | cfg_if! { 10 | if #[cfg(debug_assertions)] { 11 | macro_rules! debug_println { 12 | ($($tokens:tt),*) => { 13 | eprintln!($($tokens),*) 14 | } 15 | } 16 | } else { 17 | macro_rules! debug_println { 18 | ($($tokens:tt),*) => {()} 19 | } 20 | } 21 | } 22 | 23 | #[derive(Default)] 24 | struct Builder { 25 | dataset_name: Option, 26 | nunlinked: Option, 27 | nunlinks: Option, 28 | nread: Option, 29 | reads: Option, 30 | nwritten: Option, 31 | writes: Option, 32 | } 33 | 34 | impl Builder { 35 | fn build(&mut self, name: &str, value: CtlValue) { 36 | let mut fields = name.split('.'); 37 | let field = fields.nth(5).unwrap(); 38 | match value { 39 | CtlValue::String(s) => { 40 | if field != "dataset_name" { 41 | debug_println!("Unknown sysctl {:?}", name); 42 | } 43 | assert_eq!(self.dataset_name.replace(s), None); 44 | } 45 | CtlValue::U64(x) => match field { 46 | "nunlinked" => { 47 | self.nunlinked = Some(x); 48 | } 49 | "nunlinks" => { 50 | self.nunlinks = Some(x); 51 | } 52 | "nread" => { 53 | self.nread = Some(x); 54 | } 55 | "reads" => { 56 | self.reads = Some(x); 57 | } 58 | "nwritten" => { 59 | self.nwritten = Some(x); 60 | } 61 | "writes" => { 62 | self.writes = Some(x); 63 | } 64 | _ => { 65 | /* The zil_ stats aren't interesting to ztop */ 66 | if !name.contains(".zil_") { 67 | debug_println!("Unknown sysctl {:?}", name); 68 | } 69 | } 70 | }, 71 | _ => debug_println!("Unknown sysctl {:?}", name), 72 | }; 73 | } 74 | 75 | fn finish(mut self) -> Option { 76 | let name = self.dataset_name.take()?; 77 | // On FreeBSD 12.2 and earlier, unlinked and nunlinks will not be 78 | // present. Set them to zero. 79 | let nunlinked = self.nunlinked.take().unwrap_or(0); 80 | let nunlinks = self.nunlinks.take().unwrap_or(0); 81 | let nread = self.nread.take()?; 82 | let reads = self.reads.take()?; 83 | let nwritten = self.nwritten.take()?; 84 | let writes = self.writes.take()?; 85 | Some(Snapshot { 86 | name, 87 | nunlinked, 88 | nunlinks, 89 | nread, 90 | reads, 91 | nwritten, 92 | writes, 93 | }) 94 | } 95 | } 96 | 97 | pub(super) struct SnapshotIter { 98 | inner: Box>>, 99 | finished: bool, 100 | builder: Builder, 101 | last: Option<(String, String)>, 102 | } 103 | 104 | impl SnapshotIter { 105 | pub(crate) fn new(pool: Option<&str>) -> Result> { 106 | Ok(Self::with_inner(SysctlIter::new(pool))) 107 | } 108 | 109 | fn with_inner(inner: T) -> Self 110 | where 111 | T: Iterator> + 'static, 112 | { 113 | SnapshotIter { 114 | inner: Box::new(inner), 115 | finished: false, 116 | builder: Builder::default(), 117 | last: None, 118 | } 119 | } 120 | 121 | /// Progressively build the next Snapshot 122 | /// 123 | /// # Returns 124 | /// 125 | /// If all of the sysctls relevant to the snapshot have been received, 126 | /// returns `Some(snapshot)` and prepares `self` to build the next Snapshot. 127 | fn build(&mut self, name: String, value: CtlValue) -> Option { 128 | let mut fields = name.split('.'); 129 | let pool = fields.nth(2).unwrap(); 130 | let on = fields.nth(1).unwrap(); 131 | match &self.last { 132 | None => { 133 | self.builder.build(&name, value); 134 | self.last = Some((on.to_owned(), pool.to_owned())); 135 | None 136 | } 137 | Some((son, spool)) if son == on && pool == spool => { 138 | self.builder.build(&name, value); 139 | None 140 | } 141 | _ => { 142 | self.last = Some((on.to_owned(), pool.to_owned())); 143 | let new = Builder::default(); 144 | let old = mem::replace(&mut self.builder, new); 145 | self.builder.build(&name, value); 146 | old.finish() 147 | } 148 | } 149 | } 150 | } 151 | 152 | impl Iterator for SnapshotIter { 153 | type Item = Result>; 154 | 155 | fn next(&mut self) -> Option { 156 | // We need to read several values from the internal iterator to assemble 157 | // a Snapshot. We can't rely on them always being returned in the same 158 | // order. 159 | if self.finished { 160 | return None; 161 | } 162 | loop { 163 | match self.inner.next() { 164 | Some(Ok((name, value))) => { 165 | if let Some(snapshot) = self.build(name, value) { 166 | break Some(Ok(snapshot)); 167 | } 168 | // else continue 169 | } 170 | Some(Err(e)) => break Some(Err(Box::new(e))), 171 | None => { 172 | self.finished = true; 173 | let new = Builder::default(); 174 | let old = mem::replace(&mut self.builder, new); 175 | break old.finish().map(Ok); 176 | } 177 | } 178 | } 179 | } 180 | } 181 | 182 | /// Iterate through all of the sysctls, but only return the ones we care about. 183 | struct SysctlIter(CtlIter); 184 | 185 | impl SysctlIter { 186 | fn new(pool: Option<&str>) -> Self { 187 | let root = if let Some(s) = pool { 188 | Ctl::new(&format!("kstat.zfs.{}.dataset", s.replace('.', "%25"))) 189 | .unwrap_or_else(|_e| { 190 | eprintln!("Statistics not found for pool {s}"); 191 | std::process::exit(1); 192 | }) 193 | } else { 194 | Ctl::new("kstat.zfs").unwrap_or_else(|_e| { 195 | eprintln!("ZFS kernel module not loaded?"); 196 | std::process::exit(1); 197 | }) 198 | }; 199 | Self(CtlIter::below(root)) 200 | } 201 | } 202 | 203 | impl Iterator for SysctlIter { 204 | type Item = Result<(String, CtlValue), SysctlError>; 205 | 206 | /// Return the next Ctl that ztop cares about 207 | fn next(&mut self) -> Option { 208 | loop { 209 | match self.0.next() { 210 | Some(Ok(ctl)) => match ctl.name() { 211 | Ok(name) => { 212 | if name 213 | .splitn(4, '.') 214 | .last() 215 | .map(|l| l.starts_with("dataset")) 216 | .unwrap_or(false) 217 | { 218 | break Some(ctl.value().map(|v| (name, v))); 219 | } else { 220 | continue; 221 | } 222 | } 223 | Err(e) => { 224 | return Some(Err(e)); 225 | } 226 | }, 227 | Some(Err(e)) => { 228 | return Some(Err(e)); 229 | } 230 | None => { 231 | return None; 232 | } 233 | } 234 | } 235 | } 236 | } 237 | 238 | #[cfg(test)] 239 | mod t { 240 | mod builder { 241 | use super::super::*; 242 | 243 | #[test] 244 | fn like_freebsd_12_2() { 245 | let names = vec![ 246 | "kstat.zfs.tank.dataset.objset-0x58c.nread", 247 | "kstat.zfs.tank.dataset.objset-0x58c.reads", 248 | "kstat.zfs.tank.dataset.objset-0x58c.nwritten", 249 | "kstat.zfs.tank.dataset.objset-0x58c.writes", 250 | "kstat.zfs.tank.dataset.objset-0x58c.dataset_name", 251 | ] 252 | .into_iter(); 253 | let values = vec![ 254 | CtlValue::U64(3), 255 | CtlValue::U64(4), 256 | CtlValue::U64(5), 257 | CtlValue::U64(6), 258 | CtlValue::String("tank/foo".to_owned()), 259 | ] 260 | .into_iter(); 261 | let mut builder = Builder::default(); 262 | for (n, v) in names.zip(values) { 263 | builder.build(n, v); 264 | } 265 | let r = builder.finish().unwrap(); 266 | assert_eq!(r.name, "tank/foo"); 267 | assert_eq!(r.nunlinked, 0); 268 | assert_eq!(r.nunlinks, 0); 269 | assert_eq!(r.nread, 3); 270 | assert_eq!(r.reads, 4); 271 | assert_eq!(r.nwritten, 5); 272 | assert_eq!(r.writes, 6); 273 | } 274 | 275 | #[test] 276 | fn like_freebsd_13_0() { 277 | let names = vec![ 278 | "kstat.zfs.tank.dataset.objset-0x58c.nunlinked", 279 | "kstat.zfs.tank.dataset.objset-0x58c.nunlinks", 280 | "kstat.zfs.tank.dataset.objset-0x58c.nread", 281 | "kstat.zfs.tank.dataset.objset-0x58c.reads", 282 | "kstat.zfs.tank.dataset.objset-0x58c.nwritten", 283 | "kstat.zfs.tank.dataset.objset-0x58c.writes", 284 | "kstat.zfs.tank.dataset.objset-0x58c.dataset_name", 285 | ] 286 | .into_iter(); 287 | let values = vec![ 288 | CtlValue::U64(1), 289 | CtlValue::U64(2), 290 | CtlValue::U64(3), 291 | CtlValue::U64(4), 292 | CtlValue::U64(5), 293 | CtlValue::U64(6), 294 | CtlValue::String("tank/foo".to_owned()), 295 | ] 296 | .into_iter(); 297 | let mut builder = Builder::default(); 298 | for (n, v) in names.zip(values) { 299 | builder.build(n, v); 300 | } 301 | let r = builder.finish().unwrap(); 302 | assert_eq!(r.name, "tank/foo"); 303 | assert_eq!(r.nunlinked, 1); 304 | assert_eq!(r.nunlinks, 2); 305 | assert_eq!(r.nread, 3); 306 | assert_eq!(r.reads, 4); 307 | assert_eq!(r.nwritten, 5); 308 | assert_eq!(r.writes, 6); 309 | } 310 | } 311 | 312 | mod snapshot_iter { 313 | use super::super::*; 314 | 315 | /// No datasets are present 316 | #[test] 317 | fn empty() { 318 | let kv = std::iter::empty(); 319 | let mut iter = SnapshotIter::with_inner(kv); 320 | assert!(iter.next().is_none()); 321 | } 322 | 323 | #[test] 324 | fn like_freebsd_12_2() { 325 | let kv = vec![ 326 | ( 327 | "kstat.zfs.tank.dataset.objset-0x58c.nread".to_string(), 328 | CtlValue::U64(1), 329 | ), 330 | ( 331 | "kstat.zfs.tank.dataset.objset-0x58c.reads".to_string(), 332 | CtlValue::U64(2), 333 | ), 334 | ( 335 | "kstat.zfs.tank.dataset.objset-0x58c.nwritten".to_string(), 336 | CtlValue::U64(3), 337 | ), 338 | ( 339 | "kstat.zfs.tank.dataset.objset-0x58c.writes".to_string(), 340 | CtlValue::U64(4), 341 | ), 342 | ( 343 | "kstat.zfs.tank.dataset.objset-0x58c.dataset_name" 344 | .to_string(), 345 | CtlValue::String("tank/foo".to_string()), 346 | ), 347 | ( 348 | "kstat.zfs.tank.dataset.objset-0x58d.nread".to_string(), 349 | CtlValue::U64(11), 350 | ), 351 | ( 352 | "kstat.zfs.tank.dataset.objset-0x58d.reads".to_string(), 353 | CtlValue::U64(12), 354 | ), 355 | ( 356 | "kstat.zfs.tank.dataset.objset-0x58d.nwritten".to_string(), 357 | CtlValue::U64(13), 358 | ), 359 | ( 360 | "kstat.zfs.tank.dataset.objset-0x58d.writes".to_string(), 361 | CtlValue::U64(14), 362 | ), 363 | ( 364 | "kstat.zfs.tank.dataset.objset-0x58d.dataset_name" 365 | .to_string(), 366 | CtlValue::String("tank/bar".to_string()), 367 | ), 368 | ] 369 | .into_iter() 370 | .map(Ok); 371 | let mut iter = SnapshotIter::with_inner(kv); 372 | let ss = iter.next().unwrap().unwrap(); 373 | assert_eq!(ss.name, "tank/foo"); 374 | assert_eq!(ss.nunlinked, 0); 375 | assert_eq!(ss.nunlinks, 0); 376 | assert_eq!(ss.nread, 1); 377 | assert_eq!(ss.reads, 2); 378 | assert_eq!(ss.nwritten, 3); 379 | assert_eq!(ss.writes, 4); 380 | let ss = iter.next().unwrap().unwrap(); 381 | assert_eq!(ss.name, "tank/bar"); 382 | assert_eq!(ss.nunlinked, 0); 383 | assert_eq!(ss.nunlinks, 0); 384 | assert_eq!(ss.nread, 11); 385 | assert_eq!(ss.reads, 12); 386 | assert_eq!(ss.nwritten, 13); 387 | assert_eq!(ss.writes, 14); 388 | assert!(iter.next().is_none()); 389 | } 390 | 391 | #[test] 392 | fn like_freebsd_13_0() { 393 | let kv = vec![ 394 | ( 395 | "kstat.zfs.tank.dataset.objset-0x58c.nunlinked".to_string(), 396 | CtlValue::U64(5), 397 | ), 398 | ( 399 | "kstat.zfs.tank.dataset.objset-0x58c.nunlinks".to_string(), 400 | CtlValue::U64(6), 401 | ), 402 | ( 403 | "kstat.zfs.tank.dataset.objset-0x58c.nread".to_string(), 404 | CtlValue::U64(1), 405 | ), 406 | ( 407 | "kstat.zfs.tank.dataset.objset-0x58c.reads".to_string(), 408 | CtlValue::U64(2), 409 | ), 410 | ( 411 | "kstat.zfs.tank.dataset.objset-0x58c.nwritten".to_string(), 412 | CtlValue::U64(3), 413 | ), 414 | ( 415 | "kstat.zfs.tank.dataset.objset-0x58c.writes".to_string(), 416 | CtlValue::U64(4), 417 | ), 418 | ( 419 | "kstat.zfs.tank.dataset.objset-0x58c.dataset_name" 420 | .to_string(), 421 | CtlValue::String("tank/foo".to_string()), 422 | ), 423 | ( 424 | "kstat.zfs.tank.dataset.objset-0x58d.nunlinked".to_string(), 425 | CtlValue::U64(15), 426 | ), 427 | ( 428 | "kstat.zfs.tank.dataset.objset-0x58d.nunlinks".to_string(), 429 | CtlValue::U64(16), 430 | ), 431 | ( 432 | "kstat.zfs.tank.dataset.objset-0x58d.nread".to_string(), 433 | CtlValue::U64(11), 434 | ), 435 | ( 436 | "kstat.zfs.tank.dataset.objset-0x58d.reads".to_string(), 437 | CtlValue::U64(12), 438 | ), 439 | ( 440 | "kstat.zfs.tank.dataset.objset-0x58d.nwritten".to_string(), 441 | CtlValue::U64(13), 442 | ), 443 | ( 444 | "kstat.zfs.tank.dataset.objset-0x58d.writes".to_string(), 445 | CtlValue::U64(14), 446 | ), 447 | ( 448 | "kstat.zfs.tank.dataset.objset-0x58d.dataset_name" 449 | .to_string(), 450 | CtlValue::String("tank/bar".to_string()), 451 | ), 452 | ] 453 | .into_iter() 454 | .map(Ok); 455 | let mut iter = SnapshotIter::with_inner(kv); 456 | let ss = iter.next().unwrap().unwrap(); 457 | assert_eq!(ss.name, "tank/foo"); 458 | assert_eq!(ss.nunlinked, 5); 459 | assert_eq!(ss.nunlinks, 6); 460 | assert_eq!(ss.nread, 1); 461 | assert_eq!(ss.reads, 2); 462 | assert_eq!(ss.nwritten, 3); 463 | assert_eq!(ss.writes, 4); 464 | let ss = iter.next().unwrap().unwrap(); 465 | assert_eq!(ss.name, "tank/bar"); 466 | assert_eq!(ss.nunlinked, 15); 467 | assert_eq!(ss.nunlinks, 16); 468 | assert_eq!(ss.nread, 11); 469 | assert_eq!(ss.reads, 12); 470 | assert_eq!(ss.nwritten, 13); 471 | assert_eq!(ss.writes, 14); 472 | assert!(iter.next().is_none()); 473 | } 474 | 475 | /// If the sysctls progress from one pool to another but the objset 476 | /// number doesn't change, we must still finish the Builder 477 | #[test] 478 | fn same_objset_two_pools() { 479 | let kv = vec![ 480 | ( 481 | "kstat.zfs.tank.dataset.objset-0x36.nunlinked".to_string(), 482 | CtlValue::U64(1), 483 | ), 484 | ( 485 | "kstat.zfs.tank.dataset.objset-0x36.nunlinks".to_string(), 486 | CtlValue::U64(2), 487 | ), 488 | ( 489 | "kstat.zfs.tank.dataset.objset-0x36.nread".to_string(), 490 | CtlValue::U64(3), 491 | ), 492 | ( 493 | "kstat.zfs.tank.dataset.objset-0x36.reads".to_string(), 494 | CtlValue::U64(4), 495 | ), 496 | ( 497 | "kstat.zfs.tank.dataset.objset-0x36.nwritten".to_string(), 498 | CtlValue::U64(5), 499 | ), 500 | ( 501 | "kstat.zfs.tank.dataset.objset-0x36.writes".to_string(), 502 | CtlValue::U64(6), 503 | ), 504 | ( 505 | "kstat.zfs.tank.dataset.objset-0x36.dataset_name" 506 | .to_string(), 507 | CtlValue::String("tank/foo".to_string()), 508 | ), 509 | ( 510 | "kstat.zfs.zroot.dataset.objset-0x36.nunlinked".to_string(), 511 | CtlValue::U64(1), 512 | ), 513 | ( 514 | "kstat.zfs.zroot.dataset.objset-0x36.dataset_name" 515 | .to_string(), 516 | CtlValue::String("zroot/bar".to_string()), 517 | ), 518 | ] 519 | .into_iter() 520 | .map(Ok); 521 | let mut iter = SnapshotIter::with_inner(kv); 522 | let ss = iter.next().unwrap().unwrap(); 523 | assert_eq!(ss.name, "tank/foo"); 524 | assert_eq!(ss.nunlinked, 1); 525 | assert_eq!(ss.nunlinks, 2); 526 | assert_eq!(ss.nread, 3); 527 | assert_eq!(ss.reads, 4); 528 | assert_eq!(ss.nwritten, 5); 529 | assert_eq!(ss.writes, 6); 530 | } 531 | } 532 | } 533 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "0.7.18" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "allocator-api2" 16 | version = "0.2.16" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" 19 | 20 | [[package]] 21 | name = "anstream" 22 | version = "0.6.18" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 25 | dependencies = [ 26 | "anstyle", 27 | "anstyle-parse", 28 | "anstyle-query", 29 | "anstyle-wincon", 30 | "colorchoice", 31 | "is_terminal_polyfill", 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle" 37 | version = "1.0.10" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 40 | 41 | [[package]] 42 | name = "anstyle-parse" 43 | version = "0.2.1" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" 46 | dependencies = [ 47 | "utf8parse", 48 | ] 49 | 50 | [[package]] 51 | name = "anstyle-query" 52 | version = "1.0.0" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" 55 | dependencies = [ 56 | "windows-sys 0.48.0", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-wincon" 61 | version = "3.0.6" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" 64 | dependencies = [ 65 | "anstyle", 66 | "windows-sys 0.59.0", 67 | ] 68 | 69 | [[package]] 70 | name = "autocfg" 71 | version = "1.1.0" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 74 | 75 | [[package]] 76 | name = "bitflags" 77 | version = "1.3.2" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 80 | 81 | [[package]] 82 | name = "bitflags" 83 | version = "2.9.1" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 86 | 87 | [[package]] 88 | name = "byteorder" 89 | version = "1.4.3" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 92 | 93 | [[package]] 94 | name = "castaway" 95 | version = "0.2.3" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" 98 | dependencies = [ 99 | "rustversion", 100 | ] 101 | 102 | [[package]] 103 | name = "cfg-if" 104 | version = "1.0.0" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 107 | 108 | [[package]] 109 | name = "clap" 110 | version = "4.5.23" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" 113 | dependencies = [ 114 | "clap_builder", 115 | "clap_derive", 116 | ] 117 | 118 | [[package]] 119 | name = "clap_builder" 120 | version = "4.5.23" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" 123 | dependencies = [ 124 | "anstream", 125 | "anstyle", 126 | "clap_lex", 127 | "strsim", 128 | ] 129 | 130 | [[package]] 131 | name = "clap_derive" 132 | version = "4.5.18" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" 135 | dependencies = [ 136 | "heck", 137 | "proc-macro2", 138 | "quote", 139 | "syn", 140 | ] 141 | 142 | [[package]] 143 | name = "clap_lex" 144 | version = "0.7.4" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 147 | 148 | [[package]] 149 | name = "colorchoice" 150 | version = "1.0.0" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 153 | 154 | [[package]] 155 | name = "compact_str" 156 | version = "0.9.0" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" 159 | dependencies = [ 160 | "castaway", 161 | "cfg-if", 162 | "itoa", 163 | "rustversion", 164 | "ryu", 165 | "static_assertions", 166 | ] 167 | 168 | [[package]] 169 | name = "convert_case" 170 | version = "0.7.1" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" 173 | dependencies = [ 174 | "unicode-segmentation", 175 | ] 176 | 177 | [[package]] 178 | name = "crossterm" 179 | version = "0.29.0" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" 182 | dependencies = [ 183 | "bitflags 2.9.1", 184 | "crossterm_winapi", 185 | "derive_more", 186 | "document-features", 187 | "mio", 188 | "parking_lot", 189 | "rustix", 190 | "signal-hook", 191 | "signal-hook-mio", 192 | "winapi", 193 | ] 194 | 195 | [[package]] 196 | name = "crossterm_winapi" 197 | version = "0.9.1" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 200 | dependencies = [ 201 | "winapi", 202 | ] 203 | 204 | [[package]] 205 | name = "darling" 206 | version = "0.20.11" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" 209 | dependencies = [ 210 | "darling_core", 211 | "darling_macro", 212 | ] 213 | 214 | [[package]] 215 | name = "darling_core" 216 | version = "0.20.11" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" 219 | dependencies = [ 220 | "fnv", 221 | "ident_case", 222 | "proc-macro2", 223 | "quote", 224 | "strsim", 225 | "syn", 226 | ] 227 | 228 | [[package]] 229 | name = "darling_macro" 230 | version = "0.20.11" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" 233 | dependencies = [ 234 | "darling_core", 235 | "quote", 236 | "syn", 237 | ] 238 | 239 | [[package]] 240 | name = "deranged" 241 | version = "0.4.0" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" 244 | dependencies = [ 245 | "powerfmt", 246 | ] 247 | 248 | [[package]] 249 | name = "derive_more" 250 | version = "2.0.1" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" 253 | dependencies = [ 254 | "derive_more-impl", 255 | ] 256 | 257 | [[package]] 258 | name = "derive_more-impl" 259 | version = "2.0.1" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" 262 | dependencies = [ 263 | "convert_case", 264 | "proc-macro2", 265 | "quote", 266 | "syn", 267 | ] 268 | 269 | [[package]] 270 | name = "document-features" 271 | version = "0.2.11" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" 274 | dependencies = [ 275 | "litrs", 276 | ] 277 | 278 | [[package]] 279 | name = "either" 280 | version = "1.9.0" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" 283 | 284 | [[package]] 285 | name = "enum-as-inner" 286 | version = "0.6.1" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" 289 | dependencies = [ 290 | "heck", 291 | "proc-macro2", 292 | "quote", 293 | "syn", 294 | ] 295 | 296 | [[package]] 297 | name = "equivalent" 298 | version = "1.0.2" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 301 | 302 | [[package]] 303 | name = "errno" 304 | version = "0.3.13" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" 307 | dependencies = [ 308 | "libc", 309 | "windows-sys 0.59.0", 310 | ] 311 | 312 | [[package]] 313 | name = "fnv" 314 | version = "1.0.7" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 317 | 318 | [[package]] 319 | name = "foldhash" 320 | version = "0.1.5" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 323 | 324 | [[package]] 325 | name = "glob" 326 | version = "0.3.0" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" 329 | 330 | [[package]] 331 | name = "hashbrown" 332 | version = "0.15.4" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" 335 | dependencies = [ 336 | "allocator-api2", 337 | "equivalent", 338 | "foldhash", 339 | ] 340 | 341 | [[package]] 342 | name = "heck" 343 | version = "0.5.0" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 346 | 347 | [[package]] 348 | name = "humanize-rs" 349 | version = "0.1.5" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "016b02deb8b0c415d8d56a6f0ab265e50c22df61194e37f9be75ed3a722de8a6" 352 | 353 | [[package]] 354 | name = "ident_case" 355 | version = "1.0.1" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 358 | 359 | [[package]] 360 | name = "indoc" 361 | version = "2.0.6" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" 364 | 365 | [[package]] 366 | name = "instability" 367 | version = "0.3.8" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "205c87b35a7493e8358617fcd8a18971af74bf4e90c18fa794d1455bf37c753d" 370 | dependencies = [ 371 | "darling", 372 | "indoc", 373 | "proc-macro2", 374 | "quote", 375 | "syn", 376 | ] 377 | 378 | [[package]] 379 | name = "is_terminal_polyfill" 380 | version = "1.70.1" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 383 | 384 | [[package]] 385 | name = "itertools" 386 | version = "0.13.0" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 389 | dependencies = [ 390 | "either", 391 | ] 392 | 393 | [[package]] 394 | name = "itertools" 395 | version = "0.14.0" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" 398 | dependencies = [ 399 | "either", 400 | ] 401 | 402 | [[package]] 403 | name = "itoa" 404 | version = "1.0.14" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 407 | 408 | [[package]] 409 | name = "kasuari" 410 | version = "0.4.7" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "96d9e0d6a8bf886abccc1cbcac7d74f9000ae5882aedc4a9042188bbc9cd4487" 413 | dependencies = [ 414 | "hashbrown", 415 | "thiserror 2.0.12", 416 | ] 417 | 418 | [[package]] 419 | name = "libc" 420 | version = "0.2.174" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" 423 | 424 | [[package]] 425 | name = "libredox" 426 | version = "0.1.4" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "1580801010e535496706ba011c15f8532df6b42297d2e471fec38ceadd8c0638" 429 | dependencies = [ 430 | "bitflags 2.9.1", 431 | "libc", 432 | "redox_syscall 0.5.13", 433 | ] 434 | 435 | [[package]] 436 | name = "line-clipping" 437 | version = "0.3.3" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "51a1679740111eb63b7b4cb3c97b1d5d9f82e142292a25edcfdb4120a48b3880" 440 | dependencies = [ 441 | "bitflags 2.9.1", 442 | ] 443 | 444 | [[package]] 445 | name = "linux-raw-sys" 446 | version = "0.9.4" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 449 | 450 | [[package]] 451 | name = "litrs" 452 | version = "0.4.1" 453 | source = "registry+https://github.com/rust-lang/crates.io-index" 454 | checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" 455 | 456 | [[package]] 457 | name = "lock_api" 458 | version = "0.4.11" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" 461 | dependencies = [ 462 | "autocfg", 463 | "scopeguard", 464 | ] 465 | 466 | [[package]] 467 | name = "log" 468 | version = "0.4.20" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 471 | 472 | [[package]] 473 | name = "lru" 474 | version = "0.14.0" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "9f8cc7106155f10bdf99a6f379688f543ad6596a415375b36a59a054ceda1198" 477 | dependencies = [ 478 | "hashbrown", 479 | ] 480 | 481 | [[package]] 482 | name = "memchr" 483 | version = "2.4.1" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" 486 | 487 | [[package]] 488 | name = "mio" 489 | version = "1.0.4" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" 492 | dependencies = [ 493 | "libc", 494 | "log", 495 | "wasi", 496 | "windows-sys 0.59.0", 497 | ] 498 | 499 | [[package]] 500 | name = "nix" 501 | version = "0.27.1" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" 504 | dependencies = [ 505 | "bitflags 2.9.1", 506 | "cfg-if", 507 | "libc", 508 | ] 509 | 510 | [[package]] 511 | name = "num-conv" 512 | version = "0.1.0" 513 | source = "registry+https://github.com/rust-lang/crates.io-index" 514 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 515 | 516 | [[package]] 517 | name = "num_threads" 518 | version = "0.1.7" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" 521 | dependencies = [ 522 | "libc", 523 | ] 524 | 525 | [[package]] 526 | name = "numtoa" 527 | version = "0.2.4" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "6aa2c4e539b869820a2b82e1aef6ff40aa85e65decdd5185e83fb4b1249cd00f" 530 | 531 | [[package]] 532 | name = "parking_lot" 533 | version = "0.12.1" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 536 | dependencies = [ 537 | "lock_api", 538 | "parking_lot_core", 539 | ] 540 | 541 | [[package]] 542 | name = "parking_lot_core" 543 | version = "0.9.9" 544 | source = "registry+https://github.com/rust-lang/crates.io-index" 545 | checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" 546 | dependencies = [ 547 | "cfg-if", 548 | "libc", 549 | "redox_syscall 0.4.1", 550 | "smallvec", 551 | "windows-targets 0.48.5", 552 | ] 553 | 554 | [[package]] 555 | name = "powerfmt" 556 | version = "0.2.0" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 559 | 560 | [[package]] 561 | name = "proc-macro2" 562 | version = "1.0.92" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" 565 | dependencies = [ 566 | "unicode-ident", 567 | ] 568 | 569 | [[package]] 570 | name = "quote" 571 | version = "1.0.40" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 574 | dependencies = [ 575 | "proc-macro2", 576 | ] 577 | 578 | [[package]] 579 | name = "ratatui" 580 | version = "0.30.0-alpha.5" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "71365e96fb8f1350c02908e788815c5a57c0c1f557673b274a94edee7a4fe001" 583 | dependencies = [ 584 | "instability", 585 | "ratatui-core", 586 | "ratatui-crossterm", 587 | "ratatui-termion", 588 | "ratatui-widgets", 589 | ] 590 | 591 | [[package]] 592 | name = "ratatui-core" 593 | version = "0.1.0-alpha.6" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "f836b2eac888da74162b680a8facdbe784ae73df3b0f711eef74bb90a7477f78" 596 | dependencies = [ 597 | "bitflags 2.9.1", 598 | "compact_str", 599 | "hashbrown", 600 | "indoc", 601 | "itertools 0.14.0", 602 | "kasuari", 603 | "lru", 604 | "strum", 605 | "thiserror 2.0.12", 606 | "unicode-segmentation", 607 | "unicode-truncate", 608 | "unicode-width", 609 | ] 610 | 611 | [[package]] 612 | name = "ratatui-crossterm" 613 | version = "0.1.0-alpha.5" 614 | source = "registry+https://github.com/rust-lang/crates.io-index" 615 | checksum = "22f4a90548bf8ed759d226d621d73561110db23aee7b7dc4e12c39ac7132062f" 616 | dependencies = [ 617 | "crossterm", 618 | "instability", 619 | "ratatui-core", 620 | ] 621 | 622 | [[package]] 623 | name = "ratatui-termion" 624 | version = "0.1.0-alpha.5" 625 | source = "registry+https://github.com/rust-lang/crates.io-index" 626 | checksum = "7185a3b43ee219d766d9e1c3420472d2b061adf86472c3d688697d07c3c6b93a" 627 | dependencies = [ 628 | "instability", 629 | "ratatui-core", 630 | "termion", 631 | ] 632 | 633 | [[package]] 634 | name = "ratatui-widgets" 635 | version = "0.3.0-alpha.5" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "388428527811be6da3e23157d951308d9eae4ce1b4d1d545a55673bbcdfb7326" 638 | dependencies = [ 639 | "bitflags 2.9.1", 640 | "hashbrown", 641 | "indoc", 642 | "instability", 643 | "itertools 0.14.0", 644 | "line-clipping", 645 | "ratatui-core", 646 | "strum", 647 | "time", 648 | "unicode-segmentation", 649 | "unicode-width", 650 | ] 651 | 652 | [[package]] 653 | name = "redox_syscall" 654 | version = "0.4.1" 655 | source = "registry+https://github.com/rust-lang/crates.io-index" 656 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" 657 | dependencies = [ 658 | "bitflags 1.3.2", 659 | ] 660 | 661 | [[package]] 662 | name = "redox_syscall" 663 | version = "0.5.13" 664 | source = "registry+https://github.com/rust-lang/crates.io-index" 665 | checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" 666 | dependencies = [ 667 | "bitflags 2.9.1", 668 | ] 669 | 670 | [[package]] 671 | name = "redox_termios" 672 | version = "0.1.3" 673 | source = "registry+https://github.com/rust-lang/crates.io-index" 674 | checksum = "20145670ba436b55d91fc92d25e71160fbfbdd57831631c8d7d36377a476f1cb" 675 | 676 | [[package]] 677 | name = "regex" 678 | version = "1.5.5" 679 | source = "registry+https://github.com/rust-lang/crates.io-index" 680 | checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286" 681 | dependencies = [ 682 | "aho-corasick", 683 | "memchr", 684 | "regex-syntax", 685 | ] 686 | 687 | [[package]] 688 | name = "regex-syntax" 689 | version = "0.6.25" 690 | source = "registry+https://github.com/rust-lang/crates.io-index" 691 | checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" 692 | 693 | [[package]] 694 | name = "rustix" 695 | version = "1.0.8" 696 | source = "registry+https://github.com/rust-lang/crates.io-index" 697 | checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" 698 | dependencies = [ 699 | "bitflags 2.9.1", 700 | "errno", 701 | "libc", 702 | "linux-raw-sys", 703 | "windows-sys 0.59.0", 704 | ] 705 | 706 | [[package]] 707 | name = "rustversion" 708 | version = "1.0.14" 709 | source = "registry+https://github.com/rust-lang/crates.io-index" 710 | checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" 711 | 712 | [[package]] 713 | name = "ryu" 714 | version = "1.0.18" 715 | source = "registry+https://github.com/rust-lang/crates.io-index" 716 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 717 | 718 | [[package]] 719 | name = "same-file" 720 | version = "1.0.6" 721 | source = "registry+https://github.com/rust-lang/crates.io-index" 722 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 723 | dependencies = [ 724 | "winapi-util", 725 | ] 726 | 727 | [[package]] 728 | name = "scopeguard" 729 | version = "1.2.0" 730 | source = "registry+https://github.com/rust-lang/crates.io-index" 731 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 732 | 733 | [[package]] 734 | name = "serde" 735 | version = "1.0.219" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 738 | dependencies = [ 739 | "serde_derive", 740 | ] 741 | 742 | [[package]] 743 | name = "serde_derive" 744 | version = "1.0.219" 745 | source = "registry+https://github.com/rust-lang/crates.io-index" 746 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 747 | dependencies = [ 748 | "proc-macro2", 749 | "quote", 750 | "syn", 751 | ] 752 | 753 | [[package]] 754 | name = "signal-hook" 755 | version = "0.3.17" 756 | source = "registry+https://github.com/rust-lang/crates.io-index" 757 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 758 | dependencies = [ 759 | "libc", 760 | "signal-hook-registry", 761 | ] 762 | 763 | [[package]] 764 | name = "signal-hook-mio" 765 | version = "0.2.4" 766 | source = "registry+https://github.com/rust-lang/crates.io-index" 767 | checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" 768 | dependencies = [ 769 | "libc", 770 | "mio", 771 | "signal-hook", 772 | ] 773 | 774 | [[package]] 775 | name = "signal-hook-registry" 776 | version = "1.4.1" 777 | source = "registry+https://github.com/rust-lang/crates.io-index" 778 | checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" 779 | dependencies = [ 780 | "libc", 781 | ] 782 | 783 | [[package]] 784 | name = "smallvec" 785 | version = "1.11.2" 786 | source = "registry+https://github.com/rust-lang/crates.io-index" 787 | checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" 788 | 789 | [[package]] 790 | name = "static_assertions" 791 | version = "1.1.0" 792 | source = "registry+https://github.com/rust-lang/crates.io-index" 793 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 794 | 795 | [[package]] 796 | name = "strsim" 797 | version = "0.11.1" 798 | source = "registry+https://github.com/rust-lang/crates.io-index" 799 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 800 | 801 | [[package]] 802 | name = "strum" 803 | version = "0.27.1" 804 | source = "registry+https://github.com/rust-lang/crates.io-index" 805 | checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" 806 | dependencies = [ 807 | "strum_macros", 808 | ] 809 | 810 | [[package]] 811 | name = "strum_macros" 812 | version = "0.27.1" 813 | source = "registry+https://github.com/rust-lang/crates.io-index" 814 | checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" 815 | dependencies = [ 816 | "heck", 817 | "proc-macro2", 818 | "quote", 819 | "rustversion", 820 | "syn", 821 | ] 822 | 823 | [[package]] 824 | name = "syn" 825 | version = "2.0.104" 826 | source = "registry+https://github.com/rust-lang/crates.io-index" 827 | checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" 828 | dependencies = [ 829 | "proc-macro2", 830 | "quote", 831 | "unicode-ident", 832 | ] 833 | 834 | [[package]] 835 | name = "sysctl" 836 | version = "0.5.5" 837 | source = "registry+https://github.com/rust-lang/crates.io-index" 838 | checksum = "ec7dddc5f0fee506baf8b9fdb989e242f17e4b11c61dfbb0635b705217199eea" 839 | dependencies = [ 840 | "bitflags 2.9.1", 841 | "byteorder", 842 | "enum-as-inner", 843 | "libc", 844 | "thiserror 1.0.48", 845 | "walkdir", 846 | ] 847 | 848 | [[package]] 849 | name = "termion" 850 | version = "4.0.5" 851 | source = "registry+https://github.com/rust-lang/crates.io-index" 852 | checksum = "3669a69de26799d6321a5aa713f55f7e2cd37bd47be044b50f2acafc42c122bb" 853 | dependencies = [ 854 | "libc", 855 | "libredox", 856 | "numtoa", 857 | "redox_termios", 858 | ] 859 | 860 | [[package]] 861 | name = "thiserror" 862 | version = "1.0.48" 863 | source = "registry+https://github.com/rust-lang/crates.io-index" 864 | checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" 865 | dependencies = [ 866 | "thiserror-impl 1.0.48", 867 | ] 868 | 869 | [[package]] 870 | name = "thiserror" 871 | version = "2.0.12" 872 | source = "registry+https://github.com/rust-lang/crates.io-index" 873 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 874 | dependencies = [ 875 | "thiserror-impl 2.0.12", 876 | ] 877 | 878 | [[package]] 879 | name = "thiserror-impl" 880 | version = "1.0.48" 881 | source = "registry+https://github.com/rust-lang/crates.io-index" 882 | checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" 883 | dependencies = [ 884 | "proc-macro2", 885 | "quote", 886 | "syn", 887 | ] 888 | 889 | [[package]] 890 | name = "thiserror-impl" 891 | version = "2.0.12" 892 | source = "registry+https://github.com/rust-lang/crates.io-index" 893 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 894 | dependencies = [ 895 | "proc-macro2", 896 | "quote", 897 | "syn", 898 | ] 899 | 900 | [[package]] 901 | name = "time" 902 | version = "0.3.41" 903 | source = "registry+https://github.com/rust-lang/crates.io-index" 904 | checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" 905 | dependencies = [ 906 | "deranged", 907 | "libc", 908 | "num-conv", 909 | "num_threads", 910 | "powerfmt", 911 | "serde", 912 | "time-core", 913 | ] 914 | 915 | [[package]] 916 | name = "time-core" 917 | version = "0.1.4" 918 | source = "registry+https://github.com/rust-lang/crates.io-index" 919 | checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" 920 | 921 | [[package]] 922 | name = "unicode-ident" 923 | version = "1.0.12" 924 | source = "registry+https://github.com/rust-lang/crates.io-index" 925 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 926 | 927 | [[package]] 928 | name = "unicode-segmentation" 929 | version = "1.10.1" 930 | source = "registry+https://github.com/rust-lang/crates.io-index" 931 | checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" 932 | 933 | [[package]] 934 | name = "unicode-truncate" 935 | version = "2.0.0" 936 | source = "registry+https://github.com/rust-lang/crates.io-index" 937 | checksum = "8fbf03860ff438702f3910ca5f28f8dac63c1c11e7efb5012b8b175493606330" 938 | dependencies = [ 939 | "itertools 0.13.0", 940 | "unicode-segmentation", 941 | "unicode-width", 942 | ] 943 | 944 | [[package]] 945 | name = "unicode-width" 946 | version = "0.2.0" 947 | source = "registry+https://github.com/rust-lang/crates.io-index" 948 | checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 949 | 950 | [[package]] 951 | name = "utf8parse" 952 | version = "0.2.1" 953 | source = "registry+https://github.com/rust-lang/crates.io-index" 954 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 955 | 956 | [[package]] 957 | name = "walkdir" 958 | version = "2.3.2" 959 | source = "registry+https://github.com/rust-lang/crates.io-index" 960 | checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" 961 | dependencies = [ 962 | "same-file", 963 | "winapi", 964 | "winapi-util", 965 | ] 966 | 967 | [[package]] 968 | name = "wasi" 969 | version = "0.11.0+wasi-snapshot-preview1" 970 | source = "registry+https://github.com/rust-lang/crates.io-index" 971 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 972 | 973 | [[package]] 974 | name = "winapi" 975 | version = "0.3.9" 976 | source = "registry+https://github.com/rust-lang/crates.io-index" 977 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 978 | dependencies = [ 979 | "winapi-i686-pc-windows-gnu", 980 | "winapi-x86_64-pc-windows-gnu", 981 | ] 982 | 983 | [[package]] 984 | name = "winapi-i686-pc-windows-gnu" 985 | version = "0.4.0" 986 | source = "registry+https://github.com/rust-lang/crates.io-index" 987 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 988 | 989 | [[package]] 990 | name = "winapi-util" 991 | version = "0.1.5" 992 | source = "registry+https://github.com/rust-lang/crates.io-index" 993 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 994 | dependencies = [ 995 | "winapi", 996 | ] 997 | 998 | [[package]] 999 | name = "winapi-x86_64-pc-windows-gnu" 1000 | version = "0.4.0" 1001 | source = "registry+https://github.com/rust-lang/crates.io-index" 1002 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1003 | 1004 | [[package]] 1005 | name = "windows-sys" 1006 | version = "0.48.0" 1007 | source = "registry+https://github.com/rust-lang/crates.io-index" 1008 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1009 | dependencies = [ 1010 | "windows-targets 0.48.5", 1011 | ] 1012 | 1013 | [[package]] 1014 | name = "windows-sys" 1015 | version = "0.59.0" 1016 | source = "registry+https://github.com/rust-lang/crates.io-index" 1017 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1018 | dependencies = [ 1019 | "windows-targets 0.52.6", 1020 | ] 1021 | 1022 | [[package]] 1023 | name = "windows-targets" 1024 | version = "0.48.5" 1025 | source = "registry+https://github.com/rust-lang/crates.io-index" 1026 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1027 | dependencies = [ 1028 | "windows_aarch64_gnullvm 0.48.5", 1029 | "windows_aarch64_msvc 0.48.5", 1030 | "windows_i686_gnu 0.48.5", 1031 | "windows_i686_msvc 0.48.5", 1032 | "windows_x86_64_gnu 0.48.5", 1033 | "windows_x86_64_gnullvm 0.48.5", 1034 | "windows_x86_64_msvc 0.48.5", 1035 | ] 1036 | 1037 | [[package]] 1038 | name = "windows-targets" 1039 | version = "0.52.6" 1040 | source = "registry+https://github.com/rust-lang/crates.io-index" 1041 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1042 | dependencies = [ 1043 | "windows_aarch64_gnullvm 0.52.6", 1044 | "windows_aarch64_msvc 0.52.6", 1045 | "windows_i686_gnu 0.52.6", 1046 | "windows_i686_gnullvm", 1047 | "windows_i686_msvc 0.52.6", 1048 | "windows_x86_64_gnu 0.52.6", 1049 | "windows_x86_64_gnullvm 0.52.6", 1050 | "windows_x86_64_msvc 0.52.6", 1051 | ] 1052 | 1053 | [[package]] 1054 | name = "windows_aarch64_gnullvm" 1055 | version = "0.48.5" 1056 | source = "registry+https://github.com/rust-lang/crates.io-index" 1057 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1058 | 1059 | [[package]] 1060 | name = "windows_aarch64_gnullvm" 1061 | version = "0.52.6" 1062 | source = "registry+https://github.com/rust-lang/crates.io-index" 1063 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1064 | 1065 | [[package]] 1066 | name = "windows_aarch64_msvc" 1067 | version = "0.48.5" 1068 | source = "registry+https://github.com/rust-lang/crates.io-index" 1069 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1070 | 1071 | [[package]] 1072 | name = "windows_aarch64_msvc" 1073 | version = "0.52.6" 1074 | source = "registry+https://github.com/rust-lang/crates.io-index" 1075 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1076 | 1077 | [[package]] 1078 | name = "windows_i686_gnu" 1079 | version = "0.48.5" 1080 | source = "registry+https://github.com/rust-lang/crates.io-index" 1081 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1082 | 1083 | [[package]] 1084 | name = "windows_i686_gnu" 1085 | version = "0.52.6" 1086 | source = "registry+https://github.com/rust-lang/crates.io-index" 1087 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1088 | 1089 | [[package]] 1090 | name = "windows_i686_gnullvm" 1091 | version = "0.52.6" 1092 | source = "registry+https://github.com/rust-lang/crates.io-index" 1093 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1094 | 1095 | [[package]] 1096 | name = "windows_i686_msvc" 1097 | version = "0.48.5" 1098 | source = "registry+https://github.com/rust-lang/crates.io-index" 1099 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1100 | 1101 | [[package]] 1102 | name = "windows_i686_msvc" 1103 | version = "0.52.6" 1104 | source = "registry+https://github.com/rust-lang/crates.io-index" 1105 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1106 | 1107 | [[package]] 1108 | name = "windows_x86_64_gnu" 1109 | version = "0.48.5" 1110 | source = "registry+https://github.com/rust-lang/crates.io-index" 1111 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1112 | 1113 | [[package]] 1114 | name = "windows_x86_64_gnu" 1115 | version = "0.52.6" 1116 | source = "registry+https://github.com/rust-lang/crates.io-index" 1117 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1118 | 1119 | [[package]] 1120 | name = "windows_x86_64_gnullvm" 1121 | version = "0.48.5" 1122 | source = "registry+https://github.com/rust-lang/crates.io-index" 1123 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1124 | 1125 | [[package]] 1126 | name = "windows_x86_64_gnullvm" 1127 | version = "0.52.6" 1128 | source = "registry+https://github.com/rust-lang/crates.io-index" 1129 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1130 | 1131 | [[package]] 1132 | name = "windows_x86_64_msvc" 1133 | version = "0.48.5" 1134 | source = "registry+https://github.com/rust-lang/crates.io-index" 1135 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1136 | 1137 | [[package]] 1138 | name = "windows_x86_64_msvc" 1139 | version = "0.52.6" 1140 | source = "registry+https://github.com/rust-lang/crates.io-index" 1141 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1142 | 1143 | [[package]] 1144 | name = "ztop" 1145 | version = "0.3.0" 1146 | dependencies = [ 1147 | "cfg-if", 1148 | "clap", 1149 | "crossterm", 1150 | "glob", 1151 | "humanize-rs", 1152 | "nix", 1153 | "ratatui", 1154 | "regex", 1155 | "sysctl", 1156 | ] 1157 | --------------------------------------------------------------------------------