summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2021-03-26 09:00:31 +0200
committerLars Wirzenius <liw@liw.fi>2021-03-26 11:27:51 +0200
commitd68ed957f785f4e6969a213e05e4a6bbfc7c391a (patch)
tree27f29481bb21112449802bd071592e8e143852f4 /src
parent2288d53ce4e55e28cf8f17e3cd06cc5905b88223 (diff)
downloadjt2-d68ed957f785f4e6969a213e05e4a6bbfc7c391a.tar.gz
feat! rewrite code
This started out as a change to re-do the command line parsing. I ended up rewriting everything, and failed to do it in a way that could be rebased into a sensible series of small commits.
Diffstat (limited to 'src')
-rw-r--r--src/bin/jt2.rs76
-rw-r--r--src/config.rs115
-rw-r--r--src/error.rs28
-rw-r--r--src/lib.rs4
-rw-r--r--src/main.rs127
-rw-r--r--src/opt.rs70
6 files changed, 292 insertions, 128 deletions
diff --git a/src/bin/jt2.rs b/src/bin/jt2.rs
new file mode 100644
index 0000000..cf55aef
--- /dev/null
+++ b/src/bin/jt2.rs
@@ -0,0 +1,76 @@
+use jt2::config::Configuration;
+use jt2::error::JournalError;
+use jt2::opt::{Opt, SubCommand};
+
+use log::debug;
+use std::fs;
+use std::path::Path;
+use std::process::Command;
+use structopt::StructOpt;
+
+fn main() -> anyhow::Result<()> {
+ pretty_env_logger::init_custom_env("JT_LOG");
+ let opt = Opt::from_args();
+ let config = Configuration::read(&opt)?;
+ match opt.cmd {
+ SubCommand::Config => config.dump(),
+ SubCommand::Init {
+ journalname,
+ description,
+ } => init(&config.dirname, &journalname, &description)?,
+ SubCommand::IsJournal => is_journal(&config.dirname)?,
+ SubCommand::New { title } => new_draft(&title, &config.dirname, &config.editor)?,
+ SubCommand::Edit => edit_draft(&config.dirname, &config.editor)?,
+ SubCommand::Finish => finish_draft(&config.dirname)?,
+ }
+ Ok(())
+}
+
+fn init(dirname: &Path, _journalname: &str, _description: &str) -> anyhow::Result<()> {
+ std::fs::create_dir(dirname)
+ .map_err(|err| JournalError::CreateDirectory(dirname.to_path_buf(), err))?;
+ Ok(())
+}
+
+fn is_journal(dirname: &Path) -> anyhow::Result<()> {
+ let meta = fs::symlink_metadata(dirname)?;
+ if !meta.is_dir() {
+ return Err(JournalError::NotAJournal(dirname.display().to_string()).into());
+ }
+ Ok(())
+}
+
+fn new_draft(title: &str, dirname: &Path, _editor: &str) -> anyhow::Result<()> {
+ let drafts = dirname.join("drafts");
+ if !drafts.exists() {
+ std::fs::create_dir(&drafts)?;
+ }
+ let draft_filename = drafts.join("0.md");
+ std::fs::write(draft_filename, title)?;
+ Ok(())
+}
+
+fn edit_draft(dirname: &Path, editor: &str) -> anyhow::Result<()> {
+ debug!("edit_draft: dirname={:?}", dirname);
+ debug!("edit_draft: editor={:?}", editor);
+ let drafts = dirname.join("drafts");
+ let draft_filename = drafts.join("0.md");
+ debug!("edit_draft: draft_filename={:?}", draft_filename);
+ Command::new(editor).arg(draft_filename).status()?;
+ debug!("edit_draft: editor finished");
+ Ok(())
+}
+
+fn finish_draft(dirname: &Path) -> anyhow::Result<()> {
+ let drafts = dirname.join("drafts");
+ let draft = drafts.join("0.md");
+
+ let entries = dirname.join("entries");
+ if !entries.exists() {
+ std::fs::create_dir(&entries)?;
+ }
+ let entry = entries.join("0.md");
+
+ std::fs::rename(draft, entry)?;
+ Ok(())
+}
diff --git a/src/config.rs b/src/config.rs
new file mode 100644
index 0000000..5041492
--- /dev/null
+++ b/src/config.rs
@@ -0,0 +1,115 @@
+//! Configuration file handling.
+
+use crate::error::JournalError;
+use crate::opt::Opt;
+
+use directories_next::ProjectDirs;
+use serde::Deserialize;
+use std::default::Default;
+use std::path::{Path, PathBuf};
+
+const APP: &str = "jt2";
+
+// The configuration file we read.
+//
+// Some of the fields are optional in the file. We will use default
+// values for those, or get them command line options.
+#[derive(Debug, Default, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct InputConfiguration {
+ dirname: Option<PathBuf>,
+ editor: Option<String>,
+}
+
+impl InputConfiguration {
+ fn read(filename: &Path) -> Result<Self, JournalError> {
+ let text = std::fs::read(&filename)
+ .map_err(|err| JournalError::ReadConfig(filename.to_path_buf(), err))?;
+ let config = serde_yaml::from_slice(&text)
+ .map_err(|err| JournalError::ConfigSyntax(filename.to_path_buf(), err))?;
+ return Ok(config);
+ }
+}
+
+/// The run-time configuration.
+///
+/// This is the configuration as read from the configuration file, if
+/// any, and with all command line options applied. Nothing here is
+/// optional.
+#[derive(Debug, Deserialize)]
+pub struct Configuration {
+ /// The directory where the journal is stored.
+ pub dirname: PathBuf,
+
+ /// The editor to open for editing journal entry drafts.
+ pub editor: String,
+}
+
+impl Configuration {
+ /// Read configuration file.
+ ///
+ /// The configuration is read from the file specified by the user
+ /// on the command line, or from a default location following the
+ /// XDG base directory specification. Note that only one of those
+ /// is read.
+ ///
+ /// It's OK for the default configuration file to be missing, but
+ /// if one is specified by the user explicitly, that one MUST
+ /// exist.
+ pub fn read(opt: &Opt) -> Result<Self, JournalError> {
+ let proj_dirs =
+ ProjectDirs::from("", "", APP).expect("could not figure out home directory");
+ let filename = match &opt.global.config {
+ Some(path) => {
+ if !path.exists() {
+ return Err(JournalError::ConfigMissing(path.to_path_buf()));
+ }
+ path.to_path_buf()
+ }
+ None => proj_dirs.config_dir().to_path_buf(),
+ };
+ let input = if filename.exists() {
+ InputConfiguration::read(&filename)?
+ } else {
+ InputConfiguration::default()
+ };
+
+ Ok(Self {
+ dirname: if let Some(path) = &opt.global.dirname {
+ path.to_path_buf()
+ } else if let Some(path) = &input.dirname {
+ expand_tilde(path)
+ } else {
+ proj_dirs.data_dir().to_path_buf()
+ },
+ editor: if let Some(name) = &opt.global.editor {
+ name.to_string()
+ } else if let Some(name) = &input.editor {
+ name.to_string()
+ } else {
+ "/usr/bin/editor".to_string()
+ },
+ })
+ }
+
+ /// Write configuration to stdout.
+ pub fn dump(&self) {
+ println!("{:#?}", self);
+ }
+}
+
+fn expand_tilde(path: &Path) -> PathBuf {
+ if path.starts_with("~/") {
+ if let Some(home) = std::env::var_os("HOME") {
+ let mut expanded = PathBuf::from(home);
+ for comp in path.components().skip(1) {
+ expanded.push(comp);
+ }
+ expanded
+ } else {
+ path.to_path_buf()
+ }
+ } else {
+ path.to_path_buf()
+ }
+}
diff --git a/src/error.rs b/src/error.rs
new file mode 100644
index 0000000..00dae56
--- /dev/null
+++ b/src/error.rs
@@ -0,0 +1,28 @@
+//! Errors returned from functions in this crate.
+
+use std::path::PathBuf;
+
+#[derive(Debug, thiserror::Error)]
+pub enum JournalError {
+ /// Configuration file does not exist.
+ #[error("specified configuration file does not exist: {0}")]
+ ConfigMissing(PathBuf),
+
+ /// Failed to read the configuration file.
+ ///
+ /// This is for permission problems and such.
+ #[error("failed to read configuration file {0}")]
+ ReadConfig(PathBuf, #[source] std::io::Error),
+
+ /// Configuration file has a syntax error.
+ #[error("failed to understand configuration file syntax: {0}")]
+ ConfigSyntax(PathBuf, #[source] serde_yaml::Error),
+
+ /// The specified directory does not look like a journal.
+ #[error("directory {0} is not a journal")]
+ NotAJournal(String),
+
+ /// Failed to create the directory for the journal.
+ #[error("failed to create journal directory {0}")]
+ CreateDirectory(PathBuf, #[source] std::io::Error),
+}
diff --git a/src/lib.rs b/src/lib.rs
index 8b13789..523317b 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1 +1,3 @@
-
+pub mod config;
+pub mod error;
+pub mod opt;
diff --git a/src/main.rs b/src/main.rs
deleted file mode 100644
index c5117c8..0000000
--- a/src/main.rs
+++ /dev/null
@@ -1,127 +0,0 @@
-use anyhow::Result;
-use log::debug;
-use std::fs;
-use std::path::{Path, PathBuf};
-use std::process::Command;
-use structopt::StructOpt;
-use thiserror::Error;
-
-#[derive(Debug, StructOpt)]
-#[structopt(about = "maintain a journal")]
-enum JT {
- Init {
- #[structopt(help = "Directory where journal should be stored")]
- dirname: PathBuf,
- #[structopt(help = "Short name for journal")]
- journalname: String,
- #[structopt(help = "Short description of journal, its title")]
- description: String,
- },
- IsJournal {
- #[structopt(help = "Directory that may or may not be a journal")]
- dirname: PathBuf,
- },
- New {
- #[structopt(long, short, help = "Use DIRNAME as the location of the journal")]
- dirname: PathBuf,
- #[structopt(
- long,
- short,
- help = "Invoke EDITOR for user to edit draft",
- default_value = "/usr/bin/editor"
- )]
- editor: String,
- #[structopt(help = "Title of new draft")]
- title: String,
- },
- Edit {
- #[structopt(long, short, help = "Use DIRNAME as the location of the journal")]
- dirname: PathBuf,
- #[structopt(
- long,
- short,
- help = "Invoke EDITOR for user to edit draft",
- default_value = "/usr/bin/editor"
- )]
- editor: String,
- },
- Finish {
- #[structopt(long, short, help = "Use DIRNAME as the location of the journal")]
- dirname: PathBuf,
- },
-}
-
-#[derive(Debug, Error)]
-enum JournalError {
- #[error("directory {0} is not a journal")]
- NotAJournal(String),
-}
-
-fn main() -> Result<()> {
- pretty_env_logger::init_custom_env("JT_LOG");
- let opt = JT::from_args();
- match opt {
- JT::Init {
- dirname,
- journalname,
- description,
- } => init(&dirname, &journalname, &description)?,
- JT::IsJournal { dirname } => is_journal(&dirname)?,
- JT::New {
- title,
- dirname,
- editor,
- } => new_draft(&title, &dirname, &editor)?,
- JT::Edit { dirname, editor } => edit_draft(&dirname, &editor)?,
- JT::Finish { dirname } => finish_draft(&dirname)?,
- }
- Ok(())
-}
-
-fn init(dirname: &Path, _journalname: &str, _description: &str) -> anyhow::Result<()> {
- std::fs::create_dir(dirname)?;
- Ok(())
-}
-
-fn is_journal(dirname: &Path) -> anyhow::Result<()> {
- let meta = fs::symlink_metadata(dirname)?;
- if !meta.is_dir() {
- return Err(JournalError::NotAJournal(dirname.display().to_string()).into());
- }
- Ok(())
-}
-
-fn new_draft(title: &str, dirname: &Path, editor: &str) -> anyhow::Result<()> {
- let drafts = dirname.join("drafts");
- if !drafts.exists() {
- std::fs::create_dir(&drafts)?;
- }
- let draft_filename = drafts.join("0.md");
- std::fs::write(draft_filename, title)?;
- Ok(())
-}
-
-fn edit_draft(dirname: &Path, editor: &str) -> anyhow::Result<()> {
- debug!("edit_draft: dirname={:?}", dirname);
- debug!("edit_draft: editor={:?}", editor);
- let drafts = dirname.join("drafts");
- let draft_filename = drafts.join("0.md");
- debug!("edit_draft: draft_filename={:?}", draft_filename);
- let status = Command::new(editor).arg(draft_filename).status()?;
- debug!("edit_draft: editor finished");
- Ok(())
-}
-
-fn finish_draft(dirname: &Path) -> anyhow::Result<()> {
- let drafts = dirname.join("drafts");
- let draft = drafts.join("0.md");
-
- let entries = dirname.join("entries");
- if !entries.exists() {
- std::fs::create_dir(&entries)?;
- }
- let entry = entries.join("0.md");
-
- std::fs::rename(draft, entry)?;
- Ok(())
-}
diff --git a/src/opt.rs b/src/opt.rs
new file mode 100644
index 0000000..12c3d26
--- /dev/null
+++ b/src/opt.rs
@@ -0,0 +1,70 @@
+//! Command line options.
+
+use std::path::PathBuf;
+use structopt::StructOpt;
+
+/// A parsed command line.
+#[derive(Debug, StructOpt)]
+#[structopt(about = "maintain a journal")]
+pub struct Opt {
+ /// Global options, common for all subcommands.
+ #[structopt(flatten)]
+ pub global: GlobalOptions,
+
+ /// The subcommand.
+ #[structopt(subcommand)]
+ pub cmd: SubCommand,
+}
+
+/// Global options.
+///
+/// These options are common to all subcommands.
+#[derive(Debug, StructOpt)]
+pub struct GlobalOptions {
+ /// Which configuration file to read.
+ #[structopt(short, long, help = "Configuration file")]
+ pub config: Option<PathBuf>,
+
+ /// Which directory to use for the journal.
+ #[structopt(short, long, help = "Directory where journal should be stored")]
+ pub dirname: Option<PathBuf>,
+
+ /// Which editor to invoke for editing journal entry drafts.
+ #[structopt(
+ long,
+ short,
+ help = "Invoke EDITOR for user to edit draft journal entry"
+ )]
+ pub editor: Option<String>,
+}
+
+/// A subcommand.
+#[derive(Debug, StructOpt)]
+pub enum SubCommand {
+ /// Show configuration.
+ Config,
+
+ /// Create a new journal in the chosen directory.
+ Init {
+ #[structopt(help = "Short name for journal")]
+ journalname: String,
+
+ #[structopt(help = "Short description of journal, its title")]
+ description: String,
+ },
+
+ /// Check if a directory is a journal.
+ IsJournal,
+
+ /// Create draft for a new journal entry.
+ New {
+ #[structopt(help = "Title of new draft")]
+ title: String,
+ },
+
+ /// Invoke editor on journal entry draft.
+ Edit,
+
+ /// Finish a journal entry draft.
+ Finish,
+}