use std::io::{Write, stdout};
use carbon14::{Error, TableV1, clipboard_lines, stdin_lines};
use clap::Parser;
use iocore::{Error as IOCoreError, Path, WalkProgressHandler, walk_dir};
use serde::Serialize;
use serde_yaml;
#[derive(Parser, Debug, Clone)]
#[command(author, version, about, long_about = None)]
#[command(propagate_version = true)]
pub struct Cli {
targets: Vec<String>,
#[clap(short = 'x', long)]
pub hexonly: bool,
#[clap(short, long)]
pub clipboard: bool,
#[clap(short, long)]
pub output: bool,
#[clap(short = 'f', long)]
pub output_file: Option<Path>,
#[clap(short, long, requires = "output_file")]
pub defer_write: bool,
#[clap(short, long)]
pub log_err: bool,
}
impl Cli {
pub fn writer(&mut self) -> FWriter {
FWriter::new(self.path(), self.defer_write, self.log_err)
}
pub fn path(&self) -> Option<Path> {
if self.output {
match self.output_file.clone() {
Some(path) => Some(path),
None =>
if self.targets.len() == 1 {
let path = Path::raw(self.targets[0].clone());
if path.is_file() {
let extension = match path.extension() {
Some(extension) => format!("{}.c14", extension),
None => format!(".c14"),
};
Some(Path::raw(path.with_extension(extension).name()))
} else {
Some(Path::raw(path.name()).with_extension(".c14"))
}
} else {
None
},
}
} else {
None
}
}
pub fn objects(&self) -> Result<Vec<String>, Error> {
let targets: Vec<String> = self
.targets
.iter()
.filter(|s| !s.is_empty())
.map(|s| Path::raw(s))
.filter(|p| p.exists())
.map(|p| p.to_string())
.collect();
let objects = if targets.len() > 0 {
targets
} else {
stdin_lines().or(clipboard_lines()).unwrap_or(Vec::new())
};
if objects.is_empty() {
Err(Error::Error(format!("no targets, try --help")))
} else {
Ok(objects)
}
}
}
struct Carbon14 {
pub cli: Cli,
}
impl Carbon14 {
pub fn new() -> Carbon14 {
let cli = Cli::parse();
Carbon14 { cli }
}
pub fn scan(&mut self) -> Result<FWriter, Error> {
let mut writer = self.cli.writer();
for target in self.cli.objects()? {
let target = Path::raw(&target);
if target.exists() {
let target = target.canonicalize()?.relative_to_cwd();
if target.is_file() {
match target.read_bytes() {
Ok(bytes) => {
let meta = (!self.cli.hexonly)
.then_some(Some(target.to_string()))
.unwrap_or(None);
let table = TableV1::new(meta.clone()).cs(bytes);
writer.append(&table).and(Ok(())).unwrap_or(());
},
Err(e) => {
eprintln!("error reading {}: {}", &target, e);
},
}
} else if target.is_dir() {
walk_dir(
&target,
Table {
opt: self.cli.clone(),
},
None,
)?;
} else {
continue;
}
} else {
let target = target.to_string();
let meta = Some(target.clone());
let table = TableV1::new(meta).cs(target.as_bytes().to_vec());
writer.append(&table)?;
}
}
Ok(writer)
}
pub fn launch() -> Result<(), Error> {
let mut c14 = Carbon14::new();
let mut writer = c14.scan()?;
writer.finish()?;
Ok(())
}
}
pub fn main() {
if let Err(des) = Carbon14::launch() {
eprintln!("{}", des);
std::process::exit(0o11);
}
}
#[derive(Debug, Clone)]
pub struct FWriter {
path: Option<Path>,
defer_write: bool,
log_err: bool,
buffer: Vec<u8>,
pub standard: bool,
}
impl Default for FWriter {
fn default() -> FWriter {
FWriter {
path: None,
defer_write: true,
log_err: true,
buffer: Vec::new(),
standard: true,
}
}
}
impl FWriter {
pub fn new(path: Option<Path>, defer_write: bool, log_err: bool) -> FWriter {
FWriter {
path,
defer_write,
log_err,
buffer: Vec::new(),
standard: false,
}
}
pub fn append(&mut self, data: impl Serialize) -> Result<FWriter, Error> {
match self.encode(data) {
Ok(bytes) =>
if self.defer_write {
self.buffer.extend_from_slice(&bytes);
} else {
if let Err(y) = self
.path
.clone()
.map(|path| {
if path.exists() {
path.append(&bytes)
} else {
path.write(&bytes)
.map(|path| path.size().unwrap().as_u64() as usize)
}
})
.unwrap_or_else(|| stdout().write(&bytes).map_err(|e| IOCoreError::from(e)))
{
self.handle(y)?;
}
},
Err(y) => self.handle(y)?,
}
Ok(self.clone())
}
pub fn encode(&self, data: impl Serialize) -> Result<Vec<u8>, Error> {
let mut bytes = b"\n---\n".to_vec();
let y = serde_yaml::to_string(&data)
.map_err(|e| {
Error::Error(format!("encoding yaml destined to {}: {}", self.output(), e))
})?
.trim()
.to_string();
bytes.extend_from_slice(y.as_bytes());
bytes.extend_from_slice("#\t∎".as_bytes());
Ok(bytes)
}
pub fn handle(&self, e: impl Into<Error>) -> Result<(), Error> {
let e: Error = e.into();
if self.log_err {
eprintln!("error: {}", e);
Ok(())
} else {
Err(e)
}
}
pub fn finish(&mut self) -> Result<(), Error> {
if self.buffer.is_empty() {
if self.defer_write {
Err(Error::Error(format!("writing data to {}: empty buffer", self.output())))
} else {
Ok(())
}
} else {
let buffer = self.buffer.clone();
self.path
.clone()
.map(|path| path.write(&buffer).map(|_| buffer.len()))
.unwrap_or_else(|| stdout().write(&buffer).map_err(|e| IOCoreError::from(e)))?;
Ok(())
}
}
pub fn path(&self) -> Option<Path> {
self.path.clone()
}
fn output(&self) -> String {
self.path.clone().map(|path| path.to_string()).unwrap_or("stdout".to_string())
}
}
#[derive(Clone, Debug)]
pub struct Table {
opt: Cli,
}
impl WalkProgressHandler for Table {
fn path_matching(&mut self, location: &Path) -> Result<bool, IOCoreError> {
let mut writer = self.opt.writer();
if location.is_file() {
let bytes = location.read_bytes()?;
let meta = (!self.opt.hexonly).then_some(Some(location.to_string())).unwrap_or(None);
let table = TableV1::new(meta.clone()).cs(bytes);
writer.append(&table).and(Ok(())).unwrap_or(());
}
Ok(true)
}
fn error(&mut self, _p: &Path, _e: IOCoreError) -> Option<IOCoreError> {
None
}
}