Compare commits

...

9 Commits
v1.0 ... master

8 changed files with 289 additions and 99 deletions

View File

@ -2,9 +2,9 @@
![Markdown Bullet Journal Logo](https://github.com/dballard/markdown-bullet-journal/raw/master/Markdown-Bullet-Journal.png "Markdown Bullet Journal Logo")
Markdown Bullet Journal is a digital adaptation of [analog tech](http://bulletjournal.com/). For my personal productivity I found having a full markdown todo list file with daily migrations was the most optimal was to manage my time. I added in a utility to summarize my past work as the daily migrations made that hard to track.
Markdown Bullet Journal is a digital adaptation of [analog tech](http://bulletjournal.com/). When using analog pen + paper, bullet journal migrations are expensive in space and time, however in digital form they are cheap. I found that for my personal productivity having a full markdown todo list file with daily migrations was the most optimal way to manage my time and a digital bullet journal enabled this. I added in a utility to summarize my past work as the daily migrations intentionally removed it. I have also extended this tool to my tastes adding in custom support for repetitive daily tasks and pomodoros.
These are a simple set of utilities that work for me. Nothing fancy
These are a simple set of utilities that work for me. Nothing fancy.
## Usage
@ -15,7 +15,7 @@ Download:
- [mdbj-migreate.exe](https://www.danballard.com/resources/mdbj/mdbj-summary.exe)
- [mdbj-summary.exe](https://www.danballard.com/resources/mdbj/mdbj-migrate.exe)
And place them in a directory. Run `mdbj-migrate` to generate a template to work from and each day after to 'migrate'. Run `mdbj-summary` to generate summary.txt to review work done.
And place them in a directory you want to use. Run `mdbj-migrate` to generate a template to work from in that directory and each day after to 'migrate' to create the new day's file in that directory. Run `mdbj-summary` to generate summary.txt in the same directory to review work done.
### Linux & Mac
@ -26,7 +26,7 @@ go install github.com/dballard/markdown-bullet-journal/tree/master/mdbj-migrate
go install github.com/dballard/markdown-bullet-journal/tree/master/mdbj-summary
```
Pick a directory you want to use and run `mdbj-migreate` to generate a template to work from. Run it on successive days to 'migrate'. Run `mdbj-summary` to print a summary of done work to the console.
Pick a directory you want to use and run `mdbj-migreate` to generate a template to work from in that directory. Run `mdbj-migrate` on succesive days and it will find the last dated file in the directory and 'migrate' it. Run `mdbj-summary` in the directory to print a summary of all done work to the console.
### Recommendations
@ -36,7 +36,7 @@ My mdbj directoy is in a cloud backed up location so I can also slightly awkward
### mdbj-migrate
When run in a directory, takes the last dated .md file, copies it to a new file with today's date, and dropes all lines marked completed (with a '[x]').
When run in a directory, takes the last dated .md file, copies it to a new file with today's date, and drops all lines marked completed (with a '[x]').
### mdbj-summary
@ -56,9 +56,9 @@ into
The basics of headers with '#'
Nested lists with '-' and indentation
Nested lists with '-' for notes and indentation
Todo and done with '[ ]' and '[x]'
Todo and done with '[ ]' for open todo item, '[x]' for done todo item, and '[-]' for dropped todo item
Obviously you can use other markdown features such as **bold**, *italics* and [Links](https://guides.github.com/features/mastering-markdown/) but none of these trigger any special treatment with regards to Markdown Bullet Journal.
@ -77,10 +77,53 @@ These are tasks you might want to do a subset of on any given day, and possibly
- [x] 1x5 - minutes of meditation
```
Will get output as:
under `summary` will show as
```
- 40 pushups
- 5 minutes of meditation
```
And then on migration the '4' and '1' will get reset to 0 and the tasks will not get dropped
And then on `migrate` the '4' and '1' will get reset to 0 and the tasks will not get dropped
```
- [ ] 0x10 - Pushups
- [ ] 0x10 - Crunches
- [ ] 0x10 - Lunges
- [ ] 0x5 - minutes of meditation
```
#### Pomodoro ####
If you want to track pomodoro sessions, simply add '.'s inside the square brackets of todo items. They will not be considered done until an 'x' is included and thus will migrate to clean items the next day. They will however count towards pomodoro summaries.
```
- [..] Big Task
- [x] Part A
- [x] Part B
- [x] Part C
- [ ] Part D
- [ ] Other Task
- [..x] Thing 1
- [ ] Thing 2
```
will `migrate` to
```
- [ ] Big Task
- [ ] Part D
- [ ] Other Task
- [ ] Thing 2
```
and `summary` will be
```
Big Task / Part A
Big Task / Part B
Big Task / Part C
Other Task / Thing 1
4 / 8 - 4 Pomodoros
```

View File

@ -13,12 +13,16 @@
- [ ] nesting 3
- [ ] nesting 4
- [x] nesting 5
- [x] not nested
- notes of done thing
- notes of note done thing
- [x] not nested with done notes
-
asdasd
- tabbing
- [x] tabs handled
- [ ] tabs migrated
- [x] tabs handled
- [ ] tabs migrated
- [-] dropping this task but it's not done
- notes
# Nothing done
@ -32,3 +36,13 @@
- [ ] Group
- [ ] 0x3 nesting rep
- [x] 2x6 done nested rep
# Pomodoros
- [ ] not done
- [..] partly done
- [X...] completed
- [x] completed sub task
- notes
- more notes
- [x] other done task

View File

@ -1,13 +1,13 @@
package main
import (
"os"
"time"
"log"
"github.com/dballard/markdown-bullet-journal/process"
"fmt"
"github.com/dballard/markdown-bullet-journal/process"
"log"
"os"
"strconv"
"strings"
"time"
)
const template = `# Work
@ -30,6 +30,7 @@ const template = `# Work
type processHandler struct {
File *os.File
flagStack []process.Flags
}
func (ph *processHandler) Writeln(line string) {
@ -37,14 +38,35 @@ func (ph *processHandler) Writeln(line string) {
}
// NOP
func (ph *processHandler) Eof() {}
func (ph *processHandler) NewFile() {}
func (ph *processHandler) Eof() {}
func (ph *processHandler) NewFile() {
ph.flagStack = []process.Flags{}
}
func (ph *processHandler) ProcessLine(line string, indentLevel int, stack []string, todo bool, done bool, repTask process.RepTask) {
// TODO: handle [x] numXnum
if !done || repTask.Is {
if repTask.Is {
ph.Writeln(strings.Repeat("\t", indentLevel) + "- [ ] 0x" + strconv.Itoa(repTask.B) + stack[len(stack)-1] )
func (ph *processHandler) ProcessLine(line string, indentLevel int, indentString string, headerStack []string, lineStack []string, flags process.Flags) {
if indentLevel+1 > len(ph.flagStack) {
ph.flagStack = append(ph.flagStack, flags)
} else {
ph.flagStack[indentLevel] = flags
}
print := true
if !flags.RepTask.Is { // always print repTasks
for i, iflags := range ph.flagStack {
if i > indentLevel {
break
}
if iflags.Done || iflags.Dropped {
print = false
}
}
}
if print {
if flags.RepTask.Is {
ph.Writeln(strings.Repeat(indentString, indentLevel) + "- [ ] 0x" + strconv.Itoa(flags.RepTask.B) + " " + lineStack[len(lineStack)-1])
} else if flags.Todo {
ph.Writeln(strings.Repeat(indentString, indentLevel) + "- [ ] " + lineStack[len(lineStack)-1])
} else {
ph.Writeln(line)
}
@ -52,12 +74,18 @@ func (ph *processHandler) ProcessLine(line string, indentLevel int, stack []stri
}
func main() {
if len(os.Args) > 1 {
fmt.Println(os.Args)
fmt.Println("Markdown Bullet Journal version: " + process.Version)
return
}
ph := new(processHandler)
files := process.GetFiles()
fileName := time.Now().Format("2006-01-02") + ".md"
if _, err := os.Stat(fileName); os.IsNotExist(err) {
if _, err := os.Stat(fileName); os.IsNotExist(err) {
ph.File, err = os.Create(fileName)
if err != nil {
log.Fatal("Cannot open: ", fileName, " > ", err)
@ -73,7 +101,7 @@ func main() {
ph.File.WriteString(template)
} else {
lastFile := files[len(files)-1]
fmt.Println ("Migrating " + lastFile + " to " + fileName)
fmt.Println("Migrating " + lastFile + " to " + fileName)
process.ProcessFile(ph, lastFile)
}
}

View File

@ -22,10 +22,10 @@ const EXPECTED = `# Work
- [ ] nesting 2
- [ ] nesting 3
- [ ] nesting 4
-
asdasd
- notes of note done thing
- tabbing
- [ ] tabs migrated
- [ ] tabs migrated
- notes
# Nothing done
@ -37,8 +37,13 @@ const EXPECTED = `# Work
- [ ] 0x5 things
- [ ] 0x2 other things
- [ ] Group
- [ ] 0x3 nesting rep
- [ ] 0x6 done nested rep
- [ ] 0x3 nesting rep
- [ ] 0x6 done nested rep
# Pomodoros
- [ ] not done
- [ ] partly done
`
func TestMigrate(t *testing.T) {
@ -71,7 +76,6 @@ func TestMigrate(t *testing.T) {
line := strings.Count(string(result[:diffLoc]), "\n")
errorStr := string(result[int(math.Max(0, float64(diffLoc - 10))) : int(math.Min(float64(len(result)), float64(diffLoc + 10))) ])
t.Errorf("Summary results do not match expected:\nfirst difference at line %v: '%v'\n%v<---->\n%v\n", line, errorStr, string(result), EXPECTED)
t.Errorf("Summary results do not match expected:\nfirst difference at line %v\nexpected char: '%c'\nactual char: '%v'\nline: '%v'\nACTUAL:\n%v<---->\nEXPECTED:\n%v\n", line, EXPECTED[diffLoc], string(result[diffLoc]), errorStr, string(result), EXPECTED)
}
}

View File

@ -22,7 +22,29 @@
- [ ] not done
- note
# Nested Header
## With Nothing
- note
- [ ] undone
## With something done
- more notes
- [ ] a partly done thing
- [ ] more to do
- [x] the one done thing
# Repetition
- [x] 5x5 things
- [ ] 0x2 other things
- [ ] 0x2 other things
- category
- [x] 2x10 nested rep
# Pomodoros
- [ ] not done
- [..] partly done
- [x...] completed

View File

@ -1,19 +1,24 @@
package main
import (
"runtime"
"os"
"github.com/dballard/markdown-bullet-journal/process"
"log"
"os"
"runtime"
"strconv"
"strings"
"fmt"
)
type header struct {
text string
printed bool
}
type processHandler struct {
File *os.File
totalCount, doneCount int
header string
headerPrinted bool
File *os.File
totalCount, doneCount, pomodoroCount int
headers []header
}
func (ph *processHandler) Writeln(line string) {
@ -23,47 +28,68 @@ func (ph *processHandler) Writeln(line string) {
func (ph *processHandler) NewFile() {
ph.totalCount = 0
ph.doneCount = 0
ph.header = ""
ph.headerPrinted = false
ph.pomodoroCount = 0
ph.headers = []header{}
}
func (ph *processHandler) Eof() {
ph.Writeln(strconv.Itoa(ph.doneCount) + " / " + strconv.Itoa(ph.totalCount))
pomodoroStr := ""
if ph.pomodoroCount > 0 {
pomodoroStr = " - " + strconv.Itoa(ph.pomodoroCount) + " Pomodoros"
}
ph.Writeln(strconv.Itoa(ph.doneCount) + " / " + strconv.Itoa(ph.totalCount) + pomodoroStr)
}
func (ph *processHandler) ProcessLine(line string, indentLevel int, stack []string, todo bool, done bool, repTask process.RepTask) {
func (ph *processHandler) handleHeaderPrint() {
for i, header := range ph.headers {
if !header.printed {
ph.Writeln("\t" + strings.Repeat("#", i+1) + " " + header.text)
ph.headers[i].printed = true
}
}
}
func (ph *processHandler) ProcessLine(line string, indentLevel int, indentString string, headerStack []string, lineStack []string, flags process.Flags) {
if strings.Trim(line, " \t\n\r") == "" {
return
}
if line[0] == '#' {
ph.header = line[2:]
ph.headerPrinted = false;
return
last := headerStack[len(headerStack)-1]
if len(headerStack) > len(ph.headers) {
ph.headers = append(ph.headers, header{last, false})
} else if len(headerStack) == len(ph.headers) {
ph.headers[len(ph.headers)-1] = header{last, false}
} else if len(headerStack) < len(ph.headers) {
ph.headers = ph.headers[:len(headerStack)]
ph.headers[len(ph.headers)-1] = header{last, false}
}
}
// inc count of todo items (rep tasks shouldnt count towards outstanding todo, unless done)
if todo && !repTask.Is {
if flags.Todo && !flags.RepTask.Is {
ph.totalCount += 1
}
if done {
if !ph.headerPrinted {
ph.Writeln("\t# " + ph.header)
ph.headerPrinted = true
}
if flags.Done {
ph.handleHeaderPrint()
ph.doneCount += 1
repStr := ""
if repTask.Is {
repStr = strconv.Itoa( repTask.A * repTask.B)
if flags.RepTask.Is {
repStr = strconv.Itoa(flags.RepTask.A*flags.RepTask.B) + " "
// inc todo count here since we did a thing, its done, and we dont want a higher done count than total
ph.totalCount += 1
}
ph.Writeln("\t\t" + repStr + strings.Join(stack, " / "))
ph.Writeln("\t\t" + repStr + strings.Join(lineStack, " / "))
}
ph.pomodoroCount += flags.Pomodoros
}
func main() {
if len(os.Args) > 1 {
fmt.Println("Markdown Bullet Journal version: " + process.Version)
return
}
ph := new(processHandler)
if runtime.GOOS == "windows" {

View File

@ -16,9 +16,15 @@ const EXPECTED = `
# Test Data
nesting1 / nesting 2 / nesting 3 / nesting 4 / nesting 5
not nested
# Nested Header
## With something done
a partly done thing / the one done thing
# Repetition
25 things
4 / 11
20 category / nested rep
# Pomodoros
completed
7 / 19 - 5 Pomodoros
`
func TestSummary(t *testing.T) {
@ -54,6 +60,6 @@ func TestSummary(t *testing.T) {
line := strings.Count(string(result[:diffLoc]), "\n")
errorStr := string(result[int(math.Max(0, float64(diffLoc - 10))) : int(math.Min(float64(len(result)), float64(diffLoc + 10))) ])
t.Errorf("Summary results do not match expected:\nfirst difference at line %v: '%v'\n%v<---->\n%v\n", line, errorStr, string(result), EXPECTED)
t.Errorf("Summary results do not match expected:\nfirst difference at line %v\nexpected char: '%c'\nactual char: '%v'\nline: '%v'\nACTUAL:\n%v<---->\nEXPECTED:\n%v\n", line, EXPECTED[diffLoc], string(result[diffLoc]), errorStr, string(result), EXPECTED)
}
}
}

View File

@ -1,23 +1,42 @@
package process
import (
"os"
"log"
"bufio"
"regexp"
"io/ioutil"
"log"
"os"
"regexp"
"strconv"
"strings"
)
const (
Version = "1.1.1"
)
var (
todoTaskExp = regexp.MustCompile("^\\[([ \\.xX-]*)\\]")
startSpaces = regexp.MustCompile("^[\t ]*")
repTaskRegExp = regexp.MustCompile("^([0-9]*)[xX]([0-9]*)")
headerExp = regexp.MustCompile("^(#+) *(.+)")
)
type RepTask struct {
Is bool
Is bool
A, B int
}
type ProcessHandler interface {
type Flags struct {
Todo bool
Done bool
Dropped bool
RepTask RepTask
Pomodoros int
}
type ProcessHandler interface {
Writeln(line string)
ProcessLine(line string, indentLevel int, stack []string, todo bool, done bool, repTask RepTask)
ProcessLine(line string, indentLevel int, indentString string, headerStack []string, lineStack []string, flags Flags)
Eof()
NewFile()
}
@ -40,6 +59,14 @@ func GetFiles() (filteredFiles []string) {
return
}
func max(x, y int) int {
if x >= y {
return x
} else {
return y
}
}
func ProcessFile(ph ProcessHandler, fileName string) {
file, err := os.Open(fileName)
if err != nil {
@ -48,11 +75,12 @@ func ProcessFile(ph ProcessHandler, fileName string) {
defer file.Close()
ph.NewFile()
stack := make([]string, 0)
headerStack := make([]string, 1)
lineStack := make([]string, 0)
//flags := Flags{}
scanner := bufio.NewScanner(file)
indentPattern := ""
startSpaces := regexp.MustCompile("^[\t ]*")
indentLevel := 0
for scanner.Scan() {
line := scanner.Text()
@ -71,54 +99,73 @@ func ProcessFile(ph ProcessHandler, fileName string) {
} else {
indentLevel = strings.Count(startSpaces.FindString(line), indentPattern)
}
todo := false
done := false
var repTask RepTask
if indentLevel < len(stack)-1 {
stack = stack[: indentLevel+1]
}
if indentLevel == len(stack)-1 {
stack[len(stack)-1], todo, done, repTask = getText(line, indentLevel, indentPattern)
}
if indentLevel >= len(stack) {
row := ""
row, todo, done, repTask = getText(line, indentLevel, indentPattern)
stack = append(stack, row)
if headerExp.MatchString(line) {
matches := headerExp.FindStringSubmatch(line)
if len(matches[1]) > len(headerStack) {
headerStack = append(headerStack, matches[2])
} else if len(matches[1]) == len(headerStack) {
headerStack[len(headerStack)-1] = matches[2]
} else if len(matches[1]) < len(headerStack) {
headerStack = headerStack[:len(matches[1])]
headerStack[len(headerStack)-1] = matches[2]
}
}
ph.ProcessLine(line, indentLevel, stack, todo, done, repTask)
var flags Flags
if indentLevel < len(lineStack)-1 {
lineStack = lineStack[:indentLevel+1]
}
if indentLevel == len(lineStack)-1 {
lineStack[len(lineStack)-1], flags = getText(line, indentLevel, indentPattern)
}
if indentLevel >= len(lineStack) {
row := ""
row, flags = getText(line, indentLevel, indentPattern)
lineStack = append(lineStack, row)
}
ph.ProcessLine(line, indentLevel, indentPattern, headerStack, lineStack, flags)
}
ph.Eof()
}
func getText(str string, indentLevel int, indentPattern string) (text string, todo bool, done bool, repTask RepTask) {
func getText(str string, indentLevel int, indentPattern string) (text string, flags Flags) {
//fmt.Printf("indentLevel: %v str: '%s'\n", indentLevel, str )
if len(str) < (indentLevel*4 +2) {
return "", false, false, RepTask{false, 0, 0}
flags.Done = false
flags.Dropped = false
flags.Todo = false
flags.RepTask.Is = false
flags.Pomodoros = 0
if len(str) < (indentLevel*4 + 2) {
return "", flags
}
str = strings.TrimLeft(str, strings.Repeat(indentPattern, indentLevel))
text = str[2:]
done = false
todo = false
repTask.Is = false
if text[0] == '[' {
todo = true
if text[1] == 'x' || text[1] == 'X' {
done = true
if todoTaskExp.MatchString(text) {
flags.Todo = true
inner := string(todoTaskExp.FindSubmatch([]byte(text))[1])
if strings.ContainsAny(inner, "xX") {
flags.Done = true
}
if len(text) > 4 {
text = text[4:]
if strings.Contains(inner, "-") {
flags.Dropped = true
}
flags.Pomodoros = strings.Count(inner, ".")
if len(text) > len(inner) + 3 {
text = text[len(inner)+3:]
}
repTaskRegExp := regexp.MustCompile("^([0-9]*)[xX]([0-9]*)")
if repTaskRegExp.MatchString(text) {
repTask.Is = true
flags.RepTask.Is = true
matches := repTaskRegExp.FindStringSubmatch(text)
repTask.A, _ = strconv.Atoi(matches[1])
repTask.B, _ = strconv.Atoi(matches[2])
flags.RepTask.A, _ = strconv.Atoi(matches[1])
flags.RepTask.B, _ = strconv.Atoi(matches[2])
loc := repTaskRegExp.FindIndex([]byte(text))
text = text[loc[1]:]
text = text[loc[1]+1:]
}
}
return
}
}