use std::{
env::{VarError, var},
fmt::{Debug, Error as FmtError},
fs::{File, create_dir_all, read_to_string},
io::Write,
panic::{self, AssertUnwindSafe},
path::PathBuf,
};
use clap::{CommandFactory, FromArgMatches, Parser};
use ron::de::SpannedError;
use shadow_rs::shadow;
use thiserror::Error;
use tmux_interface::Error as TmuxError;
use crate::cmds::*;
use crate::config::{CmdParsingError, Config, SpaceTreeId};
use crate::database::DataBase;
use crate::new_tree::InteractiveError;
shadow!(build);
pub(crate) mod cmds;
pub mod config;
pub mod database;
pub mod repl;
pub mod utils;
const LONG_ABOUT: &str = "\
Devspace is a tool to save and retrieve your devlopment workspaces.";
pub type Result<T = (), E = DsError> = core::result::Result<T, E>;
#[derive(Error, Debug)]
pub enum DsError {
#[error(transparent)]
VarError(#[from] VarError),
#[error("IO: {0}")]
IoError(#[from] std::io::Error),
#[error("failed to parse a file, {0}.")]
FileParsingError(#[from] SpannedError),
#[error("failed to save the database: {0}")]
DbSavingError(#[from] ron::Error),
#[error("the space {0:?} was not found.")]
SpaceNotFound(String),
#[error("the space {0:?} already exists.")]
SpaceAlreadyExists(String),
#[error("TMUX: {0}")]
TmuxError(#[from] TmuxError),
#[error("space treee {:?} not found", .0.0)]
SpaceTreeNotFound(SpaceTreeId),
#[error("failed to parse command, {0}")]
CmdParsingError(CmdParsingError),
#[error("no space or tree to list.")]
NothingToList,
#[error(transparent)]
FmtError(#[from] FmtError),
#[error(transparent)]
ClapError(#[from] clap::Error),
#[error("failed to parse the command in the REPL.")]
InvalidREPL,
#[error("the directory {0:?} doesn't exists.")]
DirDoesntExists(PathBuf),
#[error(transparent)]
InteractiveError(#[from] InteractiveError),
}
#[derive(Parser, Debug)]
#[command(version, about, long_about = LONG_ABOUT)]
pub struct Cli {
#[command(subcommand)]
subcmds: Option<Command>,
#[arg(long)]
dir: Option<PathBuf>,
}
impl Cli {
pub fn dir(&self) -> Result<PathBuf> {
if let Some(d) = &self.dir {
return Ok(d.to_path_buf());
}
if let Ok(dir) = var("DEVSPACE_DIR") {
return Ok(dir.into());
}
let mut default = PathBuf::from(var("HOME").expect("variable HOME not found wtf"));
default.push(concat!(".", env!("CARGO_PKG_NAME"), "/"));
Ok(default)
}
}
#[derive(Parser, Debug)]
pub enum Command {
Init {
#[arg(default_value = ".")]
path: PathBuf,
tree: Option<SpaceTreeId>,
},
Wdir {
space: String,
},
#[command(visible_alias = "ls")]
ListSpaces,
#[command(visible_alias = "lt")]
ListTrees,
#[command(visible_alias = "rm-s")]
RemoveSpace {
space: String,
},
Go {
space: String,
},
Edit {
space: String,
#[arg(long, short)]
wdir: Option<PathBuf>,
#[arg(long, short)]
tree: Option<SpaceTreeId>,
},
NewTree {
name: String,
},
#[command(visible_alias = "rm-t")]
RemoveTree {
name: String,
},
}
#[derive(Debug, Clone)]
pub struct Context {
dir: PathBuf,
db: DataBase,
config: Config,
terminated: bool,
db_buf: String,
conf_buf: String,
}
impl Context {
pub fn new(dir: PathBuf) -> Result<Context> {
create_dir_all(&dir)?;
let db_path = Context::db_file_path(&dir);
let db_buf = if db_path.exists() {
read_to_string(Context::db_file_path(&dir))?
} else {
ron::ser::to_string_pretty(&DataBase::default(), utils::pretty_printer_config())?
};
let conf_path = Context::conf_file_path(&dir);
let conf_buf = if conf_path.exists() {
read_to_string(Context::conf_file_path(&dir))?
} else {
ron::ser::to_string_pretty(&Config::default(), utils::pretty_printer_config())?
};
Ok(Context {
dir,
db: ron::from_str(&db_buf)?,
config: ron::from_str(&conf_buf)?,
terminated: false,
db_buf,
conf_buf,
})
}
pub fn terminate(&mut self) -> Result {
self.terminated = true;
self.write_db_to_buf()?;
let mut db_file = File::create(Context::db_file_path(&self.dir))?;
db_file.write_all(&self.db_buf.clone().into_bytes())?;
self.write_conf_to_buf()?;
let mut conf_file = File::create(Context::conf_file_path(&self.dir))?;
conf_file.write_all(&self.conf_buf.clone().into_bytes())?;
Ok(())
}
pub(crate) fn write_db_to_buf(&mut self) -> Result {
self.db_buf = ron::ser::to_string_pretty(&self.db, utils::pretty_printer_config())?;
Ok(())
}
pub(crate) fn write_conf_to_buf(&mut self) -> Result {
self.conf_buf = ron::ser::to_string_pretty(&self.config, utils::pretty_printer_config())?;
Ok(())
}
pub(crate) fn db_file_path(dir: impl Into<PathBuf>) -> PathBuf {
let mut dir = dir.into();
dir.push("db.ron");
dir
}
pub(crate) fn conf_file_path(dir: impl Into<PathBuf>) -> PathBuf {
let mut dir = dir.into();
dir.push("config.ron");
dir
}
pub fn session_name(&self, space: &str) -> String {
let mut sname = String::from("Space_");
sname.push_str(space);
sname
}
}
impl Drop for Context {
fn drop(&mut self) {
if !self.terminated {
let res = panic::catch_unwind(AssertUnwindSafe(|| {
#[cfg(debug_assertions)]
eprintln!("INFO: Manually terminating the Context in the drop implementation.");
self.terminate().unwrap();
}));
if res.is_err() {
eprintln!(
"FATAL: The `drop` implementation of Context has panicked but luckily we catched it!"
);
}
}
}
}
pub fn run_command(args: Cli, ctx: &mut Context, repl: bool) -> Result {
match args.subcmds {
Some(Command::Init { path, tree }) => init::command(ctx, path, tree)?,
Some(Command::Wdir { space }) => wdir::command(ctx, space)?,
Some(Command::ListSpaces) => list_spaces::command(ctx)?,
Some(Command::ListTrees) => list_trees::command(ctx)?,
Some(Command::RemoveSpace { space }) => remove_space::command(ctx, space)?,
Some(Command::Go { space }) => go::command(ctx, space)?,
Some(Command::Edit { space, wdir, tree }) => edit::command(ctx, space, wdir, tree)?,
Some(Command::NewTree { name }) => new_tree::command(ctx, name)?,
Some(Command::RemoveTree { name }) => remove_tree::command(ctx, name)?,
None if !repl => {
repl::run()?;
}
None => {}
}
Ok(())
}
pub fn run() -> Result {
let matches = Cli::command()
.version(build::CLAP_LONG_VERSION)
.get_matches();
let args = Cli::from_arg_matches(&matches)?;
let mut ctx = Context::new(args.dir()?)?;
run_command(args, &mut ctx, false)?;
ctx.terminate()?;
Ok(())
}