Skip to content

Commit

Permalink
Merge pull request #15 from schneedotdev/brian/command-add
Browse files Browse the repository at this point in the history
`til add` and note entry metadata
  • Loading branch information
schneedotdev authored Aug 17, 2024
2 parents cb33df6 + 377779e commit 6d04a18
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 14 deletions.
45 changes: 45 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ path = "src/main.rs"
chrono = "0.4.38"
clap = { version = "4.5.14", features = ["derive"] }
dirs = "5.0.1"
regex = "1.10.6"

11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
til is a command-line application designed to help you keep track of the important sh%t you want to remember. Whether it's a key insight from your work, a useful programming trick, or a valuable life lesson, this app helps you store and retrieve your notes in a friendly manner.

## Current Features
* Store notes by passing a message and an optional title.
* Retrieve your notes by searching a date (MM-DD-YYYY) or title.

- Store notes by passing a message and an optional title.
- Retrieve your notes by searching a date (MM-DD-YYYY) or title.

## Installation

Expand All @@ -18,12 +19,12 @@ _today-i-learned on [crates.io](https://crates.io/crates/today-i-learned)_

## Usage

### That
### Add

To store a note, use the `that` command with a message and an optional title:
To store a note, use the `add` command, passing a message and _optional_ tags (comma-separated with no spaces):

```
til that --message "Your note message" --title "Optional title"
til add "til is build with clap, a powerful command-line argument parser" --tags "rust,clap,crates"
```

### On
Expand Down
11 changes: 11 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ pub(crate) enum Error {
CannotProcessArgs,
CannotOpenOrCreatePath(PathBuf),
CannotWriteToFile(PathBuf),
CannotParseMetaData,
CannotReadFile(PathBuf),
Custom(Message),
#[default]
Default,
Expand All @@ -35,6 +37,10 @@ impl Display for Error {
Error::CannotWriteToFile(file) => {
f.write_fmt(format_args!("cannot write to {}", file.display()))
}
Error::CannotReadFile(file) => {
f.write_fmt(format_args!("cannot read file {}", file.display()))
}
Error::CannotParseMetaData => f.write_str("cannot parse metadata"),
Error::Custom(msg) => f.write_str(msg),
Error::Default => f.write_str("something wrong happened"),
}
Expand Down Expand Up @@ -81,6 +87,11 @@ mod tests {
Error::CannotWriteToFile("src/test".into()),
"cannot write to src/test",
),
(Error::CannotParseMetaData, "cannot parse metadata"),
(
Error::CannotReadFile("src/test".into()),
"cannot read file src/test",
),
("custom message".into(), "custom message"),
(Error::default(), "something wrong happened"),
];
Expand Down
125 changes: 116 additions & 9 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use std::{
use chrono::{Datelike, Local};
use clap::{Args, Parser, Subcommand};
use error::Error;
use regex::Regex;

const PATH_FROM_ROOT: &str = ".til/notes";

Expand All @@ -25,8 +26,8 @@ struct Cli {

#[derive(Subcommand, Debug)]
enum Command {
/// stores a note entry
That {
/// Add a new note entry
Add {
#[clap(flatten)]
entry: Entry,
},
Expand All @@ -39,11 +40,10 @@ enum Command {

#[derive(Args, Debug)]
struct Entry {
#[clap(short, long)]
message: String,
content: String,

#[clap(short, long, default_value = "default")]
title: String,
#[clap(long, use_value_delimiter = true, default_value = "")]
tags: Vec<String>,
}

impl Entry {
Expand All @@ -56,7 +56,19 @@ impl Entry {
.open(&path)
.map_err(|_| Error::CannotOpenOrCreatePath(path.clone()))?;

file.write_all(format!("- {}\n", self.message).as_bytes())
let file_size = file
.metadata()
.map_err(|_| Error::CannotReadFile(path.clone()))?
.len();

if file_size == 0 {
file.write_all(self.generate_meta().as_bytes())
.map_err(|_| Error::CannotWriteToFile(path.clone()))?;
} else if !self.tags.is_empty() {
self.update_meta(&path)?;
}

file.write_all(format!("- {}\n", self.content).as_bytes())
.map_err(|_| Error::CannotWriteToFile(path.clone()))
}

Expand All @@ -66,7 +78,7 @@ impl Entry {

let root_dir = find_root_dir().ok_or(Error::CannotFindDir("root".to_owned()))?;
let path = {
let mut path = Path::new(&root_dir).join(&date).join(&self.title);
let mut path = Path::new(&root_dir).join(&date).join("default");
path.set_extension("md");
path
};
Expand All @@ -83,6 +95,101 @@ impl Entry {
Ok(path)
}

/// Generates a metadata block for a note entry.
///
/// This function will create a front matter block which includes the
/// title and tags of the note.
///
/// ## Returns
///
/// Returns a `String` containing the formatted metadata block.
///
/// ## Examples
///
/// ```
/// let entry = Entry {
/// title: "Example Title".to_string(),
/// tags: vec!["tag1".to_string(), "tag2".to_string()],
/// };
/// let meta = entry.generate_meta();
/// assert_eq!(meta, r#"---
/// title: "Example Title"
/// tags: [tag1, tag2]
/// ---
/// "#);
/// ```
fn generate_meta(&self) -> String {
format!(
r#"---
title: "default"
tags: [{}]
---
"#,
self.tags.join(", ")
)
}

/// Updates the metadata block for a note entry.
///
/// This function reads the contents of a note entry, parses the metadata,
/// and updates the "tags" field with any new tags provided in the `Entry`. Tags
/// already present are not duplicated. The function assumes the metadata is at the
/// beginning of the file, separated from the content by a `---` delimiter. If the
/// metadata is missing or cannot be parsed, an error is returned.
///
/// ## Arguments
///
/// * `path` - A reference to the path of the file where the metadata should be updated.
///
/// ## Returns
///
/// Returns a `Result` indicating success (`Ok(())`) or failure (`Error`).
///
/// ## Errors
///
/// * `Error::CannotOpenOrCreatePath` - If the file cannot be opened.
/// * `Error::CannotReadFile` - If the file cannot be read.
/// * `Error::CannotParseMetaData` - If the metadata cannot be parsed.
/// * `Error::CannotWriteToFile` - If the updated contents cannot be written back to the file.
fn update_meta(&self, path: &PathBuf) -> error::Result<()> {
let mut contents =
fs::read_to_string(&path).map_err(|_| Error::CannotReadFile(path.clone()))?;

let meta = contents
.split("\n---\n")
.next()
.ok_or(Error::CannotParseMetaData)?;

let tags_regex =
Regex::new(r"(?m)^tags:\s*\[(.*?)\]$").map_err(|_| Error::CannotParseMetaData)?;
let mut new_tags = self.tags.clone();

if let Some(captures) = tags_regex.captures(meta) {
let existing_tags: Vec<String> = captures[1]
.split(',')
.map(|s| s.trim().to_string())
.collect();

new_tags.retain(|tag| !existing_tags.contains(tag));

if !new_tags.is_empty() {
let updated_tags = existing_tags
.into_iter()
.chain(new_tags)
.collect::<Vec<_>>()
.join(", ");
contents = contents.replace(&captures[0], &format!("tags: [{}]", updated_tags));
}
} else {
return Err(Error::CannotParseMetaData);
}

fs::write(&path, contents).map_err(|_| Error::CannotWriteToFile(path.clone()))?;

Ok(())
}

fn retrieve_from(_search_params: SearchParams) {
todo!()
}
Expand All @@ -102,7 +209,7 @@ fn main() -> error::Result<()> {
match args.command {
Some(command) => {
match command {
Command::That { entry } => entry.write()?,
Command::Add { entry } => entry.write()?,
Command::On { search_params } => Entry::retrieve_from(search_params),
};

Expand Down

0 comments on commit 6d04a18

Please sign in to comment.