256 lines
4.1 KiB
Go
256 lines
4.1 KiB
Go
|
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)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|