tui.go - scoreboard - Interactive scoreboard for CTF-like games
 (HTM) git clone git://git.z3bra.org/scoreboard.git
 (DIR) Log
 (DIR) Files
 (DIR) Refs
       ---
       tui.go (6644B)
       ---
            1 package main
            2 
            3 import (
            4         "fmt"
            5         "math"
            6         "regexp"
            7         "github.com/gdamore/tcell/v2"
            8         "github.com/rivo/tview"
            9         "github.com/dustin/go-humanize"
           10 )
           11 
           12 const (
           13         HELP_TEXT = `
           14 
           15 
           16 H  Display Help
           17 L  Log in      
           18 ↲  Show Badges 
           19 
           20 
           21 
           22  Log in and select your name to see your flags
           23 `
           24 )
           25 
           26 func center(width, height int, p tview.Primitive) tview.Primitive {
           27         return tview.NewFlex().
           28                 AddItem(nil, 0, 1, false).
           29                 AddItem(tview.NewFlex().
           30                         SetDirection(tview.FlexRow).
           31                         AddItem(nil, 0, 1, false).
           32                         AddItem(p, height, 1, true).
           33                         AddItem(nil, 0, 1, false), width, 1, true).
           34                 AddItem(nil, 0, 1, false)
           35 }
           36 
           37 func BoardHeader() *tview.TextView {
           38         return tview.NewTextView().
           39                 SetDynamicColors(true).
           40                 SetTextAlign(tview.AlignRight).
           41                 SetText("[::b] RANK  NAME   FLAGS   SCORE ")
           42 }
           43 
           44 // Return a table filled with scores from the selected range
           45 // Optionally padded with "placeholder" lines
           46 func RankTable(offset, limit, rank int, fill bool) *tview.Table {
           47         t := tview.NewTable()
           48 
           49         players, err := db_ranked_players(scoreboard.db, offset, limit)
           50         if err != nil {
           51                 panic(err)
           52         }
           53 
           54         newcell := func(text string) *tview.TableCell {
           55                 return tview.NewTableCell(text).
           56                         SetTransparency(true).
           57                         SetSelectable(true).
           58                         SetAlign(tview.AlignRight).
           59                         SetExpansion(1)
           60         }
           61 
           62         t.SetSelectable(true, false).
           63                 SetSelectedStyle(tcell.StyleDefault.Reverse(true))
           64 
           65         for i := 0; i < len(players); i++ {
           66                 p := players[i]
           67                 rankstr := fmt.Sprintf("%4s", humanize.Ordinal(rank + i + 1))
           68                 scorestr := fmt.Sprintf("%5d", p.score)
           69                 flagstr := fmt.Sprintf("%5s", p.FlagStr())
           70                 t.SetCell(i, 0, newcell(rankstr))
           71                 t.SetCell(i, 1, newcell(p.name))
           72                 t.SetCell(i, 2, newcell(flagstr))
           73                 t.SetCell(i, 3, newcell(scorestr))
           74         }
           75 
           76         t.SetSelectedFunc(func (row, col int) {
           77                 var player Player
           78 
           79                 /* ignore placeholder lines */
           80                 if t.GetCell(row, 2).Text == "....." {
           81                         return
           82                 }
           83 
           84                 player.name = t.GetCell(row, 1).Text
           85                 player.db = scoreboard.db
           86                 player.Fetch()
           87                 if scoreboard.player.name == player.name {
           88                         scoreboard.Message(player.name, player.FlagsStr())
           89                 } else {
           90                         scoreboard.Popup(player.name, player.BadgeStr())
           91                 }
           92         })
           93 
           94         if fill == true {
           95                 bsize := int(math.Max(float64(BOARD_HEIGHT), float64(limit)))
           96                 for i:=t.GetRowCount(); i<bsize; i++ {
           97                         rankstr  := fmt.Sprintf("%4s", humanize.Ordinal(rank + i + 1))
           98                         scorestr := fmt.Sprintf("%5d", 0)
           99                         t.SetCell(i, 0, newcell(rankstr).SetTextColor(tcell.ColorGray))
          100                         t.SetCell(i, 1, newcell("AAA").SetTextColor(tcell.ColorGray))
          101                         t.SetCell(i, 2, newcell(".....").SetTextColor(tcell.ColorGray))
          102                         t.SetCell(i, 3, newcell(scorestr).SetTextColor(tcell.ColorGray))
          103                 }
          104         }
          105 
          106         return t
          107 }
          108 
          109 
          110 func (a *Application) SetupFrame() {
          111 
          112         var grid = tview.NewGrid().
          113                 SetColumns(BOARD_WIDTH).
          114                 SetRows(1, BOARD_HEIGHT).
          115                 SetBorders(true)
          116 
          117         grid.AddItem(BoardHeader(), 0, 0, 1, 1, 1, BOARD_WIDTH, false)
          118         grid.AddItem(a.board, 1, 0, 1, 1, BOARD_HEIGHT, BOARD_WIDTH, true)
          119 
          120         a.frame = tview.NewFrame(grid)
          121         a.frame.AddText(fmt.Sprintf("Press H for help"), false, tview.AlignCenter, 0)
          122 }
          123 
          124 func (a *Application) DrawBoard() {
          125         a.board.Clear().
          126                 SetDirection(tview.FlexRow).
          127                 AddItem(RankTable(0, -1, 0, true), BOARD_HEIGHT, 1, true)
          128 
          129         // handle additional keys to terminate application
          130         a.board.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
          131                 if event.Key() == tcell.KeyEscape || event.Rune() == 'q' {
          132                         a.app.Stop()
          133                         return nil
          134                 }
          135                 if event.Rune() == 'h' {
          136                         a.Popup("HELP", HELP_TEXT)
          137                 }
          138                 if event.Rune() == 'l' && a.player.token == "" {
          139                         page := a.Token(func () {
          140                                 a.pages.RemovePage("token")
          141                                 a.HighlightBoard(a.player.Rank() + 1)
          142                         })
          143                         a.pages.AddAndSwitchToPage("token", page, true)
          144                 }
          145                 return event
          146         })
          147 }
          148 
          149 func (a *Application) HighlightBoard(line int) {
          150         a.board.Clear().
          151                 SetDirection(tview.FlexRow).
          152                 AddItem(RankTable(0, -1, 0, true).Select(line - 1, 0), BOARD_HEIGHT, 1, true)
          153         a.app.SetFocus(a.board)
          154 
          155         a.frame.Clear()
          156         a.frame.AddText(fmt.Sprintf("🔑%s", a.player.token), false, tview.AlignCenter, 0)
          157 }
          158 
          159 func (a *Application) NewPlayer(rank int) {
          160         var offset, t1_sz, t2_sz int
          161         if rank > BOARD_HEIGHT {
          162                 offset = rank - BOARD_HEIGHT
          163                 t1_sz = BOARD_HEIGHT - 1
          164                 t2_sz = 0
          165         } else {
          166                 offset = 0
          167                 t1_sz = rank - 1
          168                 t2_sz = BOARD_HEIGHT - rank
          169         }
          170         t1 := RankTable(offset, t1_sz, rank - t1_sz - 1, false).SetSelectable(false, false)
          171         t2 := RankTable(rank - 1, BOARD_HEIGHT, rank, true).SetSelectable(false, false)
          172         box := PlayerBoxGrid(a.player, rank)
          173 
          174         a.board.Clear().
          175                 SetDirection(tview.FlexRow).
          176                 AddItem(t1, t1_sz, 0, false).
          177                 AddItem(box, 1, 0, true).
          178                 AddItem(t2, t2_sz, 0, false)
          179 }
          180 
          181 func popup(title, text string, w, h int, callback func(key tcell.Key)) tview.Primitive {
          182         p := tview.NewTextView().
          183                 SetDynamicColors(true).
          184                 SetTextAlign(tview.AlignCenter).
          185                 SetText(fmt.Sprintf("[::b]%s", text)).
          186                 SetDoneFunc(callback)
          187 
          188         popup := tview.NewFrame(p).
          189                 SetBorders(1, 0, 0, 0, 1, 1).
          190                 AddText("PRESS ENTER", false, tview.AlignRight, tcell.ColorGray)
          191 
          192         popup.SetBorder(true).SetTitle(fmt.Sprintf(" %s ", title))
          193 
          194         return center(w, h, popup)
          195 }
          196 
          197 func (a *Application) Message(title, text string) {
          198         p := popup(title, text, 72, 19, func(key tcell.Key) {
          199                 a.pages.RemovePage("popup")
          200         })
          201 
          202         a.pages.AddAndSwitchToPage("popup", p, true)
          203         a.app.SetFocus(p)
          204 }
          205 
          206 func (a *Application) Popup(title, text string) {
          207         p := popup(title, text, BOARD_WIDTH+2, BOARD_HEIGHT+4, func(key tcell.Key) {
          208                 a.pages.RemovePage("popup")
          209         })
          210 
          211         a.pages.AddAndSwitchToPage("popup", p, true)
          212         a.app.SetFocus(p)
          213 }
          214 
          215 func (a *Application) Fatal(err error) {
          216         p := popup("ERROR", fmt.Sprintf("%s", err), 28, 19, func(key tcell.Key) {
          217                 a.app.Stop()
          218         })
          219 
          220         a.pages.AddAndSwitchToPage("popup", p, true)
          221         a.app.SetFocus(p)
          222 }
          223 
          224 func (a *Application) Token(callback func()) tview.Primitive {
          225         input := tview.NewInputField().
          226                 SetLabel("TOKEN ").
          227                 SetPlaceholder("").
          228                 SetFieldStyle(tcell.StyleDefault.Reverse(true)).
          229                 SetFieldWidth(30)
          230 
          231         input.SetAcceptanceFunc(func(text string, ch rune) bool {
          232                 if len(text) > 24 {
          233                         return false
          234                 }
          235 
          236                 // tokens are base32 strings
          237                 matched, err := regexp.Match(`^[A-Z2-7]+$`, []byte(text))
          238                 if err != nil {
          239                         panic(err)
          240                 }
          241                 return matched
          242         })
          243         input.SetDoneFunc(func(key tcell.Key) {
          244                         if key == tcell.KeyEscape {
          245                                 a.pages.RemovePage("token");
          246                                 return
          247                         }
          248                         if key != tcell.KeyEnter {
          249                                 return
          250                         }
          251 
          252                         token := input.GetText()
          253 
          254                         if len(token) != 24 {
          255                                 a.Popup("ERROR", "Invalid token format")
          256                                 return
          257                         }
          258                         a.player.token = token
          259 
          260                         err := a.player.Fetch()
          261                         if err != nil {
          262                                 a.Fatal(err)
          263                                 return
          264                         }
          265 
          266                         a.pages.RemovePage("token");
          267                         callback()
          268                 })
          269 
          270         return center(40, 1, input)
          271 }