feat: cli base

Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
2025-10-13 13:48:49 +01:00
parent 4e1d105787
commit 73c75f941a
13 changed files with 1426 additions and 0 deletions

1
.envrc Normal file
View File

@@ -0,0 +1 @@
use flake self#rust

1130
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

21
Cargo.toml Normal file
View File

@@ -0,0 +1,21 @@
[package]
name = "fujicli"
version = "0.1.0"
edition = "2024"
description = "A CLI to manage Fujifilm devices, simulations, backups, and rendering"
authors = [
"Nikolaos Karaolidis <nick@karaolidis.com>",
]
[profile.release]
panic = 'abort'
strip = true
lto = true
codegen-units = 1
[dependencies]
clap = { version = "4.5.48", features = ["derive", "wrap_help"] }
libptp = "0.6.5"
log = "0.4.28"
log4rs = "1.4.0"
rusb = "0.9.4"

19
src/cli/backup.rs Normal file
View File

@@ -0,0 +1,19 @@
use super::common::file::{Input, Output};
use clap::Subcommand;
#[derive(Subcommand, Debug)]
pub enum BackupCmd {
/// Export backup
#[command(alias = "e")]
Export {
/// Output file (use '-' to write to stdout)
output_file: Output,
},
/// Import backup
#[command(alias = "i")]
Import {
/// Input file (use '-' to read from stdin)
input_file: Input,
},
}

35
src/cli/common/file.rs Normal file
View File

@@ -0,0 +1,35 @@
use std::{error::Error, path::PathBuf, str::FromStr};
#[derive(Debug, Clone)]
pub enum Input {
Path(PathBuf),
Stdin,
}
impl FromStr for Input {
type Err = Box<dyn Error + Send + Sync>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s == "-" {
Ok(Input::Stdin)
} else {
Ok(Input::Path(PathBuf::from(s)))
}
}
}
#[derive(Debug, Clone)]
pub enum Output {
Path(PathBuf),
Stdout,
}
impl FromStr for Output {
type Err = Box<dyn Error + Send + Sync>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s == "-" {
Ok(Output::Stdout)
} else {
Ok(Output::Path(PathBuf::from(s)))
}
}
}

28
src/cli/common/film.rs Normal file
View File

@@ -0,0 +1,28 @@
use std::error::Error;
use clap::Args;
#[derive(Debug, Clone)]
pub enum SimulationSelector {
Slot(u8),
Name(String),
}
impl std::str::FromStr for SimulationSelector {
type Err = Box<dyn Error + Send + Sync>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Ok(slot) = s.parse::<u8>() {
return Ok(SimulationSelector::Slot(slot));
}
if s.is_empty() {
Err("Simulation name cannot be empty".into())
} else {
Ok(SimulationSelector::Name(s.to_string()))
}
}
}
#[derive(Args, Debug)]
pub struct FilmSimulationOptions {}

2
src/cli/common/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod file;
pub mod film;

12
src/cli/device.rs Normal file
View File

@@ -0,0 +1,12 @@
use clap::Subcommand;
#[derive(Subcommand, Debug)]
pub enum DeviceCmd {
/// List devices
#[command(alias = "l")]
List,
/// Dump device info
#[command(alias = "i")]
Info,
}

55
src/cli/mod.rs Normal file
View File

@@ -0,0 +1,55 @@
mod backup;
mod common;
mod device;
mod render;
mod simulation;
use clap::{Parser, Subcommand};
use backup::BackupCmd;
use device::DeviceCmd;
use render::RenderCmd;
use simulation::SimulationCmd;
#[derive(Parser, Debug)]
#[command(version, about, long_about = None, author)]
pub struct Cli {
/// Subcommands
#[command(subcommand)]
pub command: Commands,
/// Format output using json
#[arg(long, short = 'j', global = true)]
pub json: bool,
/// Only log warnings and errors
#[arg(long, short = 'q', global = true, conflicts_with = "verbose")]
pub quiet: bool,
/// Log extra debugging information
#[arg(long, short = 'v', global = true, conflicts_with = "quiet")]
pub verbose: bool,
/// Manually specify target device
#[arg(long, short = 'd', global = true)]
pub device: Option<String>,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
/// Manage devices
#[command(alias = "d", subcommand)]
Device(DeviceCmd),
/// Manage film simulations
#[command(alias = "s", subcommand)]
Simulation(SimulationCmd),
/// Manage backups
#[command(alias = "b", subcommand)]
Backup(BackupCmd),
/// Render images
#[command(alias = "r")]
Render(RenderCmd),
}

27
src/cli/render.rs Normal file
View File

@@ -0,0 +1,27 @@
use std::path::PathBuf;
use super::common::{
file::{Input, Output},
film::{FilmSimulationOptions, SimulationSelector},
};
use clap::Args;
#[derive(Args, Debug)]
pub struct RenderCmd {
/// Simulation number or name
#[arg(long, conflicts_with = "simulation_file")]
simulation: Option<SimulationSelector>,
/// Path to exported simulation
#[arg(long, conflicts_with = "simulation")]
simulation_file: Option<PathBuf>,
#[command(flatten)]
film_simulation_options: FilmSimulationOptions,
/// RAF input file (use '-' to read from stdin)
input: Input,
/// Output file (use '-' to write to stdout)
output: Output,
}

49
src/cli/simulation.rs Normal file
View File

@@ -0,0 +1,49 @@
use super::common::{
file::{Input, Output},
film::{FilmSimulationOptions, SimulationSelector},
};
use clap::Subcommand;
#[derive(Subcommand, Debug)]
pub enum SimulationCmd {
/// List simulations
#[command(alias = "l")]
List,
/// Get simulation
#[command(alias = "g")]
Get {
/// Simulation number or name
simulation: SimulationSelector,
},
/// Set simulation parameters
#[command(alias = "s")]
Set {
/// Simulation number or name
simulation: SimulationSelector,
#[command(flatten)]
film_simulation_options: FilmSimulationOptions,
},
/// Export simulation
#[command(alias = "e")]
Export {
/// Simulation number or name
simulation: SimulationSelector,
/// Output file (use '-' to write to stdout)
output_file: Output,
},
/// Import simulation
#[command(alias = "i")]
Import {
/// Simulation number
slot: u8,
/// Input file (use '-' to read from stdin)
input_file: Input,
},
}

32
src/log.rs Normal file
View File

@@ -0,0 +1,32 @@
use log::LevelFilter;
use log4rs::{
Config,
append::console::{ConsoleAppender, Target},
config::{Appender, Root},
encode::pattern::PatternEncoder,
};
pub fn init(quiet: bool, verbose: bool) -> Result<(), Box<dyn std::error::Error>> {
let level = if quiet {
LevelFilter::Warn
} else if verbose {
LevelFilter::Debug
} else {
LevelFilter::Info
};
let encoder = Box::new(PatternEncoder::new("{d} {h({l})} {M}::{L} - {m}{n}"));
let console = ConsoleAppender::builder()
.encoder(encoder)
.target(Target::Stderr)
.build();
let config = Config::builder()
.appender(Appender::builder().build("stderr", Box::new(console)))
.build(Root::builder().appender("stderr").build(level))?;
log4rs::init_config(config)?;
Ok(())
}

15
src/main.rs Normal file
View File

@@ -0,0 +1,15 @@
use clap::Parser;
mod cli;
mod log;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = cli::Cli::parse();
log::init(cli.quiet, cli.verbose)?;
match cli.command {
_ => {}
}
Ok(())
}