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 trackerList *tview.List
28 pageServerUI = "serverUI"
31 func NewUI(c *Client) *UI {
32 app := tview.NewApplication()
33 chatBox := tview.NewTextView().
35 SetDynamicColors(true).
37 SetChangedFunc(func() {
38 app.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
40 chatBox.Box.SetBorder(true).SetTitle("| Chat |")
42 chatInput := tview.NewInputField()
45 SetFieldBackgroundColor(tcell.ColorDimGray).
46 SetDoneFunc(func(key tcell.Key) {
47 // skip send if user hit enter with no other text
48 if len(chatInput.GetText()) == 0 {
53 *NewTransaction(tranChatSend, nil,
54 NewField(fieldData, []byte(chatInput.GetText())),
57 chatInput.SetText("") // clear the input field after chat send
60 chatInput.Box.SetBorder(true).SetTitle("Send")
64 SetDynamicColors(true).
65 SetChangedFunc(func() {
66 app.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
68 userList.Box.SetBorder(true).SetTitle("Users")
73 Pages: tview.NewPages(),
76 trackerList: tview.NewList(),
81 func (ui *UI) showBookmarks() *tview.List {
82 list := tview.NewList()
83 list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
84 if event.Key() == tcell.KeyEsc {
85 ui.Pages.SwitchToPage("home")
89 list.Box.SetBorder(true).SetTitle("| Bookmarks |")
91 shortcut := 97 // rune for "a"
92 for i, srv := range ui.HLClient.pref.Bookmarks {
96 list.AddItem(srv.Name, srv.Addr, rune(shortcut+i), func() {
97 ui.Pages.RemovePage("joinServer")
99 newJS := ui.renderJoinServerForm("", addr, login, pass, "bookmarks", true, true)
101 ui.Pages.AddPage("joinServer", newJS, true, true)
108 func (ui *UI) getTrackerList() *tview.List {
109 listing, err := GetListing(ui.HLClient.pref.Tracker)
114 list := tview.NewList()
115 list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
116 if event.Key() == tcell.KeyEsc {
117 ui.Pages.SwitchToPage("home")
121 list.Box.SetBorder(true).SetTitle("| Servers |")
123 shortcut := 97 // rune for "a"
124 for i, srv := range listing {
127 list.AddItem(string(srv.Name), string(srv.Description), rune(shortcut+i), func() {
128 ui.Pages.RemovePage("joinServer")
130 newJS := ui.renderJoinServerForm(string(srvName), addr, GuestAccount, "", trackerListPage, false, true)
132 ui.Pages.AddPage("joinServer", newJS, true, true)
133 ui.Pages.ShowPage("joinServer")
140 func (ui *UI) renderSettingsForm() *tview.Flex {
141 iconStr := strconv.Itoa(ui.HLClient.pref.IconID)
142 settingsForm := tview.NewForm()
143 settingsForm.AddInputField("Your Name", ui.HLClient.pref.Username, 0, nil, nil)
144 settingsForm.AddInputField("IconID", iconStr, 0, func(idStr string, _ rune) bool {
145 _, err := strconv.Atoi(idStr)
148 settingsForm.AddInputField("Tracker", ui.HLClient.pref.Tracker, 0, nil, 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()
159 out, err := yaml.Marshal(&ui.HLClient.pref)
164 err = ioutil.WriteFile(ui.HLClient.cfgPath, out, 0666)
166 println(ui.HLClient.cfgPath)
169 ui.Pages.RemovePage("settings")
171 settingsForm.SetBorder(true)
172 settingsForm.SetCancelFunc(func() {
173 ui.Pages.RemovePage("settings")
175 settingsPage := tview.NewFlex().SetDirection(tview.FlexRow)
176 settingsPage.Box.SetBorder(true).SetTitle("Settings")
177 settingsPage.AddItem(settingsForm, 0, 1, true)
179 centerFlex := tview.NewFlex().
180 AddItem(nil, 0, 1, false).
181 AddItem(tview.NewFlex().
182 SetDirection(tview.FlexRow).
183 AddItem(nil, 0, 1, false).
184 AddItem(settingsForm, 15, 1, true).
185 AddItem(nil, 0, 1, false), 40, 1, true).
186 AddItem(nil, 0, 1, false)
191 func (ui *UI) joinServer(addr, login, password string) error {
192 // append default port to address if no port supplied
193 if len(strings.Split(addr, ":")) == 1 {
196 if err := ui.HLClient.JoinServer(addr, login, password); err != nil {
197 return fmt.Errorf("Error joining server: %v\n", err)
202 err := ui.HLClient.ReadLoop()
204 ui.HLClient.Logger.Errorw("read error", "err", err)
207 loginErrModal := tview.NewModal().
208 AddButtons([]string{"Ok"}).
209 SetText("The server connection has closed.").
210 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
211 ui.Pages.SwitchToPage("home")
213 loginErrModal.Box.SetTitle("Server Connection Error")
215 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
219 ui.Pages.SwitchToPage("home")
229 func (ui *UI) renderJoinServerForm(name, server, login, password, backPage string, save, defaultConnect bool) *tview.Flex {
230 joinServerForm := tview.NewForm()
232 // AddInputField("Name", server, 0, func(textToCheck string, lastChar rune) bool {
235 AddInputField("Server", server, 0, nil, nil).
236 AddInputField("Login", login, 0, nil, nil).
237 AddPasswordField("Password", password, 0, '*', nil).
238 AddCheckbox("Save", save, func(checked bool) {
239 ui.HLClient.Logger.Infow("saving bookmark")
240 // TODO: Implement bookmark saving
242 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())
243 out, err := yaml.Marshal(ui.HLClient.pref)
248 err = ioutil.WriteFile(ui.HLClient.cfgPath, out, 0666)
252 // pref := ui.HLClient.pref
254 AddButton("Cancel", func() {
255 ui.Pages.SwitchToPage(backPage)
257 AddButton("Connect", func() {
258 srvAddr := joinServerForm.GetFormItem(0).(*tview.InputField).GetText()
259 loginInput := joinServerForm.GetFormItem(1).(*tview.InputField).GetText()
260 err := ui.joinServer(
263 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
266 name = fmt.Sprintf("%s@%s", loginInput, srvAddr)
268 ui.HLClient.serverName = name
271 ui.HLClient.Logger.Errorw("login error", "err", err)
272 loginErrModal := tview.NewModal().
273 AddButtons([]string{"Oh no"}).
274 SetText(err.Error()).
275 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
276 ui.Pages.SwitchToPage(backPage)
279 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
283 if joinServerForm.GetFormItem(3).(*tview.Checkbox).IsChecked() {
284 // TODO: implement bookmark saving
288 joinServerForm.Box.SetBorder(true).SetTitle("| Connect |")
289 joinServerForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
290 if event.Key() == tcell.KeyEscape {
291 ui.Pages.SwitchToPage(backPage)
297 joinServerForm.SetFocus(5)
300 joinServerPage := tview.NewFlex().
301 AddItem(nil, 0, 1, false).
302 AddItem(tview.NewFlex().
303 SetDirection(tview.FlexRow).
304 AddItem(nil, 0, 1, false).
305 AddItem(joinServerForm, 14, 1, true).
306 AddItem(nil, 0, 1, false), 40, 1, true).
307 AddItem(nil, 0, 1, false)
309 return joinServerPage
312 func (ui *UI) renderServerUI() *tview.Flex {
313 ui.chatBox.SetText("") // clear any previously existing chatbox text
314 commandList := tview.NewTextView().SetDynamicColors(true)
316 SetText("[yellow]^n[-::]: Read News [yellow]^p[-::]: Post News\n[yellow]^l[-::]: View Logs [yellow]^f[-::]: View Files\n").
318 SetTitle("| Keyboard Shortcuts| ")
320 modal := tview.NewModal().
321 SetText("Disconnect from the server?").
322 AddButtons([]string{"Cancel", "Exit"}).
324 modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
325 if buttonIndex == 1 {
326 _ = ui.HLClient.Disconnect()
327 ui.Pages.RemovePage(pageServerUI)
329 ui.Pages.HidePage("modal")
333 serverUI := tview.NewFlex().
334 AddItem(tview.NewFlex().
335 SetDirection(tview.FlexRow).
336 AddItem(commandList, 4, 0, false).
337 AddItem(ui.chatBox, 0, 8, false).
338 AddItem(ui.chatInput, 3, 0, true), 0, 1, true).
339 AddItem(ui.userList, 25, 1, false)
340 serverUI.SetBorder(true).SetTitle("| Mobius - Connected to " + ui.HLClient.serverName + " |").SetTitleAlign(tview.AlignLeft)
341 serverUI.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
342 if event.Key() == tcell.KeyEscape {
343 ui.Pages.AddPage("modal", modal, false, true)
347 if event.Key() == tcell.KeyCtrlF {
348 if err := ui.HLClient.Send(*NewTransaction(tranGetFileNameList, nil)); err != nil {
349 ui.HLClient.Logger.Errorw("err", "err", err)
354 if event.Key() == tcell.KeyCtrlN {
355 if err := ui.HLClient.Send(*NewTransaction(tranGetMsgs, nil)); err != nil {
356 ui.HLClient.Logger.Errorw("err", "err", err)
361 if event.Key() == tcell.KeyCtrlP {
363 newsFlex := tview.NewFlex()
364 newsFlex.SetBorderPadding(0, 0, 1, 1)
365 newsPostTextArea := tview.NewTextView()
366 newsPostTextArea.SetBackgroundColor(tcell.ColorDarkSlateGrey)
367 newsPostTextArea.SetChangedFunc(func() {
368 ui.App.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
371 newsPostForm := tview.NewForm().
372 SetButtonsAlign(tview.AlignRight).
373 //AddButton("Cancel", nil). // TODO: implement cancel button behavior
374 AddButton("Send", nil)
375 newsPostForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
377 case tcell.KeyEscape:
378 ui.Pages.RemovePage("newsInput")
380 ui.App.SetFocus(newsPostTextArea)
382 newsText := strings.ReplaceAll(newsPostTextArea.GetText(true), "\n", "\r")
383 if len(newsText) == 0 {
386 err := ui.HLClient.Send(
387 *NewTransaction(tranOldPostNews, nil,
388 NewField(fieldData, []byte(newsText)),
392 ui.HLClient.Logger.Errorw("Error posting news", "err", err)
393 // TODO: display errModal to user
395 ui.Pages.RemovePage("newsInput")
402 SetDirection(tview.FlexRow).
404 SetTitle("| Post Message |")
406 newsPostTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
408 case tcell.KeyEscape:
409 ui.Pages.RemovePage("newsInput")
411 ui.App.SetFocus(newsPostForm)
413 _, _ = fmt.Fprintf(newsPostTextArea, "\n")
415 const windowsBackspaceRune = 8
416 const macBackspaceRune = 127
417 switch event.Rune() {
418 case macBackspaceRune, windowsBackspaceRune:
419 curTxt := newsPostTextArea.GetText(true)
421 curTxt = curTxt[:len(curTxt)-1]
422 newsPostTextArea.SetText(curTxt)
425 _, _ = fmt.Fprintf(newsPostTextArea, string(event.Rune()))
432 newsFlex.AddItem(newsPostTextArea, 10, 0, true)
433 newsFlex.AddItem(newsPostForm, 3, 0, false)
435 newsPostPage := tview.NewFlex().
436 AddItem(nil, 0, 1, false).
437 AddItem(tview.NewFlex().
438 SetDirection(tview.FlexRow).
439 AddItem(nil, 0, 1, false).
440 AddItem(newsFlex, 15, 1, true).
441 //AddItem(newsPostForm, 3, 0, false).
442 AddItem(nil, 0, 1, false), 40, 1, false).
443 AddItem(nil, 0, 1, false)
445 ui.Pages.AddPage("newsInput", newsPostPage, true, true)
446 ui.App.SetFocus(newsPostTextArea)
454 func (ui *UI) Start() {
455 home := tview.NewFlex().SetDirection(tview.FlexRow)
456 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
457 mainMenu := tview.NewList()
459 bannerItem := tview.NewTextView().
460 SetText(randomBanner()).
461 SetDynamicColors(true).
462 SetTextAlign(tview.AlignCenter)
465 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
467 home.AddItem(tview.NewFlex().
468 AddItem(nil, 0, 1, false).
469 AddItem(mainMenu, 0, 1, true).
470 AddItem(nil, 0, 1, false),
474 mainMenu.AddItem("Join Server", "", 'j', func() {
475 joinServerPage := ui.renderJoinServerForm("", "", GuestAccount, "", "home", false, false)
476 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
478 AddItem("Bookmarks", "", 'b', func() {
479 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
481 AddItem("Browse Tracker", "", 't', func() {
482 ui.trackerList = ui.getTrackerList()
483 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
485 AddItem("Settings", "", 's', func() {
486 ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, true)
488 AddItem("Quit", "", 'q', func() {
492 ui.Pages.AddPage("home", home, true, true)
494 // App level input capture
495 ui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
496 if event.Key() == tcell.KeyCtrlC {
497 ui.HLClient.Logger.Infow("Exiting")
502 if event.Key() == tcell.KeyCtrlL {
503 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
504 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
505 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
506 if key == tcell.KeyEscape {
507 ui.Pages.RemovePage("logs")
511 ui.Pages.AddPage("logs", ui.HLClient.DebugBuf.TextView, true, true)
516 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {