5 "github.com/gdamore/tcell/v2"
6 "github.com/rivo/tview"
14 chatBox *tview.TextView
15 chatInput *tview.InputField
16 App *tview.Application
18 userList *tview.TextView
19 trackerList *tview.List
25 pageServerUI = "serverUI"
28 func NewUI(c *Client) *UI {
29 app := tview.NewApplication()
30 chatBox := tview.NewTextView().
32 SetDynamicColors(true).
34 SetChangedFunc(func() {
35 app.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
37 chatBox.Box.SetBorder(true).SetTitle("| Chat |")
39 chatInput := tview.NewInputField()
42 SetFieldBackgroundColor(tcell.ColorDimGray).
43 SetDoneFunc(func(key tcell.Key) {
44 // skip send if user hit enter with no other text
45 if len(chatInput.GetText()) == 0 {
50 *NewTransaction(TranChatSend, nil,
51 NewField(FieldData, []byte(chatInput.GetText())),
54 chatInput.SetText("") // clear the input field after chat send
57 chatInput.Box.SetBorder(true).SetTitle("Send")
61 SetDynamicColors(true).
62 SetChangedFunc(func() {
63 app.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
65 userList.Box.SetBorder(true).SetTitle("Users")
70 Pages: tview.NewPages(),
73 trackerList: tview.NewList(),
78 func (ui *UI) showBookmarks() *tview.List {
79 list := tview.NewList()
80 list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
81 if event.Key() == tcell.KeyEsc {
82 ui.Pages.SwitchToPage("home")
86 list.Box.SetBorder(true).SetTitle("| Bookmarks |")
88 shortcut := 97 // rune for "a"
89 for i, srv := range ui.HLClient.Pref.Bookmarks {
93 list.AddItem(srv.Name, srv.Addr, rune(shortcut+i), func() {
94 ui.Pages.RemovePage("joinServer")
96 newJS := ui.renderJoinServerForm("", addr, login, pass, "bookmarks", true, true)
98 ui.Pages.AddPage("joinServer", newJS, true, true)
105 func (ui *UI) getTrackerList(servers []ServerRecord) *tview.List {
106 list := tview.NewList()
107 list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
108 if event.Key() == tcell.KeyEsc {
109 ui.Pages.SwitchToPage("home")
113 list.Box.SetBorder(true).SetTitle("| Servers |")
115 const shortcut = 97 // rune for "a"
116 for i, srv := range servers {
117 list.AddItem(string(srv.Name), string(srv.Description), rune(shortcut+i), func() {
118 ui.Pages.RemovePage("joinServer")
120 newJS := ui.renderJoinServerForm(string(srv.Name), srv.Addr(), GuestAccount, "", trackerListPage, false, true)
122 ui.Pages.AddPage("joinServer", newJS, true, true)
123 ui.Pages.ShowPage("joinServer")
130 func (ui *UI) renderSettingsForm() *tview.Flex {
131 iconStr := strconv.Itoa(ui.HLClient.Pref.IconID)
132 settingsForm := tview.NewForm()
133 settingsForm.AddInputField("Your Name", ui.HLClient.Pref.Username, 0, nil, nil)
134 settingsForm.AddInputField("IconID", iconStr, 0, func(idStr string, _ rune) bool {
135 _, err := strconv.Atoi(idStr)
138 settingsForm.AddInputField("Tracker", ui.HLClient.Pref.Tracker, 0, nil, nil)
139 settingsForm.AddCheckbox("Enable Terminal Bell", ui.HLClient.Pref.EnableBell, nil)
140 settingsForm.AddButton("Save", func() {
141 usernameInput := settingsForm.GetFormItem(0).(*tview.InputField).GetText()
142 if len(usernameInput) == 0 {
143 usernameInput = "unnamed"
145 ui.HLClient.Pref.Username = usernameInput
146 iconStr = settingsForm.GetFormItem(1).(*tview.InputField).GetText()
147 ui.HLClient.Pref.IconID, _ = strconv.Atoi(iconStr)
148 ui.HLClient.Pref.Tracker = settingsForm.GetFormItem(2).(*tview.InputField).GetText()
149 ui.HLClient.Pref.EnableBell = settingsForm.GetFormItem(3).(*tview.Checkbox).IsChecked()
151 out, err := yaml.Marshal(&ui.HLClient.Pref)
156 err = os.WriteFile(ui.HLClient.cfgPath, out, 0666)
158 println(ui.HLClient.cfgPath)
161 ui.Pages.RemovePage("settings")
163 settingsForm.SetBorder(true)
164 settingsForm.SetCancelFunc(func() {
165 ui.Pages.RemovePage("settings")
167 settingsPage := tview.NewFlex().SetDirection(tview.FlexRow)
168 settingsPage.Box.SetBorder(true).SetTitle("Settings")
169 settingsPage.AddItem(settingsForm, 0, 1, true)
171 centerFlex := tview.NewFlex().
172 AddItem(nil, 0, 1, false).
173 AddItem(tview.NewFlex().
174 SetDirection(tview.FlexRow).
175 AddItem(nil, 0, 1, false).
176 AddItem(settingsForm, 15, 1, true).
177 AddItem(nil, 0, 1, false), 40, 1, true).
178 AddItem(nil, 0, 1, false)
183 func (ui *UI) joinServer(addr, login, password string) error {
184 // append default port to address if no port supplied
185 if len(strings.Split(addr, ":")) == 1 {
188 if err := ui.HLClient.Connect(addr, login, password); err != nil {
189 return fmt.Errorf("Error joining server: %v\n", err)
193 if err := ui.HLClient.HandleTransactions(); err != nil {
194 ui.Pages.SwitchToPage("home")
197 loginErrModal := tview.NewModal().
198 AddButtons([]string{"Ok"}).
199 SetText("The server connection has closed.").
200 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
201 ui.Pages.SwitchToPage("home")
203 loginErrModal.Box.SetTitle("Server Connection Error")
205 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
212 func (ui *UI) renderJoinServerForm(name, server, login, password, backPage string, save, defaultConnect bool) *tview.Flex {
213 joinServerForm := tview.NewForm()
215 AddInputField("Server", server, 0, nil, nil).
216 AddInputField("Login", login, 0, nil, nil).
217 AddPasswordField("Password", password, 0, '*', nil).
218 AddCheckbox("Save", save, func(checked bool) {
219 ui.HLClient.Pref.AddBookmark(
220 joinServerForm.GetFormItem(0).(*tview.InputField).GetText(),
221 joinServerForm.GetFormItem(0).(*tview.InputField).GetText(),
222 joinServerForm.GetFormItem(1).(*tview.InputField).GetText(),
223 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
226 out, err := yaml.Marshal(ui.HLClient.Pref)
231 err = os.WriteFile(ui.HLClient.cfgPath, out, 0666)
236 AddButton("Cancel", func() {
237 ui.Pages.SwitchToPage(backPage)
239 AddButton("Connect", func() {
240 srvAddr := joinServerForm.GetFormItem(0).(*tview.InputField).GetText()
241 loginInput := joinServerForm.GetFormItem(1).(*tview.InputField).GetText()
242 err := ui.joinServer(
245 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
248 name = fmt.Sprintf("%s@%s", loginInput, srvAddr)
250 ui.HLClient.serverName = name
253 ui.HLClient.Logger.Errorw("login error", "err", err)
254 loginErrModal := tview.NewModal().
255 AddButtons([]string{"Oh no"}).
256 SetText(err.Error()).
257 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
258 ui.Pages.SwitchToPage(backPage)
261 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
265 if joinServerForm.GetFormItem(3).(*tview.Checkbox).IsChecked() {
266 // TODO: implement bookmark saving
270 joinServerForm.Box.SetBorder(true).SetTitle("| Connect |")
271 joinServerForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
272 if event.Key() == tcell.KeyEscape {
273 ui.Pages.SwitchToPage(backPage)
279 joinServerForm.SetFocus(5)
282 joinServerPage := tview.NewFlex().
283 AddItem(nil, 0, 1, false).
284 AddItem(tview.NewFlex().
285 SetDirection(tview.FlexRow).
286 AddItem(nil, 0, 1, false).
287 AddItem(joinServerForm, 14, 1, true).
288 AddItem(nil, 0, 1, false), 40, 1, true).
289 AddItem(nil, 0, 1, false)
291 return joinServerPage
294 func (ui *UI) renderServerUI() *tview.Flex {
295 ui.chatBox.SetText("") // clear any previously existing chatbox text
296 commandList := tview.NewTextView().SetDynamicColors(true)
298 SetText("[yellow]^n[-::]: Read News [yellow]^p[-::]: Post News\n[yellow]^l[-::]: View Logs [yellow]^f[-::]: View Files\n").
300 SetTitle("| Keyboard Shortcuts| ")
302 modal := tview.NewModal().
303 SetText("Disconnect from the server?").
304 AddButtons([]string{"Cancel", "Exit"}).
306 modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
307 if buttonIndex == 1 {
308 _ = ui.HLClient.Disconnect()
309 ui.Pages.RemovePage(pageServerUI)
310 ui.Pages.SwitchToPage("home")
312 ui.Pages.HidePage("modal")
316 serverUI := tview.NewFlex().
317 AddItem(tview.NewFlex().
318 SetDirection(tview.FlexRow).
319 AddItem(commandList, 4, 0, false).
320 AddItem(ui.chatBox, 0, 8, false).
321 AddItem(ui.chatInput, 3, 0, true), 0, 1, true).
322 AddItem(ui.userList, 25, 1, false)
323 serverUI.SetBorder(true).SetTitle("| Mobius - Connected to " + ui.HLClient.serverName + " |").SetTitleAlign(tview.AlignLeft)
324 serverUI.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
325 if event.Key() == tcell.KeyEscape {
326 ui.Pages.AddPage("modal", modal, false, true)
330 if event.Key() == tcell.KeyCtrlF {
331 if err := ui.HLClient.Send(*NewTransaction(TranGetFileNameList, nil)); err != nil {
332 ui.HLClient.Logger.Errorw("err", "err", err)
337 if event.Key() == tcell.KeyCtrlN {
338 if err := ui.HLClient.Send(*NewTransaction(TranGetMsgs, nil)); err != nil {
339 ui.HLClient.Logger.Errorw("err", "err", err)
344 if event.Key() == tcell.KeyCtrlP {
345 newsFlex := tview.NewFlex()
346 newsFlex.SetBorderPadding(0, 0, 1, 1)
347 newsPostTextArea := tview.NewTextView()
348 newsPostTextArea.SetBackgroundColor(tcell.ColorDarkSlateGrey)
349 newsPostTextArea.SetChangedFunc(func() {
350 ui.App.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
353 newsPostForm := tview.NewForm().
354 SetButtonsAlign(tview.AlignRight).
355 // AddButton("Cancel", nil). // TODO: implement cancel button behavior
356 AddButton("Send", nil)
357 newsPostForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
359 case tcell.KeyEscape:
360 ui.Pages.RemovePage("newsInput")
362 ui.App.SetFocus(newsPostTextArea)
364 newsText := strings.ReplaceAll(newsPostTextArea.GetText(true), "\n", "\r")
365 if len(newsText) == 0 {
368 err := ui.HLClient.Send(
369 *NewTransaction(TranOldPostNews, nil,
370 NewField(FieldData, []byte(newsText)),
374 ui.HLClient.Logger.Errorw("Error posting news", "err", err)
375 // TODO: display errModal to user
377 ui.Pages.RemovePage("newsInput")
384 SetDirection(tview.FlexRow).
386 SetTitle("| Post Message |")
388 newsPostTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
390 case tcell.KeyEscape:
391 ui.Pages.RemovePage("newsInput")
393 ui.App.SetFocus(newsPostForm)
395 _, _ = fmt.Fprintf(newsPostTextArea, "\n")
397 const windowsBackspaceRune = 8
398 const macBackspaceRune = 127
399 switch event.Rune() {
400 case macBackspaceRune, windowsBackspaceRune:
401 curTxt := newsPostTextArea.GetText(true)
403 curTxt = curTxt[:len(curTxt)-1]
404 newsPostTextArea.SetText(curTxt)
407 _, _ = fmt.Fprint(newsPostTextArea, string(event.Rune()))
414 newsFlex.AddItem(newsPostTextArea, 10, 0, true)
415 newsFlex.AddItem(newsPostForm, 3, 0, false)
417 newsPostPage := tview.NewFlex().
418 AddItem(nil, 0, 1, false).
419 AddItem(tview.NewFlex().
420 SetDirection(tview.FlexRow).
421 AddItem(nil, 0, 1, false).
422 AddItem(newsFlex, 15, 1, true).
423 // AddItem(newsPostForm, 3, 0, false).
424 AddItem(nil, 0, 1, false), 40, 1, false).
425 AddItem(nil, 0, 1, false)
427 ui.Pages.AddPage("newsInput", newsPostPage, true, true)
428 ui.App.SetFocus(newsPostTextArea)
436 func (ui *UI) Start() {
437 home := tview.NewFlex().SetDirection(tview.FlexRow)
438 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
439 mainMenu := tview.NewList()
441 bannerItem := tview.NewTextView().
442 SetText(randomBanner()).
443 SetDynamicColors(true).
444 SetTextAlign(tview.AlignCenter)
447 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
449 home.AddItem(tview.NewFlex().
450 AddItem(nil, 0, 1, false).
451 AddItem(mainMenu, 0, 1, true).
452 AddItem(nil, 0, 1, false),
456 mainMenu.AddItem("Join Server", "", 'j', func() {
457 joinServerPage := ui.renderJoinServerForm("", "", GuestAccount, "", "home", false, false)
458 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
460 AddItem("Bookmarks", "", 'b', func() {
461 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
463 AddItem("Browse Tracker", "", 't', func() {
464 listing, err := GetListing(ui.HLClient.Pref.Tracker)
466 errMsg := fmt.Sprintf("Error fetching tracker results:\n%v", err)
467 errModal := tview.NewModal()
468 errModal.SetText(errMsg)
469 errModal.AddButtons([]string{"Cancel"})
470 errModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
471 ui.Pages.RemovePage("errModal")
473 ui.Pages.RemovePage("joinServer")
474 ui.Pages.AddPage("errModal", errModal, false, true)
477 ui.trackerList = ui.getTrackerList(listing)
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 {