Something went wrong. Try again.
The most advanced terminal and desktop audio visualizer I'm aware of
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812use clap::{Parser, ValueEnum};use clap_complete::Shell;use std::{path::PathBuf, str::FromStr};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub struct RgbColor { pub red: u8, pub green: u8, pub blue: u8,}
impl FromStr for RgbColor { type Err = String;
fn from_str(value: &str) -> Result<Self, Self::Err> { let value = value.strip_prefix('#').unwrap_or(value); if value.len() != 6 || !value.bytes().all(|byte| byte.is_ascii_hexdigit()) { return Err("expected a six-digit RGB hex color such as FF2040".into()); } Ok(Self { red: u8::from_str_radix(&value[0..2], 16).map_err(|error| error.to_string())?, green: u8::from_str_radix(&value[2..4], 16).map_err(|error| error.to_string())?, blue: u8::from_str_radix(&value[4..6], 16).map_err(|error| error.to_string())?, }) }}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]pub enum Source { /// Main system output on macOS and Windows; default input elsewhere. Auto, /// Main system output via ScreenCaptureKit or WASAPI loopback. Output, /// A microphone, interface, monitor, or loopback input. Input,}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]pub enum Frontend { /// Render in the current terminal with Ratatui. Terminal, /// Render in a native GPU-accelerated desktop window. Window,}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]pub enum Theme { Aurora, Sunset, Ice, Crimson, Vaporwave, Toxic, Gold, Midnight, Ocean, Ember, Orchid, Forest, Sakura, Mono, MonoDark,}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]pub enum BarStyle { /// Smooth eighth-cell columns. Solid, /// Horizontal meter segments. Segments, /// Round points stacked vertically. Dots, /// Thin vertical needles. Lines, /// Separated rows like a hardware LED meter. Led, /// Hollow bars with sides and a top cap. Outline, /// Bright moving tips with fading tails. Comet, /// Show only the current tip of each bar. PeakOnly,}
impl BarStyle { pub fn next(self) -> Self { match self { Self::Solid => Self::Segments, Self::Segments => Self::Dots, Self::Dots => Self::Lines, Self::Lines => Self::Led, Self::Led => Self::Outline, Self::Outline => Self::Comet, Self::Comet => Self::PeakOnly, Self::PeakOnly => Self::Solid, } }}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]pub enum Display { /// Dense, continuous strokes like AE's Analog Lines display. AnalogLines, /// Points distributed along each frequency stroke. AnalogDots, /// Segmented rectangular frequency strokes. Digital, /// The original bottom-up terminal meter. Bars, /// Frequency history flowing through time. Waterfall, /// Smooth 2x4 subcell strokes, especially useful for curved paths. Braille,}
impl Display { pub fn next(self) -> Self { match self { Self::AnalogLines => Self::AnalogDots, Self::AnalogDots => Self::Digital, Self::Digital => Self::Bars, Self::Bars => Self::Waterfall, Self::Waterfall => Self::Braille, Self::Braille => Self::AnalogLines, } }
pub fn uses_bar_spacing(self) -> bool { matches!(self, Self::Digital | Self::Bars) }
pub fn label(self) -> &'static str { match self { Self::AnalogLines => "ANALOG LINES", Self::AnalogDots => "ANALOG DOTS", Self::Digital => "DIGITAL", Self::Bars => "BARS", Self::Waterfall => "WATERFALL", Self::Braille => "BRAILLE", } }}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]pub enum Path { Line, Circle, Arc, Sine, Diamond, Pulse,}
impl Path { pub fn next(self) -> Self { match self { Self::Line => Self::Circle, Self::Circle => Self::Arc, Self::Arc => Self::Sine, Self::Sine => Self::Diamond, Self::Diamond => Self::Pulse, Self::Pulse => Self::Line, } }
pub fn label(self) -> &'static str { match self { Self::Line => "LINE", Self::Circle => "CIRCLE", Self::Arc => "ARC", Self::Sine => "SINE", Self::Diamond => "DIAMOND", Self::Pulse => "PULSE", } }}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]pub enum Side { /// Draw above and below the spectrum path. Both, /// Draw above the spectrum path. A, /// Draw below the spectrum path. B,}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]pub enum FrequencyScale { /// Equal Hz per band, matching the characteristic AE distribution. Linear, /// Equal musical ratio per band for a conventional analyzer. Log, /// Musical constant-Q-style spacing backed by the shared FFT. Cqt, /// Psychoacoustic Mel spacing. Mel, /// Psychoacoustic Bark spacing. Bark,}
impl FrequencyScale { pub fn label(self) -> &'static str { match self { Self::Linear => "linear frequency", Self::Log => "logarithmic frequency", Self::Cqt => "constant-Q frequency", Self::Mel => "Mel frequency", Self::Bark => "Bark frequency", } }}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]pub enum Analysis { /// Normal frequency spectrum. Spectrum, /// Harmonic root-candidate strengths arranged like a spectrum. Fundamentals, /// Twelve musical pitch classes, C through B. Chroma,}
impl Analysis { pub fn label(self) -> &'static str { match self { Self::Spectrum => "SPECTRUM", Self::Fundamentals => "ROOTS", Self::Chroma => "CHROMA", } }}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]pub enum ChannelMode { /// Average left and right into one spectrum. Mix, /// Left on Side A and right on Side B. Stereo, /// Mid on Side A and stereo difference on Side B. MidSide,}
impl ChannelMode { pub fn label(self) -> &'static str { match self { Self::Mix => "MIX", Self::Stereo => "L/R", Self::MidSide => "M/S", } }}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]pub enum Detector { /// Keep the strongest FFT component in each band. Peak, /// Average the energy contained in each band. Rms,}
impl Side { pub fn next(self) -> Self { match self { Self::Both => Self::A, Self::A => Self::B, Self::B => Self::Both, } }
pub fn label(self) -> &'static str { match self { Self::Both => "A+B", Self::A => "A", Self::B => "B", } }}
impl Theme { pub fn next(self) -> Self { match self { Self::Aurora => Self::Sunset, Self::Sunset => Self::Ice, Self::Ice => Self::Crimson, Self::Crimson => Self::Vaporwave, Self::Vaporwave => Self::Toxic, Self::Toxic => Self::Gold, Self::Gold => Self::Midnight, Self::Midnight => Self::Ocean, Self::Ocean => Self::Ember, Self::Ember => Self::Orchid, Self::Orchid => Self::Forest, Self::Forest => Self::Sakura, Self::Sakura => Self::Mono, Self::Mono => Self::MonoDark, Self::MonoDark => Self::Aurora, } }}
#[derive(Debug, Parser)]#[command( version, about = "A responsive terminal audio spectrum visualizer", args_override_self = true, after_help = "Keys: q/esc quit | +/- gain | r profile | d display | b bar style | x path | s side | t theme | p peaks | space freeze | f spectral hold | ? help")]pub struct Cli { /// Rendering frontend. Window uses Metal on macOS and Vulkan/OpenGL under Wayland. #[arg(long, value_enum, default_value_t = Frontend::Window)] pub frontend: Frontend,
/// Capture source. Auto uses system output on macOS and Windows. #[arg(long, value_enum, default_value_t = Source::Auto)] pub source: Source,
/// Input device, or Windows loopback output, by ID or description substring. #[arg(short, long, value_name = "DEVICE")] pub device: Option<String>,
/// List available capture devices and exit. #[arg(long)] pub list_devices: bool,
/// Capture for N seconds without opening the UI, then report activity. #[arg(long, value_name = "SECONDS", value_parser = clap::value_parser!(u64).range(1..=60))] pub check: Option<u64>,
/// Print a shell completion script and exit. #[arg(long, value_enum, value_name = "SHELL")] pub generate_completion: Option<Shell>,
/// Load options from a named INI profile before applying command-line options. #[arg(long, value_name = "NAME")] pub profile: Option<String>,
/// INI profile file; defaults to the configured user profile path. #[arg(long, value_name = "PATH")] pub profile_file: Option<PathBuf>,
/// List profile names and exit. #[arg(long)] pub list_profiles: bool,
/// Number of frequency bands; 0 fits bands to terminal width. #[arg(short, long, default_value_t = 64, value_parser = parse_bands)] pub bands: usize,
/// FFT window size. Larger values improve bass resolution but add latency. #[arg(long, default_value_t = 4096, value_parser = parse_fft_size)] pub fft_size: usize,
/// Lowest displayed frequency in Hz. #[arg(long, default_value_t = 20.0, value_parser = clap::value_parser!(f32))] pub min_freq: f32,
/// Highest displayed frequency in Hz. #[arg(long, default_value_t = 2_000.0, value_parser = clap::value_parser!(f32))] pub max_freq: f32,
/// Frequency spacing along the spectrum path. #[arg(long, value_enum, default_value_t = FrequencyScale::Linear)] pub frequency_scale: FrequencyScale,
/// How FFT components are reduced into each displayed band. #[arg(long, value_enum, default_value_t = Detector::Peak)] pub detector: Detector,
/// Spectral representation. #[arg(long, value_enum, default_value_t = Analysis::Spectrum)] pub analysis: Analysis,
/// Channel mapping used by Side A and Side B. #[arg(long, value_enum, default_value_t = ChannelMode::Mix)] pub channels: ChannelMode,
/// Input gain in dB. #[arg(short, long, default_value_t = 6.0, allow_hyphen_values = true)] pub gain: f32,
/// Spectrum floor in dBFS. #[arg(long, default_value_t = -72.0, allow_hyphen_values = true)] pub db_floor: f32,
/// Spectrum ceiling in dBFS. #[arg(long, default_value_t = -3.0, allow_hyphen_values = true)] pub db_ceiling: f32,
/// Rise response time in milliseconds. #[arg(long, default_value_t = 30.0, value_parser = clap::value_parser!(f32))] pub attack: f32,
/// Fall response time in milliseconds. #[arg(long, default_value_t = 180.0, value_parser = clap::value_parser!(f32))] pub release: f32,
/// Neighbor-band smoothing amount from 0 to 1. #[arg(long, default_value_t = 0.08, value_parser = parse_unit)] pub spatial_smoothing: f32,
/// Height contrast. Values above 1 suppress weak frequencies. #[arg(long, default_value_t = 1.65, value_parser = parse_contrast)] pub contrast: f32,
/// Emphasize local spectral peaks and valleys, from 0 to 1. #[arg(long, default_value_t = 0.0, value_parser = parse_unit)] pub spectral_contrast: f32,
/// Integrate spectral energy over this many milliseconds. #[arg(long, default_value_t = 0.0, value_parser = parse_milliseconds)] pub audio_duration: f32,
/// Analyze audio this many milliseconds behind realtime. #[arg(long, default_value_t = 0.0, value_parser = parse_milliseconds)] pub audio_offset: f32,
/// Local spectral whitening from 0 to 1. #[arg(long, default_value_t = 0.0, value_parser = parse_unit)] pub whitening: f32,
/// Suppress bands that are not local peaks, from 0 to 1. #[arg(long, default_value_t = 0.0, value_parser = parse_unit)] pub peak_isolation: f32,
/// Blend toward positive spectral change rather than level, from 0 to 1. #[arg(long, default_value_t = 0.0, value_parser = parse_unit)] pub spectral_flux: f32,
/// Normalize every band against its slowly moving average, from 0 to 1. #[arg(long, default_value_t = 0.0, value_parser = parse_unit)] pub adaptive_gain: f32,
/// Adaptive-gain averaging window in milliseconds. #[arg(long, default_value_t = 4_000.0, value_parser = parse_adaptive_window)] pub adaptive_window: f32,
/// Maximum adaptive boost in dB. #[arg(long, default_value_t = 12.0, value_parser = parse_attenuation)] pub adaptive_max_boost: f32,
/// Maximum adaptive cut in dB. #[arg(long, default_value_t = 18.0, value_parser = parse_attenuation)] pub adaptive_max_cut: f32,
/// Highest harmonic used to score root candidates in fundamentals analysis. #[arg(long, default_value_t = 6, value_parser = clap::value_parser!(u8).range(2..=12))] pub root_harmonics: u8,
/// Suppress frequencies without harmonic support in fundamentals analysis. #[arg(long, default_value_t = 0.65, value_parser = parse_unit)] pub root_isolation: f32,
/// Strength of phase-coherent tonal body rendering. #[arg(long, default_value_t = 0.0, value_parser = parse_unit)] pub tonal_body: f32,
/// Strength of transient spark rendering. #[arg(long, default_value_t = 0.0, value_parser = parse_unit)] pub transient_sparks: f32,
/// Strength of the detected fundamental-frequency core. #[arg(long, default_value_t = 0.0, value_parser = parse_unit)] pub fundamental_core: f32,
/// Strength of the classified overtone halo. #[arg(long, default_value_t = 0.0, value_parser = parse_unit)] pub harmonic_halo: f32,
/// Number of harmonics to reinforce from strong fundamentals. #[arg(long, default_value_t = 0, value_parser = clap::value_parser!(u8).range(0..=12))] pub harmonics: u8,
/// Strength of synthetic harmonic reinforcement from 0 to 1. #[arg(long, default_value_t = 0.35, value_parser = parse_unit)] pub harmonic_boost: f32,
/// High-frequency compensation in dB per octave. #[arg(long, default_value_t = 0.0)] pub tilt: f32,
/// Center frequency of a smooth low-shelf cut; 0 disables it. #[arg(long, default_value_t = 0.0, value_parser = parse_nonnegative)] pub low_shelf: f32,
/// Attenuation below the low-shelf frequency, in dB. #[arg(long, default_value_t = 0.0, value_parser = parse_attenuation)] pub low_shelf_attenuation: f32,
/// Center frequency of a smooth high-shelf cut; 0 disables it. #[arg(long, default_value_t = 0.0, value_parser = parse_nonnegative)] pub high_shelf: f32,
/// Attenuation above the high-shelf frequency, in dB. #[arg(long, default_value_t = 0.0, value_parser = parse_attenuation)] pub high_shelf_attenuation: f32,
/// Width of both shelf transitions in octaves. #[arg(long, default_value_t = 1.0, value_parser = parse_shelf_width)] pub shelf_width: f32,
/// Compress bands above this dBFS level. #[arg(long, default_value_t = -18.0, allow_hyphen_values = true)] pub compressor_threshold: f32,
/// Spectral compressor ratio; 1 disables compression. #[arg(long, default_value_t = 1.0, value_parser = parse_compressor_ratio)] pub compressor_ratio: f32,
/// Makeup gain applied after spectral shelves and compression, in dB. #[arg(long, default_value_t = 0.0, value_parser = parse_post_filter_gain)] pub post_filter_gain: f32,
/// How long peak markers remain fixed in milliseconds. #[arg(long, default_value_t = 420.0, value_parser = clap::value_parser!(f32))] pub peak_hold: f32,
/// Peak marker fall rate in dB per second. #[arg(long, default_value_t = 24.0, value_parser = clap::value_parser!(f32))] pub peak_decay: f32,
/// Show peak markers. Disabled by default for an AE-like presentation. #[arg(long)] pub peaks: bool,
/// Target redraw rate. #[arg(long, default_value_t = 60, value_parser = clap::value_parser!(u16).range(10..=240))] pub fps: u16,
/// Spectrum rendering style. #[arg(long, value_enum, default_value_t = Display::AnalogLines)] pub display: Display,
/// Glyph treatment used by the bars display. #[arg(long, value_enum, default_value_t = BarStyle::Solid)] pub bar_style: BarStyle,
/// Baseline path used by non-bar spectrum modes. #[arg(long, value_enum, default_value_t = Path::Line)] pub path: Path,
/// Draw on Side A, Side B, or both sides of the center path. #[arg(long, value_enum, default_value_t = Side::Both)] pub side: Side,
/// Maximum spectrum height as a fraction of the available space. #[arg(long, default_value_t = 0.88, value_parser = parse_unit)] pub max_height: f32,
/// Simulated edge softness/glow from 0 to 1. #[arg(long, default_value_t = 0.55, value_parser = parse_unit)] pub softness: f32,
/// Strength of a slower, wider spectrum layer under the core. #[arg(long, default_value_t = 0.45, value_parser = parse_unit)] pub glow_strength: f32,
/// Release time of the slower glow layer in milliseconds. #[arg(long, default_value_t = 480.0, value_parser = parse_milliseconds)] pub glow_release: f32,
/// Number of palette interpolations across the spectrum path. #[arg(long, default_value_t = 1.0, value_parser = parse_hue_cycles)] pub hue_cycles: f32,
/// Color at the spectrum path, as RRGGBB or #RRGGBB. #[arg(long, value_name = "HEX")] pub inside_color: Option<RgbColor>,
/// Color at the tip of each spectrum stroke, as RRGGBB or #RRGGBB. #[arg(long, value_name = "HEX")] pub outside_color: Option<RgbColor>,
/// Number of delayed spectral layers; waterfall history is capped at 32. #[arg(long, default_value_t = 3, value_parser = clap::value_parser!(u8).range(0..=32))] pub trails: u8,
/// Scale spectrum motion from bass energy, from 0 to 1. #[arg(long, default_value_t = 0.2, value_parser = parse_unit)] pub beat_reactivity: f32,
/// Launch path ripples from detected note and percussion onsets. #[arg(long, default_value_t = 0.0, value_parser = parse_unit)] pub onset_ripples: f32,
/// Synchronize geometry to the estimated tempo phase. #[arg(long, default_value_t = 0.0, value_parser = parse_unit)] pub tempo_reactivity: f32,
/// Shift hue using spectral centroid/brightness. #[arg(long, default_value_t = 0.0, value_parser = parse_unit)] pub centroid_color: f32,
/// Modulate height and glow from spectral shape, coherence, and bass ratio. #[arg(long, default_value_t = 0.0, value_parser = parse_unit)] pub descriptor_motion: f32,
/// Draw a stereo phase/Lissajous scope in the center. #[arg(long, default_value_t = 0.0, value_parser = parse_unit)] pub phase_scope: f32,
/// Mark the three strongest formant-like spectral regions. #[arg(long, default_value_t = 0.0, value_parser = parse_unit)] pub formants: f32,
/// Frozen-spectrum contribution after pressing f. #[arg(long, default_value_t = 0.75, value_parser = parse_unit)] pub freeze_mix: f32,
/// How quickly a frozen spectrum morphs toward live audio; 0 holds forever. #[arg(long, default_value_t = 0.0, value_parser = parse_unit)] pub morph_speed: f32,
/// Stroke width for digital and bar displays, in terminal cells. #[arg(long, default_value_t = 2, value_parser = clap::value_parser!(u16).range(1..=8))] pub bar_width: u16,
/// Empty cells between bars. #[arg(long, default_value_t = 1, value_parser = clap::value_parser!(u16).range(0..=4))] pub gap: u16,
/// Color palette. #[arg(long, value_enum, default_value_t = Theme::Aurora)] pub theme: Theme,
/// Hide the title, source details, and frequency labels. #[arg(long)] pub minimal: bool,}
impl Cli { pub fn validate(&self) -> anyhow::Result<()> { anyhow::ensure!(self.min_freq >= 1.0, "--min-freq must be at least 1 Hz"); anyhow::ensure!( self.max_freq > self.min_freq, "--max-freq must be greater than --min-freq" ); anyhow::ensure!( self.db_ceiling > self.db_floor, "--db-ceiling must be greater than --db-floor" ); anyhow::ensure!(self.attack >= 0.0, "--attack cannot be negative"); anyhow::ensure!(self.release >= 0.0, "--release cannot be negative"); anyhow::ensure!(self.peak_hold >= 0.0, "--peak-hold cannot be negative"); anyhow::ensure!(self.peak_decay >= 0.0, "--peak-decay cannot be negative"); anyhow::ensure!( self.compressor_threshold <= self.db_ceiling, "--compressor-threshold must not exceed --db-ceiling" ); Ok(()) }}
fn parse_unit(value: &str) -> Result<f32, String> { let value = value.parse::<f32>().map_err(|error| error.to_string())?; if (0.0..=1.0).contains(&value) { Ok(value) } else { Err("expected a value from 0 to 1".into()) }}
fn parse_fft_size(value: &str) -> Result<usize, String> { let size = value.parse::<usize>().map_err(|error| error.to_string())?; if (512..=32_768).contains(&size) && size.is_power_of_two() { Ok(size) } else { Err("expected a power of two from 512 to 32768".into()) }}
fn parse_bands(value: &str) -> Result<usize, String> { let bands = value.parse::<usize>().map_err(|error| error.to_string())?; if bands <= 256 { Ok(bands) } else { Err("expected a value from 0 to 256".into()) }}
fn parse_hue_cycles(value: &str) -> Result<f32, String> { let cycles = value.parse::<f32>().map_err(|error| error.to_string())?; if (0.0..=8.0).contains(&cycles) { Ok(cycles) } else { Err("expected a value from 0 to 8".into()) }}
fn parse_contrast(value: &str) -> Result<f32, String> { let contrast = value.parse::<f32>().map_err(|error| error.to_string())?; if (0.1..=4.0).contains(&contrast) { Ok(contrast) } else { Err("expected a value from 0.1 to 4".into()) }}
fn parse_milliseconds(value: &str) -> Result<f32, String> { let milliseconds = value.parse::<f32>().map_err(|error| error.to_string())?; if (0.0..=2_000.0).contains(&milliseconds) { Ok(milliseconds) } else { Err("expected milliseconds from 0 to 2000".into()) }}
fn parse_nonnegative(value: &str) -> Result<f32, String> { let value = value.parse::<f32>().map_err(|error| error.to_string())?; if value >= 0.0 { Ok(value) } else { Err("expected a non-negative value".into()) }}
fn parse_attenuation(value: &str) -> Result<f32, String> { let value = value.parse::<f32>().map_err(|error| error.to_string())?; if (0.0..=60.0).contains(&value) { Ok(value) } else { Err("expected attenuation from 0 to 60 dB".into()) }}
fn parse_shelf_width(value: &str) -> Result<f32, String> { let value = value.parse::<f32>().map_err(|error| error.to_string())?; if (0.1..=6.0).contains(&value) { Ok(value) } else { Err("expected a transition width from 0.1 to 6 octaves".into()) }}
fn parse_compressor_ratio(value: &str) -> Result<f32, String> { let value = value.parse::<f32>().map_err(|error| error.to_string())?; if (1.0..=20.0).contains(&value) { Ok(value) } else { Err("expected a compressor ratio from 1 to 20".into()) }}
fn parse_post_filter_gain(value: &str) -> Result<f32, String> { let value = value.parse::<f32>().map_err(|error| error.to_string())?; if (0.0..=60.0).contains(&value) { Ok(value) } else { Err("expected post-filter gain from 0 to 60 dB".into()) }}
fn parse_adaptive_window(value: &str) -> Result<f32, String> { let value = value.parse::<f32>().map_err(|error| error.to_string())?; if (100.0..=30_000.0).contains(&value) { Ok(value) } else { Err("expected an adaptive window from 100 to 30000 ms".into()) }}
#[cfg(test)]mod tests { use clap::CommandFactory;
use super::*;
#[test] fn parses_hash_prefixed_rgb_colors() { assert_eq!( "#ff2040".parse(), Ok(RgbColor { red: 255, green: 32, blue: 64 }) ); assert!("red".parse::<RgbColor>().is_err()); }
#[test] fn generated_completion_contains_current_options() { let mut command = Cli::command(); let mut output = Vec::new(); clap_complete::generate(Shell::Bash, &mut command, "vizzzz", &mut output); let script = String::from_utf8(output).unwrap();
assert!(script.contains("--post-filter-gain")); assert!(script.contains("--profile-file")); assert!(script.contains("--generate-completion")); }
#[test] fn post_filter_gain_is_bounded() { let cli = Cli::try_parse_from(["vizzzz", "--post-filter-gain", "12"]).unwrap(); assert_eq!(cli.post_filter_gain, 12.0); assert!(Cli::try_parse_from(["vizzzz", "--post-filter-gain", "61"]).is_err()); }
#[test] fn parses_sakura_theme() { let cli = Cli::try_parse_from(["vizzzz", "--theme", "sakura"]).unwrap(); assert_eq!(cli.theme, Theme::Sakura); }
#[test] fn parses_window_frontend() { let cli = Cli::try_parse_from(["vizzzz", "--frontend", "window"]).unwrap(); assert_eq!(cli.frontend, Frontend::Window); }}