working minimal cli
This commit is contained in:
commit
f60d3b50fc
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal 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
20
Cargo.toml
Normal 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
21
README.md
Normal 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
33
src/cli.rs
Normal 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
33
src/main.rs
Normal 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
94
src/tasks.rs
Normal 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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user