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.SORT_START_DATE_DESC) s.doneFilterList.Sort(timertxt.SORT_START_DATE_DESC) 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 }