Compare commits
7 Commits
ab1d68b88f
...
main
Author | SHA1 | Date | |
---|---|---|---|
5d90305b95 | |||
17f34afb2e | |||
56ca9d0cb8 | |||
2aa18e77e3 | |||
33938a31e8 | |||
fd0732e6f0 | |||
714543bbfe |
20
go.mod
Normal file
20
go.mod
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
module miningtcup.me/notes-cli
|
||||||
|
|
||||||
|
go 1.24.6
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/manifoldco/promptui v0.9.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
||||||
|
golang.org/x/sys v0.35.0 // indirect
|
||||||
|
golang.org/x/term v0.34.0 // indirect
|
||||||
|
modernc.org/libc v1.66.3 // indirect
|
||||||
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
|
modernc.org/memory v1.11.0 // indirect
|
||||||
|
modernc.org/sqlite v1.38.2 // indirect
|
||||||
|
)
|
34
go.sum
Normal file
34
go.sum
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||||
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
|
||||||
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||||
|
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
|
||||||
|
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
||||||
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||||
|
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||||
|
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||||
|
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||||
|
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||||
|
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
|
||||||
|
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
|
||||||
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
|
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
|
modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
|
||||||
|
modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
|
353
main.go
Normal file
353
main.go
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"log"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/term"
|
||||||
|
|
||||||
|
"github.com/manifoldco/promptui"
|
||||||
|
|
||||||
|
_ "modernc.org/sqlite"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
serverAddress string
|
||||||
|
username string
|
||||||
|
password string
|
||||||
|
dataDir string
|
||||||
|
dataPath string
|
||||||
|
cacheDir string
|
||||||
|
cachePath string
|
||||||
|
scanner bufio.Scanner
|
||||||
|
)
|
||||||
|
|
||||||
|
type Note struct {
|
||||||
|
Body string `json:"body"`
|
||||||
|
Color int64 `json:"color"`
|
||||||
|
Date string `json:"date"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanInt(promptFormat string, maxNum int64, minNum int64, promptA ...any) (choice int64, err error) {
|
||||||
|
validateInt := func(input string) error {
|
||||||
|
parsedInt, err := strconv.ParseInt(input, 10, 64)
|
||||||
|
if parsedInt > maxNum || parsedInt < minNum {
|
||||||
|
return fmt.Errorf("Out of bounds (must be between [inclusive] %d and %d)", minNum, maxNum)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt := promptui.Prompt{
|
||||||
|
Label: fmt.Sprintf(promptFormat, promptA...),
|
||||||
|
Validate: validateInt,
|
||||||
|
}
|
||||||
|
|
||||||
|
choiceString, err := prompt.Run()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
choice, err = strconv.ParseInt(choiceString, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanString(template string, promptFormat string, promptA ...any) (choice string, err error) {
|
||||||
|
prompt := promptui.Prompt{
|
||||||
|
Label: fmt.Sprintf(promptFormat, promptA...),
|
||||||
|
Default: template,
|
||||||
|
}
|
||||||
|
|
||||||
|
choice, err = prompt.Run()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanConfirm(promptFormat string, promptA ...any) (choice string, err error) {
|
||||||
|
prompt := promptui.Prompt{
|
||||||
|
Label: fmt.Sprintf(promptFormat, promptA...),
|
||||||
|
IsConfirm: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
choice, err = prompt.Run()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanSelect(selection []string, template string, promptFormat string, promptA ...any) (choice int, err error) {
|
||||||
|
prompt := promptui.Select{
|
||||||
|
Label: fmt.Sprintf(promptFormat, promptA...),
|
||||||
|
Items: selection,
|
||||||
|
}
|
||||||
|
|
||||||
|
choice, _, err = prompt.Run()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func readPassword() (string, error) {
|
||||||
|
stdin := int(syscall.Stdin)
|
||||||
|
oldState, err := term.GetState(stdin)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer term.Restore(stdin, oldState)
|
||||||
|
|
||||||
|
sigch := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigch, os.Interrupt)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-sigch
|
||||||
|
term.Restore(stdin, oldState)
|
||||||
|
os.Exit(1)
|
||||||
|
}()
|
||||||
|
|
||||||
|
password, err := term.ReadPassword(stdin)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(password), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
scanner = *bufio.NewScanner(os.Stdin)
|
||||||
|
home := os.Getenv("HOME")
|
||||||
|
dataPath = os.Getenv("XDG_DATA_HOME")
|
||||||
|
cachePath = os.Getenv("XDG_CACHE_HOME")
|
||||||
|
if len(dataPath) < 1 {
|
||||||
|
dataPath = home + "/.local/share"
|
||||||
|
}
|
||||||
|
if len(cachePath) < 1 {
|
||||||
|
cachePath = home + "/.cache"
|
||||||
|
}
|
||||||
|
cacheDir = cachePath + "/notes"
|
||||||
|
cachePath += "/notes/notes.db"
|
||||||
|
dataDir = dataPath + "/notes"
|
||||||
|
dataPath += "/notes/userinfo.json"
|
||||||
|
|
||||||
|
err := os.Mkdir(cacheDir, 0700)
|
||||||
|
if !errors.Is(err, os.ErrExist) {
|
||||||
|
check(err, "Error creating cache directory")
|
||||||
|
}
|
||||||
|
db, err := sql.Open("sqlite", cachePath)
|
||||||
|
check(err, "Error opening cache database at '%s'", cachePath)
|
||||||
|
check(db.Ping(), "Ping of cache database failed!")
|
||||||
|
db.Exec(`
|
||||||
|
CREATE TABLE notes (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
data TEXT
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
|
||||||
|
args := os.Args[1:]
|
||||||
|
var command string
|
||||||
|
if len(args) < 1 {
|
||||||
|
command = ""
|
||||||
|
} else {
|
||||||
|
command = args[0]
|
||||||
|
}
|
||||||
|
switch command {
|
||||||
|
case "ls":
|
||||||
|
userDataBytes, err := os.ReadFile(dataPath)
|
||||||
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
|
login()
|
||||||
|
}
|
||||||
|
check(err, "Error reading user data at '%s'", dataPath)
|
||||||
|
var userData []string
|
||||||
|
err = json.Unmarshal(userDataBytes, &userData)
|
||||||
|
check(err, "Error unmarshalling user data")
|
||||||
|
|
||||||
|
serverAddress, username, password = userData[0], userData[1], userData[2]
|
||||||
|
var notes []Note
|
||||||
|
// fzf := exec.Command("fzf", "--ansi")
|
||||||
|
// pipe, err := fzf.StdinPipe()
|
||||||
|
// check(err, "Error getting stdin pipe for fzf")
|
||||||
|
// check(fzf.Start(), "Error starting fzf")
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
cacheJsonBody, err := load(db)
|
||||||
|
check(err, "Error loading data from cache")
|
||||||
|
for i := range cacheJsonBody {
|
||||||
|
go list(cacheJsonBody[i], ¬es, &wg)
|
||||||
|
wg.Add(1)
|
||||||
|
}
|
||||||
|
fetchJsonBody, err := fetch()
|
||||||
|
if err == nil {
|
||||||
|
for i := range fetchJsonBody {
|
||||||
|
if !slices.Contains(cacheJsonBody, fetchJsonBody[i]) {
|
||||||
|
go list(fetchJsonBody[i], ¬es, &wg)
|
||||||
|
wg.Add(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
slices.SortFunc(notes, func(a, b Note) int {
|
||||||
|
return strings.Compare(b.Date, a.Date)
|
||||||
|
})
|
||||||
|
for i := range notes {
|
||||||
|
timestamp, err := strconv.ParseInt(notes[i].Date, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error parsing Unix timestamp", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
date := time.Unix(timestamp/1000, 0).Format("Mon, Jan 01, 06")
|
||||||
|
r, g, b, _ := decodeColorInt(notes[i].Color)
|
||||||
|
check(err, "Error decoding color int %d", notes[i].Color)
|
||||||
|
formattedNote := strings.TrimSpace(
|
||||||
|
fmt.Sprintf(
|
||||||
|
"[%02d] %s | \033[1;38;2;%d;%d;%dm%s\033[0m | %s\n",
|
||||||
|
i+1,
|
||||||
|
date,
|
||||||
|
r,
|
||||||
|
g,
|
||||||
|
b,
|
||||||
|
strings.TrimSpace(notes[i].Title),
|
||||||
|
strings.ReplaceAll(
|
||||||
|
notes[i].Body,
|
||||||
|
"\n",
|
||||||
|
"/",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
terminalWidth, _, err := term.GetSize(int(syscall.Stdin))
|
||||||
|
check(err, "Error getting terminal size for stdin")
|
||||||
|
fmt.Printf("%.*s\n", terminalWidth, formattedNote)
|
||||||
|
}
|
||||||
|
// pipe.Close()
|
||||||
|
cache(db, fetchJsonBody)
|
||||||
|
// fzf.Wait()
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
if fetchJsonBody == nil {
|
||||||
|
fmt.Println("\033[1;31mNotes couldn't be loaded from the server.\033[0m")
|
||||||
|
fmt.Println("A connection to the server is required to create, edit, or delete notes.")
|
||||||
|
fmt.Println("\033[1mTroubleshooting steps:\033[0m")
|
||||||
|
fmt.Println(` * Check your internet or network connection.
|
||||||
|
* Make sure the server address is correct.
|
||||||
|
* Make sure the server is running the latest version of noteserver.
|
||||||
|
* Check the server's internet or network connection.
|
||||||
|
`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
chosenOperationIndex, err := scanSelect([]string{"\033[32mCreate\033[0m", "\033[31mDelete\033[0m", "\033[33mEdit\033[0m"}, "", "Select operation")
|
||||||
|
check(err, "Error scanning selection")
|
||||||
|
switch chosenOperationIndex {
|
||||||
|
case 0:
|
||||||
|
create(Note{})
|
||||||
|
fmt.Println("Success!")
|
||||||
|
case 1:
|
||||||
|
chosenNoteIndex, err := scanInt(
|
||||||
|
"Select note to \033[31mdelete\033[37m by index (1-%d)",
|
||||||
|
int64(len(notes)),
|
||||||
|
1,
|
||||||
|
len(notes),
|
||||||
|
)
|
||||||
|
check(err, "Error scanning integer")
|
||||||
|
chosenNoteIndex -= 1
|
||||||
|
scanConfirm("Really delete '%s'", notes[chosenNoteIndex].Title)
|
||||||
|
del(notes[chosenNoteIndex].ID, db)
|
||||||
|
case 2:
|
||||||
|
chosenNoteIndex, err := scanInt(
|
||||||
|
"Select note to \033[33medit\033[37m by index (1-%d)",
|
||||||
|
int64(len(notes)),
|
||||||
|
1,
|
||||||
|
len(notes),
|
||||||
|
)
|
||||||
|
check(err, "Error scanning integer")
|
||||||
|
chosenNoteIndex -= 1
|
||||||
|
create(notes[chosenNoteIndex])
|
||||||
|
del(notes[chosenNoteIndex].ID, db)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
login()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cache(db *sql.DB, jsonData [][2]string) {
|
||||||
|
for i := range jsonData {
|
||||||
|
var id string
|
||||||
|
err := db.QueryRow("SELECT id FROM notes WHERE id = ?", jsonData[i][0]).Scan(&id)
|
||||||
|
if err != sql.ErrNoRows {
|
||||||
|
check(err, "Error scanning cache database at %s for duplicates", cachePath)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
stmt, err := db.Prepare("INSERT INTO notes VALUES(?, ?)")
|
||||||
|
check(err, "Error preparing to insert note into cache database at %s", cachePath)
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
_, err = stmt.Exec(jsonData[i][0], jsonData[i][1])
|
||||||
|
check(err, "Error executing insert note into cache database at %s", cachePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func login() {
|
||||||
|
fmt.Println("Welcome to Notes! To get started, choose your server and user.")
|
||||||
|
fmt.Print("Server Address [notes.miningtcup.me]: ")
|
||||||
|
_, err := fmt.Scanln(&serverAddress)
|
||||||
|
if err != nil {
|
||||||
|
serverAddress = "notes.miningtcup.me"
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Print("Username: ")
|
||||||
|
scanner.Scan()
|
||||||
|
username = url.QueryEscape(scanner.Text())
|
||||||
|
|
||||||
|
fmt.Print("Password: ")
|
||||||
|
passwordBytes, err := readPassword()
|
||||||
|
check(err, "Error reading user input (password)")
|
||||||
|
password = url.QueryEscape(string(passwordBytes))
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal([]string{serverAddress, username, password})
|
||||||
|
check(err, "Error marshalling server address and credentials")
|
||||||
|
err = os.Mkdir(dataDir, 0700)
|
||||||
|
if !errors.Is(err, fs.ErrExist) {
|
||||||
|
check(err, "Error creating data directory at '%s'", dataDir)
|
||||||
|
}
|
||||||
|
err = os.WriteFile(dataPath, jsonData, 0600)
|
||||||
|
check(err, "Error writing to '%s'", dataPath)
|
||||||
|
|
||||||
|
fmt.Println("\nAwesome! To get started, run 'notes add' or 'notes list'")
|
||||||
|
}
|
||||||
|
|
||||||
|
func check(err error, format string, a ...any) bool {
|
||||||
|
if err != nil {
|
||||||
|
if writeErr := os.WriteFile("error.log", fmt.Appendf([]byte{}, format+": %s", a, err), 0600); writeErr != nil {
|
||||||
|
log.Fatalf("\033[31mError writing error '%s' to error.log:\033[0m %s\n", err, writeErr)
|
||||||
|
}
|
||||||
|
log.Fatalf("\033[31m"+format+":\033[0m %s\n", a, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
Reference in New Issue
Block a user