diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..1d63954 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2016 aerth + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6744946 --- /dev/null +++ b/Makefile @@ -0,0 +1,4 @@ +all: + go build --ldflags='-s' -o worktimer-gtk +install: + @mv worktimer-gtk /usr/local/bin \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a8b8efa --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# worktimer-gtk + +Status Icon timer with json output. + +Also decodes the json and adds up the hours. \ No newline at end of file diff --git a/main.go b/main.go new file mode 100644 index 0000000..ce8ded1 --- /dev/null +++ b/main.go @@ -0,0 +1,290 @@ +// command worktimer-gtk is a notification bar icon that works +package main + +import ( + "bufio" + "encoding/json" + "flag" + "fmt" + "math/big" + "os" + "strings" + "time" + + "github.com/aerthlib/filer" + "github.com/mattn/go-gtk/gdk" + "github.com/mattn/go-gtk/glib" + "github.com/mattn/go-gtk/gtk" +) + +const titlestr = "Work Mode" + +var filename string +var ( + prefix = flag.String("prefix", "", "Prefix for filename") + justclockin = flag.Bool("on", false, "Just punch in, don't launch status icon.") + justclockout = flag.Bool("off", false, "Just punch out, don't launch status icon.") + icon = flag.Bool("icon", false, "Punch in and launch icon.") + decode = flag.Bool("decode", false, "Just print total hours and exit. Need -in flag.") +) + +func init() { + flag.StringVar(&filename, "o", "", "Save to file") + flag.StringVar(&filename, "in", "", "Decode file. Need -decode flag") +} + +var start time.Time +var working = true + +// PunchIn is a duration of work, or a start time. Or a finish time. +// To save bytes in the Marshal +type PunchIn struct { + Started time.Time `json:",omitempty"` +} + +// PunchOut is a duration of work, or a start time. Or a finish time. +type PunchOut struct { + PunchIn + Started time.Time `json:",omitempty"` + Finished time.Time `json:",omitempty"` + Duration time.Duration `json:",omitempty, string"` +} + +var punchcards []PunchOut + +func clockin() { + var punch PunchIn + punch.Started = time.Now() + start = punch.Started + + filer.Touch(filename) + // save to punchcard + b, err := json.Marshal(&punch) + if err != nil { + panic(err) + } + filer.Append(filename, append(b, []byte("\n")...)) // !!! + fmt.Printf("Working since %s.\nSaving to %q.\n", start, filename) + working = true +} + +func clockout() { + var punch PunchOut + now := time.Now() + + if start.IsZero() { + start = getlastpunchin() + } + punch.Started = start + punch.Finished = now + punch.Duration = punch.Finished.Sub(punch.Started) + // reset timer for breaks + start = now + working = false + + // save to punchcard + b, err := json.Marshal(&punch) + if err != nil { + panic(err) + } + err = filer.Append(filename, append(b, []byte("\n")...)) // !!! + if err != nil { + panic(err) + } + + fmt.Printf("Worked from %s to %s, total of %s.\n", punch.Started, punch.Finished, punch.Duration) +} +func main() { + flag.Parse() + if *decode && filename != "" { + fmt.Println(gettotal()) + os.Exit(0) + } + if len(os.Args) == 1 { + flag.Usage() + os.Exit(1) + } + if flag.Args() != nil { + for _, v := range flag.Args() { + if strings.HasPrefix(v, "-") { + flag.Usage() + os.Exit(1) + } + } + } + + if filename == "" { + filename = "./" + *prefix + time.Now().Format("Jan2006") + ".json" + } + if *justclockin && !*justclockout { + clockin() + os.Exit(0) + } + + if *justclockout && !*justclockin { + clockout() + os.Exit(0) + } + + if *icon { + iconlaunch() + } + +} + +func iconlaunch() { + glib.ThreadInit(nil) + gdk.ThreadsInit() + gdk.ThreadsEnter() + gtk.Init(nil) + + glib.SetApplicationName(titlestr) + go func() { + for { + + finish := time.Now() + total := finish.Sub(start) + + if !working { + fmt.Printf("Not working. Stopped at %s, counting %s.\n", start, total) + } else { + fmt.Printf("Worked from %s to %s, counting %s.\n", start, finish, total) + } + //filer.Append(filename, []byte(fmt.Sprintf("Worked from %s to %s, counting %s.\n", start, finish, total))) + time.Sleep(time.Second * 1) + } + }() + clockin() + defer clockout() + + icon := gtk.NewStatusIconFromStock(gtk.STOCK_YES) + remenu := gtk.NewMenu() + clockinBtn := gtk.NewMenuItemWithLabel("Clock In") + quitBtn := gtk.NewMenuItemWithLabel("Exit") + clockoutBtn := gtk.NewMenuItemWithLabel("Clock Out") + startedlabel := gtk.NewMenuItemWithLabel("Started: " + start.Format(time.Kitchen)) + breaklabel := gtk.NewMenuItemWithLabel("Stopped.") + g := gdk.NewColor("green") + r := gdk.NewColor("lightgrey") + clockinBtn.Connect("activate", func() { + icon.SetFromStock(gtk.STOCK_YES) + clockinBtn.SetVisible(false) + clockoutBtn.SetVisible(true) + quitBtn.SetVisible(false) + breaklabel.SetVisible(false) + startedlabel.SetVisible(true) + remenu.ModifyBG(gtk.STATE_NORMAL, g) + + clockin() + + }) + clockoutBtn.Connect("activate", func() { + icon.SetFromStock(gtk.STOCK_NO) + clockinBtn.SetVisible(true) + clockoutBtn.SetVisible(false) + quitBtn.SetVisible(true) + breaklabel.SetVisible(true) + startedlabel.SetVisible(false) + remenu.ModifyBG(gtk.STATE_NORMAL, r) + + clockout() + + }) + quitBtn.Connect("activate", func() { + gtk.MainQuit() + }) + remenu.Append(clockoutBtn) + remenu.Append(clockinBtn) + remenu.Append(quitBtn) + remenu.Append(startedlabel) + + remenu.Append(breaklabel) + icon.SetTitle(titlestr) + icon.SetTooltipMarkup(fmt.Sprintf("%s", titlestr)) + go func() { + for { + time.Sleep(1 * time.Second) + gdk.ThreadsEnter() + remenu.SetTooltipText(time.Now().Sub(start).String()) + icon.SetTooltipMarkup(fmt.Sprintf("%s %s", titlestr, time.Now().Sub(start).String())) + gdk.ThreadsLeave() + } + }() + icon.Connect("popup-menu", func(cbx *glib.CallbackContext) { + remenu.Popup(nil, nil, gtk.StatusIconPositionMenu, icon, uint(cbx.Args(0)), uint32(cbx.Args(1))) + }) + remenu.ShowAll() + remenu.ModifyBG(gtk.STATE_NORMAL, g) + clockinBtn.SetVisible(false) + quitBtn.SetVisible(false) + breaklabel.SetVisible(false) + gtk.Main() +} +func getlastpunchin() time.Time { + + file, er := os.Open(filename) + if er != nil { + fmt.Println(er) + os.Exit(1) + } + var latest time.Time + var punchins []PunchIn + scanner := bufio.NewScanner(file) + var scan int + for scanner.Scan() { + scan++ + var p PunchIn + er = json.Unmarshal(scanner.Bytes(), &p) + if er != nil { + // log it + continue + } + + if p.Started.Sub(latest) > 0 { + latest = p.Started + } + punchins = append(punchins, p) + } + + return latest +} +func gettotal() string { + + file, er := os.Open(filename) + if er != nil { + fmt.Println(er) + os.Exit(1) + } + var punchouts []PunchOut + scanner := bufio.NewScanner(file) + var scan int + for scanner.Scan() { + scan++ + var p PunchOut + er = json.Unmarshal(scanner.Bytes(), &p) + if er != nil { + // log it panic(er) + continue + } + + punchouts = append(punchouts, p) + } + var x, y, z big.Int + z.SetInt64(0) + for _, v := range punchouts { + if int64(v.Duration) > 0 { + //fmt.Println(v.Duration) + x.SetInt64(int64(v.Duration)) + z.Add(&x, &z) + } + } + y.SetInt64(int64(time.Hour)) + // fmt.Println(z.String() + " nanoseconds / " + y.String() + " nanoseconds = ") + var f1, f2, f3 big.Float + + f1.SetInt(&z) + f2.SetInt(&y) + f3.Quo(&f1, &f2) + + return f3.String() + " Hours" +}