mod app;
mod excmd;
mod keys;
mod library;
mod lyrics;
mod matrix;
mod mpris;
mod player;
mod ui;
use std::io::IsTerminal;
use std::path::PathBuf;
use std::time::Duration;
use anyhow::{Result, bail};
use clap::{Parser, Subcommand};
use ratatui::DefaultTerminal;
use ratatui::crossterm::cursor::SetCursorStyle;
use ratatui::crossterm::event::{self, Event};
use ratatui::crossterm::execute;
use app::App;
use library::SortKey;
const TICK: Duration = Duration::from_millis(200);
const FRAME: Duration = Duration::from_millis(50);
#[derive(Parser)]
#[command(
name = "vibox",
version,
about = "a jukebox you exit with :q - a cli music player with vi motions, ex commands, and tmux manners",
after_help = "The library is just a directory. `:e <dir>` opens another one, `c` edits the\n\
filenames in it, and `:help` lists every key."
)]
struct Cli {
#[command(subcommand)]
cmd: Option<Cmd>,
path: Option<PathBuf>,
#[arg(short, long, default_value = "path")]
sort: String,
}
#[derive(Subcommand)]
enum Cmd {
#[command(verbatim_doc_comment)]
Update {
#[arg(short = 'y', long = "yes")]
yes: bool,
},
}
fn main() -> Result<()> {
let cli = Cli::parse();
if let Some(Cmd::Update { yes }) = cli.cmd {
return cmd_update(yes);
}
let Some(sort) = SortKey::parse(&cli.sort) else {
bail!(
"bad sort key `{}` - use path, title, artist, album or duration",
cli.sort
);
};
let root = match cli.path {
Some(p) => p,
None => dirs::audio_dir().unwrap_or(std::env::current_dir()?),
};
if !std::io::stdout().is_terminal() {
bail!("vibox is a full screen player and needs a terminal; it has nothing to pipe");
}
let mut app = App::new(root, sort)?;
excmd::load_state(&mut app);
excmd::load_rc(&mut app);
let terminal = ratatui::init();
let result = run(terminal, &mut app);
ratatui::restore();
excmd::save_state(&app);
result
}
fn run(mut terminal: DefaultTerminal, app: &mut App) -> Result<()> {
let mut shape = None;
while !app.quit {
terminal.draw(|frame| ui::draw(frame, app))?;
let wanted = if app.mode == app::Mode::EditInsert {
SetCursorStyle::SteadyBar
} else {
SetCursorStyle::SteadyBlock
};
if shape != Some(app.mode == app::Mode::EditInsert) {
execute!(std::io::stdout(), wanted)?;
shape = Some(app.mode == app::Mode::EditInsert);
}
let tick = if app.matrix.on { FRAME } else { TICK };
if event::poll(tick)?
&& let Event::Key(key) = event::read()?
{
keys::handle(app, key);
}
app.tick();
}
Ok(())
}
fn cmd_update(yes: bool) -> Result<()> {
use std::io::Write;
if !yes {
print!("Update vibox to the latest release via cargo? [y/N] ");
std::io::stdout().flush().ok();
let mut input = String::new();
std::io::stdin().read_line(&mut input).ok();
if !matches!(input.trim().to_lowercase().as_str(), "y" | "yes") {
println!("aborted");
return Ok(());
}
}
println!("updating via cargo install vibox --force\n");
match std::process::Command::new("cargo")
.args(["install", "vibox", "--force"])
.status()
{
Ok(status) if status.success() => {
println!("\nvibox is up to date");
Ok(())
}
Ok(status) => bail!("update failed (cargo exited {})", status.code().unwrap_or(1)),
Err(e) => {
bail!("could not run cargo: {e} - is it installed and on PATH? (https://rustup.rs)")
}
}
}