package main import ( "bytes" "encoding/json" "errors" "fmt" "io" "log" "net/http" "os" "os/exec" "strings" "github.com/gdamore/tcell/v2" _ "github.com/gdamore/tcell/v2/encoding" "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" 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) pipe, _ := cmd.StdinPipe() io.WriteString(pipe, text) pipe.Close() err := s.Suspend() if err != nil { return "", err } output, err := cmd.Output() if err != nil { return "", err } text = string(output) err = s.Resume() if err != nil { return "", err } 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 { return note{}, err } n.Content = newContent err = putNote(n) if err != nil { return note{}, nil } showNotes(s) s.Sync() return n, nil } func editNoteTitle(s tcell.Screen, n note) (note, error) { newTitle, err := edit(s, n.Title, "txt") if err != nil { return note{}, err } idx := strings.Index(newTitle, "\n") if idx != -1 { newTitle = newTitle[:idx] } if len(newTitle) == 0 { return n, nil } n.Title = newTitle err = putNote(n) if err != nil { return note{}, nil } showNotes(s) s.Sync() return n, nil } func main() { loadNotes() s, err := tcell.NewScreen() if err != nil { log.Fatalln(err) } if err := s.Init(); err != nil { log.Fatalln(err) } defStyle := tcell.StyleDefault.Background(tcell.ColorBlack).Foreground(tcell.ColorWhite) s.SetStyle(defStyle) showNotes(s) for { switch ev := s.PollEvent().(type) { case *tcell.EventResize: showNotes(s) s.Sync() case *tcell.EventKey: switch ev.Key() { case tcell.KeyUp: if selectedNote > 0 { selectedNote-- } showNotes(s) s.Show() case tcell.KeyDown: if selectedNote < len(notes)-1 { selectedNote++ } 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]) if err != nil { s.Fini() log.Fatalln(err) } notes[selectedNote] = n showNotes(s) s.Sync() } case tcell.KeyEscape: s.Fini() os.Exit(1) } } } }