Compare commits

...

16 Commits
v0.1.0 ... main

24 changed files with 1648 additions and 101 deletions

View File

@ -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:
go build -o build/gime *.go
go build ${LDFLAGS} -o build/${BINARY}
runui:
cd build && ./gime ui
clean:
rm build/*

View File

@ -27,15 +27,31 @@ func (p *Program) Initialize() error {
return nil
}
func (p *Program) GetTimerFilePath() string {
return p.timerPath
}
func (p *Program) LoadTimerList() error {
var err error
var tl timertxt.TimerList
var tl *timertxt.TimerList
tl, err = timertxt.LoadFromFilename(p.timerPath)
if err != nil {
return err
}
tl.Sort(timertxt.SORT_UNFINISHED_START)
p.TimerList = &tl
tl.Sort(timertxt.SortUnfinishedStart)
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
}
@ -43,14 +59,18 @@ func (p *Program) WriteTimerList() error {
return p.TimerList.WriteToFilename(p.timerPath)
}
func (p *Program) GetDoneFilePath() string {
return p.donePath
}
func (p *Program) LoadDoneList() error {
var err error
var tl timertxt.TimerList
var tl *timertxt.TimerList
tl, err = timertxt.LoadFromFilename(p.donePath)
if err != nil {
return err
}
p.DoneList = &tl
p.DoneList = tl
return nil
}
@ -102,14 +122,14 @@ func (p *Program) GetFilteredTimerList(args []string) *timertxt.TimerList {
}
}
list := p.TimerList.GetTimersInRange(start, end)
if includeArchive {
if err = p.LoadDoneList(); err != nil {
fmt.Println("Error loading done.txt entries")
fmt.Println(err.Error())
return nil
}
*list = append(*list, (*p.DoneList.GetTimersInRange(start, end))...)
doneList := p.DoneList.GetTimersInRange(start, end)
list.Combine(doneList)
}
if len(contextFilters) > 0 {
allFilters = append(allFilters, func(t timertxt.Timer) bool {
@ -145,6 +165,13 @@ func (p *Program) GetFilteredTimerList(args []string) *timertxt.TimerList {
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) {
work, wErr := p.TimerList.GetMostRecentTimer()
if wErr == nil && work.FinishDate.IsZero() {

View File

@ -7,6 +7,7 @@ package cmd
import (
"fmt"
"sort"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/viper"
@ -15,8 +16,11 @@ import (
// configCmd represents the config command
var configCmd = &cobra.Command{
Use: "config",
Short: "Print all configuration values",
RunE: opConfig,
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,
}
func init() {
@ -24,9 +28,36 @@ func init() {
}
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
for k, v := range viper.AllSettings() {
settings = append(settings, fmt.Sprintf("%s: %s", k, v))
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))
}
}
sort.Strings(settings)
fmt.Println("Configuration File:", viper.ConfigFileUsed())

View File

@ -6,7 +6,10 @@ package cmd
import (
"fmt"
"os"
"os/exec"
"git.bullercodeworks.com/brian/gime/cli"
"github.com/spf13/cobra"
)
@ -22,6 +25,23 @@ func init() {
}
func opEditor(cmd *cobra.Command, args []string) error {
fmt.Println("editor called")
return nil
p := cli.Program{}
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()
}

View File

@ -17,14 +17,8 @@ import (
// i3statusCmd represents the i3status command
var i3statusCmd = &cobra.Command{
Use: "i3status",
Short: "",
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,
Short: "Output your timer.txt status in json for the i3 status bar",
RunE: opI3Status,
}
func init() {
@ -52,14 +46,14 @@ func opI3Status(cmd *cobra.Command, args []string) error {
return nil
}
state := "Idle"
wrk, err := p.GetMostRecentTimer()
wrk, err := p.GetActiveOrMostRecent()
if err != nil {
fmt.Print("{\"icon\":\"time\",\"state\":\"Critical\", \"text\": \"Error loading timer entry\"}")
return nil
}
var text string
if wrk.FinishDate.IsZero() {
wrkDur := wrk.Duration().Round(time.Minute * 15)
wrkDur := util.Round(wrk.Duration())
hrs := int(wrkDur.Hours())
mins := int(wrkDur.Minutes()) - hrs*60
if hrs > 0 {
@ -97,7 +91,7 @@ func opI3Status(cmd *cobra.Command, args []string) error {
getListTotal := func(list *timertxt.TimerList) string {
var isActive bool
var total time.Duration
for _, v := range *list {
for _, v := range list.GetTimerSlice() {
dur := v.FinishDate.Sub(v.StartDate)
if v.FinishDate.IsZero() {
dur = time.Now().Sub(v.StartDate)
@ -105,7 +99,7 @@ func opI3Status(cmd *cobra.Command, args []string) error {
}
total += dur
}
total = total.Round(util.GetRoundToDuration())
total = util.Round(total)
if isActive {
return fmt.Sprintf("%.2f+", util.DurationToDecimal(total))
} else {

View File

@ -76,7 +76,7 @@ func opListTimers(cmd *cobra.Command, args []string) error {
list = list.Filter(filter)
dayTotals := make(map[string]time.Duration)
for _, v := range *list {
for _, v := range list.GetTimerSlice() {
dur := v.FinishDate.Sub(v.StartDate)
if v.FinishDate.IsZero() {
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
}
var oldDayStr, dayStr string
for _, v := range *list {
for _, v := range list.GetTimerSlice() {
oldDayStr = dayStr
dayStr = v.StartDate.Format("2006/01/02")
if dayStr != oldDayStr {
// TODO:
wrkDur := dayTotals[dayStr].Round(util.GetRoundToDuration())
wrkDur := util.Round(dayTotals[dayStr])
fmtStr := dayStr + " ( %.2f )\n"
fmt.Printf(fmtStr, util.DurationToDecimal(wrkDur))
}

View File

@ -6,21 +6,21 @@ package cmd
import (
"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"
)
// modCmd represents the mod command
var modCmd = &cobra.Command{
Use: "mod",
Short: "A brief description of your command",
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,
Short: "Modify a timer",
RunE: opMod,
}
func init() {
@ -28,6 +28,76 @@ func init() {
}
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
}

View File

@ -6,21 +6,18 @@ package cmd
import (
"fmt"
"strconv"
"git.bullercodeworks.com/brian/gime/cli"
"git.bullercodeworks.com/brian/gime/util"
"github.com/spf13/cobra"
)
// rmCmd represents the rm command
var rmCmd = &cobra.Command{
Use: "rm",
Short: "A brief description of your command",
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,
Short: "Remove a timer",
RunE: opRemove,
}
func init() {
@ -28,6 +25,33 @@ func init() {
}
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
}

View File

@ -15,14 +15,16 @@ import (
)
var (
Version = "2.0"
Version string
Build string
configFile string
// rootCmd represents the base command when called without any subcommands
rootCmd = &cobra.Command{
Use: "gime",
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,
}
)
@ -46,6 +48,7 @@ func initConfig() {
viper.SetDefault("timerfile", "timer.txt")
viper.SetDefault("donefile", "done.txt")
viper.SetDefault("reportfile", "report.txt")
viper.SetDefault("copytags", false)
var firstDir string // In case we need to make directories
if configFile != "" {
viper.SetConfigFile(configFile)

View File

@ -6,21 +6,19 @@ package cmd
import (
"fmt"
"strings"
"git.bullercodeworks.com/brian/gime/cli"
"git.bullercodeworks.com/brian/gime/util"
"git.bullercodeworks.com/brian/go-timertxt"
"github.com/spf13/cobra"
)
// startCmd represents the start command
var startCmd = &cobra.Command{
Use: "start",
Short: "A brief description of your command",
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,
Short: "Start a timer",
RunE: opStart,
}
func init() {
@ -28,6 +26,41 @@ func init() {
}
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
}

View File

@ -32,21 +32,23 @@ func opStatus(cmd *cobra.Command, args []string) error {
return err
}
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")
return nil
}
var currDur time.Duration
for _, v := range *p.TimerList {
for _, v := range active {
if v.ActiveToday() {
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))
for _, v := range *p.TimerList.GetActiveTimers() {
for _, v := range active {
fmt.Println(util.TimerToFriendlyString(v))
}
return nil

View File

@ -6,21 +6,20 @@ package cmd
import (
"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"
)
// stopCmd represents the stop command
var stopCmd = &cobra.Command{
Use: "stop",
Short: "A brief description of your command",
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,
Short: "Stop a timer",
RunE: opStop,
}
func init() {
@ -28,6 +27,47 @@ func init() {
}
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
}

View File

@ -6,21 +6,19 @@ package cmd
import (
"fmt"
"time"
"git.bullercodeworks.com/brian/gime/cli"
"git.bullercodeworks.com/brian/gime/util"
"git.bullercodeworks.com/brian/go-timertxt"
"github.com/spf13/cobra"
)
// switchCmd represents the switch command
var switchCmd = &cobra.Command{
Use: "switch",
Short: "A brief description of your command",
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,
Short: "Stop the current timer and start a new one copying the last one's parameters",
RunE: opSwitch,
}
func init() {
@ -28,6 +26,31 @@ func init() {
}
func opSwitch(cmd *cobra.Command, args []string) error {
fmt.Println("switch called")
return nil
var err error
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)
}

View File

@ -37,7 +37,7 @@ func opShowTimers(cmd *cobra.Command, args []string) error {
list := p.GetFilteredTimerList(args)
var isActive bool
var total time.Duration
for _, v := range *list {
for _, v := range list.GetTimerSlice() {
dur := v.FinishDate.Sub(v.StartDate)
if v.FinishDate.IsZero() {
dur = time.Now().Sub(v.StartDate)
@ -45,7 +45,7 @@ func opShowTimers(cmd *cobra.Command, args []string) error {
}
total += dur
}
total = total.Round(util.GetRoundToDuration())
total = util.Round(total)
if isActive {
fmt.Printf("%.2f+\n", util.DurationToDecimal(total))
} else {

View File

@ -7,20 +7,16 @@ package cmd
import (
"fmt"
"git.bullercodeworks.com/brian/gime/cli"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// toggleCmd represents the toggle command
var toggleCmd = &cobra.Command{
Use: "toggle",
Short: "A brief description of your command",
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,
Short: "Toggle the most recent timer on and off",
RunE: opToggle,
}
func init() {
@ -28,6 +24,37 @@ func init() {
}
func opToggle(cmd *cobra.Command, args []string) error {
fmt.Println("toggle called")
return nil
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
}
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{})
}
}

View File

@ -4,8 +4,8 @@ Copyright © 2022 Brian Buller <brian@bullercodeworks.com>
package cmd
import (
"fmt"
"git.bullercodeworks.com/brian/gime/cli"
"git.bullercodeworks.com/brian/gime/ui"
"github.com/spf13/cobra"
)
@ -21,6 +21,6 @@ func init() {
}
func opUi(cmd *cobra.Command, args []string) error {
fmt.Println("ui called")
return nil
p := &cli.Program{}
return ui.RunTUI(p)
}

10
go.mod
View File

@ -1,11 +1,17 @@
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/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 (
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/user-config v0.0.0-20170914134719-16e743ec93a2
github.com/muesli/go-app-paths v0.2.2

View File

@ -1,11 +1,16 @@
/*
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
Copyright © 2022 Brian Buller <brian@bullercodeworks.com>
*/
package main
import "git.bullercodeworks.com/brian/gime/cmd"
var (
VERSION string
BUILD string
)
func main() {
cmd.Execute()
}

583
ui/list_timers.go Normal file
View File

@ -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
}

71
ui/screen_settings.go Normal file
View File

@ -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,
}
}

140
ui/ui.go Normal file
View File

@ -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

223
ui/widdle_addtagtotimers.go Normal file
View File

@ -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)
}

145
ui/widdle_manageparts.go Normal file
View File

@ -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)
}

View File

@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"os"
"sort"
"strings"
"time"
@ -12,6 +13,12 @@ import (
"github.com/spf13/viper"
)
const (
ROUND_UP = 1
ROUND_EITHER = 0
ROUND_DOWN = -1
)
func PromptUser(text string) string {
var resp string
fmt.Println(text)
@ -57,6 +64,34 @@ func TimerToString(t *timertxt.Timer) string {
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 {
var dur time.Duration
dur, _ = time.ParseDuration(viper.GetString("roundto"))
@ -67,6 +102,12 @@ func DurationToDecimal(dur time.Duration) float64 {
mins := dur.Minutes() - (dur.Hours() * 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
// string slice and return those contexts and the remaining
@ -180,7 +221,7 @@ func TimerToFriendlyString(t *timertxt.Timer) string {
} else {
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))
}
@ -382,3 +423,27 @@ func BuildFilterFromArgs(args []string) func(*timertxt.Timer) bool {
}
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
}