diff --git a/.gitignore b/.gitignore index 8959a89..6560787 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ kancli .DS_Store debug.log + +tasks.csv diff --git a/column.go b/column.go index 8f54d02..2422206 100644 --- a/column.go +++ b/column.go @@ -73,6 +73,7 @@ func (c column) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } c.list, cmd = c.list.Update(msg) + updateCSV() return c, cmd } diff --git a/config.go b/config.go new file mode 100644 index 0000000..7ddbefb --- /dev/null +++ b/config.go @@ -0,0 +1,109 @@ +package main + +import ( + "encoding/csv" + "encoding/json" + "fmt" + "log" + "os" + "path/filepath" + "strings" +) + +type Config struct { + DbPath string `json:"dbPath"` +} + +func readConfig() Config { + // look for $XDG_CONFIG_HOME/kancli/config.json or $HOME/.config/kancli/config.json + configDir, err := os.UserConfigDir() + if err != nil { + homeDir, err := os.UserHomeDir() + if err != nil { + // cant find home or config just give up + return Config{} + } + configDir = filepath.Join(homeDir, ".config") + } + + // Create ~/.config/kancli/ if it does not exist + configPath := filepath.Join(configDir, "kancli") + + mkdirErr := os.MkdirAll(configPath, 0755) + if mkdirErr != nil { + log.Fatal(mkdirErr) + } + + configFile := filepath.Join(configPath, "config.json") + + var config Config + if _, err := os.Stat(configFile); os.IsNotExist(err) { + file, err := os.Create(configFile) + if err != nil { + log.Fatal(err) + } + defer file.Close() + csvFile := configDir + "/tasks.csv" + file.WriteString(fmt.Sprintf("{\"dbPath\": \"%s\"}", csvFile)) + } + + data, readJSONFileErr := os.ReadFile(configFile) + if readJSONFileErr != nil { + log.Fatal(readJSONFileErr) + } + + if readJSONerr := json.Unmarshal(data, &config); readJSONerr != nil { + log.Fatal(readJSONerr) + } + + return config +} + +func readCSV() [][]string { + if _, err := os.Stat(csvFile); os.IsNotExist(err) { + file, err := os.Create(csvFile) + if err != nil { + log.Fatal(err) + } + defer file.Close() + file.WriteString("title,description,status\n") + } + + content, err := os.ReadFile(csvFile) + if err != nil { + log.Fatal(err) + } + + r := csv.NewReader(strings.NewReader(string(content))) + r.Comma = ',' + + // Skip the header + _, readErr := r.Read() + if readErr != nil { + log.Fatal(readErr) + } + + records, readAllErr := r.ReadAll() + if readAllErr != nil { + log.Fatal(readAllErr) + } + + return records +} + +func updateCSV() { + file, err := os.Create(csvFile) + if err != nil { + log.Fatal(err) + } + defer file.Close() + file.WriteString("title,description,status\n") + w := csv.NewWriter(file) + for _, col := range board.cols { + for _, item := range col.list.Items() { + task := item.(Task) + w.Write([]string{task.Title(), task.Description(), task.status.String()}) + } + } + w.Flush() +} diff --git a/data.go b/data.go index 27efb30..af3f6fb 100644 --- a/data.go +++ b/data.go @@ -1,8 +1,21 @@ package main -import "github.com/charmbracelet/bubbles/list" +import ( + "github.com/charmbracelet/bubbles/list" +) -// Provides the mock data to fill the kanban board +func stringToStatus(s string) status { + switch s { + case "todo": + return todo + case "inProgress": + return inProgress + case "done": + return done + default: + return todo + } +} func (b *Board) initLists() { b.cols = []column{ @@ -10,21 +23,29 @@ func (b *Board) initLists() { newColumn(inProgress), newColumn(done), } - // Init To Do + + records := readCSV() + + listItems := [][]list.Item{ + todo: {}, + inProgress: {}, + done: {}, + } + + // Extract all with status todo + for _, record := range records { + if len(record) >= 3 { + s := stringToStatus(record[2]) + listItems[s] = append(listItems[s], Task{s, record[0], record[1]}) + } + } + b.cols[todo].list.Title = "To Do" - b.cols[todo].list.SetItems([]list.Item{ - Task{status: todo, title: "buy milk", description: "strawberry milk"}, - Task{status: todo, title: "eat sushi", description: "negitoro roll, miso soup, rice"}, - Task{status: todo, title: "fold laundry", description: "or wear wrinkly t-shirts"}, - }) - // Init in progress + b.cols[todo].list.SetItems(listItems[todo]) + b.cols[inProgress].list.Title = "In Progress" - b.cols[inProgress].list.SetItems([]list.Item{ - Task{status: inProgress, title: "write code", description: "don't worry, it's Go"}, - }) - // Init done + b.cols[inProgress].list.SetItems(listItems[inProgress]) + b.cols[done].list.Title = "Done" - b.cols[done].list.SetItems([]list.Item{ - Task{status: done, title: "stay cool", description: "as a cucumber"}, - }) + b.cols[done].list.SetItems(listItems[done]) } diff --git a/form.go b/form.go index 50bfc4d..1c89e1e 100644 --- a/form.go +++ b/form.go @@ -50,8 +50,8 @@ func (f Form) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyMsg: switch { case key.Matches(msg, keys.Quit): + updateCSV() return f, tea.Quit - case key.Matches(msg, keys.Back): return board.Update(nil) case key.Matches(msg, keys.Enter): diff --git a/main.go b/main.go index d348ae1..9e3e42a 100644 --- a/main.go +++ b/main.go @@ -23,9 +23,25 @@ func (s status) getPrev() status { return s - 1 } +func (s status) String() string { + switch s { + case todo: + return "todo" + case inProgress: + return "inProgress" + case done: + return "done" + default: + return "unknown" + } +} + const margin = 4 -var board *Board +var ( + board *Board + csvFile string +) const ( todo status = iota @@ -41,6 +57,13 @@ func main() { } defer f.Close() + config := readConfig() + if config.DbPath == "" { + fmt.Println("No dbPath found in config") + os.Exit(1) + } + csvFile = config.DbPath + board = NewBoard() board.initLists() p := tea.NewProgram(board) diff --git a/model.go b/model.go index f537222..da34831 100644 --- a/model.go +++ b/model.go @@ -47,6 +47,7 @@ func (m *Board) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch { case key.Matches(msg, keys.Quit): m.quitting = true + updateCSV() return m, tea.Quit case key.Matches(msg, keys.Left): m.cols[m.focused].Blur()