tempo_tapper 0.5.5

terminal tempo tapper (archive)
// tempo: terminal tempo tapper
// copyright (C) 2022-2023 Nissa <and-nissa@protonmail.com>
// licensed under MIT OR Apache-2.0

#![warn(clippy::pedantic, clippy::nursery, clippy::cargo)]

mod ringbuf;
mod tap;

#[cfg(test)]
mod tests;

use core::num::IntErrorKind;
use std::env;
use std::io::{self, BufRead, BufWriter, Error, Read, Write};
use std::process::ExitCode;

use tap::Tapper;

const DEFAULT_BUF_CAP: u16 = 10;
const DEFAULT_BOUNDED: bool = true;
const BPM_PRECISION: usize = 1;
const INPUT_BUF_SIZE: u16 = 1024;
const STDERR_BUF_SIZE: usize = 128;
const STDOUT_BUF_SIZE: usize = 256;

fn main() -> ExitCode {
    // @alloc
    let mut stderr = BufWriter::with_capacity(STDERR_BUF_SIZE, io::stderr());

    if env::args().len() > 1 {
        let _ = writeln!(stderr, "usage: {}", env!("CARGO_BIN_NAME"));
        let _ = stderr.flush();
        return ExitCode::FAILURE;
    }

    // @alloc
    let mut stdout = BufWriter::with_capacity(STDOUT_BUF_SIZE, io::stdout());

    let result = try_main(&mut stdout);
    let _ = stdout.flush();
    if let Err(error) = result {
        let _ = writeln!(stderr, "fatal: {error}");
        let _ = stderr.flush();
        ExitCode::FAILURE
    } else {
        ExitCode::SUCCESS
    }
}

fn try_main<W: Write>(mut stdout: W) -> Result<(), Error> {
    let mut tapper = Tapper::new(DEFAULT_BUF_CAP, DEFAULT_BOUNDED);
    let mut input = String::with_capacity(INPUT_BUF_SIZE.into()); // @alloc

    // splash text
    writeln!(
        stdout,
        "{} {}: {}",
        env!("CARGO_BIN_NAME"),
        env!("CARGO_PKG_VERSION"),
        env!("CARGO_PKG_DESCRIPTION"),
    )?;
    writeln!(stdout, r#"type "h" for help, "l" for license"#)?;
    writeln!(stdout)?;

    loop {
        // print the bpm and buffer stats
        writeln!(
            stdout,
            "{}/{}{} samples in buffer",
            tapper.count(),
            tapper.cap(),
            if tapper.is_bounded() { "" } else { "+" }
        )?;
        writeln!(stdout, "{:.BPM_PRECISION$} bpm", tapper.bpm())?;

        readln(
            &mut stdout,
            &mut input,
            if tapper.is_recording() { " * " } else { " ; " },
        )?;
        if let Some(cmd) = Command::from_str(input.trim()) {
            match cmd {
                Command::Help => {
                    writeln!(stdout)?;
                    for cmd in Command::iter() {
                        writeln!(stdout, " {}. {}.", cmd.short_name(), cmd.description())?;
                    }
                }

                Command::Tap => tapper.tap(),

                Command::Clear => tapper.clear(),

                Command::Size => {
                    writeln!(stdout)?;
                    readln(&mut stdout, &mut input, " new buffer size? ")?;
                    let trimmed = input.trim();
                    if !trimmed.is_empty() {
                        let cap = match trimmed.parse::<u16>() {
                            Ok(cap) => Some(cap),
                            Err(error) => {
                                if *error.kind() == IntErrorKind::PosOverflow {
                                    Some(u16::MAX)
                                } else {
                                    writeln!(stdout, " invalid integer: {error}")?;
                                    None
                                }
                            }
                        };
                        if let Some(cap) = cap {
                            tapper.resize(cap);
                            let reported = tapper.cap();
                            if reported < cap {
                                writeln!(stdout, " size too large, clamped to {reported}")?;
                            }
                        }
                    }
                }

                Command::Bound => tapper.toggle_bounded(),

                Command::Print => {
                    writeln!(stdout)?;
                    writeln!(stdout, " {tapper:.BPM_PRECISION$}")?;
                }

                Command::License => {
                    writeln!(stdout)?;
                    writeln!(
                        stdout,
                        " copyright (C) 2022-2023 {}",
                        env!("CARGO_PKG_AUTHORS")
                    )?;
                    writeln!(stdout, " licensed under {}", env!("CARGO_PKG_LICENSE"))?;
                }

                Command::Quit => break,
            }
        } else {
            writeln!(stdout)?;
            writeln!(stdout, r#" unrecognized command. try "h" for help."#)?;
        }

        writeln!(stdout)?;
    }

    Ok(())
}

fn readln(mut out: impl Write, input: &mut String, prompt: &str) -> io::Result<()> {
    write!(out, "{prompt}")?;
    out.flush()?;
    input.clear();
    io::stdin()
        .lock()
        .take(INPUT_BUF_SIZE.into())
        .read_line(input)?;
    Ok(())
}

#[derive(Clone, Copy, Debug)]
enum Command {
    Help,
    Tap,
    Clear,
    Size,
    Bound,
    Print,
    License,
    Quit,
}

impl Command {
    pub fn from_str(s: &str) -> Option<Self> {
        for cmd in Self::iter() {
            if s == cmd.literal() {
                return Some(*cmd);
            }
        }
        None
    }

    pub const fn iter() -> &'static [Self] {
        &[
            Self::Help,
            Self::Tap,
            Self::Clear,
            Self::Size,
            Self::Bound,
            Self::Print,
            Self::License,
            Self::Quit,
        ]
    }

    pub const fn literal(self) -> &'static str {
        match self {
            Self::Help => "h",
            Self::Tap => "",
            Self::Clear => "c",
            Self::Size => "s",
            Self::Bound => "b",
            Self::Print => "p",
            Self::License => "l",
            Self::Quit => "q",
        }
    }

    pub const fn short_name(self) -> &'static str {
        match self {
            Self::Tap => "<enter>",
            other => other.literal(),
        }
    }

    pub const fn description(self) -> &'static str {
        match self {
            Self::Help => "show help",
            Self::Tap => "register a tap",
            Self::Clear => "clear buffer",
            Self::Size => "adjust buffer size",
            Self::Bound => "bound or unbound buffer to size",
            Self::Print => "print buffer contents",
            Self::License => "print license info",
            Self::Quit => "quit",
        }
    }
}