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 }