use clap::{CommandFactory, Parser, Subcommand};
use color_eyre::eyre::Result;
use color_eyre::Report;
use figment::providers::{Env, Format, Json};
use figment::Figment;
use leafslug_jnl::{book, effectors, Entry, Mood};
use std::env;
use std::{ffi::OsString, path::PathBuf, sync::Arc};
use time::{format_description, OffsetDateTime};
const DEFAULT_CONFIG_PATH: &str = "/home/a/.config/leafslug/jnl.json";
fn main() -> Result<()> {
color_eyre::install()?;
tracing_subscriber::fmt::init();
let args = Args::parse();
let config = Configs::try_from(&args.config.unwrap_or(PathBuf::from(DEFAULT_CONFIG_PATH)))?;
let time_offset = config
.time_offset
.ok_or(Report::msg("time offset for is not set"))
.map(|(h, m, s)| time::UtcOffset::from_hms(h, m, s))??;
let now = OffsetDateTime::now_utc().to_offset(time_offset);
let Some(command) = args.command else {
return Ok(());
};
router(command, now, &config)?;
Ok(())
}
fn router(journal_command: Commands, now: OffsetDateTime, config: &Configs) -> Result<(), Report> {
match journal_command {
Commands::Completions { shell } => {
shell.generate(&mut Args::command(), &mut std::io::stdout());
Ok::<_, Report>(())
}
Commands::New {
entry,
tag,
mood,
people,
} => {
let repo_root = leafslug_effects::git::repo_root(&config.journal_path()?)?
.to_string_lossy()
.into_owned();
effectors::new_entry(
&Entry {
at: now,
body: Arc::new(entry),
tag,
mood,
people,
},
&config.journal_path()?,
&repo_root,
now,
&config.journal_file_formatting()?,
)?;
Ok(())
}
Commands::List => {
let format = time::format_description::parse_borrowed::<2>(
"[year]-[month]-[day] [hour]:[minute]",
)?;
effectors::list_entries(book(&config.journal_path()?)?, &format)?;
Ok(())
}
Commands::Edit(edit_type) => {
let repo_root = leafslug_effects::git::repo_root(&config.journal_path()?)?
.to_string_lossy()
.into_owned();
match edit_type {
JournalEditCommands::Last => {
effectors::edit_last_entry(
&config.journal_path()?,
book(&config.journal_path()?)?,
&repo_root,
editor(env::var_os("EDITOR"))?,
)?;
Ok(())
}
JournalEditCommands::All => {
effectors::edit_all_entries(
editor(env::var_os("EDITOR"))?,
book(&config.journal_path()?)?,
&repo_root,
&config.journal_path()?,
)?;
Ok(())
}
JournalEditCommands::Specific { item } => {
effectors::edit_specific_entry(
&config.journal_path()?,
&item,
book(&config.journal_path()?)?,
&repo_root,
editor(env::var_os("EDITOR"))?,
)?;
Ok(())
}
}
}
}?;
Ok(())
}
fn editor(extractor: Option<OsString>) -> Result<String> {
let editor = extractor.ok_or(Report::msg("editor variable is not specified"))?;
if editor.is_empty() {
return Err(Report::msg("editor variable is not specified"));
};
match editor.into_string() {
Ok(s) => Ok(s),
Err(e) => Err(Report::msg(format!(
"could not convert file name to string: {e:?}"
))),
}
}
#[derive(Clone, Debug, Parser)]
#[command(version,about="Personal journaling, like never before", long_about = None, arg_required_else_help = true)]
pub struct Args {
#[arg(short, long, value_name = "FILE")]
pub config: Option<PathBuf>,
#[command(subcommand)]
pub command: Option<Commands>,
}
#[derive(Clone, Debug, Subcommand)]
pub enum Commands {
Completions {
#[arg(value_enum)]
shell: clap_complete_command::Shell,
},
New {
entry: String,
#[arg(short = 't', long)]
tag: Vec<String>,
#[arg(short = 'm', long)]
mood: Mood,
#[arg(short = 'p', long)]
people: Vec<String>,
},
List,
#[command(subcommand)]
Edit(JournalEditCommands),
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Subcommand)]
pub enum JournalEditCommands {
Last,
All,
Specific { item: String },
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Configs {
pub time_offset: Option<(i8, i8, i8)>,
pub path: Option<PathBuf>,
pub file_name_format: Option<String>,
}
impl TryFrom<PathBuf> for Configs {
type Error = Error;
fn try_from(value: PathBuf) -> Result<Self, Self::Error> {
Ok(Figment::new()
.merge(Json::file(value))
.merge(Env::prefixed("JNL_"))
.extract()?)
}
}
impl TryFrom<&PathBuf> for Configs {
type Error = Error;
fn try_from(value: &PathBuf) -> Result<Self, Self::Error> {
Ok(Figment::new()
.merge(Json::file(value))
.merge(Env::prefixed("JNL_"))
.extract()?)
}
}
#[derive(Clone, Debug, PartialEq, thiserror::Error)]
pub enum Error {
#[error("could not extract configuration: {0}")]
ExtractionFailed(#[from] figment::Error),
#[error("The path to the directory is not given.")]
DirDoesNotExist,
#[error("{0} was not set in the the configuration.")]
UnsetConfiguration(String),
#[error("File format descriptor for journal is not valid: {0}.")]
TheFormatIsNotValid(#[from] time::error::InvalidFormatDescription),
}
impl Configs {
pub fn journal_path(&self) -> Result<PathBuf, Error> {
self.clone().path.ok_or(Error::DirDoesNotExist)
}
pub fn journal_file_formatting(&self) -> Result<format_description::OwnedFormatItem, Error> {
Ok(format_description::parse_owned::<2>(
&self
.clone()
.file_name_format
.ok_or(Error::UnsetConfiguration("file_name_format".to_owned()))?,
)?)
}
}