6 "github.com/gdamore/tcell/v2"
7 "github.com/rivo/tview"
16 chatBox *tview.TextView
17 chatInput *tview.InputField
18 App *tview.Application
20 userList *tview.TextView
21 trackerList *tview.List
27 pageServerUI = "serverUI"
30 func NewUI(c *Client) *UI {
31 app := tview.NewApplication()
32 chatBox := tview.NewTextView().
34 SetDynamicColors(true).
36 SetChangedFunc(func() {
37 app.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
39 chatBox.Box.SetBorder(true).SetTitle("| Chat |")
41 chatInput := tview.NewInputField()
44 SetFieldBackgroundColor(tcell.ColorDimGray).
45 SetDoneFunc(func(key tcell.Key) {
46 // skip send if user hit enter with no other text
47 if len(chatInput.GetText()) == 0 {
52 *NewTransaction(tranChatSend, nil,
53 NewField(fieldData, []byte(chatInput.GetText())),
56 chatInput.SetText("") // clear the input field after chat send
59 chatInput.Box.SetBorder(true).SetTitle("Send")
63 SetDynamicColors(true).
64 SetChangedFunc(func() {
65 app.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
67 userList.Box.SetBorder(true).SetTitle("Users")
72 Pages: tview.NewPages(),
75 trackerList: tview.NewList(),
80 func (ui *UI) showBookmarks() *tview.List {
81 list := tview.NewList()
82 list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
83 if event.Key() == tcell.KeyEsc {
84 ui.Pages.SwitchToPage("home")
88 list.Box.SetBorder(true).SetTitle("| Bookmarks |")
90 shortcut := 97 // rune for "a"
91 for i, srv := range ui.HLClient.pref.Bookmarks {
95 list.AddItem(srv.Name, srv.Addr, rune(shortcut+i), func() {
96 ui.Pages.RemovePage("joinServer")
98 newJS := ui.renderJoinServerForm("", addr, login, pass, "bookmarks", true, true)
100 ui.Pages.AddPage("joinServer", newJS, true, true)
107 func (ui *UI) getTrackerList() *tview.List {
108 listing, err := GetListing(ui.HLClient.pref.Tracker)
113 list := tview.NewList()
114 list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
115 if event.Key() == tcell.KeyEsc {
116 ui.Pages.SwitchToPage("home")
120 list.Box.SetBorder(true).SetTitle("| Servers |")
122 shortcut := 97 // rune for "a"
123 for i, srv := range listing {
126 list.AddItem(string(srv.Name), string(srv.Description), rune(shortcut+i), func() {
127 ui.Pages.RemovePage("joinServer")
129 newJS := ui.renderJoinServerForm(string(srvName), addr, GuestAccount, "", trackerListPage, false, true)
131 ui.Pages.AddPage("joinServer", newJS, true, true)
132 ui.Pages.ShowPage("joinServer")
139 func (ui *UI) renderSettingsForm() *tview.Flex {
140 iconStr := strconv.Itoa(ui.HLClient.pref.IconID)
141 settingsForm := tview.NewForm()
142 settingsForm.AddInputField("Your Name", ui.HLClient.pref.Username, 0, nil, nil)
143 settingsForm.AddInputField("IconID", iconStr, 0, func(idStr string, _ rune) bool {
144 _, err := strconv.Atoi(idStr)
147 settingsForm.AddInputField("Tracker", ui.HLClient.pref.Tracker, 0, nil, nil)
148 settingsForm.AddCheckbox("Enable Terminal Bell", ui.HLClient.pref.EnableBell, nil)
149 settingsForm.AddButton("Save", func() {
150 usernameInput := settingsForm.GetFormItem(0).(*tview.InputField).GetText()
151 if len(usernameInput) == 0 {
152 usernameInput = "unnamed"
154 ui.HLClient.pref.Username = usernameInput
155 iconStr = settingsForm.GetFormItem(1).(*tview.InputField).GetText()
156 ui.HLClient.pref.IconID, _ = strconv.Atoi(iconStr)
157 ui.HLClient.pref.Tracker = settingsForm.GetFormItem(2).(*tview.InputField).GetText()
158 ui.HLClient.pref.EnableBell = settingsForm.GetFormItem(3).(*tview.Checkbox).IsChecked()
160 out, err := yaml.Marshal(&ui.HLClient.pref)
165 err = ioutil.WriteFile(ui.HLClient.cfgPath, out, 0666)
167 println(ui.HLClient.cfgPath)
170 ui.Pages.RemovePage("settings")
172 settingsForm.SetBorder(true)
173 settingsForm.SetCancelFunc(func() {
174 ui.Pages.RemovePage("settings")
176 settingsPage := tview.NewFlex().SetDirection(tview.FlexRow)
177 settingsPage.Box.SetBorder(true).SetTitle("Settings")
178 settingsPage.AddItem(settingsForm, 0, 1, true)
180 centerFlex := tview.NewFlex().
181 AddItem(nil, 0, 1, false).
182 AddItem(tview.NewFlex().
183 SetDirection(tview.FlexRow).
184 AddItem(nil, 0, 1, false).
185 AddItem(settingsForm, 15, 1, true).
186 AddItem(nil, 0, 1, false), 40, 1, true).
187 AddItem(nil, 0, 1, false)
192 func (ui *UI) joinServer(addr, login, password string) error {
193 // append default port to address if no port supplied
194 if len(strings.Split(addr, ":")) == 1 {
197 if err := ui.HLClient.JoinServer(addr, login, password); err != nil {
198 return fmt.Errorf("Error joining server: %v\n", err)
202 // Create a new scanner for parsing incoming bytes into transaction tokens
203 scanner := bufio.NewScanner(ui.HLClient.Connection)
204 scanner.Split(transactionScanner)
206 // Scan for new transactions and handle them as they come in.
208 // Make a new []byte slice and copy the scanner bytes to it. This is critical to avoid a data race as the
209 // scanner re-uses the buffer for subsequent scans.
210 buf := make([]byte, len(scanner.Bytes()))
211 copy(buf, scanner.Bytes())
214 _, err := t.Write(buf)
218 if err := ui.HLClient.HandleTransaction(&t); err != nil {
219 ui.HLClient.Logger.Errorw("Error handling transaction", "err", err)
223 if scanner.Err() == nil {
224 loginErrModal := tview.NewModal().
225 AddButtons([]string{"Ok"}).
226 SetText("The server connection has closed.").
227 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
228 ui.Pages.SwitchToPage("home")
230 loginErrModal.Box.SetTitle("Server Connection Error")
232 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
236 ui.Pages.SwitchToPage("home")
243 func (ui *UI) renderJoinServerForm(name, server, login, password, backPage string, save, defaultConnect bool) *tview.Flex {
244 joinServerForm := tview.NewForm()
246 // AddInputField("Name", server, 0, func(textToCheck string, lastChar rune) bool {
249 AddInputField("Server", server, 0, nil, nil).
250 AddInputField("Login", login, 0, nil, nil).
251 AddPasswordField("Password", password, 0, '*', nil).
252 AddCheckbox("Save", save, func(checked bool) {
253 ui.HLClient.Logger.Infow("saving bookmark")
254 // TODO: Implement bookmark saving
256 ui.HLClient.pref.AddBookmark(joinServerForm.GetFormItem(0).(*tview.InputField).GetText(), joinServerForm.GetFormItem(0).(*tview.InputField).GetText(), joinServerForm.GetFormItem(1).(*tview.InputField).GetText(), joinServerForm.GetFormItem(2).(*tview.InputField).GetText())
257 out, err := yaml.Marshal(ui.HLClient.pref)
262 err = ioutil.WriteFile(ui.HLClient.cfgPath, out, 0666)
266 // pref := ui.HLClient.pref
268 AddButton("Cancel", func() {
269 ui.Pages.SwitchToPage(backPage)
271 AddButton("Connect", func() {
272 srvAddr := joinServerForm.GetFormItem(0).(*tview.InputField).GetText()
273 loginInput := joinServerForm.GetFormItem(1).(*tview.InputField).GetText()
274 err := ui.joinServer(
277 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
280 name = fmt.Sprintf("%s@%s", loginInput, srvAddr)
282 ui.HLClient.serverName = name
285 ui.HLClient.Logger.Errorw("login error", "err", err)
286 loginErrModal := tview.NewModal().
287 AddButtons([]string{"Oh no"}).
288 SetText(err.Error()).
289 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
290 ui.Pages.SwitchToPage(backPage)
293 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
297 if joinServerForm.GetFormItem(3).(*tview.Checkbox).IsChecked() {
298 // TODO: implement bookmark saving
302 joinServerForm.Box.SetBorder(true).SetTitle("| Connect |")
303 joinServerForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
304 if event.Key() == tcell.KeyEscape {
305 ui.Pages.SwitchToPage(backPage)
311 joinServerForm.SetFocus(5)
314 joinServerPage := tview.NewFlex().
315 AddItem(nil, 0, 1, false).
316 AddItem(tview.NewFlex().
317 SetDirection(tview.FlexRow).
318 AddItem(nil, 0, 1, false).
319 AddItem(joinServerForm, 14, 1, true).
320 AddItem(nil, 0, 1, false), 40, 1, true).
321 AddItem(nil, 0, 1, false)
323 return joinServerPage
326 func (ui *UI) renderServerUI() *tview.Flex {
327 ui.chatBox.SetText("") // clear any previously existing chatbox text
328 commandList := tview.NewTextView().SetDynamicColors(true)
330 SetText("[yellow]^n[-::]: Read News [yellow]^p[-::]: Post News\n[yellow]^l[-::]: View Logs [yellow]^f[-::]: View Files\n").
332 SetTitle("| Keyboard Shortcuts| ")
334 modal := tview.NewModal().
335 SetText("Disconnect from the server?").
336 AddButtons([]string{"Cancel", "Exit"}).
338 modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
339 if buttonIndex == 1 {
340 _ = ui.HLClient.Disconnect()
341 ui.Pages.RemovePage(pageServerUI)
342 ui.Pages.SwitchToPage("home")
344 ui.Pages.HidePage("modal")
348 serverUI := tview.NewFlex().
349 AddItem(tview.NewFlex().
350 SetDirection(tview.FlexRow).
351 AddItem(commandList, 4, 0, false).
352 AddItem(ui.chatBox, 0, 8, false).
353 AddItem(ui.chatInput, 3, 0, true), 0, 1, true).
354 AddItem(ui.userList, 25, 1, false)
355 serverUI.SetBorder(true).SetTitle("| Mobius - Connected to " + ui.HLClient.serverName + " |").SetTitleAlign(tview.AlignLeft)
356 serverUI.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
357 if event.Key() == tcell.KeyEscape {
358 ui.Pages.AddPage("modal", modal, false, true)
362 if event.Key() == tcell.KeyCtrlF {
363 if err := ui.HLClient.Send(*NewTransaction(tranGetFileNameList, nil)); err != nil {
364 ui.HLClient.Logger.Errorw("err", "err", err)
369 if event.Key() == tcell.KeyCtrlN {
370 if err := ui.HLClient.Send(*NewTransaction(tranGetMsgs, nil)); err != nil {
371 ui.HLClient.Logger.Errorw("err", "err", err)
376 if event.Key() == tcell.KeyCtrlP {
378 newsFlex := tview.NewFlex()
379 newsFlex.SetBorderPadding(0, 0, 1, 1)
380 newsPostTextArea := tview.NewTextView()
381 newsPostTextArea.SetBackgroundColor(tcell.ColorDarkSlateGrey)
382 newsPostTextArea.SetChangedFunc(func() {
383 ui.App.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
386 newsPostForm := tview.NewForm().
387 SetButtonsAlign(tview.AlignRight).
388 // AddButton("Cancel", nil). // TODO: implement cancel button behavior
389 AddButton("Send", nil)
390 newsPostForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
392 case tcell.KeyEscape:
393 ui.Pages.RemovePage("newsInput")
395 ui.App.SetFocus(newsPostTextArea)
397 newsText := strings.ReplaceAll(newsPostTextArea.GetText(true), "\n", "\r")
398 if len(newsText) == 0 {
401 err := ui.HLClient.Send(
402 *NewTransaction(tranOldPostNews, nil,
403 NewField(fieldData, []byte(newsText)),
407 ui.HLClient.Logger.Errorw("Error posting news", "err", err)
408 // TODO: display errModal to user
410 ui.Pages.RemovePage("newsInput")
417 SetDirection(tview.FlexRow).
419 SetTitle("| Post Message |")
421 newsPostTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
423 case tcell.KeyEscape:
424 ui.Pages.RemovePage("newsInput")
426 ui.App.SetFocus(newsPostForm)
428 _, _ = fmt.Fprintf(newsPostTextArea, "\n")
430 const windowsBackspaceRune = 8
431 const macBackspaceRune = 127
432 switch event.Rune() {
433 case macBackspaceRune, windowsBackspaceRune:
434 curTxt := newsPostTextArea.GetText(true)
436 curTxt = curTxt[:len(curTxt)-1]
437 newsPostTextArea.SetText(curTxt)
440 _, _ = fmt.Fprintf(newsPostTextArea, string(event.Rune()))
447 newsFlex.AddItem(newsPostTextArea, 10, 0, true)
448 newsFlex.AddItem(newsPostForm, 3, 0, false)
450 newsPostPage := tview.NewFlex().
451 AddItem(nil, 0, 1, false).
452 AddItem(tview.NewFlex().
453 SetDirection(tview.FlexRow).
454 AddItem(nil, 0, 1, false).
455 AddItem(newsFlex, 15, 1, true).
456 // AddItem(newsPostForm, 3, 0, false).
457 AddItem(nil, 0, 1, false), 40, 1, false).
458 AddItem(nil, 0, 1, false)
460 ui.Pages.AddPage("newsInput", newsPostPage, true, true)
461 ui.App.SetFocus(newsPostTextArea)
469 func (ui *UI) Start() {
470 home := tview.NewFlex().SetDirection(tview.FlexRow)
471 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
472 mainMenu := tview.NewList()
474 bannerItem := tview.NewTextView().
475 SetText(randomBanner()).
476 SetDynamicColors(true).
477 SetTextAlign(tview.AlignCenter)
480 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
482 home.AddItem(tview.NewFlex().
483 AddItem(nil, 0, 1, false).
484 AddItem(mainMenu, 0, 1, true).
485 AddItem(nil, 0, 1, false),
489 mainMenu.AddItem("Join Server", "", 'j', func() {
490 joinServerPage := ui.renderJoinServerForm("", "", GuestAccount, "", "home", false, false)
491 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
493 AddItem("Bookmarks", "", 'b', func() {
494 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
496 AddItem("Browse Tracker", "", 't', func() {
497 ui.trackerList = ui.getTrackerList()
498 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
500 AddItem("Settings", "", 's', func() {
501 ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, true)
503 AddItem("Quit", "", 'q', func() {
507 ui.Pages.AddPage("home", home, true, true)
509 // App level input capture
510 ui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
511 if event.Key() == tcell.KeyCtrlC {
512 ui.HLClient.Logger.Infow("Exiting")
517 if event.Key() == tcell.KeyCtrlL {
518 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
519 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
520 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
521 if key == tcell.KeyEscape {
522 ui.Pages.RemovePage("logs")
526 ui.Pages.AddPage("logs", ui.HLClient.DebugBuf.TextView, true, true)
531 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {