175 Commits

Author SHA1 Message Date
bel
45b2060083 add -dedupe to move dupe TODO and SCHEDULED to DONE 2024-12-31 10:21:36 -07:00
bel
1413d02764 accept -archive to push -f Done into -archive Done 2024-12-31 10:09:55 -07:00
bel
284b57b9c4 accept -g to merge files 2024-12-31 10:03:37 -07:00
bel
9539d54145 oopoop 2024-12-20 20:39:55 -07:00
bel
c17e61a6ca oop 2024-12-20 20:39:39 -07:00
bel
def5422482 Merge branch 'master' of https://gitea.inhome.blapointe.com/gogs/pttodo 2024-12-20 20:38:21 -07:00
bel
f72cb35e79 instead of os.rename, io.WriteFile and os.Remove 2024-12-20 20:38:11 -07:00
Bel LaPointe
7344c660ed todos as failing tests 2024-08-10 09:30:06 -06:00
Bel LaPointe
301fb6046f tests yay 2024-08-09 17:22:08 -06:00
Bel LaPointe
b067d0fb8e failing test 2024-08-09 17:07:25 -06:00
Bel LaPointe
e05509f28f tests are good 2024-08-09 15:48:10 -07:00
Bel LaPointe
33d318edd4 o that swap wasnt too bd 2024-08-08 13:25:38 -07:00
Bel LaPointe
a5f5013ef6 ew 2024-08-08 13:17:32 -07:00
Bel LaPointe
52977ff0a7 insert reveal i cannot handle no src db 2024-08-08 13:14:26 -07:00
Bel LaPointe
452b692c6a wipper 2024-08-08 12:58:21 -07:00
Bel LaPointe
5c74ec0c15 todo 2024-08-08 12:53:15 -07:00
Bel LaPointe
602939b25c revert testdata 2024-08-04 13:36:08 -06:00
Bel LaPointe
7bf1d15835 need tests for all the edits--like adding a scheduled, deleting a scheduled should add a _doned 2024-08-04 13:35:48 -06:00
Bel LaPointe
e4834d8a11 gr 2024-08-04 13:35:29 -06:00
Bel LaPointe
2425f22b56 grr edit dupes 2024-08-04 11:44:22 -06:00
Bel LaPointe
50ea3258b7 edit implemented maybe 2024-08-04 11:42:09 -06:00
Bel LaPointe
6eba05895f comment 2024-06-14 09:59:49 -06:00
Bel LaPointe
5a45cd1410 comment THE PLAN 2024-06-14 09:54:46 -06:00
Bel LaPointe
7832d09fe8 ok vim runs that was ez 2024-06-14 09:49:22 -06:00
Bel LaPointe
aa9e032c31 tempdir ty 2024-06-14 09:27:01 -06:00
Bel LaPointe
b949502869 now it gets gross ew 2024-06-14 09:26:26 -06:00
Bel LaPointe
41cab74028 eprint dont err on conflict 2024-06-14 09:24:05 -06:00
Bel LaPointe
e389f34bd6 impl save and dry run 2024-06-14 09:16:36 -06:00
Bel LaPointe
3cfaf1f1a0 when adding w schedule then set ts to now 2024-06-14 08:42:58 -06:00
Bel LaPointe
b1d1c5d2b2 todo 2024-05-23 16:41:13 -04:00
Bel LaPointe
4d8fd0597b okcool can read and even doesnt wrap so i like that some 2024-05-23 16:36:05 -04:00
Bel LaPointe
8f8202282f parent level DB has multiple tasks that each have a file and file modified timestamp associated 2024-05-23 16:18:06 -04:00
Bel LaPointe
e81f221910 gathering info on read 2024-05-23 16:11:56 -04:00
Bel LaPointe
7ad6dad214 gathering info on read 2024-05-23 16:11:35 -04:00
Bel LaPointe
c7163268d4 stub 2024-05-23 15:45:01 -04:00
Bel LaPointe
e90cf02664 revive test func 2024-05-23 15:37:50 -04:00
Bel LaPointe
7129723442 accept flags but need to accept directories for multifile as buckets 2024-05-22 15:34:04 -04:00
Bel LaPointe
74b02118bc parse go times with tz 2024-05-22 15:21:11 -04:00
Bel LaPointe
c7b0fc8dba less usable key 2024-05-22 14:33:28 -04:00
Bel LaPointe
50e9c80473 ok got Tasks has due func 2024-05-22 14:30:05 -04:00
Bel LaPointe
c5ed06b76c okcool 2024-05-22 14:14:14 -04:00
Bel LaPointe
039c4dad04 okcool 2024-05-22 14:04:38 -04:00
Bel LaPointe
3c132528e3 ok that was fun 2024-05-15 23:33:38 -04:00
Bel LaPointe
1639286189 so close tho 2024-05-15 23:27:03 -04:00
Bel LaPointe
2a04a030f7 functions good 2024-05-15 23:16:26 -04:00
Bel LaPointe
931edec4a4 wip 2024-05-15 23:15:09 -04:00
Bel LaPointe
4154e1aad8 Task has crud 2024-05-15 23:05:34 -04:00
Bel LaPointe
4f4e7fd907 oooo tests pass 2024-05-15 22:56:40 -04:00
Bel LaPointe
a627223e8a oop 2024-05-15 22:54:37 -04:00
Bel LaPointe
00813e39a9 close tho 2024-05-15 22:53:34 -04:00
Bel LaPointe
66f533a210 okeydoke 2024-05-15 22:27:16 -04:00
Bel LaPointe
233967e177 okeydoke 2024-05-15 22:24:21 -04:00
Bel LaPointe
282f653c75 pttodoer 2024-05-15 21:57:05 -04:00
bel
1d958e8a52 later 2024-05-06 22:25:55 -06:00
bel
5a75b3e5f2 o i am too structured huh 2024-05-06 22:10:56 -06:00
bel
ad9c32c987 testdata 2024-05-06 21:18:20 -06:00
bel
3ee1dc069e stub rust? 2024-05-06 21:00:31 -06:00
Bel LaPointe
9b482d45b4 bucket inboxes by target for excessive automoving 2023-11-14 18:36:00 -07:00
Bel LaPointe
ba81940b0b k 2023-11-09 08:19:27 -07:00
Bel LaPointe
5d74b458d5 again 2023-11-09 08:19:14 -07:00
Bel LaPointe
f3ac2c63fe pkg from gogs 2023-11-09 08:18:00 -07:00
Bel LaPointe
a3ca9b1917 more manual testing is all k 2023-11-09 07:51:42 -07:00
Bel LaPointe
ca84bcbee3 inboxes can only merge to Todo because tempfiles only have Todo 2023-11-09 07:50:53 -07:00
Bel LaPointe
89a8f61fa3 cache 2023-11-07 07:20:14 -07:00
Bel LaPointe
d3f34709b3 consistent 2023-11-07 06:11:30 -07:00
Bel LaPointe
ea904ea729 if no original file, then edit merges with nil and calls itself an inbox 2023-11-07 06:10:36 -07:00
bel
ead31e077b edit doesnt excess delete but keeps to many files like inboxes 2023-11-06 22:19:39 -07:00
bel
36c5228a6c oop 2023-11-06 22:07:38 -07:00
bel
72fee73a00 dont yield dirs 2023-11-06 21:39:57 -07:00
bel
550bace88d manual test an inbox 2023-11-06 21:29:33 -07:00
bel
0329b9690c untested refacotor of edit.go 2023-11-06 21:25:30 -07:00
bel
fe8595cc68 comment 2023-11-06 20:58:03 -07:00
bel
3549010ab4 wipper on edit.go 2023-11-06 20:49:08 -07:00
Bel LaPointe
caa9f9d6fd progressing 2023-11-06 13:29:56 -07:00
Bel LaPointe
8b63254cf0 progressing 2023-11-06 13:29:21 -07:00
Bel LaPointe
97f2cb197f wip 2023-11-06 13:23:28 -07:00
Bel LaPointe
b2a51e65b0 time for edit.go 2023-11-06 13:15:01 -07:00
Bel LaPointe
f75dcb3f7a test touched a local file oop 2023-11-06 13:14:20 -07:00
Bel LaPointe
d4466598b4 drop test file 2023-11-06 13:13:42 -07:00
Bel LaPointe
3ba3b78afc use default for effectively default 2023-11-06 13:12:09 -07:00
Bel LaPointe
f2e3e6f505 rm extra mem 2023-11-06 13:11:40 -07:00
Bel LaPointe
1ce120a647 shuffle code 2023-11-06 13:11:22 -07:00
Bel LaPointe
bfdeebb7a2 use pttodo.Todos.Like...() 2023-11-06 13:06:11 -07:00
Bel LaPointe
39345e5e2a use pttodo.NewRootFromFiles in cli 2023-11-06 12:58:46 -07:00
Bel LaPointe
bfcec9d1c5 add pttodo.NewRootFromFiles 2023-11-06 12:58:06 -07:00
Bel LaPointe
3e1f58c7b9 test pttodo.NewRootFromFile accepts both []Todo and Root 2023-11-06 12:54:46 -07:00
Bel LaPointe
92e8e14c08 bubble root.yaml to first 2023-11-06 12:44:31 -07:00
Bel LaPointe
84aa7cb319 test config resolving targets 2023-11-06 12:38:11 -07:00
Bel LaPointe
d36f9e74b3 bake listing files not cached in config 2023-11-06 12:25:52 -07:00
Bel LaPointe
8dacd8da5d ptr 2023-11-06 12:17:46 -07:00
Bel LaPointe
23ef7f13ee gitignore bin 2023-11-06 12:15:29 -07:00
Bel LaPointe
02dab4b726 move pttodo-cli up from /cmd/ 2023-11-06 12:15:12 -07:00
Bel LaPointe
cdd1be46a8 drop unused 2023-11-06 12:14:37 -07:00
Bel LaPointe
760c822323 add pttodo.NewRootFromFile, pttodo.NewTodosFromFile 2023-11-06 12:14:27 -07:00
Bel LaPointe
4e69646e88 drop dumb verifies 2023-11-06 12:14:16 -07:00
Bel LaPointe
9aee322c4e drop old 2023-11-06 12:11:44 -07:00
Bel LaPointe
887875f6d8 add creates a .todo.* file 2023-11-06 12:03:03 -07:00
Bel LaPointe
d2f986d8b6 split config.go 2023-11-06 10:37:34 -07:00
Bel LaPointe
58aa05c155 split dump.go from main.go in cli 2023-11-06 10:35:01 -07:00
Bel LaPointe
97aff0f0b8 split cli.go into add.go, edit.go too 2023-11-06 10:33:32 -07:00
Bel LaPointe
eb20706d12 drop some args 2023-11-06 10:28:36 -07:00
bel
a800227c6f oop 2023-10-15 10:40:13 -06:00
bel
2c7563c1ab default file path if not empty 2023-10-15 10:39:58 -06:00
bel
a8a135bb2f fix nil ptr on no previous file 2023-10-15 10:39:01 -06:00
Bel LaPointe
c1c625afc0 skip hidden files 2023-08-16 06:54:21 -06:00
Bel LaPointe
0b790e3468 nil ptr 2023-07-16 19:47:11 -06:00
Bel LaPointe
7175d777cb on write conflict during vim, save as filename.uuid..fileExtension 2023-07-02 10:22:04 -06:00
Bel LaPointe
debe28dbbc support -add-tags x,y,z 2023-04-24 15:02:13 -06:00
Bel LaPointe
c1426566a0 fix 2023-04-10 10:45:16 -06:00
Bel LaPointe
6f79f7da5e gr 2023-04-10 10:44:41 -06:00
Bel LaPointe
284d7c06bd partial go mod 2023-04-10 10:42:11 -06:00
Bel LaPointe
be702b1d74 gitignore 2022-09-10 11:25:23 -06:00
Bel LaPointe
de62d99340 no prior file exists ok 2022-09-10 11:24:41 -06:00
Bel LaPointe
bca9259caa on scheduled task done, in addition to requeue, add to done 2022-09-10 11:13:18 -06:00
Bel LaPointe
dccf5c4028 quick add via cli -add, -add-schedule 2022-04-07 12:48:17 -06:00
Bel LaPointe
55e174e3b1 root moves fixed future schedules to shceduled on automove 2022-03-24 14:43:45 -06:00
Bel LaPointe
d7dab75f48 revert to clean todo 2022-03-24 09:26:44 -06:00
Bel LaPointe
3426deae4d revert automove for crons 2022-03-24 09:25:34 -06:00
Bel LaPointe
7fe4686c05 always write ts, when a todo with schedule deleted, conditionally go back to schedule for loopers 2022-03-24 09:18:19 -06:00
Bel LaPointe
eb57593665 cli on delete todo, write in Done identified by entire struct so dupes wouldnt but thats ok 2022-03-23 09:46:16 -06:00
Bel LaPointe
814ae3ab23 add todo.ID() 2022-03-23 09:42:21 -06:00
Bel LaPointe
939793bd3f ready for find deleted 2022-03-23 08:06:57 -06:00
Bel LaPointe
1d26cf125f if item in todo is scheduled for later, then move it 2022-03-23 08:04:35 -06:00
Bel LaPointe
e20ce478d5 new edit ready for shuffles 2022-03-23 07:49:54 -06:00
Bel LaPointe
0bd6347a93 getconfig in pttodo-cli 2022-03-23 07:27:49 -06:00
Bel LaPointe
20770ff5e6 update tests for mst 2022-03-23 07:13:55 -06:00
bel
65178b8bdf wip 2022-02-20 17:11:16 -07:00
Bel LaPointe
4066d4aeb5 change default file path from stdin 2022-01-10 09:44:30 -05:00
Bel LaPointe
7f611e67bc update todo 2022-01-09 10:27:30 -05:00
Bel LaPointe
71c03f3ef5 accept THH on ezdate 2022-01-09 10:24:40 -05:00
Bel LaPointe
4b8d82c2a0 dont log noops 2022-01-09 10:22:23 -05:00
Bel LaPointe
850ff92d98 do not save if no effective changes for edit, dump 2022-01-09 10:18:08 -05:00
Bel LaPointe
c2d1381607 add root.equals and todo.equals 2022-01-09 10:17:44 -05:00
Bel LaPointe
33ca7c60e1 root.yaml prio, otherwise sort -f results 2022-01-08 23:44:30 -05:00
Bel LaPointe
cde6ea6cb6 if cli -f is a dir, then edit each file+clean each file+keep each as individual file 2022-01-08 23:42:05 -05:00
Bel LaPointe
6d3f423845 parseinlocation for non utc crud 2022-01-06 21:28:44 -05:00
Bel LaPointe
fe1bd22987 instead of trashing editing file in cli, write err msg and re-edit 2022-01-04 12:37:35 -05:00
Bel LaPointe
6f9589b100 err msg 2022-01-04 12:33:39 -05:00
Bel LaPointe
a0c0cb7053 fix checking hastags and nothas tags 2022-01-04 08:56:19 -05:00
Bel LaPointe
4a22b964db add anti tag search 2022-01-04 07:01:35 -05:00
Bel LaPointe
2caf2ae352 create script to scrape list of lists and scrape aech into a yaml 2022-01-03 16:36:59 -05:00
Bel LaPointe
455a7d52d5 update todo with no more ts in todo section 2022-01-02 20:45:17 -05:00
Bel LaPointe
1a9221f7c7 update todo 2022-01-02 20:44:23 -05:00
Bel LaPointe
6178e6ff93 root does not marshal TSs on todos, always on schedules, dones. Test. 2022-01-02 20:44:04 -05:00
Bel LaPointe
fdb24fcc60 only write TS on a todo if writeTS is set 2022-01-02 20:37:40 -05:00
Bel LaPointe
8002b5e75c new todo 2022-01-02 20:32:16 -05:00
Bel LaPointe
3dd8cd1e03 update todo 2022-01-02 20:31:09 -05:00
Bel LaPointe
c6ab36806d update todo.yaml 2022-01-02 20:17:32 -05:00
Bel LaPointe
bf997c1814 add cli -g to merge in a file 2022-01-02 20:07:52 -05:00
Bel LaPointe
69eb868db6 add root MergeIn 2022-01-01 17:43:20 -05:00
Bel LaPointe
0c80162394 todo 2022-01-01 17:33:59 -05:00
Bel LaPointe
c634fdd4d4 consts for what to print, default to just the todos 2022-01-01 17:30:40 -05:00
Bel LaPointe
92d76443bc ezdate for yyyy-mm-dd for schedule 2022-01-01 17:21:38 -05:00
Bel LaPointe
a51d5e6960 rm test code 2022-01-01 17:14:03 -05:00
Bel LaPointe
7fc594d5c2 ts shouldnt yield zero ever, yield now if so 2022-01-01 17:13:50 -05:00
Bel LaPointe
2d8cfa6397 fix getting ts for completed tasks 2022-01-01 17:04:21 -05:00
Bel LaPointe
f01dc04277 todo 2021-12-31 23:27:21 -05:00
Bel LaPointe
249ee84688 support tag, simple case insensitve search when recursing 2021-12-31 23:20:27 -05:00
Bel LaPointe
4111d1f490 tag saerch on todo 2021-12-31 23:14:31 -05:00
Bel LaPointe
5b6f62983b when writing output file, dont recurse 2021-12-31 23:03:08 -05:00
Bel LaPointe
1056f5d29e update todo 2021-12-31 23:00:16 -05:00
Bel LaPointe
7886723fe3 times are now unix dates over ints 2021-12-31 22:59:13 -05:00
Bel LaPointe
ff77af9ed4 remove unused stub 2021-12-31 22:49:51 -05:00
Bel LaPointe
3bd1c6462d fix syntax highlight by using tempfile.yaml over tempfile 2021-12-31 22:46:49 -05:00
Bel LaPointe
3bcabde553 dont try to find vimrc 2021-12-31 22:45:29 -05:00
Bel LaPointe
604cd610a1 use $EDITOR, default to vim, use $HOME/.vimrc if exists 2021-12-31 22:44:33 -05:00
Bel LaPointe
9d35347b0c updated todo because i oofed 2021-12-31 22:37:47 -05:00
Bel LaPointe
1a8c687260 update install for gomo 2021-12-31 22:32:47 -05:00
Bel LaPointe
3246900db0 if optional positional arg is todo/scheduled/done, then resolve first level 2021-12-31 22:32:07 -05:00
Bel LaPointe
79cc171af5 up install script for location 2021-12-31 22:28:12 -05:00
Bel LaPointe
a75d898487 set ts to currenttime if should display 2021-12-31 22:27:48 -05:00
Bel LaPointe
f63f152b0e cli has dry mode and install script 2021-12-31 22:27:33 -05:00
Bel LaPointe
76a0231511 pttodo-cli default file via env 2021-12-31 22:07:16 -05:00
Bel LaPointe
7f37feea77 pttodo-cli works even if file does not initially exist 2021-12-31 22:05:47 -05:00
Bel LaPointe
b84f8b59c9 rename gomod, root project 2021-12-31 21:58:02 -05:00
43 changed files with 3167 additions and 692 deletions

3
.gitignore vendored
View File

@@ -3,3 +3,6 @@ todo-server-yaml
cmd/cmd cmd/cmd
cmd/cli cmd/cli
cmd/pttodo/pttodo cmd/pttodo/pttodo
cmd/pttodo-cli/pttodo-cli
cmd/pttodo-cli
pttodoer/target

36
cmd/add.go Normal file
View File

@@ -0,0 +1,36 @@
package main
import (
"fmt"
"io/ioutil"
"os"
"github.com/google/uuid"
"gitea.inhome.blapointe.com/gogs/pttodo/pttodo"
"gopkg.in/yaml.v2"
)
func add(config *config) error {
if config.add == "" {
return nil
}
v := pttodo.Todo{
Todo: config.add,
Schedule: pttodo.Schedule(config.addSchedule),
Tags: config.addTags,
}
return _add(config.Targets()[0], v)
}
func _add(filepath string, todo pttodo.Todo) error {
target := fmt.Sprintf("%s.todo.%s", filepath, uuid.New().String())
c, err := yaml.Marshal([]pttodo.Todo{todo})
if err != nil {
return err
} else if err := ioutil.WriteFile(target, c, os.ModePerm); err != nil {
return err
}
return nil
}

106
cmd/config.go Normal file
View File

@@ -0,0 +1,106 @@
package main
import (
"flag"
"fmt"
"os"
"path"
"path/filepath"
"sort"
"strings"
)
type config struct {
target string
target2 string
root string
tags []string
search string
edit bool
add string
addSchedule string
addTags string
dryRun bool
archive string
dedupe bool
}
func getConfig() config {
defaultFilepath := os.Getenv("PTTODO_FILE")
if defaultFilepath == "" {
defaultFilepath = "./todo.yaml"
}
var config config
flag.StringVar(&config.target, "f", defaultFilepath, "($PTTODO_FILE) path to yaml file or dir (starting with root then alphabetical for dir)")
flag.StringVar(&config.target2, "g", "", "path to yaml file to merge into root of -f")
flag.StringVar(&config.archive, "archive", "", "path to yaml file to migrate done")
flag.StringVar(&config.root, "root", DUMP_TODO, "path to pretty print ("+fmt.Sprint([]string{DUMP_ALL, DUMP_TODO, DUMP_SCHEDULED, DUMP_DONE})+")")
var tagss string
flag.StringVar(&tagss, "tags", "", "csv of all tags to find, -x to invert")
flag.StringVar(&config.search, "search", "", "fts case insensitive")
flag.BoolVar(&config.edit, "e", false, "edit file")
flag.BoolVar(&config.dedupe, "dedupe", false, "dedupe file")
flag.BoolVar(&config.dryRun, "dry-run", false, "dry run")
flag.StringVar(&config.add, "add", "", "todo to add")
flag.StringVar(&config.addSchedule, "add-schedule", "", "todo to add schedule")
flag.StringVar(&config.addTags, "add-tags", "", "todo to add csv tags")
flag.Parse()
config.tags = strings.Split(tagss, ",")
config.Targets()
return config
}
func (config config) Targets() []string {
result, err := config.targets()
if err != nil {
panic(err)
}
sort.Strings(result)
for i := range result {
if path.Base(result[i]) == "root.yaml" {
newresult := append([]string{result[i]}, result[:i]...)
newresult = append(newresult, result[i+1:]...)
result = newresult
break
}
}
return result
}
func (config config) targets() ([]string, error) {
patterns := []string{config.target, fmt.Sprintf("%s.*", config.target)}
isDir := false
if stat, err := os.Stat(config.target); err == nil {
isDir = stat.IsDir()
}
if isDir {
patterns = []string{fmt.Sprintf("%s/*", config.target)}
}
result := make([]string, 0, 1)
for _, pattern := range patterns {
results, err := filepath.Glob(pattern)
if err != nil {
return nil, err
}
for i := range results {
if stat, err := os.Stat(results[i]); err != nil {
return nil, err
} else if !stat.IsDir() {
result = append(result, results[i])
}
}
}
if len(result) == 0 {
if isDir {
return []string{path.Join(config.target, "root.yaml")}, nil
}
return []string{config.target}, nil
}
return result, nil
}

87
cmd/config_test.go Normal file
View File

@@ -0,0 +1,87 @@
package main
import (
"os"
"path"
"testing"
)
func TestConfigTargets(t *testing.T) {
touch := func(t *testing.T, p string) {
if err := os.WriteFile(p, nil, os.ModePerm); err != nil {
t.Fatal(err)
}
}
cases := map[string]struct {
setup func(*testing.T, string)
given string
want []string
}{
"does not exist": {
given: "x",
want: []string{"x"},
},
"one file": {
setup: func(t *testing.T, d string) {
touch(t, path.Join(d, "x"))
},
given: "x",
want: []string{"x"},
},
"two files": {
setup: func(t *testing.T, d string) {
touch(t, path.Join(d, "a"))
touch(t, path.Join(d, "root.yaml"))
touch(t, path.Join(d, "z"))
},
given: "",
want: []string{"root.yaml", "a", "z"},
},
"empty dir": {
setup: func(t *testing.T, d string) {
os.Mkdir(path.Join(d, "x"), os.ModePerm)
},
given: "x",
want: []string{"root.yaml"},
},
"dir": {
setup: func(t *testing.T, d string) {
os.Mkdir(path.Join(d, "x"), os.ModePerm)
touch(t, path.Join(d, "x", "a"))
touch(t, path.Join(d, "x", "a.todo.xyz"))
touch(t, path.Join(d, "x", "b"))
},
given: "x",
want: []string{"a", "a.todo.xyz", "b"},
},
}
for name, d := range cases {
c := d
t.Run(name, func(t *testing.T) {
d := t.TempDir()
if c.setup != nil {
c.setup(t, d)
}
config := config{target: path.Join(d, c.given)}
got := config.Targets()
for i := range got {
got[i] = path.Base(got[i])
}
for i := range c.want {
c.want[i] = path.Base(c.want[i])
}
t.Logf("want\n\t%+v, got \n\t%+v", c.want, got)
if len(got) != len(c.want) {
t.Error(c.want, got)
}
for i := range got {
if got[i] != c.want[i] {
t.Errorf("[%d] wanted %s, got %s", i, c.want[i], got[i])
}
}
})
}
}

44
cmd/dump.go Normal file
View File

@@ -0,0 +1,44 @@
package main
import (
"io"
"os"
"gitea.inhome.blapointe.com/gogs/pttodo/pttodo"
"gopkg.in/yaml.v2"
)
func dump(config config) error {
return _dump(os.Stdout, config.Targets(), config.tags, config.search, config.root)
}
func _dump(writer io.Writer, filepaths []string, tags []string, search, rootDisplay string) error {
root, err := pttodo.NewRootFromFiles(filepaths...)
if err != nil {
return err
}
for _, x := range []*[]pttodo.Todo{
&root.Todo,
&root.Scheduled,
&root.Done,
} {
y := pttodo.Todos(*x)
y = y.LikeTags(tags)
y = y.LikeSearch(search)
*x = y
}
var v interface{} = root
switch rootDisplay {
case DUMP_TODO:
v = root.Todo
case DUMP_SCHEDULED:
v = root.Scheduled
case DUMP_DONE:
v = root.Done
default:
}
return yaml.NewEncoder(writer).Encode(v)
}

262
cmd/edit.go Normal file
View File

@@ -0,0 +1,262 @@
package main
import (
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"path"
"sort"
"strings"
"syscall"
"time"
"gitea.inhome.blapointe.com/gogs/pttodo/pttodo"
"github.com/google/uuid"
"gopkg.in/yaml.v2"
)
func edit(config *config) error {
if !config.edit {
return nil
}
return _edit(config.Targets())
}
func _edit(filepaths []string) error {
editableDir, err := inEditableDirAsTodos(filepaths)
if err != nil {
return err
}
if err := modifyEditableDir(editableDir, func() error {
files, err := listDir(editableDir)
if err != nil {
return err
}
for _, p := range files {
if _, err := pttodo.NewTodosFromFile(p); err != nil {
return err
}
}
return nil
}); err != nil {
return err
}
editedFiles, err := listDir(editableDir)
if err != nil {
return err
}
edits := map[string]string{}
for _, editedFile := range editedFiles {
edits[path.Base(editedFile)] = editedFile
edited, err := pttodo.NewTodosFromFile(editedFile)
if err != nil {
return err
}
original, err := pttodo.NewRootFromFile(func() string {
for _, f := range filepaths {
if path.Base(f) == path.Base(editedFile) {
return f
}
}
return "/dev/null"
}())
if err != nil {
return err
}
editedIDs := map[string]int{}
for i := range edited {
editedIDs[edited[i].ID()] = 1
}
for i := range original.Todo {
if _, ok := editedIDs[original.Todo[i].ID()]; ok {
continue
}
original.Todo[i].TS = pttodo.TS(time.Now().Unix())
if string(original.Todo[i].Schedule) != "" && !original.Todo[i].Triggered() {
original.Scheduled = append(original.Scheduled, original.Todo[i])
}
original.Done = append(original.Done, original.Todo[i])
}
original.Todo = edited
original.AutoMove()
if err := func() error {
f, err := os.Create(editedFile)
if err != nil {
return err
}
defer f.Close()
return yaml.NewEncoder(f).Encode(original)
}(); err != nil {
return err
}
}
dir := ""
for _, f := range filepaths {
if edited, ok := edits[path.Base(f)]; ok {
if err := rename(edited, f); err != nil {
return err
}
delete(edits, path.Base(f))
} else if err := os.Remove(f); err != nil {
return err
}
dir = path.Dir(f)
}
for base, editedFile := range edits {
f := path.Join(dir, base)
if _, err := os.Stat(f); err == nil {
f = fmt.Sprintf("%s.todo.%s", f, uuid.New().String())
}
if err := rename(editedFile, f); err != nil {
return err
}
}
return nil
}
func inEditableDirAsTodos(filepaths []string) (string, error) {
tempDir, err := ioutil.TempDir(os.TempDir(), "edit-pttodo-*")
if err != nil {
return "", err
}
return tempDir, copyTodoToDir(tempDir, filepaths)
}
func copyTodoToDir(d string, filepaths []string) error {
inboxes := map[string][]string{}
for _, target := range filepaths {
p := path.Join(d, path.Base(target))
if strings.Contains(path.Base(target), ".todo.") {
p := path.Join(d, strings.Split(path.Base(p), ".todo")[0])
inboxes[p] = append(inboxes[p], target)
continue
}
if root, err := pttodo.NewRootFromFile(target); err != nil {
return err
} else if b, err := yaml.Marshal(root.Todo); err != nil {
return err
} else if err := os.WriteFile(p, b, os.ModePerm); err != nil {
return err
}
}
for p, inboxes := range inboxes {
inboxRoot := pttodo.Root{}
for _, inbox := range inboxes {
subInboxRoot, err := pttodo.NewRootFromFile(inbox)
if err != nil {
return err
}
inboxRoot.MergeIn(pttodo.Root{Todo: subInboxRoot.Todo})
inboxRoot.MergeIn(pttodo.Root{Todo: subInboxRoot.Scheduled})
}
root, err := pttodo.NewRootFromFile(p)
if err != nil {
return err
}
root.MergeIn(pttodo.Root{Todo: inboxRoot.Todo})
root.MergeIn(pttodo.Root{Todo: inboxRoot.Scheduled})
if b, err := yaml.Marshal(root.Todo); err != nil {
return err
} else if err := os.WriteFile(p, b, os.ModePerm); err != nil {
return err
}
}
return nil
}
func modifyEditableDir(d string, check func() error) error {
for {
if err := vimd(d); err != nil {
return err
}
err := check()
if err == nil {
return nil
}
log.Printf("%s, press <Enter> to resume editing", err)
b := make([]byte, 1)
if _, err := os.Stdin.Read(b); err != nil {
return err
}
}
}
func vimd(d string) error {
bin := "vim"
editorbin, err := exec.LookPath(bin)
if err != nil {
editorbin, err = exec.LookPath("vi")
}
if err != nil {
return err
}
args := []string{editorbin, "-p"}
files, err := listDir(d)
if err != nil {
return err
}
args = append(args, files...)
cpid, err := syscall.ForkExec(
editorbin,
args,
&syscall.ProcAttr{
Dir: d,
Env: os.Environ(),
Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()},
Sys: nil,
},
)
if err != nil {
return err
}
proc, err := os.FindProcess(cpid)
if err != nil {
return err
}
state, err := proc.Wait()
if err != nil {
return err
}
if exitCode := state.ExitCode(); exitCode != 0 {
return fmt.Errorf("bad exit code on vim: %d, state: %+v", exitCode, state)
}
return nil
}
func listDir(dname string) ([]string, error) {
entries, err := os.ReadDir(dname)
if err != nil {
return nil, err
}
paths := make([]string, 0, len(entries))
for i := range entries {
if entries[i].IsDir() {
continue
}
if strings.HasPrefix(path.Base(entries[i].Name()), ".") {
continue
}
paths = append(paths, path.Join(dname, entries[i].Name()))
}
sort.Slice(paths, func(i, j int) bool {
if path.Base(paths[i]) == "root.yaml" {
return true
}
return paths[i] < paths[j]
})
return paths, nil
}

13
cmd/go.mod Normal file
View File

@@ -0,0 +1,13 @@
module pttodo-cli
go 1.21
require (
gitea.inhome.blapointe.com/gogs/pttodo v0.0.0-20231109151914-5d74b458d542
github.com/google/uuid v1.3.0
gopkg.in/yaml.v2 v2.4.0
)
require github.com/robfig/cron/v3 v3.0.1 // indirect
replace gogs.inhome.blapointe.com/gogs/pttodo => ./..

View File

@@ -1,3 +1,7 @@
gitea.inhome.blapointe.com/gogs/pttodo v0.0.0-20231109151914-5d74b458d542 h1:eOWrA2hEQoyu413vbdXbEXHLXEX2TVBXjWawlWndFhg=
gitea.inhome.blapointe.com/gogs/pttodo v0.0.0-20231109151914-5d74b458d542/go.mod h1:9CFZf/SSod0Z/8WvbmOI4gEzy6xGpBQAq8coVNncNvk=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=

301
cmd/main.go Normal file
View File

@@ -0,0 +1,301 @@
package main
import (
"bytes"
"io"
"io/ioutil"
"log"
"os"
"slices"
"gitea.inhome.blapointe.com/gogs/pttodo/pttodo"
"gopkg.in/yaml.v2"
)
const (
DUMP_ALL = "all"
DUMP_TODO = "todo"
DUMP_SCHEDULED = "scheduled"
DUMP_DONE = "done"
)
func main() {
if err := _main(); err != nil {
panic(err)
}
}
func _main() error {
config := getConfig()
if config.target2 != "" {
return merge(&config)
} else if config.archive != "" {
return archive(&config)
} else if config.dedupe {
return dedupe(&config)
} else {
if err := add(&config); err != nil {
return err
}
if err := edit(&config); err != nil {
return err
}
return dump(config)
}
}
func dedupe(config *config) error {
baseReader, err := filePathReader(config.target)
if err != nil {
return err
}
baseB, err := ioutil.ReadAll(baseReader)
if err != nil {
return err
}
var base pttodo.Root
if err := yaml.Unmarshal(baseB, &base); err != nil {
return err
}
do := func(todos []pttodo.Todo) []pttodo.Todo {
i := len(todos) - 1
for i >= 0 {
j := slices.IndexFunc(todos, func(todo pttodo.Todo) bool {
return todo.Equals(todos[i])
})
if j >= 0 && j != i {
todos[j] = todos[i]
base.Done = append(base.Done, todos[j])
todos = todos[:i]
}
i -= 1
}
return todos
}
base.Todo = do(base.Todo)
base.Scheduled = do(base.Scheduled)
if tmppath, err := marshalRootToTempFile(base); err != nil {
return err
} else if config.dryRun {
log.Println("===before===")
_dump(os.Stdout, []string{config.target}, config.tags, config.search, config.root)
log.Println("===after===")
_dump(os.Stdout, []string{tmppath}, config.tags, config.search, config.root)
} else if err := rename(tmppath, config.target); err != nil {
return err
}
return nil
}
func archive(config *config) error {
if config.archive == "" {
return nil
}
baseReader, err := filePathReader(config.target)
if err != nil {
return err
}
baseB, err := ioutil.ReadAll(baseReader)
if err != nil {
return err
}
archiveReader, err := filePathReader(config.archive)
if err != nil {
return err
}
archiveB, err := ioutil.ReadAll(archiveReader)
if err != nil {
return err
}
var base, archive pttodo.Root
if err := yaml.Unmarshal(baseB, &base); err != nil {
return err
}
if err := yaml.Unmarshal(archiveB, &archive); err != nil {
return err
}
archive.Done = append(archive.Done, base.Done...)
if tmppath2, err := marshalRootToTempFile(archive); err != nil {
return err
} else if config.dryRun {
} else if err := rename(tmppath2, config.archive); err != nil {
return err
}
base.Done = base.Done[:0]
if tmppath, err := marshalRootToTempFile(base); err != nil {
return err
} else if config.dryRun {
log.Println("===before===")
_dump(os.Stdout, []string{config.target}, config.tags, config.search, config.root)
log.Println("===after===")
_dump(os.Stdout, []string{tmppath}, config.tags, config.search, config.root)
} else if err := rename(tmppath, config.target); err != nil {
return err
}
return nil
}
func merge(config *config) error {
if config.target2 == "" {
return nil
}
baseReader, err := filePathReader(config.target)
if err != nil {
return err
}
baseB, err := ioutil.ReadAll(baseReader)
if err != nil {
return err
}
mergingReader, err := filePathReader(config.target2)
if err != nil {
return err
}
mergingB, err := ioutil.ReadAll(mergingReader)
if err != nil {
return err
}
var base, merging pttodo.Root
if err := yaml.Unmarshal(baseB, &base); err != nil {
return err
}
if err := yaml.Unmarshal(mergingB, &merging); err != nil {
return err
}
slurp := func(dst, src *[]pttodo.Todo) {
_src := *src
for i := range _src {
found := false
for j := range *dst {
found = found || _src[i].Equals((*dst)[j])
}
if !found {
*dst = append(*dst, _src[i])
}
}
}
slurp(&base.Todo, &merging.Todo)
slurp(&base.Scheduled, &merging.Scheduled)
slurp(&base.Done, &merging.Done)
if tmppath, err := marshalRootToTempFile(base); err != nil {
return err
} else if config.dryRun {
log.Println("===before===")
_dump(os.Stdout, []string{config.target}, config.tags, config.search, config.root)
log.Println("===after===")
_dump(os.Stdout, []string{tmppath}, config.tags, config.search, config.root)
} else if err := rename(tmppath, config.target); err != nil {
return err
}
merging.Done = append(merging.Done, merging.Todo...)
merging.Todo = merging.Todo[:0]
merging.Scheduled = append(merging.Done, merging.Scheduled...)
merging.Scheduled = merging.Scheduled[:0]
if tmppath2, err := marshalRootToTempFile(merging); err != nil {
return err
} else if config.dryRun {
} else if err := rename(tmppath2, config.target2); err != nil {
return err
}
return nil
}
func _merge(filepath string, mergeTargetFilePath string) error {
baseReader, err := filePathReader(filepath)
if err != nil {
return err
}
baseB, err := ioutil.ReadAll(baseReader)
if err != nil {
return err
}
mergingReader, err := filePathReader(mergeTargetFilePath)
if err != nil {
return err
}
mergingB, err := ioutil.ReadAll(mergingReader)
if err != nil {
return err
}
var base, merging pttodo.Root
if err := yaml.Unmarshal(baseB, &base); err != nil {
return err
}
if err := yaml.Unmarshal(mergingB, &merging); err != nil {
return err
}
base.MergeIn(merging)
tmppath, err := marshalRootToTempFile(base)
if err != nil {
return err
}
return rename(tmppath, filepath)
}
func rename(oldpath, newpath string) error {
b, err := os.ReadFile(oldpath)
if err != nil {
return err
}
if err := os.WriteFile(newpath, b, os.ModePerm); err != nil {
return err
}
return os.Remove(oldpath)
}
func marshalRootToTempFile(root pttodo.Root) (string, error) {
f, err := ioutil.TempFile(os.TempDir(), "tmp")
if err != nil {
return "", err
}
f.Close()
os.Remove(f.Name())
b, err := yaml.Marshal(root)
if err != nil {
return "", err
}
filepath := f.Name() + ".yaml"
err = ioutil.WriteFile(filepath, b, os.ModePerm)
return filepath, err
}
func filePathReader(path string) (io.Reader, error) {
if path == "-" {
return os.Stdin, nil
}
b, err := ioutil.ReadFile(path)
if os.IsNotExist(err) {
return bytes.NewReader([]byte("{}")), nil
}
if err != nil {
return nil, err
}
return bytes.NewReader(b), nil
}

View File

@@ -1,529 +0,0 @@
package main
import (
"bytes"
"flag"
"fmt"
"io"
"io/ioutil"
"local/pt-todo-server/pttodo"
"log"
"os"
"os/exec"
"path"
"sort"
"strings"
"syscall"
"time"
"gopkg.in/yaml.v2"
)
const (
DUMP_ALL = "all"
DUMP_TODO = "todo"
DUMP_SCHEDULED = "scheduled"
DUMP_DONE = "done"
)
type config struct {
targets []string
mergeme string
root string
tags []string
search string
edit bool
dry bool
}
func main() {
if err := _main(); err != nil {
panic(err)
}
}
func _main() error {
config := getConfig()
if config.mergeme != "" {
if err := merge(config.dry, config.targets[0], config.mergeme); err != nil {
return err
}
}
if config.edit {
if err := edit(config.dry, config.targets); err != nil {
return err
}
}
return dump(config.dry, os.Stdout, config.targets, config.tags, config.search, config.root)
}
func getConfig() config {
defaultFilepath, ok := os.LookupEnv("PTTODO_FILE")
if !ok {
defaultFilepath = "./todo.yaml"
}
var config config
var target string
flag.StringVar(&target, "f", defaultFilepath, "($PTTODO_FILE) path to yaml file or dir (starting with root then alphabetical for dir)")
flag.StringVar(&config.mergeme, "g", "", "path to yaml file to merge into -f (modifies f)")
flag.StringVar(&config.root, "root", DUMP_TODO, "path to pretty print ("+fmt.Sprint([]string{DUMP_ALL, DUMP_TODO, DUMP_SCHEDULED, DUMP_DONE})+")")
var tagss string
flag.StringVar(&tagss, "tags", "", "csv of all tags to find, -tag to invert")
flag.StringVar(&config.search, "search", "", "fts case insensitive")
flag.BoolVar(&config.edit, "e", false, "edit file")
flag.BoolVar(&config.dry, "dry", false, "dry run")
flag.Parse()
config.tags = strings.Split(tagss, ",")
config.targets = []string{target}
if stat, err := os.Stat(target); err == nil && stat.IsDir() {
config.targets, _ = listDir(target)
}
return config
}
func verifyRoot(root pttodo.Root) error {
f, err := ioutil.TempFile(os.TempDir(), "tmp")
if err != nil {
return err
}
f.Close()
tempFile := f.Name()
b, err := yaml.Marshal(root)
if err != nil {
return err
}
if err := ioutil.WriteFile(tempFile, b, os.ModePerm); err != nil {
return err
}
defer os.Remove(tempFile)
return verifyFile(tempFile)
}
func verifyFile(path string) error {
if err := dump(true, io.Discard, []string{path}, nil, "", DUMP_ALL); err != nil {
return fmt.Errorf("failed verifying file %s: %w", path, err)
}
return nil
}
func edit(dry bool, filepaths []string) error {
tempDir, err := ioutil.TempDir(os.TempDir(), "edit-pttodo-*")
if err != nil {
return err
}
originals := map[string]pttodo.Root{}
for _, target := range filepaths {
var original pttodo.Root
if b, err := ioutil.ReadFile(target); err != nil && !os.IsNotExist(err) {
return err
} else if err := yaml.Unmarshal(b, &original); err != nil {
return err
} else if c, err := yaml.Marshal(original.Todo); err != nil {
return err
} else if err := ioutil.WriteFile(path.Join(tempDir, path.Base(target)), c, os.ModePerm); err != nil {
return err
}
originals[target] = original
}
if err := vimd(tempDir); err != nil {
return err
}
for _, target := range filepaths {
for {
err := func() error {
var todos []pttodo.Todo
if b, err := ioutil.ReadFile(path.Join(tempDir, path.Base(target))); err != nil {
return err
} else if err := yaml.Unmarshal(b, &todos); err != nil {
return err
}
return nil
}()
if err == nil {
break
}
log.Printf("%s, press <Enter> to resume editing", err)
b := make([]byte, 1)
if _, err := os.Stdin.Read(b); err != nil {
return err
}
if err := vimd(tempDir); err != nil {
return err
}
}
}
if dry {
return nil
}
for _, target := range filepaths {
b, err := ioutil.ReadFile(path.Join(tempDir, path.Base(target)))
if err != nil {
return err
}
var newTodos []pttodo.Todo
if err := yaml.Unmarshal(b, &newTodos); err != nil {
return err
}
original := originals[target]
newTodoIDs := map[string]struct{}{}
for i := range newTodos {
newTodoIDs[newTodos[i].ID()] = struct{}{}
}
for i := range original.Todo {
if _, ok := newTodoIDs[original.Todo[i].ID()]; !ok {
original.Todo[i].TS = pttodo.TS(time.Now().Unix())
if string(original.Todo[i].Schedule) != "" && !original.Todo[i].Triggered() {
original.Scheduled = append(original.Scheduled, original.Todo[i])
} else {
original.Done = append(original.Done, original.Todo[i])
}
}
}
original.Todo = newTodos
original.AutoMove()
c, err := yaml.Marshal(original)
if err != nil {
return err
}
if err := ioutil.WriteFile(target, c, os.ModePerm); err != nil {
return err
}
}
return nil
}
func _edit(dry bool, filepaths []string) error {
tempDir, err := ioutil.TempDir(os.TempDir(), "edit-pttodo")
if err != nil {
return err
}
cpOne := func(filepath string) error {
f, err := os.Create(path.Join(tempDir, path.Base(filepath)))
if err != nil {
return err
}
if _, err := os.Stat(filepath); err == nil {
g, err := os.Open(filepath)
if err != nil {
return err
}
if _, err := io.Copy(f, g); err != nil {
return err
}
g.Close()
}
f.Close()
return nil
}
cp := func() error {
for _, filepath := range filepaths {
if err := cpOne(filepath); err != nil {
return err
}
}
return nil
}
vi := func() error {
return vimd(tempDir)
}
verifyOne := func(tempFile string) error {
for {
err := verifyFile(tempFile)
if err == nil {
break
}
log.Printf("%v, press <Enter> to resume editing", err)
b := make([]byte, 1)
if _, err := os.Stdin.Read(b); err != nil {
break
}
if err := vi(); err != nil {
return err
}
}
return verifyFile(tempFile)
}
verify := func() error {
for _, filepath := range filepaths {
tempFile := path.Join(tempDir, path.Base(filepath))
if err := verifyOne(tempFile); err != nil {
return err
}
}
return nil
}
saveOne := func(filepath string) error {
tempFile := path.Join(tempDir, path.Base(filepath))
var rootTemp, rootOld pttodo.Root
if a, err := ioutil.ReadFile(tempFile); err != nil {
return err
} else if err := yaml.Unmarshal(a, &rootTemp); err != nil {
return err
} else if b, err := ioutil.ReadFile(filepath); err != nil {
return err
} else if err := yaml.Unmarshal(b, &rootOld); err != nil {
return err
} else if rootTemp.Equals(rootOld) {
//log.Printf("no changes to %s", filepath)
return nil
}
if dry {
log.Printf("would've saved %s as %s", tempFile, filepath)
return nil
}
return os.Rename(tempFile, filepath)
}
save := func() error {
for _, filepath := range filepaths {
if err := saveOne(filepath); err != nil {
return err
}
}
return nil
}
for _, foo := range []func() error{cp, vi, verify, save} {
if err := foo(); err != nil {
return err
}
}
if !dry {
os.RemoveAll(tempDir)
}
return nil
}
func vimd(d string) error {
bin := "vim"
editorbin, err := exec.LookPath(bin)
if err != nil {
editorbin, err = exec.LookPath("vi")
}
if err != nil {
return err
}
args := []string{editorbin, "-p"}
files, err := listDir(d)
if err != nil {
return err
}
args = append(args, files...)
cpid, err := syscall.ForkExec(
editorbin,
args,
&syscall.ProcAttr{
Dir: d,
Env: os.Environ(),
Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()},
Sys: nil,
},
)
if err != nil {
return err
}
proc, err := os.FindProcess(cpid)
if err != nil {
return err
}
state, err := proc.Wait()
if err != nil {
return err
}
if exitCode := state.ExitCode(); exitCode != 0 {
return fmt.Errorf("bad exit code on vim: %d, state: %+v", exitCode, state)
}
return nil
}
func merge(dry bool, filepath string, mergeTargetFilePath string) error {
baseReader, err := filePathReader(filepath)
if err != nil {
return err
}
baseB, err := ioutil.ReadAll(baseReader)
if err != nil {
return err
}
mergingReader, err := filePathReader(mergeTargetFilePath)
if err != nil {
return err
}
mergingB, err := ioutil.ReadAll(mergingReader)
if err != nil {
return err
}
var base, merging pttodo.Root
if err := yaml.Unmarshal(baseB, &base); err != nil {
return err
}
if err := yaml.Unmarshal(mergingB, &merging); err != nil {
return err
}
base.MergeIn(merging)
if err := verifyRoot(base); err != nil {
return err
}
tmppath, err := marshalRootToTempFile(base)
if err != nil {
return err
}
if dry {
log.Printf("would've moved %s to %s when adding %s", tmppath, filepath, mergeTargetFilePath)
return nil
}
return os.Rename(tmppath, filepath)
}
func marshalRootToTempFile(root pttodo.Root) (string, error) {
f, err := ioutil.TempFile(os.TempDir(), "tmp")
if err != nil {
return "", err
}
f.Close()
os.Remove(f.Name())
b, err := yaml.Marshal(root)
if err != nil {
return "", err
}
filepath := f.Name() + ".yaml"
err = ioutil.WriteFile(filepath, b, os.ModePerm)
return filepath, err
}
func dump(dry bool, writer io.Writer, filepaths []string, tags []string, search, rootDisplay string) error {
var root pttodo.Root
for _, filepath := range filepaths {
reader, err := filePathReader(filepath)
if err != nil {
return err
}
b, err := ioutil.ReadAll(reader)
if err != nil {
return err
}
var root2, root2post pttodo.Root
if err := yaml.Unmarshal(b, &root2); err != nil {
return err
}
if err := yaml.Unmarshal(b, &root2post); err != nil {
return err
}
root2.MoveScheduledToTodo()
if !dry {
if !root2.Equals(root2post) {
log.Printf("refreshing %s", filepath)
b3, err := yaml.Marshal(root2)
if err != nil {
return err
}
if err := os.WriteFile(filepath, b3, os.ModePerm); err != nil {
return err
}
} else {
//log.Printf("not refreshing %s", filepath)
}
}
root.MergeIn(root2)
}
root.MoveScheduledToTodo()
var v interface{} = root
switch rootDisplay {
case DUMP_ALL:
case DUMP_TODO:
v = root.Todo
case DUMP_SCHEDULED:
v = root.Scheduled
case DUMP_DONE:
v = root.Done
}
if todos, ok := v.([]pttodo.Todo); ok {
if len(tags) > 0 {
result := make([]pttodo.Todo, 0, len(todos))
for _, todo := range todos {
skip := false
for _, tag := range tags {
positiveTag := strings.TrimLeft(tag, "-")
hasTag := strings.Contains(todo.Tags, positiveTag)
wantToHaveTag := !strings.HasPrefix(tag, "-")
skip = skip || !(hasTag == wantToHaveTag)
}
if !skip {
result = append(result, todo)
}
}
todos = result
}
if len(search) > 0 {
result := make([]pttodo.Todo, 0, len(todos))
for _, todo := range todos {
if strings.Contains(strings.ToLower(fmt.Sprint(todo)), strings.ToLower(search)) {
result = append(result, todo)
}
}
todos = result
}
v = todos
}
b2, err := yaml.Marshal(v)
if err != nil {
return err
}
fmt.Fprintf(writer, "%s\n", b2)
return nil
}
func filePathReader(path string) (io.Reader, error) {
var reader io.Reader
if path == "-" {
reader = os.Stdin
} else {
b, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
reader = bytes.NewReader(b)
}
return reader, nil
}
func listDir(dname string) ([]string, error) {
entries, err := os.ReadDir(dname)
if err != nil {
return nil, err
}
paths := make([]string, 0, len(entries))
for i := range entries {
if entries[i].IsDir() {
continue
}
paths = append(paths, path.Join(dname, entries[i].Name()))
}
sort.Slice(paths, func(i, j int) bool {
if path.Base(paths[i]) == "root.yaml" {
return true
}
return paths[i] < paths[j]
})
return paths, nil
}

View File

@@ -1,12 +0,0 @@
module pttodo-cli
go 1.17
require (
gopkg.in/yaml.v2 v2.4.0
local/pt-todo-server v0.0.0-00010101000000-000000000000
)
require github.com/robfig/cron/v3 v3.0.1 // indirect
replace local/pt-todo-server => ../../

View File

@@ -1,4 +0,0 @@
todo:
- "1"
scheduled: []
done: []

View File

@@ -1,4 +0,0 @@
todo:
- root
scheduled: []
done: []

View File

@@ -1,45 +0,0 @@
todo:
- todo: b
ts: Thu Mar 24 09:03:24 MDT 2022
scheduled:
- todo: due
schedule: "2022-05-01"
ts: Thu Mar 24 09:16:21 MDT 2022
- todo: loop
schedule: 10s
ts: Thu Mar 24 09:17:11 MDT 2022
done:
- todo: abc
schedule: "2022-05-01"
ts: Wed Mar 23 08:03:45 MDT 2022
- todo: def
schedule: "2022-05-01"
ts: Wed Mar 23 08:04:05 MDT 2022
- todo: other
ts: Wed Mar 23 09:44:57 MDT 2022
- todo: b
ts: Wed Mar 23 09:45:13 MDT 2022
- todo: a
ts: Wed Mar 23 09:45:15 MDT 2022
- todo: a
ts: Wed Mar 23 09:45:24 MDT 2022
- todo: a
ts: Wed Mar 23 09:45:31 MDT 2022
- todo: looper
schedule: 15s
ts: Thu Mar 24 09:01:41 MDT 2022
- todo: looper
schedule: 15s
ts: Thu Mar 24 09:04:51 MDT 2022
- todo: loop
schedule: 10s
ts: Thu Mar 24 09:06:59 MDT 2022
- todo: due
schedule: "2022-02-01"
ts: Thu Mar 24 09:15:42 MDT 2022
- todo: due
schedule: "2022-02-01"
ts: Thu Mar 24 09:16:53 MDT 2022
- todo: due
schedule: "2022-03-01"
ts: Thu Mar 24 09:17:07 MDT 2022

18
cmd/testdata/1.yaml vendored Executable file
View File

@@ -0,0 +1,18 @@
todo:
- "1"
- "2"
- "1"
- "2"
- "2"
scheduled:
- todo: "10"
- todo: "20"
- todo: "10"
- todo: "20"
- todo: "20"
done:
- todo: "100"
- todo: "200"
- todo: "100"
- todo: "200"
- todo: "200"

0
cmd/pttodo-cli/testdata/2.yaml → cmd/testdata/2.yaml vendored Normal file → Executable file
View File

0
cmd/pttodo-cli/testdata/3.yaml → cmd/testdata/3.yaml vendored Normal file → Executable file
View File

17
cmd/testdata/root.yaml vendored Executable file
View File

@@ -0,0 +1,17 @@
todo:
- root
- stub
scheduled:
- todo: abc
schedule: "2024-01-01"
ts: Thu Nov 9 07:49:59 MST 2023
- todo: "1"
schedule: "2024-01-02"
ts: Thu Nov 9 07:51:13 MST 2023
- todo: "1"
schedule: "2024-01-02"
ts: Thu Nov 9 07:51:28 MST 2023
- todo: "1"
schedule: "2024-01-02"
ts: Thu Nov 9 07:51:36 MST 2023
done: []

10
cmd/testdata/test.yaml vendored Executable file
View File

@@ -0,0 +1,10 @@
todo:
- hi20890
- hi
- b
- todo: loop
schedule: 10s
- todo: past
schedule: "2000-01-02"
scheduled: []
done: []

View File

@@ -1,21 +0,0 @@
#! /bin/bash
main() {
cd "$(dirname "$BASH_SOURCE")"
source ./from_todo_server_to_pttodo.sh
type from_todo_server_to_pttodo_main
list_lists | jq .list[] | jq -c . | while read -r line; do
local name="$(echo "$line" | jq -r .name | sed 's/^Today$/todo/g' | tr '[:upper:]' '[:lower:]' | sed 's/ /-/g')"
local id="$(echo "$line" | jq -r .id)"
echo $name, $id
TODO_SERVER_LIST="$id" from_todo_server_to_pttodo_main > ./$name.yaml
done
}
list_lists() {
todo_server_curl "$TODO_SERVER_URL/ajax.php?loadLists=&rnd=0.9900282499544026"
}
if [ "$0" == "$BASH_SOURCE" ]; then
main "$@"
fi

View File

@@ -1,55 +0,0 @@
#! /bin/bash
export TODO_SERVER_URL="${TODO_SERVER_URL:-"https://todo-server.remote.blapointe.com"}"
export TODO_SERVER_HEADERS="${TODO_SERVER_HEADERS:-"Cookie: BOAuthZ=$TODO_SERVER_BOAUTHZ"}"
export TODO_SERVER_LIST="${TODO_SERVER_LIST:-"2548023766"}"
main() {
from_todo_server_to_pttodo_main "$@"
}
from_todo_server_to_pttodo_main() {
set -e
set -o pipefail
local tasks_in_todo="$(fetch_tasks_in_list | format_tasks_in_list)"
local schedule_tasks_in_flight="$(COMPL=1 LOOPING=1 fetch_tasks_in_list | format_tasks_in_list)"
echo "{\"todo\": $tasks_in_todo, \"scheduled\": $schedule_tasks_in_flight, \"done\": []}" | yq -P eval -
}
format_tasks_in_list() {
jq -c .list[] | while read -r line; do
echo "$line" \
| jq '{
todo: .title,
details:.note,
ts: (if .compl == 1 then (.dateCompleted | strptime("%d %b %Y %I:%M %p") | mktime) else .dateEditedInt end),
subtasks: [],
tags: .tags,
schedule: (if (.cron != "") then (.cron) else (.loop) end)
}' \
| jq -c .
done | jq -sc | yq -P eval - | grep -v -E ' (""|\[]|0s)$' | yq -j eval - | jq -c .
}
fetch_tasks_in_list() {
todo_server_curl "$TODO_SERVER_URL/ajax.php?loadTasks=&list=$TODO_SERVER_LIST&compl=${COMPL:-0}&looping=${LOOPING:-0}"
}
todo_server_curl() {
local csv_headers="$TODO_SERVER_HEADERS"
local headers=()
while [ "$csv_headers" != "" ]; do
header="${csv_headers%%,*}"
headers+=("-H" "${header%%:*}: ${header#*:}")
if echo "$csv_headers" | grep -q ,; then
csv_headers="${csv_headers#*,}"
else
csv_headers=""
fi
done
curl -sS "${headers[@]}" "$@"
}
if [ "$0" == "$BASH_SOURCE" ]; then
main "$@"
fi

12
go.mod
View File

@@ -1,16 +1,8 @@
module pttodo module gitea.inhome.blapointe.com/gogs/pttodo
go 1.17 go 1.21
require ( require (
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0
) )
require (
github.com/bytbox/go-pop3 v0.0.0-20120201222208-3046caf0763e // indirect
github.com/emersion/go-imap v1.2.0 // indirect
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect
golang.org/x/text v0.3.7 // indirect
)

12
go.sum
View File

@@ -1,17 +1,5 @@
github.com/bytbox/go-pop3 v0.0.0-20120201222208-3046caf0763e h1:mQTN05gz0rDZSABqKMzAPMb5ATWcvvdMljRzEh0LjBo=
github.com/bytbox/go-pop3 v0.0.0-20120201222208-3046caf0763e/go.mod h1:alXX+s7a4cKaIprgjeEboqi4Tm7XR/HXEwUTxUV/ywU=
github.com/emersion/go-imap v1.2.0 h1:lyUQ3+EVM21/qbWE/4Ya5UG9r5+usDxlg4yfp3TgHFA=
github.com/emersion/go-imap v1.2.0/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=

View File

@@ -1,11 +1,58 @@
package pttodo package pttodo
import (
"bytes"
"os"
yaml "gopkg.in/yaml.v2"
)
type Root struct { type Root struct {
Todo []Todo Todo []Todo
Scheduled []Todo Scheduled []Todo
Done []Todo Done []Todo
} }
func NewRootFromFiles(p ...string) (Root, error) {
var result Root
for _, p := range p {
subroot, err := NewRootFromFile(p)
if err != nil {
return Root{}, err
}
result.MergeIn(subroot)
}
result.AutoMove()
return result, nil
}
func NewRootFromFile(p string) (Root, error) {
if b, err := os.ReadFile(p); err == nil && len(bytes.TrimSpace(b)) == 0 {
return Root{}, nil
}
f, err := os.Open(p)
if os.IsNotExist(err) {
return Root{}, nil
}
if err != nil {
return Root{}, err
}
defer f.Close()
var result Root
if err := yaml.NewDecoder(f).Decode(&result); err != nil {
todos, err2 := NewTodosFromFile(p)
if err2 != nil {
return Root{}, err
}
result.Todo = todos
}
result.AutoMove()
return result, nil
}
func (root Root) Equals(other Root) bool { func (root Root) Equals(other Root) bool {
for i, slice := range [][2][]Todo{ for i, slice := range [][2][]Todo{
[2][]Todo{root.Todo, other.Todo}, [2][]Todo{root.Todo, other.Todo},

View File

@@ -4,6 +4,8 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os"
"path"
"strconv" "strconv"
"strings" "strings"
"testing" "testing"
@@ -180,3 +182,78 @@ done:
t.Fatalf("want\n\t%q, got\n\t%q", want, string(got)) t.Fatalf("want\n\t%q, got\n\t%q", want, string(got))
} }
} }
func TestRootFromFile(t *testing.T) {
cases := map[string]struct {
given string
want Root
}{
"empty": {},
"happy": {
given: `{"todo":["a", "b"],"scheduled":["c"], "done":[{"todo": "d"}]}`,
want: Root{
Todo: []Todo{
Todo{Todo: "a"},
Todo{Todo: "b"},
},
Scheduled: []Todo{
Todo{Todo: "c"},
},
Done: []Todo{
Todo{Todo: "d"},
},
},
},
"todos": {
given: `["a", {"todo": "b"}]`,
want: Root{
Todo: []Todo{
{Todo: "a"},
{Todo: "b"},
},
},
},
}
for name, d := range cases {
c := d
t.Run(name, func(t *testing.T) {
d := t.TempDir()
p := path.Join(d, "input.yaml")
if err := os.WriteFile(p, []byte(c.given), os.ModePerm); err != nil {
t.Fatal(err)
}
got, err := NewRootFromFile(p)
if err != nil {
t.Fatal(err)
}
if fmt.Sprintf("%+v", got) != fmt.Sprintf("%+v", c.want) {
t.Errorf("want\n\t%+v, got\n\t%+v", c.want, got)
}
})
}
}
func TestRootFromFiles(t *testing.T) {
d := t.TempDir()
ps := []string{
path.Join(d, "a"),
path.Join(d, "b"),
}
os.WriteFile(ps[0], []byte(`["a"]`), os.ModePerm)
os.WriteFile(ps[1], []byte(`["b"]`), os.ModePerm)
got, err := NewRootFromFiles(ps...)
if err != nil {
t.Fatal(err)
}
want := Root{
Todo: []Todo{
{Todo: "a"},
{Todo: "b"},
},
}
if fmt.Sprintf("%+v", got) != fmt.Sprintf("%+v", want) {
t.Errorf("want\n\t%+v, got \n\t%+v", want, got)
}
}

View File

@@ -4,7 +4,10 @@ import (
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"hash/crc32" "hash/crc32"
"os"
"time" "time"
yaml "gopkg.in/yaml.v2"
) )
type Todo struct { type Todo struct {
@@ -17,6 +20,24 @@ type Todo struct {
writeTS bool writeTS bool
} }
func NewTodosFromFile(p string) ([]Todo, error) {
f, err := os.Open(p)
if os.IsNotExist(err) {
return nil, nil
}
if err != nil {
return nil, err
}
defer f.Close()
var result []Todo
if err := yaml.NewDecoder(f).Decode(&result); err != nil {
return nil, err
}
return result, nil
}
func (todo Todo) ID() string { func (todo Todo) ID() string {
hash := crc32.NewIEEE() hash := crc32.NewIEEE()
fmt.Fprintf(hash, "%d:%s", 0, todo.Todo) fmt.Fprintf(hash, "%d:%s", 0, todo.Todo)

36
pttodo/todos.go Normal file
View File

@@ -0,0 +1,36 @@
package pttodo
import "strings"
type Todos []Todo
func (todos Todos) LikeSearch(search string) Todos {
return todos.Like(func(todo Todo) bool {
return strings.Contains(
strings.ToLower(todo.Todo),
strings.ToLower(search),
)
})
}
func (todos Todos) LikeTags(tags []string) Todos {
return todos.Like(func(todo Todo) bool {
matches := true
for _, tag := range tags {
str := strings.TrimLeft(tag, "-")
want := !strings.HasPrefix(tag, "-")
matches = matches && strings.Contains(todo.Tags, str) == want
}
return matches
})
}
func (todos Todos) Like(like func(Todo) bool) Todos {
result := make(Todos, 0)
for i := range todos {
if like(todos[i]) {
result = append(result, todos[i])
}
}
return result
}

17
pttodo/todos_test.go Normal file
View File

@@ -0,0 +1,17 @@
package pttodo
import "testing"
func TestTodosLikeTags(t *testing.T) {
todos := Todos{
{Todo: "a", Tags: "x"},
{Todo: "b", Tags: "x,y"},
}
result := todos.LikeTags([]string{"x", "-y"})
if len(result) != 1 {
t.Error(result)
} else if result[0].Todo != "a" {
t.Error(result[0].Todo)
}
}

634
pttodoer/Cargo.lock generated Normal file
View File

@@ -0,0 +1,634 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "0.6.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
[[package]]
name = "anstyle-parse"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
dependencies = [
"anstyle",
"windows-sys",
]
[[package]]
name = "autocfg"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
[[package]]
name = "bumpalo"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "cc"
version = "1.0.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-targets",
]
[[package]]
name = "clap"
version = "4.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
[[package]]
name = "colorchoice"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
[[package]]
name = "core-foundation-sys"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
[[package]]
name = "croner"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "516aad5374ea0ea75a0f0f4512fb4e7ad46c5eeff9971cb8ebc8fd74f1cd16c1"
dependencies = [
"chrono",
]
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "fuchsia-cprng"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "iana-time-zone"
version = "0.1.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "indexmap"
version = "2.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
[[package]]
name = "itoa"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "js-sys"
version = "0.3.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.154"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
[[package]]
name = "log"
version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
[[package]]
name = "memchr"
version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "proc-macro2"
version = "1.0.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba"
dependencies = [
"unicode-ident",
]
[[package]]
name = "pttodoer"
version = "0.1.0"
dependencies = [
"chrono",
"clap",
"croner",
"regex",
"serde",
"serde_yaml",
"tempdir",
]
[[package]]
name = "quote"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293"
dependencies = [
"fuchsia-cprng",
"libc",
"rand_core 0.3.1",
"rdrand",
"winapi",
]
[[package]]
name = "rand_core"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
dependencies = [
"rand_core 0.4.2",
]
[[package]]
name = "rand_core"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
[[package]]
name = "rdrand"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
dependencies = [
"rand_core 0.3.1",
]
[[package]]
name = "regex"
version = "1.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
[[package]]
name = "remove_dir_all"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
dependencies = [
"winapi",
]
[[package]]
name = "ryu"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "serde"
version = "1.0.202"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.202"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c993ed8ccba56ae856363b1845da7266a7cb78e1d146c8a32d54b45a8b831fc9"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tempdir"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8"
dependencies = [
"rand",
"remove_dir_all",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "utf8parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "wasm-bindgen"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
[[package]]
name = "windows_i686_gnu"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
[[package]]
name = "windows_i686_msvc"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"

15
pttodoer/Cargo.toml Normal file
View File

@@ -0,0 +1,15 @@
[package]
name = "pttodoer"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
chrono = "0.4.38"
clap = { version = "4.4.8", features = ["derive"] }
croner = "2.0.4"
regex = "1.10.4"
serde = { version = "1.0.202", features = [ "serde_derive" ] }
serde_yaml = "0.9.34"
tempdir = "0.3.7"

1372
pttodoer/src/main.rs Normal file

File diff suppressed because it is too large Load Diff

4
pttodoer/src/testdata/.mvp.yaml.done vendored Normal file
View File

@@ -0,0 +1,4 @@
- todo: a b and c
schedule: '* * * * *'
ts: 123
- d e f

17
pttodoer/src/testdata/legacy.yaml vendored Normal file
View File

@@ -0,0 +1,17 @@
todo:
- a
- todo: b
schedule: 2000-01-01
scheduled:
- c
- todo: d
schedule: 2000-02-01
- todo: e
ts: Sun Dec 3 23:29:27 EST 2023
schedule: '0 0 1 1 *'
- todo: f
schedule: 2099-02-02
done:
- g
- todo: h
ts: Sun Dec 3 23:29:27 EST 2023

View File

@@ -0,0 +1 @@
plaintext

View File

@@ -0,0 +1,4 @@
todo: todo here
schedule: '* * * * *'
details: |
hello world

2
pttodoer/src/testdata/mvp.yaml vendored Normal file
View File

@@ -0,0 +1,2 @@
- x
- y and z

View File

@@ -0,0 +1,10 @@
- 1do: due
schedule: 2006-04-05
- 2do: scheduled
schedule: 2099-04-05
- 3do: repeating scheduled
schedule: 0 0 1 1 *
ts: 2001-02-03T04:05:06Z
- _done:
do: done
schedule: 2006-04-05

View File

@@ -0,0 +1 @@
- file

View File

@@ -0,0 +1 @@
- a

View File

@@ -0,0 +1 @@
- b

View File

@@ -0,0 +1,4 @@
- todo
- do: scheduled
schedule: 2099-01-01
- _done: any