A file-based task manager
0
fork

Configure Feed

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

Add basic task creation

+204 -26
+52
Cargo.lock
··· 806 806 checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 807 807 808 808 [[package]] 809 + name = "serde" 810 + version = "1.0.203" 811 + source = "registry+https://github.com/rust-lang/crates.io-index" 812 + checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" 813 + dependencies = [ 814 + "serde_derive", 815 + ] 816 + 817 + [[package]] 818 + name = "serde_derive" 819 + version = "1.0.203" 820 + source = "registry+https://github.com/rust-lang/crates.io-index" 821 + checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" 822 + dependencies = [ 823 + "proc-macro2", 824 + "quote", 825 + "syn", 826 + ] 827 + 828 + [[package]] 809 829 name = "signal-hook" 810 830 version = "0.3.17" 811 831 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 854 874 ] 855 875 856 876 [[package]] 877 + name = "smallstr" 878 + version = "0.3.0" 879 + source = "registry+https://github.com/rust-lang/crates.io-index" 880 + checksum = "63b1aefdf380735ff8ded0b15f31aab05daf1f70216c01c02a12926badd1df9d" 881 + dependencies = [ 882 + "serde", 883 + "smallvec", 884 + ] 885 + 886 + [[package]] 857 887 name = "smallvec" 858 888 version = "1.13.2" 859 889 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 935 965 ] 936 966 937 967 [[package]] 968 + name = "thiserror" 969 + version = "1.0.64" 970 + source = "registry+https://github.com/rust-lang/crates.io-index" 971 + checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" 972 + dependencies = [ 973 + "thiserror-impl", 974 + ] 975 + 976 + [[package]] 977 + name = "thiserror-impl" 978 + version = "1.0.64" 979 + source = "registry+https://github.com/rust-lang/crates.io-index" 980 + checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" 981 + dependencies = [ 982 + "proc-macro2", 983 + "quote", 984 + "syn", 985 + ] 986 + 987 + [[package]] 938 988 name = "tracing" 939 989 version = "0.1.40" 940 990 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 960 1010 "edit", 961 1011 "iocraft", 962 1012 "nix", 1013 + "smallstr", 963 1014 "smol", 1015 + "thiserror", 964 1016 "xattr", 965 1017 ] 966 1018
+3 -1
Cargo.toml
··· 9 9 clap_mangen = "0.2.23" 10 10 edit = "0.1.5" 11 11 iocraft = "0.2.3" 12 - nix = "0.29.0" 12 + nix = { version = "0.29.0", features = ["fs"] } 13 + smallstr = { version = "0.3.0", features = ["std"] } 13 14 smol = "2.0.2" 15 + thiserror = "1.0.64" 14 16 xattr = "1.3.1"
+17
src/errors.rs
··· 1 + use thiserror::Error as ThisError; 2 + 3 + pub type Result<T> = std::result::Result<T, Error>; 4 + 5 + #[derive(ThisError, Debug)] 6 + pub enum Error { 7 + #[error("The workspace is not initialized. Run `tsk init` to initialize it.")] 8 + Uninitialized, 9 + #[error("The tsk workspace is already initialized. No change.")] 10 + AlreadyInitialized, 11 + #[error("Unable to read file: {0}")] 12 + Io(#[from] std::io::Error), 13 + #[error("Unable to acquire locc: {0}")] 14 + Lock(nix::errno::Errno), 15 + #[error("Unable to parse id: {0}")] 16 + ParseId(#[from] std::num::ParseIntError), 17 + }
+33 -25
src/main.rs
··· 1 + mod errors; 2 + mod workspace; 1 3 use std::path::PathBuf; 2 4 use std::{env::current_dir, io::Read}; 5 + use workspace::Workspace; 3 6 4 7 //use smol; 5 8 //use iocraft::prelude::*; ··· 12 15 13 16 #[derive(Parser)] 14 17 // TODO: add long_about 15 - #[command(version, about, long_about = None)] 18 + #[command(version, about)] 16 19 struct Cli { 17 - #[arg(short = 'C', env = "TSK_DIR", value_name = "DIR")] 20 + #[arg(short = 'C', env = "TSK_ROOT", value_name = "DIR")] 18 21 dir: Option<PathBuf>, 19 22 // TODO: other global options 20 23 #[command(subcommand)] ··· 23 26 24 27 #[derive(Subcommand)] 25 28 enum Commands { 29 + Init, 26 30 /// Creates a new task, automatically assigning it a unique identifider and persisting 27 31 Push { 28 32 /// Whether to open $EDITOR to edit the content of the task. The first line if the ··· 56 60 57 61 fn main() { 58 62 let cli = Cli::parse(); 59 - if let Commands::Push { edit, body, title } = cli.command { 60 - let title = if let Some(title) = title.title { 61 - eprintln!("TITLE: {}", title); 62 - title 63 - } else if let Some(title) = title.title_simple { 64 - let joined = title.join(" "); 65 - eprintln!("TITLE simple: {}", joined); 66 - joined 67 - } else { 68 - "".to_string() 69 - }; 70 - let mut body = body.unwrap_or_default(); 71 - if body == "-" { 72 - // add newline so you can type directly in the shell 73 - eprintln!(""); 74 - body.clear(); 75 - std::io::stdin() 76 - .read_to_string(&mut body) 77 - .expect("Failed to read stdin"); 78 - } 79 - if edit { 80 - body = open_editor(format!("{title}\n\n{body}")).expect("Failed to edit file"); 63 + match cli.command { 64 + Commands::Init => Workspace::init(cli.dir.unwrap_or(default_dir())).expect("Init failed"), 65 + Commands::Push { edit, body, title } => { 66 + let title = if let Some(title) = title.title { 67 + title 68 + } else if let Some(title) = title.title_simple { 69 + let joined = title.join(" "); 70 + joined 71 + } else { 72 + "".to_string() 73 + }; 74 + let mut body = body.unwrap_or_default(); 75 + if body == "-" { 76 + // add newline so you can type directly in the shell 77 + eprintln!(""); 78 + body.clear(); 79 + std::io::stdin() 80 + .read_to_string(&mut body) 81 + .expect("Failed to read stdin"); 82 + } 83 + if edit { 84 + body = open_editor(format!("{title}\n\n{body}")).expect("Failed to edit file"); 85 + } 86 + Workspace::from_path(cli.dir.unwrap_or(default_dir())) 87 + .expect("Unable to find .tsk dir") 88 + .new_task(title, body) 89 + .expect("Failed to create task"); 81 90 } 82 - eprintln!("BODY: {body}"); 83 91 } 84 92 }
+99
src/workspace.rs
··· 1 + #![allow(dead_code)] 2 + use nix::fcntl::{Flock, FlockArg}; 3 + 4 + use crate::errors::{Error, Result}; 5 + use std::io::{BufReader, Seek}; 6 + use std::path::PathBuf; 7 + use std::{ 8 + fs::OpenOptions, 9 + io::{BufRead, Write}, 10 + }; 11 + 12 + pub struct Id(u32); 13 + 14 + pub struct Workspace { 15 + /// The path to the workspace root, excluding the .tsk directory. This should *contain* the 16 + /// .tsk directory. 17 + path: PathBuf, 18 + } 19 + 20 + impl Workspace { 21 + pub fn init(path: PathBuf) -> Result<()> { 22 + let tsk_dir = path.join(".tsk"); 23 + if tsk_dir.exists() { 24 + return Err(Error::AlreadyInitialized); 25 + } 26 + std::fs::create_dir(&tsk_dir)?; 27 + // Create the tasks directory 28 + std::fs::create_dir(&tsk_dir.join("tasks"))?; 29 + let mut next = OpenOptions::new() 30 + .read(true) 31 + .write(true) 32 + .create(true) 33 + .open(tsk_dir.join("next"))?; 34 + next.write_all(b"1\n")?; 35 + Ok(()) 36 + } 37 + 38 + pub fn from_path(path: PathBuf) -> Result<Self> { 39 + // TODO: recursively walk up the path until we find a .tsk dir or error if we can't find 40 + // one / cross a filesystem boundary 41 + let tsk_dir = path.join(".tsk"); 42 + if !tsk_dir.exists() { 43 + return Err(Error::Uninitialized); 44 + } else { 45 + Ok(Self { path }) 46 + } 47 + } 48 + 49 + pub fn next_id(&self) -> Result<Id> { 50 + let file = OpenOptions::new() 51 + .read(true) 52 + .write(true) 53 + .create(true) 54 + .open(self.path.join("next"))?; 55 + let mut lock = 56 + Flock::lock(file, FlockArg::LockExclusive).map_err(|(_, errno)| Error::Lock(errno))?; 57 + let mut buf = String::new(); 58 + BufReader::new(&*lock).read_line(&mut buf)?; 59 + let id = buf.trim().parse::<u32>()?; 60 + // reset the files contents 61 + lock.set_len(0)?; 62 + // TODO: figure out if this is necessary 63 + lock.seek(std::io::SeekFrom::Start(0))?; 64 + // store the *next* if 65 + lock.write_all(format!("{}\n", id + 1).as_bytes())?; 66 + Ok(Id(id)) 67 + } 68 + 69 + pub fn new_task(&self, title: String, body: String) -> Result<Task> { 70 + // TODO: we could improperly increment the id if the task is not written to disk/errors 71 + let id = self.next_id()?; 72 + let file = OpenOptions::new() 73 + .read(true) 74 + .write(true) 75 + .create(true) 76 + .open(self.path.join(format!("tsk-{}.tsk", id.0)))?; 77 + let mut file = 78 + Flock::lock(file, FlockArg::LockExclusive).map_err(|(_, errno)| Error::Lock(errno))?; 79 + file.write_all(format!("{title}\n\n{body}").as_bytes())?; 80 + Ok(Task { 81 + id, 82 + title, 83 + body, 84 + file, 85 + }) 86 + } 87 + } 88 + 89 + pub struct Task { 90 + id: Id, 91 + title: String, 92 + body: String, 93 + file: Flock<File>, 94 + } 95 + 96 + #[cfg(test)] 97 + mod test { 98 + fn test_next_id() {} 99 + }