···11+use thiserror::Error as ThisError;
22+33+pub type Result<T> = std::result::Result<T, Error>;
44+55+#[derive(ThisError, Debug)]
66+pub enum Error {
77+ #[error("The workspace is not initialized. Run `tsk init` to initialize it.")]
88+ Uninitialized,
99+ #[error("The tsk workspace is already initialized. No change.")]
1010+ AlreadyInitialized,
1111+ #[error("Unable to read file: {0}")]
1212+ Io(#[from] std::io::Error),
1313+ #[error("Unable to acquire locc: {0}")]
1414+ Lock(nix::errno::Errno),
1515+ #[error("Unable to parse id: {0}")]
1616+ ParseId(#[from] std::num::ParseIntError),
1717+}
+33-25
src/main.rs
···11+mod errors;
22+mod workspace;
13use std::path::PathBuf;
24use std::{env::current_dir, io::Read};
55+use workspace::Workspace;
3647//use smol;
58//use iocraft::prelude::*;
···12151316#[derive(Parser)]
1417// TODO: add long_about
1515-#[command(version, about, long_about = None)]
1818+#[command(version, about)]
1619struct Cli {
1717- #[arg(short = 'C', env = "TSK_DIR", value_name = "DIR")]
2020+ #[arg(short = 'C', env = "TSK_ROOT", value_name = "DIR")]
1821 dir: Option<PathBuf>,
1922 // TODO: other global options
2023 #[command(subcommand)]
···23262427#[derive(Subcommand)]
2528enum Commands {
2929+ Init,
2630 /// Creates a new task, automatically assigning it a unique identifider and persisting
2731 Push {
2832 /// Whether to open $EDITOR to edit the content of the task. The first line if the
···56605761fn main() {
5862 let cli = Cli::parse();
5959- if let Commands::Push { edit, body, title } = cli.command {
6060- let title = if let Some(title) = title.title {
6161- eprintln!("TITLE: {}", title);
6262- title
6363- } else if let Some(title) = title.title_simple {
6464- let joined = title.join(" ");
6565- eprintln!("TITLE simple: {}", joined);
6666- joined
6767- } else {
6868- "".to_string()
6969- };
7070- let mut body = body.unwrap_or_default();
7171- if body == "-" {
7272- // add newline so you can type directly in the shell
7373- eprintln!("");
7474- body.clear();
7575- std::io::stdin()
7676- .read_to_string(&mut body)
7777- .expect("Failed to read stdin");
7878- }
7979- if edit {
8080- body = open_editor(format!("{title}\n\n{body}")).expect("Failed to edit file");
6363+ match cli.command {
6464+ Commands::Init => Workspace::init(cli.dir.unwrap_or(default_dir())).expect("Init failed"),
6565+ Commands::Push { edit, body, title } => {
6666+ let title = if let Some(title) = title.title {
6767+ title
6868+ } else if let Some(title) = title.title_simple {
6969+ let joined = title.join(" ");
7070+ joined
7171+ } else {
7272+ "".to_string()
7373+ };
7474+ let mut body = body.unwrap_or_default();
7575+ if body == "-" {
7676+ // add newline so you can type directly in the shell
7777+ eprintln!("");
7878+ body.clear();
7979+ std::io::stdin()
8080+ .read_to_string(&mut body)
8181+ .expect("Failed to read stdin");
8282+ }
8383+ if edit {
8484+ body = open_editor(format!("{title}\n\n{body}")).expect("Failed to edit file");
8585+ }
8686+ Workspace::from_path(cli.dir.unwrap_or(default_dir()))
8787+ .expect("Unable to find .tsk dir")
8888+ .new_task(title, body)
8989+ .expect("Failed to create task");
8190 }
8282- eprintln!("BODY: {body}");
8391 }
8492}
+99
src/workspace.rs
···11+#![allow(dead_code)]
22+use nix::fcntl::{Flock, FlockArg};
33+44+use crate::errors::{Error, Result};
55+use std::io::{BufReader, Seek};
66+use std::path::PathBuf;
77+use std::{
88+ fs::OpenOptions,
99+ io::{BufRead, Write},
1010+};
1111+1212+pub struct Id(u32);
1313+1414+pub struct Workspace {
1515+ /// The path to the workspace root, excluding the .tsk directory. This should *contain* the
1616+ /// .tsk directory.
1717+ path: PathBuf,
1818+}
1919+2020+impl Workspace {
2121+ pub fn init(path: PathBuf) -> Result<()> {
2222+ let tsk_dir = path.join(".tsk");
2323+ if tsk_dir.exists() {
2424+ return Err(Error::AlreadyInitialized);
2525+ }
2626+ std::fs::create_dir(&tsk_dir)?;
2727+ // Create the tasks directory
2828+ std::fs::create_dir(&tsk_dir.join("tasks"))?;
2929+ let mut next = OpenOptions::new()
3030+ .read(true)
3131+ .write(true)
3232+ .create(true)
3333+ .open(tsk_dir.join("next"))?;
3434+ next.write_all(b"1\n")?;
3535+ Ok(())
3636+ }
3737+3838+ pub fn from_path(path: PathBuf) -> Result<Self> {
3939+ // TODO: recursively walk up the path until we find a .tsk dir or error if we can't find
4040+ // one / cross a filesystem boundary
4141+ let tsk_dir = path.join(".tsk");
4242+ if !tsk_dir.exists() {
4343+ return Err(Error::Uninitialized);
4444+ } else {
4545+ Ok(Self { path })
4646+ }
4747+ }
4848+4949+ pub fn next_id(&self) -> Result<Id> {
5050+ let file = OpenOptions::new()
5151+ .read(true)
5252+ .write(true)
5353+ .create(true)
5454+ .open(self.path.join("next"))?;
5555+ let mut lock =
5656+ Flock::lock(file, FlockArg::LockExclusive).map_err(|(_, errno)| Error::Lock(errno))?;
5757+ let mut buf = String::new();
5858+ BufReader::new(&*lock).read_line(&mut buf)?;
5959+ let id = buf.trim().parse::<u32>()?;
6060+ // reset the files contents
6161+ lock.set_len(0)?;
6262+ // TODO: figure out if this is necessary
6363+ lock.seek(std::io::SeekFrom::Start(0))?;
6464+ // store the *next* if
6565+ lock.write_all(format!("{}\n", id + 1).as_bytes())?;
6666+ Ok(Id(id))
6767+ }
6868+6969+ pub fn new_task(&self, title: String, body: String) -> Result<Task> {
7070+ // TODO: we could improperly increment the id if the task is not written to disk/errors
7171+ let id = self.next_id()?;
7272+ let file = OpenOptions::new()
7373+ .read(true)
7474+ .write(true)
7575+ .create(true)
7676+ .open(self.path.join(format!("tsk-{}.tsk", id.0)))?;
7777+ let mut file =
7878+ Flock::lock(file, FlockArg::LockExclusive).map_err(|(_, errno)| Error::Lock(errno))?;
7979+ file.write_all(format!("{title}\n\n{body}").as_bytes())?;
8080+ Ok(Task {
8181+ id,
8282+ title,
8383+ body,
8484+ file,
8585+ })
8686+ }
8787+}
8888+8989+pub struct Task {
9090+ id: Id,
9191+ title: String,
9292+ body: String,
9393+ file: Flock<File>,
9494+}
9595+9696+#[cfg(test)]
9797+mod test {
9898+ fn test_next_id() {}
9999+}