use std::process;
use clap::{Parser, Subcommand};
use tokio::io::AsyncWriteExt;
use tokio::net::UnixStream;
use whisrs::history::HistoryEntry;
use whisrs::{encode_message, read_message, socket_path, Command, Response, State};
const ASCII_BANNER: &str = concat!(
"\n",
" __ _\n",
" _ _| |__ |_|___ _ __ ___\n",
" \\ \\//\\ / '_ \\| / __| '__/ __|\n",
" \\ /\\ \\ | | | \\__ \\ | \\__ \\\n",
" \\/ \\/|_| |_|_|___/_| |___/\n",
"\n",
" speak. type. done.\n",
"\n",
env!("CARGO_PKG_VERSION"),
);
const GREEN: &str = "\x1b[32m";
const YELLOW: &str = "\x1b[33m";
const RED: &str = "\x1b[31m";
const BOLD: &str = "\x1b[1m";
const RESET: &str = "\x1b[0m";
#[derive(Parser)]
#[command(
name = "whisrs",
about = "Linux-first voice-to-text dictation tool",
long_version = ASCII_BANNER,
)]
struct Cli {
#[command(subcommand)]
command: SubCmd,
}
#[derive(Subcommand)]
enum SubCmd {
Setup,
Toggle,
Cancel,
Status,
Log {
#[arg(short = 'n', long, default_value = "20")]
limit: usize,
#[arg(long)]
clear: bool,
},
Command,
}
fn is_tty() -> bool {
use std::io::IsTerminal;
std::io::stdout().is_terminal()
}
fn format_state(state: State, use_color: bool) -> String {
if !use_color {
return format!("{state}");
}
match state {
State::Idle => format!("{BOLD}idle{RESET}"),
State::Recording => format!("{BOLD}{GREEN}recording{RESET}"),
State::Transcribing => format!("{BOLD}{YELLOW}transcribing{RESET}"),
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
match cli.command {
SubCmd::Setup => {
if let Err(e) = whisrs::config::setup::run_setup() {
if is_tty() {
eprintln!("{RED}setup failed:{RESET} {e:#}");
} else {
eprintln!("setup failed: {e:#}");
}
process::exit(1);
}
}
SubCmd::Toggle => {
send_command(Command::Toggle).await?;
}
SubCmd::Cancel => {
send_command(Command::Cancel).await?;
}
SubCmd::Status => {
send_command(Command::Status).await?;
}
SubCmd::Log { limit, clear } => {
if clear {
send_command(Command::ClearHistory).await?;
} else {
send_command(Command::Log { limit }).await?;
}
}
SubCmd::Command => {
send_command(Command::CommandMode).await?;
}
}
Ok(())
}
async fn send_command(cmd: Command) -> anyhow::Result<()> {
let path = socket_path();
let use_color = is_tty();
let stream = match UnixStream::connect(&path).await {
Ok(s) => s,
Err(_) => {
if use_color {
eprintln!(
"{RED}whisrsd is not running.{RESET} Start it with:\n\
\n\
\x20 whisrsd &\n\
\n\
Or enable the systemd service:\n\
\n\
\x20 systemctl --user enable --now whisrs.service"
);
} else {
eprintln!(
"whisrsd is not running. Start it with:\n\
\n\
\x20 whisrsd &\n\
\n\
Or enable the systemd service:\n\
\n\
\x20 systemctl --user enable --now whisrs.service"
);
}
process::exit(1);
}
};
let (mut reader, mut writer) = stream.into_split();
let encoded = encode_message(&cmd)?;
writer.write_all(&encoded).await?;
writer.shutdown().await?;
let response: Response = read_message(&mut reader).await?;
match response {
Response::Ok { state } => {
println!("{}", format_state(state, use_color));
}
Response::History { entries } => {
if entries.is_empty() {
println!("No transcription history.");
} else {
print_history(&entries, use_color);
}
}
Response::Error { message } => {
if use_color {
eprintln!("{RED}error:{RESET} {message}");
} else {
eprintln!("error: {message}");
}
process::exit(1);
}
}
Ok(())
}
fn print_history(entries: &[HistoryEntry], use_color: bool) {
let dim = if use_color { "\x1b[2m" } else { "" };
for entry in entries {
let ts = entry.timestamp.format("%Y-%m-%d %H:%M:%S");
let duration = format!("{:.1}s", entry.duration_secs);
if use_color {
println!(
"{dim}{ts}{RESET} {dim}[{backend} | {lang} | {dur}]{RESET}",
backend = entry.backend,
lang = entry.language,
dur = duration,
);
} else {
println!(
"{ts} [{backend} | {lang} | {dur}]",
backend = entry.backend,
lang = entry.language,
dur = duration,
);
}
println!(" {}", entry.text);
println!();
}
}