A file-based task manager

ADD: completion and edit commands

+65 -16
+34 -9
src/main.rs
··· 2 mod stack; 3 mod util; 4 mod workspace; 5 use std::path::PathBuf; 6 use std::{env::current_dir, io::Read}; 7 - use clap_complete::Shell; 8 use workspace::Workspace; 9 10 //use smol; 11 //use iocraft::prelude::*; 12 - use clap::{Args, Parser, Subcommand}; 13 use edit::edit as open_editor; 14 15 fn default_dir() -> PathBuf { ··· 59 60 Edit { 61 #[arg(short = 't')] 62 - task_id: u32 63 }, 64 65 Completion { 66 #[arg(short = 's')] 67 - shell: Shell 68 } 69 } 70 71 #[derive(Args)] ··· 88 command_push(cli.dir.unwrap_or(default_dir()), edit, body, title) 89 } 90 Commands::List { all, count } => command_list(cli.dir.unwrap_or(default_dir()), all, count), 91 - Commands::Swap => command_swap(cli.dir.unwrap_or(default_dir())) 92 } 93 } 94 ··· 98 99 fn command_push(dir: PathBuf, edit: bool, body: Option<String>, title: Title) { 100 let workspace = Workspace::from_path(dir).expect("Unable to find .tsk dir"); 101 - let title = if let Some(title) = title.title { 102 title 103 } else if let Some(title) = title.title_simple { 104 let joined = title.join(" "); ··· 116 .expect("Failed to read stdin"); 117 } 118 if edit { 119 - body = open_editor(format!("{title}\n\n{body}")).expect("Failed to edit file"); 120 } 121 let task = workspace 122 .new_task(title, body) ··· 147 workspace.swap_top().expect("swap to work"); 148 } 149 150 - fn command_edit(dir: PathBuf) { 151 let workspace = Workspace::from_path(dir).expect("Unable to find .tsk dir"); 152 - let task = workspace. 153 }
··· 2 mod stack; 3 mod util; 4 mod workspace; 5 + use clap_complete::{generate, Shell}; 6 + use std::io; 7 use std::path::PathBuf; 8 use std::{env::current_dir, io::Read}; 9 use workspace::Workspace; 10 11 //use smol; 12 //use iocraft::prelude::*; 13 + use clap::{Args, CommandFactory, Parser, Subcommand}; 14 use edit::edit as open_editor; 15 16 fn default_dir() -> PathBuf { ··· 60 61 Edit { 62 #[arg(short = 't')] 63 + task_id: u32, 64 }, 65 66 Completion { 67 #[arg(short = 's')] 68 + shell: Shell, 69 + }, 70 + 71 + /* 72 + Drop { 73 + #[arg(short = 't')] 74 + task_id: Option<u32>, 75 } 76 + */ 77 } 78 79 #[derive(Args)] ··· 96 command_push(cli.dir.unwrap_or(default_dir()), edit, body, title) 97 } 98 Commands::List { all, count } => command_list(cli.dir.unwrap_or(default_dir()), all, count), 99 + Commands::Swap => command_swap(cli.dir.unwrap_or(default_dir())), 100 + Commands::Edit { task_id } => command_edit(cli.dir.unwrap_or(default_dir()), task_id), 101 + Commands::Completion { shell } => command_completion(shell), 102 } 103 } 104 ··· 108 109 fn command_push(dir: PathBuf, edit: bool, body: Option<String>, title: Title) { 110 let workspace = Workspace::from_path(dir).expect("Unable to find .tsk dir"); 111 + let mut title = if let Some(title) = title.title { 112 title 113 } else if let Some(title) = title.title_simple { 114 let joined = title.join(" "); ··· 126 .expect("Failed to read stdin"); 127 } 128 if edit { 129 + let new_content = open_editor(format!("{title}\n\n{body}")).expect("Failed to edit file"); 130 + if let Some(content) = new_content.split_once("\n") { 131 + title = content.0.to_string(); 132 + body = content.1.to_string(); 133 + } 134 } 135 let task = workspace 136 .new_task(title, body) ··· 161 workspace.swap_top().expect("swap to work"); 162 } 163 164 + fn command_edit(dir: PathBuf, id: u32) { 165 let workspace = Workspace::from_path(dir).expect("Unable to find .tsk dir"); 166 + let mut task = workspace.task(id.into()).expect("To read task from disk"); 167 + let new_content = 168 + open_editor(format!("{}\n\n{}", task.title.trim(), task.body.trim())).expect("Failed to edit file"); 169 + if let Some((title, body)) = new_content.split_once("\n") { 170 + task.title = title.to_string(); 171 + task.body = body.to_string(); 172 + task.save().expect("Failed to save task"); 173 + } 174 + } 175 + 176 + fn command_completion(shell: Shell) { 177 + generate(shell, &mut Cli::command(), "tsk", &mut io::stdout()) 178 }
+31 -7
src/workspace.rs
··· 6 use crate::util; 7 use std::fmt::Display; 8 use std::fs::File; 9 - use std::io::{Read, Seek}; 10 use std::path::PathBuf; 11 use std::str::FromStr; 12 use std::{fs::OpenOptions, io::Write}; ··· 30 impl Display for Id { 31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 32 write!(f, "tsk-{}", self.0) 33 } 34 } 35 ··· 82 // reset the files contents 83 file.set_len(0)?; 84 // TODO: figure out if this is necessary 85 - file.seek(std::io::SeekFrom::Start(0))?; 86 // store the *next* if 87 file.write_all(format!("{}\n", id + 1).as_bytes())?; 88 Ok(Id(id)) ··· 105 } 106 107 pub fn task(&self, id: Id) -> Result<Task> { 108 - let mut file = util::flopen( 109 self.path.join("tasks").join(format!("tsk-{}.tsk", id.0)), 110 FlockArg::LockExclusive, 111 )?; 112 - 113 } 114 115 pub fn read_stack(&self, count: Option<usize>) -> Result<TaskStack> { ··· 138 pub file: Flock<File>, 139 } 140 141 - #[cfg(test)] 142 - mod test { 143 - fn test_next_id() {} 144 }
··· 6 use crate::util; 7 use std::fmt::Display; 8 use std::fs::File; 9 + use std::io::{BufRead as _, BufReader, Read, Seek, SeekFrom}; 10 use std::path::PathBuf; 11 use std::str::FromStr; 12 use std::{fs::OpenOptions, io::Write}; ··· 30 impl Display for Id { 31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 32 write!(f, "tsk-{}", self.0) 33 + } 34 + } 35 + 36 + impl From<u32> for Id { 37 + fn from(value: u32) -> Self { 38 + Id(value) 39 } 40 } 41 ··· 88 // reset the files contents 89 file.set_len(0)?; 90 // TODO: figure out if this is necessary 91 + file.seek(SeekFrom::Start(0))?; 92 // store the *next* if 93 file.write_all(format!("{}\n", id + 1).as_bytes())?; 94 Ok(Id(id)) ··· 111 } 112 113 pub fn task(&self, id: Id) -> Result<Task> { 114 + let file = util::flopen( 115 self.path.join("tasks").join(format!("tsk-{}.tsk", id.0)), 116 FlockArg::LockExclusive, 117 )?; 118 + let mut title = String::new(); 119 + let mut body = String::new(); 120 + let mut reader = BufReader::new(&*file); 121 + reader.read_line(&mut title)?; 122 + reader.read_to_string(&mut body)?; 123 + drop(reader); 124 + Ok(Task { 125 + id, 126 + title, 127 + body, 128 + file, 129 + }) 130 } 131 132 pub fn read_stack(&self, count: Option<usize>) -> Result<TaskStack> { ··· 155 pub file: Flock<File>, 156 } 157 158 + impl Task { 159 + /// Consumes a task and saves it to disk. 160 + pub fn save(mut self) -> Result<()> { 161 + self.file.set_len(0)?; 162 + self.file.seek(SeekFrom::Start(0))?; 163 + self.file.write_all(self.title.trim().as_bytes())?; 164 + self.file.write_all(b"\n\n")?; 165 + self.file.write_all(self.body.trim().as_bytes())?; 166 + Ok(()) 167 + } 168 }