From 8c2a0c9464dcb226587f6d84fe79d675700dbe3a Mon Sep 17 00:00:00 2001 From: Liang-Shih Lin Date: Mon, 26 Aug 2024 10:34:37 +0800 Subject: [PATCH 1/5] feat: add csv data persistence --- .gitignore | 2 ++ column.go | 1 + data.go | 103 ++++++++++++++++++++++++++++++++++++++++++++--------- main.go | 13 +++++++ 4 files changed, 103 insertions(+), 16 deletions(-) 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/data.go b/data.go index 27efb30..65ece3d 100644 --- a/data.go +++ b/data.go @@ -1,8 +1,27 @@ package main -import "github.com/charmbracelet/bubbles/list" +import ( + "encoding/csv" + "fmt" + "log" + "os" + "strings" -// Provides the mock data to fill the kanban board + "github.com/charmbracelet/bubbles/list" +) + +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 +29,73 @@ func (b *Board) initLists() { newColumn(inProgress), newColumn(done), } - // Init To Do + + if _, err := os.Stat("tasks.csv"); os.IsNotExist(err) { + file, err := os.Create("tasks.csv") + if err != nil { + log.Fatal(err) + } + defer file.Close() + file.WriteString("title,description,status\n") + } + + content, err := os.ReadFile("tasks.csv") + if err != nil { + log.Fatal(err) + } + + r := csv.NewReader(strings.NewReader(string(content))) + r.Comma = ',' + + // Skip the header + _, readErr := r.Read() + if readErr != nil { + fmt.Println("Error reading CSV header:", err) + } + + records, readAllErr := r.ReadAll() + if readAllErr != nil { + log.Fatal(err) + } + + fmt.Println(records) + 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]) +} + +func updateCSV() { + file, err := os.Create("tasks.csv") + 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/main.go b/main.go index d348ae1..04f116d 100644 --- a/main.go +++ b/main.go @@ -23,6 +23,19 @@ 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 From 8eb1c5881ba1fef228dea7baf00ce7dd0b116c7f Mon Sep 17 00:00:00 2001 From: Liang-Shih Lin Date: Mon, 26 Aug 2024 11:17:08 +0800 Subject: [PATCH 2/5] fix: exiting cli not saving changes --- form.go | 2 +- model.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) 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/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() From dc33b50de0dbe6fb1472afef09e9e9bc3046abf8 Mon Sep 17 00:00:00 2001 From: Liang-Shih Lin Date: Tue, 27 Aug 2024 17:01:20 +0800 Subject: [PATCH 3/5] chore: clean up fmt.println --- data.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/data.go b/data.go index 65ece3d..62d2240 100644 --- a/data.go +++ b/data.go @@ -2,7 +2,6 @@ package main import ( "encoding/csv" - "fmt" "log" "os" "strings" @@ -50,7 +49,7 @@ func (b *Board) initLists() { // Skip the header _, readErr := r.Read() if readErr != nil { - fmt.Println("Error reading CSV header:", err) + log.Fatal(err) } records, readAllErr := r.ReadAll() @@ -58,7 +57,6 @@ func (b *Board) initLists() { log.Fatal(err) } - fmt.Println(records) listItems := [][]list.Item{ todo: {}, inProgress: {}, From 568eff4218015fdb9336ac5978edacc923d25690 Mon Sep 17 00:00:00 2001 From: Liang-Shih Lin Date: Wed, 28 Aug 2024 09:53:45 +0800 Subject: [PATCH 4/5] fix: read and write config and csv from XDG_CONFIG_HOME --- config.go | 98 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ data.go | 50 +--------------------------- main.go | 8 ++++- 3 files changed, 106 insertions(+), 50 deletions(-) create mode 100644 config.go diff --git a/config.go b/config.go new file mode 100644 index 0000000..0cabc1a --- /dev/null +++ b/config.go @@ -0,0 +1,98 @@ +package main + +import ( + "encoding/csv" + "encoding/json" + "fmt" + "log" + "os" + "strings" +) + +type Config struct { + DbPath string +} + +var ( + configHome = os.Getenv("XDG_CONFIG_HOME") + configDir = configHome + "/kancli" + configFile = configDir + "/config.json" +) + +func readConfig() Config { + mkdirErr := os.MkdirAll(configDir, 0755) + if mkdirErr != nil { + log.Fatal(mkdirErr) + } + + 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 62d2240..af3f6fb 100644 --- a/data.go +++ b/data.go @@ -1,11 +1,6 @@ package main import ( - "encoding/csv" - "log" - "os" - "strings" - "github.com/charmbracelet/bubbles/list" ) @@ -29,33 +24,7 @@ func (b *Board) initLists() { newColumn(done), } - if _, err := os.Stat("tasks.csv"); os.IsNotExist(err) { - file, err := os.Create("tasks.csv") - if err != nil { - log.Fatal(err) - } - defer file.Close() - file.WriteString("title,description,status\n") - } - - content, err := os.ReadFile("tasks.csv") - 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(err) - } - - records, readAllErr := r.ReadAll() - if readAllErr != nil { - log.Fatal(err) - } + records := readCSV() listItems := [][]list.Item{ todo: {}, @@ -80,20 +49,3 @@ func (b *Board) initLists() { b.cols[done].list.Title = "Done" b.cols[done].list.SetItems(listItems[done]) } - -func updateCSV() { - file, err := os.Create("tasks.csv") - 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/main.go b/main.go index 04f116d..6de37e2 100644 --- a/main.go +++ b/main.go @@ -38,7 +38,10 @@ func (s status) String() string { const margin = 4 -var board *Board +var ( + board *Board + csvFile string +) const ( todo status = iota @@ -54,6 +57,9 @@ func main() { } defer f.Close() + config := readConfig() + csvFile = config.DbPath + board = NewBoard() board.initLists() p := tea.NewProgram(board) From ec98736d8cd927abe42d067bb47c1a9496da277f Mon Sep 17 00:00:00 2001 From: Liang-Shih Lin Date: Wed, 28 Aug 2024 11:43:14 +0800 Subject: [PATCH 5/5] fix: Use golang's native directory functions fix: Exit if dbPath not found fix: Added json definition for config --- config.go | 27 +++++++++++++++++++-------- main.go | 4 ++++ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/config.go b/config.go index 0cabc1a..7ddbefb 100644 --- a/config.go +++ b/config.go @@ -6,25 +6,36 @@ import ( "fmt" "log" "os" + "path/filepath" "strings" ) type Config struct { - DbPath string + DbPath string `json:"dbPath"` } -var ( - configHome = os.Getenv("XDG_CONFIG_HOME") - configDir = configHome + "/kancli" - configFile = configDir + "/config.json" -) - func readConfig() Config { - mkdirErr := os.MkdirAll(configDir, 0755) + // 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) diff --git a/main.go b/main.go index 6de37e2..9e3e42a 100644 --- a/main.go +++ b/main.go @@ -58,6 +58,10 @@ 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()