···1+DO THE THING
2+3+4+remember to do part1
5+6+and part2
7+8+and part3
+3-3
.tsk/index
···1-tsk-30 Add flag to only print IDs in list command 1735007126
2tsk-28 Add tool to clean up old tasks not in index 1735006519
3tsk-10 foreign workspaces 1732594198
4tsk-21 Add command to setup git stuff 1732594198
5-tsk-8 IMAP4-based sync 1732594198
6tsk-17 Add reopen command 1732594198
7-tsk-16 Add ability to search archived tasks with find command 1732594198
8tsk-15 Add link identification to tasks 1732594198
9tsk-9 fix timestamp storage and parsing 1732594198
10tsk-7 allow for creating tasks that don't go to top of stack 1732594198
···1+tsk-30 Add flag to only print IDs in list command 1763257109
2tsk-28 Add tool to clean up old tasks not in index 1735006519
3tsk-10 foreign workspaces 1732594198
4tsk-21 Add command to setup git stuff 1732594198
5+tsk-8 IMAP4-based sync 1767469318
6tsk-17 Add reopen command 1732594198
7+tsk-16 Add ability to search archived tasks with find command 1767466011
8tsk-15 Add link identification to tasks 1732594198
9tsk-9 fix timestamp storage and parsing 1732594198
10tsk-7 allow for creating tasks that don't go to top of stack 1732594198
···184A quick overview of the format:
185186- \!Bolded\! text is surrounded by exclamation marks (!)
187-- \*Italicized\* text is surrouneded by single asterists (*)
188- \_Underlined\_ text is surrounded by underscores (_)
189-- \~Strikenthrough\~ text is surrounded by tildes (~)
00190191Links like in Markdown, along with the wiki-style links documented above.
0192193Misc
194----
···184A quick overview of the format:
185186- \!Bolded\! text is surrounded by exclamation marks (!)
187+- \*Italicized\* text is surrounded by single asterisks (*)
188- \_Underlined\_ text is surrounded by underscores (_)
189+- \~Strikethrough\~ text is surrounded by tildes (~)
190+- \=Highlighted\= text is surrounded by equals signs (=)
191+- \`Inline code\` is surrounded by backticks (`)
192193Links like in Markdown, along with the wiki-style links documented above.
194+Raw links can also be written as \<https://example.com\>.
195196Misc
197----
+1-1
src/attrs.rs
···01use std::collections::btree_map::Entry;
2use std::collections::btree_map::{IntoIter as BTreeIntoIter, Iter as BTreeMapIter};
3-use std::collections::BTreeMap;
4use std::iter::Chain;
56type Map = BTreeMap<String, String>;
···1+use std::collections::BTreeMap;
2use std::collections::btree_map::Entry;
3use std::collections::btree_map::{IntoIter as BTreeIntoIter, Iter as BTreeMapIter};
04use std::iter::Chain;
56type Map = BTreeMap<String, String>;
+14-6
src/fzf.rs
···1use crate::errors::{Error, Result};
02use std::fmt::Display;
3use std::io::Write;
4use std::process::{Command, Stdio};
···67/// Sends each item as a line to stdin to the `fzf` command and returns the selected item's string
8/// representation as output
9-pub fn select<I>(input: impl IntoIterator<Item = I>) -> Result<Option<I>>
00010where
11- I: Display + FromStr,
12- Error: From<<I as FromStr>::Err>,
0013{
14- let mut child = Command::new("fzf")
15- .args(["-d", "\t"])
0016 .stderr(Stdio::inherit())
17 .stdin(Stdio::piped())
18 .stdout(Stdio::piped())
···20 // unwrap: this can never fail
21 let child_in = child.stdin.as_mut().unwrap();
22 for item in input.into_iter() {
23- writeln!(child_in, "{item}")?;
24 }
25 let output = child.wait_with_output()?;
26 if output.stdout.is_empty() {
···1use crate::errors::{Error, Result};
2+use std::ffi::OsStr;
3use std::fmt::Display;
4use std::io::Write;
5use std::process::{Command, Stdio};
···78/// Sends each item as a line to stdin to the `fzf` command and returns the selected item's string
9/// representation as output
10+pub fn select<I, O, S>(
11+ input: impl IntoIterator<Item = I>,
12+ extra: impl IntoIterator<Item = S>,
13+) -> Result<Option<O>>
14where
15+ O: FromStr,
16+ I: Display,
17+ Error: From<<O as FromStr>::Err>,
18+ S: AsRef<OsStr>,
19{
20+ let mut command = Command::new("fzf");
21+ let mut child = command
22+ .args(extra)
23+ .arg("--read0")
24 .stderr(Stdio::inherit())
25 .stdin(Stdio::piped())
26 .stdout(Stdio::piped())
···28 // unwrap: this can never fail
29 let child_in = child.stdin.as_mut().unwrap();
30 for item in input.into_iter() {
31+ write!(child_in, "{item}\0")?;
32 }
33 let output = child.wait_with_output()?;
34 if output.stdout.is_empty() {
+31-22
src/main.rs
···5mod task;
6mod util;
7mod workspace;
8-use clap_complete::{generate, Shell};
9use errors::Result;
10use std::io::{self, Write};
11use std::path::PathBuf;
···86 all: bool,
87 #[arg(short = 'c', default_value_t = 10)]
88 count: usize,
89- #[arg(short = 'i', default_value_t = false)]
90- ids: bool
91 },
9293 /// Swaps the top two tasks on the stack. If there are less than 2 tasks on the stack, there is
···222#[derive(Args)]
223#[group(required = false, multiple = false)]
224struct FindArgs {
225- /// Include the contents of tasks in the search criteria.
226 #[arg(short = 'b', default_value_t = false)]
227- search_body: bool,
228 /* TODO: implement this
229 /// Include archived tasks in the search criteria. Combine with `-b` to include archived
230 /// bodies in the search criteria.
···239 TaskIdentifier::Id(id)
240 } else if value.find.find {
241 TaskIdentifier::Find {
242- search_body: value.find.args.search_body,
243 archived: false,
244 }
245 } else {
···255 Commands::Init => command_init(dir),
256 Commands::Push { edit, body, title } => command_push(dir, edit, body, title),
257 Commands::Append { edit, body, title } => command_append(dir, edit, body, title),
258- Commands::List { all, count, ids } => command_list(dir, all, ids, count),
259 Commands::Swap => command_swap(dir),
260 Commands::Show {
261 task_id,
···293 relative_id: 0,
294 find: Find {
295 find: false,
296- args: FindArgs { search_body: false },
297 },
298 }
299}
···315 } else {
316 "".to_string()
317 };
318- let mut body = body.unwrap_or_default();
00000000000000319 if body == "-" {
320 // add newline so you can type directly in the shell
321 //eprintln!("");
···329 body = content.1.to_string();
330 }
331 }
00332 let task = workspace.new_task(title, body)?;
333 workspace.handle_metadata(&task, None)?;
334 Ok(task)
···346 workspace.append_task(task)
347}
348349-fn command_list(dir: PathBuf, all: bool, only_print_ids: bool, count: usize) -> Result<()> {
350 let workspace = Workspace::from_path(dir)?;
351 let stack = workspace.read_stack()?;
352···360 .enumerate()
361 .take_while(|(idx, _)| all || idx < &count)
362 {
363- match (task::parse(&stack_item.title), only_print_ids) {
364- (None, false) => {
365- println!("{stack_item}");
366- },
367- (Some(parsed), false) => {
368- println!("{}\t{}", stack_item.id, parsed.content.trim())
369- },
370- (None, true) | (Some(_), true) => {
371- println!("{}", stack_item.id)
372- },
373 }
374 }
375 Ok(())
···388 let pre_links = task::parse(&task.to_string()).map(|pt| pt.intenal_links());
389 let new_content = open_editor(format!("{}\n\n{}", task.title.trim(), task.body.trim()))?;
390 if let Some((title, body)) = new_content.split_once("\n") {
391- task.title = title.to_string();
0392 task.body = body.to_string();
393 workspace.handle_metadata(&task, pre_links)?;
394 task.save()?;
···413}
414415fn command_find(dir: PathBuf, short_id: bool, find_args: FindArgs) -> Result<()> {
416- let id = Workspace::from_path(dir)?.search(None, find_args.search_body, false)?;
417 if let Some(id) = id {
418 if short_id {
419 // print as integer
···5mod task;
6mod util;
7mod workspace;
8+use clap_complete::{Shell, generate};
9use errors::Result;
10use std::io::{self, Write};
11use std::path::PathBuf;
···86 all: bool,
87 #[arg(short = 'c', default_value_t = 10)]
88 count: usize,
0089 },
9091 /// Swaps the top two tasks on the stack. If there are less than 2 tasks on the stack, there is
···220#[derive(Args)]
221#[group(required = false, multiple = false)]
222struct FindArgs {
223+ /// Exclude the contents of tasks in the search criteria.
224 #[arg(short = 'b', default_value_t = false)]
225+ exclude_body: bool,
226 /* TODO: implement this
227 /// Include archived tasks in the search criteria. Combine with `-b` to include archived
228 /// bodies in the search criteria.
···237 TaskIdentifier::Id(id)
238 } else if value.find.find {
239 TaskIdentifier::Find {
240+ exclude_body: value.find.args.exclude_body,
241 archived: false,
242 }
243 } else {
···253 Commands::Init => command_init(dir),
254 Commands::Push { edit, body, title } => command_push(dir, edit, body, title),
255 Commands::Append { edit, body, title } => command_append(dir, edit, body, title),
256+ Commands::List { all, count } => command_list(dir, all, count),
257 Commands::Swap => command_swap(dir),
258 Commands::Show {
259 task_id,
···291 relative_id: 0,
292 find: Find {
293 find: false,
294+ args: FindArgs { exclude_body: true },
295 },
296 }
297}
···313 } else {
314 "".to_string()
315 };
316+ // If no body was explicitly provided and the title contains newlines,
317+ // treat the first line as the title and the rest as the body (like git commit -m)
318+ let mut body = if body.is_none() {
319+ if let Some((first_line, rest)) = title.split_once('\n') {
320+ let extracted_body = rest.to_string();
321+ title = first_line.to_string();
322+ extracted_body
323+ } else {
324+ String::new()
325+ }
326+ } else {
327+ // Body was explicitly provided, so strip any newlines from title
328+ title = title.replace(['\n', '\r'], " ");
329+ body.unwrap_or_default()
330+ };
331 if body == "-" {
332 // add newline so you can type directly in the shell
333 //eprintln!("");
···341 body = content.1.to_string();
342 }
343 }
344+ // Ensure title never contains newlines (invariant for index file format)
345+ title = title.replace(['\n', '\r'], " ");
346 let task = workspace.new_task(title, body)?;
347 workspace.handle_metadata(&task, None)?;
348 Ok(task)
···360 workspace.append_task(task)
361}
362363+fn command_list(dir: PathBuf, all: bool, count: usize) -> Result<()> {
364 let workspace = Workspace::from_path(dir)?;
365 let stack = workspace.read_stack()?;
366···374 .enumerate()
375 .take_while(|(idx, _)| all || idx < &count)
376 {
377+ if let Some(parsed) = task::parse(&stack_item.title) {
378+ println!("{}\t{}", stack_item.id, parsed.content.trim());
379+ } else {
380+ println!("{stack_item}");
000000381 }
382 }
383 Ok(())
···396 let pre_links = task::parse(&task.to_string()).map(|pt| pt.intenal_links());
397 let new_content = open_editor(format!("{}\n\n{}", task.title.trim(), task.body.trim()))?;
398 if let Some((title, body)) = new_content.split_once("\n") {
399+ // Ensure title never contains newlines (invariant for index file format)
400+ task.title = title.replace(['\n', '\r'], " ");
401 task.body = body.to_string();
402 workspace.handle_metadata(&task, pre_links)?;
403 task.save()?;
···422}
423424fn command_find(dir: PathBuf, short_id: bool, find_args: FindArgs) -> Result<()> {
425+ let id = Workspace::from_path(dir)?.search(None, !find_args.exclude_body, false)?;
426 if let Some(id) = id {
427 if short_id {
428 // print as integer