Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite vsvg-cli using bpaf #153

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ approx = "0.5.1"
arrayvec = "0.7.2"
base64 = "0.22.1"
bitvec = "1.0.1"
bpaf = "0.9"
bumpalo = "3.14.0" # avoid yanked 3.12.1, pulled by wasm-bindgen
bytemuck = "1.13.1"
camino = "1.1.0"
Expand Down
5 changes: 4 additions & 1 deletion crates/vsvg-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,17 @@ path = "src/main.rs"
vsvg = { workspace = true, features = ["egui"] }
vsvg-viewer.workspace = true

anyhow = { workspace = true, features = ["backtrace"] }
bpaf = { workspace = true, features = ["derive", "autocomplete", "dull-color"] }
clap = { workspace = true, features = ["cargo"] }
dhat = { workspace = true, optional = true } # for heap profiling
dhat = { workspace = true, optional = true } # for heap profiling
eframe.workspace = true
egui.workspace = true
kurbo.workspace = true
rand.workspace = true
rand_chacha.workspace = true
serde.workspace = true
thiserror = { workspace = true }
tracing-subscriber.workspace = true


Expand Down
252 changes: 31 additions & 221 deletions crates/vsvg-cli/src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,234 +1,44 @@
use clap::{arg, command, value_parser, Arg, ArgAction, ArgGroup, ArgMatches, Command, Id};
use std::collections::{BTreeMap, HashMap};
use std::error::Error;
use crate::commands::{context, draw, io, layers, ops, transforms, DynCommand, State};
use bpaf::{construct, short, OptionParser, Parser};

use std::fmt::{Debug, Display, Formatter};

use crate::draw_state::{DrawState, LayerDrawer};
use std::path::PathBuf;
use vsvg::{Document, DocumentTrait, LayerID};

/// A trait for types that can be used as command line arguments.
trait CommandArg: Clone + Into<CommandValue> + Send + Sync + Debug + 'static {}
impl<T: Clone + Into<CommandValue> + Send + Sync + Debug + 'static> CommandArg for T {}

#[derive(Debug)]
pub enum CliError {
NotAVector,
}

impl Display for CliError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
CliError::NotAVector => write!(f, "Not a vector"),
}
}
}

impl Error for CliError {}

#[derive(Debug)]
pub(crate) struct State {
pub document: Document,
pub draw_state: DrawState,
pub draw_layer: LayerID,
}

impl State {
pub(crate) fn draw(&mut self) -> LayerDrawer {
LayerDrawer {
state: &self.draw_state,
layer: self.document.get_mut(self.draw_layer),
}
}
struct Options {
verbose: bool,
commands: Vec<DynCommand>,
}

impl Default for State {
fn default() -> Self {
Self {
draw_layer: 1,
draw_state: DrawState::default(),
document: Document::default(),
}
}
}

type CommandAction = dyn Fn(&CommandValue, &mut State) -> Result<(), Box<dyn Error>>;

pub(crate) struct CommandDesc<'a> {
pub(crate) id: Id,
pub(crate) arg: Arg,
pub(crate) multiple_args: bool,
pub(crate) action: &'a CommandAction,
}

impl<'a> CommandDesc<'a> {
pub(crate) fn new(arg: Arg, action: &'a CommandAction) -> Self {
let multiple_args = arg.get_num_args().unwrap_or_default().max_values() > 1;
Self {
id: arg.get_id().clone(),
arg: arg.group("commands").action(ArgAction::Append),
multiple_args,
action,
}
}
}

pub(crate) fn cli(command_descs: &HashMap<Id, CommandDesc>) -> Command {
let mut cli = command!()
.args([
arg!(<PATH> "Path to the SVG file (or '-' for stdin)")
.value_parser(value_parser!(PathBuf)),
Arg::new("single-layer")
.long("single")
.help("Single layer mode")
.num_args(0),
Arg::new("no-show")
.long("no-show")
.help("Don't show the GUI")
.num_args(0),
arg!(-v --verbose "Enable debug output"),
])
.group(ArgGroup::new("commands").multiple(true))
.next_help_heading("COMMANDS");

for command in command_descs.values() {
cli = cli.arg(command.arg.clone());
}

cli
/// Parser for all possible commands.
fn command() -> impl Parser<DynCommand> {
let context = context::parser();
let draw = draw::parser();
let io = io::parser();
let layers = layers::parser();
let ops = ops::parser();
let transform = transforms::parser();
construct!([context, io, layers, ops, transform, draw])
}

#[derive(Clone, PartialEq, Debug)]
pub enum CommandValue {
Bool(bool),
String(String),
Float(f64),
Vector(Vec<CommandValue>),
LayerID(LayerID),
/// Parser for the top-level options.
fn options() -> OptionParser<Options> {
let verbose = short('v')
.long("verbose")
.help("Enable verbose output")
.switch();
let commands = command().many();
construct!(Options { verbose, commands }).to_options()
}

impl CommandValue {
pub(crate) fn from_matches(
matches: &ArgMatches,
command_descs: &HashMap<Id, CommandDesc>,
) -> Vec<(Id, Self)> {
let mut values = BTreeMap::new();
for id in matches.ids() {
if matches.try_get_many::<Id>(id.as_str()).is_ok() {
// ignore groups
continue;
}
let value_source = matches
.value_source(id.as_str())
.expect("id came from matches");
if value_source != clap::parser::ValueSource::CommandLine {
// Any other source just gets tacked on at the end (like default values)
continue;
}

let desc = command_descs.get(id).expect("id came from matches");

if Self::extract::<String>(matches, id, desc, &mut values) {
continue;
}
if Self::extract::<bool>(matches, id, desc, &mut values) {
continue;
}
if Self::extract::<f64>(matches, id, desc, &mut values) {
continue;
}
if Self::extract::<LayerID>(matches, id, desc, &mut values) {
continue;
}
unimplemented!("unknown type for {}: {:?}", id, matches);
}
values.into_values().collect::<Vec<_>>()
}

fn extract<T: CommandArg>(
matches: &ArgMatches,
id: &Id,
command_desc: &CommandDesc,
output: &mut BTreeMap<usize, (Id, Self)>,
) -> bool {
#[allow(clippy::match_same_arms)]
match matches.try_get_many::<T>(id.as_str()) {
Ok(Some(_)) => {
let occurrences: Vec<Vec<T>> = matches
.get_occurrences(id.as_str())
.expect("id came from matches")
.map(|occ| occ.cloned().collect())
.collect();

let indices: Vec<usize> = matches
.indices_of(id.as_str())
.expect("id came from matches")
.collect();
/// Run the CLI.
pub fn cli() -> anyhow::Result<()> {
let options = options().run();

let mut indices_idx = 0_usize;
for value in &occurrences {
let index = indices[indices_idx];
let mut state = State::default();

if command_desc.multiple_args {
output.insert(index, (id.clone(), value.clone().into()));
} else {
output.insert(index, (id.clone(), value[0].clone().into()));
}

indices_idx += value.len();
}

true
}
Ok(None) => {
unreachable!("`ids` only reports what is present")
}
Err(clap::parser::MatchesError::UnknownArgument { .. }) => {
unreachable!("id came from matches")
}
Err(clap::parser::MatchesError::Downcast { .. }) => false,

Err(_) => {
unreachable!("id came from matches")
}
for command in options.commands {
if options.verbose {
println!("Executing stage: {:?}", command);
}
command.execute(&mut state)?;
}

pub(crate) fn try_vector(&self) -> Result<&[CommandValue], CliError> {
match self {
Self::Vector(v) => Ok(&v[..]),
_ => Err(CliError::NotAVector),
}
}
}

impl From<String> for CommandValue {
fn from(other: String) -> Self {
Self::String(other)
}
}

impl From<bool> for CommandValue {
fn from(other: bool) -> Self {
Self::Bool(other)
}
}

impl From<f64> for CommandValue {
fn from(other: f64) -> Self {
Self::Float(other)
}
}

impl From<LayerID> for CommandValue {
fn from(other: LayerID) -> Self {
Self::LayerID(other)
}
}

impl<T: CommandArg> From<Vec<T>> for CommandValue {
fn from(other: Vec<T>) -> Self {
Self::Vector(other.into_iter().map(std::convert::Into::into).collect())
}
Ok(())
}
Loading
Loading