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