Initial Commit

This commit is contained in:
Brian Buller 2019-02-20 20:44:07 -06:00
parent 7305e4e54c
commit cf80d56e20
6 changed files with 632 additions and 0 deletions

1
.gitignore vendored
View File

@ -1,5 +1,6 @@
# ---> Go
# Binaries for programs and plugins
gime
*.exe
*.dll
*.so

189
app_state.go Normal file
View File

@ -0,0 +1,189 @@
package main
import (
"fmt"
"os"
"strings"
timertxt "git.bullercodeworks.com/brian/go-timertxt"
"github.com/br0xen/user-config"
)
type AppState struct {
Name string
Version int
config *userConfig.Config
directory string
fileTimer string
fileDone string
fileReport string
ValidOperations map[string][]string
OpFuncs map[string]func([]string) int
TimerList *timertxt.TimerList
}
func NewApp() *AppState {
app := &AppState{Name: AppName, Version: AppVersion}
app.initialize()
app.doVersionCheck()
if err := app.LoadTimerList(); err != nil {
if len(os.Args) > 1 && os.Args[1] != "--reinit" {
panic(err)
}
}
return app
}
func (a *AppState) run(parms []string) int {
if len(parms) == 0 || parms[0] == "ui" {
// UI Mode
//return uiLoop()
}
if fn, ok := a.OpFuncs[parms[0]]; ok {
return fn(parms[1:])
}
fmt.Println("Unknown Command")
return 1
}
func (a *AppState) getTimerFile() string {
return a.directory + a.fileTimer
}
func (a *AppState) getDoneFile() string {
return a.directory + a.fileDone
}
func (a *AppState) getReportFile() string {
return a.directory + a.fileReport
}
func (a *AppState) addOperation(name string, desc []string, fn func([]string) int) {
a.ValidOperations[name] = desc
a.OpFuncs[name] = fn
}
func (a *AppState) doVersionCheck() {
confVer, _ := a.config.GetInt("version")
for confVer < a.Version {
confVer = a.migrate(confVer, a.Version)
}
a.config.SetInt("version", confVer)
}
func (a *AppState) migrate(from, to int) int {
if from == to {
return to
}
switch from {
case 0:
a.initializeConfig()
return 1
}
// If we get all the way down here, we _must_ be done.
return to
}
func (a *AppState) initialize() {
var err error
a.config, err = userConfig.NewConfig(a.Name)
if err != nil {
panic(err)
}
a.ValidOperations = make(map[string][]string)
a.OpFuncs = make(map[string]func([]string) int)
a.addOperation("ls",
[]string{
"ls - List Timers",
},
a.opListTimers,
)
a.addOperation("lsa",
[]string{
"lsa - The same as 'ls -a'",
},
func(args []string) int {
return a.opListTimers(append([]string{"-a"}, args...))
},
)
a.addOperation("start",
[]string{
"start [time] [@contexts...] [+projects...] [tag:value...]",
" - Start a timer with the given details",
" If the first argument looks like a time,",
" the timer will be started then (past or future)",
},
a.opStartTimer,
)
a.addOperation("stop",
[]string{
"stop [time] - Stops the current timer",
" If the first argument looks like a time,",
" the timer will be stopped then (past or future)",
},
a.opStopTimer,
)
a.addOperation("status",
[]string{
"status - Prints the status of all active timers",
},
a.opStatus,
)
a.addOperation("fuzzyparse",
[]string{
"fuzzyparse [date string] - Parses the passed string and print the RFC3339 result (for testing)",
},
a.opFuzzyParse,
)
a.addOperation("--reinit",
[]string{"--reinit - Reset all Configuration Settings"},
func(args []string) int {
a.initializeConfig()
return 0
},
)
a.addOperation("-h",
[]string{"-h - Print this message"},
a.opPrintUsage,
)
a.addOperation("help",
[]string{"help - Print this message"},
a.opPrintUsage,
)
a.addOperation("--h",
[]string{"--h - Print this message"},
a.opPrintUsage,
)
a.directory = a.config.Get("directory")
a.fileTimer = a.config.Get("timerfile")
a.fileDone = a.config.Get("donefile")
a.fileReport = a.config.Get("reportfile")
}
func (a *AppState) initializeConfig() {
fmt.Println("Initializing " + a.Name)
for {
var add string
if a.directory != "" {
add = " (" + a.directory + ")"
}
fmt.Println("Path to timer.txt" + add + ":")
var resp string
fmt.Scanln(&resp)
if resp == "" && a.directory != "" {
resp = a.directory
}
if resp != "" {
if !strings.HasSuffix(resp, "/") {
resp = resp + "/"
}
fmt.Println("Setting timer.txt directory to: " + resp)
a.config.Set("directory", resp)
break
}
}
a.config.Set("timerfile", "timer.txt")
a.config.Set("donefile", "done.txt")
a.config.Set("reportfile", "report.txt")
}

234
helpers.go Normal file
View File

@ -0,0 +1,234 @@
package main
import (
"errors"
"fmt"
"strings"
"time"
timertxt "git.bullercodeworks.com/brian/go-timertxt"
)
func GetRoundToDuration() time.Duration {
var dur time.Duration
var err error
if dur, err = time.ParseDuration(app.config.Get("roundto")); err != nil {
app.config.Set("roundto", DefRoundTo)
dur, _ = time.ParseDuration(DefRoundTo)
}
return dur
}
func DurationToDecimal(dur time.Duration) float64 {
mins := dur.Minutes() - (dur.Hours() * 60)
return dur.Hours() + (mins / 60)
}
// getContextsFromSlice pulls all '@' (contexts) out of the
// string slice and return those contexts and the remaining
// strings from the slice
func getContextsFromSlice(args []string) ([]string, []string) {
return splitSlice(args, func(v string) bool {
return strings.HasPrefix(v, "@")
})
}
// getProjectsFromSlice pulls all '+' (projects) out of the
// string slice and return those projects and the remaining
// strings from the slice
func getProjectsFromSlice(args []string) ([]string, []string) {
return splitSlice(args, func(v string) bool {
return strings.HasPrefix(v, "+")
})
}
func splitSlice(args []string, predicate func(string) bool) ([]string, []string) {
var rem1, rem2 []string
for _, v := range args {
if predicate(v) {
rem1 = append(rem1, v)
} else {
rem2 = append(rem2, v)
}
}
return rem1, rem2
}
func parseFuzzyTime(t string) (time.Time, error) {
var ret time.Time
var err error
for i := range fuzzyFormats {
ret, err = time.Parse(fuzzyFormats[i], t)
if err == nil {
// Make sure it's in the local timezone
tz := time.Now().Format("Z07:00")
t = ret.Format("2006-01-02T15:04:05") + tz
if ret, err = time.Parse(time.RFC3339, t); err != nil {
return ret, err
}
// Check for zero on year/mo/day
if ret.Year() == 0 && ret.Month() == time.January && ret.Day() == 1 {
ret = ret.AddDate(time.Now().Year(), int(time.Now().Month())-1, time.Now().Day()-1)
}
return ret, nil
}
}
return time.Time{}, errors.New("Unable to parse time: " + t)
}
var fuzzyFormats = []string{
"1504",
"15:04", // Kitchen, 24hr
time.Kitchen,
time.RFC3339,
"2006-01-02T15:04:05", // RFC3339 without timezone
"2006-01-02T15:04", // RFC3339 without seconds or timezone
time.Stamp,
"02 Jan 06 15:04:05", // RFC822 with second
time.RFC822,
"01/02/2006 15:04", // U.S. Format
"01/02/2006 15:04:05", // U.S. Format with seconds
"01/02/06 15:04", // U.S. Format, short year
"01/02/06 15:04:05", // U.S. Format, short year, with seconds
"2006-01-02",
"2006-01-02 15:04",
"2006-01-02 15:04:05",
"20060102",
"20060102 15:04",
"20060102 15:04:05",
"20060102 1504",
"20060102 150405",
"20060102T15:04",
"20060102T15:04:05",
"20060102T1504",
"20060102T150405",
}
func timerToFriendlyString(t *timertxt.Timer) string {
var start, end, contexts, projects string
start = t.StartDate.Format(friendlyFormatForTime(t.StartDate))
if t.FinishDate.IsZero() {
end = "**:**"
} else {
end = t.FinishDate.Format(friendlyFormatForTime(t.FinishDate))
}
for _, v := range t.Contexts {
contexts += "@" + v + " "
}
for _, v := range t.Projects {
projects += "+" + v + " "
}
return fmt.Sprintf("% 2d. %s - %s [ %s] [ %s] %s", t.Id, start, end, contexts, projects, t.Notes)
}
func friendlyFormatForTime(t time.Time) string {
nowTime := time.Now()
if t.Year() != nowTime.Year() || t.Month() != nowTime.Month() {
return "2006-01-02 15:04"
} else if t.Day() != nowTime.Day() {
return "01/02 15:04"
}
return "15:04"
}
// timeToFriendlyString returns an easier to read version of the time
// giving enough details that the user should be fine inferring the rest
func timeToFriendlyString(t time.Time) string {
return t.Format(friendlyFormatForTime(t))
}
func sinceToString(tm time.Time) string {
return diffToString(tm, time.Now())
}
func diffToString(tm1, tm2 time.Time) string {
ret := ""
yr, mo, dy, hr, mn, sc := diff(tm1, tm2)
higher := false
if yr > 0 {
ret += fmt.Sprintf("%4dy ", yr)
higher = true
}
if mo > 0 || higher {
ret += fmt.Sprintf("%2dm ", mo)
higher = true
}
if dy > 0 || higher {
ret += fmt.Sprintf("%2dd ", dy)
higher = true
}
if hr > 0 || higher {
ret += fmt.Sprintf("%2dh ", hr)
higher = true
}
if mn > 0 || higher {
ret += fmt.Sprintf("%2dm ", mn)
higher = true
}
if sc > 0 || higher {
ret += fmt.Sprintf("%2ds", sc)
}
return ret
}
func padRight(st string, l int) string {
for len(st) < l {
st = st + " "
}
return st
}
func padLeft(st string, l int) string {
for len(st) < l {
st = " " + st
}
return st
}
func diff(a, b time.Time) (year, month, day, hour, min, sec int) {
if a.Location() != b.Location() {
b = b.In(a.Location())
}
if a.After(b) {
a, b = b, a
}
y1, M1, d1 := a.Date()
y2, M2, d2 := b.Date()
h1, m1, s1 := a.Clock()
h2, m2, s2 := b.Clock()
year = int(y2 - y1)
month = int(M2 - M1)
day = int(d2 - d1)
hour = int(h2 - h1)
min = int(m2 - m1)
sec = int(s2 - s1)
// Normalize negative values
if sec < 0 {
sec += 60
min--
}
if min < 0 {
min += 60
hour--
}
if hour < 0 {
hour += 24
day--
}
if day < 0 {
// days in month:
t := time.Date(y1, M1, 32, 0, 0, 0, 0, time.UTC)
day += 32 - t.Day()
month--
}
if month < 0 {
month += 12
year--
}
return
}

25
main.go Normal file
View File

@ -0,0 +1,25 @@
package main
import "os"
const (
AppName = "gime"
AppVersion = 1
DefRoundTo = "1m0s"
)
var app *AppState
func main() {
app = NewApp()
var parms []string
if len(os.Args) > 1 {
parms = os.Args[1:]
} else {
// if no parameters were passed, just do a status
parms = append(parms, "status")
}
os.Exit(app.run(parms))
}

45
model.go Normal file
View File

@ -0,0 +1,45 @@
package main
import (
"time"
timertxt "git.bullercodeworks.com/brian/go-timertxt"
)
func (a *AppState) SetTimerFinished(id int, end time.Time) error {
var t *timertxt.Timer
var err error
if t, err = a.TimerList.GetTimer(id); err != nil {
return err
}
t.FinishDate = end
t.Finished = true
return nil
}
func (a *AppState) LoadTimerList() error {
var err error
var tl timertxt.TimerList
tl, err = timertxt.LoadFromFilename(a.getTimerFile())
tl.Sort(timertxt.SORT_UNFINISHED_START)
a.TimerList = &tl
return err
}
func (a *AppState) WriteList() error {
return a.TimerList.WriteToFilename(a.getTimerFile())
}
/*
func (a *AppState) LoadDoneList() error {
var err error
var tl timertxt.TimerList
tl, err = timertxt.LoadFromFilename(a.getDoneFile())
a.DoneList = &tl
return err
}
func (a *AppState) WriteDoneList() error {
return a.DoneList.WriteToFilename(a.getDoneFile())
}
*/

138
task_ops.go Normal file
View File

@ -0,0 +1,138 @@
package main
import (
"fmt"
"strconv"
"time"
timertxt "git.bullercodeworks.com/brian/go-timertxt"
)
func (a *AppState) opStatus(args []string) int {
fmt.Println("Current Time: " + time.Now().Format(time.Stamp))
if len(*a.TimerList.GetActiveTimers()) == 0 {
fmt.Println("No timers running")
return 0
}
for _, v := range *a.TimerList.GetActiveTimers() {
fmt.Println(timerToFriendlyString(&v))
}
return 0
}
func (a *AppState) opListTimers(args []string) int {
// By default only list today
// Format:
// 2019/02/20 (9.25)
// start - end [ contexts ] [ projects ]
// ...
dayTotals := make(map[string]time.Duration)
for _, v := range *a.TimerList {
dur := v.FinishDate.Sub(v.StartDate)
if v.FinishDate.IsZero() {
dur = time.Now().Sub(v.StartDate)
}
dayTotals[v.StartDate.Format("2006/01/02")] += dur
}
var oldDayStr, dayStr string
for _, v := range *a.TimerList {
oldDayStr = dayStr
dayStr = v.StartDate.Format("2006/01/02")
if dayStr != oldDayStr {
wrkDur := dayTotals[dayStr].Round(GetRoundToDuration())
fmtStr := dayStr + " ( %.2f )\n"
fmt.Printf(fmtStr, DurationToDecimal(wrkDur))
}
fmt.Println(" " + timerToFriendlyString(&v))
}
return 0
}
func (a *AppState) opStartTimer(args []string) int {
var contexts, projects []string
t := timertxt.NewTimer()
contexts, args = getContextsFromSlice(args)
projects, args = getProjectsFromSlice(args)
if len(args) > 0 {
if start, err := parseFuzzyTime(args[0]); err == nil {
t.StartDate = start
args = args[1:]
}
}
t.Contexts = contexts
t.Projects = projects
a.TimerList.AddTimer(t)
if err := a.WriteList(); err != nil {
fmt.Println(err.Error())
return 1
}
return 0
}
func (a *AppState) opStopTimer(args []string) int {
var err error
var wrk time.Time
end := time.Now()
id := -1
if len(args) > 0 {
if wrk, err = parseFuzzyTime(args[0]); err != nil {
id, err = strconv.Atoi(args[0])
} else {
end = wrk
args = args[1:]
}
}
fmt.Println("Stopping at : " + end.Format(time.RFC3339))
var timerIds []int
if id == -1 {
for _, v := range *a.TimerList.GetActiveTimers() {
timerIds = append(timerIds, v.Id)
}
} else {
timerIds = append(timerIds, id)
}
for _, v := range timerIds {
var stopped *timertxt.Timer
if stopped, err = a.TimerList.GetTimer(v); err != nil {
fmt.Println(err.Error())
}
if err = a.SetTimerFinished(v, end); err != nil {
fmt.Println(err.Error())
continue
}
fmt.Println("Stopped Timer:", timerToFriendlyString(stopped))
}
if err = a.WriteList(); err != nil {
fmt.Println(err.Error())
return 1
}
return 0
}
func (a *AppState) opState(args []string) int {
return 0
}
func (a *AppState) opFuzzyParse(args []string) int {
if len(args) > 0 {
if start, err := parseFuzzyTime(args[0]); err == nil {
fmt.Println(start.Format(time.RFC3339))
} else {
fmt.Println(err.Error())
}
}
return 0
}
func (a *AppState) opPrintUsage(args []string) int {
for _, v := range a.ValidOperations {
for _, vv := range v {
fmt.Println(" " + vv)
fmt.Println("")
}
}
return 0
}