6 "github.com/gdamore/tcell/v2"
7 "github.com/rivo/tview"
15 chatBox *tview.TextView
16 chatInput *tview.InputField
17 App *tview.Application
19 userList *tview.TextView
20 trackerList *tview.List
26 pageServerUI = "serverUI"
29 func NewUI(c *Client) *UI {
30 app := tview.NewApplication()
31 chatBox := tview.NewTextView().
33 SetDynamicColors(true).
35 SetChangedFunc(func() {
36 app.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
38 chatBox.Box.SetBorder(true).SetTitle("| Chat |")
40 chatInput := tview.NewInputField()
43 SetFieldBackgroundColor(tcell.ColorDimGray).
44 SetDoneFunc(func(key tcell.Key) {
45 // skip send if user hit enter with no other text
46 if len(chatInput.GetText()) == 0 {
51 *NewTransaction(TranChatSend, nil,
52 NewField(FieldData, []byte(chatInput.GetText())),
55 chatInput.SetText("") // clear the input field after chat send
58 chatInput.Box.SetBorder(true).SetTitle("Send")
62 SetDynamicColors(true).
63 SetChangedFunc(func() {
64 app.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
66 userList.Box.SetBorder(true).SetTitle("Users")
71 Pages: tview.NewPages(),
74 trackerList: tview.NewList(),
79 func (ui *UI) showBookmarks() *tview.List {
80 list := tview.NewList()
81 list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
82 if event.Key() == tcell.KeyEsc {
83 ui.Pages.SwitchToPage("home")
87 list.Box.SetBorder(true).SetTitle("| Bookmarks |")
89 shortcut := 97 // rune for "a"
90 for i, srv := range ui.HLClient.Pref.Bookmarks {
94 list.AddItem(srv.Name, srv.Addr, rune(shortcut+i), func() {
95 ui.Pages.RemovePage("joinServer")
97 newJS := ui.renderJoinServerForm("", addr, login, pass, "bookmarks", true, true)
99 ui.Pages.AddPage("joinServer", newJS, true, true)
106 func (ui *UI) getTrackerList(servers []ServerRecord) *tview.List {
107 list := tview.NewList()
108 list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
109 if event.Key() == tcell.KeyEsc {
110 ui.Pages.SwitchToPage("home")
114 list.Box.SetBorder(true).SetTitle("| Servers |")
116 const shortcut = 97 // rune for "a"
117 for i, _ := range servers {
119 list.AddItem(string(srv.Name), string(srv.Description), rune(shortcut+i), func() {
120 ui.Pages.RemovePage("joinServer")
122 newJS := ui.renderJoinServerForm(string(srv.Name), srv.Addr(), GuestAccount, "", trackerListPage, false, true)
124 ui.Pages.AddPage("joinServer", newJS, true, true)
125 ui.Pages.ShowPage("joinServer")
132 func (ui *UI) renderSettingsForm() *tview.Flex {
133 iconStr := strconv.Itoa(ui.HLClient.Pref.IconID)
134 settingsForm := tview.NewForm()
135 settingsForm.AddInputField("Your Name", ui.HLClient.Pref.Username, 0, nil, nil)
136 settingsForm.AddInputField("IconID", iconStr, 0, func(idStr string, _ rune) bool {
137 _, err := strconv.Atoi(idStr)
140 settingsForm.AddInputField("Tracker", ui.HLClient.Pref.Tracker, 0, nil, nil)
141 settingsForm.AddCheckbox("Enable Terminal Bell", ui.HLClient.Pref.EnableBell, nil)
142 settingsForm.AddButton("Save", func() {
143 usernameInput := settingsForm.GetFormItem(0).(*tview.InputField).GetText()
144 if len(usernameInput) == 0 {
145 usernameInput = "unnamed"
147 ui.HLClient.Pref.Username = usernameInput
148 iconStr = settingsForm.GetFormItem(1).(*tview.InputField).GetText()
149 ui.HLClient.Pref.IconID, _ = strconv.Atoi(iconStr)
150 ui.HLClient.Pref.Tracker = settingsForm.GetFormItem(2).(*tview.InputField).GetText()
151 ui.HLClient.Pref.EnableBell = settingsForm.GetFormItem(3).(*tview.Checkbox).IsChecked()
153 out, err := yaml.Marshal(&ui.HLClient.Pref)
158 err = os.WriteFile(ui.HLClient.cfgPath, out, 0666)
160 println(ui.HLClient.cfgPath)
163 ui.Pages.RemovePage("settings")
165 settingsForm.SetBorder(true)
166 settingsForm.SetCancelFunc(func() {
167 ui.Pages.RemovePage("settings")
169 settingsPage := tview.NewFlex().SetDirection(tview.FlexRow)
170 settingsPage.Box.SetBorder(true).SetTitle("Settings")
171 settingsPage.AddItem(settingsForm, 0, 1, true)
173 centerFlex := tview.NewFlex().
174 AddItem(nil, 0, 1, false).
175 AddItem(tview.NewFlex().
176 SetDirection(tview.FlexRow).
177 AddItem(nil, 0, 1, false).
178 AddItem(settingsForm, 15, 1, true).
179 AddItem(nil, 0, 1, false), 40, 1, true).
180 AddItem(nil, 0, 1, false)
185 func (ui *UI) joinServer(addr, login, password string) error {
186 // append default port to address if no port supplied
187 if len(strings.Split(addr, ":")) == 1 {
190 if err := ui.HLClient.Connect(addr, login, password); err != nil {
191 return fmt.Errorf("Error joining server: %v\n", err)
195 if err := ui.HLClient.HandleTransactions(context.TODO()); err != nil {
196 ui.Pages.SwitchToPage("home")
199 loginErrModal := tview.NewModal().
200 AddButtons([]string{"Ok"}).
201 SetText("The server connection has closed.").
202 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
203 ui.Pages.SwitchToPage("home")
205 loginErrModal.Box.SetTitle("Server Connection Error")
207 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
214 func (ui *UI) renderJoinServerForm(name, server, login, password, backPage string, save, defaultConnect bool) *tview.Flex {
215 joinServerForm := tview.NewForm()
217 AddInputField("Server", server, 0, nil, nil).
218 AddInputField("Login", login, 0, nil, nil).
219 AddPasswordField("Password", password, 0, '*', nil).
220 AddCheckbox("Save", save, func(checked bool) {
221 ui.HLClient.Pref.AddBookmark(
222 joinServerForm.GetFormItem(0).(*tview.InputField).GetText(),
223 joinServerForm.GetFormItem(0).(*tview.InputField).GetText(),
224 joinServerForm.GetFormItem(1).(*tview.InputField).GetText(),
225 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
228 out, err := yaml.Marshal(ui.HLClient.Pref)
233 err = os.WriteFile(ui.HLClient.cfgPath, out, 0666)
238 AddButton("Cancel", func() {
239 ui.Pages.SwitchToPage(backPage)
241 AddButton("Connect", func() {
242 srvAddr := joinServerForm.GetFormItem(0).(*tview.InputField).GetText()
243 loginInput := joinServerForm.GetFormItem(1).(*tview.InputField).GetText()
244 err := ui.joinServer(
247 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
250 name = fmt.Sprintf("%s@%s", loginInput, srvAddr)
252 ui.HLClient.serverName = name
255 ui.HLClient.Logger.Error("login error", "err", err)
256 loginErrModal := tview.NewModal().
257 AddButtons([]string{"Oh no"}).
258 SetText(err.Error()).
259 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
260 ui.Pages.SwitchToPage(backPage)
263 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
267 if joinServerForm.GetFormItem(3).(*tview.Checkbox).IsChecked() {
268 // TODO: implement bookmark saving
272 joinServerForm.Box.SetBorder(true).SetTitle("| Connect |")
273 joinServerForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
274 if event.Key() == tcell.KeyEscape {
275 ui.Pages.SwitchToPage(backPage)
281 joinServerForm.SetFocus(5)
284 joinServerPage := tview.NewFlex().
285 AddItem(nil, 0, 1, false).
286 AddItem(tview.NewFlex().
287 SetDirection(tview.FlexRow).
288 AddItem(nil, 0, 1, false).
289 AddItem(joinServerForm, 14, 1, true).
290 AddItem(nil, 0, 1, false), 40, 1, true).
291 AddItem(nil, 0, 1, false)
293 return joinServerPage
296 func (ui *UI) renderServerUI() *tview.Flex {
297 ui.chatBox.SetText("") // clear any previously existing chatbox text
298 commandList := tview.NewTextView().SetDynamicColors(true)
300 SetText("[yellow]^n[-::]: Read News [yellow]^p[-::]: Post News\n[yellow]^l[-::]: View Logs [yellow]^f[-::]: View Files\n").
302 SetTitle("| Keyboard Shortcuts| ")
304 modal := tview.NewModal().
305 SetText("Disconnect from the server?").
306 AddButtons([]string{"Cancel", "Exit"}).
308 modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
309 if buttonIndex == 1 {
310 _ = ui.HLClient.Disconnect()
311 ui.Pages.RemovePage(pageServerUI)
312 ui.Pages.SwitchToPage("home")
314 ui.Pages.HidePage("modal")
318 serverUI := tview.NewFlex().
319 AddItem(tview.NewFlex().
320 SetDirection(tview.FlexRow).
321 AddItem(commandList, 4, 0, false).
322 AddItem(ui.chatBox, 0, 8, false).
323 AddItem(ui.chatInput, 3, 0, true), 0, 1, true).
324 AddItem(ui.userList, 25, 1, false)
325 serverUI.SetBorder(true).SetTitle("| Mobius - Connected to " + ui.HLClient.serverName + " |").SetTitleAlign(tview.AlignLeft)
326 serverUI.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
327 if event.Key() == tcell.KeyEscape {
328 ui.Pages.AddPage("modal", modal, false, true)
332 if event.Key() == tcell.KeyCtrlF {
333 if err := ui.HLClient.Send(*NewTransaction(TranGetFileNameList, nil)); err != nil {
334 ui.HLClient.Logger.Error("err", "err", err)
339 if event.Key() == tcell.KeyCtrlN {
340 if err := ui.HLClient.Send(*NewTransaction(TranGetMsgs, nil)); err != nil {
341 ui.HLClient.Logger.Error("err", "err", err)
346 if event.Key() == tcell.KeyCtrlP {
347 newsFlex := tview.NewFlex()
348 newsFlex.SetBorderPadding(0, 0, 1, 1)
349 newsPostTextArea := tview.NewTextView()
350 newsPostTextArea.SetBackgroundColor(tcell.ColorDarkSlateGrey)
351 newsPostTextArea.SetChangedFunc(func() {
352 ui.App.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
355 newsPostForm := tview.NewForm().
356 SetButtonsAlign(tview.AlignRight).
357 // AddButton("Cancel", nil). // TODO: implement cancel button behavior
358 AddButton("Send", nil)
359 newsPostForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
361 case tcell.KeyEscape:
362 ui.Pages.RemovePage("newsInput")
364 ui.App.SetFocus(newsPostTextArea)
366 newsText := strings.ReplaceAll(newsPostTextArea.GetText(true), "\n", "\r")
367 if len(newsText) == 0 {
370 err := ui.HLClient.Send(
371 *NewTransaction(TranOldPostNews, nil,
372 NewField(FieldData, []byte(newsText)),
376 ui.HLClient.Logger.Error("Error posting news", "err", err)
377 // TODO: display errModal to user
379 ui.Pages.RemovePage("newsInput")
386 SetDirection(tview.FlexRow).
388 SetTitle("| Post Message |")
390 newsPostTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
392 case tcell.KeyEscape:
393 ui.Pages.RemovePage("newsInput")
395 ui.App.SetFocus(newsPostForm)
397 _, _ = fmt.Fprintf(newsPostTextArea, "\n")
399 const windowsBackspaceRune = 8
400 const macBackspaceRune = 127
401 switch event.Rune() {
402 case macBackspaceRune, windowsBackspaceRune:
403 curTxt := newsPostTextArea.GetText(true)
405 curTxt = curTxt[:len(curTxt)-1]
406 newsPostTextArea.SetText(curTxt)
409 _, _ = fmt.Fprint(newsPostTextArea, string(event.Rune()))
416 newsFlex.AddItem(newsPostTextArea, 10, 0, true)
417 newsFlex.AddItem(newsPostForm, 3, 0, false)
419 newsPostPage := tview.NewFlex().
420 AddItem(nil, 0, 1, false).
421 AddItem(tview.NewFlex().
422 SetDirection(tview.FlexRow).
423 AddItem(nil, 0, 1, false).
424 AddItem(newsFlex, 15, 1, true).
425 // AddItem(newsPostForm, 3, 0, false).
426 AddItem(nil, 0, 1, false), 40, 1, false).
427 AddItem(nil, 0, 1, false)
429 ui.Pages.AddPage("newsInput", newsPostPage, true, true)
430 ui.App.SetFocus(newsPostTextArea)
438 func (ui *UI) Start() {
439 home := tview.NewFlex().SetDirection(tview.FlexRow)
440 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
441 mainMenu := tview.NewList()
443 bannerItem := tview.NewTextView().
444 SetText(randomBanner()).
445 SetDynamicColors(true).
446 SetTextAlign(tview.AlignCenter)
449 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
451 home.AddItem(tview.NewFlex().
452 AddItem(nil, 0, 1, false).
453 AddItem(mainMenu, 0, 1, true).
454 AddItem(nil, 0, 1, false),
458 mainMenu.AddItem("Join Server", "", 'j', func() {
459 joinServerPage := ui.renderJoinServerForm("", "", GuestAccount, "", "home", false, false)
460 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
462 AddItem("Bookmarks", "", 'b', func() {
463 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
465 AddItem("Browse Tracker", "", 't', func() {
466 listing, err := GetListing(ui.HLClient.Pref.Tracker)
468 errMsg := fmt.Sprintf("Error fetching tracker results:\n%v", err)
469 errModal := tview.NewModal()
470 errModal.SetText(errMsg)
471 errModal.AddButtons([]string{"Cancel"})
472 errModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
473 ui.Pages.RemovePage("errModal")
475 ui.Pages.RemovePage("joinServer")
476 ui.Pages.AddPage("errModal", errModal, false, true)
479 ui.trackerList = ui.getTrackerList(listing)
480 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
482 AddItem("Settings", "", 's', func() {
483 ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, true)
485 AddItem("Quit", "", 'q', func() {
489 ui.Pages.AddPage("home", home, true, true)
491 // App level input capture
492 ui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
493 if event.Key() == tcell.KeyCtrlC {
494 ui.HLClient.Logger.Info("Exiting")
499 if event.Key() == tcell.KeyCtrlL {
500 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
501 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
502 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
503 if key == tcell.KeyEscape {
504 ui.Pages.RemovePage("logs")
508 ui.Pages.AddPage("logs", ui.HLClient.DebugBuf.TextView, true, true)
513 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {