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.AddButton("Save", func() {
149 usernameInput := settingsForm.GetFormItem(0).(*tview.InputField).GetText()
150 if len(usernameInput) == 0 {
151 usernameInput = "unnamed"
153 ui.HLClient.pref.Username = usernameInput
154 iconStr = settingsForm.GetFormItem(1).(*tview.InputField).GetText()
155 ui.HLClient.pref.IconID, _ = strconv.Atoi(iconStr)
156 ui.HLClient.pref.Tracker = settingsForm.GetFormItem(2).(*tview.InputField).GetText()
158 out, err := yaml.Marshal(&ui.HLClient.pref)
163 err = ioutil.WriteFile(ui.HLClient.cfgPath, out, 0666)
165 println(ui.HLClient.cfgPath)
168 ui.Pages.RemovePage("settings")
170 settingsForm.SetBorder(true)
171 settingsForm.SetCancelFunc(func() {
172 ui.Pages.RemovePage("settings")
174 settingsPage := tview.NewFlex().SetDirection(tview.FlexRow)
175 settingsPage.Box.SetBorder(true).SetTitle("Settings")
176 settingsPage.AddItem(settingsForm, 0, 1, true)
178 centerFlex := tview.NewFlex().
179 AddItem(nil, 0, 1, false).
180 AddItem(tview.NewFlex().
181 SetDirection(tview.FlexRow).
182 AddItem(nil, 0, 1, false).
183 AddItem(settingsForm, 15, 1, true).
184 AddItem(nil, 0, 1, false), 40, 1, true).
185 AddItem(nil, 0, 1, false)
190 func (ui *UI) joinServer(addr, login, password string) error {
191 // append default port to address if no port supplied
192 if len(strings.Split(addr, ":")) == 1 {
195 if err := ui.HLClient.JoinServer(addr, login, password); err != nil {
196 return fmt.Errorf("Error joining server: %v\n", err)
200 // Create a new scanner for parsing incoming bytes into transaction tokens
201 scanner := bufio.NewScanner(ui.HLClient.Connection)
202 scanner.Split(transactionScanner)
204 // Scan for new transactions and handle them as they come in.
206 // Make a new []byte slice and copy the scanner bytes to it. This is critical to avoid a data race as the
207 // scanner re-uses the buffer for subsequent scans.
208 buf := make([]byte, len(scanner.Bytes()))
209 copy(buf, scanner.Bytes())
211 t, _, err := ReadTransaction(buf)
215 if err := ui.HLClient.HandleTransaction(t); err != nil {
216 ui.HLClient.Logger.Errorw("Error handling transaction", "err", err)
220 if scanner.Err() == nil {
221 loginErrModal := tview.NewModal().
222 AddButtons([]string{"Ok"}).
223 SetText("The server connection has closed.").
224 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
225 ui.Pages.SwitchToPage("home")
227 loginErrModal.Box.SetTitle("Server Connection Error")
229 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
233 ui.Pages.SwitchToPage("home")
240 func (ui *UI) renderJoinServerForm(name, server, login, password, backPage string, save, defaultConnect bool) *tview.Flex {
241 joinServerForm := tview.NewForm()
243 // AddInputField("Name", server, 0, func(textToCheck string, lastChar rune) bool {
246 AddInputField("Server", server, 0, nil, nil).
247 AddInputField("Login", login, 0, nil, nil).
248 AddPasswordField("Password", password, 0, '*', nil).
249 AddCheckbox("Save", save, func(checked bool) {
250 ui.HLClient.Logger.Infow("saving bookmark")
251 // TODO: Implement bookmark saving
253 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())
254 out, err := yaml.Marshal(ui.HLClient.pref)
259 err = ioutil.WriteFile(ui.HLClient.cfgPath, out, 0666)
263 // pref := ui.HLClient.pref
265 AddButton("Cancel", func() {
266 ui.Pages.SwitchToPage(backPage)
268 AddButton("Connect", func() {
269 srvAddr := joinServerForm.GetFormItem(0).(*tview.InputField).GetText()
270 loginInput := joinServerForm.GetFormItem(1).(*tview.InputField).GetText()
271 err := ui.joinServer(
274 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
277 name = fmt.Sprintf("%s@%s", loginInput, srvAddr)
279 ui.HLClient.serverName = name
282 ui.HLClient.Logger.Errorw("login error", "err", err)
283 loginErrModal := tview.NewModal().
284 AddButtons([]string{"Oh no"}).
285 SetText(err.Error()).
286 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
287 ui.Pages.SwitchToPage(backPage)
290 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
294 if joinServerForm.GetFormItem(3).(*tview.Checkbox).IsChecked() {
295 // TODO: implement bookmark saving
299 joinServerForm.Box.SetBorder(true).SetTitle("| Connect |")
300 joinServerForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
301 if event.Key() == tcell.KeyEscape {
302 ui.Pages.SwitchToPage(backPage)
308 joinServerForm.SetFocus(5)
311 joinServerPage := tview.NewFlex().
312 AddItem(nil, 0, 1, false).
313 AddItem(tview.NewFlex().
314 SetDirection(tview.FlexRow).
315 AddItem(nil, 0, 1, false).
316 AddItem(joinServerForm, 14, 1, true).
317 AddItem(nil, 0, 1, false), 40, 1, true).
318 AddItem(nil, 0, 1, false)
320 return joinServerPage
323 func (ui *UI) renderServerUI() *tview.Flex {
324 ui.chatBox.SetText("") // clear any previously existing chatbox text
325 commandList := tview.NewTextView().SetDynamicColors(true)
327 SetText("[yellow]^n[-::]: Read News [yellow]^p[-::]: Post News\n[yellow]^l[-::]: View Logs [yellow]^f[-::]: View Files\n").
329 SetTitle("| Keyboard Shortcuts| ")
331 modal := tview.NewModal().
332 SetText("Disconnect from the server?").
333 AddButtons([]string{"Cancel", "Exit"}).
335 modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
336 if buttonIndex == 1 {
337 _ = ui.HLClient.Disconnect()
338 ui.Pages.RemovePage(pageServerUI)
339 ui.Pages.SwitchToPage("home")
341 ui.Pages.HidePage("modal")
345 serverUI := tview.NewFlex().
346 AddItem(tview.NewFlex().
347 SetDirection(tview.FlexRow).
348 AddItem(commandList, 4, 0, false).
349 AddItem(ui.chatBox, 0, 8, false).
350 AddItem(ui.chatInput, 3, 0, true), 0, 1, true).
351 AddItem(ui.userList, 25, 1, false)
352 serverUI.SetBorder(true).SetTitle("| Mobius - Connected to " + ui.HLClient.serverName + " |").SetTitleAlign(tview.AlignLeft)
353 serverUI.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
354 if event.Key() == tcell.KeyEscape {
355 ui.Pages.AddPage("modal", modal, false, true)
359 if event.Key() == tcell.KeyCtrlF {
360 if err := ui.HLClient.Send(*NewTransaction(tranGetFileNameList, nil)); err != nil {
361 ui.HLClient.Logger.Errorw("err", "err", err)
366 if event.Key() == tcell.KeyCtrlN {
367 if err := ui.HLClient.Send(*NewTransaction(tranGetMsgs, nil)); err != nil {
368 ui.HLClient.Logger.Errorw("err", "err", err)
373 if event.Key() == tcell.KeyCtrlP {
375 newsFlex := tview.NewFlex()
376 newsFlex.SetBorderPadding(0, 0, 1, 1)
377 newsPostTextArea := tview.NewTextView()
378 newsPostTextArea.SetBackgroundColor(tcell.ColorDarkSlateGrey)
379 newsPostTextArea.SetChangedFunc(func() {
380 ui.App.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
383 newsPostForm := tview.NewForm().
384 SetButtonsAlign(tview.AlignRight).
385 // AddButton("Cancel", nil). // TODO: implement cancel button behavior
386 AddButton("Send", nil)
387 newsPostForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
389 case tcell.KeyEscape:
390 ui.Pages.RemovePage("newsInput")
392 ui.App.SetFocus(newsPostTextArea)
394 newsText := strings.ReplaceAll(newsPostTextArea.GetText(true), "\n", "\r")
395 if len(newsText) == 0 {
398 err := ui.HLClient.Send(
399 *NewTransaction(tranOldPostNews, nil,
400 NewField(fieldData, []byte(newsText)),
404 ui.HLClient.Logger.Errorw("Error posting news", "err", err)
405 // TODO: display errModal to user
407 ui.Pages.RemovePage("newsInput")
414 SetDirection(tview.FlexRow).
416 SetTitle("| Post Message |")
418 newsPostTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
420 case tcell.KeyEscape:
421 ui.Pages.RemovePage("newsInput")
423 ui.App.SetFocus(newsPostForm)
425 _, _ = fmt.Fprintf(newsPostTextArea, "\n")
427 const windowsBackspaceRune = 8
428 const macBackspaceRune = 127
429 switch event.Rune() {
430 case macBackspaceRune, windowsBackspaceRune:
431 curTxt := newsPostTextArea.GetText(true)
433 curTxt = curTxt[:len(curTxt)-1]
434 newsPostTextArea.SetText(curTxt)
437 _, _ = fmt.Fprintf(newsPostTextArea, string(event.Rune()))
444 newsFlex.AddItem(newsPostTextArea, 10, 0, true)
445 newsFlex.AddItem(newsPostForm, 3, 0, false)
447 newsPostPage := tview.NewFlex().
448 AddItem(nil, 0, 1, false).
449 AddItem(tview.NewFlex().
450 SetDirection(tview.FlexRow).
451 AddItem(nil, 0, 1, false).
452 AddItem(newsFlex, 15, 1, true).
453 // AddItem(newsPostForm, 3, 0, false).
454 AddItem(nil, 0, 1, false), 40, 1, false).
455 AddItem(nil, 0, 1, false)
457 ui.Pages.AddPage("newsInput", newsPostPage, true, true)
458 ui.App.SetFocus(newsPostTextArea)
466 func (ui *UI) Start() {
467 home := tview.NewFlex().SetDirection(tview.FlexRow)
468 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
469 mainMenu := tview.NewList()
471 bannerItem := tview.NewTextView().
472 SetText(randomBanner()).
473 SetDynamicColors(true).
474 SetTextAlign(tview.AlignCenter)
477 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
479 home.AddItem(tview.NewFlex().
480 AddItem(nil, 0, 1, false).
481 AddItem(mainMenu, 0, 1, true).
482 AddItem(nil, 0, 1, false),
486 mainMenu.AddItem("Join Server", "", 'j', func() {
487 joinServerPage := ui.renderJoinServerForm("", "", GuestAccount, "", "home", false, false)
488 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
490 AddItem("Bookmarks", "", 'b', func() {
491 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
493 AddItem("Browse Tracker", "", 't', func() {
494 ui.trackerList = ui.getTrackerList()
495 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
497 AddItem("Settings", "", 's', func() {
498 ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, true)
500 AddItem("Quit", "", 'q', func() {
504 ui.Pages.AddPage("home", home, true, true)
506 // App level input capture
507 ui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
508 if event.Key() == tcell.KeyCtrlC {
509 ui.HLClient.Logger.Infow("Exiting")
514 if event.Key() == tcell.KeyCtrlL {
515 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
516 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
517 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
518 if key == tcell.KeyEscape {
519 ui.Pages.RemovePage("logs")
523 ui.Pages.AddPage("logs", ui.HLClient.DebugBuf.TextView, true, true)
528 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {