working minimal cli

This commit is contained in:
wieerwill 2023-10-08 21:43:29 +02:00
commit f60d3b50fc
6 changed files with 215 additions and 0 deletions

14
.gitignore vendored Normal file
View File

@ -0,0 +1,14 @@
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb

20
Cargo.toml Normal file
View File

@ -0,0 +1,20 @@
[package]
name = "rusty-journal"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0"
home = "0.5"
serde_json = "1.0"
structopt = "0.3"
[dependencies.chrono]
features = ["serde"] # We're also going to need the serde feature for the chrono crate, so we can serialize the DateTime field.
version = "0.4"
[dependencies.serde] # Add serde in its own section.
features = ["derive"] # We'll need the derive feature.
version = "1.0"

21
README.md Normal file
View File

@ -0,0 +1,21 @@
# Rusty Journal
The application is a command line task tracker. It records our tasks in a text file, displays them as a list in the terminal, and lets us mark them as done.
This programme follows the Microsoft Learning Path for [Rust](https://learn.microsoft.com/de-de/training/modules/rust-create-command-line-program/)
1. install rust toolchain with [rustup](https://rustup.rs)
2. `cargo run` to start the app
Try it out:
```sh
cargo run -- -j test-journal.json add "buy milk"
cargo run -- -j test-journal.json add "take the dog for a walk"
cargo run -- -j test-journal.json add "water the plants"
cargo run -- -j test-journal.json list
cargo run -- -j test-journal.json done 2
cargo run -- -j test-journal.json list
```
To compile the programme, switch to the terminal and execute the command `cargo run --release`.
The compiled binary (the executable) is located in the `target/release/` directory and is named after the project name. If you are using macOS or Linux, it will be called `rusty-journal`. If you use Windows, it is called `rusty-journal.exe`.

33
src/cli.rs Normal file
View File

@ -0,0 +1,33 @@
use std::path::PathBuf;
use structopt::StructOpt;
#[derive(Debug, StructOpt)]
pub enum Action {
/// Write tasks to the journal file.
Add {
/// The task description text.
#[structopt()]
text: String,
},
/// Remove an entry from the journal file by position.
Done {
#[structopt()]
position: usize,
},
/// List all tasks in the journal file.
List,
}
#[derive(Debug, StructOpt)]
#[structopt(
name = "Rusty Journal",
about = "A command line to-do app written in Rust"
)]
pub struct CommandLineArgs {
#[structopt(subcommand)]
pub action: Action,
/// Use a different journal file.
#[structopt(parse(from_os_str), short, long)]
pub journal_file: Option<PathBuf>,
}

33
src/main.rs Normal file
View File

@ -0,0 +1,33 @@
use anyhow::anyhow;
use std::path::PathBuf;
use structopt::StructOpt;
mod cli;
mod tasks;
use cli::{Action::*, CommandLineArgs};
use tasks::Task;
fn find_default_journal_file() -> Option<PathBuf> {
home::home_dir().map(|mut path| {
path.push(".rust-journal.json");
path
})
}
fn main() -> anyhow::Result<()> {
let CommandLineArgs {
action,
journal_file,
} = CommandLineArgs::from_args();
let journal_file = journal_file
.or_else(find_default_journal_file)
.ok_or(anyhow!("Failed to find journal file."))?;
match action {
Add { text } => tasks::add_task(journal_file, Task::new(text)),
List => tasks::list_tasks(journal_file),
Done { position } => tasks::complete_task(journal_file, position),
}?;
Ok(())
}

94
src/tasks.rs Normal file
View File

@ -0,0 +1,94 @@
use chrono::{serde::ts_seconds, DateTime, Local, Utc};
use serde::Deserialize;
use serde::Serialize;
use std::fmt;
use std::fs::{File, OpenOptions};
use std::io::{Error, ErrorKind, Result, Seek, SeekFrom};
use std::path::PathBuf;
#[derive(Debug, Deserialize, Serialize)]
pub struct Task {
pub text: String,
#[serde(with = "ts_seconds")]
pub created_at: DateTime<Utc>,
}
impl Task {
pub fn new(text: String) -> Task {
let created_at: DateTime<Utc> = Utc::now();
Task { text, created_at }
}
}
fn collect_tasks(mut file: &File) -> Result<Vec<Task>> {
file.seek(SeekFrom::Start(0))?; // Rewind the file before.
let tasks = match serde_json::from_reader(file) {
Ok(tasks) => tasks,
Err(e) if e.is_eof() => Vec::new(),
Err(e) => Err(e)?,
};
file.seek(SeekFrom::Start(0))?; // Rewind the file after.
Ok(tasks)
}
pub fn add_task(journal_path: PathBuf, task: Task) -> Result<()> {
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(journal_path)?;
let mut tasks = collect_tasks(&file)?;
tasks.push(task);
serde_json::to_writer(file, &tasks)?;
Ok(())
}
pub fn complete_task(journal_path: PathBuf, task_position: usize) -> Result<()> {
// Open the file.
let file = OpenOptions::new()
.read(true)
.write(true)
.open(journal_path)?;
// Consume file's contents as a vector of tasks.
let mut tasks = collect_tasks(&file)?;
// Try to remove the task.
if task_position == 0 || task_position > tasks.len() {
return Err(Error::new(ErrorKind::InvalidInput, "Invalid Task ID"));
}
tasks.remove(task_position - 1);
// Write the modified task list back into the file.
file.set_len(0)?;
serde_json::to_writer(file, &tasks)?;
Ok(())
}
pub fn list_tasks(journal_path: PathBuf) -> Result<()> {
// Open the file.
let file = OpenOptions::new().read(true).open(journal_path)?;
// Parse the file and collect the tasks.
let tasks = collect_tasks(&file)?;
// Enumerate and display tasks, if any.
if tasks.is_empty() {
println!("Task list is empty!");
} else {
let mut order: u32 = 1;
for task in tasks {
println!("{}: {}", order, task);
order += 1;
}
}
Ok(())
}
impl fmt::Display for Task {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let created_at = self.created_at.with_timezone(&Local).format("%F %H:%M");
write!(f, "{:<50} [{}]", self.text, created_at)
}
}