#![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 {
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;
}
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());
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 {
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",
}
}
}