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:
|
||||
go build -o build/gime *.go
|
||||
go build ${LDFLAGS} -o build/${BINARY}
|
||||
|
||||
runui:
|
||||
cd build && ./gime ui
|
||||
|
||||
clean:
|
||||
rm build/*
|
||||
|
|
41
cli/cli.go
41
cli/cli.go
|
@ -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() {
|
||||
|
|
|
@ -7,6 +7,7 @@ package cmd
|
|||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
@ -15,7 +16,10 @@ import (
|
|||
// configCmd represents the config command
|
||||
var configCmd = &cobra.Command{
|
||||
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,
|
||||
}
|
||||
|
||||
|
@ -24,10 +28,37 @@ 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() {
|
||||
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())
|
||||
for _, v := range settings {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -17,13 +17,7 @@ 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.`,
|
||||
Short: "Output your timer.txt status in json for the i3 status bar",
|
||||
RunE: opI3Status,
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
86
cmd/mod.go
86
cmd/mod.go
|
@ -6,20 +6,20 @@ 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.`,
|
||||
Short: "Modify a timer",
|
||||
RunE: opMod,
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
|
40
cmd/rm.go
40
cmd/rm.go
|
@ -6,20 +6,17 @@ 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.`,
|
||||
Short: "Remove a timer",
|
||||
RunE: opRemove,
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
49
cmd/start.go
49
cmd/start.go
|
@ -6,20 +6,18 @@ 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.`,
|
||||
Short: "Start a timer",
|
||||
RunE: opStart,
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
56
cmd/stop.go
56
cmd/stop.go
|
@ -6,20 +6,19 @@ 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.`,
|
||||
Short: "Stop a timer",
|
||||
RunE: opStop,
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
|
|
@ -6,20 +6,18 @@ 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.`,
|
||||
Short: "Stop the current timer and start a new one copying the last one's parameters",
|
||||
RunE: opSwitch,
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -7,19 +7,15 @@ 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.`,
|
||||
Short: "Toggle the most recent timer on and off",
|
||||
RunE: opToggle,
|
||||
}
|
||||
|
||||
|
@ -28,6 +24,37 @@ func init() {
|
|||
}
|
||||
|
||||
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
|
||||
}
|
||||
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
|
||||
|
||||
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
10
go.mod
|
@ -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
|
||||
|
|
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
|
||||
|
||||
import "git.bullercodeworks.com/brian/gime/cmd"
|
||||
|
||||
var (
|
||||
VERSION string
|
||||
BUILD string
|
||||
)
|
||||
|
||||
func main() {
|
||||
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"
|
||||
"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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue