From a0ce5eb412454c0051059545da2725dd9cde6d9f Mon Sep 17 00:00:00 2001 From: Chris Cummer Date: Fri, 20 Apr 2018 15:19:54 -0700 Subject: [PATCH] Basic selectable todo functionality working Can: - move between todo items - toggle checked/unchecked state Cannot: - persiste changes to file - add items - delete items --- git/display.go | 2 - git/widget.go | 2 - github/widget.go | 2 - textfile/widget.go | 20 ++------- todo/display.go | 31 ++++++++++++++ todo/item.go | 29 +++++++++++++ todo/list.go | 44 +++++++++++++++++++ todo/widget.go | 101 ++++++++++++++++++++++++++++++++++++++++++++ weather/widget.go | 2 - wtf.go | 3 ++ wtf/config_files.go | 61 ++++++++++++++++++++++++-- wtf/utils.go | 10 +++++ 12 files changed, 279 insertions(+), 28 deletions(-) create mode 100644 todo/display.go create mode 100644 todo/item.go create mode 100644 todo/list.go create mode 100644 todo/widget.go diff --git a/git/display.go b/git/display.go index 4a915079..0894d32a 100644 --- a/git/display.go +++ b/git/display.go @@ -64,8 +64,6 @@ func (widget *Widget) formatChange(line string) string { line = strings.Replace(line, "M", "[yellow]M[white]", 1) case 'R': line = strings.Replace(line, "R", "[purple]R[white]", 1) - default: - line = line } return fmt.Sprintf(" %s\n", strings.Replace(line, "\"", "", -1)) diff --git a/git/widget.go b/git/widget.go index 4b8cba20..04c2f225 100644 --- a/git/widget.go +++ b/git/widget.go @@ -97,6 +97,4 @@ func (widget *Widget) keyboardIntercept(event *tcell.EventKey) *tcell.EventKey { default: return event } - - return event } diff --git a/github/widget.go b/github/widget.go index 1f59ee28..3f89b8f3 100644 --- a/github/widget.go +++ b/github/widget.go @@ -96,6 +96,4 @@ func (widget *Widget) keyboardIntercept(event *tcell.EventKey) *tcell.EventKey { default: return event } - - return event } diff --git a/textfile/widget.go b/textfile/widget.go index f829d0db..141be1fd 100644 --- a/textfile/widget.go +++ b/textfile/widget.go @@ -2,7 +2,6 @@ package textfile import ( "fmt" - "io/ioutil" "time" "github.com/olebedev/config" @@ -20,8 +19,8 @@ type Widget struct { func NewWidget() *Widget { widget := Widget{ - TextWidget: wtf.NewTextWidget(" Text File ", "textfile"), - FilePath: Config.UString("wtf.mods.textfile.filepath"), + TextWidget: wtf.NewTextWidget(" 📄 Text File ", "textfile"), + FilePath: Config.UString("wtf.mods.textfile.filename"), } widget.View.SetWrap(true) @@ -41,7 +40,7 @@ func (widget *Widget) Refresh() { widget.View.Clear() - fileData, err := widget.readFile() + fileData, err := wtf.ReadFile(widget.FilePath) if err != nil { fmt.Fprintf(widget.View, "%s", err) @@ -49,16 +48,3 @@ func (widget *Widget) Refresh() { fmt.Fprintf(widget.View, "%s", fileData) } } - -/* -------------------- Uneported Functions -------------------- */ - -func (widget *Widget) readFile() (string, error) { - absPath, _ := wtf.ExpandHomeDir(widget.FilePath) - - bytes, err := ioutil.ReadFile(absPath) - if err != nil { - return "", err - } - - return string(bytes), nil -} diff --git a/todo/display.go b/todo/display.go new file mode 100644 index 00000000..e79769d0 --- /dev/null +++ b/todo/display.go @@ -0,0 +1,31 @@ +package todo + +import ( + "fmt" +) + +func (widget *Widget) display() { + widget.View.Clear() + + title := fmt.Sprintf(" 📝 %s ", widget.FilePath) + widget.View.SetTitle(title) + + str := "" + for idx, item := range widget.list.Items { + foreColor, backColor := "white", "black" + + if widget.View.HasFocus() && idx == widget.list.selected { + foreColor, backColor = "black", "olive" + } + + str = str + fmt.Sprintf( + "[%s:%s]|%s| %s[white]\n", + foreColor, + backColor, + item.CheckMark(), + item.Text, + ) + } + + fmt.Fprintf(widget.View, "%s", str) +} diff --git a/todo/item.go b/todo/item.go new file mode 100644 index 00000000..de5498c3 --- /dev/null +++ b/todo/item.go @@ -0,0 +1,29 @@ +package todo + +import( + "time" +) + +type Item struct { + Checked bool + Index int + Text string + + createdAt time.Time + updatedAt time.Time +} + +func (item *Item) CheckMark() string { + if item.Checked { + return "x" + } else { + return " " + } +} + +func (item *Item) Toggle() { + item.Checked = !item.Checked + item.updatedAt = time.Now() +} + + diff --git a/todo/list.go b/todo/list.go new file mode 100644 index 00000000..d3ccf9ed --- /dev/null +++ b/todo/list.go @@ -0,0 +1,44 @@ +package todo + +import () + +type List struct { + Items []*Item + + selected int +} + +func (list *List) Len() int { + return len(list.Items) +} + +func (list *List) Less(i, j int) bool { + return list.Items[i].Index < list.Items[j].Index +} + +func (list *List) Swap(i, j int) { + list.Items[i], list.Items[j] = list.Items[j], list.Items[i] +} + +func (list *List) Next() { + list.selected = list.selected + 1 + if list.selected >= len(list.Items) { + list.selected = 0 + } +} + +func (list *List) Prev() { + list.selected = list.selected - 1 + if list.selected < 0 { + list.selected = len(list.Items) - 1 + } +} + +// Toggle switches the checked state of the selected item +func (list *List) Toggle() { + list.Items[list.selected].Toggle() +} + +func (list *List) Unselect() { + list.selected = -1 +} diff --git a/todo/widget.go b/todo/widget.go new file mode 100644 index 00000000..d0009d50 --- /dev/null +++ b/todo/widget.go @@ -0,0 +1,101 @@ +package todo + +import ( + "fmt" + "time" + + "github.com/gdamore/tcell" + "github.com/olebedev/config" + "github.com/senorprogrammer/wtf/wtf" + "gopkg.in/yaml.v2" +) + +// Config is a pointer to the global config object +var Config *config.Config + +type Widget struct { + wtf.TextWidget + + FilePath string + list *List +} + +func NewWidget() *Widget { + widget := Widget{ + TextWidget: wtf.NewTextWidget(" 📝 Todo ", "todo"), + FilePath: Config.UString("wtf.mods.todo.filename"), + + list: &List{selected: -1}, + } + + widget.init() + widget.View.SetInputCapture(widget.keyboardIntercept) + + return &widget +} + +/* -------------------- Exported Functions -------------------- */ + +func (widget *Widget) Refresh() { + if widget.Disabled() { + return + } + + confDir, _ := wtf.ConfigDir() + + fileData, _ := wtf.ReadYamlFile(fmt.Sprintf("%s/%s", confDir, widget.FilePath)) + yaml.Unmarshal(fileData, &widget.list) + + widget.display() + widget.RefreshedAt = time.Now() +} + +/* -------------------- Unexported Functions -------------------- */ + +func (widget *Widget) init() { + _, err := wtf.CreateFile(widget.FilePath) + if err != nil { + panic(err) + } +} + +func (widget *Widget) keyboardIntercept(event *tcell.EventKey) *tcell.EventKey { + switch string(event.Rune()) { + case " ": + // Check/uncheck selected item + widget.list.Toggle() + widget.display() + return nil + case "e": + // Edit selected item + return nil + case "n": + // Add a new item + return nil + } + + switch event.Key() { + case tcell.KeyCtrlD: + // Delete selected item + return nil + case tcell.KeyDown: + widget.list.Next() + widget.display() + return nil + //case tcell.KeySpac: + //// Check/uncheck an item + //return nil + case tcell.KeyEsc: + // Unselect the current row and pass the key on through to unselect the widget + widget.list.Unselect() + widget.display() + return event + case tcell.KeyUp: + // Select next item up + widget.list.Prev() + widget.display() + return nil + default: + return event + } +} diff --git a/weather/widget.go b/weather/widget.go index 40840d66..788ffa19 100644 --- a/weather/widget.go +++ b/weather/widget.go @@ -204,6 +204,4 @@ func (widget *Widget) keyboardIntercept(event *tcell.EventKey) *tcell.EventKey { default: return event } - - return event } diff --git a/wtf.go b/wtf.go index 93026d0c..cc2bd6ac 100644 --- a/wtf.go +++ b/wtf.go @@ -20,6 +20,7 @@ import ( "github.com/senorprogrammer/wtf/security" "github.com/senorprogrammer/wtf/status" "github.com/senorprogrammer/wtf/textfile" + "github.com/senorprogrammer/wtf/todo" "github.com/senorprogrammer/wtf/weather" "github.com/senorprogrammer/wtf/wtf" ) @@ -126,6 +127,7 @@ func main() { security.Config = Config status.Config = Config textfile.Config = Config + todo.Config = Config weather.Config = Config Widgets = []wtf.TextViewer{ @@ -140,6 +142,7 @@ func main() { security.NewWidget(), status.NewWidget(), textfile.NewWidget(), + todo.NewWidget(), weather.NewWidget(), } diff --git a/wtf/config_files.go b/wtf/config_files.go index 2500204c..0161b108 100644 --- a/wtf/config_files.go +++ b/wtf/config_files.go @@ -3,16 +3,26 @@ package wtf import ( "fmt" "os" + "io/ioutil" "github.com/olebedev/config" ) +func ConfigDir() (string, error) { + configDir, err := ExpandHomeDir("~/.wtf/") + if err != nil { + return "", err + } + + return configDir, nil +} + // CreateConfigDir creates the .wtf directory in the user's home dir func CreateConfigDir() bool { - homeDir, _ := ExpandHomeDir("~/.wtf/") + configDir, _ := ConfigDir() - if _, err := os.Stat(homeDir); os.IsNotExist(err) { - err := os.Mkdir(homeDir, os.ModePerm) + if _, err := os.Stat(configDir); os.IsNotExist(err) { + err := os.Mkdir(configDir, os.ModePerm) if err != nil { panic(err) } @@ -21,6 +31,34 @@ func CreateConfigDir() bool { return true } +// CreateFile creates the named file in the config directory, if it does not already exist. +// If the file exists it does not recreate it. +// If successful, eturns the absolute path to the file +// If unsuccessful, returns an error +func CreateFile(fileName string) (string, error) { + configDir, err := ConfigDir() + if err != nil { + return "", err + } + + filePath := fmt.Sprintf("%s/%s", configDir, fileName) + + // Check if the file already exists; if it does not, create it + _, err = os.Stat(filePath) + if err != nil { + if os.IsNotExist(err) { + _, err = os.Create(filePath) + if err != nil { + return "", err + } + } else { + return "", err + } + } + + return filePath, nil +} + // LoadConfigFile loads the config.yml file to configure the app func LoadConfigFile(filePath string) *config.Config { absPath, _ := ExpandHomeDir(filePath) @@ -34,3 +72,20 @@ func LoadConfigFile(filePath string) *config.Config { return cfg } + + +func ReadFile(fileName string) (string, error) { + configDir, err := ConfigDir() + if err != nil { + return "", err + } + + filePath := fmt.Sprintf("%s/%s", configDir, fileName) + + bytes, err := ioutil.ReadFile(filePath) + if err != nil { + return "", err + } + + return string(bytes), nil +} diff --git a/wtf/utils.go b/wtf/utils.go index 3db7c142..59e4b066 100644 --- a/wtf/utils.go +++ b/wtf/utils.go @@ -82,6 +82,16 @@ func Now() time.Time { return time.Now().Local() } +func ReadYamlFile(filePath string) ([]byte, error) { + file, err := ioutil.ReadFile(filePath) + + if err != nil { + return []byte{}, err + } + + return file, nil +} + func RightAlignFormat(view *tview.TextView) string { _, _, w, _ := view.GetInnerRect() return fmt.Sprintf("%%%ds", w-1)