package main import ( "errors" "fmt" "strings" "time" todotxt "github.com/br0xen/go-todotxt" "github.com/br0xen/termbox-screen" "github.com/br0xen/termbox-util" termbox "github.com/nsf/termbox-go" ) const MainScreenId = 0 type ViewPort struct { bytesPerRow int numberOfRows int firstRow int } // MainScreen holds all that's going on type MainScreen struct { viewPort ViewPort message string messageTimeout time.Duration messageTime time.Time messageColorBg termbox.Attribute messageColorFg termbox.Attribute mode int cursor map[string]int inputField *termboxUtil.InputField currentList string currentFilter string activeList *todotxt.TaskList displayList *todotxt.TaskList undoQueue []string redoQueue []string backspaceDoes int } const ( MainBundleListKey = "mainscreen.list" MainBundleFilterKey = "mainscreen.filter" MainBundleListTodo = "mainscreen.list.todo" MainBundleListDone = "mainscreen.list.done" MainBackspaceNothing = iota MainBackspaceMain MainBackspaceFilter InputIDFilter = "filter" InputIDAddTask = "add task" InputIDIncompleteArchive = "archive incomplete task? (y/n)" InputIDUnArchiveTask = "move task to active list? (y/n)" ) func (screen *MainScreen) Id() int { return MainScreenId } func (screen *MainScreen) Initialize(bundle termboxScreen.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) ResizeScreen() { screen.Initialize(nil) } func (screen *MainScreen) refreshList(bundle termboxScreen.Bundle) error { whichList := bundle.GetString(MainBundleListKey, MainBundleListTodo) switch whichList { case MainBundleListTodo: return app.LoadTaskList() case MainBundleListDone: return app.LoadDoneList() } return errors.New("Invalid refresh request.") } func (screen *MainScreen) reloadList(bundle termboxScreen.Bundle) error { // We add tasks to the display list using append because we want to persist task Ids screen.displayList = todotxt.NewTaskList() screen.currentList = bundle.GetString(MainBundleListKey, MainBundleListTodo) switch screen.currentList { case MainBundleListTodo: screen.setActiveList(app.TaskList) 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) = append(*screen.displayList, av) break } } } } else { for _, av := range *screen.activeList { (*screen.displayList) = append(*screen.displayList, av) } } case MainBundleListDone: if err := app.LoadDoneList(); err != nil { return err } screen.setActiveList(app.DoneList) 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) = append(*screen.displayList, av) break } } } } else { for _, av := range *screen.activeList { (*screen.displayList) = append(*screen.displayList, av) } } } if screen.cursor[screen.currentList] > len(*screen.displayList)-1 { screen.cursor[screen.currentList] = len(*screen.displayList) - 1 } return nil } func (screen *MainScreen) HandleKeyEvent(event termbox.Event) int { if screen.inputField.GetID() != "" { return screen.handleInputKeyEvent(event) } if event.Ch == '?' { // Go to About Screen b := termboxScreen.Bundle{} if err := app.uiManager.InitializeScreen(AboutScreenId, b); err != nil { screen.setErrorMessage(err.Error()) } return AboutScreenId } else if event.Key == termbox.KeyBackspace || event.Key == termbox.KeyBackspace2 { if screen.backspaceDoes == MainBackspaceNothing { if screen.currentList == MainBundleListDone { screen.reloadList(screen.buildBundle(MainBundleListTodo, screen.currentFilter)) } else if screen.currentFilter != "" { screen.reloadList(screen.buildBundle(screen.currentList, "")) } } else if screen.backspaceDoes == MainBackspaceMain { screen.reloadList(screen.buildBundle(MainBundleListTodo, screen.currentFilter)) } else if screen.backspaceDoes == MainBackspaceFilter { screen.reloadList(screen.buildBundle(screen.currentList, "")) } return MainScreenId } else if event.Key == termbox.KeySpace { return screen.toggleTaskComplete() } 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.KeyCtrlR { b := screen.buildBundle(screen.currentList, screen.currentFilter) screen.refreshList(b) screen.reloadList(b) } 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 == 'L' { return screen.toggleViewList() } else if event.Ch == 'a' { return screen.startAddNewTask() } else if event.Ch == 'l' || event.Key == termbox.KeyEnter || event.Key == termbox.KeyArrowRight { return screen.startEditTaskScreen() } 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 == 'G' { screen.cursor[screen.currentList] = len(*screen.displayList) - 1 } else if event.Ch == 'g' { screen.cursor[screen.currentList] = 0 } else if event.Ch == '/' { screen.startFilter() } else if event.Ch == 'D' { screen.confirmArchiveItem() } else if event.Ch == 'q' { return ExitScreenId } return MainScreenId } 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 MainScreenId } case InputIDAddTask: if event.Key == termbox.KeyEnter { // Create the new item err := app.addTask(screen.inputField.GetValue()) if err != nil { screen.setErrorMessage(err.Error()) } screen.inputField.SetID("") screen.inputField.SetValue("") screen.reloadList(screen.buildBundle(screen.currentList, screen.currentFilter)) return MainScreenId } case InputIDIncompleteArchive: if event.Ch == 'y' || event.Ch == 'Y' { return screen.archiveCurrentItem() } screen.inputField.SetID("") screen.inputField.SetValue("") screen.reloadList(screen.buildBundle(screen.currentList, screen.currentFilter)) return MainScreenId case InputIDUnArchiveTask: if event.Ch == 'y' || event.Ch == 'Y' { screen.inputField.SetID("") screen.inputField.SetValue("") screen.reloadList(screen.buildBundle(screen.currentList, screen.currentFilter)) return screen.unarchiveCurrentItem() } screen.inputField.SetID("") screen.inputField.SetValue("") screen.reloadList(screen.buildBundle(screen.currentList, screen.currentFilter)) return MainScreenId } 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 MainScreenId } } else if event.Key == termbox.KeyEsc { screen.reloadList(screen.buildBundle(screen.currentList, screen.currentFilter)) screen.inputField.SetID("") screen.inputField.SetValue("") return MainScreenId } screen.inputField.HandleEvent(event) return MainScreenId } func (screen *MainScreen) setActiveList(list *todotxt.TaskList) { screen.activeList = list } 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.getTaskString(v), 0, lineY, useFg, useBg) } } screen.drawFooter() } func (screen *MainScreen) drawHeader() { width, _ := termbox.Size() headerString := screen.currentFilter if headerString == "" { if screen.currentList == MainBundleListTodo { headerString = "Todo List" } else if screen.currentList == MainBundleListDone { headerString = "Done List" } } 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, screen.messageColorFg, screen.messageColorBg) } func (screen *MainScreen) confirmArchiveItem() int { if screen.currentList != MainBundleListTodo { screen.inputField.SetID(InputIDUnArchiveTask) return MainScreenId } // Find the task under the cursor if screen.cursor[screen.currentList] < len(*screen.displayList) { t := (*screen.displayList)[screen.cursor[screen.currentList]] if !t.Completed { // Task isn't completed, verify that the user wants to archive it screen.inputField.SetID(InputIDIncompleteArchive) } else { return screen.archiveCurrentItem() } } return MainScreenId } func (screen *MainScreen) archiveCurrentItem() int { if screen.currentList != MainBundleListTodo { screen.setErrorMessage("Task is already archived") return MainScreenId } // Find the task under the cursor if len(*screen.displayList) > screen.cursor[screen.currentList] { t := (*screen.displayList)[screen.cursor[screen.currentList]] if err := app.archiveTask(t.Id); err != nil { screen.setErrorMessage(err.Error()) return MainScreenId } // Reload the list b := screen.buildBundle(screen.currentList, screen.currentFilter) if err := screen.reloadList(b); err != nil { screen.setErrorMessage(err.Error()) } } return MainScreenId } func (screen *MainScreen) unarchiveCurrentItem() int { if screen.currentList == MainBundleListTodo { screen.setErrorMessage("Task is not archived") return MainScreenId } // Find the task under the cursor if len(*screen.displayList) > screen.cursor[screen.currentList] { t := (*screen.displayList)[screen.cursor[screen.currentList]] if err := app.unarchiveTask(t.Id); err != nil { screen.setErrorMessage(err.Error()) return MainScreenId } // Reload the list b := screen.buildBundle(screen.currentList, screen.currentFilter) if err := screen.reloadList(b); err != nil { screen.setErrorMessage(err.Error()) } } return MainScreenId } func (screen *MainScreen) startEditTaskScreen() int { // Find the task under the cursor if len(*screen.displayList) > screen.cursor[screen.currentList] { t := (*screen.displayList)[screen.cursor[screen.currentList]] // Load the task screen with this task b := termboxScreen.Bundle{} b.SetValue(TaskBundleTaskIdKey, t.Id) if err := app.uiManager.InitializeScreen(TaskScreenId, b); err != nil { screen.setErrorMessage(err.Error()) return MainScreenId } return TaskScreenId } return MainScreenId } func (screen *MainScreen) reloadCurrentView() { bundle := termboxScreen.Bundle{} bundle.SetValue(MainBundleListKey, screen.currentList) bundle.SetValue(MainBundleFilterKey, screen.currentFilter) screen.reloadList(bundle) } func (screen *MainScreen) toggleViewList() int { bundle := termboxScreen.Bundle{} if screen.currentList == MainBundleListTodo { bundle.SetValue(MainBundleListKey, MainBundleListDone) screen.backspaceDoes = MainBackspaceMain } else { bundle.SetValue(MainBundleListKey, MainBundleListTodo) } bundle.SetValue(MainBundleFilterKey, screen.currentFilter) screen.reloadList(bundle) return MainScreenId } func (screen *MainScreen) startAddNewTask() int { screen.inputField.SetID(InputIDAddTask) return MainScreenId } func (screen *MainScreen) toggleTaskComplete() int { if screen.currentList == MainBundleListDone { screen.setErrorMessage("Task is archived, unable to modify.") return MainScreenId } // Find the task under the cursor if len(*screen.displayList) > screen.cursor[screen.currentList] { t := (*screen.displayList)[screen.cursor[screen.currentList]] err := app.toggleTaskComplete(t.Id) if err != nil { screen.setErrorMessage(err.Error()) return MainScreenId } } screen.reloadCurrentView() return MainScreenId } 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 MainScreenId } func (screen *MainScreen) setErrorMessage(msg string) { screen.message = " " + msg + " " screen.messageTime = time.Now() screen.messageTimeout = time.Second * 2 screen.messageColorBg = termbox.ColorRed screen.messageColorFg = termbox.ColorWhite | termbox.AttrBold } func (screen *MainScreen) setMessage(msg string) { screen.message = msg screen.messageTime = time.Now() screen.messageTimeout = time.Second * 2 screen.messageColorBg = DefaultBg screen.messageColorFg = DefaultFg } /* 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 screen.messageColorBg = DefaultBg screen.messageColorFg = DefaultFg } func (screen *MainScreen) buildBundle(list, filter string) termboxScreen.Bundle { bundle := termboxScreen.Bundle{} bundle.SetValue(MainBundleListKey, list) bundle.SetValue(MainBundleFilterKey, filter) return bundle }