ghee 0.6.1

That thin layer of data change management over the filesystem
Documentation
use std::{env::current_dir, ffi::OsString, path::PathBuf};

use clap::Parser;
use ghee_cli::{Cli, Commands};
use ghee_lang::Key;
use rustyline::error::ReadlineError;
use rustyline::DefaultEditor;
use thiserror::Error;

use ghee::{
    cmd::{
        commit, cp, create, del, get, idx, init, ins, log, ls, mv, reset, restore, rm, set, status,
        touch, NewFileHandling,
    },
    APP_NAME, PKG_NAME, XDG_DIRS,
};

#[derive(Error, Debug)]
pub enum TableOpenErr {
    #[error("Table path could not be opened because it doesn't exist")]
    NoSuchPath,
}

fn repl() -> rustyline::Result<()> {
    println!("{} {}", *APP_NAME, env!("CARGO_PKG_VERSION"));
    println!();

    let prompt = format!("{}$ ", *PKG_NAME);

    let history_path: PathBuf = XDG_DIRS
        .place_state_file("history.txt")
        .unwrap_or_else(|e| panic!("Error placing history file: {}", e));

    let mut rl = DefaultEditor::new()?;
    if rl.load_history(&history_path).is_err() {
        // do nothing
    }

    loop {
        let readline = rl.readline(&prompt);
        match readline {
            Ok(line) => {
                rl.add_history_entry(line.as_str())?;

                match Cli::try_parse_from(
                    std::iter::once(OsString::from(PKG_NAME.as_str()))
                        .chain(line.split_terminator(" ").map(OsString::from)),
                ) {
                    Ok(args) => match args.command.as_ref() {
                        Some(cmd) => {
                            run_command(cmd);
                        }
                        None => {}
                    },
                    Err(e) => {
                        eprintln!("{}", e);
                    }
                }
            }
            Err(ReadlineError::Interrupted) => {
                println!("CTRL-C");
                break;
            }
            Err(ReadlineError::Eof) => {
                println!("CTRL-D");
                break;
            }
            Err(err) => {
                println!("Error: {:?}", err);
                break;
            }
        }
    }
    rl.save_history(&history_path)
        .unwrap_or_else(|e| panic!("Error saving history to {}: {}", history_path.display(), e));

    Ok(())
}

fn run_command(cmd: &Commands) {
    match cmd {
        Commands::Cp {
            src,
            dest,
            fields,
            verbose,
        } => {
            cp(src, dest, fields, *verbose)
                .unwrap_or_else(|e| panic!("Error copying xattr(s): {}", e));
        }
        Commands::Mv {
            src,
            dest,
            fields,
            verbose,
        } => {
            mv(src, dest, fields, *verbose)
                .unwrap_or_else(|e| panic!("Error moving xattr(s): {}", e));
        }
        Commands::Rm {
            paths,
            fields,
            flat,
            force,
            verbose,
        } => {
            rm(paths, fields, !*flat, *force, *verbose)
                .unwrap_or_else(|e| panic!("Error removing xattr(s): {}", e));
        }
        Commands::Get {
            paths,
            fields,
            json,
            where_,
            flat,
            all,
            sort,
            visit_empty,
        } => get(
            paths,
            fields,
            *json,
            where_,
            !*flat,
            *all,
            *sort,
            *visit_empty,
        )
        .unwrap_or_else(|e| {
            panic!("Error getting record(s): {}", e);
        }),
        Commands::Set {
            paths,
            field_assignments,
            flat,
            verbose,
        } => set(paths, field_assignments, !*flat, *verbose)
            .unwrap_or_else(|e| panic!("Error setting xattrs: {}", e)),

        Commands::Ins {
            table_path,
            records_path,
            verbose,
        } => {
            ins(table_path, records_path, *verbose).unwrap_or_else(|e| {
                panic!(
                    "Error inserting record(s) into {}: {}",
                    table_path.display(),
                    e
                )
            });
        }
        Commands::Del {
            table_path,
            where_,
            key,
            verbose,
        } => {
            del(table_path, where_, key, *verbose).unwrap_or_else(|e| {
                panic!("Error deleting record from {}: {}", table_path.display(), e)
            });
        }
        Commands::Idx {
            src,
            dest,
            keys,
            verbose,
        } => {
            let key = Key::new(keys.clone());
            idx(src, dest.as_ref(), &key, *verbose).unwrap_or_else(|e| {
                panic!(
                    "Error indexing src {} to dest {:?} with keys {:?}: {}",
                    src.display(),
                    dest,
                    keys,
                    e
                )
            });
        }
        Commands::Ls { paths, sort } => {
            ls(paths, *sort).unwrap_or_else(|e| panic!("Error listing {:?}: {}", paths, e));
        }
        Commands::Init {
            dir,
            keys,
            records_path,
            verbose,
        } => {
            let key = Key::new(keys.clone());
            init(dir, &key, *verbose)
                .unwrap_or_else(|e| panic!("Error initializing table {}: {}", dir.display(), e));

            if atty::isnt(atty::Stream::Stdin) {
                //TODO Dedupe from Commands::Ins
                ins(dir, records_path, *verbose).unwrap_or_else(|e| {
                    panic!("Error inserting record(s) into {}: {}", dir.display(), e)
                });
            }
        }
        Commands::Create {
            dir,
            keys,
            records_path,
            verbose,
        } => {
            let key = Key::new(keys.clone());
            create(dir, &key, *verbose)
                .unwrap_or_else(|e| panic!("Error creating table {}: {}", dir.display(), e));

            if atty::isnt(atty::Stream::Stdin) {
                //TODO Dedupe from Commands::Ins
                ins(dir, records_path, *verbose).unwrap_or_else(|e| {
                    panic!("Error inserting record(s) into {}: {}", dir.display(), e)
                });
            }
        }
        Commands::Commit {
            dir,
            message,
            verbose,
        } => {
            let cur = current_dir().unwrap();
            let dir = dir.as_ref().unwrap_or(&cur);

            let uuid = commit(dir, message, *verbose)
                .unwrap_or_else(|e| panic!("Error committing {}: {}", dir.display(), e));

            println!("{}", uuid);
        }

        Commands::Log { dir } => {
            let cur = current_dir().unwrap();
            let dir = dir.as_ref().unwrap_or(&cur);

            log(dir).unwrap_or_else(|e| panic!("Could not print commit log: {}", e));
        }

        Commands::Touch { path, parents } => {
            touch(path, *parents)
                .unwrap_or_else(|e| panic!("Could not touch path {}: {}", path.display(), e));
        }

        Commands::Status { path } => {
            let cur = current_dir().unwrap();
            let path = path.as_ref().unwrap_or(&cur);
            status(path)
                .unwrap_or_else(|e| panic!("Error printing status for {}: {}", cur.display(), e));
        }

        Commands::Restore {
            paths,
            keep,
            flat,
            verbose,
        } => {
            let new_file_handling = if *keep {
                NewFileHandling::Keep
            } else {
                NewFileHandling::Delete
            };
            restore(paths, !*flat, *verbose, new_file_handling)
                .unwrap_or_else(|e| panic!("Error restoring paths {:?}: {}", paths, e));
        }

        Commands::Reset {
            commit_uuid,
            keep,
            verbose,
        } => {
            let new_file_handling = if *keep {
                NewFileHandling::Keep
            } else {
                NewFileHandling::Delete
            };
            reset(commit_uuid, *verbose, new_file_handling)
                .unwrap_or_else(|e| panic!("Error resetting to {}: {}", commit_uuid, e));
        }
    }
}

fn main() {
    let cli = Cli::parse();

    match cli.command.as_ref() {
        None => repl().unwrap_or_else(|e| panic!("Could not initialize REPL: {}", e)),
        Some(command) => run_command(command),
    }
}