···1mod errors;
2-mod workspace;
3mod stack;
4mod util;
5-mod buffer;
6use std::path::PathBuf;
7use std::{env::current_dir, io::Read};
8use workspace::Workspace;
···47 #[command(flatten)]
48 title: Title,
49 },
00000000050}
5152#[derive(Args)]
···64fn main() {
65 let cli = Cli::parse();
66 match cli.command {
67- Commands::Init => Workspace::init(cli.dir.unwrap_or(default_dir())).expect("Init failed"),
68 Commands::Push { edit, body, title } => {
69- let title = if let Some(title) = title.title {
70- title
71- } else if let Some(title) = title.title_simple {
72- let joined = title.join(" ");
73- joined
74- } else {
75- "".to_string()
76- };
77- let mut body = body.unwrap_or_default();
78- if body == "-" {
79- // add newline so you can type directly in the shell
80- eprintln!("");
81- body.clear();
82- std::io::stdin()
83- .read_to_string(&mut body)
84- .expect("Failed to read stdin");
85- }
86- if edit {
87- body = open_editor(format!("{title}\n\n{body}")).expect("Failed to edit file");
88- }
89- Workspace::from_path(cli.dir.unwrap_or(default_dir()))
90- .expect("Unable to find .tsk dir")
91- .new_task(title, body)
92- .expect("Failed to create task");
93 }
0094 }
95}
0000000000000000000000000000000000000000000000000000000
···1mod errors;
02mod stack;
3mod util;
4+mod workspace;
5use std::path::PathBuf;
6use std::{env::current_dir, io::Read};
7use workspace::Workspace;
···46 #[command(flatten)]
47 title: Title,
48 },
49+ List {
50+ /// Whether to list all tasks in the task stack. If specified, -c / count is ignored.
51+ #[arg(short = 'a', default_value_t = false)]
52+ all: bool,
53+ #[arg(short = 'c', default_value_t = 10)]
54+ count: usize,
55+ },
56+57+ Swap,
58}
5960#[derive(Args)]
···72fn main() {
73 let cli = Cli::parse();
74 match cli.command {
75+ Commands::Init => command_init(cli.dir.unwrap_or(default_dir())),
76 Commands::Push { edit, body, title } => {
77+ command_push(cli.dir.unwrap_or(default_dir()), edit, body, title)
0000000000000000000000078 }
79+ Commands::List { all, count } => command_list(cli.dir.unwrap_or(default_dir()), all, count),
80+ Commands::Swap => command_swap(cli.dir.unwrap_or(default_dir()))
81 }
82}
83+84+fn command_init(dir: PathBuf) {
85+ Workspace::init(dir).expect("Init failed")
86+}
87+88+fn command_push(dir: PathBuf, edit: bool, body: Option<String>, title: Title) {
89+ let workspace = Workspace::from_path(dir).expect("Unable to find .tsk dir");
90+ let title = if let Some(title) = title.title {
91+ title
92+ } else if let Some(title) = title.title_simple {
93+ let joined = title.join(" ");
94+ joined
95+ } else {
96+ "".to_string()
97+ };
98+ let mut body = body.unwrap_or_default();
99+ if body == "-" {
100+ // add newline so you can type directly in the shell
101+ eprintln!("");
102+ body.clear();
103+ std::io::stdin()
104+ .read_to_string(&mut body)
105+ .expect("Failed to read stdin");
106+ }
107+ if edit {
108+ body = open_editor(format!("{title}\n\n{body}")).expect("Failed to edit file");
109+ }
110+ let task = workspace
111+ .new_task(title, body)
112+ .expect("Failed to create task");
113+ workspace
114+ .push_task(task)
115+ .expect("Failed to push task to stack");
116+}
117+118+fn command_list(dir: PathBuf, all: bool, count: usize) {
119+ let workspace = Workspace::from_path(dir).expect("Unable to find .tsk dir");
120+ let stack = if all {
121+ workspace.read_stack(None).expect("Failed to read index")
122+ } else {
123+ workspace
124+ .read_stack(Some(count))
125+ .expect("Failed to read index")
126+ };
127+ if stack.empty() {
128+ println!("*No tasks*");
129+ } else {
130+ println!("{}", stack);
131+ }
132+}
133+134+fn command_swap(dir: PathBuf) {
135+ let workspace = Workspace::from_path(dir).expect("Unable to find .tsk dir");
136+ workspace.swap_top().expect("swap to work");
137+}
+144-33
src/stack.rs
···01//! 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.
34use crate::errors::{Error, Result};
5use crate::util;
6-use std::io::{self, BufRead, Read};
0007use std::{fs::File, path::PathBuf};
89use nix::fcntl::{Flock, FlockArg};
1011-use crate::workspace::{Id, Workspace};
1213-struct StackItem {
00014 id: Id,
15 title: String,
16- next: Id,
000000000000000000000000000017}
1819fn eof() -> Error {
···24}
2526impl StackItem {
27- fn from_reader(workspace_path: &PathBuf, reader: &mut impl BufRead) -> Result<Self> {
28- let mut buf = String::new();
29- reader.read_line(&mut buf)?;
30- if buf.is_empty() {
31- return Err(Error::Io(io::Error::new(
32- io::ErrorKind::UnexpectedEof,
33- "Empty line",
34- )));
0000000000000000000000000000000035 }
36- let (id, next) = Self::parse(&buf)?;
37- let title = util::flopen(workspace_path.join("tasks").join(id), mode)
38- todo!();
39- }
40-41- fn parse(line: &str) -> Result<(Id, Id)> {
42- let mut split = line.split("->");
43- let curr = split.next().ok_or(eof())?;
44- let next = split.next().ok_or(eof())?;
45- if let Some(rest) = split.next() {
46- Err(Error::Parse(format!(
47- "Got unexpected data in index item: {rest}"
48- )))
49- } else {
50- Ok((curr.parse()?, next.parse()?))
51- }
52 }
53}
5455pub struct TaskStack {
56- /// The index into `all` that is the top of the stack
57- top: usize,
58- all: Vec<StackItem>,
59 file: Flock<File>,
60}
6100000000062impl TaskStack {
63- fn from_tskdir(path: &PathBuf) -> Result<Self> {
64- todo!()
0000000000000000000000000000000000000000000000065 }
66}
···1+#![allow(dead_code)]
2//! The Task stack. Tasks created with `push` end up at the top here. It is invalid for a task that
3//! has been completed/archived to be on the stack.
45use crate::errors::{Error, Result};
6use crate::util;
7+use std::collections::VecDeque;
8+use std::fmt::Display;
9+use std::io::{self, BufRead, BufReader, Seek, Write};
10+use std::time::{Duration, SystemTime, UNIX_EPOCH};
11use std::{fs::File, path::PathBuf};
1213use nix::fcntl::{Flock, FlockArg};
1415+use crate::workspace::{Id, Task};
1617+const TASKSFOLDER: &str = "tasks";
18+const INDEXFILE: &str = "index";
19+20+pub(crate) struct StackItem {
21 id: Id,
22 title: String,
23+ modify_time: SystemTime,
24+}
25+26+impl Display for StackItem {
27+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28+ // .trim is used here on the title because there may be a newline in here if we read the
29+ // title from the task file.
30+ write!(
31+ f,
32+ // NOTE: we do NOT print the access time.
33+ "{}\t{}",
34+ self.id,
35+ self.title.trim(),
36+ )
37+ }
38+}
39+40+impl TryFrom<Task> for StackItem {
41+ type Error = Error;
42+43+ fn try_from(value: Task) -> std::result::Result<Self, Self::Error> {
44+ let modify_time = value.file.metadata()?.modified()?;
45+ Ok(Self {
46+ id: value.id,
47+ // replace tabs with spaces, they're not valid in StackItem titles.
48+ title: value.title.replace("\t", " "),
49+ modify_time,
50+ })
51+ }
52}
5354fn eof() -> Error {
···59}
6061impl StackItem {
62+ /// Parses a [`StackItem`] from a string. The expected format is a tab-delimited line with the
63+ /// files: task id title
64+ fn from_line(workspace_path: &PathBuf, line: String) -> Result<Self> {
65+ let mut parts = line.split("\t");
66+ let id: Id = parts
67+ .next()
68+ .ok_or(Error::Parse(format!(
69+ "Incomplete index line. Missing tsk ID"
70+ )))?
71+ .parse()?;
72+ let mut title: String = parts
73+ .next()
74+ .ok_or(Error::Parse(format!(
75+ "Incomplete index line. Missing title."
76+ )))?
77+ .trim()
78+ .to_string();
79+ // parse the timestamp as an integer
80+ let index_epoch: u64 = parts.next().unwrap_or("0").parse()?;
81+ // get a usable system time from the UNIX epoch, defaulting to the UNIX_EPOCH if there's
82+ // any failures. This means that if there's errors, we will always read the title and
83+ // modify_time from the task file.
84+ let mut modify_time = UNIX_EPOCH
85+ .checked_add(Duration::from_secs(index_epoch))
86+ .unwrap_or(UNIX_EPOCH);
87+ let modify_epoch = modify_time
88+ .duration_since(UNIX_EPOCH)
89+ .expect("We're before the dawn of time!?")
90+ .as_secs();
91+ let task = util::flopen(
92+ workspace_path.join(TASKSFOLDER).join(id.to_string()),
93+ FlockArg::LockExclusive,
94+ )?;
95+ let task_modify_time = task.metadata()?.modified()?;
96+ // if the task file has been modified since we last looked at it, re-read the title and
97+ // metadata
98+ if modify_epoch > index_epoch {
99+ title.clear();
100+ BufReader::new(&*task).read_line(&mut title)?;
101+ modify_time = task_modify_time;
102 }
103+ Ok(Self {
104+ id,
105+ title,
106+ modify_time,
107+ })
00000000000108 }
109}
110111pub struct TaskStack {
112+ /// All items within the stack
113+ all: VecDeque<StackItem>,
0114 file: Flock<File>,
115}
116117+impl Display for TaskStack {
118+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119+ for task in self.all.iter() {
120+ write!(f, "{task}\n")?;
121+ }
122+ Ok(())
123+ }
124+}
125+126impl TaskStack {
127+ pub fn from_tskdir(workspace_path: &PathBuf, count: Option<usize>) -> Result<Self> {
128+ let file = util::flopen(workspace_path.join(INDEXFILE), FlockArg::LockExclusive)?;
129+ let index = BufReader::new(&*file).lines();
130+ let mut all = VecDeque::new();
131+ if let Some(count) = count {
132+ for line in index.take(count) {
133+ let line = line?;
134+ let stack_item = StackItem::from_line(workspace_path, line)?;
135+ all.push_back(stack_item);
136+ }
137+ } else {
138+ for line in index {
139+ let stack_item = StackItem::from_line(workspace_path, line?)?;
140+ all.push_back(stack_item);
141+ }
142+ };
143+ Ok(Self { all, file })
144+ }
145+146+ /// Saves the task stack to disk.
147+ pub fn save(mut self) -> Result<()> {
148+ // Clear the file
149+ self.file.seek(std::io::SeekFrom::Start(0))?;
150+ self.file.set_len(0)?;
151+ for item in self.all.iter() {
152+ self.file.write_all(format!("{item}\n").as_bytes())?;
153+ }
154+ Ok(())
155+ }
156+157+ pub fn push(&mut self, item: StackItem) {
158+ self.all.push_front(item);
159+ }
160+161+ pub fn pop(&mut self) -> Option<StackItem> {
162+ self.all.pop_front()
163+ }
164+165+ pub fn swap(&mut self) {
166+ let tip = self.all.pop_front();
167+ let second = self.all.pop_front();
168+ if tip.is_some() && second.is_some() {
169+ self.all.push_front(tip.unwrap());
170+ self.all.push_front(second.unwrap());
171+ }
172+ }
173+174+ pub fn empty(&self) -> bool {
175+ self.all.is_empty()
176 }
177}