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, _ := range servers {
118 list.AddItem(string(srv.Name), string(srv.Description), rune(shortcut+i), func() {
119 ui.Pages.RemovePage("joinServer")
121 newJS := ui.renderJoinServerForm(string(srv.Name), srv.Addr(), GuestAccount, "", trackerListPage, false, true)
123 ui.Pages.AddPage("joinServer", newJS, true, true)
124 ui.Pages.ShowPage("joinServer")
131 func (ui *UI) renderSettingsForm() *tview.Flex {
132 iconStr := strconv.Itoa(ui.HLClient.Pref.IconID)
133 settingsForm := tview.NewForm()
134 settingsForm.AddInputField("Your Name", ui.HLClient.Pref.Username, 0, nil, nil)
135 settingsForm.AddInputField("IconID", iconStr, 0, func(idStr string, _ rune) bool {
136 _, err := strconv.Atoi(idStr)
139 settingsForm.AddInputField("Tracker", ui.HLClient.Pref.Tracker, 0, nil, nil)
140 settingsForm.AddCheckbox("Enable Terminal Bell", ui.HLClient.Pref.EnableBell, nil)
141 settingsForm.AddButton("Save", func() {
142 usernameInput := settingsForm.GetFormItem(0).(*tview.InputField).GetText()
143 if len(usernameInput) == 0 {
144 usernameInput = "unnamed"
146 ui.HLClient.Pref.Username = usernameInput
147 iconStr = settingsForm.GetFormItem(1).(*tview.InputField).GetText()
148 ui.HLClient.Pref.IconID, _ = strconv.Atoi(iconStr)
149 ui.HLClient.Pref.Tracker = settingsForm.GetFormItem(2).(*tview.InputField).GetText()
150 ui.HLClient.Pref.EnableBell = settingsForm.GetFormItem(3).(*tview.Checkbox).IsChecked()
152 out, err := yaml.Marshal(&ui.HLClient.Pref)
157 err = os.WriteFile(ui.HLClient.cfgPath, out, 0666)
159 println(ui.HLClient.cfgPath)
162 ui.Pages.RemovePage("settings")
164 settingsForm.SetBorder(true)
165 settingsForm.SetCancelFunc(func() {
166 ui.Pages.RemovePage("settings")
168 settingsPage := tview.NewFlex().SetDirection(tview.FlexRow)
169 settingsPage.Box.SetBorder(true).SetTitle("Settings")
170 settingsPage.AddItem(settingsForm, 0, 1, true)
172 centerFlex := tview.NewFlex().
173 AddItem(nil, 0, 1, false).
174 AddItem(tview.NewFlex().
175 SetDirection(tview.FlexRow).
176 AddItem(nil, 0, 1, false).
177 AddItem(settingsForm, 15, 1, true).
178 AddItem(nil, 0, 1, false), 40, 1, true).
179 AddItem(nil, 0, 1, false)
184 func (ui *UI) joinServer(addr, login, password string) error {
185 // append default port to address if no port supplied
186 if len(strings.Split(addr, ":")) == 1 {
189 if err := ui.HLClient.Connect(addr, login, password); err != nil {
190 return fmt.Errorf("Error joining server: %v\n", err)
194 if err := ui.HLClient.HandleTransactions(); err != nil {
195 ui.Pages.SwitchToPage("home")
198 loginErrModal := tview.NewModal().
199 AddButtons([]string{"Ok"}).
200 SetText("The server connection has closed.").
201 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
202 ui.Pages.SwitchToPage("home")
204 loginErrModal.Box.SetTitle("Server Connection Error")
206 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
213 func (ui *UI) renderJoinServerForm(name, server, login, password, backPage string, save, defaultConnect bool) *tview.Flex {
214 joinServerForm := tview.NewForm()
216 AddInputField("Server", server, 0, nil, nil).
217 AddInputField("Login", login, 0, nil, nil).
218 AddPasswordField("Password", password, 0, '*', nil).
219 AddCheckbox("Save", save, func(checked bool) {
220 ui.HLClient.Pref.AddBookmark(
221 joinServerForm.GetFormItem(0).(*tview.InputField).GetText(),
222 joinServerForm.GetFormItem(0).(*tview.InputField).GetText(),
223 joinServerForm.GetFormItem(1).(*tview.InputField).GetText(),
224 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
227 out, err := yaml.Marshal(ui.HLClient.Pref)
232 err = os.WriteFile(ui.HLClient.cfgPath, out, 0666)
237 AddButton("Cancel", func() {
238 ui.Pages.SwitchToPage(backPage)
240 AddButton("Connect", func() {
241 srvAddr := joinServerForm.GetFormItem(0).(*tview.InputField).GetText()
242 loginInput := joinServerForm.GetFormItem(1).(*tview.InputField).GetText()
243 err := ui.joinServer(
246 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
249 name = fmt.Sprintf("%s@%s", loginInput, srvAddr)
251 ui.HLClient.serverName = name
254 ui.HLClient.Logger.Errorw("login error", "err", err)
255 loginErrModal := tview.NewModal().
256 AddButtons([]string{"Oh no"}).
257 SetText(err.Error()).
258 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
259 ui.Pages.SwitchToPage(backPage)
262 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
266 if joinServerForm.GetFormItem(3).(*tview.Checkbox).IsChecked() {
267 // TODO: implement bookmark saving
271 joinServerForm.Box.SetBorder(true).SetTitle("| Connect |")
272 joinServerForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
273 if event.Key() == tcell.KeyEscape {
274 ui.Pages.SwitchToPage(backPage)
280 joinServerForm.SetFocus(5)
283 joinServerPage := tview.NewFlex().
284 AddItem(nil, 0, 1, false).
285 AddItem(tview.NewFlex().
286 SetDirection(tview.FlexRow).
287 AddItem(nil, 0, 1, false).
288 AddItem(joinServerForm, 14, 1, true).
289 AddItem(nil, 0, 1, false), 40, 1, true).
290 AddItem(nil, 0, 1, false)
292 return joinServerPage
295 func (ui *UI) renderServerUI() *tview.Flex {
296 ui.chatBox.SetText("") // clear any previously existing chatbox text
297 commandList := tview.NewTextView().SetDynamicColors(true)
299 SetText("[yellow]^n[-::]: Read News [yellow]^p[-::]: Post News\n[yellow]^l[-::]: View Logs [yellow]^f[-::]: View Files\n").
301 SetTitle("| Keyboard Shortcuts| ")
303 modal := tview.NewModal().
304 SetText("Disconnect from the server?").
305 AddButtons([]string{"Cancel", "Exit"}).
307 modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
308 if buttonIndex == 1 {
309 _ = ui.HLClient.Disconnect()
310 ui.Pages.RemovePage(pageServerUI)
311 ui.Pages.SwitchToPage("home")
313 ui.Pages.HidePage("modal")
317 serverUI := tview.NewFlex().
318 AddItem(tview.NewFlex().
319 SetDirection(tview.FlexRow).
320 AddItem(commandList, 4, 0, false).
321 AddItem(ui.chatBox, 0, 8, false).
322 AddItem(ui.chatInput, 3, 0, true), 0, 1, true).
323 AddItem(ui.userList, 25, 1, false)
324 serverUI.SetBorder(true).SetTitle("| Mobius - Connected to " + ui.HLClient.serverName + " |").SetTitleAlign(tview.AlignLeft)
325 serverUI.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
326 if event.Key() == tcell.KeyEscape {
327 ui.Pages.AddPage("modal", modal, false, true)
331 if event.Key() == tcell.KeyCtrlF {
332 if err := ui.HLClient.Send(*NewTransaction(TranGetFileNameList, nil)); err != nil {
333 ui.HLClient.Logger.Errorw("err", "err", err)
338 if event.Key() == tcell.KeyCtrlN {
339 if err := ui.HLClient.Send(*NewTransaction(TranGetMsgs, nil)); err != nil {
340 ui.HLClient.Logger.Errorw("err", "err", err)
345 if event.Key() == tcell.KeyCtrlP {
346 newsFlex := tview.NewFlex()
347 newsFlex.SetBorderPadding(0, 0, 1, 1)
348 newsPostTextArea := tview.NewTextView()
349 newsPostTextArea.SetBackgroundColor(tcell.ColorDarkSlateGrey)
350 newsPostTextArea.SetChangedFunc(func() {
351 ui.App.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
354 newsPostForm := tview.NewForm().
355 SetButtonsAlign(tview.AlignRight).
356 // AddButton("Cancel", nil). // TODO: implement cancel button behavior
357 AddButton("Send", nil)
358 newsPostForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
360 case tcell.KeyEscape:
361 ui.Pages.RemovePage("newsInput")
363 ui.App.SetFocus(newsPostTextArea)
365 newsText := strings.ReplaceAll(newsPostTextArea.GetText(true), "\n", "\r")
366 if len(newsText) == 0 {
369 err := ui.HLClient.Send(
370 *NewTransaction(TranOldPostNews, nil,
371 NewField(FieldData, []byte(newsText)),
375 ui.HLClient.Logger.Errorw("Error posting news", "err", err)
376 // TODO: display errModal to user
378 ui.Pages.RemovePage("newsInput")
385 SetDirection(tview.FlexRow).
387 SetTitle("| Post Message |")
389 newsPostTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
391 case tcell.KeyEscape:
392 ui.Pages.RemovePage("newsInput")
394 ui.App.SetFocus(newsPostForm)
396 _, _ = fmt.Fprintf(newsPostTextArea, "\n")
398 const windowsBackspaceRune = 8
399 const macBackspaceRune = 127
400 switch event.Rune() {
401 case macBackspaceRune, windowsBackspaceRune:
402 curTxt := newsPostTextArea.GetText(true)
404 curTxt = curTxt[:len(curTxt)-1]
405 newsPostTextArea.SetText(curTxt)
408 _, _ = fmt.Fprint(newsPostTextArea, string(event.Rune()))
415 newsFlex.AddItem(newsPostTextArea, 10, 0, true)
416 newsFlex.AddItem(newsPostForm, 3, 0, false)
418 newsPostPage := tview.NewFlex().
419 AddItem(nil, 0, 1, false).
420 AddItem(tview.NewFlex().
421 SetDirection(tview.FlexRow).
422 AddItem(nil, 0, 1, false).
423 AddItem(newsFlex, 15, 1, true).
424 // AddItem(newsPostForm, 3, 0, false).
425 AddItem(nil, 0, 1, false), 40, 1, false).
426 AddItem(nil, 0, 1, false)
428 ui.Pages.AddPage("newsInput", newsPostPage, true, true)
429 ui.App.SetFocus(newsPostTextArea)
437 func (ui *UI) Start() {
438 home := tview.NewFlex().SetDirection(tview.FlexRow)
439 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
440 mainMenu := tview.NewList()
442 bannerItem := tview.NewTextView().
443 SetText(randomBanner()).
444 SetDynamicColors(true).
445 SetTextAlign(tview.AlignCenter)
448 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
450 home.AddItem(tview.NewFlex().
451 AddItem(nil, 0, 1, false).
452 AddItem(mainMenu, 0, 1, true).
453 AddItem(nil, 0, 1, false),
457 mainMenu.AddItem("Join Server", "", 'j', func() {
458 joinServerPage := ui.renderJoinServerForm("", "", GuestAccount, "", "home", false, false)
459 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
461 AddItem("Bookmarks", "", 'b', func() {
462 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
464 AddItem("Browse Tracker", "", 't', func() {
465 listing, err := GetListing(ui.HLClient.Pref.Tracker)
467 errMsg := fmt.Sprintf("Error fetching tracker results:\n%v", err)
468 errModal := tview.NewModal()
469 errModal.SetText(errMsg)
470 errModal.AddButtons([]string{"Cancel"})
471 errModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
472 ui.Pages.RemovePage("errModal")
474 ui.Pages.RemovePage("joinServer")
475 ui.Pages.AddPage("errModal", errModal, false, true)
478 ui.trackerList = ui.getTrackerList(listing)
479 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
481 AddItem("Settings", "", 's', func() {
482 ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, true)
484 AddItem("Quit", "", 'q', func() {
488 ui.Pages.AddPage("home", home, true, true)
490 // App level input capture
491 ui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
492 if event.Key() == tcell.KeyCtrlC {
493 ui.HLClient.Logger.Infow("Exiting")
498 if event.Key() == tcell.KeyCtrlL {
499 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
500 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
501 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
502 if key == tcell.KeyEscape {
503 ui.Pages.RemovePage("logs")
507 ui.Pages.AddPage("logs", ui.HLClient.DebugBuf.TextView, true, true)
512 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {