From c98a4dea0c60f851d40a0062fe400ee11bd8a376 Mon Sep 17 00:00:00 2001 From: Brian Buller Date: Fri, 6 Jan 2023 09:34:55 -0600 Subject: [PATCH 1/2] Working on UI --- Makefile | 5 +- cmd/ui.go | 8 +- go.mod | 8 +- ui/list_timers.go | 168 ++++++++++++++++++++++++++++++++++++++++++ ui/screen_settings.go | 71 ++++++++++++++++++ ui/ui.go | 128 ++++++++++++++++++++++++++++++++ util/helpers.go | 10 +++ 7 files changed, 392 insertions(+), 6 deletions(-) create mode 100644 ui/list_timers.go create mode 100644 ui/screen_settings.go create mode 100644 ui/ui.go diff --git a/Makefile b/Makefile index edd8efd..2701366 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -# This how we want to name the binary output +# This is what we want to name the binary output BINARY=gime # These are the values we want to pass for VERSION and BUILD @@ -14,5 +14,8 @@ LDFLAGS=-ldflags "-w -s -X cmd.Version=${VERSION} -X cmd.Build=${BUILD}" gime: go build ${LDFLAGS} -o build/${BINARY} +runui: + cd build && ./gime ui + clean: rm build/* diff --git a/cmd/ui.go b/cmd/ui.go index 4d268c9..d72b23d 100644 --- a/cmd/ui.go +++ b/cmd/ui.go @@ -4,8 +4,8 @@ Copyright © 2022 Brian Buller 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) } diff --git a/go.mod b/go.mod index 42d70dd..6f24dba 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,17 @@ module git.bullercodeworks.com/brian/gime -go 1.17 +go 1.18 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/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 diff --git a/ui/list_timers.go b/ui/list_timers.go new file mode 100644 index 0000000..fe22fd3 --- /dev/null +++ b/ui/list_timers.go @@ -0,0 +1,168 @@ +package ui + +import ( + "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" +) + +type listTimersScreen struct { + ui *Ui + + initialized bool + menu *widdles.TopMenu + scrollbar *widdles.Scrollbar + + cursor int + + timerList *timertxt.TimerList + doneList *timertxt.TimerList + + timerFilterList *timertxt.TimerList + doneFilterList *timertxt.TimerList + + filter string + + msg string + err error +} + +type ListTimersMsg ScreenMsg + +func NewListTimersScreen(u *Ui) *listTimersScreen { + w, h := termbox.Size() + s := listTimersScreen{ + ui: u, + menu: widdles.NewTopMenu(0, 0, 0), + scrollbar: widdles.NewScrollbar(w-2, 2, 1, h-2), + } + 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.Hotkey = 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.timerFilterList, s.doneFilterList = s.timerList, s.doneList + s.timerFilterList.Sort(timertxt.SORT_START_DATE_DESC) + s.doneFilterList.Sort(timertxt.SORT_START_DATE_DESC) + + return nil +} + +func (s *listTimersScreen) Update(msg wandle.Msg) wandle.Cmd { + switch msg := msg.(type) { + case ScreenMsg: + case termbox.Event: + return s.handleTermboxEvent(msg) + } + return nil +} + +func (s *listTimersScreen) View(style wandle.Style) { + _, h := termbox.Size() + if s.menu.IsActive() { + s.menu.View(style) + } + y := 2 + 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() + } + wandle.Print(1, y, st, "[ ]") + s.ViewTimer(5, y, st, tmr) + y++ + } + y++ + wandle.Print(1, y, style.Bold(true), "Done Timers") + y++ + for idx, tmr := range s.doneFilterList.GetTimerSlice() { + if y > h-2 { + break + } + st := style + if s.cursor == s.timerFilterList.Size()+idx { + st = st.Invert() + } + wandle.Print(1, y, st, "[ ]") + s.ViewTimer(5, y, st, tmr) + y++ + } + + s.scrollbar.View(style) +} + +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 (msg.Type == termbox.EventKey && msg.Key == termbox.KeyEsc) || s.menu.IsActive() { + return s.menu.Update(msg) + } + switch msg.Type { + case termbox.EventKey: + top := s.timerFilterList.Size() + s.doneFilterList.Size() - 2 + if msg.Key == termbox.KeyEnter { + /* + if s.cursor >= 0 && s.cursor < s.timerFilterList.Size()+s.doneFilterList.Size() { + } else { + } + */ + } 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 < top { + s.cursor++ + } else { + s.cursor = top + } + return nil + } else if msg.Ch == 'G' { + s.cursor = top + } + } + return nil +} + +func (s *listTimersScreen) gotoSettingsScreen() wandle.Msg { + return ScreenMsg{ + source: ListTimersId, + command: CmdGotoSettings, + } +} diff --git a/ui/screen_settings.go b/ui/screen_settings.go new file mode 100644 index 0000000..87eaab8 --- /dev/null +++ b/ui/screen_settings.go @@ -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.Hotkey = 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, + } +} diff --git a/ui/ui.go b/ui/ui.go new file mode 100644 index 0000000..e9263f1 --- /dev/null +++ b/ui/ui.go @@ -0,0 +1,128 @@ +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 + + // Goto Screen Commands + CmdGotoSettings + CmdGotoTimerList +) + +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(wandle.NewStyle( + termbox.RGBToAttribute(uint8(0), uint8(255), uint8(0)), + termbox.RGBToAttribute(uint8(0), uint8(0), uint8(0)), + )) + 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 diff --git a/util/helpers.go b/util/helpers.go index fdb7ca7..b2dcafe 100644 --- a/util/helpers.go +++ b/util/helpers.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "os" + "sort" "strings" "time" @@ -416,3 +417,12 @@ 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 +} From c412f54294eea414082308c95e7fbc12d826fcbc Mon Sep 17 00:00:00 2001 From: Brian Buller Date: Thu, 12 Jan 2023 06:04:17 -0600 Subject: [PATCH 2/2] Updated TimerList struct --- cli/cli.go | 12 ++-- cmd/i3status.go | 2 +- cmd/list.go | 4 +- cmd/mod.go | 5 +- cmd/status.go | 7 +- cmd/stop.go | 2 +- cmd/switch.go | 2 +- cmd/time.go | 2 +- ui/list_timers.go | 108 ++++++++++++++++++++++++++--- ui/widdle_manageparts.go | 143 +++++++++++++++++++++++++++++++++++++++ 10 files changed, 260 insertions(+), 27 deletions(-) create mode 100644 ui/widdle_manageparts.go diff --git a/cli/cli.go b/cli/cli.go index 52953cd..43c83b0 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -33,13 +33,13 @@ func (p *Program) GetTimerFilePath() string { 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 + p.TimerList = tl return nil } @@ -53,12 +53,12 @@ func (p *Program) GetDoneFilePath() string { 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 } @@ -110,14 +110,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 { diff --git a/cmd/i3status.go b/cmd/i3status.go index 46dd643..7ff5465 100644 --- a/cmd/i3status.go +++ b/cmd/i3status.go @@ -91,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) diff --git a/cmd/list.go b/cmd/list.go index b9730ea..2f045ec 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -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,7 +84,7 @@ 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 { diff --git a/cmd/mod.go b/cmd/mod.go index 63e8e8c..66e75ce 100644 --- a/cmd/mod.go +++ b/cmd/mod.go @@ -42,8 +42,9 @@ func opMod(cmd *cobra.Command, args []string) error { id, err := strconv.Atoi(args[0]) if err != nil { // We didn't have a timer id, so try to modify the first active timer - if len(*p.TimerList.GetActiveTimers()) > 0 { - timer = (*p.TimerList.GetActiveTimers())[0] + 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) diff --git a/cmd/status.go b/cmd/status.go index 760bf94..39df92a 100644 --- a/cmd/status.go +++ b/cmd/status.go @@ -34,12 +34,13 @@ func opStatus(cmd *cobra.Command, args []string) error { if err := p.LoadTimerList(); err != nil { 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() } @@ -47,7 +48,7 @@ func opStatus(cmd *cobra.Command, args []string) error { 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 diff --git a/cmd/stop.go b/cmd/stop.go index 11c0062..cc0872c 100644 --- a/cmd/stop.go +++ b/cmd/stop.go @@ -51,7 +51,7 @@ func opStop(cmd *cobra.Command, args []string) error { fmt.Println("Stopping at : " + end.Format(time.RFC3339)) var timerIds []int if id == -1 { - for _, v := range *p.TimerList.GetActiveTimers() { + for _, v := range p.TimerList.GetActiveTimers().GetTimerSlice() { timerIds = append(timerIds, v.Id) } } else { diff --git a/cmd/switch.go b/cmd/switch.go index a88b62c..2226534 100644 --- a/cmd/switch.go +++ b/cmd/switch.go @@ -38,7 +38,7 @@ func opSwitch(cmd *cobra.Command, args []string) error { 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() { + for _, v := range p.TimerList.GetActiveTimers().GetTimerSlice() { timerIds = append(timerIds, v.Id) } fmt.Print("Stopping ", timerIds, "\n") diff --git a/cmd/time.go b/cmd/time.go index 4df246b..954a457 100644 --- a/cmd/time.go +++ b/cmd/time.go @@ -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) diff --git a/ui/list_timers.go b/ui/list_timers.go index fe22fd3..bb28e0c 100644 --- a/ui/list_timers.go +++ b/ui/list_timers.go @@ -21,13 +21,19 @@ type listTimersScreen struct { cursor int + fullList *timertxt.TimerList timerList *timertxt.TimerList doneList *timertxt.TimerList timerFilterList *timertxt.TimerList doneFilterList *timertxt.TimerList - filter string + selected map[int]bool + + inputDialog *widdles.InputDialog + filter string + + partManager *PartManager msg string err error @@ -38,9 +44,12 @@ type ListTimersMsg ScreenMsg func NewListTimersScreen(u *Ui) *listTimersScreen { w, h := termbox.Size() s := listTimersScreen{ - ui: u, - menu: widdles.NewTopMenu(0, 0, 0), - scrollbar: widdles.NewScrollbar(w-2, 2, 1, h-2), + ui: u, + menu: widdles.NewTopMenu(0, 0, 0), + scrollbar: widdles.NewScrollbar(w-2, 2, 1, h-2), + selected: make(map[int]bool), + inputDialog: widdles.NewInputDialog("", ""), + partManager: NewPartManager("", 0, 0, 0, 0), } return &s } @@ -62,9 +71,17 @@ func (s *listTimersScreen) Init() wandle.Cmd { 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.SORT_START_DATE_DESC) s.doneFilterList.Sort(timertxt.SORT_START_DATE_DESC) + w, h := termbox.Size() + s.partManager.SetX(w / 4) + s.partManager.SetY(h / 4) + s.partManager.SetWidth(w / 2) + s.partManager.SetHeight(h / 2) return nil } @@ -80,9 +97,6 @@ func (s *listTimersScreen) Update(msg wandle.Msg) wandle.Cmd { func (s *listTimersScreen) View(style wandle.Style) { _, h := termbox.Size() - if s.menu.IsActive() { - s.menu.View(style) - } y := 2 wandle.Print(1, y, style.Bold(true), "Active Timers") y++ @@ -94,7 +108,11 @@ func (s *listTimersScreen) View(style wandle.Style) { if s.cursor == idx { st = st.Invert() } - wandle.Print(1, y, st, "[ ]") + if s.selected[idx] { + wandle.Print(1, y, st, "[✔] ") + } else { + wandle.Print(1, y, st, "[ ] ") + } s.ViewTimer(5, y, st, tmr) y++ } @@ -102,19 +120,31 @@ func (s *listTimersScreen) View(style wandle.Style) { wandle.Print(1, y, style.Bold(true), "Done Timers") y++ for idx, tmr := range s.doneFilterList.GetTimerSlice() { - if y > h-2 { + if y > h-3 { break } st := style if s.cursor == s.timerFilterList.Size()+idx { st = st.Invert() } - wandle.Print(1, y, st, "[ ]") + if s.selected[s.timerFilterList.Size()+idx] { + wandle.Print(1, y, st, "[✔] ") + } else { + wandle.Print(1, y, st, "[ ] ") + } s.ViewTimer(5, y, st, tmr) y++ } + wandle.Print(1, h-1, style, "[p]roject(+), [c]ontext(@), [t]ags(:)") + if len(s.selected) > 0 { + wandle.Print(39, h-1, style, fmt.Sprintf("(%d selected)", len(s.selected))) + } s.scrollbar.View(style) + if s.menu.IsActive() { + s.menu.View(style) + } + s.partManager.View(style) } func (s *listTimersScreen) ViewTimer(x, y int, style wandle.Style, tmr *timertxt.Timer) { @@ -127,18 +157,38 @@ func (s *listTimersScreen) ViewTimer(x, y int, style wandle.Style, tmr *timertxt } func (s *listTimersScreen) handleTermboxEvent(msg termbox.Event) wandle.Cmd { + if pmRes := s.partManager.Update(msg); pmRes != nil { + return pmRes + } if (msg.Type == termbox.EventKey && msg.Key == termbox.KeyEsc) || s.menu.IsActive() { return s.menu.Update(msg) } switch msg.Type { case termbox.EventKey: top := s.timerFilterList.Size() + s.doneFilterList.Size() - 2 + selected := len(s.selected) + var selTimer *timertxt.Timer + if selected == 0 { + if s.cursor < s.timerFilterList.Size() { + selTimer, s.err = s.timerFilterList.GetTimer(s.cursor) + } else { + selTimer, s.err = s.doneFilterList.GetTimer(s.cursor) + } + } if msg.Key == termbox.KeyEnter { + // 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 + } } else if msg.Key == termbox.KeyArrowUp || msg.Ch == 'k' { if s.cursor > 0 { s.cursor-- @@ -155,6 +205,44 @@ func (s *listTimersScreen) handleTermboxEvent(msg termbox.Event) wandle.Cmd { return nil } else if msg.Ch == 'G' { s.cursor = top + } else if msg.Ch == 't' { + // Edit tag(s) + tags := s.fullList.GetTagKeys() + s.partManager.SetOptions(tags) + if selected > 0 { + // TODO + //s.partManager.SetLabel(fmt.Sprintf("Manage Tags on %d Timers", selected)) + } else { + s.partManager.SetLabel("Manage Tags for Timer") + s.partManager.SetType("Tag") + } + s.partManager.Show() + } else if msg.Ch == 'p' { + // Edit project(s) + projs := s.fullList.GetProjects() + s.partManager.SetOptions(projs) + if selected > 0 { + // TODO + //s.partManager.SetLabel(fmt.Sprintf("Manage Projects on %d Timers", selected)) + } else { + s.partManager.SetLabel("Manage Projects for Timer") + s.partManager.SetType("Project") + s.partManager.SetValue(strings.Join(selTimer.Projects, " ")) + } + s.partManager.Show() + } else if msg.Ch == 'c' { + // Edit context(s) + ctxts := s.fullList.GetContexts() + s.partManager.SetOptions(ctxts) + if selected > 0 { + // TODO + //s.partManager.SetLabel(fmt.Sprintf("Manage Contexts on %d Timers", selected)) + } else { + s.partManager.SetLabel("Manage Contexts for Timer") + s.partManager.SetType("Context") + s.partManager.SetValue(strings.Join(selTimer.Contexts, " ")) + } + s.partManager.Show() } } return nil diff --git a/ui/widdle_manageparts.go b/ui/widdle_manageparts.go new file mode 100644 index 0000000..7af2498 --- /dev/null +++ b/ui/widdle_manageparts.go @@ -0,0 +1,143 @@ +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) 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) +}