···11mod errors;
22-mod workspace;
32mod stack;
43mod util;
55-mod buffer;
44+mod workspace;
65use std::path::PathBuf;
76use std::{env::current_dir, io::Read};
87use workspace::Workspace;
···4746 #[command(flatten)]
4847 title: Title,
4948 },
4949+ List {
5050+ /// Whether to list all tasks in the task stack. If specified, -c / count is ignored.
5151+ #[arg(short = 'a', default_value_t = false)]
5252+ all: bool,
5353+ #[arg(short = 'c', default_value_t = 10)]
5454+ count: usize,
5555+ },
5656+5757+ Swap,
5058}
51595260#[derive(Args)]
···6472fn main() {
6573 let cli = Cli::parse();
6674 match cli.command {
6767- Commands::Init => Workspace::init(cli.dir.unwrap_or(default_dir())).expect("Init failed"),
7575+ Commands::Init => command_init(cli.dir.unwrap_or(default_dir())),
6876 Commands::Push { edit, body, title } => {
6969- let title = if let Some(title) = title.title {
7070- title
7171- } else if let Some(title) = title.title_simple {
7272- let joined = title.join(" ");
7373- joined
7474- } else {
7575- "".to_string()
7676- };
7777- let mut body = body.unwrap_or_default();
7878- if body == "-" {
7979- // add newline so you can type directly in the shell
8080- eprintln!("");
8181- body.clear();
8282- std::io::stdin()
8383- .read_to_string(&mut body)
8484- .expect("Failed to read stdin");
8585- }
8686- if edit {
8787- body = open_editor(format!("{title}\n\n{body}")).expect("Failed to edit file");
8888- }
8989- Workspace::from_path(cli.dir.unwrap_or(default_dir()))
9090- .expect("Unable to find .tsk dir")
9191- .new_task(title, body)
9292- .expect("Failed to create task");
7777+ command_push(cli.dir.unwrap_or(default_dir()), edit, body, title)
9378 }
7979+ Commands::List { all, count } => command_list(cli.dir.unwrap_or(default_dir()), all, count),
8080+ Commands::Swap => command_swap(cli.dir.unwrap_or(default_dir()))
9481 }
9582}
8383+8484+fn command_init(dir: PathBuf) {
8585+ Workspace::init(dir).expect("Init failed")
8686+}
8787+8888+fn command_push(dir: PathBuf, edit: bool, body: Option<String>, title: Title) {
8989+ let workspace = Workspace::from_path(dir).expect("Unable to find .tsk dir");
9090+ let title = if let Some(title) = title.title {
9191+ title
9292+ } else if let Some(title) = title.title_simple {
9393+ let joined = title.join(" ");
9494+ joined
9595+ } else {
9696+ "".to_string()
9797+ };
9898+ let mut body = body.unwrap_or_default();
9999+ if body == "-" {
100100+ // add newline so you can type directly in the shell
101101+ eprintln!("");
102102+ body.clear();
103103+ std::io::stdin()
104104+ .read_to_string(&mut body)
105105+ .expect("Failed to read stdin");
106106+ }
107107+ if edit {
108108+ body = open_editor(format!("{title}\n\n{body}")).expect("Failed to edit file");
109109+ }
110110+ let task = workspace
111111+ .new_task(title, body)
112112+ .expect("Failed to create task");
113113+ workspace
114114+ .push_task(task)
115115+ .expect("Failed to push task to stack");
116116+}
117117+118118+fn command_list(dir: PathBuf, all: bool, count: usize) {
119119+ let workspace = Workspace::from_path(dir).expect("Unable to find .tsk dir");
120120+ let stack = if all {
121121+ workspace.read_stack(None).expect("Failed to read index")
122122+ } else {
123123+ workspace
124124+ .read_stack(Some(count))
125125+ .expect("Failed to read index")
126126+ };
127127+ if stack.empty() {
128128+ println!("*No tasks*");
129129+ } else {
130130+ println!("{}", stack);
131131+ }
132132+}
133133+134134+fn command_swap(dir: PathBuf) {
135135+ let workspace = Workspace::from_path(dir).expect("Unable to find .tsk dir");
136136+ workspace.swap_top().expect("swap to work");
137137+}
+144-33
src/stack.rs
···11+#![allow(dead_code)]
12//! The Task stack. Tasks created with `push` end up at the top here. It is invalid for a task that
23//! has been completed/archived to be on the stack.
3445use crate::errors::{Error, Result};
56use crate::util;
66-use std::io::{self, BufRead, Read};
77+use std::collections::VecDeque;
88+use std::fmt::Display;
99+use std::io::{self, BufRead, BufReader, Seek, Write};
1010+use std::time::{Duration, SystemTime, UNIX_EPOCH};
711use std::{fs::File, path::PathBuf};
812913use nix::fcntl::{Flock, FlockArg};
10141111-use crate::workspace::{Id, Workspace};
1515+use crate::workspace::{Id, Task};
12161313-struct StackItem {
1717+const TASKSFOLDER: &str = "tasks";
1818+const INDEXFILE: &str = "index";
1919+2020+pub(crate) struct StackItem {
1421 id: Id,
1522 title: String,
1616- next: Id,
2323+ modify_time: SystemTime,
2424+}
2525+2626+impl Display for StackItem {
2727+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2828+ // .trim is used here on the title because there may be a newline in here if we read the
2929+ // title from the task file.
3030+ write!(
3131+ f,
3232+ // NOTE: we do NOT print the access time.
3333+ "{}\t{}",
3434+ self.id,
3535+ self.title.trim(),
3636+ )
3737+ }
3838+}
3939+4040+impl TryFrom<Task> for StackItem {
4141+ type Error = Error;
4242+4343+ fn try_from(value: Task) -> std::result::Result<Self, Self::Error> {
4444+ let modify_time = value.file.metadata()?.modified()?;
4545+ Ok(Self {
4646+ id: value.id,
4747+ // replace tabs with spaces, they're not valid in StackItem titles.
4848+ title: value.title.replace("\t", " "),
4949+ modify_time,
5050+ })
5151+ }
1752}
18531954fn eof() -> Error {
···2459}
25602661impl StackItem {
2727- fn from_reader(workspace_path: &PathBuf, reader: &mut impl BufRead) -> Result<Self> {
2828- let mut buf = String::new();
2929- reader.read_line(&mut buf)?;
3030- if buf.is_empty() {
3131- return Err(Error::Io(io::Error::new(
3232- io::ErrorKind::UnexpectedEof,
3333- "Empty line",
3434- )));
6262+ /// Parses a [`StackItem`] from a string. The expected format is a tab-delimited line with the
6363+ /// files: task id title
6464+ fn from_line(workspace_path: &PathBuf, line: String) -> Result<Self> {
6565+ let mut parts = line.split("\t");
6666+ let id: Id = parts
6767+ .next()
6868+ .ok_or(Error::Parse(format!(
6969+ "Incomplete index line. Missing tsk ID"
7070+ )))?
7171+ .parse()?;
7272+ let mut title: String = parts
7373+ .next()
7474+ .ok_or(Error::Parse(format!(
7575+ "Incomplete index line. Missing title."
7676+ )))?
7777+ .trim()
7878+ .to_string();
7979+ // parse the timestamp as an integer
8080+ let index_epoch: u64 = parts.next().unwrap_or("0").parse()?;
8181+ // get a usable system time from the UNIX epoch, defaulting to the UNIX_EPOCH if there's
8282+ // any failures. This means that if there's errors, we will always read the title and
8383+ // modify_time from the task file.
8484+ let mut modify_time = UNIX_EPOCH
8585+ .checked_add(Duration::from_secs(index_epoch))
8686+ .unwrap_or(UNIX_EPOCH);
8787+ let modify_epoch = modify_time
8888+ .duration_since(UNIX_EPOCH)
8989+ .expect("We're before the dawn of time!?")
9090+ .as_secs();
9191+ let task = util::flopen(
9292+ workspace_path.join(TASKSFOLDER).join(id.to_string()),
9393+ FlockArg::LockExclusive,
9494+ )?;
9595+ let task_modify_time = task.metadata()?.modified()?;
9696+ // if the task file has been modified since we last looked at it, re-read the title and
9797+ // metadata
9898+ if modify_epoch > index_epoch {
9999+ title.clear();
100100+ BufReader::new(&*task).read_line(&mut title)?;
101101+ modify_time = task_modify_time;
35102 }
3636- let (id, next) = Self::parse(&buf)?;
3737- let title = util::flopen(workspace_path.join("tasks").join(id), mode)
3838- todo!();
3939- }
4040-4141- fn parse(line: &str) -> Result<(Id, Id)> {
4242- let mut split = line.split("->");
4343- let curr = split.next().ok_or(eof())?;
4444- let next = split.next().ok_or(eof())?;
4545- if let Some(rest) = split.next() {
4646- Err(Error::Parse(format!(
4747- "Got unexpected data in index item: {rest}"
4848- )))
4949- } else {
5050- Ok((curr.parse()?, next.parse()?))
5151- }
103103+ Ok(Self {
104104+ id,
105105+ title,
106106+ modify_time,
107107+ })
52108 }
53109}
5411055111pub struct TaskStack {
5656- /// The index into `all` that is the top of the stack
5757- top: usize,
5858- all: Vec<StackItem>,
112112+ /// All items within the stack
113113+ all: VecDeque<StackItem>,
59114 file: Flock<File>,
60115}
61116117117+impl Display for TaskStack {
118118+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119119+ for task in self.all.iter() {
120120+ write!(f, "{task}\n")?;
121121+ }
122122+ Ok(())
123123+ }
124124+}
125125+62126impl TaskStack {
6363- fn from_tskdir(path: &PathBuf) -> Result<Self> {
6464- todo!()
127127+ pub fn from_tskdir(workspace_path: &PathBuf, count: Option<usize>) -> Result<Self> {
128128+ let file = util::flopen(workspace_path.join(INDEXFILE), FlockArg::LockExclusive)?;
129129+ let index = BufReader::new(&*file).lines();
130130+ let mut all = VecDeque::new();
131131+ if let Some(count) = count {
132132+ for line in index.take(count) {
133133+ let line = line?;
134134+ let stack_item = StackItem::from_line(workspace_path, line)?;
135135+ all.push_back(stack_item);
136136+ }
137137+ } else {
138138+ for line in index {
139139+ let stack_item = StackItem::from_line(workspace_path, line?)?;
140140+ all.push_back(stack_item);
141141+ }
142142+ };
143143+ Ok(Self { all, file })
144144+ }
145145+146146+ /// Saves the task stack to disk.
147147+ pub fn save(mut self) -> Result<()> {
148148+ // Clear the file
149149+ self.file.seek(std::io::SeekFrom::Start(0))?;
150150+ self.file.set_len(0)?;
151151+ for item in self.all.iter() {
152152+ self.file.write_all(format!("{item}\n").as_bytes())?;
153153+ }
154154+ Ok(())
155155+ }
156156+157157+ pub fn push(&mut self, item: StackItem) {
158158+ self.all.push_front(item);
159159+ }
160160+161161+ pub fn pop(&mut self) -> Option<StackItem> {
162162+ self.all.pop_front()
163163+ }
164164+165165+ pub fn swap(&mut self) {
166166+ let tip = self.all.pop_front();
167167+ let second = self.all.pop_front();
168168+ if tip.is_some() && second.is_some() {
169169+ self.all.push_front(tip.unwrap());
170170+ self.all.push_front(second.unwrap());
171171+ }
172172+ }
173173+174174+ pub fn empty(&self) -> bool {
175175+ self.all.is_empty()
65176 }
66177}