diff --git a/app_state.go b/app_state.go index 327bdad..e219699 100644 --- a/app_state.go +++ b/app_state.go @@ -20,10 +20,17 @@ type AppState struct { ValidOperations map[string][]string OpFuncs map[string]func([]string) int + mode ResourceId + TaskList *todotxt.TaskList DoneList *todotxt.TaskList + taskListLoaded bool + doneListLoaded bool + screens []Screen + + lang *Translator } func NewApp() *AppState { @@ -41,8 +48,10 @@ func NewApp() *AppState { func (a *AppState) run(parms []string) int { if len(parms) == 0 || parms[0] == "ui" { // UI Mode + a.mode = ResModeUI return uiLoop() } + a.mode = ResModeCLI if fn, ok := a.OpFuncs[parms[0]]; ok { return fn(parms[1:]) } @@ -117,6 +126,8 @@ func (a *AppState) migrate(from, to int) int { } func (a *AppState) initialize() { + a.initLanguage() + var err error a.config, err = userConfig.NewConfig(a.Name) if err != nil { diff --git a/model.go b/model.go index 6500fea..9a25b44 100644 --- a/model.go +++ b/model.go @@ -7,7 +7,36 @@ import ( todotxt "github.com/br0xen/go-todotxt" ) +// diskListChanged returns true if the task list in todo.txt +// is different than what we have previously loaded. +func (a *AppState) diskListChanged() bool { + if !a.taskListLoaded { + return false + } + curr, err := todotxt.LoadFromFilename(a.getTodoFile()) + if err != nil { + return false + } + return curr.String() != a.TaskList.String() +} + +// diskDoneListChanged returns true if the task list in done.txt +// is different than what we have previously loaded. +func (a *AppState) diskDoneListChanged() bool { + if !a.doneListLoaded { + return false + } + curr, err := todotxt.LoadFromFilename(a.getDoneFile()) + if err != nil { + return false + } + return curr.String() != a.DoneList.String() +} + func (a *AppState) addTask(taskString string) error { + if a.diskListChanged() { + return a.e(ResStrListChanged) + } t, err := todotxt.ParseTask(taskString) if err != nil { return err @@ -20,6 +49,9 @@ func (a *AppState) addTask(taskString string) error { } func (a *AppState) toggleTaskComplete(id int) error { + if a.diskListChanged() { + return a.e(ResStrListChanged) + } var task *todotxt.Task var err error if task, err = a.TaskList.GetTask(id); err != nil { @@ -34,12 +66,15 @@ func (a *AppState) toggleTaskComplete(id int) error { } func (a *AppState) archiveTask(id int) error { + if a.diskListChanged() { + return a.e(ResStrListChanged) + } var err error var task *todotxt.Task if task, err = a.TaskList.GetTask(id); err != nil { return err } - if err := a.TaskList.ArchiveTaskToFile(*task, app.getDoneFile()); err != nil { + if err := a.TaskList.ArchiveTaskToFile(*task, a.getDoneFile()); err != nil { return err } a.TaskList.RemoveTask(*task) @@ -47,6 +82,9 @@ func (a *AppState) archiveTask(id int) error { } func (a *AppState) unarchiveTask(id int) error { + if a.diskListChanged() { + return a.e(ResStrListChanged) + } var err error var task *todotxt.Task if task, err = a.DoneList.GetTask(id); err != nil { @@ -112,6 +150,7 @@ func (a *AppState) LoadTaskList() error { var tl todotxt.TaskList tl, err = todotxt.LoadFromFilename(a.getTodoFile()) a.TaskList = &tl + a.taskListLoaded = true return err } @@ -120,13 +159,20 @@ func (a *AppState) LoadDoneList() error { var tl todotxt.TaskList tl, err = todotxt.LoadFromFilename(a.getDoneFile()) a.DoneList = &tl + a.doneListLoaded = true return err } func (a *AppState) WriteList() error { + if !a.taskListLoaded { + return a.e(ResStrTaskListNotLoaded) + } return a.TaskList.WriteToFilename(a.getTodoFile()) } func (a *AppState) WriteDoneList() error { + if !a.doneListLoaded { + return a.e(ResStrDoneListNotLoaded) + } return a.DoneList.WriteToFilename(a.getDoneFile()) } diff --git a/resources.go b/resources.go new file mode 100644 index 0000000..8dd1bc5 --- /dev/null +++ b/resources.go @@ -0,0 +1,79 @@ +package main + +import "errors" + +type ResourceId uint16 + +type Translator struct { + values map[ResourceId]string +} + +func NewTranslator() *Translator { + return &Translator{ + values: make(map[ResourceId]string), + } +} + +func (t *Translator) addString(res ResourceId, val string) { + t.values[res] = val + if _, ok := t.values[(res & 255)]; !ok { + t.values[(res & 255)] = val + } +} + +func (t *Translator) getString(res ResourceId) (string, error) { + if v, ok := t.values[res]; ok { + return v, nil + } + // Couldn't pull the exact resource, try the basic one + if v, ok := t.values[(res & 255)]; ok { + return v, nil + } + return "", errors.New("String Resource Not Found") +} + +const ( + ResStrListChanged ResourceId = iota + ResStrTaskListNotLoaded + ResStrDoneListNotLoaded + ResStrInvalidRefreshRequest + + ResModeCLI ResourceId = 1 << (iota + 9) + ResModeUI +) + +func (a *AppState) initLanguage() { + a.lang = NewTranslator() + + // Strings that are the same regardless of mode + a.lang.addString((ResStrTaskListNotLoaded), "Task list hasn't been loaded") + a.lang.addString((ResStrDoneListNotLoaded), "Done list hasn't been loaded") + + // CLI Strings + a.lang.addString((ResStrListChanged | ResModeCLI), "List changed somewhere else") + + // UI Strings + a.lang.addString((ResStrListChanged | ResModeUI), "List changed somewhere else, reload and try again (Ctrl+R)") +} + +// s returns a string with the given stringId +// (per the constants above) +func (a *AppState) s(stringId ResourceId) string { + if stringId&255 != stringId { + // the attribute already has extended data in it. + if v, err := a.lang.getString(stringId); err == nil { + return v + } + return "" + } + if v, err := a.lang.getString((stringId | a.mode)); err == nil { + return v + } + return "" +} + +// e is basically the same as 's', but returns +// the string in an error object +func (a *AppState) e(stringId ResourceId) error { + return errors.New(a.s(stringId)) +} diff --git a/screen.go b/screen.go index 98a50af..3a099c8 100644 --- a/screen.go +++ b/screen.go @@ -1,6 +1,8 @@ package main import ( + "time" + termbox "github.com/nsf/termbox-go" ) @@ -49,15 +51,16 @@ func readUserInput(e chan termbox.Event) { } } -func refreshList(e chan termbox.Event) { - /* - for { - time.Sleep(5 * time.Minute) - app.LoadTasklist() - app.LoadDoneList() - e <- termbox.Event{Type: termbox.EventNone} +func checkForUpdate(e chan termbox.Event) { + for { + time.Sleep(time.Minute) + if app.diskListChanged() { + e <- termbox.Event{ + Type: termbox.EventError, + Err: app.e(ResStrListChanged), + } } - */ + } } /* diff --git a/screen_main.go b/screen_main.go index 90d6898..f402126 100644 --- a/screen_main.go +++ b/screen_main.go @@ -1,6 +1,7 @@ package main import ( + "errors" "fmt" "strings" "time" @@ -16,6 +17,8 @@ type MainScreen struct { message string messageTimeout time.Duration messageTime time.Time + messageColorBg termbox.Attribute + messageColorFg termbox.Attribute mode int cursor map[string]int @@ -64,6 +67,17 @@ func (screen *MainScreen) initialize(bundle Bundle) error { return nil } +func (screen *MainScreen) refreshList(bundle 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 Bundle) error { // We add tasks to the display list using append because we want to persist task Ids screen.displayList = todotxt.NewTaskList() @@ -121,7 +135,7 @@ func (screen *MainScreen) handleKeyEvent(event termbox.Event) int { // Go to About Screen b := Bundle{} if err := app.screens[ScreenAbout].initialize(b); err != nil { - screen.setMessage(err.Error()) + screen.setErrorMessage(err.Error()) } return ScreenAbout } else if event.Key == termbox.KeyBackspace || event.Key == termbox.KeyBackspace2 { @@ -149,7 +163,9 @@ func (screen *MainScreen) handleKeyEvent(event termbox.Event) int { screen.cursor[screen.currentList] = len(*screen.displayList) - 1 } else if event.Key == termbox.KeyCtrlR { - screen.reloadList(screen.buildBundle(screen.currentList, screen.currentFilter)) + b := screen.buildBundle(screen.currentList, screen.currentFilter) + screen.refreshList(b) + screen.reloadList(b) } else if event.Key == termbox.KeyCtrlF { // Jump forward half a screen @@ -217,7 +233,7 @@ func (screen *MainScreen) handleInputKeyEvent(event termbox.Event) int { // Create the new item err := app.addTask(screen.inputField.GetValue()) if err != nil { - screen.setMessage(err.Error()) + screen.setErrorMessage(err.Error()) } screen.inputField.SetID("") screen.inputField.SetValue("") @@ -330,7 +346,7 @@ func (screen *MainScreen) drawFooter() { screen.inputField.Draw() } // And the 'message' - termboxUtil.DrawStringAtPoint(screen.message, 0, height-1, DefaultFg, DefaultBg) + termboxUtil.DrawStringAtPoint(screen.message, 0, height-1, screen.messageColorFg, screen.messageColorBg) } func (screen *MainScreen) confirmArchiveItem() int { @@ -353,20 +369,20 @@ func (screen *MainScreen) confirmArchiveItem() int { func (screen *MainScreen) archiveCurrentItem() int { if screen.currentList != MainBundleListTodo { - screen.setMessage("Task is already archived") + screen.setErrorMessage("Task is already archived") return ScreenMain } // 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.setMessage(err.Error()) + screen.setErrorMessage(err.Error()) return ScreenMain } // Reload the list b := screen.buildBundle(screen.currentList, screen.currentFilter) if err := screen.reloadList(b); err != nil { - screen.setMessage(err.Error()) + screen.setErrorMessage(err.Error()) } } return ScreenMain @@ -374,20 +390,20 @@ func (screen *MainScreen) archiveCurrentItem() int { func (screen *MainScreen) unarchiveCurrentItem() int { if screen.currentList == MainBundleListTodo { - screen.setMessage("Task is not archived") + screen.setErrorMessage("Task is not archived") return ScreenMain } // 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.setMessage(err.Error()) + screen.setErrorMessage(err.Error()) return ScreenMain } // Reload the list b := screen.buildBundle(screen.currentList, screen.currentFilter) if err := screen.reloadList(b); err != nil { - screen.setMessage(err.Error()) + screen.setErrorMessage(err.Error()) } } return ScreenMain @@ -401,7 +417,7 @@ func (screen *MainScreen) startEditTaskScreen() int { b := Bundle{} b.setValue(TaskBundleTaskIdKey, t.Id) if err := app.screens[ScreenTask].initialize(b); err != nil { - screen.setMessage(err.Error()) + screen.setErrorMessage(err.Error()) return ScreenMain } return ScreenTask @@ -436,7 +452,7 @@ func (screen *MainScreen) startAddNewTask() int { func (screen *MainScreen) toggleTaskComplete() int { if screen.currentList == MainBundleListDone { - screen.setMessage("Task is archived, unable to modify.") + screen.setErrorMessage("Task is archived, unable to modify.") return ScreenMain } @@ -445,7 +461,7 @@ func (screen *MainScreen) toggleTaskComplete() int { t := (*screen.displayList)[screen.cursor[screen.currentList]] err := app.toggleTaskComplete(t.Id) if err != nil { - screen.setMessage(err.Error()) + screen.setErrorMessage(err.Error()) return ScreenMain } } @@ -476,10 +492,20 @@ func (screen *MainScreen) startFilter() int { return ScreenMain } +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 @@ -494,6 +520,8 @@ func (screen *MainScreen) setMessageWithTimeout(msg string, timeout time.Duratio 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) Bundle { diff --git a/ui_loop.go b/ui_loop.go index 634d8eb..5c594ad 100644 --- a/ui_loop.go +++ b/ui_loop.go @@ -25,6 +25,7 @@ func uiLoop() int { app.layoutAndDrawScreen(displayScreen) eventChan := make(chan termbox.Event) go readUserInput(eventChan) + go checkForUpdate(eventChan) for { event := <-eventChan if event.Type == termbox.EventKey {