Compare commits
16 Commits
Author | SHA1 | Date |
---|---|---|
Brian Buller | b30d663eb1 | |
Brian Buller | b061cbae00 | |
Brian Buller | 34208ad5c9 | |
Brian Buller | 3165c0254d | |
Brian Buller | 2378c9e617 | |
Brian Buller | 5ff74e851c | |
Brian Buller | e0affc82d4 | |
Brian Buller | 56f12e4a58 | |
Brian Buller | 36d24ee5d3 | |
Brian Buller | a4f1603df5 | |
Brian Buller | c412f54294 | |
Brian Buller | c98a4dea0c | |
Brian Buller | f06c0b4b7c | |
Brian Buller | a8c1812bbf | |
Brian Buller | b0f5e928d9 | |
Brian Buller | 45d13e7052 |
17
Makefile
17
Makefile
|
@ -1,6 +1,21 @@
|
||||||
|
# This is what we want to name the binary output
|
||||||
|
BINARY=gime
|
||||||
|
|
||||||
|
# These are the values we want to pass for VERSION and BUILD
|
||||||
|
# git tag 1.0.1
|
||||||
|
# git commit -am "One more change after the tags"
|
||||||
|
VERSION="beta" # `git describe --tags`
|
||||||
|
BUILD=`date +%FT%T%z`
|
||||||
|
|
||||||
|
# Setup the -ldflags option for go build here, interpolate the variable values
|
||||||
|
LDFLAGS=-ldflags "-w -s -X cmd.Version=${VERSION} -X cmd.Build=${BUILD}"
|
||||||
|
|
||||||
|
# Builds the project
|
||||||
gime:
|
gime:
|
||||||
go build -o build/gime *.go
|
go build ${LDFLAGS} -o build/${BINARY}
|
||||||
|
|
||||||
|
runui:
|
||||||
|
cd build && ./gime ui
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm build/*
|
rm build/*
|
||||||
|
|
41
cli/cli.go
41
cli/cli.go
|
@ -27,15 +27,31 @@ func (p *Program) Initialize() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Program) GetTimerFilePath() string {
|
||||||
|
return p.timerPath
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Program) LoadTimerList() error {
|
func (p *Program) LoadTimerList() error {
|
||||||
var err error
|
var err error
|
||||||
var tl timertxt.TimerList
|
var tl *timertxt.TimerList
|
||||||
tl, err = timertxt.LoadFromFilename(p.timerPath)
|
tl, err = timertxt.LoadFromFilename(p.timerPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
tl.Sort(timertxt.SORT_UNFINISHED_START)
|
tl.Sort(timertxt.SortUnfinishedStart)
|
||||||
p.TimerList = &tl
|
p.TimerList = tl
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Program) WriteLists() error {
|
||||||
|
err := p.WriteTimerList()
|
||||||
|
if dErr := p.WriteDoneList(); dErr != nil {
|
||||||
|
if err == nil {
|
||||||
|
return fmt.Errorf("Error writing Done list %w", dErr)
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("Error writing Both lists (Timer: %s; Done: %s) (%w)", err.Error(), dErr.Error(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,14 +59,18 @@ func (p *Program) WriteTimerList() error {
|
||||||
return p.TimerList.WriteToFilename(p.timerPath)
|
return p.TimerList.WriteToFilename(p.timerPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Program) GetDoneFilePath() string {
|
||||||
|
return p.donePath
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Program) LoadDoneList() error {
|
func (p *Program) LoadDoneList() error {
|
||||||
var err error
|
var err error
|
||||||
var tl timertxt.TimerList
|
var tl *timertxt.TimerList
|
||||||
tl, err = timertxt.LoadFromFilename(p.donePath)
|
tl, err = timertxt.LoadFromFilename(p.donePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
p.DoneList = &tl
|
p.DoneList = tl
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,14 +122,14 @@ func (p *Program) GetFilteredTimerList(args []string) *timertxt.TimerList {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
list := p.TimerList.GetTimersInRange(start, end)
|
list := p.TimerList.GetTimersInRange(start, end)
|
||||||
|
|
||||||
if includeArchive {
|
if includeArchive {
|
||||||
if err = p.LoadDoneList(); err != nil {
|
if err = p.LoadDoneList(); err != nil {
|
||||||
fmt.Println("Error loading done.txt entries")
|
fmt.Println("Error loading done.txt entries")
|
||||||
fmt.Println(err.Error())
|
fmt.Println(err.Error())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
*list = append(*list, (*p.DoneList.GetTimersInRange(start, end))...)
|
doneList := p.DoneList.GetTimersInRange(start, end)
|
||||||
|
list.Combine(doneList)
|
||||||
}
|
}
|
||||||
if len(contextFilters) > 0 {
|
if len(contextFilters) > 0 {
|
||||||
allFilters = append(allFilters, func(t timertxt.Timer) bool {
|
allFilters = append(allFilters, func(t timertxt.Timer) bool {
|
||||||
|
@ -145,6 +165,13 @@ func (p *Program) GetFilteredTimerList(args []string) *timertxt.TimerList {
|
||||||
return list.Filter(doFilters)
|
return list.Filter(doFilters)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Program) GetActiveOrMostRecent() (*timertxt.Timer, error) {
|
||||||
|
work, wErr := p.TimerList.GetActiveOrMostRecent()
|
||||||
|
if wErr == nil {
|
||||||
|
return work, nil
|
||||||
|
}
|
||||||
|
return p.GetMostRecentTimer()
|
||||||
|
}
|
||||||
func (p *Program) GetMostRecentTimer() (*timertxt.Timer, error) {
|
func (p *Program) GetMostRecentTimer() (*timertxt.Timer, error) {
|
||||||
work, wErr := p.TimerList.GetMostRecentTimer()
|
work, wErr := p.TimerList.GetMostRecentTimer()
|
||||||
if wErr == nil && work.FinishDate.IsZero() {
|
if wErr == nil && work.FinishDate.IsZero() {
|
||||||
|
|
|
@ -7,6 +7,7 @@ package cmd
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
@ -15,7 +16,10 @@ import (
|
||||||
// configCmd represents the config command
|
// configCmd represents the config command
|
||||||
var configCmd = &cobra.Command{
|
var configCmd = &cobra.Command{
|
||||||
Use: "config",
|
Use: "config",
|
||||||
Short: "Print all configuration values",
|
Short: "Show or update configuration values",
|
||||||
|
Long: `To set values just list them in key=value format.
|
||||||
|
For example:
|
||||||
|
gime config copytags=true roundto=30m`,
|
||||||
RunE: opConfig,
|
RunE: opConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,10 +28,37 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func opConfig(cmd *cobra.Command, args []string) error {
|
func opConfig(cmd *cobra.Command, args []string) error {
|
||||||
|
updConfig := make(map[string]string)
|
||||||
|
if len(args) > 0 {
|
||||||
|
// We're setting arguments
|
||||||
|
for _, a := range args {
|
||||||
|
pts := strings.Split(a, "=")
|
||||||
|
if len(pts) == 2 {
|
||||||
|
updConfig[pts[0]] = pts[1]
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("Unable to parse config values.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
var settings []string
|
var settings []string
|
||||||
for k, v := range viper.AllSettings() {
|
for k, v := range viper.AllSettings() {
|
||||||
|
switch v.(type) {
|
||||||
|
case bool:
|
||||||
|
if nv, ok := updConfig[k]; ok {
|
||||||
|
v = nv == "true"
|
||||||
|
viper.Set(k, v)
|
||||||
|
viper.WriteConfig()
|
||||||
|
}
|
||||||
|
settings = append(settings, fmt.Sprintf("%s: %t", k, v))
|
||||||
|
default:
|
||||||
|
if nv, ok := updConfig[k]; ok {
|
||||||
|
v = nv
|
||||||
|
viper.Set(k, v)
|
||||||
|
viper.WriteConfig()
|
||||||
|
}
|
||||||
settings = append(settings, fmt.Sprintf("%s: %s", k, v))
|
settings = append(settings, fmt.Sprintf("%s: %s", k, v))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
sort.Strings(settings)
|
sort.Strings(settings)
|
||||||
fmt.Println("Configuration File:", viper.ConfigFileUsed())
|
fmt.Println("Configuration File:", viper.ConfigFileUsed())
|
||||||
for _, v := range settings {
|
for _, v := range settings {
|
||||||
|
|
|
@ -6,7 +6,10 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"git.bullercodeworks.com/brian/gime/cli"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -22,6 +25,23 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func opEditor(cmd *cobra.Command, args []string) error {
|
func opEditor(cmd *cobra.Command, args []string) error {
|
||||||
fmt.Println("editor called")
|
p := cli.Program{}
|
||||||
return nil
|
if err := p.Initialize(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
file := p.GetTimerFilePath()
|
||||||
|
if len(args) > 0 {
|
||||||
|
if args[0] == "d" || args[0] == "done" {
|
||||||
|
file = p.GetDoneFilePath()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
editor := os.Getenv("EDITOR")
|
||||||
|
if editor == "" {
|
||||||
|
return fmt.Errorf("No EDITOR set")
|
||||||
|
}
|
||||||
|
fmt.Println("Starting", editor, file)
|
||||||
|
c := exec.Command(editor, file)
|
||||||
|
c.Stdin = os.Stdin
|
||||||
|
c.Stdout = os.Stdout
|
||||||
|
return c.Run()
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,13 +17,7 @@ import (
|
||||||
// i3statusCmd represents the i3status command
|
// i3statusCmd represents the i3status command
|
||||||
var i3statusCmd = &cobra.Command{
|
var i3statusCmd = &cobra.Command{
|
||||||
Use: "i3status",
|
Use: "i3status",
|
||||||
Short: "",
|
Short: "Output your timer.txt status in json for the i3 status bar",
|
||||||
Long: `A longer description that spans multiple lines and likely contains examples
|
|
||||||
and usage of using your command. For example:
|
|
||||||
|
|
||||||
Cobra is a CLI library for Go that empowers applications.
|
|
||||||
This application is a tool to generate the needed files
|
|
||||||
to quickly create a Cobra application.`,
|
|
||||||
RunE: opI3Status,
|
RunE: opI3Status,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,14 +46,14 @@ func opI3Status(cmd *cobra.Command, args []string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
state := "Idle"
|
state := "Idle"
|
||||||
wrk, err := p.GetMostRecentTimer()
|
wrk, err := p.GetActiveOrMostRecent()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Print("{\"icon\":\"time\",\"state\":\"Critical\", \"text\": \"Error loading timer entry\"}")
|
fmt.Print("{\"icon\":\"time\",\"state\":\"Critical\", \"text\": \"Error loading timer entry\"}")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
var text string
|
var text string
|
||||||
if wrk.FinishDate.IsZero() {
|
if wrk.FinishDate.IsZero() {
|
||||||
wrkDur := wrk.Duration().Round(time.Minute * 15)
|
wrkDur := util.Round(wrk.Duration())
|
||||||
hrs := int(wrkDur.Hours())
|
hrs := int(wrkDur.Hours())
|
||||||
mins := int(wrkDur.Minutes()) - hrs*60
|
mins := int(wrkDur.Minutes()) - hrs*60
|
||||||
if hrs > 0 {
|
if hrs > 0 {
|
||||||
|
@ -97,7 +91,7 @@ func opI3Status(cmd *cobra.Command, args []string) error {
|
||||||
getListTotal := func(list *timertxt.TimerList) string {
|
getListTotal := func(list *timertxt.TimerList) string {
|
||||||
var isActive bool
|
var isActive bool
|
||||||
var total time.Duration
|
var total time.Duration
|
||||||
for _, v := range *list {
|
for _, v := range list.GetTimerSlice() {
|
||||||
dur := v.FinishDate.Sub(v.StartDate)
|
dur := v.FinishDate.Sub(v.StartDate)
|
||||||
if v.FinishDate.IsZero() {
|
if v.FinishDate.IsZero() {
|
||||||
dur = time.Now().Sub(v.StartDate)
|
dur = time.Now().Sub(v.StartDate)
|
||||||
|
@ -105,7 +99,7 @@ func opI3Status(cmd *cobra.Command, args []string) error {
|
||||||
}
|
}
|
||||||
total += dur
|
total += dur
|
||||||
}
|
}
|
||||||
total = total.Round(util.GetRoundToDuration())
|
total = util.Round(total)
|
||||||
if isActive {
|
if isActive {
|
||||||
return fmt.Sprintf("%.2f+", util.DurationToDecimal(total))
|
return fmt.Sprintf("%.2f+", util.DurationToDecimal(total))
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -76,7 +76,7 @@ func opListTimers(cmd *cobra.Command, args []string) error {
|
||||||
list = list.Filter(filter)
|
list = list.Filter(filter)
|
||||||
|
|
||||||
dayTotals := make(map[string]time.Duration)
|
dayTotals := make(map[string]time.Duration)
|
||||||
for _, v := range *list {
|
for _, v := range list.GetTimerSlice() {
|
||||||
dur := v.FinishDate.Sub(v.StartDate)
|
dur := v.FinishDate.Sub(v.StartDate)
|
||||||
if v.FinishDate.IsZero() {
|
if v.FinishDate.IsZero() {
|
||||||
dur = time.Now().Sub(v.StartDate)
|
dur = time.Now().Sub(v.StartDate)
|
||||||
|
@ -84,12 +84,12 @@ func opListTimers(cmd *cobra.Command, args []string) error {
|
||||||
dayTotals[v.StartDate.Format("2006/01/02")] += dur
|
dayTotals[v.StartDate.Format("2006/01/02")] += dur
|
||||||
}
|
}
|
||||||
var oldDayStr, dayStr string
|
var oldDayStr, dayStr string
|
||||||
for _, v := range *list {
|
for _, v := range list.GetTimerSlice() {
|
||||||
oldDayStr = dayStr
|
oldDayStr = dayStr
|
||||||
dayStr = v.StartDate.Format("2006/01/02")
|
dayStr = v.StartDate.Format("2006/01/02")
|
||||||
if dayStr != oldDayStr {
|
if dayStr != oldDayStr {
|
||||||
// TODO:
|
// TODO:
|
||||||
wrkDur := dayTotals[dayStr].Round(util.GetRoundToDuration())
|
wrkDur := util.Round(dayTotals[dayStr])
|
||||||
fmtStr := dayStr + " ( %.2f )\n"
|
fmtStr := dayStr + " ( %.2f )\n"
|
||||||
fmt.Printf(fmtStr, util.DurationToDecimal(wrkDur))
|
fmt.Printf(fmtStr, util.DurationToDecimal(wrkDur))
|
||||||
}
|
}
|
||||||
|
|
86
cmd/mod.go
86
cmd/mod.go
|
@ -6,20 +6,20 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.bullercodeworks.com/brian/gime/cli"
|
||||||
|
"git.bullercodeworks.com/brian/gime/util"
|
||||||
|
"git.bullercodeworks.com/brian/go-timertxt"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
// modCmd represents the mod command
|
// modCmd represents the mod command
|
||||||
var modCmd = &cobra.Command{
|
var modCmd = &cobra.Command{
|
||||||
Use: "mod",
|
Use: "mod",
|
||||||
Short: "A brief description of your command",
|
Short: "Modify a timer",
|
||||||
Long: `A longer description that spans multiple lines and likely contains examples
|
|
||||||
and usage of using your command. For example:
|
|
||||||
|
|
||||||
Cobra is a CLI library for Go that empowers applications.
|
|
||||||
This application is a tool to generate the needed files
|
|
||||||
to quickly create a Cobra application.`,
|
|
||||||
RunE: opMod,
|
RunE: opMod,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,6 +28,76 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func opMod(cmd *cobra.Command, args []string) error {
|
func opMod(cmd *cobra.Command, args []string) error {
|
||||||
fmt.Println("mod called")
|
var err error
|
||||||
|
p := cli.Program{}
|
||||||
|
err = p.Initialize()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = p.LoadTimerList(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var timer *timertxt.Timer
|
||||||
|
var contexts, projects []string
|
||||||
|
id, err := strconv.Atoi(args[0])
|
||||||
|
if err != nil {
|
||||||
|
// We didn't have a timer id, so try to modify the first active timer
|
||||||
|
active := p.TimerList.GetActiveTimers().GetTimerSlice()
|
||||||
|
if len(active) > 0 {
|
||||||
|
timer = active[0]
|
||||||
|
} else {
|
||||||
|
// And we don't have any active timers
|
||||||
|
return fmt.Errorf("No active timers, 'id' must be provided: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
args = args[1:]
|
||||||
|
if timer, err = p.TimerList.GetTimer(id); err != nil {
|
||||||
|
return fmt.Errorf("Error getting timer %d: %w", id, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var start, end time.Time
|
||||||
|
|
||||||
|
for _, v := range args {
|
||||||
|
pts := strings.Split(v, "=")
|
||||||
|
switch pts[0] {
|
||||||
|
case "beginning", "start":
|
||||||
|
if start, err = util.ParseFuzzyTime(pts[1]); err != nil {
|
||||||
|
return fmt.Errorf("Error parsing start time: %w", err)
|
||||||
|
}
|
||||||
|
case "stop", "finish", "end":
|
||||||
|
if end, err = util.ParseFuzzyTime(pts[1]); err != nil {
|
||||||
|
return fmt.Errorf("Error parsing end time: %w", err)
|
||||||
|
}
|
||||||
|
case "project", "projects":
|
||||||
|
projects = strings.Split(pts[1], ",")
|
||||||
|
case "context", "contexts":
|
||||||
|
contexts = strings.Split(pts[1], ",")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(contexts) > 0 {
|
||||||
|
for k := range contexts {
|
||||||
|
contexts[k] = strings.TrimPrefix(contexts[k], "@")
|
||||||
|
}
|
||||||
|
timer.Contexts = contexts
|
||||||
|
}
|
||||||
|
if len(projects) > 0 {
|
||||||
|
for k := range projects {
|
||||||
|
projects[k] = strings.TrimPrefix(projects[k], "+")
|
||||||
|
}
|
||||||
|
timer.Projects = projects
|
||||||
|
}
|
||||||
|
if !start.IsZero() {
|
||||||
|
timer.StartDate = start
|
||||||
|
}
|
||||||
|
if !end.IsZero() {
|
||||||
|
timer.FinishDate = end
|
||||||
|
timer.Finished = true
|
||||||
|
}
|
||||||
|
fmt.Println("Modified Timer:")
|
||||||
|
fmt.Println(util.TimerToFriendlyString(timer))
|
||||||
|
|
||||||
|
if err := p.WriteTimerList(); err != nil {
|
||||||
|
return fmt.Errorf("Error writing timer list: %w", err)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
40
cmd/rm.go
40
cmd/rm.go
|
@ -6,20 +6,17 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.bullercodeworks.com/brian/gime/cli"
|
||||||
|
"git.bullercodeworks.com/brian/gime/util"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
// rmCmd represents the rm command
|
// rmCmd represents the rm command
|
||||||
var rmCmd = &cobra.Command{
|
var rmCmd = &cobra.Command{
|
||||||
Use: "rm",
|
Use: "rm",
|
||||||
Short: "A brief description of your command",
|
Short: "Remove a timer",
|
||||||
Long: `A longer description that spans multiple lines and likely contains examples
|
|
||||||
and usage of using your command. For example:
|
|
||||||
|
|
||||||
Cobra is a CLI library for Go that empowers applications.
|
|
||||||
This application is a tool to generate the needed files
|
|
||||||
to quickly create a Cobra application.`,
|
|
||||||
RunE: opRemove,
|
RunE: opRemove,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,6 +25,33 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func opRemove(cmd *cobra.Command, args []string) error {
|
func opRemove(cmd *cobra.Command, args []string) error {
|
||||||
fmt.Println("remove called")
|
var err error
|
||||||
|
p := cli.Program{}
|
||||||
|
err = p.Initialize()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = p.LoadTimerList(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(args) == 0 {
|
||||||
|
return fmt.Errorf("No timer id given")
|
||||||
|
}
|
||||||
|
id, err := strconv.Atoi(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Invalid timer id given %s: %w", args[0], err)
|
||||||
|
}
|
||||||
|
t, err := p.TimerList.GetTimer(id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error getting timer with id %s: %w", args[0], err)
|
||||||
|
}
|
||||||
|
if err = p.TimerList.RemoveTimerById(id); err != nil {
|
||||||
|
return fmt.Errorf("Error Removing Timer: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Println("Timer removed")
|
||||||
|
fmt.Println(util.TimerToString(t))
|
||||||
|
if err := p.WriteTimerList(); err != nil {
|
||||||
|
return fmt.Errorf("Error writing timer list: %w", err)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,14 +15,16 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
Version = "2.0"
|
Version string
|
||||||
|
Build string
|
||||||
|
|
||||||
configFile string
|
configFile string
|
||||||
|
|
||||||
// rootCmd represents the base command when called without any subcommands
|
// rootCmd represents the base command when called without any subcommands
|
||||||
rootCmd = &cobra.Command{
|
rootCmd = &cobra.Command{
|
||||||
Use: "gime",
|
Use: "gime",
|
||||||
Short: "Timer.txt Client",
|
Short: "Timer.txt Client",
|
||||||
Long: "Gime is a timer.txt client to make all aspects of timekeeping simpler.",
|
Long: fmt.Sprintf("Gime is a timer.txt client to make all aspects of timekeeping simpler.\nVersion: %s\nBuild Date: %s\n", Version, Build),
|
||||||
RunE: opStatus,
|
RunE: opStatus,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -46,6 +48,7 @@ func initConfig() {
|
||||||
viper.SetDefault("timerfile", "timer.txt")
|
viper.SetDefault("timerfile", "timer.txt")
|
||||||
viper.SetDefault("donefile", "done.txt")
|
viper.SetDefault("donefile", "done.txt")
|
||||||
viper.SetDefault("reportfile", "report.txt")
|
viper.SetDefault("reportfile", "report.txt")
|
||||||
|
viper.SetDefault("copytags", false)
|
||||||
var firstDir string // In case we need to make directories
|
var firstDir string // In case we need to make directories
|
||||||
if configFile != "" {
|
if configFile != "" {
|
||||||
viper.SetConfigFile(configFile)
|
viper.SetConfigFile(configFile)
|
||||||
|
|
49
cmd/start.go
49
cmd/start.go
|
@ -6,20 +6,18 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.bullercodeworks.com/brian/gime/cli"
|
||||||
|
"git.bullercodeworks.com/brian/gime/util"
|
||||||
|
"git.bullercodeworks.com/brian/go-timertxt"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
// startCmd represents the start command
|
// startCmd represents the start command
|
||||||
var startCmd = &cobra.Command{
|
var startCmd = &cobra.Command{
|
||||||
Use: "start",
|
Use: "start",
|
||||||
Short: "A brief description of your command",
|
Short: "Start a timer",
|
||||||
Long: `A longer description that spans multiple lines and likely contains examples
|
|
||||||
and usage of using your command. For example:
|
|
||||||
|
|
||||||
Cobra is a CLI library for Go that empowers applications.
|
|
||||||
This application is a tool to generate the needed files
|
|
||||||
to quickly create a Cobra application.`,
|
|
||||||
RunE: opStart,
|
RunE: opStart,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,6 +26,41 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func opStart(cmd *cobra.Command, args []string) error {
|
func opStart(cmd *cobra.Command, args []string) error {
|
||||||
fmt.Println("start called")
|
var err error
|
||||||
|
p := cli.Program{}
|
||||||
|
err = p.Initialize()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = p.LoadTimerList(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var contexts, projects, strTags []string
|
||||||
|
t := timertxt.NewTimer()
|
||||||
|
if len(args) > 0 {
|
||||||
|
if start, err := util.ParseFuzzyTime(args[0]); err == nil {
|
||||||
|
t.StartDate = start
|
||||||
|
args = args[1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
contexts, args = util.GetContextsFromSlice(args)
|
||||||
|
projects, args = util.GetProjectsFromSlice(args)
|
||||||
|
strTags, args = util.GetAdditionalTagsFromSlice(args)
|
||||||
|
for _, v := range contexts {
|
||||||
|
t.Contexts = append(t.Contexts, strings.TrimPrefix(v, "@"))
|
||||||
|
}
|
||||||
|
for _, v := range projects {
|
||||||
|
t.Projects = append(t.Projects, strings.TrimPrefix(v, "+"))
|
||||||
|
}
|
||||||
|
for _, v := range strTags {
|
||||||
|
tgPts := strings.Split(v, ":")
|
||||||
|
t.AdditionalTags[tgPts[0]] = tgPts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
p.TimerList.AddTimer(t)
|
||||||
|
if err := p.WriteTimerList(); err != nil {
|
||||||
|
return fmt.Errorf("Error writing timer list: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Println("Started: ", util.TimerToString(t))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,21 +32,23 @@ func opStatus(cmd *cobra.Command, args []string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := p.LoadTimerList(); err != nil {
|
if err := p.LoadTimerList(); err != nil {
|
||||||
return err
|
return fmt.Errorf("Error loading timer list: %w", err)
|
||||||
}
|
}
|
||||||
if len(*p.TimerList.GetActiveTimers()) == 0 {
|
active := p.TimerList.GetActiveTimers().GetTimerSlice()
|
||||||
|
if len(active) == 0 {
|
||||||
fmt.Println("No timers running")
|
fmt.Println("No timers running")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
var currDur time.Duration
|
var currDur time.Duration
|
||||||
for _, v := range *p.TimerList {
|
for _, v := range active {
|
||||||
if v.ActiveToday() {
|
if v.ActiveToday() {
|
||||||
currDur += v.Duration()
|
currDur += v.Duration()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
d := currDur.Round(util.GetRoundToDuration())
|
d := util.Round(currDur)
|
||||||
|
|
||||||
fmt.Printf("%s ( %.2f hrs )\n", time.Now().Format(time.Stamp), util.DurationToDecimal(d))
|
fmt.Printf("%s ( %.2f hrs )\n", time.Now().Format(time.Stamp), util.DurationToDecimal(d))
|
||||||
for _, v := range *p.TimerList.GetActiveTimers() {
|
for _, v := range active {
|
||||||
fmt.Println(util.TimerToFriendlyString(v))
|
fmt.Println(util.TimerToFriendlyString(v))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
56
cmd/stop.go
56
cmd/stop.go
|
@ -6,20 +6,19 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.bullercodeworks.com/brian/gime/cli"
|
||||||
|
"git.bullercodeworks.com/brian/gime/util"
|
||||||
|
"git.bullercodeworks.com/brian/go-timertxt"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
// stopCmd represents the stop command
|
// stopCmd represents the stop command
|
||||||
var stopCmd = &cobra.Command{
|
var stopCmd = &cobra.Command{
|
||||||
Use: "stop",
|
Use: "stop",
|
||||||
Short: "A brief description of your command",
|
Short: "Stop a timer",
|
||||||
Long: `A longer description that spans multiple lines and likely contains examples
|
|
||||||
and usage of using your command. For example:
|
|
||||||
|
|
||||||
Cobra is a CLI library for Go that empowers applications.
|
|
||||||
This application is a tool to generate the needed files
|
|
||||||
to quickly create a Cobra application.`,
|
|
||||||
RunE: opStop,
|
RunE: opStop,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,6 +27,47 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func opStop(cmd *cobra.Command, args []string) error {
|
func opStop(cmd *cobra.Command, args []string) error {
|
||||||
fmt.Println("stop called")
|
var err error
|
||||||
|
p := cli.Program{}
|
||||||
|
err = p.Initialize()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = p.LoadTimerList(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var wrk time.Time
|
||||||
|
end := time.Now()
|
||||||
|
id := -1
|
||||||
|
|
||||||
|
if len(args) > 0 {
|
||||||
|
if wrk, err = util.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 p.TimerList.GetActiveTimers().GetTimerSlice() {
|
||||||
|
timerIds = append(timerIds, v.Id)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
timerIds = append(timerIds, id)
|
||||||
|
}
|
||||||
|
for _, v := range timerIds {
|
||||||
|
var stopped *timertxt.Timer
|
||||||
|
if stopped, err = p.TimerList.GetTimer(v); err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
stopped.FinishDate = end
|
||||||
|
stopped.Finished = true
|
||||||
|
fmt.Println("Stopped Timer:", util.TimerToFriendlyString(stopped))
|
||||||
|
}
|
||||||
|
if err = p.WriteTimerList(); err != nil {
|
||||||
|
return fmt.Errorf("Error writing timer list: %w", err)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,20 +6,18 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.bullercodeworks.com/brian/gime/cli"
|
||||||
|
"git.bullercodeworks.com/brian/gime/util"
|
||||||
|
"git.bullercodeworks.com/brian/go-timertxt"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
// switchCmd represents the switch command
|
// switchCmd represents the switch command
|
||||||
var switchCmd = &cobra.Command{
|
var switchCmd = &cobra.Command{
|
||||||
Use: "switch",
|
Use: "switch",
|
||||||
Short: "A brief description of your command",
|
Short: "Stop the current timer and start a new one copying the last one's parameters",
|
||||||
Long: `A longer description that spans multiple lines and likely contains examples
|
|
||||||
and usage of using your command. For example:
|
|
||||||
|
|
||||||
Cobra is a CLI library for Go that empowers applications.
|
|
||||||
This application is a tool to generate the needed files
|
|
||||||
to quickly create a Cobra application.`,
|
|
||||||
RunE: opSwitch,
|
RunE: opSwitch,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,6 +26,31 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func opSwitch(cmd *cobra.Command, args []string) error {
|
func opSwitch(cmd *cobra.Command, args []string) error {
|
||||||
fmt.Println("switch called")
|
var err error
|
||||||
return nil
|
p := cli.Program{}
|
||||||
|
err = p.Initialize()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = p.LoadTimerList(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var timerIds []int
|
||||||
|
end := time.Now()
|
||||||
|
// Stop all running timers and start a new one with the given args
|
||||||
|
for _, v := range p.TimerList.GetActiveTimers().GetTimerSlice() {
|
||||||
|
timerIds = append(timerIds, v.Id)
|
||||||
|
}
|
||||||
|
fmt.Print("Stopping ", timerIds, "\n")
|
||||||
|
for _, v := range timerIds {
|
||||||
|
var stopped *timertxt.Timer
|
||||||
|
if stopped, err = p.TimerList.GetTimer(v); err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
stopped.FinishDate = end
|
||||||
|
stopped.Finished = true
|
||||||
|
fmt.Println("Stopped Timer:", util.TimerToFriendlyString(stopped))
|
||||||
|
}
|
||||||
|
return opStart(cmd, args)
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ func opShowTimers(cmd *cobra.Command, args []string) error {
|
||||||
list := p.GetFilteredTimerList(args)
|
list := p.GetFilteredTimerList(args)
|
||||||
var isActive bool
|
var isActive bool
|
||||||
var total time.Duration
|
var total time.Duration
|
||||||
for _, v := range *list {
|
for _, v := range list.GetTimerSlice() {
|
||||||
dur := v.FinishDate.Sub(v.StartDate)
|
dur := v.FinishDate.Sub(v.StartDate)
|
||||||
if v.FinishDate.IsZero() {
|
if v.FinishDate.IsZero() {
|
||||||
dur = time.Now().Sub(v.StartDate)
|
dur = time.Now().Sub(v.StartDate)
|
||||||
|
@ -45,7 +45,7 @@ func opShowTimers(cmd *cobra.Command, args []string) error {
|
||||||
}
|
}
|
||||||
total += dur
|
total += dur
|
||||||
}
|
}
|
||||||
total = total.Round(util.GetRoundToDuration())
|
total = util.Round(total)
|
||||||
if isActive {
|
if isActive {
|
||||||
fmt.Printf("%.2f+\n", util.DurationToDecimal(total))
|
fmt.Printf("%.2f+\n", util.DurationToDecimal(total))
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -7,19 +7,15 @@ package cmd
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"git.bullercodeworks.com/brian/gime/cli"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
// toggleCmd represents the toggle command
|
// toggleCmd represents the toggle command
|
||||||
var toggleCmd = &cobra.Command{
|
var toggleCmd = &cobra.Command{
|
||||||
Use: "toggle",
|
Use: "toggle",
|
||||||
Short: "A brief description of your command",
|
Short: "Toggle the most recent timer on and off",
|
||||||
Long: `A longer description that spans multiple lines and likely contains examples
|
|
||||||
and usage of using your command. For example:
|
|
||||||
|
|
||||||
Cobra is a CLI library for Go that empowers applications.
|
|
||||||
This application is a tool to generate the needed files
|
|
||||||
to quickly create a Cobra application.`,
|
|
||||||
RunE: opToggle,
|
RunE: opToggle,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,6 +24,37 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func opToggle(cmd *cobra.Command, args []string) error {
|
func opToggle(cmd *cobra.Command, args []string) error {
|
||||||
fmt.Println("toggle called")
|
var err error
|
||||||
|
p := cli.Program{}
|
||||||
|
err = p.Initialize()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = p.LoadTimerList(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
wrk, err := p.GetMostRecentTimer()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Print("{\"icon\":\"time\",\"state\":\"Critical\", \"text\": \"Error loading timer entry\"}")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
var startArgs []string
|
||||||
|
if wrk.Finished {
|
||||||
|
// Start a new timer with the same data
|
||||||
|
for _, v := range wrk.Contexts {
|
||||||
|
startArgs = append(startArgs, "@"+v)
|
||||||
|
}
|
||||||
|
for _, v := range wrk.Projects {
|
||||||
|
startArgs = append(startArgs, "+"+v)
|
||||||
|
}
|
||||||
|
if viper.GetBool("copytags") {
|
||||||
|
for k, v := range wrk.AdditionalTags {
|
||||||
|
startArgs = append(startArgs, k+":"+v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return opStart(cmd, startArgs)
|
||||||
|
} else {
|
||||||
|
// Stop the active timer
|
||||||
|
return opStop(cmd, []string{})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,8 +4,8 @@ Copyright © 2022 Brian Buller <brian@bullercodeworks.com>
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"git.bullercodeworks.com/brian/gime/cli"
|
||||||
|
"git.bullercodeworks.com/brian/gime/ui"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -21,6 +21,6 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func opUi(cmd *cobra.Command, args []string) error {
|
func opUi(cmd *cobra.Command, args []string) error {
|
||||||
fmt.Println("ui called")
|
p := &cli.Program{}
|
||||||
return nil
|
return ui.RunTUI(p)
|
||||||
}
|
}
|
||||||
|
|
10
go.mod
10
go.mod
|
@ -1,11 +1,17 @@
|
||||||
module git.bullercodeworks.com/brian/gime
|
module git.bullercodeworks.com/brian/gime
|
||||||
|
|
||||||
go 1.17
|
go 1.20
|
||||||
|
|
||||||
replace git.bullercodeworks.com/brian/go-timertxt => /home/brbuller/Development/go/src/git.bullercodeworks.com/brian/go-timertxt
|
replace git.bullercodeworks.com/brian/go-timertxt => /home/brbuller/Development/go/src/git.bullercodeworks.com/brian/go-timertxt
|
||||||
|
|
||||||
|
replace git.bullercodeworks.com/brian/wandle => /home/brbuller/Development/go/src/git.bullercodeworks.com/brian/wandle
|
||||||
|
|
||||||
|
replace git.bullercodeworks.com/brian/widdles => /home/brbuller/Development/go/src/git.bullercodeworks.com/brian/widdles
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.bullercodeworks.com/brian/go-timertxt v0.0.0-20210302170637-d35b67037e23
|
git.bullercodeworks.com/brian/go-timertxt v1.5.0
|
||||||
|
git.bullercodeworks.com/brian/wandle v1.0.3
|
||||||
|
git.bullercodeworks.com/brian/widdles v0.0.0-00010101000000-000000000000
|
||||||
github.com/br0xen/termbox-util v0.0.0-20200220160819-dc6d6950ba00
|
github.com/br0xen/termbox-util v0.0.0-20200220160819-dc6d6950ba00
|
||||||
github.com/br0xen/user-config v0.0.0-20170914134719-16e743ec93a2
|
github.com/br0xen/user-config v0.0.0-20170914134719-16e743ec93a2
|
||||||
github.com/muesli/go-app-paths v0.2.2
|
github.com/muesli/go-app-paths v0.2.2
|
||||||
|
|
7
main.go
7
main.go
|
@ -1,11 +1,16 @@
|
||||||
/*
|
/*
|
||||||
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
|
Copyright © 2022 Brian Buller <brian@bullercodeworks.com>
|
||||||
|
|
||||||
*/
|
*/
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import "git.bullercodeworks.com/brian/gime/cmd"
|
import "git.bullercodeworks.com/brian/gime/cmd"
|
||||||
|
|
||||||
|
var (
|
||||||
|
VERSION string
|
||||||
|
BUILD string
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
cmd.Execute()
|
cmd.Execute()
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,583 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.bullercodeworks.com/brian/gime/util"
|
||||||
|
"git.bullercodeworks.com/brian/go-timertxt"
|
||||||
|
"git.bullercodeworks.com/brian/wandle"
|
||||||
|
"git.bullercodeworks.com/brian/widdles"
|
||||||
|
"github.com/nsf/termbox-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
activeToggleActive = iota
|
||||||
|
activeToggleInactive
|
||||||
|
activeToggleAll
|
||||||
|
activeToggleErr
|
||||||
|
)
|
||||||
|
|
||||||
|
type listTimersScreen struct {
|
||||||
|
ui *Ui
|
||||||
|
|
||||||
|
initialized bool
|
||||||
|
menu *widdles.TopMenu
|
||||||
|
scrollbar *widdles.Scrollbar
|
||||||
|
|
||||||
|
cursor int
|
||||||
|
|
||||||
|
activeToggle int
|
||||||
|
fullList *timertxt.TimerList
|
||||||
|
timerList *timertxt.TimerList
|
||||||
|
doneList *timertxt.TimerList
|
||||||
|
|
||||||
|
fullFilterList *timertxt.TimerList
|
||||||
|
timerFilterList *timertxt.TimerList
|
||||||
|
doneFilterList *timertxt.TimerList
|
||||||
|
|
||||||
|
selected map[int]bool
|
||||||
|
|
||||||
|
confirm *widdles.ConfirmDialog
|
||||||
|
filter string
|
||||||
|
|
||||||
|
choiceMenu *widdles.MenuV
|
||||||
|
tagEditor *PromptForTagWiddle
|
||||||
|
//partManager *PartManager
|
||||||
|
|
||||||
|
msg string
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListTimersMsg ScreenMsg
|
||||||
|
|
||||||
|
func NewListTimersMsg(data interface{}, err error) ListTimersMsg {
|
||||||
|
return ListTimersMsg{
|
||||||
|
source: ListTimersId,
|
||||||
|
command: CmdArchiveTimer,
|
||||||
|
data: data,
|
||||||
|
err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewListTimersScreen(u *Ui) *listTimersScreen {
|
||||||
|
s := listTimersScreen{
|
||||||
|
ui: u,
|
||||||
|
menu: widdles.NewTopMenu(0, 0, 0),
|
||||||
|
scrollbar: widdles.NewScrollbar(0, 0, 0, 0),
|
||||||
|
selected: make(map[int]bool),
|
||||||
|
confirm: widdles.NewConfirmDialog("", ""),
|
||||||
|
|
||||||
|
choiceMenu: widdles.NewMenuV(0, 0, 0, 0),
|
||||||
|
tagEditor: NewPromptForTagWiddle(0, 0, widdles.AUTO_SIZE, widdles.AUTO_SIZE, "", ""),
|
||||||
|
}
|
||||||
|
return &s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *listTimersScreen) Init() wandle.Cmd {
|
||||||
|
if s.initialized {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
s.initialized = true
|
||||||
|
// Set up the top menu
|
||||||
|
fileMenu := s.menu.NewSubMenu("File")
|
||||||
|
settingsOption := widdles.NewMenuItem("Settings")
|
||||||
|
settingsOption.SetCommand(s.ui.GotoScreen(SettingsId))
|
||||||
|
fileMenu.AddOption(settingsOption)
|
||||||
|
quitOption := widdles.NewMenuItem("Quit")
|
||||||
|
quitOption.SetHotkey(widdles.NewHotkey(termbox.KeyCtrlC))
|
||||||
|
quitOption.SetCommand(func() wandle.Msg { return wandle.Quit() })
|
||||||
|
fileMenu.AddOption(quitOption)
|
||||||
|
s.menu.Measure()
|
||||||
|
// Timer Lists
|
||||||
|
s.timerList, s.doneList = s.ui.program.TimerList, s.ui.program.DoneList
|
||||||
|
s.fullList = timertxt.NewTimerList()
|
||||||
|
s.fullList.AddTimers(s.timerList.GetTimerSlice())
|
||||||
|
s.fullList.AddTimers(s.doneList.GetTimerSlice())
|
||||||
|
s.timerFilterList, s.doneFilterList = s.timerList, s.doneList
|
||||||
|
s.timerFilterList.Sort(timertxt.SortStartDateDesc)
|
||||||
|
s.doneFilterList.Sort(timertxt.SortStartDateDesc)
|
||||||
|
s.updateFullFilterList()
|
||||||
|
w, h := termbox.Size()
|
||||||
|
s.choiceMenu.SetBorder(wandle.BRD_CSIMPLE)
|
||||||
|
s.choiceMenu.SetX((w / 2) - 7)
|
||||||
|
s.choiceMenu.SetY((h / 2) - 7)
|
||||||
|
s.choiceMenu.SetWidth(widdles.AUTO_SIZE)
|
||||||
|
s.choiceMenu.SetHeight(widdles.AUTO_SIZE)
|
||||||
|
s.choiceMenu.SetPadding(0, 1, 0, 1)
|
||||||
|
s.tagEditor.SetX(w / 4)
|
||||||
|
s.tagEditor.SetY(h / 4)
|
||||||
|
s.tagEditor.SetWidth(w / 2)
|
||||||
|
s.tagEditor.SetHeight(h / 2)
|
||||||
|
s.confirm.EnableHotkeys()
|
||||||
|
s.confirm.SetX(w / 4)
|
||||||
|
s.confirm.SetY(h / 4)
|
||||||
|
s.confirm.SetWidth(w / 2)
|
||||||
|
s.confirm.SetHeight(h / 2)
|
||||||
|
s.updateFullFilterList()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *listTimersScreen) Update(msg wandle.Msg) wandle.Cmd {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case ScreenMsg:
|
||||||
|
s.err = msg.err
|
||||||
|
case termbox.Event:
|
||||||
|
return s.handleTermboxEvent(msg)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *listTimersScreen) View(style wandle.Style) {
|
||||||
|
_, h := termbox.Size()
|
||||||
|
y := 2
|
||||||
|
printedTimers := 0
|
||||||
|
if s.activeToggle == activeToggleAll || s.activeToggle == activeToggleActive {
|
||||||
|
wandle.Print(1, y, style.Bold(true), "Active Timers")
|
||||||
|
y++
|
||||||
|
for idx, tmr := range s.timerFilterList.GetTimerSlice() {
|
||||||
|
if y > h-2 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
st := style
|
||||||
|
if s.cursor == idx {
|
||||||
|
st = st.Invert()
|
||||||
|
}
|
||||||
|
if s.selected[idx] {
|
||||||
|
wandle.Print(1, y, st, "[✔] ")
|
||||||
|
} else {
|
||||||
|
wandle.Print(1, y, st, "[ ] ")
|
||||||
|
}
|
||||||
|
s.ViewTimer(5, y, st, tmr)
|
||||||
|
y++
|
||||||
|
}
|
||||||
|
y++
|
||||||
|
printedTimers = s.timerFilterList.Size()
|
||||||
|
}
|
||||||
|
if s.activeToggle == activeToggleAll || s.activeToggle == activeToggleInactive {
|
||||||
|
wandle.Print(1, y, style.Bold(true), "Done Timers")
|
||||||
|
y++
|
||||||
|
for idx, tmr := range s.doneFilterList.GetTimerSlice() {
|
||||||
|
if y > h-3 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
st := style
|
||||||
|
if s.cursor == printedTimers+idx {
|
||||||
|
st = st.Invert()
|
||||||
|
}
|
||||||
|
if s.selected[printedTimers+idx] {
|
||||||
|
wandle.Print(1, y, st, "[✔] ")
|
||||||
|
} else {
|
||||||
|
wandle.Print(1, y, st, "[ ] ")
|
||||||
|
}
|
||||||
|
s.ViewTimer(5, y, st, tmr)
|
||||||
|
y++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selectedStatus := fmt.Sprintf("%s", s.getSelectedTimerDuration())
|
||||||
|
if len(s.selected) > 0 {
|
||||||
|
selectedStatus = fmt.Sprintf("%s (%d / %d selected)", selectedStatus, len(s.selected), s.fullFilterList.Size())
|
||||||
|
}
|
||||||
|
wandle.Print(1, h-2, style, selectedStatus)
|
||||||
|
var archiveText string
|
||||||
|
if s.areSelectedInSameList() {
|
||||||
|
if s.areSelectedInDoneList() {
|
||||||
|
archiveText = "Un[A]rchive Selected, "
|
||||||
|
} else {
|
||||||
|
archiveText = "[A]rchive Selected, "
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
archiveText = "Not in Same List"
|
||||||
|
}
|
||||||
|
help := fmt.Sprintf("[T]oggle Display, [p]roject(+), [c]ontext(@), [t]ags(:), %s[Ctrl+A]: Select All/None, [Ctrl+I]: Invert Selection", archiveText)
|
||||||
|
wandle.Print(1, h-1, style, help)
|
||||||
|
|
||||||
|
s.scrollbar.View(style)
|
||||||
|
if s.menu.IsActive() {
|
||||||
|
s.menu.View(style)
|
||||||
|
}
|
||||||
|
if s.choiceMenu.IsActive() {
|
||||||
|
s.choiceMenu.View(style)
|
||||||
|
}
|
||||||
|
if s.tagEditor.IsActive() {
|
||||||
|
s.tagEditor.View(style)
|
||||||
|
}
|
||||||
|
if s.confirm.IsActive() {
|
||||||
|
s.confirm.View(style)
|
||||||
|
}
|
||||||
|
wandle.Print(1, h-3, style, s.msg)
|
||||||
|
if s.err != nil {
|
||||||
|
wandle.Print(1, h-4, ErrStyle, s.err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *listTimersScreen) ViewTimer(x, y int, style wandle.Style, tmr *timertxt.Timer) {
|
||||||
|
var tags []string
|
||||||
|
for _, k := range util.SortedTagKeyList(tmr.AdditionalTags) {
|
||||||
|
tags = append(tags, fmt.Sprintf("%s:%s", k, tmr.AdditionalTags[k]))
|
||||||
|
}
|
||||||
|
wandle.Print(x, y, style, fmt.Sprintf("%s %s %s %s %s", tmr.StartDate.Format(time.Stamp), tmr.Duration(), tmr.Contexts, tmr.Projects, strings.Join(tags, "; ")))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *listTimersScreen) handleTermboxEvent(msg termbox.Event) wandle.Cmd {
|
||||||
|
if s.confirm.IsActive() {
|
||||||
|
return s.confirm.Update(msg)
|
||||||
|
}
|
||||||
|
if s.choiceMenu.IsActive() {
|
||||||
|
if msg.Type == termbox.EventKey && msg.Key == termbox.KeyEsc {
|
||||||
|
s.choiceMenu.SetActive(false)
|
||||||
|
return wandle.EmptyCmd
|
||||||
|
} else {
|
||||||
|
return s.choiceMenu.Update(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s.tagEditor.IsActive() {
|
||||||
|
return s.tagEditor.Update(msg)
|
||||||
|
}
|
||||||
|
if (msg.Type == termbox.EventKey && msg.Key == termbox.KeyEsc) || s.menu.IsActive() {
|
||||||
|
return s.menu.Update(msg)
|
||||||
|
}
|
||||||
|
switch msg.Type {
|
||||||
|
case termbox.EventKey:
|
||||||
|
if msg.Key == termbox.KeyEnter {
|
||||||
|
// TODO: Edit the entry
|
||||||
|
/*
|
||||||
|
if s.cursor >= 0 && s.cursor < s.timerFilterList.Size()+s.doneFilterList.Size() {
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
} else if msg.Key == termbox.KeySpace {
|
||||||
|
// (un)Select the entry
|
||||||
|
if v := s.selected[s.cursor]; v {
|
||||||
|
delete(s.selected, s.cursor)
|
||||||
|
} else {
|
||||||
|
s.selected[s.cursor] = true
|
||||||
|
}
|
||||||
|
if s.cursor < s.fullFilterList.Size()-1 {
|
||||||
|
s.cursor++
|
||||||
|
}
|
||||||
|
} else if msg.Ch == 'T' {
|
||||||
|
s.activeToggle = (s.activeToggle + 1) % activeToggleErr
|
||||||
|
s.updateFullFilterList()
|
||||||
|
} else if msg.Ch == 'A' {
|
||||||
|
return s.showArchiveSelected()
|
||||||
|
} else if msg.Key == termbox.KeyArrowUp || msg.Ch == 'k' {
|
||||||
|
if s.cursor > 0 {
|
||||||
|
s.cursor--
|
||||||
|
} else {
|
||||||
|
s.cursor = 0
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
} else if msg.Key == termbox.KeyArrowDown || msg.Ch == 'j' {
|
||||||
|
if s.cursor < s.fullFilterList.Size()-1 {
|
||||||
|
s.cursor++
|
||||||
|
} else {
|
||||||
|
s.cursor = s.fullFilterList.Size() - 1
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
} else if msg.Ch == 'G' {
|
||||||
|
s.cursor = s.fullFilterList.Size() - 1
|
||||||
|
} else if msg.Ch == 'g' {
|
||||||
|
s.cursor = 0
|
||||||
|
} else if msg.Ch == 't' {
|
||||||
|
// Edit tag(s)
|
||||||
|
return s.showEditTagsChoice()
|
||||||
|
} else if msg.Ch == 'p' {
|
||||||
|
// Edit project(s)
|
||||||
|
// TODO: Prompt for Choice: Add/Edit/Remove
|
||||||
|
projs := s.fullList.GetProjects()
|
||||||
|
_ = projs
|
||||||
|
} else if msg.Ch == 'c' {
|
||||||
|
// Edit context(s)
|
||||||
|
// TODO: Prompt for choice: Add/Edit/Remove
|
||||||
|
ctxts := s.fullList.GetContexts()
|
||||||
|
_ = ctxts
|
||||||
|
} else if msg.Key == termbox.KeyCtrlA {
|
||||||
|
if len(s.selected) != s.fullFilterList.Size() {
|
||||||
|
// Select None
|
||||||
|
for k := range s.selected {
|
||||||
|
delete(s.selected, k)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Select All
|
||||||
|
for i := 0; i < s.fullFilterList.Size(); i++ {
|
||||||
|
s.selected[i] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := 0; i < s.fullFilterList.Size(); i++ {
|
||||||
|
if v := s.selected[i]; v {
|
||||||
|
delete(s.selected, i)
|
||||||
|
} else {
|
||||||
|
s.selected[i] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if msg.Key == termbox.KeyCtrlI {
|
||||||
|
for i := 0; i < s.fullFilterList.Size(); i++ {
|
||||||
|
if v := s.selected[i]; v {
|
||||||
|
delete(s.selected, i)
|
||||||
|
} else {
|
||||||
|
s.selected[i] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *listTimersScreen) showArchiveSelected() wandle.Cmd {
|
||||||
|
return func() wandle.Msg {
|
||||||
|
if len(s.selected) > 0 {
|
||||||
|
s.confirm.SetTitle(fmt.Sprintf("Archive %d Timers?", len(s.selected)))
|
||||||
|
s.confirm.SetMessage("Are you sure you want to archive these timers? (y/n)")
|
||||||
|
} else {
|
||||||
|
s.confirm.SetTitle("Archive Timer?")
|
||||||
|
s.confirm.SetMessage("Are you sure you want to archive this timer? (y/n)")
|
||||||
|
}
|
||||||
|
s.confirm.SetOkCommand(func() wandle.Msg {
|
||||||
|
s.confirm.SetVisible(false)
|
||||||
|
return s.doArchiveSelected()
|
||||||
|
})
|
||||||
|
s.confirm.SetCancelCommand(func() wandle.Msg {
|
||||||
|
s.confirm.SetVisible(false)
|
||||||
|
return wandle.EmptyCmd
|
||||||
|
})
|
||||||
|
s.confirm.SetVisible(true)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (s *listTimersScreen) doArchiveSelected() wandle.Cmd {
|
||||||
|
archiveTimer := func(t *timertxt.Timer) error {
|
||||||
|
if remErr := s.timerList.RemoveTimer(*t); remErr != nil {
|
||||||
|
return remErr
|
||||||
|
}
|
||||||
|
s.doneList.AddTimer(t)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
selected := len(s.selected)
|
||||||
|
if selected == 0 {
|
||||||
|
if s.cursor < s.fullFilterList.Size() {
|
||||||
|
var selTimer *timertxt.Timer
|
||||||
|
if selTimer, s.err = s.fullFilterList.GetTimer(s.cursor); s.err == nil {
|
||||||
|
if archErr := archiveTimer(selTimer); archErr != nil {
|
||||||
|
s.err = archErr
|
||||||
|
return wandle.EmptyCmd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for i := range s.selected {
|
||||||
|
if tmr, err := s.fullFilterList.GetTimer(i); err == nil {
|
||||||
|
if err := archiveTimer(tmr); err != nil {
|
||||||
|
s.err = err
|
||||||
|
return wandle.EmptyCmd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return wandle.EmptyCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *listTimersScreen) showEditTagsChoice() wandle.Cmd {
|
||||||
|
tags := s.getSelectedTimerTags()
|
||||||
|
var showTagEditor = func(key, val string, multival bool) wandle.Cmd {
|
||||||
|
return func() wandle.Msg {
|
||||||
|
s.tagEditor.SetTag(key, val)
|
||||||
|
s.tagEditor.SetDoneCommand(func() wandle.Msg {
|
||||||
|
s.updateTagOnSelectedTimers(s.tagEditor.GetTag())
|
||||||
|
s.tagEditor.Done()
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
s.tagEditor.SetCancelCommand(func() wandle.Msg {
|
||||||
|
s.tagEditor.SetActive(false)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
s.choiceMenu.SetActive(false)
|
||||||
|
s.tagEditor.SetActive(true)
|
||||||
|
s.tagEditor.SetMultiVal(multival)
|
||||||
|
return wandle.EmptyCmd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.choiceMenu.SetTitle("")
|
||||||
|
s.choiceMenu.ClearOptions()
|
||||||
|
addTag := widdles.NewMenuItem("[A]dd New Tag")
|
||||||
|
addTag.SetHotkey(widdles.NewHotkeyCh('a'))
|
||||||
|
addTag.SetCommand(showTagEditor("", "", false))
|
||||||
|
s.choiceMenu.AddOption(addTag)
|
||||||
|
editTag := widdles.NewMenuItem("[E]dit Tag")
|
||||||
|
editTag.SetHotkey(widdles.NewHotkeyCh('e'))
|
||||||
|
editTag.SetEnabled(len(tags) > 0)
|
||||||
|
editTag.SetCommand(func() wandle.Msg {
|
||||||
|
s.choiceMenu.ClearOptions()
|
||||||
|
s.choiceMenu.SetTitle("Choose Tag to Edit")
|
||||||
|
for k, v := range tags {
|
||||||
|
var vals string
|
||||||
|
var multival bool
|
||||||
|
if len(v) == 1 {
|
||||||
|
vals = v[0]
|
||||||
|
} else {
|
||||||
|
vals = ""
|
||||||
|
multival = true
|
||||||
|
}
|
||||||
|
opt := widdles.NewMenuItem(fmt.Sprintf("%s (%s)", k, vals))
|
||||||
|
opt.SetCommand(showTagEditor(k, vals, multival))
|
||||||
|
s.choiceMenu.AddOption(opt)
|
||||||
|
}
|
||||||
|
return wandle.EmptyCmd
|
||||||
|
})
|
||||||
|
s.choiceMenu.AddOption(editTag)
|
||||||
|
removeTag := widdles.NewMenuItem("[R]emove Tag")
|
||||||
|
removeTag.SetHotkey(widdles.NewHotkeyCh('r'))
|
||||||
|
removeTag.SetCommand(func() wandle.Msg {
|
||||||
|
s.choiceMenu.ClearOptions()
|
||||||
|
s.choiceMenu.SetTitle("Choose Tag to Remove")
|
||||||
|
for k, v := range tags {
|
||||||
|
opt := widdles.NewMenuItem(fmt.Sprintf("%s: %s", k, v))
|
||||||
|
opt.SetCommand(func() wandle.Msg {
|
||||||
|
s.removeTagOnSelectedTimers(k)
|
||||||
|
return wandle.EmptyCmd
|
||||||
|
})
|
||||||
|
s.choiceMenu.AddOption(opt)
|
||||||
|
}
|
||||||
|
s.choiceMenu.SetActive(true)
|
||||||
|
return wandle.EmptyCmd
|
||||||
|
})
|
||||||
|
s.choiceMenu.AddOption(removeTag)
|
||||||
|
s.choiceMenu.SetActive(true)
|
||||||
|
return wandle.EmptyCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *listTimersScreen) updateFullFilterList() {
|
||||||
|
s.fullFilterList = timertxt.NewTimerList()
|
||||||
|
switch s.activeToggle {
|
||||||
|
case activeToggleAll:
|
||||||
|
s.fullFilterList.Combine(s.timerFilterList)
|
||||||
|
s.fullFilterList.Combine(s.doneFilterList)
|
||||||
|
case activeToggleActive:
|
||||||
|
s.fullFilterList.Combine(s.timerFilterList)
|
||||||
|
case activeToggleInactive:
|
||||||
|
s.fullFilterList.Combine(s.doneFilterList)
|
||||||
|
}
|
||||||
|
if s.cursor >= s.fullFilterList.Size() {
|
||||||
|
s.cursor = s.fullFilterList.Size() - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *listTimersScreen) gotoSettingsScreen() wandle.Msg {
|
||||||
|
return ScreenMsg{
|
||||||
|
source: ListTimersId,
|
||||||
|
command: CmdGotoSettings,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writes the lists through the program, putting errors in s.err
|
||||||
|
func (s *listTimersScreen) writeLists() {
|
||||||
|
var errText string
|
||||||
|
if err := s.ui.program.WriteLists(); err != nil {
|
||||||
|
errText = fmt.Sprintf("Errors Writing Lists (%v)", err)
|
||||||
|
}
|
||||||
|
if len(errText) > 0 {
|
||||||
|
s.err = errors.New(errText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (s *listTimersScreen) getSelectedTimers() []*timertxt.Timer {
|
||||||
|
var ret []*timertxt.Timer
|
||||||
|
selected := len(s.selected)
|
||||||
|
if selected == 0 {
|
||||||
|
if s.cursor < s.fullFilterList.Size() {
|
||||||
|
var selTimer *timertxt.Timer
|
||||||
|
if selTimer, s.err = s.fullFilterList.GetTimer(s.cursor); s.err == nil {
|
||||||
|
ret = append(ret, selTimer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for i := range s.selected {
|
||||||
|
if tmr, err := s.fullFilterList.GetTimer(i); err == nil {
|
||||||
|
ret = append(ret, tmr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
func (s *listTimersScreen) getSelectedTimerTags() map[string][]string {
|
||||||
|
ret := make(map[string][]string)
|
||||||
|
sel := s.getSelectedTimers()
|
||||||
|
for _, tmr := range sel {
|
||||||
|
for k, v := range tmr.AdditionalTags {
|
||||||
|
ret[k] = util.AppendStringIfDistinct(ret[k], v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
func (s *listTimersScreen) updateTagOnSelectedTimers(key, val string) {
|
||||||
|
sel := s.getSelectedTimers()
|
||||||
|
for _, tmr := range sel {
|
||||||
|
tmr.AdditionalTags[key] = val
|
||||||
|
}
|
||||||
|
s.writeLists()
|
||||||
|
}
|
||||||
|
func (s *listTimersScreen) removeTagOnSelectedTimers(key string) {
|
||||||
|
sel := s.getSelectedTimers()
|
||||||
|
for _, tmr := range sel {
|
||||||
|
if _, ok := tmr.AdditionalTags[key]; ok {
|
||||||
|
delete(tmr.AdditionalTags, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.writeLists()
|
||||||
|
}
|
||||||
|
func (s *listTimersScreen) getSelectedTimerProjects() []string {
|
||||||
|
var ret []string
|
||||||
|
sel := s.getSelectedTimers()
|
||||||
|
for _, tmr := range sel {
|
||||||
|
for _, v := range tmr.Contexts {
|
||||||
|
ret = util.AppendStringIfDistinct(ret, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
func (s *listTimersScreen) getSelectedTimerContexts() []string {
|
||||||
|
var ret []string
|
||||||
|
sel := s.getSelectedTimers()
|
||||||
|
for _, tmr := range sel {
|
||||||
|
for _, v := range tmr.Contexts {
|
||||||
|
ret = util.AppendStringIfDistinct(ret, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
func (s *listTimersScreen) getSelectedTimerDuration() time.Duration {
|
||||||
|
sel := s.getSelectedTimers()
|
||||||
|
var ret time.Duration
|
||||||
|
for _, tmr := range sel {
|
||||||
|
ret = util.AddDurations(ret, util.Round(tmr.Duration()))
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true if all selected timers are done
|
||||||
|
func (s *listTimersScreen) areSelectedInDoneList() bool {
|
||||||
|
sel := s.getSelectedTimers()
|
||||||
|
for i := range sel {
|
||||||
|
if s.timerList.Contains(sel[i]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return true if all selected timers are from the same list (file)
|
||||||
|
func (s *listTimersScreen) areSelectedInSameList() bool {
|
||||||
|
sel := s.getSelectedTimers()
|
||||||
|
var inActive, inDone int
|
||||||
|
for i := range sel {
|
||||||
|
if s.timerList.Contains(sel[i]) {
|
||||||
|
inActive++
|
||||||
|
}
|
||||||
|
if s.doneList.Contains(sel[i]) {
|
||||||
|
inDone++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return inActive == 0 || inDone == 0
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.bullercodeworks.com/brian/wandle"
|
||||||
|
"git.bullercodeworks.com/brian/widdles"
|
||||||
|
"github.com/nsf/termbox-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
type settingsScreen struct {
|
||||||
|
ui *Ui
|
||||||
|
initialized bool
|
||||||
|
menu *widdles.TopMenu
|
||||||
|
}
|
||||||
|
|
||||||
|
type SettingsMsg ScreenMsg
|
||||||
|
|
||||||
|
func NewSettingsScreen(u *Ui) *settingsScreen {
|
||||||
|
return &settingsScreen{
|
||||||
|
ui: u,
|
||||||
|
menu: widdles.NewTopMenu(0, 0, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *settingsScreen) Init() wandle.Cmd {
|
||||||
|
if s.initialized {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
s.initialized = true
|
||||||
|
// Set up the top menu
|
||||||
|
fileMenu := s.menu.NewSubMenu("File")
|
||||||
|
quitOption := widdles.NewMenuItem("Quit")
|
||||||
|
quitOption.SetHotkey(widdles.NewHotkey(termbox.KeyCtrlC))
|
||||||
|
quitOption.SetCommand(func() wandle.Msg { return wandle.Quit() })
|
||||||
|
fileMenu.AddOption(quitOption)
|
||||||
|
gotoMenu := s.menu.NewSubMenu("Goto")
|
||||||
|
timerListOption := widdles.NewMenuItem("Timer List")
|
||||||
|
timerListOption.SetCommand(s.ui.GotoScreen(ListTimersId))
|
||||||
|
gotoMenu.AddOption(timerListOption)
|
||||||
|
s.menu.Measure()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *settingsScreen) Update(msg wandle.Msg) wandle.Cmd {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case ScreenMsg:
|
||||||
|
case termbox.Event:
|
||||||
|
return s.handleTermboxEvent(msg)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *settingsScreen) View(style wandle.Style) {
|
||||||
|
wandle.Print(1, 1, style, "Settings")
|
||||||
|
if s.menu.IsActive() {
|
||||||
|
s.menu.View(style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *settingsScreen) handleTermboxEvent(msg termbox.Event) wandle.Cmd {
|
||||||
|
if (msg.Type == termbox.EventKey && msg.Key == termbox.KeyEsc) || s.menu.IsActive() {
|
||||||
|
return s.menu.Update(msg)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *settingsScreen) gotoTimerList() wandle.Msg {
|
||||||
|
return ScreenMsg{
|
||||||
|
source: SettingsId,
|
||||||
|
command: CmdGotoTimerList,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,140 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.bullercodeworks.com/brian/gime/cli"
|
||||||
|
"git.bullercodeworks.com/brian/wandle"
|
||||||
|
"github.com/nsf/termbox-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Screen/Source Ids
|
||||||
|
const (
|
||||||
|
ListTimersId = ScreenMsgSource(iota << 8)
|
||||||
|
SettingsId
|
||||||
|
ErrorId
|
||||||
|
)
|
||||||
|
|
||||||
|
// Commands
|
||||||
|
const (
|
||||||
|
CmdCanceled = ScreenMsgCommand(iota)
|
||||||
|
CmdSaved
|
||||||
|
|
||||||
|
// ListTimers Commands
|
||||||
|
CmdArchiveTimer
|
||||||
|
|
||||||
|
// Goto Screen Commands
|
||||||
|
CmdGotoSettings
|
||||||
|
CmdGotoTimerList
|
||||||
|
)
|
||||||
|
|
||||||
|
// Styles
|
||||||
|
var (
|
||||||
|
DefaultStyle = wandle.NewStyle(
|
||||||
|
termbox.RGBToAttribute(uint8(0), uint8(255), uint8(0)),
|
||||||
|
termbox.RGBToAttribute(uint8(0), uint8(0), uint8(0)),
|
||||||
|
)
|
||||||
|
ErrStyle = wandle.NewStyle(
|
||||||
|
termbox.RGBToAttribute(uint8(255), uint8(0), uint8(0)),
|
||||||
|
termbox.RGBToAttribute(uint8(0), uint8(0), uint8(0)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
func RunTUI(p *cli.Program) error {
|
||||||
|
ui := NewUi(p)
|
||||||
|
ui.debug = true
|
||||||
|
if err := ui.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Exiting
|
||||||
|
fmt.Printf("Done\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Ui struct {
|
||||||
|
debug bool
|
||||||
|
wandle *wandle.Program
|
||||||
|
program *cli.Program
|
||||||
|
err error
|
||||||
|
|
||||||
|
screens map[ScreenMsgSource]wandle.Screen
|
||||||
|
|
||||||
|
prevScreen, currScreen ScreenMsgSource
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUi(p *cli.Program) *Ui {
|
||||||
|
ui := &Ui{
|
||||||
|
screens: make(map[ScreenMsgSource]wandle.Screen),
|
||||||
|
program: p,
|
||||||
|
err: p.Initialize(),
|
||||||
|
}
|
||||||
|
var s wandle.Screen
|
||||||
|
var sId ScreenMsgSource
|
||||||
|
if ui.err != nil {
|
||||||
|
s, sId = NewSettingsScreen(ui), SettingsId
|
||||||
|
} else {
|
||||||
|
if ui.err = ui.program.LoadTimerList(); ui.err != nil {
|
||||||
|
s, sId = NewSettingsScreen(ui), SettingsId
|
||||||
|
} else if ui.err = ui.program.LoadDoneList(); ui.err != nil {
|
||||||
|
s, sId = NewSettingsScreen(ui), SettingsId
|
||||||
|
} else {
|
||||||
|
s, sId = NewListTimersScreen(ui), ListTimersId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ui.screens[sId] = s
|
||||||
|
ui.currScreen = sId
|
||||||
|
ui.wandle = wandle.NewProgram(s)
|
||||||
|
ui.wandle.Style(DefaultStyle)
|
||||||
|
return ui
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *Ui) GotoScreen(id ScreenMsgSource) func() wandle.Msg {
|
||||||
|
u.prevScreen, u.currScreen = u.currScreen, id
|
||||||
|
if s, ok := u.screens[id]; ok {
|
||||||
|
return wandle.SwitchScreenCmd(s)
|
||||||
|
}
|
||||||
|
var s wandle.Screen
|
||||||
|
switch id {
|
||||||
|
case ListTimersId:
|
||||||
|
s = NewListTimersScreen(u)
|
||||||
|
case SettingsId:
|
||||||
|
s = NewSettingsScreen(u)
|
||||||
|
}
|
||||||
|
u.screens[id] = s
|
||||||
|
return wandle.SwitchScreenCmd(s)
|
||||||
|
}
|
||||||
|
func (u *Ui) CanGoBack() bool {
|
||||||
|
return u.prevScreen > 0
|
||||||
|
}
|
||||||
|
func (u *Ui) GoBack() func() wandle.Msg {
|
||||||
|
if !u.CanGoBack() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return u.GotoScreen(u.prevScreen)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *Ui) Log(v string) error {
|
||||||
|
f, err := os.OpenFile("gime.log", os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
_, err = f.WriteString(fmt.Sprintf("%s: %s\n", time.Now().Format(time.RFC3339), v))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *Ui) Start() error {
|
||||||
|
return u.wandle.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScreenMsg struct {
|
||||||
|
source ScreenMsgSource
|
||||||
|
command ScreenMsgCommand
|
||||||
|
data interface{}
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScreenMsgSource int
|
||||||
|
type ScreenMsgCommand int
|
|
@ -0,0 +1,223 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.bullercodeworks.com/brian/wandle"
|
||||||
|
"git.bullercodeworks.com/brian/widdles"
|
||||||
|
"github.com/nsf/termbox-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A widdle to prompt the user for a tag key and value
|
||||||
|
*/
|
||||||
|
type PromptForTagWiddle struct {
|
||||||
|
active bool
|
||||||
|
x, y, w, h int
|
||||||
|
|
||||||
|
origKey, origVal string
|
||||||
|
|
||||||
|
keyInput *widdles.ToggleField
|
||||||
|
valInput *widdles.ToggleField
|
||||||
|
|
||||||
|
cancelButton *widdles.Button
|
||||||
|
doneButton *widdles.Button
|
||||||
|
|
||||||
|
multipleValues bool
|
||||||
|
|
||||||
|
msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ widdles.Widdle = (*PromptForTagWiddle)(nil)
|
||||||
|
|
||||||
|
func NewPromptForTagWiddle(x, y, w, h int, key, val string) *PromptForTagWiddle {
|
||||||
|
keyInp := widdles.NewToggleField("Key", key, 0, 0, 0, 0)
|
||||||
|
keyInp.SetActive(true)
|
||||||
|
return &PromptForTagWiddle{
|
||||||
|
x: x, y: y, w: w, h: h,
|
||||||
|
origKey: key, origVal: val,
|
||||||
|
keyInput: keyInp,
|
||||||
|
valInput: widdles.NewToggleField("Value", val, 0, 0, 0, 0),
|
||||||
|
cancelButton: widdles.NewButton("Cancel", 0, 0, 0, 0),
|
||||||
|
doneButton: widdles.NewButton("Done", 0, 0, 0, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *PromptForTagWiddle) Init() wandle.Cmd {
|
||||||
|
return func() wandle.Msg {
|
||||||
|
w.Measure()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (w *PromptForTagWiddle) Update(msg wandle.Msg) wandle.Cmd {
|
||||||
|
if w.active {
|
||||||
|
// Make sure a widdle is active
|
||||||
|
var found bool
|
||||||
|
for _, wdl := range []widdles.Widdle{w.keyInput, w.valInput, w.cancelButton, w.doneButton} {
|
||||||
|
if wdl.IsActive() {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
w.keyInput.SetActive(true)
|
||||||
|
}
|
||||||
|
if msg, ok := msg.(termbox.Event); ok {
|
||||||
|
return w.handleTermboxEvent(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (w *PromptForTagWiddle) View(style wandle.Style) {
|
||||||
|
title := "Add Tag"
|
||||||
|
if w.origKey != "" {
|
||||||
|
title = "Edit Tag"
|
||||||
|
}
|
||||||
|
wandle.TitledBorderFilled(title, w.x, w.y, w.x+w.w, w.y+w.h, style, wandle.BRD_CSIMPLE)
|
||||||
|
w.keyInput.View(style)
|
||||||
|
w.valInput.View(style)
|
||||||
|
if w.multipleValues {
|
||||||
|
red := wandle.NewStyle(
|
||||||
|
termbox.RGBToAttribute(uint8(200), uint8(0), uint8(0)),
|
||||||
|
termbox.RGBToAttribute(uint8(0), uint8(0), uint8(0)),
|
||||||
|
)
|
||||||
|
wandle.Print(w.valInput.GetX(), w.valInput.GetY()+1, red, "Changing a tag on timers that have different values!")
|
||||||
|
}
|
||||||
|
w.cancelButton.View(style)
|
||||||
|
w.doneButton.View(style)
|
||||||
|
wandle.Print(w.x+1, w.y+w.h-2, style, w.msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *PromptForTagWiddle) IsActive() bool { return w.active }
|
||||||
|
func (w *PromptForTagWiddle) SetActive(b bool) { w.active = b }
|
||||||
|
func (w *PromptForTagWiddle) Focusable() bool { return true }
|
||||||
|
func (w *PromptForTagWiddle) SetX(x int) {
|
||||||
|
w.x = x
|
||||||
|
w.Measure()
|
||||||
|
}
|
||||||
|
func (w *PromptForTagWiddle) GetX() int { return w.x }
|
||||||
|
func (w *PromptForTagWiddle) SetY(y int) {
|
||||||
|
w.y = y
|
||||||
|
w.Measure()
|
||||||
|
}
|
||||||
|
func (w *PromptForTagWiddle) GetY() int { return w.y }
|
||||||
|
func (w *PromptForTagWiddle) SetHeight(h int) {
|
||||||
|
w.h = h
|
||||||
|
w.Measure()
|
||||||
|
}
|
||||||
|
func (w *PromptForTagWiddle) GetHeight() int {
|
||||||
|
//if w.h == widdles.AUTO_SIZE { }
|
||||||
|
return w.h
|
||||||
|
}
|
||||||
|
func (w *PromptForTagWiddle) SetWidth(v int) {
|
||||||
|
w.w = v
|
||||||
|
w.Measure()
|
||||||
|
}
|
||||||
|
func (w *PromptForTagWiddle) GetWidth() int {
|
||||||
|
//if w.w == widdles.AUTO_SIZE { }
|
||||||
|
return w.w
|
||||||
|
}
|
||||||
|
func (w *PromptForTagWiddle) Measure() {
|
||||||
|
w.keyInput.SetX(w.x + 1)
|
||||||
|
w.keyInput.SetY(w.y + 1)
|
||||||
|
w.keyInput.SetWidth(w.w - 2)
|
||||||
|
w.keyInput.SetHeight(1)
|
||||||
|
|
||||||
|
w.valInput.SetX(w.x + 1)
|
||||||
|
w.valInput.SetY(w.y + 2)
|
||||||
|
w.valInput.SetWidth(w.w - 2)
|
||||||
|
w.valInput.SetHeight(1)
|
||||||
|
|
||||||
|
w.doneButton.SetX(w.x + w.w - 9)
|
||||||
|
w.doneButton.SetY(w.y + w.h - 1)
|
||||||
|
w.doneButton.SetWidth(8)
|
||||||
|
w.doneButton.SetHeight(1)
|
||||||
|
|
||||||
|
w.cancelButton.SetX(w.doneButton.GetX() - 12)
|
||||||
|
w.cancelButton.SetY(w.y + w.h - 1)
|
||||||
|
w.cancelButton.SetWidth(10)
|
||||||
|
w.cancelButton.SetHeight(1)
|
||||||
|
}
|
||||||
|
func (w *PromptForTagWiddle) SetPos(x, y int) { w.x, w.y = x, y }
|
||||||
|
func (w *PromptForTagWiddle) SetSize(wdt, hgt int) { w.w, w.h = wdt, hgt }
|
||||||
|
|
||||||
|
func (w *PromptForTagWiddle) handleTermboxEvent(msg termbox.Event) wandle.Cmd {
|
||||||
|
if msg.Key == termbox.KeyEsc {
|
||||||
|
w.Done()
|
||||||
|
return wandle.EmptyCmd
|
||||||
|
} else if msg.Key == termbox.KeyEnter {
|
||||||
|
if w.keyInput.IsEditable() {
|
||||||
|
w.keyInput.SetActive(false)
|
||||||
|
w.keyInput.SetEditable(false)
|
||||||
|
w.valInput.SetActive(true)
|
||||||
|
w.valInput.SetEditable(true)
|
||||||
|
return wandle.EmptyCmd
|
||||||
|
} else if w.valInput.IsEditable() {
|
||||||
|
w.valInput.SetActive(false)
|
||||||
|
w.valInput.SetEditable(false)
|
||||||
|
if w.keyInput.GetValue() != "" && w.valInput.GetValue() != "" {
|
||||||
|
w.doneButton.SetActive(true)
|
||||||
|
} else {
|
||||||
|
w.cancelButton.SetActive(true)
|
||||||
|
}
|
||||||
|
return wandle.EmptyCmd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
widdles := []widdles.Widdle{w.keyInput, w.valInput, w.cancelButton, w.doneButton}
|
||||||
|
for _, wdl := range widdles {
|
||||||
|
if wdl.IsActive() {
|
||||||
|
if ret := wdl.Update(msg); ret != nil {
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if msg.Ch == 'j' || msg.Key == termbox.KeyArrowDown || msg.Key == termbox.KeyArrowRight {
|
||||||
|
for i := range widdles {
|
||||||
|
if widdles[i].IsActive() {
|
||||||
|
return func() wandle.Msg {
|
||||||
|
widdles[i].SetActive(false)
|
||||||
|
next := ((i + 1) + len(widdles)) % len(widdles)
|
||||||
|
widdles[next].SetActive(true)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if msg.Ch == 'k' || msg.Key == termbox.KeyArrowUp || msg.Key == termbox.KeyArrowLeft {
|
||||||
|
for i := range widdles {
|
||||||
|
if widdles[i].IsActive() {
|
||||||
|
return func() wandle.Msg {
|
||||||
|
widdles[i].SetActive(false)
|
||||||
|
next := ((i - 1) + len(widdles)) % len(widdles)
|
||||||
|
widdles[next].SetActive(true)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (w *PromptForTagWiddle) SetTag(key, val string) {
|
||||||
|
w.origKey, w.origVal = key, val
|
||||||
|
w.keyInput.SetValue(key)
|
||||||
|
if key == "" && val == "" {
|
||||||
|
w.keyInput.SetEditable(true)
|
||||||
|
}
|
||||||
|
w.valInput.SetValue(val)
|
||||||
|
}
|
||||||
|
func (w *PromptForTagWiddle) GetTag() (string, string) {
|
||||||
|
return w.keyInput.GetValue(), w.valInput.GetValue()
|
||||||
|
}
|
||||||
|
func (w *PromptForTagWiddle) SetMultiVal(v bool) { w.multipleValues = v }
|
||||||
|
|
||||||
|
func (w *PromptForTagWiddle) ClearCancelCommand() { w.SetCancelCommand(wandle.EmptyCmd) }
|
||||||
|
func (w *PromptForTagWiddle) SetCancelCommand(cmd func() wandle.Msg) { w.cancelButton.SetCommand(cmd) }
|
||||||
|
func (w *PromptForTagWiddle) ClearDoneCommand() { w.SetDoneCommand(wandle.EmptyCmd) }
|
||||||
|
func (w *PromptForTagWiddle) SetDoneCommand(cmd func() wandle.Msg) { w.doneButton.SetCommand(cmd) }
|
||||||
|
|
||||||
|
func (w *PromptForTagWiddle) Done() {
|
||||||
|
w.origKey, w.origVal = "", ""
|
||||||
|
w.multipleValues = false
|
||||||
|
w.keyInput.SetValue("")
|
||||||
|
w.valInput.SetValue("")
|
||||||
|
w.ClearCancelCommand()
|
||||||
|
w.ClearDoneCommand()
|
||||||
|
w.SetActive(false)
|
||||||
|
}
|
|
@ -0,0 +1,145 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.bullercodeworks.com/brian/wandle"
|
||||||
|
"git.bullercodeworks.com/brian/widdles"
|
||||||
|
"github.com/nsf/termbox-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* PartManager is for adding/editing/removing groups of strings
|
||||||
|
*/
|
||||||
|
type PartManager struct {
|
||||||
|
active bool
|
||||||
|
visible bool
|
||||||
|
|
||||||
|
x, y, w, h int
|
||||||
|
|
||||||
|
label string
|
||||||
|
typeString string
|
||||||
|
options []string
|
||||||
|
selOptions map[int]bool
|
||||||
|
input *widdles.ToggleField
|
||||||
|
|
||||||
|
cursor int
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ widdles.Widdle = (*PartManager)(nil)
|
||||||
|
|
||||||
|
func NewPartManager(label string, x, y, w, h int) *PartManager {
|
||||||
|
return &PartManager{
|
||||||
|
label: label,
|
||||||
|
selOptions: make(map[int]bool),
|
||||||
|
input: widdles.NewToggleField("Value:", "", x+1, y+2, w-2, 1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *PartManager) Init() wandle.Cmd { return nil }
|
||||||
|
func (w *PartManager) Update(msg wandle.Msg) wandle.Cmd {
|
||||||
|
if w.active {
|
||||||
|
if msg, ok := msg.(termbox.Event); ok {
|
||||||
|
return w.handleTermboxEvent(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (w *PartManager) handleTermboxEvent(msg termbox.Event) wandle.Cmd {
|
||||||
|
if w.cursor == 0 {
|
||||||
|
if opt := w.input.Update(msg); opt != nil {
|
||||||
|
return opt
|
||||||
|
} else {
|
||||||
|
//return w.handleKeyPress(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return w.handleKeyPress(msg)
|
||||||
|
}
|
||||||
|
func (w *PartManager) handleKeyPress(msg termbox.Event) wandle.Cmd {
|
||||||
|
if msg.Key == termbox.KeyEsc {
|
||||||
|
return func() wandle.Msg {
|
||||||
|
w.Hide()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else if msg.Ch == 'j' || msg.Key == termbox.KeyArrowDown {
|
||||||
|
if w.cursor < len(w.options) {
|
||||||
|
w.cursor = w.cursor + 1
|
||||||
|
}
|
||||||
|
} else if msg.Ch == 'k' || msg.Key == termbox.KeyArrowUp {
|
||||||
|
if w.cursor > 0 {
|
||||||
|
w.cursor = w.cursor - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if w.IsActive() {
|
||||||
|
return func() wandle.Msg { return nil }
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (w *PartManager) View(style wandle.Style) {
|
||||||
|
if w.visible {
|
||||||
|
wandle.TitledBorderFilled(w.label, w.x, w.y, w.x+w.w, w.y+w.h, style, wandle.BRD_SIMPLE)
|
||||||
|
st := style
|
||||||
|
if w.cursor == 0 {
|
||||||
|
st = st.Invert()
|
||||||
|
}
|
||||||
|
w.input.View(st)
|
||||||
|
y := w.input.GetY() + 1
|
||||||
|
wandle.Print(w.x+1, y, style, fmt.Sprintf("Add %s:", w.typeString))
|
||||||
|
y++
|
||||||
|
for i := range w.options {
|
||||||
|
st := style
|
||||||
|
if w.cursor-1 == i {
|
||||||
|
st = st.Invert()
|
||||||
|
}
|
||||||
|
wandle.Print(w.x+3, y, st, w.options[i])
|
||||||
|
y++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (w *PartManager) IsActive() bool { return w.active }
|
||||||
|
func (w *PartManager) SetActive(b bool) { w.active = b }
|
||||||
|
func (w *PartManager) Focusable() bool { return true }
|
||||||
|
func (w *PartManager) SetX(x int) {
|
||||||
|
w.x = x
|
||||||
|
w.Measure()
|
||||||
|
}
|
||||||
|
func (w *PartManager) GetX() int { return w.x }
|
||||||
|
func (w *PartManager) SetY(y int) {
|
||||||
|
w.y = y
|
||||||
|
w.Measure()
|
||||||
|
}
|
||||||
|
func (w *PartManager) GetY() int { return w.y }
|
||||||
|
func (w *PartManager) SetHeight(h int) {
|
||||||
|
w.h = h
|
||||||
|
w.Measure()
|
||||||
|
}
|
||||||
|
func (w *PartManager) GetHeight() int { return w.h }
|
||||||
|
func (w *PartManager) SetWidth(v int) {
|
||||||
|
w.w = v
|
||||||
|
w.Measure()
|
||||||
|
}
|
||||||
|
func (w *PartManager) GetWidth() int { return w.w }
|
||||||
|
func (w *PartManager) Measure() {
|
||||||
|
w.input.SetX(w.x + 1)
|
||||||
|
w.input.SetY(w.y + 1)
|
||||||
|
w.input.SetWidth(w.w - 2)
|
||||||
|
w.input.SetHeight(1)
|
||||||
|
}
|
||||||
|
func (w *PartManager) SetPos(x, y int) { w.x, w.y = x, y }
|
||||||
|
func (w *PartManager) SetSize(wdt, hgt int) { w.w, w.h = wdt, hgt }
|
||||||
|
|
||||||
|
func (w *PartManager) SetLabel(lbl string) { w.label = lbl }
|
||||||
|
func (w *PartManager) SetType(tp string) { w.typeString = tp }
|
||||||
|
func (w *PartManager) SetValue(val string) { w.input.SetValue(val) }
|
||||||
|
func (w *PartManager) GetValue() string { return w.input.GetValue() }
|
||||||
|
func (w *PartManager) SetOptions(opts []string) { w.options = opts }
|
||||||
|
func (w *PartManager) SetVisible(is bool) { w.visible = is }
|
||||||
|
func (w *PartManager) IsVisible() bool { return w.visible }
|
||||||
|
func (w *PartManager) Show() {
|
||||||
|
w.SetVisible(true)
|
||||||
|
w.SetActive(true)
|
||||||
|
}
|
||||||
|
func (w *PartManager) Hide() {
|
||||||
|
w.SetVisible(false)
|
||||||
|
w.SetActive(false)
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -12,6 +13,12 @@ import (
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ROUND_UP = 1
|
||||||
|
ROUND_EITHER = 0
|
||||||
|
ROUND_DOWN = -1
|
||||||
|
)
|
||||||
|
|
||||||
func PromptUser(text string) string {
|
func PromptUser(text string) string {
|
||||||
var resp string
|
var resp string
|
||||||
fmt.Println(text)
|
fmt.Println(text)
|
||||||
|
@ -57,6 +64,34 @@ func TimerToString(t *timertxt.Timer) string {
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Round(dur time.Duration) time.Duration {
|
||||||
|
roundDur := GetRoundToDuration()
|
||||||
|
wrk := dur.Round(roundDur)
|
||||||
|
switch GetRoundDirection() {
|
||||||
|
case ROUND_UP:
|
||||||
|
if wrk < dur {
|
||||||
|
return wrk + roundDur
|
||||||
|
}
|
||||||
|
case ROUND_DOWN:
|
||||||
|
if wrk > dur {
|
||||||
|
return wrk - roundDur
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return wrk
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetRoundDirection() int {
|
||||||
|
dir := viper.GetString("round")
|
||||||
|
switch dir {
|
||||||
|
case "up":
|
||||||
|
return ROUND_UP
|
||||||
|
case "down":
|
||||||
|
return ROUND_DOWN
|
||||||
|
default:
|
||||||
|
return ROUND_EITHER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func GetRoundToDuration() time.Duration {
|
func GetRoundToDuration() time.Duration {
|
||||||
var dur time.Duration
|
var dur time.Duration
|
||||||
dur, _ = time.ParseDuration(viper.GetString("roundto"))
|
dur, _ = time.ParseDuration(viper.GetString("roundto"))
|
||||||
|
@ -67,6 +102,12 @@ func DurationToDecimal(dur time.Duration) float64 {
|
||||||
mins := dur.Minutes() - (dur.Hours() * 60)
|
mins := dur.Minutes() - (dur.Hours() * 60)
|
||||||
return dur.Hours() + (mins / 60)
|
return dur.Hours() + (mins / 60)
|
||||||
}
|
}
|
||||||
|
func AddDurations(dur1, dur2 time.Duration) time.Duration {
|
||||||
|
return time.Duration(int64(dur1) + int64(dur2))
|
||||||
|
}
|
||||||
|
func SubDurations(dur1, dur2 time.Duration) time.Duration {
|
||||||
|
return time.Duration(int64(dur1) - int64(dur2))
|
||||||
|
}
|
||||||
|
|
||||||
// GetContextsFromSlice pulls all '@' (contexts) out of the
|
// GetContextsFromSlice pulls all '@' (contexts) out of the
|
||||||
// string slice and return those contexts and the remaining
|
// string slice and return those contexts and the remaining
|
||||||
|
@ -180,7 +221,7 @@ func TimerToFriendlyString(t *timertxt.Timer) string {
|
||||||
} else {
|
} else {
|
||||||
dur = t.FinishDate.Sub(t.StartDate)
|
dur = t.FinishDate.Sub(t.StartDate)
|
||||||
}
|
}
|
||||||
dur = dur.Round(GetRoundToDuration())
|
dur = Round(dur)
|
||||||
return fmt.Sprintf("% 2d. %s - %s [ %s] [ %s] [ %s] %s ( %.2f )", t.Id, start, end, contexts, projects, tags, t.Notes, DurationToDecimal(dur))
|
return fmt.Sprintf("% 2d. %s - %s [ %s] [ %s] [ %s] %s ( %.2f )", t.Id, start, end, contexts, projects, tags, t.Notes, DurationToDecimal(dur))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -382,3 +423,27 @@ func BuildFilterFromArgs(args []string) func(*timertxt.Timer) bool {
|
||||||
}
|
}
|
||||||
return doFilters
|
return doFilters
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SortedTagKeyList(m map[string]string) []string {
|
||||||
|
var ret []string
|
||||||
|
for k := range m {
|
||||||
|
ret = append(ret, k)
|
||||||
|
}
|
||||||
|
sort.Strings(ret)
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func StringSliceContains(sl []string, val string) bool {
|
||||||
|
for i := range sl {
|
||||||
|
if sl[i] == val {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
func AppendStringIfDistinct(sl []string, val string) []string {
|
||||||
|
if !StringSliceContains(sl, val) {
|
||||||
|
return append(sl, val)
|
||||||
|
}
|
||||||
|
return sl
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue