diff --git a/api.go b/api.go new file mode 100644 index 0000000..2176431 --- /dev/null +++ b/api.go @@ -0,0 +1,105 @@ +package main + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" +) + +type note struct { + Id int64 `json:"id"` + Title string `json:"title"` + Content string `json:"content"` +} + +const endpoint = "http://localhost:8080/api" + +func loadNotes() ([]note, error) { + res, err := http.Get(endpoint + "/notes") + if err != nil { + return []note{}, nil + } + + data, err := io.ReadAll(res.Body) + if err != nil { + return []note{}, nil + } + + var loadedNotes []note + json.Unmarshal(data, &loadedNotes) + return loadedNotes, nil +} + +func putNote(n note) error { + json, err := json.Marshal(n) + if err != nil { + return err + } + + req, err := http.NewRequest("PUT", endpoint+"/notes/"+fmt.Sprint(n.Id), bytes.NewReader(json)) + if err != nil { + return err + } + + req.Header.Add("Content-Type", "application/json") + + res, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + + if res.StatusCode != http.StatusOK { + return errors.New("Request failed: " + res.Status) + } + + return nil +} + +func postNote(n note) (note, error) { + jsonBytes, err := json.Marshal(n) + if err != nil { + return note{}, err + } + + res, err := http.Post(endpoint+"/notes", "application/json", bytes.NewReader(jsonBytes)) + if err != nil { + return note{}, err + } + + if res.StatusCode != http.StatusCreated { + return note{}, errors.New("Request failed: " + res.Status) + } + + resBytes, err := io.ReadAll(res.Body) + if err != nil { + return note{}, err + } + + err = json.Unmarshal(resBytes, &n) + if err != nil { + return note{}, err + } + + return n, nil +} + +func deleteNote(n note) error { + req, err := http.NewRequest("DELETE", endpoint+"/notes/"+fmt.Sprint(n.Id), bytes.NewReader([]byte{})) + if err != nil { + return err + } + + res, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + + if res.StatusCode != http.StatusOK { + return errors.New("Request failed: " + res.Status) + } + + return nil +} diff --git a/main.go b/main.go index b0779b6..4de9da3 100644 --- a/main.go +++ b/main.go @@ -1,13 +1,9 @@ package main import ( - "bytes" - "encoding/json" - "errors" "fmt" "io" "log" - "net/http" "os" "os/exec" "strings" @@ -17,72 +13,18 @@ import ( "github.com/mattn/go-runewidth" ) -// TODO new, delete - -type note struct { - Id int64 `json:"id"` - Title string `json:"title"` - Content string `json:"content"` -} - const lineHeight = 2 const editor = "vim" -const endpoint = "http://localhost:8080/api" +const ( + uiMain = 0 + uiDelete = 1 +) +var uiState = uiMain var selectedNote = 0 var notes []note -func emitStr(s tcell.Screen, x int, y int, style tcell.Style, str string) { - for _, c := range str { - var comb []rune - w := runewidth.RuneWidth(c) - - if w == 0 { - comb = []rune{c} - c = ' ' - w = 1 - } - - s.SetContent(x, y, c, comb, style) - x += w - } -} - -func showNotes(s tcell.Screen) { - s.Fill(' ', tcell.StyleDefault) - - for idx, note := range notes { - style := tcell.StyleDefault - - if idx == selectedNote { - style = style.Foreground(tcell.ColorBlack).Background(tcell.ColorWhite) - } - - text := fmt.Sprint(idx) + ". " + note.Title - emitStr(s, 1, 1+idx*lineHeight, style, text) - } - - _, h := s.Size() - emitStr(s, 1, h-1, tcell.StyleDefault.Foreground(tcell.ColorBlue), "n - new, r - rename, d - delete, Enter - edit, Esc - exit") -} - -func loadNotes() { - res, err := http.Get(endpoint + "/notes") - if err != nil { - log.Fatalln(err) - } - - data, err := io.ReadAll(res.Body) - if err != nil { - log.Fatalln(err) - } - - var loadedNotes []note - json.Unmarshal(data, &loadedNotes) - notes = loadedNotes -} - func edit(s tcell.Screen, text string, ext string) (string, error) { cmd := exec.Command("vipe", "--suffix", ext) cmd.Env = append(cmd.Env, "EDITOR="+editor) @@ -111,31 +53,6 @@ func edit(s tcell.Screen, text string, ext string) (string, error) { return text, nil } -func putNote(n note) error { - json, err := json.Marshal(n) - if err != nil { - return err - } - - req, err := http.NewRequest("PUT", endpoint+"/notes/"+fmt.Sprint(n.Id), bytes.NewReader(json)) - if err != nil { - return err - } - - req.Header.Add("Content-Type", "application/json") - - res, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - - if res.StatusCode != 200 { - return errors.New("Request failed: " + res.Status) - } - - return nil -} - func editNote(s tcell.Screen, n note) (note, error) { newContent, err := edit(s, n.Content, "md") if err != nil { @@ -183,8 +100,65 @@ func editNoteTitle(s tcell.Screen, n note) (note, error) { return n, nil } +func emitStr(s tcell.Screen, x int, y int, style tcell.Style, str string) { + for _, c := range str { + var comb []rune + w := runewidth.RuneWidth(c) + + if w == 0 { + comb = []rune{c} + c = ' ' + w = 1 + } + + s.SetContent(x, y, c, comb, style) + x += w + } +} + +func showNotes(s tcell.Screen) { + uiState = uiMain + + s.Fill(' ', tcell.StyleDefault) + + for idx, note := range notes { + style := tcell.StyleDefault + + if idx == selectedNote { + style = style.Foreground(tcell.ColorBlack).Background(tcell.ColorWhite) + } + + text := fmt.Sprint(idx) + ". " + note.Title + emitStr(s, 1, 1+idx*lineHeight, style, text) + } + + _, h := s.Size() + emitStr(s, 1, h-1, tcell.StyleDefault.Foreground(tcell.ColorBlue), "n - new, r - rename, d - delete, Enter - edit, Esc - exit") +} + +func showDelete(s tcell.Screen) { + uiState = uiDelete + + s.Fill(' ', tcell.StyleDefault) + emitStr(s, 1, 1, tcell.StyleDefault, "Do you really want to delete the note '"+notes[selectedNote].Title+"'?") + emitStr(s, 1, 3, tcell.StyleDefault.Foreground(tcell.ColorBlue), "y - yes, n - no") +} + +func showUI(s tcell.Screen) { + switch uiState { + case uiMain: + showNotes(s) + case uiDelete: + showDelete(s) + } +} + func main() { - loadNotes() + var err error + notes, err = loadNotes() + if err != nil { + log.Fatalln(err) + } s, err := tcell.NewScreen() if err != nil { @@ -203,38 +177,30 @@ func main() { for { switch ev := s.PollEvent().(type) { case *tcell.EventResize: - showNotes(s) + showUI(s) s.Sync() case *tcell.EventKey: - switch ev.Key() { - case tcell.KeyUp: - if selectedNote > 0 { - selectedNote-- - } + switch uiState { + case uiMain: + switch ev.Key() { + case tcell.KeyUp: + if selectedNote > 0 { + selectedNote-- + } - showNotes(s) - s.Show() + showNotes(s) + s.Show() - case tcell.KeyDown: - if selectedNote < len(notes)-1 { - selectedNote++ - } + case tcell.KeyDown: + if selectedNote < len(notes)-1 { + selectedNote++ + } - showNotes(s) - s.Show() + showNotes(s) + s.Show() - case tcell.KeyEnter: - n, err := editNote(s, notes[selectedNote]) - if err != nil { - s.Fini() - log.Fatalln(err) - } - - notes[selectedNote] = n - - case tcell.KeyRune: - if ev.Rune() == 'r' { - n, err := editNoteTitle(s, notes[selectedNote]) + case tcell.KeyEnter: + n, err := editNote(s, notes[selectedNote]) if err != nil { s.Fini() log.Fatalln(err) @@ -242,13 +208,80 @@ func main() { notes[selectedNote] = n - showNotes(s) - s.Sync() + case tcell.KeyEscape: + s.Fini() + os.Exit(1) + + case tcell.KeyRune: + switch ev.Rune() { + case 'r': + n, err := editNoteTitle(s, notes[selectedNote]) + if err != nil { + s.Fini() + log.Fatalln(err) + } + + notes[selectedNote] = n + + showNotes(s) + s.Sync() + + case 'n': + title, err := edit(s, "", "txt") + if err != nil { + s.Fini() + log.Fatalln(err) + } + + if len(title) == 0 { + showNotes(s) + s.Sync() + break + } + + n2, err := postNote(note{Title: title}) + if err != nil { + s.Fini() + log.Fatalln(err) + } + + notes = append(notes, n2) + + selectedNote = len(notes) - 1 + + showNotes(s) + s.Sync() + + case 'd': + showDelete(s) + s.Show() + } } - case tcell.KeyEscape: - s.Fini() - os.Exit(1) + case uiDelete: + switch ev.Key() { + case tcell.KeyRune: + switch ev.Rune() { + case 'y': + err := deleteNote(notes[selectedNote]) + if err != nil { + s.Fini() + log.Fatalln(err) + } + + notes = append(notes[:selectedNote], notes[selectedNote+1:]...) + + if selectedNote >= len(notes) { + selectedNote = len(notes) - 1 + } + + showNotes(s) + s.Show() + case 'n': + showNotes(s) + s.Show() + } + } } } }