A file-based task manager
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

WIP: task stack

+116 -18
+4
src/errors.rs
··· 14 14 Lock(nix::errno::Errno), 15 15 #[error("Unable to parse id: {0}")] 16 16 ParseId(#[from] std::num::ParseIntError), 17 + #[error("General parsing error: {0}")] 18 + Parse(String), 19 + #[error("An unexpected error occurred: {0}")] 20 + Oops(Box<dyn std::error::Error>), 17 21 }
+2
src/main.rs
··· 1 1 mod errors; 2 2 mod workspace; 3 + mod stack; 4 + mod util; 3 5 use std::path::PathBuf; 4 6 use std::{env::current_dir, io::Read}; 5 7 use workspace::Workspace;
+72
src/stack.rs
··· 1 + //! The Task stack. Tasks created with `push` end up at the top here. It is invalid for a task that 2 + //! has been completed/archived to be on the stack. 3 + 4 + use crate::errors::{Error, Result}; 5 + use crate::util; 6 + use std::io::{self, BufRead}; 7 + use std::num::ParseIntError; 8 + use std::{fs::File, io::Read, path::PathBuf}; 9 + 10 + use nix::fcntl::{Flock, FlockArg}; 11 + 12 + use crate::workspace::Id; 13 + 14 + const INDEXFILE: &str = "index"; 15 + const TITLECACHEFILE: &str = "cache"; 16 + 17 + struct StackItem { 18 + id: Id, 19 + title: String, 20 + next: Id, 21 + } 22 + 23 + fn eof() -> Error { 24 + Error::Io(io::Error::new( 25 + io::ErrorKind::UnexpectedEof, 26 + "Unexpected end of file", 27 + )) 28 + } 29 + 30 + impl StackItem { 31 + fn from_reader(reader: &mut impl BufRead) -> Result<Self> { 32 + let mut buf = String::new(); 33 + reader.read_line(&mut buf)?; 34 + if buf.is_empty() { 35 + return Err(Error::Io(io::Error::new( 36 + io::ErrorKind::UnexpectedEof, 37 + "Empty line", 38 + ))); 39 + } 40 + let (id, next) = Self::parse(&buf)?; 41 + todo!(); 42 + } 43 + 44 + fn parse(line: &str) -> Result<(Id, Id)> { 45 + let mut split = line.split("->"); 46 + let curr = split.next().ok_or(eof())?; 47 + let next = split.next().ok_or(eof())?; 48 + if let Some(rest) = split.next() { 49 + Err(Error::Parse(format!( 50 + "Got unexpected data in index item: {rest}" 51 + ))) 52 + } else { 53 + Ok((curr.parse()?, next.parse()?)) 54 + } 55 + } 56 + } 57 + 58 + pub struct TaskStack { 59 + /// The index into `all` that is the top of the stack 60 + top: usize, 61 + all: Vec<StackItem>, 62 + file: Flock<File>, 63 + } 64 + 65 + impl TaskStack { 66 + fn from_tskdir(path: &PathBuf) -> Self { 67 + let index = util::flopen(&path.join(INDEXFILE), FlockArg::LockExclusive); 68 + let cache = util::flopen(&path.join(TITLECACHEFILE), FlockArg::LockShared); 69 + 70 + todo!() 71 + } 72 + }
+16
src/util.rs
··· 1 + use crate::errors::{Error, Result}; 2 + use std::{ 3 + fs::{File, OpenOptions}, 4 + path::PathBuf, 5 + }; 6 + 7 + use nix::fcntl::{Flock, FlockArg}; 8 + 9 + pub fn flopen(path: &PathBuf, mode: FlockArg) -> Result<Flock<File>> { 10 + let file = OpenOptions::new() 11 + .read(true) 12 + .write(true) 13 + .create(true) 14 + .open(path)?; 15 + Ok(Flock::lock(file, mode).map_err(|(_, errno)| Error::Lock(errno))?) 16 + }
+22 -18
src/workspace.rs
··· 2 2 use nix::fcntl::{Flock, FlockArg}; 3 3 4 4 use crate::errors::{Error, Result}; 5 + use crate::util; 5 6 use std::fs::File; 6 7 use std::io::{Read, Seek}; 7 8 use std::path::PathBuf; 9 + use std::str::FromStr; 8 10 use std::{fs::OpenOptions, io::Write}; 9 11 12 + /// A unique identifier for a task. When referenced in text, it is prefixed with `tsk-`. 10 13 pub struct Id(u32); 14 + 15 + impl FromStr for Id { 16 + type Err = Error; 17 + 18 + fn from_str(s: &str) -> std::result::Result<Self, Self::Err> { 19 + s.strip_prefix("tsk-") 20 + .ok_or(Self::Err::Parse("expected tsk- prefix ".to_string()))?; 21 + Ok(Self(s.parse()?)) 22 + } 23 + } 11 24 12 25 pub struct Workspace { 13 26 /// The path to the workspace root, excluding the .tsk directory. This should *contain* the ··· 45 58 } 46 59 47 60 pub fn next_id(&self) -> Result<Id> { 48 - let file = OpenOptions::new() 49 - .read(true) 50 - .write(true) 51 - .create(false) 52 - .open(self.path.join("next"))?; 53 - let mut lock = 54 - Flock::lock(file, FlockArg::LockExclusive).map_err(|(_, errno)| Error::Lock(errno))?; 61 + let mut file = util::flopen(&self.path.join("next"), FlockArg::LockExclusive)?; 55 62 let mut buf = String::new(); 56 - lock.read_to_string(&mut buf)?; 63 + file.read_to_string(&mut buf)?; 57 64 let id = buf.trim().parse::<u32>()?; 58 65 // reset the files contents 59 - lock.set_len(0)?; 66 + file.set_len(0)?; 60 67 // TODO: figure out if this is necessary 61 - lock.seek(std::io::SeekFrom::Start(0))?; 68 + file.seek(std::io::SeekFrom::Start(0))?; 62 69 // store the *next* if 63 - lock.write_all(format!("{}\n", id + 1).as_bytes())?; 70 + file.write_all(format!("{}\n", id + 1).as_bytes())?; 64 71 Ok(Id(id)) 65 72 } 66 73 67 74 pub fn new_task(&self, title: String, body: String) -> Result<Task> { 68 75 // TODO: we could improperly increment the id if the task is not written to disk/errors 69 76 let id = self.next_id()?; 70 - let file = OpenOptions::new() 71 - .read(true) 72 - .write(true) 73 - .create(true) 74 - .open(self.path.join("tasks").join(format!("tsk-{}.tsk", id.0)))?; 75 - let mut file = 76 - Flock::lock(file, FlockArg::LockExclusive).map_err(|(_, errno)| Error::Lock(errno))?; 77 + let mut file = util::flopen( 78 + &self.path.join("tasks").join(format!("tsk-{}.tsk", id.0)), 79 + FlockArg::LockExclusive, 80 + )?; 77 81 file.write_all(format!("{title}\n\n{body}").as_bytes())?; 78 82 Ok(Task { 79 83 id,