tangled
alpha
login
or
join now
ngp.computer
/
tsk
A file-based task manager
0
fork
atom
overview
issues
pulls
pipelines
Make better use of fzf's search
ngp.computer
2 months ago
29fccf98
8b44c770
+67
-42
5 changed files
expand all
collapse all
unified
split
src
attrs.rs
fzf.rs
main.rs
stack.rs
workspace.rs
+1
-1
src/attrs.rs
···
0
1
use std::collections::btree_map::Entry;
2
use std::collections::btree_map::{IntoIter as BTreeIntoIter, Iter as BTreeMapIter};
3
-
use std::collections::BTreeMap;
4
use std::iter::Chain;
5
6
type Map = BTreeMap<String, String>;
···
1
+
use std::collections::BTreeMap;
2
use std::collections::btree_map::Entry;
3
use std::collections::btree_map::{IntoIter as BTreeIntoIter, Iter as BTreeMapIter};
0
4
use std::iter::Chain;
5
6
type Map = BTreeMap<String, String>;
+14
-6
src/fzf.rs
···
1
use crate::errors::{Error, Result};
0
2
use std::fmt::Display;
3
use std::io::Write;
4
use std::process::{Command, Stdio};
···
6
7
/// 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>>
0
0
0
10
where
11
-
I: Display + FromStr,
12
-
Error: From<<I as FromStr>::Err>,
0
0
13
{
14
-
let mut child = Command::new("fzf")
15
-
.args(["-d", "\t"])
0
0
16
.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() {
···
1
use crate::errors::{Error, Result};
2
+
use std::ffi::OsStr;
3
use std::fmt::Display;
4
use std::io::Write;
5
use std::process::{Command, Stdio};
···
7
8
/// 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>>
14
where
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() {
+1
-1
src/main.rs
···
5
mod task;
6
mod util;
7
mod workspace;
8
-
use clap_complete::{generate, Shell};
9
use errors::Result;
10
use std::io::{self, Write};
11
use std::path::PathBuf;
···
5
mod task;
6
mod util;
7
mod workspace;
8
+
use clap_complete::{Shell, generate};
9
use errors::Result;
10
use std::io::{self, Write};
11
use std::path::PathBuf;
+1
-1
src/stack.rs
···
4
5
use crate::errors::{Error, Result};
6
use crate::util;
7
-
use std::collections::vec_deque::Iter;
8
use std::collections::VecDeque;
0
9
use std::fmt::Display;
10
use std::fs::File;
11
use std::io::{self, BufRead, BufReader, Seek, Write};
···
4
5
use crate::errors::{Error, Result};
6
use crate::util;
0
7
use std::collections::VecDeque;
8
+
use std::collections::vec_deque::Iter;
9
use std::fmt::Display;
10
use std::fs::File;
11
use std::io::{self, BufRead, BufReader, Seek, Write};
+50
-33
src/workspace.rs
···
7
use crate::stack::{StackItem, TaskStack};
8
use crate::task::parse as parse_task;
9
use crate::{fzf, util};
10
-
use std::collections::{vec_deque, BTreeMap, HashSet};
11
use std::ffi::OsString;
12
use std::fmt::Display;
13
-
use std::fs::{remove_file, File};
14
use std::io::{BufRead as _, BufReader, Read, Seek, SeekFrom};
15
use std::ops::Deref;
16
use std::os::unix::fs::symlink;
17
use std::path::PathBuf;
0
18
use std::str::FromStr;
19
use std::{fs::OpenOptions, io::Write};
20
···
30
type Err = Error;
31
32
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
33
-
let s = s
0
34
.trim()
35
-
.strip_prefix("tsk-")
36
.ok_or(Self::Err::Parse(format!("expected tsk- prefix. Got {s}")))?;
37
Ok(Self(s.parse()?))
38
}
···
93
.create(true)
94
.truncate(true)
95
.open(tsk_dir.join("next"))?;
0
96
next.write_all(b"1\n")?;
97
Ok(())
98
}
···
347
workspace: self,
348
};
349
// search the entirety of a task
350
-
Ok(fzf::select(loader)?.map(|bt| bt.id))
0
0
0
0
0
0
0
0
0
0
0
0
351
} else {
352
// just search the stack
353
-
Ok(fzf::select(stack)?.map(|si| si.id))
0
0
0
354
}
355
}
356
···
406
Ok(())
407
}
408
0
409
fn bare(self) -> SearchTask {
410
SearchTask {
411
id: self.id,
···
422
pub body: String,
423
}
424
425
-
impl FromStr for SearchTask {
426
-
type Err = Error;
427
-
428
-
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
429
-
let (tsk_id, task_content) = s.split_once('\t').ok_or(Error::Parse(
430
-
"Missing TSK-ID or content or task parse.".to_owned(),
431
-
))?;
432
-
let (title, body) = task_content
433
-
.split_once('\t')
434
-
.ok_or(Error::Parse("Missing body for task parse.".to_owned()))?;
435
-
Ok(Self {
436
-
id: tsk_id.parse()?,
437
-
title: title.to_string(),
438
-
body: body.to_string(),
439
-
})
440
-
}
441
-
}
442
-
443
impl Display for SearchTask {
444
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
445
-
write!(
446
-
f,
447
-
"{}\t{}\t{}",
448
-
self.id,
449
-
self.title.trim(),
450
-
self.body.replace('\n', " ").replace('\r', "")
451
-
)
452
}
453
}
454
···
470
}
471
}
472
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
473
#[cfg(test)]
474
mod test {
475
use super::*;
···
479
let task = SearchTask {
480
id: Id(123),
481
title: "Hello, world".to_string(),
482
-
body: "The body of the task.\nAnother line\r\nis here.".to_string(),
483
};
484
assert_eq!(
485
-
"tsk-123\tHello, world\tThe body of the task. Another line is here.",
486
task.to_string()
487
);
488
}
···
7
use crate::stack::{StackItem, TaskStack};
8
use crate::task::parse as parse_task;
9
use crate::{fzf, util};
10
+
use std::collections::{BTreeMap, HashSet, vec_deque};
11
use std::ffi::OsString;
12
use std::fmt::Display;
13
+
use std::fs::{File, remove_file};
14
use std::io::{BufRead as _, BufReader, Read, Seek, SeekFrom};
15
use std::ops::Deref;
16
use std::os::unix::fs::symlink;
17
use std::path::PathBuf;
18
+
use std::process::{Command, Stdio};
19
use std::str::FromStr;
20
use std::{fs::OpenOptions, io::Write};
21
···
31
type Err = Error;
32
33
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
34
+
let upper = s.to_uppercase();
35
+
let s = upper
36
.trim()
37
+
.strip_prefix("TSK-")
38
.ok_or(Self::Err::Parse(format!("expected tsk- prefix. Got {s}")))?;
39
Ok(Self(s.parse()?))
40
}
···
95
.create(true)
96
.truncate(true)
97
.open(tsk_dir.join("next"))?;
98
+
// initialize the next file with ID 1
99
next.write_all(b"1\n")?;
100
Ok(())
101
}
···
350
workspace: self,
351
};
352
// search the entirety of a task
353
+
Ok(fzf::select::<_, Id, _>(
354
+
loader,
355
+
[
356
+
"--no-multi-line",
357
+
"--accept-nth=1",
358
+
"--delimiter=\t",
359
+
"--preview=tsk show -T {1}",
360
+
"--preview-window=top",
361
+
"--ansi",
362
+
"--info-command=tsk show -T {1} | head -n1",
363
+
"--info=inline-right",
364
+
],
365
+
)?)
366
} else {
367
// just search the stack
368
+
Ok(fzf::select::<_, Id, _>(
369
+
stack,
370
+
["--delimiter=\t", "--accept-nth=1"],
371
+
)?)
372
}
373
}
374
···
424
Ok(())
425
}
426
427
+
/// Returns a [`SearchTas`] which is plain task data with no file or attrs
428
fn bare(self) -> SearchTask {
429
SearchTask {
430
id: self.id,
···
441
pub body: String,
442
}
443
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
444
impl Display for SearchTask {
445
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
446
+
write!(f, "{}\t{}", self.id, self.title.trim())?;
447
+
if !self.body.is_empty() {
448
+
write!(f, "\n\n{}", self.body)?;
449
+
}
450
+
Ok(())
0
0
451
}
452
}
453
···
469
}
470
}
471
472
+
fn select_task(input: impl IntoIterator<Item = SearchTask>) -> Result<Option<Id>> {
473
+
let mut child = Command::new("cat")
474
+
.stderr(Stdio::inherit())
475
+
.stdin(Stdio::piped())
476
+
.stdout(Stdio::piped())
477
+
.spawn()?;
478
+
let child_in = child.stdin.as_mut().unwrap();
479
+
for item in input.into_iter() {
480
+
writeln!(child_in, "{item}\0")?;
481
+
}
482
+
let output = child.wait_with_output()?;
483
+
if output.stdout.is_empty() {
484
+
Ok(None)
485
+
} else {
486
+
Ok(Some(String::from_utf8(output.stdout)?.parse()?))
487
+
}
488
+
}
489
+
490
#[cfg(test)]
491
mod test {
492
use super::*;
···
496
let task = SearchTask {
497
id: Id(123),
498
title: "Hello, world".to_string(),
499
+
body: "The body of the task.\nAnother line is here.".to_string(),
500
};
501
assert_eq!(
502
+
"tsk-123\tHello, world\n\nThe body of the task.\nAnother line is here.",
503
task.to_string()
504
);
505
}