diff --git a/imag-view/Cargo.toml b/imag-view/Cargo.toml new file mode 100644 index 00000000..5dd2504d --- /dev/null +++ b/imag-view/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "imag-view" +version = "0.1.0" +authors = ["Matthias Beyer "] + +[dependencies] +clap = "2.1.1" +glob = "0.2.11" +log = "0.3.5" +rustbox = "0.8.1" +semver = "0.2.1" +toml = "0.1.25" +version = "2.0.1" + +[dependencies.libimagstore] +path = "../libimagstore" + +[dependencies.libimagrt] +path = "../libimagrt" + +[dependencies.libimagutil] +path = "../libimagutil" + diff --git a/imag-view/src/error.rs b/imag-view/src/error.rs new file mode 100644 index 00000000..57379f43 --- /dev/null +++ b/imag-view/src/error.rs @@ -0,0 +1,89 @@ +use std::error::Error; +use std::fmt::Error as FmtError; +use std::clone::Clone; +use std::fmt::{Debug, Display, Formatter}; +use std::fmt; +use std::convert::From; + +/** + * Kind of store error + */ +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum ViewErrorKind { + StoreError, + NoVersion, + PatternError, + GlobBuildError, +} + +fn view_error_type_as_str(e: &ViewErrorKind) -> &'static str { + match e { + &ViewErrorKind::StoreError => "Store error", + &ViewErrorKind::NoVersion => "No version specified", + &ViewErrorKind::PatternError => "Error in Pattern", + &ViewErrorKind::GlobBuildError => "Could not build glob() Argument", + } +} + +impl Display for ViewErrorKind { + + fn fmt(&self, fmt: &mut Formatter) -> Result<(), FmtError> { + try!(write!(fmt, "{}", view_error_type_as_str(self))); + Ok(()) + } + +} + +/** + * View error type + */ +#[derive(Debug)] +pub struct ViewError { + err_type: ViewErrorKind, + cause: Option>, +} + +impl ViewError { + + /** + * Build a new ViewError from an ViewErrorKind, optionally with cause + */ + pub fn new(errtype: ViewErrorKind, cause: Option>) + -> ViewError + { + ViewError { + err_type: errtype, + cause: cause, + } + } + + /** + * Get the error type of this ViewError + */ + pub fn err_type(&self) -> ViewErrorKind { + self.err_type.clone() + } + +} + +impl Display for ViewError { + + fn fmt(&self, fmt: &mut Formatter) -> Result<(), FmtError> { + try!(write!(fmt, "[{}]", view_error_type_as_str(&self.err_type.clone()))); + Ok(()) + } + +} + +impl Error for ViewError { + + fn description(&self) -> &str { + view_error_type_as_str(&self.err_type.clone()) + } + + fn cause(&self) -> Option<&Error> { + self.cause.as_ref().map(|e| &**e) + } + +} + diff --git a/imag-view/src/main.rs b/imag-view/src/main.rs new file mode 100644 index 00000000..1b4d5222 --- /dev/null +++ b/imag-view/src/main.rs @@ -0,0 +1,185 @@ +extern crate clap; +extern crate glob; +#[macro_use] extern crate log; +extern crate semver; +extern crate toml; +#[macro_use] extern crate version; + +extern crate libimagrt; +extern crate libimagstore; +extern crate libimagutil; + +use std::result::Result as RResult; +use std::process::exit; + +use libimagrt::runtime::Runtime; +use libimagstore::store::FileLockEntry; +use libimagstore::store::Result as StoreResult; +use libimagutil::trace::trace_error; + +mod error; +mod ui; +mod viewer; + +use error::{ViewError, ViewErrorKind}; +use ui::build_ui; +use viewer::Viewer; +use viewer::ViewInformation; +use viewer::stdout::StdoutViewer; + +type Result = RResult; + +fn main() { + let name = "imag-view"; + let version = &version!()[..]; + let about = "View entries (readonly)"; + let ui = build_ui(Runtime::get_default_cli_builder(name, version, about)); + let rt = { + let rt = Runtime::new(ui); + if rt.is_ok() { + rt.unwrap() + } else { + println!("Could not set up Runtime"); + println!("{:?}", rt.err().unwrap()); + exit(1); // we can afford not-executing destructors here + } + }; + + rt.init_logger(); + + debug!("Hello. Logging was just enabled"); + debug!("I already set up the Runtime object and build the commandline interface parser."); + debug!("Lets get rollin' ..."); + + info!("No implementation yet"); + + let entry_id = rt.cli().value_of("id").unwrap(); // enforced by clap + + if rt.cli().is_present("versions") { + if let Err(e) = view_versions_of(entry_id, &rt) { + trace_error(&e); + exit(1); // we can afford not-executing destructors here + } + } else { + let entry_version = rt.cli().value_of("version"); + let view_header = rt.cli().is_present("view-header"); + let view_content = rt.cli().is_present("view-content"); + let view_copy = rt.cli().is_present("view-copy"); + let keep_copy = rt.cli().is_present("keep-copy"); + + let scmd = rt.cli().subcommand_matches("view-in"); + if scmd.is_none() { + debug!("No commandline call"); + exit(1); // we can afford not-executing destructors here + } + let scmd = scmd.unwrap(); + + let viewer = { + if scmd.is_present("view-in-stdout") { + Box::new(StdoutViewer::new()) + } else if scmd.is_present("view-in-ui") { + warn!("Viewing in UI is currently not supported, switch to stdout"); + Box::new(StdoutViewer::new()) + } else if scmd.is_present("view-in-browser") { + warn!("Viewing in browser is currently not supported, switch to stdout"); + Box::new(StdoutViewer::new()) + } else if scmd.is_present("view-in-texteditor") { + warn!("Viewing in texteditor is currently not supported, switch to stdout"); + Box::new(StdoutViewer::new()) + } else if scmd.is_present("view-in-custom") { + warn!("Viewing in custom is currently not supported, switch to stdout"); + Box::new(StdoutViewer::new()) + } else { + Box::new(StdoutViewer::new()) + } + }; + + let entry = load_entry(entry_id, entry_version, &rt); + if entry.is_err() { + trace_error(&entry.err().unwrap()); + exit(1); // we can afford not-executing destructors here + } + let entry = entry.unwrap(); + + let view_info = ViewInformation { + entry: entry, + view_header: view_header, + view_content: view_content, + view_copy: view_copy, + keep_copy: keep_copy, + }; + + viewer.view(view_info); + } +} + +// TODO: This is a shameless adaption of imag-store/src/util.rs +fn load_entry<'a>(id: &str, + version: Option<&str>, + rt: &'a Runtime) + -> Result> +{ + debug!("Checking path element for version"); + + let version = { + if version.is_none() { + let r = id.split("~").last(); + if r.is_none() { + warn!("No version"); + return Err(ViewError::new(ViewErrorKind::NoVersion, None)); + } else { + r.unwrap() + } + } else { + version.unwrap() + } + }; + + debug!("Building path from {:?} and {:?}", id, version); + let mut path = rt.store().path().clone(); + + if id.chars().next() == Some('/') { + path.push(format!("{}~{}", &id[1..id.len()], version)); + } else { + path.push(format!("{}~{}", id, version)); + } + + // the above is the adaption... + + rt.store().retrieve(path) + .map_err(|e| ViewError::new(ViewErrorKind::StoreError, Some(Box::new(e)))) +} + +fn view_versions_of(id: &str, rt: &Runtime) -> Result<()> { + use glob::glob; + + let mut path = rt.store().path().clone(); + + if id.chars().next() == Some('/') { + path.push(format!("{}~*", &id[1..id.len()])); + } else { + path.push(format!("{}~*", id)); + } + + if let Some(path) = path.to_str() { + match glob(path) { + Ok(paths) => { + for entry in paths { + match entry { + Ok(path) => println!("{}", path.file_name().and_then(|s| s.to_str()).unwrap()), + Err(e) => trace_error(e.error()), + } + } + Ok(()) + }, + Err(e) => { + debug!("Error in pattern"); + Err(ViewError::new(ViewErrorKind::PatternError, Some(Box::new(e)))) + }, + } + } else { + warn!("Could not build glob() argument!"); + Err(ViewError::new(ViewErrorKind::GlobBuildError, None)) + } +} + diff --git a/imag-view/src/ui.rs b/imag-view/src/ui.rs new file mode 100644 index 00000000..619e4f27 --- /dev/null +++ b/imag-view/src/ui.rs @@ -0,0 +1,119 @@ +use clap::{Arg, App, ArgGroup, SubCommand}; + +pub fn build_ui<'a>(app: App<'a, 'a>) -> App<'a, 'a> { + app + .arg(Arg::with_name("id") + .long("id") + .short("i") + .takes_value(true) + .required(true) + .help("View this entry at this store path")) + + .arg(Arg::with_name("version") + .long("version") + .short("V") + .takes_value(true) + .required(false) + .help("View this version (youngest if not specified)")) + + .arg(Arg::with_name("versions") + .long("versions") + .takes_value(false) + .required(false) + .help("Only print available versions for this file")) + + .arg(Arg::with_name("view-header") + .long("header") + .short("h") + .takes_value(false) + .required(false) + .help("View header")) + .arg(Arg::with_name("view-content") + .long("content") + .short("C") + .takes_value(false) + .required(false) + .help("View content")) + + .arg(Arg::with_name("view-copy") + .long("copy") + .takes_value(false) + .required(false) + .help("Copy before opening (copies to /tmp/) and removes the file after viewing.")) + + .arg(Arg::with_name("keep-copy") + .long("keep-copy") + .short("k") + .takes_value(false) + .required(false) + .help("If --copy was passed, keep the copy after viewing.")) + + .subcommand(SubCommand::with_name("view-in") + .about("View the entry in ...") + .version("0.1") + + .arg(Arg::with_name("view-in-stdout") + .long("stdout") + .short("s") + .takes_value(false) + .required(false) + .help("View by printing to stdout")) + + .arg(Arg::with_name("view-in-ui") + .long("ui") + .short("u") + .takes_value(false) + .required(false) + .help("View by opening own curses-like UI (default)")) + + .arg(Arg::with_name("view-in-browser") + .long("browser") + .short("b") + .takes_value(true) // optional, which browser + .required(false) + .help("View content in $BROWSER (fails if no env variable $BROWSER)")) + + .arg(Arg::with_name("view-in-texteditor") + .long("editor") + .short("e") + .takes_value(true) // optional, which editor + .required(false) + .help("View content in $EDITOR")) + + .arg(Arg::with_name("view-in-custom") + .long("custom") + .short("c") + .takes_value(true) // non-optional, call-string + .required(false) + .help("View content in custom program, for example 'libreoffice %e', replace '%e' with entry path")) + + .group(ArgGroup::with_name("viewer") + .args(&["view-in-stdout", + "view-in-ui", + "view-in-browser", + "view-in-texteditor", + "view-in-custom", + ]) + .required(false)) + ) + + .subcommand(SubCommand::with_name("compile") + .about("Compile content to other format for viewing, implies that the entry gets copied to /tmp") + .version("0.1") + .arg(Arg::with_name("from") + .long("from") + .short("f") + .takes_value(true) // "markdown" or "textile" or "restructuredtex" + .required(true) + .help("Compile from")) + + .arg(Arg::with_name("to") + .long("to") + .short("t") + .takes_value(true) // "html" or "HTML" or ... json maybe? + .required(true) + .help("Compile to")) + ) +} + + diff --git a/imag-view/src/viewer/mod.rs b/imag-view/src/viewer/mod.rs new file mode 100644 index 00000000..f7e3653f --- /dev/null +++ b/imag-view/src/viewer/mod.rs @@ -0,0 +1,16 @@ +pub mod stdout; + +use libimagstore::store::FileLockEntry; + +pub struct ViewInformation<'a> { + pub entry: FileLockEntry<'a>, + pub view_header: bool, + pub view_content: bool, + pub view_copy: bool, + pub keep_copy: bool, +} + +pub trait Viewer { + fn view(&self, vi: ViewInformation); +} + diff --git a/imag-view/src/viewer/stdout.rs b/imag-view/src/viewer/stdout.rs new file mode 100644 index 00000000..a42af1c1 --- /dev/null +++ b/imag-view/src/viewer/stdout.rs @@ -0,0 +1,39 @@ +use std::io::{Stdout, stdout}; + +use toml::encode_str; + +use viewer::{ViewInformation, Viewer}; + +pub struct StdoutViewer { + out: Stdout, +} + +impl StdoutViewer { + + pub fn new() -> StdoutViewer { + StdoutViewer { out: stdout() } + } + +} + +impl Viewer for StdoutViewer { + + fn view(&self, vi: ViewInformation) { + if vi.view_copy { + unimplemented!(); + } + + if vi.view_header { + println!("{}", encode_str(vi.entry.get_header().header())); + } + + if vi.view_content { + println!("{}", vi.entry.get_content()); + } + + if vi.view_copy && !vi.keep_copy { + unimplemented!() + } + } + +}