5 "github.com/gdamore/tcell/v2"
6 "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)
201 err := ui.HLClient.ReadLoop()
203 ui.HLClient.Logger.Errorw("read error", "err", err)
206 loginErrModal := tview.NewModal().
207 AddButtons([]string{"Ok"}).
208 SetText("The server connection has closed.").
209 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
210 ui.Pages.SwitchToPage("home")
212 loginErrModal.Box.SetTitle("Server Connection Error")
214 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
218 ui.Pages.SwitchToPage("home")
228 func (ui *UI) renderJoinServerForm(name, server, login, password, backPage string, save, defaultConnect bool) *tview.Flex {
229 joinServerForm := tview.NewForm()
231 // AddInputField("Name", server, 0, func(textToCheck string, lastChar rune) bool {
234 AddInputField("Server", server, 0, nil, nil).
235 AddInputField("Login", login, 0, nil, nil).
236 AddPasswordField("Password", password, 0, '*', nil).
237 AddCheckbox("Save", save, func(checked bool) {
238 ui.HLClient.Logger.Infow("saving bookmark")
239 // TODO: Implement bookmark saving
241 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())
242 out, err := yaml.Marshal(ui.HLClient.pref)
247 err = ioutil.WriteFile(ui.HLClient.cfgPath, out, 0666)
251 // pref := ui.HLClient.pref
253 AddButton("Cancel", func() {
254 ui.Pages.SwitchToPage(backPage)
256 AddButton("Connect", func() {
257 srvAddr := joinServerForm.GetFormItem(0).(*tview.InputField).GetText()
258 loginInput := joinServerForm.GetFormItem(1).(*tview.InputField).GetText()
259 err := ui.joinServer(
262 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
265 name = fmt.Sprintf("%s@%s", loginInput, srvAddr)
267 ui.HLClient.serverName = name
270 ui.HLClient.Logger.Errorw("login error", "err", err)
271 loginErrModal := tview.NewModal().
272 AddButtons([]string{"Oh no"}).
273 SetText(err.Error()).
274 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
275 ui.Pages.SwitchToPage(backPage)
278 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
282 if joinServerForm.GetFormItem(3).(*tview.Checkbox).IsChecked() {
283 // TODO: implement bookmark saving
287 joinServerForm.Box.SetBorder(true).SetTitle("| Connect |")
288 joinServerForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
289 if event.Key() == tcell.KeyEscape {
290 ui.Pages.SwitchToPage(backPage)
296 joinServerForm.SetFocus(5)
299 joinServerPage := tview.NewFlex().
300 AddItem(nil, 0, 1, false).
301 AddItem(tview.NewFlex().
302 SetDirection(tview.FlexRow).
303 AddItem(nil, 0, 1, false).
304 AddItem(joinServerForm, 14, 1, true).
305 AddItem(nil, 0, 1, false), 40, 1, true).
306 AddItem(nil, 0, 1, false)
308 return joinServerPage
311 func (ui *UI) renderServerUI() *tview.Flex {
312 ui.chatBox.SetText("") // clear any previously existing chatbox text
313 commandList := tview.NewTextView().SetDynamicColors(true)
315 SetText("[yellow]^n[-::]: Read News [yellow]^p[-::]: Post News\n[yellow]^l[-::]: View Logs [yellow]^f[-::]: View Files\n").
317 SetTitle("| Keyboard Shortcuts| ")
319 modal := tview.NewModal().
320 SetText("Disconnect from the server?").
321 AddButtons([]string{"Cancel", "Exit"}).
323 modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
324 if buttonIndex == 1 {
325 _ = ui.HLClient.Disconnect()
326 ui.Pages.RemovePage(pageServerUI)
328 ui.Pages.HidePage("modal")
332 serverUI := tview.NewFlex().
333 AddItem(tview.NewFlex().
334 SetDirection(tview.FlexRow).
335 AddItem(commandList, 4, 0, false).
336 AddItem(ui.chatBox, 0, 8, false).
337 AddItem(ui.chatInput, 3, 0, true), 0, 1, true).
338 AddItem(ui.userList, 25, 1, false)
339 serverUI.SetBorder(true).SetTitle("| Mobius - Connected to " + ui.HLClient.serverName + " |").SetTitleAlign(tview.AlignLeft)
340 serverUI.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
341 if event.Key() == tcell.KeyEscape {
342 ui.Pages.AddPage("modal", modal, false, true)
346 if event.Key() == tcell.KeyCtrlF {
347 if err := ui.HLClient.Send(*NewTransaction(tranGetFileNameList, nil)); err != nil {
348 ui.HLClient.Logger.Errorw("err", "err", err)
353 if event.Key() == tcell.KeyCtrlN {
354 if err := ui.HLClient.Send(*NewTransaction(tranGetMsgs, nil)); err != nil {
355 ui.HLClient.Logger.Errorw("err", "err", err)
360 if event.Key() == tcell.KeyCtrlP {
362 newsFlex := tview.NewFlex()
363 newsFlex.SetBorderPadding(0, 0, 1, 1)
364 newsPostTextArea := tview.NewTextView()
365 newsPostTextArea.SetBackgroundColor(tcell.ColorDarkSlateGrey)
366 newsPostTextArea.SetChangedFunc(func() {
367 ui.App.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
370 newsPostForm := tview.NewForm().
371 SetButtonsAlign(tview.AlignRight).
372 // AddButton("Cancel", nil). // TODO: implement cancel button behavior
373 AddButton("Send", nil)
374 newsPostForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
376 case tcell.KeyEscape:
377 ui.Pages.RemovePage("newsInput")
379 ui.App.SetFocus(newsPostTextArea)
381 newsText := strings.ReplaceAll(newsPostTextArea.GetText(true), "\n", "\r")
382 if len(newsText) == 0 {
385 err := ui.HLClient.Send(
386 *NewTransaction(tranOldPostNews, nil,
387 NewField(fieldData, []byte(newsText)),
391 ui.HLClient.Logger.Errorw("Error posting news", "err", err)
392 // TODO: display errModal to user
394 ui.Pages.RemovePage("newsInput")
401 SetDirection(tview.FlexRow).
403 SetTitle("| Post Message |")
405 newsPostTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
407 case tcell.KeyEscape:
408 ui.Pages.RemovePage("newsInput")
410 ui.App.SetFocus(newsPostForm)
412 _, _ = fmt.Fprintf(newsPostTextArea, "\n")
414 const windowsBackspaceRune = 8
415 const macBackspaceRune = 127
416 switch event.Rune() {
417 case macBackspaceRune, windowsBackspaceRune:
418 curTxt := newsPostTextArea.GetText(true)
420 curTxt = curTxt[:len(curTxt)-1]
421 newsPostTextArea.SetText(curTxt)
424 _, _ = fmt.Fprintf(newsPostTextArea, string(event.Rune()))
431 newsFlex.AddItem(newsPostTextArea, 10, 0, true)
432 newsFlex.AddItem(newsPostForm, 3, 0, false)
434 newsPostPage := tview.NewFlex().
435 AddItem(nil, 0, 1, false).
436 AddItem(tview.NewFlex().
437 SetDirection(tview.FlexRow).
438 AddItem(nil, 0, 1, false).
439 AddItem(newsFlex, 15, 1, true).
440 // AddItem(newsPostForm, 3, 0, false).
441 AddItem(nil, 0, 1, false), 40, 1, false).
442 AddItem(nil, 0, 1, false)
444 ui.Pages.AddPage("newsInput", newsPostPage, true, true)
445 ui.App.SetFocus(newsPostTextArea)
453 func (ui *UI) Start() {
454 home := tview.NewFlex().SetDirection(tview.FlexRow)
455 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
456 mainMenu := tview.NewList()
458 bannerItem := tview.NewTextView().
459 SetText(randomBanner()).
460 SetDynamicColors(true).
461 SetTextAlign(tview.AlignCenter)
464 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
466 home.AddItem(tview.NewFlex().
467 AddItem(nil, 0, 1, false).
468 AddItem(mainMenu, 0, 1, true).
469 AddItem(nil, 0, 1, false),
473 mainMenu.AddItem("Join Server", "", 'j', func() {
474 joinServerPage := ui.renderJoinServerForm("", "", GuestAccount, "", "home", false, false)
475 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
477 AddItem("Bookmarks", "", 'b', func() {
478 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
480 AddItem("Browse Tracker", "", 't', func() {
481 ui.trackerList = ui.getTrackerList()
482 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
484 AddItem("Settings", "", 's', func() {
485 ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, true)
487 AddItem("Quit", "", 'q', func() {
491 ui.Pages.AddPage("home", home, true, true)
493 // App level input capture
494 ui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
495 if event.Key() == tcell.KeyCtrlC {
496 ui.HLClient.Logger.Infow("Exiting")
501 if event.Key() == tcell.KeyCtrlL {
502 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
503 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
504 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
505 if key == tcell.KeyEscape {
506 ui.Pages.RemovePage("logs")
510 ui.Pages.AddPage("logs", ui.HLClient.DebugBuf.TextView, true, true)
515 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {