Compare commits

...

48 Commits

Author SHA1 Message Date
Bel LaPointe 50913b8913 change default file path from stdin 2022-01-10 09:44:30 -05:00
Bel LaPointe 42d532ee61 update todo 2022-01-09 10:27:30 -05:00
Bel LaPointe 24316fe690 accept THH on ezdate 2022-01-09 10:24:40 -05:00
Bel LaPointe 8058304219 dont log noops 2022-01-09 10:22:23 -05:00
Bel LaPointe 7f24b3f337 do not save if no effective changes for edit, dump 2022-01-09 10:18:08 -05:00
Bel LaPointe fc12e0550d add root.equals and todo.equals 2022-01-09 10:17:44 -05:00
Bel LaPointe 05f5244cd1 root.yaml prio, otherwise sort -f results 2022-01-08 23:44:30 -05:00
Bel LaPointe c559c8eba6 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 dd7ac8d786 parseinlocation for non utc crud 2022-01-06 21:28:44 -05:00
Bel LaPointe ccdd2615c4 instead of trashing editing file in cli, write err msg and re-edit 2022-01-04 12:37:35 -05:00
Bel LaPointe 0241f2d76c err msg 2022-01-04 12:33:39 -05:00
Bel LaPointe fa812a16ee fix checking hastags and nothas tags 2022-01-04 08:56:19 -05:00
Bel LaPointe cf3b233a11 add anti tag search 2022-01-04 07:01:35 -05:00
Bel LaPointe 64868bdc0b create script to scrape list of lists and scrape aech into a yaml 2022-01-03 16:36:59 -05:00
Bel LaPointe 1b1724bea7 update todo with no more ts in todo section 2022-01-02 20:45:17 -05:00
Bel LaPointe f4968d1d1b update todo 2022-01-02 20:44:23 -05:00
Bel LaPointe 6203ced79b root does not marshal TSs on todos, always on schedules, dones. Test. 2022-01-02 20:44:04 -05:00
Bel LaPointe 4d171d10d0 only write TS on a todo if writeTS is set 2022-01-02 20:37:40 -05:00
Bel LaPointe 8ad2ca9345 new todo 2022-01-02 20:32:16 -05:00
Bel LaPointe bc0bb05fb6 update todo 2022-01-02 20:31:09 -05:00
Bel LaPointe 22dc04341c update todo.yaml 2022-01-02 20:17:32 -05:00
Bel LaPointe bf10c00708 add cli -g to merge in a file 2022-01-02 20:07:52 -05:00
Bel LaPointe cd5ce0f0df add root MergeIn 2022-01-01 17:43:20 -05:00
Bel LaPointe eb41f065a5 todo 2022-01-01 17:33:59 -05:00
Bel LaPointe 96bfb96ee3 consts for what to print, default to just the todos 2022-01-01 17:30:40 -05:00
Bel LaPointe 770f2719d2 ezdate for yyyy-mm-dd for schedule 2022-01-01 17:21:38 -05:00
Bel LaPointe 3554bf8ba4 rm test code 2022-01-01 17:14:03 -05:00
Bel LaPointe b4aa4ad310 ts shouldnt yield zero ever, yield now if so 2022-01-01 17:13:50 -05:00
Bel LaPointe 0dddf26265 fix getting ts for completed tasks 2022-01-01 17:04:21 -05:00
Bel LaPointe 5afdeed3b5 todo 2021-12-31 23:27:21 -05:00
Bel LaPointe af0f094a65 support tag, simple case insensitve search when recursing 2021-12-31 23:20:27 -05:00
Bel LaPointe 031db8788b tag saerch on todo 2021-12-31 23:14:31 -05:00
Bel LaPointe b8efdbfa52 when writing output file, dont recurse 2021-12-31 23:03:08 -05:00
Bel LaPointe a54ccae4c2 update todo 2021-12-31 23:00:16 -05:00
Bel LaPointe a27d5c38d7 times are now unix dates over ints 2021-12-31 22:59:13 -05:00
Bel LaPointe cb4886992a remove unused stub 2021-12-31 22:49:51 -05:00
Bel LaPointe 15c5f03ccf fix syntax highlight by using tempfile.yaml over tempfile 2021-12-31 22:46:49 -05:00
Bel LaPointe b82f11c248 dont try to find vimrc 2021-12-31 22:45:29 -05:00
Bel LaPointe 3c9b34202b use $EDITOR, default to vim, use $HOME/.vimrc if exists 2021-12-31 22:44:33 -05:00
Bel LaPointe 967a02c90a updated todo because i oofed 2021-12-31 22:37:47 -05:00
Bel LaPointe 3ed7d8cd9e update install for gomo 2021-12-31 22:32:47 -05:00
Bel LaPointe 492e0af993 if optional positional arg is todo/scheduled/done, then resolve first level 2021-12-31 22:32:07 -05:00
Bel LaPointe 10b95672e3 up install script for location 2021-12-31 22:28:12 -05:00
Bel LaPointe 183f39bd2a set ts to currenttime if should display 2021-12-31 22:27:48 -05:00
Bel LaPointe 24154df995 cli has dry mode and install script 2021-12-31 22:27:33 -05:00
Bel LaPointe f7dac79233 pttodo-cli default file via env 2021-12-31 22:07:16 -05:00
Bel LaPointe 8c45d4a7df pttodo-cli works even if file does not initially exist 2021-12-31 22:05:47 -05:00
Bel LaPointe 1e3f24b4d5 rename gomod, root project 2021-12-31 21:58:02 -05:00
22 changed files with 892 additions and 163 deletions

421
cmd/pttodo-cli/cli.go Normal file
View File

@ -0,0 +1,421 @@
package main
import (
"bytes"
"flag"
"fmt"
"io"
"io/ioutil"
"local/pt-todo-server/pttodo"
"log"
"os"
"os/exec"
"path"
"sort"
"strings"
"syscall"
"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 {
defaultFilepath, ok := os.LookupEnv("PTTODO_FILE")
if !ok {
defaultFilepath = "./todo.yaml"
}
filepath := flag.String("f", defaultFilepath, "($PTTODO_FILE) path to yaml file or dir (starting with root then alphabetical for dir)")
filepathToMergeIn := flag.String("g", "", "path to yaml file to merge into -f (modifies f)")
root := flag.String("root", DUMP_TODO, "path to pretty print ("+fmt.Sprint([]string{DUMP_ALL, DUMP_TODO, DUMP_SCHEDULED, DUMP_DONE})+")")
tags := flag.String("tags", "", "csv of all tags to find, -tag to invert")
search := flag.String("search", "", "fts case insensitive")
e := flag.Bool("e", false, "edit file")
dry := flag.Bool("dry", false, "dry run")
flag.Parse()
if *filepathToMergeIn != "" {
if err := merge(*dry, *filepath, *filepathToMergeIn); err != nil {
return err
}
}
filepaths := []string{*filepath}
if stat, err := os.Stat(*filepath); err != nil && !os.IsNotExist(err) {
return err
} else if err == nil && stat.IsDir() {
filepaths, err = listDir(*filepath)
if err != nil {
return err
}
}
if *e {
if err := edit(*dry, filepaths); err != nil {
return err
}
}
var tagslist []string
if *tags != "" {
tagslist = strings.Split(*tags, ",")
}
return dump(*dry, os.Stdout, filepaths, tagslist, *search, *root)
}
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
}
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 {
bin := "vim"
editorbin, err := exec.LookPath(bin)
if err != nil {
editorbin, err = exec.LookPath("vi")
}
if err != nil {
return err
}
args := []string{editorbin, "-p"}
tempfiles, err := listDir(tempDir)
if err != nil {
return err
}
args = append(args, tempfiles...)
cpid, err := syscall.ForkExec(
editorbin,
args,
&syscall.ProcAttr{
Dir: tempDir,
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
}
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 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

@ -0,0 +1,29 @@
package main
import (
"fmt"
"io/ioutil"
"os"
"path"
"strconv"
"testing"
)
func TestListDir(t *testing.T) {
d := t.TempDir()
want := []string{}
for i := 0; i < 3; i++ {
p := path.Join(d, strconv.Itoa(i))
ioutil.WriteFile(p, []byte{}, os.ModePerm)
want = append(want, p)
}
os.Mkdir(path.Join(d, "d"), os.ModePerm)
got, err := listDir(d)
if err != nil {
t.Fatal(err)
}
if fmt.Sprint(want) != fmt.Sprint(got) {
t.Fatal(want, got)
}
}

View File

@ -1,4 +1,4 @@
module pttodo
module pttodo-cli
go 1.17

20
cmd/pttodo-cli/install.sh Normal file
View File

@ -0,0 +1,20 @@
#! /bin/bash
cd "$(dirname "$BASH_SOURCE")"
binary_name="$(head -n 1 go.mod | awk '{print $NF}' | sed 's/.*\///')"
git_commit="$((
git rev-list -1 HEAD
if git diff | grep . > /dev/null; then
echo "-dirty"
fi
) 2> /dev/null | tr -d '\n')"
GOFLAGS="" \
GO111MODULE="" \
CGO_ENABLED=0 \
go build \
-o $GOPATH/bin/$binary_name \
-a \
-installsuffix cgo \
-ldflags "-s -w -X main.GitCommit=$git_commit"

4
cmd/pttodo-cli/testdata/1.yaml vendored Normal file
View File

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

4
cmd/pttodo-cli/testdata/2.yaml vendored Normal file
View File

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

4
cmd/pttodo-cli/testdata/3.yaml vendored Normal file
View File

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

4
cmd/pttodo-cli/testdata/root.yaml vendored Normal file
View File

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

View File

@ -1,134 +0,0 @@
package main
import (
"bytes"
"flag"
"fmt"
"io"
"io/ioutil"
"local/pt-todo-server/pttodo"
"os"
"os/exec"
"path"
"syscall"
"gopkg.in/yaml.v2"
)
func main() {
if err := _main(); err != nil {
panic(err)
}
}
func _main() error {
filepath := flag.String("f", "-", "path to yaml file")
e := flag.Bool("e", false, "edit file")
flag.Parse()
if *e {
if err := edit(*filepath); err != nil {
return err
}
}
return dump(os.Stdout, *filepath)
}
func edit(filepath string) error {
var tempFile string
cp := func() error {
f, err := ioutil.TempFile(os.TempDir(), path.Base(filepath))
if err != nil {
return err
}
g, err := os.Open(filepath)
if err != nil {
return err
}
if _, err := io.Copy(f, g); err != nil {
return err
}
g.Close()
f.Close()
tempFile = f.Name()
return nil
}
vi := func() error {
vibin, err := exec.LookPath("vi")
if err != nil {
return err
}
cpid, err := syscall.ForkExec(
vibin,
[]string{vibin, tempFile},
&syscall.ProcAttr{
Dir: "",
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
}
verify := func() error {
return dump(io.Discard, tempFile)
}
save := func() error {
return os.Rename(tempFile, filepath)
}
for _, foo := range []func() error{cp, vi, verify, save} {
if err := foo(); err != nil {
if tempFile != "" {
os.Remove(tempFile)
}
return err
}
}
return nil
}
func dump(writer io.Writer, filepath string) error {
var reader io.Reader
if filepath == "-" {
reader = os.Stdin
} else {
b, err := ioutil.ReadFile(filepath)
if err != nil {
return err
}
reader = bytes.NewReader(b)
}
b, err := ioutil.ReadAll(reader)
if err != nil {
return err
}
var root pttodo.Root
if err := yaml.Unmarshal(b, &root); err != nil {
return err
}
root.MoveScheduledToTodo()
b2, err := yaml.Marshal(root)
if err != nil {
return err
}
fmt.Fprintf(writer, "%s\n", b2)
return nil
}

View File

@ -0,0 +1,21 @@
#! /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

@ -0,0 +1,55 @@
#! /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

2
go.mod
View File

@ -1,4 +1,4 @@
module local/pt-todo-server
module pttodo
go 1.17

View File

@ -6,6 +6,20 @@ type Root struct {
Done []Todo
}
func (root Root) Equals(other Root) bool {
for i, slice := range [][2][]Todo{
[2][]Todo{root.Todo, other.Todo},
[2][]Todo{root.Scheduled, other.Scheduled},
[2][]Todo{root.Done, other.Done},
} {
_ = i
if !equalTodoSlices(slice[0], slice[1]) {
return false
}
}
return true
}
func (root *Root) MoveScheduledToTodo() {
for i := len(root.Scheduled) - 1; i >= 0; i-- {
if root.Scheduled[i].Triggered() {
@ -18,3 +32,35 @@ func (root *Root) MoveScheduledToTodo() {
}
}
}
func (root *Root) MergeIn(root2 Root) {
for _, listPair := range [][2]*[]Todo{
[2]*[]Todo{&root.Todo, &root2.Todo},
[2]*[]Todo{&root.Scheduled, &root2.Scheduled},
[2]*[]Todo{&root.Done, &root2.Done},
} {
for _, candidate := range *listPair[1] {
found := false
for i := range *listPair[0] {
found = found || ((*listPair[0])[i].Todo == candidate.Todo)
}
if !found {
*listPair[0] = append(*listPair[0], candidate)
}
}
}
}
func (root Root) MarshalYAML() (interface{}, error) {
for i := range root.Todo {
root.Todo[i].writeTS = false
}
for i := range root.Scheduled {
root.Scheduled[i].writeTS = true
}
for i := range root.Done {
root.Done[i].writeTS = true
}
type Alt Root
return (Alt)(root), nil
}

View File

@ -4,8 +4,11 @@ import (
"encoding/json"
"fmt"
"strconv"
"strings"
"testing"
"time"
yaml "gopkg.in/yaml.v2"
)
func TestJSONRoot(t *testing.T) {
@ -21,6 +24,8 @@ func TestJSONRoot(t *testing.T) {
t.Fatal(err)
} else if fmt.Sprint(root) != fmt.Sprint(root2) {
t.Fatal(root, root2)
} else if !root.Equals(root2) {
t.Fatal(root2)
}
}
@ -92,7 +97,83 @@ func TestRootMoveScheduledToTodo(t *testing.T) {
got.MoveScheduledToTodo()
if fmt.Sprintf("%+v", got) != fmt.Sprintf("%+v", want) {
t.Fatalf("want \n\t%+v, got \n\t%+v", want, got)
} else if !got.Equals(want) {
t.Fatalf("want \n\t%+v, got \n\t%+v", want, got)
}
})
}
}
func TestMergeRoots(t *testing.T) {
root0yaml := `
todo:
- a
- b
- todo: c
- todo: d
tags: a
- exclusive to 0
`
root1yaml := `
todo:
- a
- b
- todo: c
- todo: d
tags: b
- exclusive to 1
`
rootWantyaml := `
todo:
- a
- b
- todo: c
- todo: d
tags: a
- exclusive to 0
- exclusive to 1
`
var root0, root1, rootWant Root
if err := yaml.Unmarshal([]byte(root0yaml), &root0); err != nil {
t.Fatal(err)
}
if err := yaml.Unmarshal([]byte(root1yaml), &root1); err != nil {
t.Fatal(err)
}
if err := yaml.Unmarshal([]byte(rootWantyaml), &rootWant); err != nil {
t.Fatal(err)
}
root0.MergeIn(root1)
if fmt.Sprintf("%+v", root0) != fmt.Sprintf("%+v", rootWant) {
t.Fatalf("want \n\t%+v, got \n\t%+v", rootWant, root0)
} else if !root0.Equals(rootWant) {
t.Fatalf("want \n\t%+v, got \n\t%+v", rootWant, root0)
}
}
func TestRootMarshalYAMLWriteTS(t *testing.T) {
root := Root{
Todo: []Todo{Todo{Todo: "todo", TS: 1, writeTS: true}},
Scheduled: []Todo{Todo{Todo: "sched", TS: 2, writeTS: false, Schedule: "2099-01-01"}},
Done: []Todo{Todo{Todo: "done", TS: 3, writeTS: false}},
}
got, err := yaml.Marshal(root)
if err != nil {
t.Fatal(err)
}
if strings.TrimSpace(string(got)) != strings.TrimSpace(`
todo:
- todo
scheduled:
- todo: sched
schedule: "2099-01-01"
ts: Wed Dec 31 19:00:02 EST 1969
done:
- todo: done
ts: Wed Dec 31 19:00:03 EST 1969
`) {
t.Fatal(string(got))
}
}

View File

@ -37,6 +37,8 @@ func schedulerFactory(s string) scheduler {
} else if scheduleDuePattern.MatchString(s) {
n, _ := strconv.Atoi(s)
return scheduleDue(n)
} else if scheduleEZDatePattern.MatchString(s) {
return scheduleEZDate(s)
}
return scheduleCron(s)
}
@ -88,3 +90,15 @@ var scheduleDuePattern = regexp.MustCompile(`^[0-9]+$`)
func (due scheduleDue) next(time.Time) (time.Time, error) {
return TS(due).time(), nil
}
// 2022-01-01
type scheduleEZDate string
var scheduleEZDatePattern = regexp.MustCompile(`^[0-9]{4}-[0-9]{2}-[0-9]{2}(T[0-9]{2})?$`)
func (ezdate scheduleEZDate) next(time.Time) (time.Time, error) {
if len(ezdate) == len("20xx-xx-xxTxx") {
return time.ParseInLocation("2006-01-02T15", string(ezdate), time.Local)
}
return time.ParseInLocation("2006-01-02", string(ezdate), time.Local)
}

View File

@ -50,11 +50,18 @@ func TestJSONSchedule(t *testing.T) {
func TestSchedulerFactory(t *testing.T) {
start := time.Date(2000, 1, 1, 1, 1, 1, 1, time.UTC)
someDay := time.Date(2001, 2, 3, 0, 0, 0, 0, time.UTC)
someHour := time.Date(2001, 2, 3, 15, 0, 0, 0, time.UTC)
cases := map[string]struct {
input string
want interface{}
next time.Time
}{
"ezdate with hour": {
input: `2000-01-03T15`,
want: scheduleEZDate(`2000-01-03T15`),
next: someHour,
},
"long dur": {
input: `2h1m`,
want: scheduleDuration("2h1m"),
@ -70,11 +77,6 @@ func TestSchedulerFactory(t *testing.T) {
want: scheduleDue(1),
next: time.Unix(1, 0),
},
"zero ts": {
input: `0`,
want: scheduleDue(0),
next: time.Unix(0, 0),
},
"never": {
input: ``,
want: scheduleNever{},
@ -90,6 +92,11 @@ func TestSchedulerFactory(t *testing.T) {
want: scheduleCron(`5 * * * *`),
next: start.Add(time.Duration(-1*start.Nanosecond() + -1*start.Minute() + -1*start.Second())).Add(5 * time.Minute),
},
"ezdate": {
input: `2000-01-03`,
want: scheduleEZDate(`2000-01-03`),
next: someDay,
},
}
for name, d := range cases {
@ -108,4 +115,18 @@ func TestSchedulerFactory(t *testing.T) {
}
})
}
t.Run("zero ts", func(t *testing.T) {
got := schedulerFactory("0")
if fmt.Sprintf("%T", scheduleDue(0)) != fmt.Sprintf("%T", got) {
t.Fatalf("want type %T, got %T", scheduleDue(0), got)
}
if fmt.Sprint(scheduleDue(0)) != fmt.Sprint(got) {
t.Fatalf("want %+v, got %+v", scheduleDue(0), got)
}
next, _ := got.next(start)
if now := time.Now(); next.Sub(now) > time.Second {
t.Fatalf("want next %+v, got %+v", now, next)
}
})
}

View File

@ -8,10 +8,11 @@ import (
type Todo struct {
Todo string
Details string `yaml:",omitempty"`
TS TS `yaml:",omitempty"`
Schedule Schedule `yaml:",omitempty"`
Tags string `yaml:",omitempty"`
Subtasks []Todo `yaml:",omitempty"`
TS TS `yaml:",omitempty"`
writeTS bool
}
func (todo Todo) Triggered() bool {
@ -21,6 +22,11 @@ func (todo Todo) Triggered() bool {
}
func (todo Todo) MarshalYAML() (interface{}, error) {
if !todo.writeTS {
todo.TS = 0
} else {
todo.TS = TS(todo.TS.time().Unix())
}
if fmt.Sprint(todo) == fmt.Sprint(Todo{Todo: todo.Todo}) {
return todo.Todo, nil
}
@ -37,3 +43,37 @@ func (todo *Todo) UnmarshalYAML(unmarshal func(interface{}) error) error {
alt := (*Alt)(todo)
return unmarshal(alt)
}
func (todo Todo) Equals(other Todo) bool {
if !equalTodoSlices(todo.Subtasks, other.Subtasks) {
return false
}
if todo.TS != other.TS {
return false
}
if todo.Tags != other.Tags {
return false
}
if todo.Schedule != other.Schedule {
return false
}
if todo.Details != other.Details {
return false
}
if todo.Todo != other.Todo {
return false
}
return true
}
func equalTodoSlices(a, b []Todo) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if !a[i].Equals(b[i]) {
return false
}
}
return true
}

View File

@ -2,12 +2,18 @@ package pttodo
import (
"encoding/json"
"errors"
"time"
yaml "gopkg.in/yaml.v2"
)
type TS int64
func (ts TS) time() time.Time {
if ts == 0 {
ts = TS(time.Now().Unix())
}
return time.Unix(int64(ts), 0)
}
@ -23,9 +29,24 @@ func (ts TS) MarshalYAML() (interface{}, error) {
if ts == 0 {
ts = TS(time.Now().Unix())
}
return int64(ts), nil
return ts.time().Format(time.UnixDate), nil
}
func (ts *TS) UnmarshalJSON(b []byte) error {
return json.Unmarshal(b, (*int64)(ts))
return yaml.Unmarshal(b, ts)
}
func (ts *TS) UnmarshalYAML(unmarshaller func(interface{}) error) error {
var n int64
if err := unmarshaller(&n); err == nil {
*ts = TS(n)
return nil
}
var s string
if err := unmarshaller(&s); err == nil {
t, err := time.ParseInLocation(time.UnixDate, s, time.Local)
*ts = TS(t.Unix())
return err
}
return errors.New("illegal TS")
}

View File

@ -2,12 +2,41 @@ package pttodo
import (
"encoding/json"
"strings"
"testing"
"time"
yaml "gopkg.in/yaml.v2"
)
func TestTSMarshalYaml(t *testing.T) {
t.Run("nonzero", func(t *testing.T) {
var ts TS
if b, err := yaml.Marshal(TS(5)); err != nil {
t.Fatal(err)
} else if s := string(b); !strings.HasSuffix(strings.TrimSpace(s), ` 1969`) {
t.Fatal(s)
} else if err := yaml.Unmarshal(b, &ts); err != nil {
t.Fatal(err)
} else if ts != 5 {
t.Fatal(ts)
}
})
t.Run("zero", func(t *testing.T) {
var ts TS
if b, err := yaml.Marshal(TS(0)); err != nil {
t.Fatal(err)
} else if s := string(b); strings.TrimSpace(s) == `0` {
t.Fatal(s)
} else if err := yaml.Unmarshal(b, &ts); err != nil {
t.Fatal(err)
} else if ts == 0 {
t.Fatal(ts)
}
})
}
func TestJSONTS(t *testing.T) {
ts := TS(time.Now().Unix())
ts := TS(1234567890)
js, err := json.Marshal(ts)
if err != nil {
t.Fatal(err)
@ -18,7 +47,7 @@ func TestJSONTS(t *testing.T) {
t.Fatal(err)
}
if ts != ts2 {
t.Fatal(ts2)
t.Fatalf("want: %v, got: %v", ts, ts2)
}
if err := json.Unmarshal([]byte(`123`), &ts2); err != nil {

View File

View File

@ -1,9 +1,11 @@
todo:
- todo: when to run scheduled modifier? like, syncthing could have conflicts if I modify only file on remote
details: |
- if it's a web ui hook or somethin, then it'd only have file conflict if I modify without waiting
- but thats a step back from current todo solution
- when merging, check for complete/incomplete and cross reference todo vs scheduled
vs done
- click on links when dumped via cli
scheduled: []
done:
- xactions emails extend ledger,todo-server with pttodo - schedule merge inbox style
- xactions emails extend ledger,todo-server with pttodo
- todo: ez edit on many platforms, even offline and mobile
details: |
mobile view + complete method
@ -12,22 +14,69 @@ todo:
web server access to ops
is a web ui for mobile best solution?
let git be smart-ish and keep latest? would provide versioning OOTB without touching raw
done:
- todo: when to run scheduled modifier? like, syncthing could have conflicts if I
modify only file on remote
details: |
- if it's a web ui hook or somethin, then it'd only have file conflict if I modify without waiting
- but thats a step back from current todo solution
tags: stuffToTry,secondTag
- what do about todo-now vs todo vs watch vs later... could do many files and manage
outside binary, but search would suck. Good would be vendor lockin, and that's UI
problem
- merge multi todo files for 'inbox' style with some dedupe (probably best to just
do this via calling pttodo-cli from script or somethin) (probably at an off hour
so no collisions from live editing)
- how to defer a scheduled task?
- todo: add tag anti-search, like -tag=-notMe
ts: Tue Jan 4 06:56:03 EST 2022
- todo: if in todo, then omit ts field
ts: Sun Jan 2 20:44:27 EST 2022
- todo: merge full files to import from todo-server
ts: Sun Jan 2 20:44:27 EST 2022
- todo: more schedule formats, like just 2022-01-15, for deferring
ts: Sun Jan 2 20:44:27 EST 2022
- todo: from todo-server to pttodo format
ts: Sun Jan 2 20:44:27 EST 2022
- todo: add -tag to search via cli
ts: Sun Jan 2 20:44:27 EST 2022
- todo: ts to human readable
ts: Sun Jan 2 20:44:27 EST 2022
- todo: here is my really here is my really here is my really here is my really here
is my really here is my really here is my really here is my really here is my
really here is my really here is my really here is my really here is my really
here is my really here is my really long string
ts: Sun Jan 2 20:44:27 EST 2022
- todo: vim doesnt source vimrc, so stuff like tab width and tab characters, also
syntax highlight :(
ts: Sun Jan 2 20:44:27 EST 2022
- todo: crontab -e style editing to ensure good syntax
ts: Sun Jan 2 20:44:27 EST 2022
- todo: YAML based todo
details:
because goddamnit a year of this shit
isn't significant on disk or in RAM for vim
details: because goddamnit a year of this shit isn't significant on disk or in RAM
for vim
ts: Fri Dec 31 22:33:12 EST 2021
- todo: yaml based todo for plaintext
details: a year isnt even a mb
- ez edit, start on many platforms
- defer
- schedule
- looping
- details
ts: Fri Dec 31 22:33:12 EST 2021
- todo: ez edit, start on many platforms
ts: Sun Jan 2 20:44:27 EST 2022
- todo: defer
ts: Sun Jan 2 20:44:27 EST 2022
- todo: schedule
ts: Sun Jan 2 20:44:27 EST 2022
- todo: looping
ts: Sun Jan 2 20:44:27 EST 2022
- todo: details
ts: Sun Jan 2 20:44:27 EST 2022
- todo: let UI be UI for whatever platform
- tags
ts: Sun Jan 2 20:44:27 EST 2022
- todo: tags
ts: Sun Jan 2 20:44:27 EST 2022
- todo: sub tasks
subtasks:
- a
ts: Fri Dec 31 22:33:12 EST 2021
- todo: crap losing on a bad edit hurts
details: |
?
ts: Fri Dec 31 22:37:58 EST 2021