gime/ui/list_timers.go

584 lines
15 KiB
Go
Raw Normal View History

2023-01-12 12:53:26 +00:00
package ui
import (
2023-01-25 22:51:41 +00:00
"errors"
2023-01-12 12:53:26 +00:00
"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"
)
2023-01-12 17:09:49 +00:00
const (
activeToggleActive = iota
activeToggleInactive
activeToggleAll
activeToggleErr
)
2023-01-12 12:53:26 +00:00
type listTimersScreen struct {
ui *Ui
initialized bool
menu *widdles.TopMenu
scrollbar *widdles.Scrollbar
cursor int
2023-01-12 17:09:49 +00:00
activeToggle int
fullList *timertxt.TimerList
timerList *timertxt.TimerList
doneList *timertxt.TimerList
2023-01-12 12:53:26 +00:00
2023-01-12 17:09:49 +00:00
fullFilterList *timertxt.TimerList
2023-01-12 12:53:26 +00:00
timerFilterList *timertxt.TimerList
doneFilterList *timertxt.TimerList
selected map[int]bool
2023-01-25 22:51:41 +00:00
confirm *widdles.ConfirmDialog
filter string
2023-01-12 12:53:26 +00:00
2023-01-12 17:09:49 +00:00
choiceMenu *widdles.MenuV
tagEditor *PromptForTagWiddle
//partManager *PartManager
2023-01-12 12:53:26 +00:00
msg string
err error
}
type ListTimersMsg ScreenMsg
2023-02-02 12:08:50 +00:00
func NewListTimersMsg(data interface{}, err error) ListTimersMsg {
return ListTimersMsg{
source: ListTimersId,
command: CmdArchiveTimer,
data: data,
err: err,
}
}
2023-01-12 12:53:26 +00:00
func NewListTimersScreen(u *Ui) *listTimersScreen {
s := listTimersScreen{
2023-01-25 22:51:41 +00:00
ui: u,
menu: widdles.NewTopMenu(0, 0, 0),
scrollbar: widdles.NewScrollbar(0, 0, 0, 0),
selected: make(map[int]bool),
confirm: widdles.NewConfirmDialog("", ""),
2023-01-12 17:09:49 +00:00
choiceMenu: widdles.NewMenuV(0, 0, 0, 0),
tagEditor: NewPromptForTagWiddle(0, 0, widdles.AUTO_SIZE, widdles.AUTO_SIZE, "", ""),
2023-01-12 12:53:26 +00:00
}
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")
2023-01-25 22:51:41 +00:00
quitOption.SetHotkey(widdles.NewHotkey(termbox.KeyCtrlC))
2023-01-12 12:53:26 +00:00
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
2023-09-07 18:20:12 +00:00
s.timerFilterList.Sort(timertxt.SortStartDateDesc)
s.doneFilterList.Sort(timertxt.SortStartDateDesc)
2023-01-25 22:51:41 +00:00
s.updateFullFilterList()
2023-01-12 12:53:26 +00:00
w, h := termbox.Size()
2023-01-12 17:09:49 +00:00
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)
2023-01-25 22:51:41 +00:00
s.confirm.EnableHotkeys()
s.confirm.SetX(w / 4)
s.confirm.SetY(h / 4)
s.confirm.SetWidth(w / 2)
s.confirm.SetHeight(h / 2)
2023-01-12 17:09:49 +00:00
s.updateFullFilterList()
2023-01-12 12:53:26 +00:00
return nil
}
func (s *listTimersScreen) Update(msg wandle.Msg) wandle.Cmd {
switch msg := msg.(type) {
case ScreenMsg:
2023-02-02 12:08:50 +00:00
s.err = msg.err
2023-01-12 12:53:26 +00:00
case termbox.Event:
return s.handleTermboxEvent(msg)
}
return nil
}
func (s *listTimersScreen) View(style wandle.Style) {
_, h := termbox.Size()
y := 2
2023-01-12 17:09:49 +00:00
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++
2023-01-12 12:53:26 +00:00
}
y++
2023-01-12 17:09:49 +00:00
printedTimers = s.timerFilterList.Size()
2023-01-12 12:53:26 +00:00
}
2023-01-12 17:09:49 +00:00
if s.activeToggle == activeToggleAll || s.activeToggle == activeToggleInactive {
wandle.Print(1, y, style.Bold(true), "Done Timers")
2023-01-12 12:53:26 +00:00
y++
2023-01-12 17:09:49 +00:00
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++
}
2023-01-12 12:53:26 +00:00
}
2023-01-25 22:51:41 +00:00
selectedStatus := fmt.Sprintf("%s", s.getSelectedTimerDuration())
2023-01-12 12:53:26 +00:00
if len(s.selected) > 0 {
2023-01-25 22:51:41 +00:00
selectedStatus = fmt.Sprintf("%s (%d / %d selected)", selectedStatus, len(s.selected), s.fullFilterList.Size())
2023-01-12 12:53:26 +00:00
}
2023-01-25 22:51:41 +00:00
wandle.Print(1, h-2, style, selectedStatus)
2023-02-02 12:08:50 +00:00
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)
2023-01-25 22:51:41 +00:00
wandle.Print(1, h-1, style, help)
2023-01-12 12:53:26 +00:00
s.scrollbar.View(style)
if s.menu.IsActive() {
s.menu.View(style)
}
2023-01-12 17:09:49 +00:00
if s.choiceMenu.IsActive() {
s.choiceMenu.View(style)
}
if s.tagEditor.IsActive() {
s.tagEditor.View(style)
}
2023-01-25 22:51:41 +00:00
if s.confirm.IsActive() {
s.confirm.View(style)
}
2023-02-02 12:08:50 +00:00
wandle.Print(1, h-3, style, s.msg)
if s.err != nil {
wandle.Print(1, h-4, ErrStyle, s.err.Error())
}
2023-01-12 12:53:26 +00:00
}
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 {
2023-01-25 22:51:41 +00:00
if s.confirm.IsActive() {
return s.confirm.Update(msg)
}
2023-01-12 17:09:49 +00:00
if s.choiceMenu.IsActive() {
2023-02-02 12:08:50 +00:00
if msg.Type == termbox.EventKey && msg.Key == termbox.KeyEsc {
s.choiceMenu.SetActive(false)
return wandle.EmptyCmd
} else {
return s.choiceMenu.Update(msg)
}
2023-01-12 17:09:49 +00:00
}
if s.tagEditor.IsActive() {
return s.tagEditor.Update(msg)
2023-01-12 12:53:26 +00:00
}
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 {
2023-01-12 17:09:49 +00:00
// TODO: Edit the entry
2023-01-12 12:53:26 +00:00
/*
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
}
2023-01-12 17:09:49 +00:00
if s.cursor < s.fullFilterList.Size()-1 {
s.cursor++
}
2023-01-25 22:51:41 +00:00
} else if msg.Ch == 'T' {
2023-01-12 17:09:49 +00:00
s.activeToggle = (s.activeToggle + 1) % activeToggleErr
s.updateFullFilterList()
2023-01-25 22:51:41 +00:00
} else if msg.Ch == 'A' {
return s.showArchiveSelected()
2023-01-12 12:53:26 +00:00
} 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' {
2023-01-12 17:09:49 +00:00
if s.cursor < s.fullFilterList.Size()-1 {
2023-01-12 12:53:26 +00:00
s.cursor++
} else {
2023-01-12 17:09:49 +00:00
s.cursor = s.fullFilterList.Size() - 1
2023-01-12 12:53:26 +00:00
}
return nil
} else if msg.Ch == 'G' {
2023-01-12 17:09:49 +00:00
s.cursor = s.fullFilterList.Size() - 1
2023-02-02 12:08:50 +00:00
} else if msg.Ch == 'g' {
s.cursor = 0
2023-01-12 12:53:26 +00:00
} else if msg.Ch == 't' {
// Edit tag(s)
2023-01-12 17:09:49 +00:00
return s.showEditTagsChoice()
2023-01-12 12:53:26 +00:00
} else if msg.Ch == 'p' {
// Edit project(s)
2023-01-12 17:09:49 +00:00
// TODO: Prompt for Choice: Add/Edit/Remove
2023-01-12 12:53:26 +00:00
projs := s.fullList.GetProjects()
2023-01-12 17:09:49 +00:00
_ = projs
2023-01-12 12:53:26 +00:00
} else if msg.Ch == 'c' {
// Edit context(s)
2023-01-12 17:09:49 +00:00
// TODO: Prompt for choice: Add/Edit/Remove
2023-01-12 12:53:26 +00:00
ctxts := s.fullList.GetContexts()
2023-01-12 17:09:49 +00:00
_ = ctxts
2023-01-25 22:51:41 +00:00
} 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
}
}
2023-01-12 17:09:49 +00:00
}
}
return nil
}
2023-01-25 22:51:41 +00:00
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)))
2023-02-02 12:08:50 +00:00
s.confirm.SetMessage("Are you sure you want to archive these timers? (y/n)")
2023-01-25 22:51:41 +00:00
} else {
s.confirm.SetTitle("Archive Timer?")
2023-02-02 12:08:50 +00:00
s.confirm.SetMessage("Are you sure you want to archive this timer? (y/n)")
2023-01-25 22:51:41 +00:00
}
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 {
2023-02-02 12:08:50 +00:00
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
2023-01-25 22:51:41 +00:00
}
}
2023-02-02 12:08:50 +00:00
}
} 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
2023-01-25 22:51:41 +00:00
}
}
}
}
2023-02-02 12:08:50 +00:00
return wandle.EmptyCmd
2023-01-25 22:51:41 +00:00
}
2023-01-12 17:09:49 +00:00
func (s *listTimersScreen) showEditTagsChoice() wandle.Cmd {
tags := s.getSelectedTimerTags()
2023-01-25 22:51:41 +00:00
var showTagEditor = func(key, val string, multival bool) wandle.Cmd {
2023-01-12 17:09:49 +00:00
return func() wandle.Msg {
s.tagEditor.SetTag(key, val)
2023-01-25 22:51:41 +00:00
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
})
2023-01-12 17:09:49 +00:00
s.choiceMenu.SetActive(false)
s.tagEditor.SetActive(true)
2023-01-25 22:51:41 +00:00
s.tagEditor.SetMultiVal(multival)
2023-01-12 17:09:49 +00:00
return wandle.EmptyCmd
}
}
s.choiceMenu.SetTitle("")
s.choiceMenu.ClearOptions()
2023-01-25 22:51:41 +00:00
addTag := widdles.NewMenuItem("[A]dd New Tag")
addTag.SetHotkey(widdles.NewHotkeyCh('a'))
addTag.SetCommand(showTagEditor("", "", false))
2023-01-12 17:09:49 +00:00
s.choiceMenu.AddOption(addTag)
2023-01-25 22:51:41 +00:00
editTag := widdles.NewMenuItem("[E]dit Tag")
editTag.SetHotkey(widdles.NewHotkeyCh('e'))
2023-01-12 17:09:49 +00:00
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
2023-01-25 22:51:41 +00:00
var multival bool
2023-01-12 17:09:49 +00:00
if len(v) == 1 {
vals = v[0]
2023-01-12 12:53:26 +00:00
} else {
2023-01-25 22:51:41 +00:00
vals = ""
multival = true
2023-01-12 12:53:26 +00:00
}
2023-01-12 17:09:49 +00:00
opt := widdles.NewMenuItem(fmt.Sprintf("%s (%s)", k, vals))
2023-01-25 22:51:41 +00:00
opt.SetCommand(showTagEditor(k, vals, multival))
2023-01-12 17:09:49 +00:00
s.choiceMenu.AddOption(opt)
2023-01-12 12:53:26 +00:00
}
2023-01-12 17:09:49 +00:00
return wandle.EmptyCmd
})
s.choiceMenu.AddOption(editTag)
2023-01-25 22:51:41 +00:00
removeTag := widdles.NewMenuItem("[R]emove Tag")
removeTag.SetHotkey(widdles.NewHotkeyCh('r'))
2023-01-12 17:09:49 +00:00
removeTag.SetCommand(func() wandle.Msg {
s.choiceMenu.ClearOptions()
s.choiceMenu.SetTitle("Choose Tag to Remove")
2023-02-02 12:08:50 +00:00
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)
2023-01-12 17:09:49 +00:00
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
2023-01-12 12:53:26 +00:00
}
}
func (s *listTimersScreen) gotoSettingsScreen() wandle.Msg {
return ScreenMsg{
source: ListTimersId,
command: CmdGotoSettings,
}
}
2023-01-12 17:09:49 +00:00
2023-02-02 12:08:50 +00:00
// 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
2023-01-12 17:09:49 +00:00
selected := len(s.selected)
if selected == 0 {
if s.cursor < s.fullFilterList.Size() {
2023-01-25 22:51:41 +00:00
var selTimer *timertxt.Timer
if selTimer, s.err = s.fullFilterList.GetTimer(s.cursor); s.err == nil {
2023-02-02 12:08:50 +00:00
ret = append(ret, selTimer)
2023-01-12 17:09:49 +00:00
}
}
} else {
for i := range s.selected {
if tmr, err := s.fullFilterList.GetTimer(i); err == nil {
2023-02-02 12:08:50 +00:00
ret = append(ret, tmr)
2023-01-12 17:09:49 +00:00
}
}
}
return ret
}
2023-02-02 12:08:50 +00:00
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)
2023-01-25 22:51:41 +00:00
}
}
2023-02-02 12:08:50 +00:00
return ret
}
func (s *listTimersScreen) updateTagOnSelectedTimers(key, val string) {
sel := s.getSelectedTimers()
for _, tmr := range sel {
tmr.AdditionalTags[key] = val
2023-01-25 22:51:41 +00:00
}
2023-02-02 12:08:50 +00:00
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)
2023-01-25 22:51:41 +00:00
}
}
2023-02-02 12:08:50 +00:00
s.writeLists()
2023-01-25 22:51:41 +00:00
}
2023-01-12 17:09:49 +00:00
func (s *listTimersScreen) getSelectedTimerProjects() []string {
var ret []string
2023-02-02 12:08:50 +00:00
sel := s.getSelectedTimers()
for _, tmr := range sel {
for _, v := range tmr.Contexts {
ret = util.AppendStringIfDistinct(ret, v)
2023-01-12 17:09:49 +00:00
}
}
return ret
}
func (s *listTimersScreen) getSelectedTimerContexts() []string {
var ret []string
2023-02-02 12:08:50 +00:00
sel := s.getSelectedTimers()
for _, tmr := range sel {
for _, v := range tmr.Contexts {
ret = util.AppendStringIfDistinct(ret, v)
2023-01-12 17:09:49 +00:00
}
}
return ret
}
2023-01-25 22:51:41 +00:00
func (s *listTimersScreen) getSelectedTimerDuration() time.Duration {
2023-02-02 12:08:50 +00:00
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
2023-01-25 22:51:41 +00:00
}
}
2023-02-02 12:08:50 +00:00
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++
2023-01-25 22:51:41 +00:00
}
}
2023-02-02 12:08:50 +00:00
return inActive == 0 || inDone == 0
2023-01-25 22:51:41 +00:00
}