5 "github.com/davecgh/go-spew/spew"
6 "github.com/gdamore/tcell/v2"
7 "github.com/rivo/tview"
17 chatBox *tview.TextView
18 chatInput *tview.InputField
19 App *tview.Application
21 userList *tview.TextView
22 agreeModal *tview.Modal
23 trackerList *tview.List
29 pageServerUI = "serverUI"
32 func NewUI(c *Client) *UI {
33 app := tview.NewApplication()
34 chatBox := tview.NewTextView().
36 SetDynamicColors(true).
38 SetChangedFunc(func() {
39 app.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
41 chatBox.Box.SetBorder(true).SetTitle("| Chat |")
43 chatInput := tview.NewInputField()
46 SetFieldBackgroundColor(tcell.ColorDimGray).
47 SetDoneFunc(func(key tcell.Key) {
48 // skip send if user hit enter with no other text
49 if len(chatInput.GetText()) == 0 {
54 *NewTransaction(tranChatSend, nil,
55 NewField(fieldData, []byte(chatInput.GetText())),
58 chatInput.SetText("") // clear the input field after chat send
61 chatInput.Box.SetBorder(true).SetTitle("Send")
65 SetDynamicColors(true).
66 SetChangedFunc(func() {
67 app.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
69 userList.Box.SetBorder(true).SetTitle("Users")
74 Pages: tview.NewPages(),
77 trackerList: tview.NewList(),
78 agreeModal: tview.NewModal(),
83 func (ui *UI) showBookmarks() *tview.List {
84 list := tview.NewList()
85 list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
86 if event.Key() == tcell.KeyEsc {
87 ui.Pages.SwitchToPage("home")
91 list.Box.SetBorder(true).SetTitle("| Bookmarks |")
93 shortcut := 97 // rune for "a"
94 for i, srv := range ui.HLClient.pref.Bookmarks {
98 list.AddItem(srv.Name, srv.Addr, rune(shortcut+i), func() {
99 ui.Pages.RemovePage("joinServer")
101 newJS := ui.renderJoinServerForm("", addr, login, pass, "bookmarks", true, true)
103 ui.Pages.AddPage("joinServer", newJS, true, true)
110 func (ui *UI) getTrackerList() *tview.List {
111 listing, err := GetListing(ui.HLClient.pref.Tracker)
116 list := tview.NewList()
117 list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
118 if event.Key() == tcell.KeyEsc {
119 ui.Pages.SwitchToPage("home")
123 list.Box.SetBorder(true).SetTitle("| Servers |")
125 shortcut := 97 // rune for "a"
126 for i, srv := range listing {
129 list.AddItem(string(srv.Name), string(srv.Description), rune(shortcut+i), func() {
130 ui.Pages.RemovePage("joinServer")
132 newJS := ui.renderJoinServerForm(string(srvName), addr, GuestAccount, "", trackerListPage, false, true)
134 ui.Pages.AddPage("joinServer", newJS, true, true)
135 ui.Pages.ShowPage("joinServer")
142 func (ui *UI) renderSettingsForm() *tview.Flex {
143 iconStr := strconv.Itoa(ui.HLClient.pref.IconID)
144 settingsForm := tview.NewForm()
145 settingsForm.AddInputField("Your Name", ui.HLClient.pref.Username, 0, nil, nil)
146 settingsForm.AddInputField("IconID", iconStr, 0, func(idStr string, _ rune) bool {
147 _, err := strconv.Atoi(idStr)
150 settingsForm.AddInputField("Tracker", ui.HLClient.pref.Tracker, 0, nil, nil)
151 settingsForm.AddButton("Save", func() {
152 usernameInput := settingsForm.GetFormItem(0).(*tview.InputField).GetText()
153 if len(usernameInput) == 0 {
154 usernameInput = "unnamed"
156 ui.HLClient.pref.Username = usernameInput
157 iconStr = settingsForm.GetFormItem(1).(*tview.InputField).GetText()
158 ui.HLClient.pref.IconID, _ = strconv.Atoi(iconStr)
159 ui.HLClient.pref.Tracker = settingsForm.GetFormItem(2).(*tview.InputField).GetText()
161 out, err := yaml.Marshal(&ui.HLClient.pref)
166 err = ioutil.WriteFile(ui.HLClient.cfgPath, out, 0666)
168 println(ui.HLClient.cfgPath)
171 ui.Pages.RemovePage("settings")
173 settingsForm.SetBorder(true)
174 settingsForm.SetCancelFunc(func() {
175 ui.Pages.RemovePage("settings")
177 settingsPage := tview.NewFlex().SetDirection(tview.FlexRow)
178 settingsPage.Box.SetBorder(true).SetTitle("Settings")
179 settingsPage.AddItem(settingsForm, 0, 1, true)
181 centerFlex := tview.NewFlex().
182 AddItem(nil, 0, 1, false).
183 AddItem(tview.NewFlex().
184 SetDirection(tview.FlexRow).
185 AddItem(nil, 0, 1, false).
186 AddItem(settingsForm, 15, 1, true).
187 AddItem(nil, 0, 1, false), 40, 1, true).
188 AddItem(nil, 0, 1, false)
193 func (ui *UI) joinServer(addr, login, password string) error {
194 // append default port to address if no port supplied
195 if len(strings.Split(addr, ":")) == 1 {
198 if err := ui.HLClient.JoinServer(addr, login, password); err != nil {
199 return fmt.Errorf("Error joining server: %v\n", err)
204 err := ui.HLClient.ReadLoop()
206 ui.HLClient.Logger.Errorw("read error", "err", err)
209 loginErrModal := tview.NewModal().
210 AddButtons([]string{"Ok"}).
211 SetText("The server connection has closed.").
212 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
213 ui.Pages.SwitchToPage("home")
215 loginErrModal.Box.SetTitle("Server Connection Error")
217 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
221 ui.Pages.SwitchToPage("home")
231 func (ui *UI) renderJoinServerForm(name, server, login, password, backPage string, save, defaultConnect bool) *tview.Flex {
232 joinServerForm := tview.NewForm()
234 // AddInputField("Name", server, 0, func(textToCheck string, lastChar rune) bool {
237 AddInputField("Server", server, 0, nil, nil).
238 AddInputField("Login", login, 0, nil, nil).
239 AddPasswordField("Password", password, 0, '*', nil).
240 AddCheckbox("Save", save, func(checked bool) {
241 ui.HLClient.Logger.Infow("saving bookmark")
242 // TODO: Implement bookmark saving
244 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())
245 out, err := yaml.Marshal(ui.HLClient.pref)
250 err = ioutil.WriteFile(ui.HLClient.cfgPath, out, 0666)
254 // pref := ui.HLClient.pref
256 AddButton("Cancel", func() {
257 ui.Pages.SwitchToPage(backPage)
259 AddButton("Connect", func() {
260 srvAddr := joinServerForm.GetFormItem(0).(*tview.InputField).GetText()
261 loginInput := joinServerForm.GetFormItem(1).(*tview.InputField).GetText()
262 err := ui.joinServer(
265 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
268 name = fmt.Sprintf("%s@%s", loginInput, srvAddr)
270 ui.HLClient.serverName = name
273 ui.HLClient.Logger.Errorw("login error", "err", err)
274 loginErrModal := tview.NewModal().
275 AddButtons([]string{"Oh no"}).
276 SetText(err.Error()).
277 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
278 ui.Pages.SwitchToPage(backPage)
281 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
285 if joinServerForm.GetFormItem(3).(*tview.Checkbox).IsChecked() {
286 // TODO: implement bookmark saving
290 joinServerForm.Box.SetBorder(true).SetTitle("| Connect |")
291 joinServerForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
292 if event.Key() == tcell.KeyEscape {
293 ui.Pages.SwitchToPage(backPage)
299 joinServerForm.SetFocus(5)
302 joinServerPage := tview.NewFlex().
303 AddItem(nil, 0, 1, false).
304 AddItem(tview.NewFlex().
305 SetDirection(tview.FlexRow).
306 AddItem(nil, 0, 1, false).
307 AddItem(joinServerForm, 14, 1, true).
308 AddItem(nil, 0, 1, false), 40, 1, true).
309 AddItem(nil, 0, 1, false)
311 return joinServerPage
314 func (ui *UI) renderServerUI() *tview.Flex {
315 ui.chatBox.SetText("") // clear any previously existing chatbox text
316 commandList := tview.NewTextView().SetDynamicColors(true)
318 SetText("[yellow]^n[-::]: Read News [yellow]^p[-::]: Post News\n[yellow]^l[-::]: View Logs [yellow]^f[-::]: View Files\n").
320 SetTitle("| Keyboard Shortcuts| ")
322 modal := tview.NewModal().
323 SetText("Disconnect from the server?").
324 AddButtons([]string{"Cancel", "Exit"}).
326 modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
327 if buttonIndex == 1 {
328 _ = ui.HLClient.Disconnect()
329 ui.Pages.RemovePage(pageServerUI)
331 ui.Pages.HidePage("modal")
335 serverUI := tview.NewFlex().
336 AddItem(tview.NewFlex().
337 SetDirection(tview.FlexRow).
338 AddItem(commandList, 4, 0, false).
339 AddItem(ui.chatBox, 0, 8, false).
340 AddItem(ui.chatInput, 3, 0, true), 0, 1, true).
341 AddItem(ui.userList, 25, 1, false)
342 serverUI.SetBorder(true).SetTitle("| Mobius - Connected to " + ui.HLClient.serverName + " |").SetTitleAlign(tview.AlignLeft)
343 serverUI.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
344 if event.Key() == tcell.KeyEscape {
345 ui.Pages.AddPage("modal", modal, false, true)
349 if event.Key() == tcell.KeyCtrlF {
350 if err := ui.HLClient.Send(*NewTransaction(tranGetFileNameList, nil)); err != nil {
351 ui.HLClient.Logger.Errorw("err", "err", err)
356 if event.Key() == tcell.KeyCtrlN {
357 if err := ui.HLClient.Send(*NewTransaction(tranGetMsgs, nil)); err != nil {
358 ui.HLClient.Logger.Errorw("err", "err", err)
363 if event.Key() == tcell.KeyCtrlP {
365 newsFlex := tview.NewFlex()
366 newsFlex.SetBorderPadding(0, 0, 1, 1)
367 newsPostTextArea := tview.NewTextView()
368 newsPostTextArea.SetBackgroundColor(tcell.ColorDarkSlateGrey)
369 newsPostTextArea.SetChangedFunc(func() {
370 ui.App.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
373 newsPostForm := tview.NewForm().
374 SetButtonsAlign(tview.AlignRight).
375 //AddButton("Cancel", nil). // TODO: implement cancel button behavior
376 AddButton("Send", nil)
377 newsPostForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
379 case tcell.KeyEscape:
380 ui.Pages.RemovePage("newsInput")
382 ui.App.SetFocus(newsPostTextArea)
384 newsText := strings.ReplaceAll(newsPostTextArea.GetText(true), "\n", "\r")
385 if len(newsText) == 0 {
388 err := ui.HLClient.Send(
389 *NewTransaction(tranOldPostNews, nil,
390 NewField(fieldData, []byte(newsText)),
394 ui.HLClient.Logger.Errorw("Error posting news", "err", err)
395 // TODO: display errModal to user
397 ui.Pages.RemovePage("newsInput")
404 SetDirection(tview.FlexRow).
406 SetTitle("| Post Message |")
408 newsPostTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
410 case tcell.KeyEscape:
411 ui.Pages.RemovePage("newsInput")
413 ui.App.SetFocus(newsPostForm)
415 _, _ = fmt.Fprintf(newsPostTextArea, "\n")
417 const windowsBackspaceRune = 8
418 const macBackspaceRune = 127
419 switch event.Rune() {
420 case macBackspaceRune, windowsBackspaceRune:
421 curTxt := newsPostTextArea.GetText(true)
423 curTxt = curTxt[:len(curTxt)-1]
424 newsPostTextArea.SetText(curTxt)
427 _, _ = fmt.Fprintf(newsPostTextArea, string(event.Rune()))
434 newsFlex.AddItem(newsPostTextArea, 10, 0, true)
435 newsFlex.AddItem(newsPostForm, 3, 0, false)
437 newsPostPage := tview.NewFlex().
438 AddItem(nil, 0, 1, false).
439 AddItem(tview.NewFlex().
440 SetDirection(tview.FlexRow).
441 AddItem(nil, 0, 1, false).
442 AddItem(newsFlex, 15, 1, true).
443 //AddItem(newsPostForm, 3, 0, false).
444 AddItem(nil, 0, 1, false), 40, 1, false).
445 AddItem(nil, 0, 1, false)
447 ui.Pages.AddPage("newsInput", newsPostPage, true, true)
448 ui.App.SetFocus(newsPostTextArea)
456 func (ui *UI) Start() {
457 home := tview.NewFlex().SetDirection(tview.FlexRow)
458 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
459 mainMenu := tview.NewList()
461 bannerItem := tview.NewTextView().
462 SetText(randomBanner()).
463 SetDynamicColors(true).
464 SetTextAlign(tview.AlignCenter)
467 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
469 home.AddItem(tview.NewFlex().
470 AddItem(nil, 0, 1, false).
471 AddItem(mainMenu, 0, 1, true).
472 AddItem(nil, 0, 1, false),
476 mainMenu.AddItem("Join Server", "", 'j', func() {
477 joinServerPage := ui.renderJoinServerForm("", "", GuestAccount, "", "home", false, false)
478 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
480 AddItem("Bookmarks", "", 'b', func() {
481 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
483 AddItem("Browse Tracker", "", 't', func() {
484 ui.trackerList = ui.getTrackerList()
485 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
487 AddItem("Settings", "", 's', func() {
488 ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, true)
490 AddItem("Quit", "", 'q', func() {
494 ui.Pages.AddPage("home", home, true, true)
496 // App level input capture
497 ui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
498 if event.Key() == tcell.KeyCtrlC {
499 ui.HLClient.Logger.Infow("Exiting")
504 if event.Key() == tcell.KeyCtrlL {
505 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
506 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
507 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
508 if key == tcell.KeyEscape {
509 ui.Pages.RemovePage("logs")
513 ui.Pages.AddPage("logs", ui.HLClient.DebugBuf.TextView, true, true)
518 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {