6 "github.com/davecgh/go-spew/spew"
7 "github.com/gdamore/tcell/v2"
8 "github.com/rivo/tview"
18 chatBox *tview.TextView
19 chatInput *tview.InputField
20 App *tview.Application
22 userList *tview.TextView
23 agreeModal *tview.Modal
24 trackerList *tview.List
30 pageServerUI = "serverUI"
33 func NewUI(c *Client) *UI {
34 app := tview.NewApplication()
35 chatBox := tview.NewTextView().
37 SetDynamicColors(true).
39 SetChangedFunc(func() {
40 app.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
42 chatBox.Box.SetBorder(true).SetTitle("| Chat |")
44 chatInput := tview.NewInputField()
47 SetFieldBackgroundColor(tcell.ColorDimGray).
48 SetDoneFunc(func(key tcell.Key) {
49 // skip send if user hit enter with no other text
50 if len(chatInput.GetText()) == 0 {
55 *NewTransaction(tranChatSend, nil,
56 NewField(fieldData, []byte(chatInput.GetText())),
59 chatInput.SetText("") // clear the input field after chat send
62 chatInput.Box.SetBorder(true).SetTitle("Send")
66 SetDynamicColors(true).
67 SetChangedFunc(func() {
68 app.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
70 userList.Box.SetBorder(true).SetTitle("Users")
75 Pages: tview.NewPages(),
78 trackerList: tview.NewList(),
79 agreeModal: tview.NewModal(),
84 func (ui *UI) showBookmarks() *tview.List {
85 list := tview.NewList()
86 list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
87 if event.Key() == tcell.KeyEsc {
88 ui.Pages.SwitchToPage("home")
92 list.Box.SetBorder(true).SetTitle("| Bookmarks |")
94 shortcut := 97 // rune for "a"
95 for i, srv := range ui.HLClient.pref.Bookmarks {
99 list.AddItem(srv.Name, srv.Addr, rune(shortcut+i), func() {
100 ui.Pages.RemovePage("joinServer")
102 newJS := ui.renderJoinServerForm("", addr, login, pass, "bookmarks", true, true)
104 ui.Pages.AddPage("joinServer", newJS, true, true)
111 func (ui *UI) getTrackerList() *tview.List {
112 listing, err := GetListing(ui.HLClient.pref.Tracker)
117 list := tview.NewList()
118 list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
119 if event.Key() == tcell.KeyEsc {
120 ui.Pages.SwitchToPage("home")
124 list.Box.SetBorder(true).SetTitle("| Servers |")
126 shortcut := 97 // rune for "a"
127 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("", 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 errors.New(fmt.Sprintf("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 err := ui.joinServer(
261 joinServerForm.GetFormItem(0).(*tview.InputField).GetText(),
262 joinServerForm.GetFormItem(1).(*tview.InputField).GetText(),
263 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
266 ui.HLClient.Logger.Errorw("login error", "err", err)
267 loginErrModal := tview.NewModal().
268 AddButtons([]string{"Oh no"}).
269 SetText(err.Error()).
270 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
271 ui.Pages.SwitchToPage(backPage)
274 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
278 if joinServerForm.GetFormItem(3).(*tview.Checkbox).IsChecked() {
279 // TODO: implement bookmark saving
283 joinServerForm.Box.SetBorder(true).SetTitle("| Connect |")
284 joinServerForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
285 if event.Key() == tcell.KeyEscape {
286 ui.Pages.SwitchToPage(backPage)
292 joinServerForm.SetFocus(5)
295 joinServerPage := tview.NewFlex().
296 AddItem(nil, 0, 1, false).
297 AddItem(tview.NewFlex().
298 SetDirection(tview.FlexRow).
299 AddItem(nil, 0, 1, false).
300 AddItem(joinServerForm, 14, 1, true).
301 AddItem(nil, 0, 1, false), 40, 1, true).
302 AddItem(nil, 0, 1, false)
304 return joinServerPage
307 func (ui *UI) renderServerUI() *tview.Flex {
308 ui.chatBox.SetText("") // clear any previously existing chatbox text
309 commandList := tview.NewTextView().SetDynamicColors(true)
311 SetText("[yellow]^n[-::]: Read News [yellow]^p[-::]: Post News\n[yellow]^l[-::]: View Logs [yellow]^f[-::]: View Files\n").
313 SetTitle("| Keyboard Shortcuts| ")
315 modal := tview.NewModal().
316 SetText("Disconnect from the server?").
317 AddButtons([]string{"Cancel", "Exit"}).
319 modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
320 if buttonIndex == 1 {
321 _ = ui.HLClient.Disconnect()
322 ui.Pages.RemovePage(pageServerUI)
324 ui.Pages.HidePage("modal")
328 serverUI := tview.NewFlex().
329 AddItem(tview.NewFlex().
330 SetDirection(tview.FlexRow).
331 AddItem(commandList, 4, 0, false).
332 AddItem(ui.chatBox, 0, 8, false).
333 AddItem(ui.chatInput, 3, 0, true), 0, 1, true).
334 AddItem(ui.userList, 25, 1, false)
335 serverUI.SetBorder(true).SetTitle("| Mobius - Connected to " + "TODO" + " |").SetTitleAlign(tview.AlignLeft)
336 serverUI.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
337 if event.Key() == tcell.KeyEscape {
338 ui.Pages.AddPage("modal", modal, false, true)
342 if event.Key() == tcell.KeyCtrlF {
343 if err := ui.HLClient.Send(*NewTransaction(tranGetFileNameList, nil)); err != nil {
344 ui.HLClient.Logger.Errorw("err", "err", err)
349 if event.Key() == tcell.KeyCtrlN {
350 if err := ui.HLClient.Send(*NewTransaction(tranGetMsgs, nil)); err != nil {
351 ui.HLClient.Logger.Errorw("err", "err", err)
356 if event.Key() == tcell.KeyCtrlP {
358 newsFlex := tview.NewFlex()
359 newsFlex.SetBorderPadding(0, 0, 1, 1)
360 newsPostTextArea := tview.NewTextView()
361 newsPostTextArea.SetBackgroundColor(tcell.ColorDarkSlateGrey)
362 newsPostTextArea.SetChangedFunc(func() {
363 ui.App.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
366 newsPostForm := tview.NewForm().
367 SetButtonsAlign(tview.AlignRight).
368 //AddButton("Cancel", nil). // TODO: implement cancel button behavior
369 AddButton("Send", nil)
370 newsPostForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
372 case tcell.KeyEscape:
373 ui.Pages.RemovePage("newsInput")
375 ui.App.SetFocus(newsPostTextArea)
377 newsText := strings.ReplaceAll(newsPostTextArea.GetText(true), "\n", "\r")
378 if len(newsText) == 0 {
381 err := ui.HLClient.Send(
382 *NewTransaction(tranOldPostNews, nil,
383 NewField(fieldData, []byte(newsText)),
387 ui.HLClient.Logger.Errorw("Error posting news", "err", err)
388 // TODO: display errModal to user
390 ui.Pages.RemovePage("newsInput")
397 SetDirection(tview.FlexRow).
399 SetTitle("| Post Message |")
401 newsPostTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
403 case tcell.KeyEscape:
404 ui.Pages.RemovePage("newsInput")
406 ui.App.SetFocus(newsPostForm)
408 _, _ = fmt.Fprintf(newsPostTextArea, "\n")
410 const windowsBackspaceRune = 8
411 const macBackspaceRune = 127
412 switch event.Rune() {
413 case macBackspaceRune, windowsBackspaceRune:
414 curTxt := newsPostTextArea.GetText(true)
416 curTxt = curTxt[:len(curTxt)-1]
417 newsPostTextArea.SetText(curTxt)
420 _, _ = fmt.Fprintf(newsPostTextArea, string(event.Rune()))
427 newsFlex.AddItem(newsPostTextArea, 10, 0, true)
428 newsFlex.AddItem(newsPostForm, 3, 0, false)
430 newsPostPage := tview.NewFlex().
431 AddItem(nil, 0, 1, false).
432 AddItem(tview.NewFlex().
433 SetDirection(tview.FlexRow).
434 AddItem(nil, 0, 1, false).
435 AddItem(newsFlex, 15, 1, true).
436 //AddItem(newsPostForm, 3, 0, false).
437 AddItem(nil, 0, 1, false), 40, 1, false).
438 AddItem(nil, 0, 1, false)
440 ui.Pages.AddPage("newsInput", newsPostPage, true, true)
441 ui.App.SetFocus(newsPostTextArea)
449 func (ui *UI) Start() {
450 home := tview.NewFlex().SetDirection(tview.FlexRow)
451 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
452 mainMenu := tview.NewList()
454 bannerItem := tview.NewTextView().
455 SetText(randomBanner()).
456 SetDynamicColors(true).
457 SetTextAlign(tview.AlignCenter)
460 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
462 home.AddItem(tview.NewFlex().
463 AddItem(nil, 0, 1, false).
464 AddItem(mainMenu, 0, 1, true).
465 AddItem(nil, 0, 1, false),
469 mainMenu.AddItem("Join Server", "", 'j', func() {
470 joinServerPage := ui.renderJoinServerForm("", "", GuestAccount, "", "home", false, false)
471 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
473 AddItem("Bookmarks", "", 'b', func() {
474 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
476 AddItem("Browse Tracker", "", 't', func() {
477 ui.trackerList = ui.getTrackerList()
478 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
480 AddItem("Settings", "", 's', func() {
481 ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, true)
483 AddItem("Quit", "", 'q', func() {
487 ui.Pages.AddPage("home", home, true, true)
489 // App level input capture
490 ui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
491 if event.Key() == tcell.KeyCtrlC {
492 ui.HLClient.Logger.Infow("Exiting")
497 if event.Key() == tcell.KeyCtrlL {
498 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
499 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
500 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
501 if key == tcell.KeyEscape {
502 ui.Pages.RemovePage("logs")
506 ui.Pages.AddPage("logs", ui.HLClient.DebugBuf.TextView, true, true)
511 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {