library/cmd/cli/main.go

337 lines
7.9 KiB
Go
Raw Normal View History

2021-06-26 18:08:35 +00:00
package main
2021-07-03 00:46:58 +00:00
import (
2021-07-07 01:40:12 +00:00
"context"
2021-07-03 01:44:45 +00:00
"fmt"
2021-07-03 00:46:58 +00:00
"log"
2021-07-07 01:40:12 +00:00
"os"
"runtime/debug"
2021-08-08 20:56:45 +00:00
"strings"
2021-07-07 01:40:12 +00:00
"sync"
2021-07-03 00:46:58 +00:00
"git.yetaga.in/alazyreader/library/config"
2021-07-07 01:40:12 +00:00
"git.yetaga.in/alazyreader/library/database"
2021-08-01 22:50:53 +00:00
"git.yetaga.in/alazyreader/library/importer"
2022-03-05 15:58:15 +00:00
"git.yetaga.in/alazyreader/library/media"
2021-07-03 17:30:08 +00:00
"git.yetaga.in/alazyreader/library/ui"
2023-04-08 02:42:54 +00:00
"github.com/gdamore/tcell/v2"
2021-07-03 00:46:58 +00:00
"github.com/kelseyhightower/envconfig"
)
2021-07-07 01:40:12 +00:00
// State holds the UI state keys=>value map and manages access to the map with a mutex
type State struct {
m sync.Mutex
stateMap map[string]interface{}
}
// key, present
func (s *State) Get(key string) interface{} {
s.m.Lock()
defer s.m.Unlock()
if s.stateMap == nil {
s.stateMap = make(map[string]interface{})
}
k, ok := s.stateMap[key]
if !ok {
return nil
}
return k
}
// key, value
func (s *State) Set(key string, value interface{}) {
s.m.Lock()
defer s.m.Unlock()
if s.stateMap == nil {
s.stateMap = make(map[string]interface{})
}
s.stateMap[key] = value
}
2021-08-08 20:56:45 +00:00
func max(a, b int) int {
if a > b {
return a
}
return b
}
2021-07-29 01:36:45 +00:00
// UI states
const (
IN_MENU = iota
IN_BOOK
2021-07-29 01:36:45 +00:00
IN_IMPORT
)
2021-06-26 18:08:35 +00:00
func main() {
2021-07-03 00:46:58 +00:00
var c config.Config
err := envconfig.Process("library", &c)
if err != nil {
log.Fatalln(err)
}
2021-07-07 01:40:12 +00:00
// create state
state := State{}
// set up DB connection
if c.DBUser == "" || c.DBPass == "" || c.DBHost == "" || c.DBPort == "" || c.DBName == "" {
2021-08-08 20:56:45 +00:00
if c.DBPass != "" { // obscure password
c.DBPass = c.DBPass[0:max(3, len(c.DBPass))] + strings.Repeat("*", max(0, len(c.DBPass)-3))
}
2021-07-07 01:40:12 +00:00
log.Fatalf("vars: %+v", c)
}
lib, err := database.NewMySQLConnection(c.DBUser, c.DBPass, c.DBHost, c.DBPort, c.DBName)
if err != nil {
log.Fatalln(err)
}
err = lib.PrepareDatabase(context.Background())
if err != nil {
log.Fatalln(err)
}
_, _, err = lib.RunMigrations(context.Background())
if err != nil {
log.Fatalln(err)
}
books, err := lib.GetAllBooks(context.Background())
if err != nil {
log.Fatalln(err)
}
state.Set("library", books)
2021-07-03 00:46:58 +00:00
screen, err := tcell.NewScreen()
if err != nil {
log.Fatalln(err)
}
err = screen.Init()
if err != nil {
log.Fatalln(err)
}
// cleanup our screen and log if we panic and crash out somewhere
defer func() {
if r := recover(); r != nil {
if screen != nil {
screen.Fini()
}
fmt.Println("fatal panic;", r)
if c.Debug {
fmt.Println("stacktrace: \n" + string(debug.Stack()))
}
return
}
}()
2021-07-03 14:58:45 +00:00
2021-07-29 01:36:45 +00:00
// book list and options menu (left column)
2022-03-05 15:58:15 +00:00
l := ui.NewList(Titles(state.Get("library").([]media.Book)), 0)
2021-07-03 17:30:08 +00:00
menu := ui.NewBox(
2021-07-03 14:58:45 +00:00
"library",
2021-07-29 01:50:09 +00:00
[]string{"˄˅ select", "⏎ edit", "(n)ew", "(i)mport", "(q)uit"},
2021-07-03 17:30:08 +00:00
ui.Contents{{
Offsets: ui.Offsets{Top: 1, Left: 2, Bottom: -2, Right: -2},
Container: l,
2021-07-03 14:58:45 +00:00
}},
2021-07-13 22:32:01 +00:00
ui.StyleActive,
false,
2021-07-03 14:58:45 +00:00
)
2022-03-05 15:58:15 +00:00
activeBookDetails := ui.NewBookDetails(&media.Book{})
2021-07-29 01:36:45 +00:00
// book display (right column)
2021-07-03 17:30:08 +00:00
activeBook := ui.NewBox(
"book",
2021-07-29 01:50:09 +00:00
[]string{"˄˅ select", "⏎ edit", "(esc) close"},
ui.Contents{{
2021-07-07 01:40:12 +00:00
Offsets: ui.Offsets{Top: 1, Left: 2, Bottom: 0, Right: 0},
2021-08-01 22:50:53 +00:00
Container: activeBookDetails,
}},
2021-07-13 22:32:01 +00:00
ui.StyleInactive,
false,
2021-07-03 17:30:08 +00:00
)
2021-07-29 01:41:35 +00:00
// parent container
2021-07-03 17:30:08 +00:00
container := ui.NewContainer(
ui.Contents{
{Container: menu, Offsets: ui.Offsets{Percent: 1}},
{Container: activeBook, Offsets: ui.Offsets{Percent: 2}},
},
ui.LayoutHorizontalPercent,
2021-07-03 17:30:08 +00:00
)
2021-07-03 14:58:45 +00:00
2021-07-29 01:41:35 +00:00
// import pop-up
2021-08-01 19:50:11 +00:00
wd, _ := os.Getwd()
fileSelector := ui.NewEditableTextLine(wd)
fileSelector.ResetCursor(false)
fileSelector.SetStyle(ui.StyleActive.Underline(true))
popup := ui.NewBox(
"import csv file",
[]string{"⏎ submit", "(esc)close"},
ui.Contents{
{Container: fileSelector, Offsets: ui.Offsets{Top: 2, Left: 2}},
},
ui.StyleActive,
false,
)
2021-07-29 01:41:35 +00:00
popup.SetVisible(false)
2021-08-01 22:50:53 +00:00
// error pop-up
errorMessage := ui.NewEditableTextLine("")
errorPopup := ui.NewBox(
"error",
[]string{"⏎ close"},
ui.Contents{
{Container: errorMessage, Offsets: ui.Offsets{Top: 1, Left: 1}},
},
ui.StyleActive.Bold(true).Foreground(tcell.ColorRed),
false,
)
errorPopup.SetVisible(false)
2021-07-03 14:58:45 +00:00
// init
2021-07-03 00:46:58 +00:00
screen.Clear()
2021-07-03 01:44:45 +00:00
w, h := screen.Size()
2021-07-03 17:30:08 +00:00
container.SetSize(0, 0, h, w)
container.Draw(screen)
2021-07-03 14:58:45 +00:00
screen.Sync()
2021-07-07 01:40:12 +00:00
// init UI state
state.Set("ui_state", IN_MENU)
2021-07-13 22:32:01 +00:00
screen.PostEvent(NewEventLoadBook(l.SelectedID()))
2021-07-03 14:58:45 +00:00
// UI loop
2021-07-03 00:46:58 +00:00
for {
e := screen.PollEvent()
switch v := e.(type) {
case *tcell.EventError:
2021-07-07 01:40:12 +00:00
fmt.Fprintf(os.Stderr, "%v", v)
2021-07-03 00:46:58 +00:00
screen.Beep()
2021-07-03 14:58:45 +00:00
case *tcell.EventKey: // input handling
2021-07-07 01:40:12 +00:00
curr := state.Get("ui_state").(int)
if curr == IN_MENU {
if v.Key() == tcell.KeyUp && l.Selected() > 0 {
l.SetSelected(l.Selected() - 1)
2021-07-13 22:32:01 +00:00
screen.PostEvent(NewEventLoadBook(l.SelectedID()))
}
if v.Key() == tcell.KeyDown && l.Selected() < len(l.ListMembers())-1 {
l.SetSelected(l.Selected() + 1)
2021-07-13 22:32:01 +00:00
screen.PostEvent(NewEventLoadBook(l.SelectedID()))
}
2021-07-13 22:32:01 +00:00
if v.Key() == tcell.KeyEnter {
2021-07-29 01:36:45 +00:00
screen.PostEvent(NewEventEnterBook())
}
if v.Rune() == 'i' {
screen.PostEvent(NewEventOpenImport())
}
if v.Rune() == 'q' {
screen.PostEvent(NewEventQuit())
}
2021-07-07 01:40:12 +00:00
} else if curr == IN_BOOK {
2021-07-13 22:32:01 +00:00
if v.Key() == tcell.KeyEsc {
2021-07-29 01:36:45 +00:00
screen.PostEvent(NewEventExitBook())
}
} else if curr == IN_IMPORT {
if v.Key() == tcell.KeyEsc {
2021-08-01 19:50:11 +00:00
fileSelector.SetText(wd)
fileSelector.ResetCursor(false)
2021-07-29 01:36:45 +00:00
screen.PostEvent(NewEventCloseImport())
}
2021-08-01 19:50:11 +00:00
if v.Key() == tcell.KeyBackspace || v.Key() == tcell.KeyBackspace2 {
fileSelector.DeleteAtCursor()
} else if v.Key() == tcell.KeyRight {
fileSelector.MoveCursor(1)
} else if v.Key() == tcell.KeyLeft {
fileSelector.MoveCursor(-1)
2021-08-01 22:50:53 +00:00
} else if v.Key() == tcell.KeyEnter {
screen.PostEvent(NewEventAttemptImport(fileSelector.Text()))
2021-08-01 19:50:11 +00:00
} else if v.Rune() != 0 {
fileSelector.InsertAtCursor(v.Rune())
}
2021-07-03 00:46:58 +00:00
}
2021-07-03 14:58:45 +00:00
case *tcell.EventResize: // screen redraw
2021-07-03 01:44:45 +00:00
w, h := screen.Size()
2021-07-03 17:30:08 +00:00
container.SetSize(0, 0, h, w)
2021-07-07 01:40:12 +00:00
case *EventBookUpdate:
// TK
2021-07-29 01:36:45 +00:00
case *EventEnterBook:
activeBook.SetStyle(ui.StyleActive)
menu.SetStyle(ui.StyleInactive)
state.Set("ui_state", IN_BOOK)
case *EventExitBook:
state.Set("ui_state", IN_MENU)
activeBook.SetStyle(ui.StyleInactive)
menu.SetStyle(ui.StyleActive)
2021-07-07 01:40:12 +00:00
case *EventLoadBook:
2021-08-01 22:50:53 +00:00
activeBookDetails.SetBook(GetBookByID(v.ID, books))
2021-07-29 01:36:45 +00:00
case *EventOpenImport:
state.Set("ui_state", IN_IMPORT)
menu.SetStyle(ui.StyleInactive)
popup.SetVisible(true)
2021-08-01 19:50:11 +00:00
popup.SetSize(6, 3, 5, 80)
2021-08-01 22:50:53 +00:00
case *EventAttemptImport:
// this will block other events, but it shouldn't take too long...
f, err := os.Open(v.filename)
if err != nil {
screen.PostEvent(NewEventError(err))
continue
}
books, err := importer.CSVToBooks(f)
if err != nil {
screen.PostEvent(NewEventError(err))
continue
}
for b := range books {
err = lib.AddBook(context.Background(), &books[b])
if err != nil {
screen.PostEvent(NewEventError(err))
}
}
screen.PostEvent(NewEventCloseImport())
allbooks, err := lib.GetAllBooks(context.Background())
if err != nil {
screen.PostEvent(NewEventError(err))
}
state.Set("library", allbooks)
state.Set("ui_state", IN_MENU)
2021-07-29 01:36:45 +00:00
case *EventCloseImport:
state.Set("ui_state", IN_MENU)
2021-08-01 22:50:53 +00:00
screen.HideCursor()
2021-07-29 01:36:45 +00:00
menu.SetStyle(ui.StyleActive)
popup.SetVisible(false)
2021-08-01 22:50:53 +00:00
case *EventError:
errorMessage.SetText(v.err.Error())
errorPopup.SetVisible(true)
2021-07-29 01:36:45 +00:00
case *EventQuit:
screen.Fini()
fmt.Printf("Thank you for playing Wing Commander!\n\n")
return
2021-07-03 01:44:45 +00:00
case *tcell.EventInterrupt:
case *tcell.EventMouse:
2021-07-03 00:46:58 +00:00
case *tcell.EventTime:
2021-07-03 01:44:45 +00:00
default:
2021-07-03 00:46:58 +00:00
}
2021-07-29 01:36:45 +00:00
// repaint
2022-03-05 15:58:15 +00:00
l.SetMembers(Titles(state.Get("library").([]media.Book)))
2021-07-29 01:36:45 +00:00
container.Draw(screen)
popup.Draw(screen)
2021-08-01 22:50:53 +00:00
errorPopup.Draw(screen)
2021-07-29 01:36:45 +00:00
screen.Show()
2021-07-03 01:44:45 +00:00
}
2021-06-26 18:08:35 +00:00
}
2021-07-07 01:40:12 +00:00
2022-03-05 15:58:15 +00:00
func Titles(lb []media.Book) []ui.ListKeyValue {
2021-07-07 01:40:12 +00:00
r := []ui.ListKeyValue{}
for i := range lb {
r = append(r, ui.ListKeyValue{
Key: lb[i].ID,
Value: lb[i].Title,
})
}
return r
}
2022-03-05 15:58:15 +00:00
func GetBookByID(id int, lb []media.Book) *media.Book {
2021-07-07 01:40:12 +00:00
for i := range lb {
if lb[i].ID == id {
return &lb[i]
}
}
2022-03-05 15:58:15 +00:00
return &media.Book{}
2021-07-07 01:40:12 +00:00
}