From 69e1c959f6535d48fbe670a319a00e0715eb91e2 Mon Sep 17 00:00:00 2001 From: Brian Buller Date: Fri, 22 Feb 2019 17:56:27 -0600 Subject: [PATCH] UI Mode Work & Archiving --- bundle.go | 28 +++++ screen.go | 67 ++++++++++ screen_about.go | 93 ++++++++++++++ screen_main.go | 327 ++++++++++++++++++++++++++++++++++++++++++++++++ timer_ops.go | 212 +++++++++++++++++++++++++++++++ ui_loop.go | 56 +++++++++ 6 files changed, 783 insertions(+) create mode 100644 bundle.go create mode 100644 screen.go create mode 100644 screen_about.go create mode 100644 screen_main.go create mode 100644 timer_ops.go create mode 100644 ui_loop.go diff --git a/bundle.go b/bundle.go new file mode 100644 index 0000000..8d51992 --- /dev/null +++ b/bundle.go @@ -0,0 +1,28 @@ +package main + +type Bundle map[string]interface{} + +func (b Bundle) setValue(key string, val interface{}) { + b[key] = val +} + +func (b Bundle) getBool(key string, def bool) bool { + if v, ok := b[key].(bool); ok { + return v + } + return def +} + +func (b Bundle) getString(key, def string) string { + if v, ok := b[key].(string); ok { + return v + } + return def +} + +func (b Bundle) getInt(key string, def int) int { + if v, ok := b[key].(int); ok { + return v + } + return def +} diff --git a/screen.go b/screen.go new file mode 100644 index 0000000..56537c3 --- /dev/null +++ b/screen.go @@ -0,0 +1,67 @@ +package main + +import ( + termbox "github.com/nsf/termbox-go" +) + +type Screen interface { + handleKeyEvent(termbox.Event) int + initialize(Bundle) error + drawScreen() +} + +const ( + ScreenMain = iota + ScreenAbout + ScreenExit + + DefaultBg = termbox.ColorBlack + DefaultFg = termbox.ColorWhite + TitleFg = termbox.ColorWhite + TitleBg = termbox.ColorBlue + CursorFg = termbox.ColorBlack + CursorBg = termbox.ColorGreen +) + +func (a *AppState) BuildScreens() { + mainScreen := MainScreen{} + aboutScreen := AboutScreen{} + a.screens = append(a.screens, &mainScreen) + a.screens = append(a.screens, &aboutScreen) +} + +func (a *AppState) drawBackground(bg termbox.Attribute) { + termbox.Clear(0, bg) +} + +func (a *AppState) layoutAndDrawScreen(s Screen) { + a.drawBackground(DefaultBg) + s.drawScreen() + termbox.Flush() +} + +func readUserInput(e chan termbox.Event) { + for { + e <- termbox.PollEvent() + } +} + +func refreshList(e chan termbox.Event) { + /* + for { + time.Sleep(5 * time.Minute) + app.LoadTasklist() + app.LoadDoneList() + e <- termbox.Event{Type: termbox.EventNone} + } + */ +} + +/* + * ViewPort helps keep track of what's being displayed on the screen + */ +type ViewPort struct { + bytesPerRow int + numberOfRows int + firstRow int +} diff --git a/screen_about.go b/screen_about.go new file mode 100644 index 0000000..02a09f8 --- /dev/null +++ b/screen_about.go @@ -0,0 +1,93 @@ +package main + +import ( + "fmt" + "time" + + "github.com/br0xen/termbox-util" + termbox "github.com/nsf/termbox-go" +) + +// AboutScreen holds all that's going on +type AboutScreen struct { + viewPort ViewPort + message string + messageTimeout time.Duration + messageTime time.Time + + titleTemplate []string + commandsCol1 []Command + commandsCol2 []Command +} + +type Command struct { + key string + description string +} + +func (screen *AboutScreen) initialize(bundle Bundle) error { + screen.titleTemplate = []string{ + " .__ ", + " ____ |__| _____ ____ ", + " / ___\\| |/ \\_/ __ \\ ", + " / /_/ > | Y Y \\ ___/ ", + " \\___ /|__|__|_| /\\___ >", + "/_____/ \\/ \\/ ", + } + + screen.commandsCol1 = []Command{ + Command{"j,↓", "down"}, + Command{"k,↑", "up"}, + Command{"l,→", "open task"}, + Command{"------", "---------"}, + Command{"g", "goto top"}, + Command{"G", "goto bottom"}, + Command{"ctrl+f", "jump down"}, + Command{"ctrl+b", "jump up"}, + } + screen.commandsCol2 = []Command{ + Command{"D", "archive timer to done.txt"}, + Command{"------", "---------"}, + Command{"?", "this screen"}, + Command{"q", "quit program"}, + } + + return nil +} + +func (screen *AboutScreen) handleKeyEvent(event termbox.Event) int { + return ScreenMain +} + +func (screen *AboutScreen) drawScreen() { + width, height := termbox.Size() + xPos := (width - len(screen.titleTemplate[0])) / 2 + yPos := 1 + for _, line := range screen.titleTemplate { + termboxUtil.DrawStringAtPoint(line, xPos, yPos, DefaultFg, DefaultBg) + yPos++ + } + + numCols := 2 + if width < 80 { + numCols = 1 + } + col1XPos := (width - (width * 3 / 4)) + col2XPos := (width - (width * 2 / 4)) + if numCols == 1 { + col2XPos = col1XPos + } + screen.drawCommandsAtPoint(screen.commandsCol1, col1XPos, yPos) + screen.drawCommandsAtPoint(screen.commandsCol2, col2XPos, yPos) + exitTxt := "Press any key to return to tasks" + termboxUtil.DrawStringAtPoint(exitTxt, (width-len(exitTxt))/2, height-1, TitleFg, TitleBg) +} + +func (screen *AboutScreen) drawCommandsAtPoint(commands []Command, x, y int) { + xPos, yPos := x, y + for _, cmd := range commands { + termboxUtil.DrawStringAtPoint(fmt.Sprintf("%6s", cmd.key), xPos, yPos, DefaultFg, DefaultBg) + termboxUtil.DrawStringAtPoint(cmd.description, xPos+8, yPos, DefaultFg, DefaultBg) + yPos++ + } +} diff --git a/screen_main.go b/screen_main.go new file mode 100644 index 0000000..e38671c --- /dev/null +++ b/screen_main.go @@ -0,0 +1,327 @@ +package main + +import ( + "fmt" + "strings" + "time" + + timertxt "git.bullercodeworks.com/brian/go-timertxt" + "github.com/br0xen/termbox-util" + termbox "github.com/nsf/termbox-go" +) + +const ( + MainBundleListKey = "mainscreen.list" + MainBundleFilterKey = "mainscreen.filter" + + MainBundleListRecent = "mainscreen.list.recent" + MainBundleListArchive = "mainscreen.list.archive" + + MainBackspaceNothing = iota + MainBackspaceMain + MainBackspaceFilter + + InputIDFilter = "filter" + InputIDAddTimer = "add timer" + InputIDUnArchiveTask = "move timer to active list? (y/n)" +) + +type MainScreen struct { + viewPort ViewPort + message string + messageTimeout time.Duration + messageTime time.Time + mode int + + cursor map[string]int + inputField *termboxUtil.InputField + + currentList string + currentFilter string + backspaceDoes int + displayList *timertxt.TimerList + activeList *timertxt.TimerList +} + +func (screen *MainScreen) initialize(bundle Bundle) error { + width, height := termbox.Size() + screen.inputField = termboxUtil.CreateInputField(0, (height - 3), width, 1, DefaultFg, DefaultBg) + + screen.cursor = make(map[string]int) + if bundle != nil { + if err := screen.reloadList(bundle); err != nil { + return err + } + screen.inputField.SetID("") + screen.inputField.SetBordered(false) + } + + return nil +} + +func (screen *MainScreen) reloadList(bundle Bundle) error { + screen.displayList = timertxt.NewTimerList() + screen.currentList = bundle.getString(MainBundleListKey, MainBundleListRecent) + switch screen.currentList { + case MainBundleListRecent: + screen.setActiveList(app.TimerList) + if screen.currentFilter = bundle.getString(MainBundleFilterKey, ""); screen.currentFilter != "" { + filteredList := app.filterList(screen.activeList, screen.currentFilter) + for _, av := range *screen.activeList { + for _, fv := range *filteredList { + if av.String() == fv.String() { + screen.displayList.AddTimer(&av) + break + } + } + } + } else { + for _, av := range *screen.activeList { + screen.displayList.AddTimer(&av) + } + } + case MainBundleListArchive: + //screen.setActiveList( + } + if screen.cursor[screen.currentList] > len(*screen.displayList)-1 { + screen.cursor[screen.currentList] = len(*screen.displayList) - 1 + } + return nil +} + +func (screen *MainScreen) reloadCurrentView() { + bundle := Bundle{} + bundle.setValue(MainBundleListKey, screen.currentList) + bundle.setValue(MainBundleFilterKey, screen.currentFilter) + screen.reloadList(bundle) +} + +func (screen *MainScreen) handleKeyEvent(event termbox.Event) int { + if screen.inputField.GetID() != "" { + return screen.handleInputKeyEvent(event) + } + if event.Ch == '?' { + // Go to About Screen + b := Bundle{} + if err := app.screens[ScreenAbout].initialize(b); err != nil { + screen.setMessage(err.Error()) + } + return ScreenAbout + + } else if event.Ch == 'g' { + screen.cursor[screen.currentList] = 0 + + } else if event.Ch == 'G' { + screen.cursor[screen.currentList] = len(*screen.displayList) - 1 + + } else if event.Key == termbox.KeyCtrlF { + // Jump forward half a screen + _, h := termbox.Size() + screen.cursor[screen.currentList] += (h / 2) + if screen.cursor[screen.currentList] >= len(*screen.displayList) { + screen.cursor[screen.currentList] = len(*screen.displayList) - 1 + } + + } else if event.Key == termbox.KeyCtrlB { + // Jump back half a screen + _, h := termbox.Size() + screen.cursor[screen.currentList] -= (h / 2) + if screen.cursor[screen.currentList] < 0 { + screen.cursor[screen.currentList] = 0 + } + + } else if event.Ch == 'j' || event.Key == termbox.KeyArrowDown { + screen.moveCursorDown() + + } else if event.Ch == 'k' || event.Key == termbox.KeyArrowUp { + screen.moveCursorUp() + + } else if event.Ch == '/' { + screen.startFilter() + + } else if event.Ch == 'L' { + return screen.toggleViewList() + + } else if event.Ch == 'q' { + return ScreenExit + + } + return ScreenMain +} + +func (screen *MainScreen) handleInputKeyEvent(event termbox.Event) int { + switch screen.inputField.GetID() { + case InputIDFilter: + if event.Key == termbox.KeyEnter { + // Apply the filter + filter := screen.inputField.GetValue() + screen.inputField.SetID("") + screen.inputField.SetValue("") + screen.backspaceDoes = MainBackspaceFilter + screen.reloadList(screen.buildBundle(screen.currentList, filter)) + return ScreenMain + } + case InputIDAddTimer: + if event.Key == termbox.KeyEnter { + // Create the new item + err := app.addTimer(screen.inputField.GetValue()) + if err != nil { + screen.setMessage(err.Error()) + } + screen.inputField.SetID("") + screen.inputField.SetValue("") + screen.reloadList(screen.buildBundle(screen.currentList, screen.currentFilter)) + return ScreenMain + } + } + if event.Key == termbox.KeyBackspace || event.Key == termbox.KeyBackspace2 { + if screen.inputField.GetValue() == "" { + screen.reloadList(screen.buildBundle(screen.currentList, screen.inputField.GetValue())) + screen.inputField.SetID("") + screen.inputField.SetValue("") + return ScreenMain + } + } else if event.Key == termbox.KeyEsc { + screen.reloadList(screen.buildBundle(screen.currentList, screen.currentFilter)) + screen.inputField.SetID("") + screen.inputField.SetValue("") + return ScreenMain + } + screen.inputField.HandleEvent(event) + return ScreenMain +} + +func (screen *MainScreen) drawScreen() { + _, height := termbox.Size() + screen.viewPort.numberOfRows = height - 1 + if screen.inputField.GetID() != "" { + screen.viewPort.numberOfRows-- + } + screen.viewPort.firstRow = 1 + displayOffset := 0 + maxCursor := screen.viewPort.numberOfRows * 2 / 3 + if screen.cursor[screen.currentList] > maxCursor { + displayOffset = screen.cursor[screen.currentList] - maxCursor + } + + if screen.message == "" { + screen.setMessageWithTimeout("Press '?' for help", -1) + } + screen.drawHeader() + topId := 0 + for _, v := range *screen.displayList { + if v.Id > topId { + topId = v.Id + } + } + padCnt := fmt.Sprintf("%d", topId) + for k, v := range *screen.displayList { + pad := strings.Repeat(" ", len(padCnt)-len(fmt.Sprintf("%d", v.Id))) + useFg, useBg := DefaultFg, DefaultBg + if k == screen.cursor[screen.currentList] { + useFg, useBg = CursorFg, CursorBg + } + lineY := k + 1 - displayOffset + if lineY > 0 && lineY < screen.viewPort.numberOfRows { + termboxUtil.DrawStringAtPoint(pad+app.getTimerString(v), 0, lineY, useFg, useBg) + } + } + screen.drawFooter() +} + +func (screen *MainScreen) drawHeader() { + width, _ := termbox.Size() + headerString := screen.currentFilter + if headerString == "" { + if screen.currentList == MainBundleListRecent { + headerString = "Timers" + } else if screen.currentList == MainBundleListArchive { + headerString = "Timer Archive" + } + } + spaces := strings.Repeat(" ", ((width-len(headerString))/2)+1) + termboxUtil.DrawStringAtPoint(fmt.Sprintf("%s%s%s", spaces, headerString, spaces), 0, 0, TitleFg, TitleBg) +} + +func (screen *MainScreen) drawFooter() { + if screen.messageTimeout > 0 && time.Since(screen.messageTime) > screen.messageTimeout { + screen.clearMessage() + } + width, height := termbox.Size() + if screen.inputField.GetID() != "" { + screen.inputField.SetX(len(screen.inputField.GetID()) + 2) + pad := width - len(screen.inputField.GetID()+":") + field := screen.inputField.GetID() + ":" + strings.Repeat(" ", pad) + termboxUtil.DrawStringAtPoint(field, 0, height-2, DefaultFg, DefaultBg) + screen.inputField.Draw() + } + // And the 'message' + termboxUtil.DrawStringAtPoint(screen.message, 0, height-1, DefaultFg, DefaultBg) +} + +func (screen *MainScreen) toggleViewList() int { + bundle := Bundle{} + if screen.currentList == MainBundleListRecent { + bundle.setValue(MainBundleListKey, MainBundleListArchive) + screen.backspaceDoes = MainBackspaceMain + } else { + bundle.setValue(MainBundleListKey, MainBundleListRecent) + } + bundle.setValue(MainBundleFilterKey, screen.currentFilter) + screen.reloadList(bundle) + return ScreenMain +} + +func (screen *MainScreen) moveCursorDown() bool { + screen.cursor[screen.currentList]++ + if screen.cursor[screen.currentList] >= len(*screen.displayList) { + screen.cursor[screen.currentList] = len(*screen.displayList) - 1 + return false + } + return true +} + +func (screen *MainScreen) moveCursorUp() bool { + screen.cursor[screen.currentList]-- + if screen.cursor[screen.currentList] < 0 { + screen.cursor[screen.currentList] = 0 + return false + } + return true +} + +func (screen *MainScreen) startFilter() int { + screen.inputField.SetID(InputIDFilter) + return ScreenMain +} + +func (screen *MainScreen) setMessage(msg string) { + screen.message = msg + screen.messageTime = time.Now() + screen.messageTimeout = time.Second * 2 +} + +/* setMessageWithTimeout lets you specify the timeout for the message + * setting it to -1 means it won't timeout + */ +func (screen *MainScreen) setMessageWithTimeout(msg string, timeout time.Duration) { + screen.message = msg + screen.messageTime = time.Now() + screen.messageTimeout = timeout +} + +func (screen *MainScreen) clearMessage() { + screen.message = fmt.Sprintf("%d Total Tasks", len(*screen.activeList)) + screen.messageTimeout = -1 +} + +func (screen *MainScreen) buildBundle(list, filter string) Bundle { + bundle := Bundle{} + bundle.setValue(MainBundleListKey, list) + bundle.setValue(MainBundleFilterKey, filter) + return bundle +} + +func (screen *MainScreen) setActiveList(list *timertxt.TimerList) { + screen.activeList = list +} diff --git a/timer_ops.go b/timer_ops.go new file mode 100644 index 0000000..3676892 --- /dev/null +++ b/timer_ops.go @@ -0,0 +1,212 @@ +package main + +import ( + "fmt" + "strconv" + "strings" + "time" + + timertxt "git.bullercodeworks.com/brian/go-timertxt" +) + +func (a *AppState) opStatus(args []string) int { + if len(*a.TimerList.GetActiveTimers()) == 0 { + fmt.Println("No timers running") + return 0 + } + var currDur time.Duration + for _, v := range *a.TimerList { + if v.ActiveToday() { + currDur += v.Duration() + } + } + d := currDur.Round(GetRoundToDuration()) + fmt.Printf("%s ( %.2f hrs )\n", time.Now().Format(time.Stamp), DurationToDecimal(d)) + for _, v := range *a.TimerList.GetActiveTimers() { + fmt.Println(timerToFriendlyString(&v)) + } + return 0 +} + +/** + * List timers for a given time span + * By default, only list Today + */ +func (a *AppState) opListTimers(args []string) int { + var start, end time.Time + var err error + + if len(args) > 0 { + if args[0] == "--a" { + start = time.Time{} + end = time.Now() + args = args[1:] + } else { + if start, err = parseFuzzyTime(args[0]); err != nil { + y, m, d := time.Now().Date() + start = time.Date(y, m, d, 0, 0, 0, 0, time.Now().Location()) + } else { + args = args[1:] + } + if end, err = parseFuzzyTime(args[0]); err != nil { + y, m, d := time.Now().Date() + end = time.Date(y, m, d, 23, 59, 59, 0, time.Now().Location()) + } else { + args = args[1:] + } + } + } + + list := a.TimerList.GetTimersInRange(start, end) + dayTotals := make(map[string]time.Duration) + for _, v := range *list { + dur := v.FinishDate.Sub(v.StartDate) + if v.FinishDate.IsZero() { + dur = time.Now().Sub(v.StartDate) + } + dayTotals[v.StartDate.Format("2006/01/02")] += dur + } + var oldDayStr, dayStr string + for _, v := range *list { + oldDayStr = dayStr + dayStr = v.StartDate.Format("2006/01/02") + if dayStr != oldDayStr { + wrkDur := dayTotals[dayStr].Round(GetRoundToDuration()) + fmtStr := dayStr + " ( %.2f )\n" + fmt.Printf(fmtStr, DurationToDecimal(wrkDur)) + } + + fmt.Println(" " + timerToFriendlyString(&v)) + } + return 0 +} + +func (a *AppState) opStartTimer(args []string) int { + var contexts, projects []string + t := timertxt.NewTimer() + contexts, args = getContextsFromSlice(args) + projects, args = getProjectsFromSlice(args) + if len(args) > 0 { + if start, err := parseFuzzyTime(args[0]); err == nil { + t.StartDate = start + args = args[1:] + } + } + for _, v := range contexts { + t.Contexts = append(t.Contexts, strings.TrimPrefix(v, "@")) + } + for _, v := range projects { + t.Projects = append(t.Projects, strings.TrimPrefix(v, "+")) + } + + a.TimerList.AddTimer(t) + if err := a.WriteList(); err != nil { + fmt.Println(err.Error()) + return 1 + } + return 0 +} + +func (a *AppState) opStopTimer(args []string) int { + var err error + var wrk time.Time + end := time.Now() + id := -1 + + if len(args) > 0 { + if wrk, err = 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 *a.TimerList.GetActiveTimers() { + timerIds = append(timerIds, v.Id) + } + } else { + timerIds = append(timerIds, id) + } + for _, v := range timerIds { + var stopped *timertxt.Timer + if stopped, err = a.TimerList.GetTimer(v); err != nil { + fmt.Println(err.Error()) + } + if err = a.SetTimerFinished(v, end); err != nil { + fmt.Println(err.Error()) + continue + } + fmt.Println("Stopped Timer:", timerToFriendlyString(stopped)) + } + if err = a.WriteList(); err != nil { + fmt.Println(err.Error()) + return 1 + } + return 0 +} + +func (a *AppState) opSwitchTimer(args []string) int { + return 0 +} + +func (a *AppState) opArchiveTimer(args []string) int { + if len(args) > 0 { + for _, v := range args { + var id int + var timer *timertxt.Timer + var err error + if id, err = strconv.Atoi(v); err != nil { + fmt.Printf("Invalid id given: %s\n", v) + return 1 + } + if timer, err = a.TimerList.GetTimer(id); err != nil { + fmt.Printf("Error getting timer %d\n", id) + return 1 + } + if err = a.archiveTimer(id); err != nil { + fmt.Printf("Error archiving timer %d\n", id) + return 1 + } + fmt.Println(a.getDoneTimerString(*timer)) + } + } else { + for _, v := range *a.TimerList { + if v.Finished { + if err := a.archiveTimer(v.Id); err != nil { + fmt.Printf("Error archiving task %d\n", v.Id) + return 1 + } + fmt.Println(a.getDoneTimerString(v)) + } + } + } + return 0 +} + +func (a *AppState) opState(args []string) int { + return 0 +} + +func (a *AppState) opFuzzyParse(args []string) int { + if len(args) > 0 { + if start, err := parseFuzzyTime(args[0]); err == nil { + fmt.Println(start.Format(time.RFC3339)) + } else { + fmt.Println(err.Error()) + } + } + return 0 +} + +func (a *AppState) opPrintUsage(args []string) int { + for _, v := range a.ValidOperations { + for _, vv := range v { + fmt.Println(" " + vv) + fmt.Println("") + } + } + return 0 +} diff --git a/ui_loop.go b/ui_loop.go new file mode 100644 index 0000000..8cd7ce7 --- /dev/null +++ b/ui_loop.go @@ -0,0 +1,56 @@ +package main + +import ( + "fmt" + "os" + "runtime" + "syscall" + + termbox "github.com/nsf/termbox-go" +) + +func uiLoop() int { + err := termbox.Init() + if err != nil { + fmt.Println(err.Error()) + return 1 + } + termbox.SetOutputMode(termbox.Output256) + app.BuildScreens() + displayScreen := app.screens[ScreenMain] + bundle := Bundle{} + bundle.setValue(MainBundleListKey, MainBundleListRecent) + displayScreen.initialize(bundle) + app.layoutAndDrawScreen(displayScreen) + eventChan := make(chan termbox.Event) + go readUserInput(eventChan) + for { + event := <-eventChan + if event.Type == termbox.EventKey { + if event.Key == termbox.KeyCtrlC { + break + } else if event.Key == termbox.KeyCtrlZ { + if runtime.GOOS != "windows" { + process, _ := os.FindProcess(os.Getpid()) + termbox.Close() + process.Signal(syscall.SIGSTOP) + termbox.Init() + } + } + newScreenIndex := displayScreen.handleKeyEvent(event) + if newScreenIndex < len(app.screens) { + displayScreen = app.screens[newScreenIndex] + app.layoutAndDrawScreen(displayScreen) + } else { + break + } + } + if event.Type == termbox.EventResize { + displayScreen.initialize(nil) + app.layoutAndDrawScreen(displayScreen) + } + } + termbox.Close() + // Any wrap up should be done here... + return 0 +}