diff --git a/app_state.go b/app_state.go new file mode 100644 index 0000000..b0ec740 --- /dev/null +++ b/app_state.go @@ -0,0 +1,207 @@ +package main + +import ( + "fmt" + "os" + "strings" + + todotxt "github.com/JamesClonk/go-todotxt" + "github.com/br0xen/user-config" +) + +type AppState struct { + Name string + Version int + config *userConfig.Config + directory string + fileTodo string + fileDone string + fileReport string + ValidOperations map[string][]string + OpFuncs map[string]func([]string) int + + TaskList *todotxt.TaskList + DoneList *todotxt.TaskList + + screens []Screen +} + +func NewApp() *AppState { + app := &AppState{Name: AppName, Version: AppVersion} + app.initialize() + app.doVersionCheck() + if err := app.LoadTaskList(); err != nil { + if len(os.Args) > 1 && os.Args[1] != "--reinit" { + panic(err) + } + } + return app +} + +func (a *AppState) run(parms []string) int { + if len(parms) == 0 || parms[0] == "ui" { + // UI Mode + return uiLoop() + } + if fn, ok := a.OpFuncs[parms[0]]; ok { + return fn(parms[1:]) + } + fmt.Println("Unknown Command") + return 1 +} + +func (a *AppState) filterList(list *todotxt.TaskList, filter string) *todotxt.TaskList { + return list.Filter(a.getFilterPredicate(filter)) +} + +func (a *AppState) getFilteredList(filter string) *todotxt.TaskList { + return a.TaskList.Filter(a.getFilterPredicate(filter)) +} + +func (a *AppState) getFilteredDoneList(filter string) *todotxt.TaskList { + return a.DoneList.Filter(a.getFilterPredicate(filter)) +} + +func (a *AppState) getTodoFile() string { + return a.directory + a.fileTodo +} +func (a *AppState) getDoneFile() string { + return a.directory + a.fileDone +} +func (a *AppState) getReportFile() string { + return a.directory + a.fileReport +} + +func (a *AppState) addOperation(name string, desc []string, fn func([]string) int) { + a.ValidOperations[name] = desc + a.OpFuncs[name] = fn +} + +func (a *AppState) getTaskString(task todotxt.Task) string { + var completed string + completed = " " + if task.Completed { + completed = "X" + } + return fmt.Sprintf("%d. [%s] %s", task.Id, completed, strings.TrimPrefix(task.String(), "x ")) +} + +func (a *AppState) getDoneTaskString(task todotxt.Task) string { + var completed string + completed = " " + if task.Completed { + completed = "X" + } + return fmt.Sprintf("0. [%s] %s", completed, strings.TrimPrefix(task.String(), "x ")) +} + +func (a *AppState) doVersionCheck() { + confVer, _ := a.config.GetInt("version") + for confVer < a.Version { + confVer = a.migrate(confVer, a.Version) + } + a.config.SetInt("version", confVer) +} + +func (a *AppState) migrate(from, to int) int { + if from == to { + return to + } + switch from { + case 0: + a.initializeConfig() + return 1 + } + // If we get all the way down here, we _must_ be done. + return to +} + +func (a *AppState) initialize() { + var err error + a.config, err = userConfig.NewConfig(a.Name) + if err != nil { + panic(err) + } + a.ValidOperations = make(map[string][]string) + a.OpFuncs = make(map[string]func([]string) int) + a.addOperation("ls", + []string{"ls - List Tasks"}, + a.opListTasks, + ) + a.addOperation("lsa", + []string{"lsa - The same as 'ls -a'"}, + func(args []string) int { + return a.opListTasks(append([]string{"-a"}, args...)) + }, + ) + a.addOperation("add", + []string{"add - Add a task"}, + a.opAddTask, + ) + a.addOperation("new", + []string{"new - Same as 'add'"}, + a.opAddTask, + ) + a.addOperation("x", + []string{"x - Toggle a task's complete flag on/off"}, + a.opToggleTaskComplete, + ) + a.addOperation("done", + []string{"done - The same as 'x'"}, + a.opToggleTaskComplete, + ) + a.addOperation("archive", + []string{"archive [id1 id2 ...] - Archive completed tasks"}, + a.opArchiveTasks, + ) + a.addOperation("--reinit", + []string{"--reinit - Reset all Configuration Settings"}, + func(args []string) int { + a.initializeConfig() + return 0 + }, + ) + a.addOperation("-h", + []string{"-h - Print this message"}, + a.opPrintUsage, + ) + a.addOperation("help", + []string{"help - Print this message"}, + a.opPrintUsage, + ) + a.addOperation("--h", + []string{"--h - Print this message"}, + a.opPrintUsage, + ) + a.directory = a.config.Get("directory") + a.fileTodo = a.config.Get("todofile") + a.fileDone = a.config.Get("donefile") + a.fileReport = a.config.Get("reportfile") +} + +func (a *AppState) initializeConfig() { + fmt.Println("Initializing " + a.Name) + for { + var add string + if a.directory != "" { + add = " (" + a.directory + ")" + } + fmt.Println("Path to todo.txt" + add + ":") + var resp string + fmt.Scanln(&resp) + if resp == "" && a.directory != "" { + resp = a.directory + } + if resp != "" { + if !strings.HasSuffix(resp, "/") { + resp = resp + "/" + } + fmt.Println("Setting todo.txt directory to: " + resp) + a.config.Set("directory", resp) + break + } + } + a.config.Set("todofile", "todo.txt") + a.config.Set("donefile", "done.txt") + a.config.Set("reportfile", "report.txt") +} 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/main.go b/main.go new file mode 100644 index 0000000..74f8721 --- /dev/null +++ b/main.go @@ -0,0 +1,25 @@ +package main + +import ( + "os" +) + +const ( + AppName = "gask" + AppVersion = 1 +) + +var app *AppState + +func main() { + app = NewApp() + + var parms []string + if len(os.Args) > 1 { + parms = os.Args[1:] + } else { + // If no parameters were passed, just do an ls + parms = append(parms, "ui") + } + os.Exit(app.run(parms)) +} diff --git a/model.go b/model.go new file mode 100644 index 0000000..ff2fff5 --- /dev/null +++ b/model.go @@ -0,0 +1,117 @@ +package main + +import ( + "strings" + "time" + + todotxt "github.com/JamesClonk/go-todotxt" +) + +func (a *AppState) addTask(taskString string) error { + t, err := todotxt.ParseTask(taskString) + if err != nil { + return err + } + if t.CreatedDate.IsZero() { + t.CreatedDate = time.Now() + } + a.TaskList.AddTask(t) + return nil +} + +func (a *AppState) toggleTaskComplete(id int) error { + var task *todotxt.Task + var err error + if task, err = a.TaskList.GetTask(id); err != nil { + return err + } + if task.Completed { + task.Reopen() + } else { + task.Complete() + } + return nil +} + +func (a *AppState) archiveTask(id int) error { + var err error + var task *todotxt.Task + if task, err = a.TaskList.GetTask(id); err != nil { + return err + } + a.TaskList.RemoveTask(*task) + task.Completed = true + a.DoneList.AddTask(task) + return nil +} + +func (a *AppState) getFilterPredicate(filter string) func(todotxt.Task) bool { + var predicates []func(todotxt.Task) bool + // If none of the 'filter' is in upper-case, do a case-insensitive filter + checkCase := true + if strings.ToLower(filter) == filter { + checkCase = false + } + filterParts := strings.Split(filter, " ") + for _, part := range filterParts { + if strings.HasPrefix(part, "@") { + predicates = append(predicates, func(t todotxt.Task) bool { + for _, v := range t.Contexts { + if "@"+v == part { + return true + } + } + return false + }) + } else if strings.HasPrefix(part, "+") { + predicates = append(predicates, func(t todotxt.Task) bool { + for _, v := range t.Projects { + if "+"+v == part { + return true + } + } + return false + }) + } else { + predicates = append(predicates, func(t todotxt.Task) bool { + val := t.Original + if !checkCase { + val = strings.ToLower(t.Original) + } + return strings.Contains(val, part) + }) + } + } + return func(t todotxt.Task) bool { + for _, v := range predicates { + if v(t) { + return true + } + } + return false + } +} + +func (a *AppState) LoadTaskList() error { + var err error + var tl todotxt.TaskList + tl, err = todotxt.LoadFromFilename(a.getTodoFile()) + a.TaskList = &tl + return err +} + +func (a *AppState) LoadDoneList() error { + var err error + var tl todotxt.TaskList + tl, err = todotxt.LoadFromFilename(a.getDoneFile()) + a.DoneList = &tl + return err +} + +func (a *AppState) WriteList() error { + return a.TaskList.WriteToFilename(a.getTodoFile()) +} + +func (a *AppState) WriteDoneList() error { + return a.DoneList.WriteToFilename(a.getDoneFile()) +} diff --git a/screen.go b/screen.go new file mode 100644 index 0000000..6762c01 --- /dev/null +++ b/screen.go @@ -0,0 +1,62 @@ +package main + +import ( + termbox "github.com/nsf/termbox-go" +) + +type Screen interface { + handleKeyEvent(termbox.Event) int + initialize(Bundle) error + drawScreen() +} + +const ( + ScreenMain = iota + ScreenTask + 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{ + viewPort: &ViewPort{}, + } + aboutScreen := AboutScreen{} + taskScreen := TaskScreen{ + viewPort: &ViewPort{}, + } + a.screens = append(a.screens, &mainScreen) + a.screens = append(a.screens, &taskScreen) + 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() +} + +// ViewPort helps keep track of what's being displayed on the screen +type ViewPort struct { + bytesPerRow int + numberOfRows int + firstRow int + cursor int +} + +func readUserInput(e chan termbox.Event) { + for { + e <- termbox.PollEvent() + } +} diff --git a/screen_about.go b/screen_about.go new file mode 100644 index 0000000..3653d7e --- /dev/null +++ b/screen_about.go @@ -0,0 +1,30 @@ +package main + +import ( + "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 +} + +func (screen *AboutScreen) initialize(bundle Bundle) error { + return nil +} + +func (screen *AboutScreen) handleKeyEvent(event termbox.Event) int { + return ScreenMain +} + +func (screen *AboutScreen) drawScreen() { + width, height := termbox.Size() + exitTxt := "Press any key to return to tasks" + termboxUtil.DrawStringAtPoint(exitTxt, (width-len(exitTxt))/2, height-1, TitleFg, TitleBg) +} diff --git a/screen_main.go b/screen_main.go new file mode 100644 index 0000000..0d365d4 --- /dev/null +++ b/screen_main.go @@ -0,0 +1,380 @@ +package main + +import ( + "fmt" + "strings" + "time" + + todotxt "github.com/JamesClonk/go-todotxt" + "github.com/br0xen/termbox-util" + termbox "github.com/nsf/termbox-go" +) + +// MainScreen holds all that's going on +type MainScreen struct { + viewPort *ViewPort + message string + messageTimeout time.Duration + messageTime time.Time + mode int + + inputField *termboxUtil.InputField + + currentList string + currentFilter string + activeList todotxt.TaskList + displayList []todotxt.Task + + undoQueue []string + redoQueue []string + backspaceDoes int +} + +const ( + MainBundleListKey = "mainscreen.list" + MainBundleFilterKey = "mainscreen.filter" + + MainBundleListTodo = "mainscreen.list.todo" + MainBundleListDone = "mainscreen.list.done" + + MainModalIdFilter = "mainscreen.filter" + MainModalIdAddTask = "mainscreen.addtask" + + MainBackspaceNothing = iota + MainBackspaceMain + MainBackspaceFilter +) + +func (screen *MainScreen) initialize(bundle Bundle) error { + if err := screen.reloadList(bundle); err != nil { + return err + } + width, height := termbox.Size() + screen.inputField = termboxUtil.CreateInputField(0, (height - 3), width, 1, DefaultFg, DefaultBg) + screen.inputField.SetID("") + screen.inputField.SetBordered(false) + + return nil +} + +func (screen *MainScreen) reloadList(bundle Bundle) error { + screen.displayList = []todotxt.Task{} + 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) + } + } + } + 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 + return ScreenAbout + } 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 ScreenMain + + } else if event.Key == termbox.KeySpace { + return screen.toggleTaskComplete() + + } else if event.Ch == 'L' { + return screen.toggleViewList() + + } else if event.Ch == 'a' { + return screen.startAddNewTask() + + } else if event.Ch == 'e' || event.Ch == 'l' || event.Key == termbox.KeyEnter { + 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 == '/' { + screen.startFilter() + + } else if event.Ch == 'D' { + screen.archiveCurrentItem() + + } else if event.Ch == 'q' { + return ScreenExit + } + return ScreenMain +} + +func (screen *MainScreen) handleConfirmKeyEvent(event termbox.Event) int { + return ScreenMain +} + +func (screen *MainScreen) handleInputKeyEvent(event termbox.Event) int { + screen.inputField.SetID("add task") + id := screen.inputField.GetID() + if id == "filter" { + if event.Key == termbox.KeyEnter { + // Apply the filter + screen.backspaceDoes = MainBackspaceFilter + screen.reloadList(screen.buildBundle(screen.currentList, screen.inputField.GetValue())) + screen.inputField.SetID("") + screen.inputField.SetValue("") + return ScreenMain + } + } else if id == "add task" { + if event.Key == termbox.KeyEnter { + // Create the new item + err := app.addTask(screen.inputField.GetValue()) + if err != nil { + screen.setMessage(err.Error()) + } + if err = app.WriteList(); err != nil { + screen.setMessage(err.Error()) + return ScreenMain + } + screen.reloadList(screen.buildBundle(screen.currentList, screen.currentFilter)) + screen.inputField.SetID("") + screen.inputField.SetValue("") + 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 + } + } + screen.inputField.HandleEvent(event) + return ScreenMain +} + +func (screen *MainScreen) setActiveList(list todotxt.TaskList) { + screen.activeList = todotxt.NewTaskList() + for _, v := range list { + screen.activeList.AddTask(&v) + } +} + +func (screen *MainScreen) drawScreen() { + if screen.message == "" { + screen.setMessageWithTimeout("Press '?' for help", -1) + } + screen.drawHeader() + for k, v := range screen.displayList { + useFg, useBg := DefaultFg, DefaultBg + if k == screen.viewPort.cursor { + useFg, useBg = CursorFg, CursorBg + } + termboxUtil.DrawStringAtPoint(app.getTaskString(v), 0, k+1, 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() + } + _, height := termbox.Size() + if screen.inputField.GetID() != "" { + termboxUtil.DrawStringAtPoint(screen.inputField.GetID()+": ", 0, height-2, DefaultFg, DefaultBg) + screen.inputField.Draw() + } + // And the 'message' + termboxUtil.DrawStringAtPoint(screen.message, 0, height-1, DefaultFg, DefaultBg) +} + +func (screen *MainScreen) archiveCurrentItem() int { + /* + // Find the task under the cursor + if len(screen.displayList) > screen.viewPort.cursor { + t := screen.displayList[screen.viewPort.cursor] + // Load the task screen with this task + if err := a.archiveTask(t.Id); err != nil { + screen.setMessage(error.Error()) + } + + + + b := Bundle{} + b.setValue(TaskBundleTaskIdKey, t.Id) + if err := app.screens[ScreenTask].initialize(b); err != nil { + screen.setMessage(err.Error()) + } + return ScreenTask + } + */ + return ScreenMain +} + +func (screen *MainScreen) startEditTaskScreen() int { + // Find the task under the cursor + if len(screen.displayList) > screen.viewPort.cursor { + t := screen.displayList[screen.viewPort.cursor] + // Load the task screen with this task + b := Bundle{} + b.setValue(TaskBundleTaskIdKey, t.Id) + if err := app.screens[ScreenTask].initialize(b); err != nil { + screen.setMessage(err.Error()) + } + return ScreenTask + } + return ScreenMain +} + +func (screen *MainScreen) reloadCurrentView() { + bundle := Bundle{} + bundle.setValue(MainBundleListKey, screen.currentList) + bundle.setValue(MainBundleFilterKey, screen.currentFilter) + screen.initialize(bundle) +} + +func (screen *MainScreen) toggleViewList() int { + bundle := Bundle{} + if screen.currentList == MainBundleListTodo { + bundle.setValue(MainBundleListKey, MainBundleListDone) + screen.backspaceDoes = MainBackspaceMain + } else { + bundle.setValue(MainBundleListKey, MainBundleListTodo) + } + bundle.setValue(MainBundleFilterKey, screen.currentFilter) + screen.initialize(bundle) + return ScreenMain +} + +func (screen *MainScreen) startAddNewTask() int { + screen.inputField.SetID("add task") + screen.inputField.SetX(len(screen.inputField.GetID()) + 2) + return ScreenMain +} + +func (screen *MainScreen) toggleTaskComplete() int { + // Find the task under the cursor + if len(screen.displayList) > screen.viewPort.cursor { + t := screen.displayList[screen.viewPort.cursor] + err := app.toggleTaskComplete(t.Id) + if err != nil { + screen.setMessage(err.Error()) + return ScreenMain + } + if err = app.WriteList(); err != nil { + screen.setMessage(err.Error()) + } + } + screen.reloadCurrentView() + return ScreenMain +} + +func (screen *MainScreen) moveCursorDown() bool { + screen.viewPort.cursor++ + if screen.viewPort.cursor >= len(screen.displayList) { + screen.viewPort.cursor = len(screen.displayList) - 1 + return false + } + return true +} + +func (screen *MainScreen) moveCursorUp() bool { + screen.viewPort.cursor-- + if screen.viewPort.cursor < 0 { + screen.viewPort.cursor = 0 + return false + } + return true +} + +func (screen *MainScreen) startFilter() { + screen.inputField.SetID("filter") + screen.inputField.SetX(len(screen.inputField.GetID()) + 2) +} + +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 = "" + screen.messageTimeout = -1 +} + +func (screen *MainScreen) buildBundle(list, filter string) Bundle { + bundle := Bundle{} + bundle.setValue(MainBundleListKey, list) + bundle.setValue(MainBundleFilterKey, filter) + return bundle +} diff --git a/screen_task.go b/screen_task.go new file mode 100644 index 0000000..badf3d8 --- /dev/null +++ b/screen_task.go @@ -0,0 +1,122 @@ +package main + +import ( + "errors" + "fmt" + "strings" + "time" + + todotxt "github.com/JamesClonk/go-todotxt" + "github.com/br0xen/termbox-util" + termbox "github.com/nsf/termbox-go" +) + +// TaskScreen holds all that's going on +type TaskScreen struct { + viewPort *ViewPort + message string + messageTimeout time.Duration + messageTime time.Time + + inputModal *termboxUtil.InputModal + confirmModal *termboxUtil.ConfirmModal + + currentTaskId int + displayTask *todotxt.Task +} + +const ( + TaskBundleTaskIdKey = "taskscreen.taskid" +) + +func (screen *TaskScreen) initialize(bundle Bundle) error { + var err error + screen.currentTaskId = bundle.getInt(TaskBundleTaskIdKey, -1) + if screen.currentTaskId == -1 { + return errors.New("Task Screen Initialization Failed") + } + if screen.displayTask, err = app.TaskList.GetTask(screen.currentTaskId); err != nil { + return err + } + return nil +} + +func (screen *TaskScreen) handleKeyEvent(event termbox.Event) int { + if event.Key == termbox.KeyBackspace || event.Key == termbox.KeyBackspace2 || event.Ch == 'h' || event.Key == termbox.KeyArrowLeft { + return ScreenMain + + } else if event.Ch == 'j' || event.Key == termbox.KeyArrowDown { + screen.moveCursorDown() + + } else if event.Ch == 'k' || event.Key == termbox.KeyArrowUp { + screen.moveCursorUp() + } + return ScreenTask +} + +func (screen *TaskScreen) drawScreen() { + screen.drawHeader() + + yPos := 1 + termboxUtil.DrawStringAtPoint(screen.displayTask.Todo, 0, yPos, DefaultFg, DefaultBg) + yPos++ + termboxUtil.DrawStringAtPoint(fmt.Sprintf("Priority: %s", screen.displayTask.Priority), 0, yPos, DefaultFg, DefaultBg) + yPos++ + termboxUtil.DrawStringAtPoint(fmt.Sprintf("Projects: %s", screen.displayTask.Projects), 0, yPos, DefaultFg, DefaultBg) + yPos++ + termboxUtil.DrawStringAtPoint(fmt.Sprintf("Contexts: %s", screen.displayTask.Contexts), 0, yPos, DefaultFg, DefaultBg) + yPos++ + termboxUtil.DrawStringAtPoint("Additional Tags:", 0, yPos, DefaultFg, DefaultBg) + yPos++ + for k, v := range screen.displayTask.AdditionalTags { + termboxUtil.DrawStringAtPoint(fmt.Sprintf("%s: %s", k, v), 0, yPos, DefaultFg, DefaultBg) + yPos++ + } + + screen.drawFooter() +} + +func (screen *TaskScreen) drawHeader() { + width, _ := termbox.Size() + headerString := screen.displayTask.Todo + spaces := strings.Repeat(" ", ((width-len(headerString))/2)+1) + termboxUtil.DrawStringAtPoint(fmt.Sprintf("%s%s%s", spaces, headerString, spaces), 0, 0, TitleFg, TitleBg) +} + +func (screen *TaskScreen) drawFooter() { + if screen.messageTimeout > 0 && time.Since(screen.messageTime) > screen.messageTimeout { + screen.clearMessage() + } + _, height := termbox.Size() + termboxUtil.DrawStringAtPoint(screen.message, 0, height-1, DefaultFg, DefaultBg) +} + +func (screen *TaskScreen) moveCursorDown() bool { + screen.viewPort.cursor++ + return true +} + +func (screen *TaskScreen) moveCursorUp() bool { + screen.viewPort.cursor-- + return true +} + +func (screen *TaskScreen) 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 *TaskScreen) setMessageWithTimeout(msg string, timeout time.Duration) { + screen.message = msg + screen.messageTime = time.Now() + screen.messageTimeout = timeout +} + +func (screen *TaskScreen) clearMessage() { + screen.message = "" + screen.messageTimeout = -1 +} diff --git a/task_ops.go b/task_ops.go new file mode 100644 index 0000000..178d322 --- /dev/null +++ b/task_ops.go @@ -0,0 +1,140 @@ +package main + +import ( + "fmt" + "strconv" + "strings" + + todotxt "github.com/JamesClonk/go-todotxt" +) + +func (a *AppState) opListTasks(args []string) int { + var lastIdx int + var filterString string + showAll := len(args) > 0 && args[0] == "-1" + if showAll { + args = args[1:] + } + if len(args) > 0 { + filterString = strings.Join(args, " ") + a.TaskList = a.getFilteredList(filterString) + } + for _, v := range *a.TaskList { + fmt.Println(a.getTaskString(v)) + lastIdx++ + } + if showAll { + if err := app.LoadDoneList(); err != nil { + fmt.Println("Error loading 'Done' list") + fmt.Println(err.Error()) + return 1 + } + if filterString != "" { + a.DoneList = a.getFilteredList(filterString) + } + for _, v := range *a.DoneList { + fmt.Println(a.getDoneTaskString(v)) + } + } + return 0 +} + +func (a *AppState) opAddTask(args []string) int { + taskString := strings.Join(args, " ") + if err := a.addTask(taskString); err != nil { + fmt.Println("Error adding task") + fmt.Println(err.Error()) + return 1 + } + if err := a.WriteList(); err != nil { + fmt.Println("Error saving task list") + fmt.Println(err.Error()) + return 1 + } + return 0 +} + +func (a *AppState) opToggleTaskComplete(args []string) int { + if len(args) == 0 { + fmt.Println("No id given") + return 1 + } + if len(args) == 0 { + fmt.Println("No id given") + return 1 + } + for _, v := range args { + var id int + var err error + if id, err = strconv.Atoi(v); err != nil { + fmt.Printf("Invalid id given: %s\n", v) + return 1 + } + if err := a.toggleTaskComplete(id); err != nil { + fmt.Println(err.Error()) + return 1 + } + } + if a.WriteList() != nil { + fmt.Println("Error saving list") + return 1 + } + return 0 +} + +func (a *AppState) opArchiveTasks(args []string) int { + if err := app.LoadDoneList(); err != nil { + fmt.Println("Error loading 'Done' list") + fmt.Println(err.Error()) + return 1 + } + if len(args) > 0 { + for _, v := range args { + var id int + var task *todotxt.Task + var err error + if id, err = strconv.Atoi(v); err != nil { + fmt.Printf("Invalid id given: %s\n", v) + return 1 + } + if task, err = a.TaskList.GetTask(id); err != nil { + fmt.Printf("Error getting task %d\n", id) + return 1 + } + if err = a.archiveTask(id); err != nil { + fmt.Printf("Error archiving task %d\n", id) + return 1 + } + fmt.Println(a.getDoneTaskString(*task)) + } + } else { + for _, v := range *a.TaskList { + if v.Completed { + if err := a.archiveTask(v.Id); err != nil { + fmt.Printf("Error archiving task %d\n", v.Id) + return 1 + } + fmt.Println(a.getDoneTaskString(v)) + } + } + } + if a.WriteDoneList() != nil { + fmt.Println("Error saving archive list") + return 1 + } + if a.WriteList() != nil { + fmt.Println("Error saving task list") + return 1 + } + 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..5e51bb9 --- /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, MainBundleListTodo) + + 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 { + app.layoutAndDrawScreen(displayScreen) + } + } + termbox.Close() + // Any wrap up should be done here... + return 0 +}