Initial Commit
This commit is contained in:
parent
7305e4e54c
commit
cf80d56e20
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,5 +1,6 @@
|
||||
# ---> Go
|
||||
# Binaries for programs and plugins
|
||||
gime
|
||||
*.exe
|
||||
*.dll
|
||||
*.so
|
||||
|
189
app_state.go
Normal file
189
app_state.go
Normal 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
234
helpers.go
Normal 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
25
main.go
Normal 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
45
model.go
Normal 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
138
task_ops.go
Normal 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
|
||||
}
|
Loading…
Reference in New Issue
Block a user