package main import ( "context" "fmt" "log" "os" "runtime/debug" "sync" "git.yetaga.in/alazyreader/library/book" "git.yetaga.in/alazyreader/library/config" "git.yetaga.in/alazyreader/library/database" "git.yetaga.in/alazyreader/library/ui" "github.com/gdamore/tcell" "github.com/kelseyhightower/envconfig" ) // 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 } // UI states const ( IN_MENU = iota IN_BOOK IN_IMPORT ) func main() { var c config.Config err := envconfig.Process("library", &c) if err != nil { log.Fatalln(err) } // create state state := State{} // set up DB connection if c.DBUser == "" || c.DBPass == "" || c.DBHost == "" || c.DBPort == "" || c.DBName == "" { 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) 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 } }() // book list and options menu (left column) l := ui.NewList(Titles(state.Get("library").([]book.Book)), 0) menu := ui.NewBox( "library", []string{"˄˅ select", "⏎ edit", "(n)ew", "(i)mport", "(q)uit"}, ui.Contents{{ Offsets: ui.Offsets{Top: 1, Left: 2, Bottom: -2, Right: -2}, Container: l, }}, ui.StyleActive, false, ) book := ui.NewBookDetails(&book.Book{ Title: "test title", }) // book display (right column) activeBook := ui.NewBox( "book", []string{"˄˅ select", "⏎ edit", "(esc) close"}, ui.Contents{{ Offsets: ui.Offsets{Top: 1, Left: 2, Bottom: 0, Right: 0}, Container: book, }}, ui.StyleInactive, false, ) // parent container container := ui.NewContainer( ui.Contents{ {Container: menu, Offsets: ui.Offsets{Percent: 1}}, {Container: activeBook, Offsets: ui.Offsets{Percent: 2}}, }, ui.LayoutHorizontalPercent, ) // import pop-up popup := ui.NewBox("import", []string{"⏎ submit", "(esc)close"}, nil, ui.StyleActive, false) popup.SetVisible(false) // init screen.Clear() w, h := screen.Size() container.SetSize(0, 0, h, w) container.Draw(screen) screen.Sync() // init UI state state.Set("ui_state", IN_MENU) screen.PostEvent(NewEventLoadBook(l.SelectedID())) // UI loop for { e := screen.PollEvent() switch v := e.(type) { case *tcell.EventError: fmt.Fprintf(os.Stderr, "%v", v) screen.Beep() case *tcell.EventKey: // input handling curr := state.Get("ui_state").(int) if curr == IN_MENU { if v.Key() == tcell.KeyUp && l.Selected() > 0 { l.SetSelected(l.Selected() - 1) screen.PostEvent(NewEventLoadBook(l.SelectedID())) } if v.Key() == tcell.KeyDown && l.Selected() < len(l.ListMembers())-1 { l.SetSelected(l.Selected() + 1) screen.PostEvent(NewEventLoadBook(l.SelectedID())) } if v.Key() == tcell.KeyEnter { screen.PostEvent(NewEventEnterBook()) } if v.Rune() == 'i' { screen.PostEvent(NewEventOpenImport()) } if v.Rune() == 'q' { screen.PostEvent(NewEventQuit()) } } else if curr == IN_BOOK { if v.Key() == tcell.KeyEsc { screen.PostEvent(NewEventExitBook()) } } else if curr == IN_IMPORT { if v.Key() == tcell.KeyEsc { screen.PostEvent(NewEventCloseImport()) } } case *tcell.EventResize: // screen redraw w, h := screen.Size() container.SetSize(0, 0, h, w) case *EventBookUpdate: // TK 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) case *EventLoadBook: book.SetBook(GetBookByID(v.ID, books)) case *EventOpenImport: state.Set("ui_state", IN_IMPORT) menu.SetStyle(ui.StyleInactive) popup.SetVisible(true) popup.SetSize(6, 3, 10, 80) case *EventCloseImport: state.Set("ui_state", IN_MENU) menu.SetStyle(ui.StyleActive) popup.SetVisible(false) case *EventQuit: screen.Fini() fmt.Printf("Thank you for playing Wing Commander!\n\n") return case *tcell.EventInterrupt: case *tcell.EventMouse: case *tcell.EventTime: default: } // repaint screen.Clear() container.Draw(screen) popup.Draw(screen) screen.Show() } } func Titles(lb []book.Book) []ui.ListKeyValue { r := []ui.ListKeyValue{} for i := range lb { r = append(r, ui.ListKeyValue{ Key: lb[i].ID, Value: lb[i].Title, }) } return r } func GetBookByID(id int, lb []book.Book) *book.Book { for i := range lb { if lb[i].ID == id { return &lb[i] } } return &book.Book{} }