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() *tview.List {
106 listing, err := GetListing(ui.HLClient.Pref.Tracker)
111 list := tview.NewList()
112 list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
113 if event.Key() == tcell.KeyEsc {
114 ui.Pages.SwitchToPage("home")
118 list.Box.SetBorder(true).SetTitle("| Servers |")
120 shortcut := 97 // rune for "a"
121 for i, srv := range listing {
124 list.AddItem(string(srv.Name), string(srv.Description), rune(shortcut+i), func() {
125 ui.Pages.RemovePage("joinServer")
127 newJS := ui.renderJoinServerForm(string(srvName), addr, GuestAccount, "", trackerListPage, false, true)
129 ui.Pages.AddPage("joinServer", newJS, true, true)
130 ui.Pages.ShowPage("joinServer")
137 func (ui *UI) renderSettingsForm() *tview.Flex {
138 iconStr := strconv.Itoa(ui.HLClient.Pref.IconID)
139 settingsForm := tview.NewForm()
140 settingsForm.AddInputField("Your Name", ui.HLClient.Pref.Username, 0, nil, nil)
141 settingsForm.AddInputField("IconID", iconStr, 0, func(idStr string, _ rune) bool {
142 _, err := strconv.Atoi(idStr)
145 settingsForm.AddInputField("Tracker", ui.HLClient.Pref.Tracker, 0, nil, nil)
146 settingsForm.AddCheckbox("Enable Terminal Bell", ui.HLClient.Pref.EnableBell, nil)
147 settingsForm.AddButton("Save", func() {
148 usernameInput := settingsForm.GetFormItem(0).(*tview.InputField).GetText()
149 if len(usernameInput) == 0 {
150 usernameInput = "unnamed"
152 ui.HLClient.Pref.Username = usernameInput
153 iconStr = settingsForm.GetFormItem(1).(*tview.InputField).GetText()
154 ui.HLClient.Pref.IconID, _ = strconv.Atoi(iconStr)
155 ui.HLClient.Pref.Tracker = settingsForm.GetFormItem(2).(*tview.InputField).GetText()
156 ui.HLClient.Pref.EnableBell = settingsForm.GetFormItem(3).(*tview.Checkbox).IsChecked()
158 out, err := yaml.Marshal(&ui.HLClient.Pref)
163 err = os.WriteFile(ui.HLClient.cfgPath, out, 0666)
165 println(ui.HLClient.cfgPath)
168 ui.Pages.RemovePage("settings")
170 settingsForm.SetBorder(true)
171 settingsForm.SetCancelFunc(func() {
172 ui.Pages.RemovePage("settings")
174 settingsPage := tview.NewFlex().SetDirection(tview.FlexRow)
175 settingsPage.Box.SetBorder(true).SetTitle("Settings")
176 settingsPage.AddItem(settingsForm, 0, 1, true)
178 centerFlex := tview.NewFlex().
179 AddItem(nil, 0, 1, false).
180 AddItem(tview.NewFlex().
181 SetDirection(tview.FlexRow).
182 AddItem(nil, 0, 1, false).
183 AddItem(settingsForm, 15, 1, true).
184 AddItem(nil, 0, 1, false), 40, 1, true).
185 AddItem(nil, 0, 1, false)
190 func (ui *UI) joinServer(addr, login, password string) error {
191 // append default port to address if no port supplied
192 if len(strings.Split(addr, ":")) == 1 {
195 if err := ui.HLClient.Connect(addr, login, password); err != nil {
196 return fmt.Errorf("Error joining server: %v\n", err)
200 if err := ui.HLClient.HandleTransactions(); err != nil {
201 ui.Pages.SwitchToPage("home")
204 loginErrModal := tview.NewModal().
205 AddButtons([]string{"Ok"}).
206 SetText("The server connection has closed.").
207 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
208 ui.Pages.SwitchToPage("home")
210 loginErrModal.Box.SetTitle("Server Connection Error")
212 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
219 func (ui *UI) renderJoinServerForm(name, server, login, password, backPage string, save, defaultConnect bool) *tview.Flex {
220 joinServerForm := tview.NewForm()
222 AddInputField("Server", server, 0, nil, nil).
223 AddInputField("Login", login, 0, nil, nil).
224 AddPasswordField("Password", password, 0, '*', nil).
225 AddCheckbox("Save", save, func(checked bool) {
226 ui.HLClient.Pref.AddBookmark(
227 joinServerForm.GetFormItem(0).(*tview.InputField).GetText(),
228 joinServerForm.GetFormItem(0).(*tview.InputField).GetText(),
229 joinServerForm.GetFormItem(1).(*tview.InputField).GetText(),
230 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
233 out, err := yaml.Marshal(ui.HLClient.Pref)
238 err = os.WriteFile(ui.HLClient.cfgPath, out, 0666)
243 AddButton("Cancel", func() {
244 ui.Pages.SwitchToPage(backPage)
246 AddButton("Connect", func() {
247 srvAddr := joinServerForm.GetFormItem(0).(*tview.InputField).GetText()
248 loginInput := joinServerForm.GetFormItem(1).(*tview.InputField).GetText()
249 err := ui.joinServer(
252 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
255 name = fmt.Sprintf("%s@%s", loginInput, srvAddr)
257 ui.HLClient.serverName = name
260 ui.HLClient.Logger.Errorw("login error", "err", err)
261 loginErrModal := tview.NewModal().
262 AddButtons([]string{"Oh no"}).
263 SetText(err.Error()).
264 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
265 ui.Pages.SwitchToPage(backPage)
268 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
272 if joinServerForm.GetFormItem(3).(*tview.Checkbox).IsChecked() {
273 // TODO: implement bookmark saving
277 joinServerForm.Box.SetBorder(true).SetTitle("| Connect |")
278 joinServerForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
279 if event.Key() == tcell.KeyEscape {
280 ui.Pages.SwitchToPage(backPage)
286 joinServerForm.SetFocus(5)
289 joinServerPage := tview.NewFlex().
290 AddItem(nil, 0, 1, false).
291 AddItem(tview.NewFlex().
292 SetDirection(tview.FlexRow).
293 AddItem(nil, 0, 1, false).
294 AddItem(joinServerForm, 14, 1, true).
295 AddItem(nil, 0, 1, false), 40, 1, true).
296 AddItem(nil, 0, 1, false)
298 return joinServerPage
301 func (ui *UI) renderServerUI() *tview.Flex {
302 ui.chatBox.SetText("") // clear any previously existing chatbox text
303 commandList := tview.NewTextView().SetDynamicColors(true)
305 SetText("[yellow]^n[-::]: Read News [yellow]^p[-::]: Post News\n[yellow]^l[-::]: View Logs [yellow]^f[-::]: View Files\n").
307 SetTitle("| Keyboard Shortcuts| ")
309 modal := tview.NewModal().
310 SetText("Disconnect from the server?").
311 AddButtons([]string{"Cancel", "Exit"}).
313 modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
314 if buttonIndex == 1 {
315 _ = ui.HLClient.Disconnect()
316 ui.Pages.RemovePage(pageServerUI)
317 ui.Pages.SwitchToPage("home")
319 ui.Pages.HidePage("modal")
323 serverUI := tview.NewFlex().
324 AddItem(tview.NewFlex().
325 SetDirection(tview.FlexRow).
326 AddItem(commandList, 4, 0, false).
327 AddItem(ui.chatBox, 0, 8, false).
328 AddItem(ui.chatInput, 3, 0, true), 0, 1, true).
329 AddItem(ui.userList, 25, 1, false)
330 serverUI.SetBorder(true).SetTitle("| Mobius - Connected to " + ui.HLClient.serverName + " |").SetTitleAlign(tview.AlignLeft)
331 serverUI.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
332 if event.Key() == tcell.KeyEscape {
333 ui.Pages.AddPage("modal", modal, false, true)
337 if event.Key() == tcell.KeyCtrlF {
338 if err := ui.HLClient.Send(*NewTransaction(TranGetFileNameList, nil)); err != nil {
339 ui.HLClient.Logger.Errorw("err", "err", err)
344 if event.Key() == tcell.KeyCtrlN {
345 if err := ui.HLClient.Send(*NewTransaction(TranGetMsgs, nil)); err != nil {
346 ui.HLClient.Logger.Errorw("err", "err", err)
351 if event.Key() == tcell.KeyCtrlP {
352 newsFlex := tview.NewFlex()
353 newsFlex.SetBorderPadding(0, 0, 1, 1)
354 newsPostTextArea := tview.NewTextView()
355 newsPostTextArea.SetBackgroundColor(tcell.ColorDarkSlateGrey)
356 newsPostTextArea.SetChangedFunc(func() {
357 ui.App.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
360 newsPostForm := tview.NewForm().
361 SetButtonsAlign(tview.AlignRight).
362 // AddButton("Cancel", nil). // TODO: implement cancel button behavior
363 AddButton("Send", nil)
364 newsPostForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
366 case tcell.KeyEscape:
367 ui.Pages.RemovePage("newsInput")
369 ui.App.SetFocus(newsPostTextArea)
371 newsText := strings.ReplaceAll(newsPostTextArea.GetText(true), "\n", "\r")
372 if len(newsText) == 0 {
375 err := ui.HLClient.Send(
376 *NewTransaction(TranOldPostNews, nil,
377 NewField(FieldData, []byte(newsText)),
381 ui.HLClient.Logger.Errorw("Error posting news", "err", err)
382 // TODO: display errModal to user
384 ui.Pages.RemovePage("newsInput")
391 SetDirection(tview.FlexRow).
393 SetTitle("| Post Message |")
395 newsPostTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
397 case tcell.KeyEscape:
398 ui.Pages.RemovePage("newsInput")
400 ui.App.SetFocus(newsPostForm)
402 _, _ = fmt.Fprintf(newsPostTextArea, "\n")
404 const windowsBackspaceRune = 8
405 const macBackspaceRune = 127
406 switch event.Rune() {
407 case macBackspaceRune, windowsBackspaceRune:
408 curTxt := newsPostTextArea.GetText(true)
410 curTxt = curTxt[:len(curTxt)-1]
411 newsPostTextArea.SetText(curTxt)
414 _, _ = fmt.Fprintf(newsPostTextArea, string(event.Rune()))
421 newsFlex.AddItem(newsPostTextArea, 10, 0, true)
422 newsFlex.AddItem(newsPostForm, 3, 0, false)
424 newsPostPage := tview.NewFlex().
425 AddItem(nil, 0, 1, false).
426 AddItem(tview.NewFlex().
427 SetDirection(tview.FlexRow).
428 AddItem(nil, 0, 1, false).
429 AddItem(newsFlex, 15, 1, true).
430 // AddItem(newsPostForm, 3, 0, false).
431 AddItem(nil, 0, 1, false), 40, 1, false).
432 AddItem(nil, 0, 1, false)
434 ui.Pages.AddPage("newsInput", newsPostPage, true, true)
435 ui.App.SetFocus(newsPostTextArea)
443 func (ui *UI) Start() {
444 home := tview.NewFlex().SetDirection(tview.FlexRow)
445 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
446 mainMenu := tview.NewList()
448 bannerItem := tview.NewTextView().
449 SetText(randomBanner()).
450 SetDynamicColors(true).
451 SetTextAlign(tview.AlignCenter)
454 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
456 home.AddItem(tview.NewFlex().
457 AddItem(nil, 0, 1, false).
458 AddItem(mainMenu, 0, 1, true).
459 AddItem(nil, 0, 1, false),
463 mainMenu.AddItem("Join Server", "", 'j', func() {
464 joinServerPage := ui.renderJoinServerForm("", "", GuestAccount, "", "home", false, false)
465 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
467 AddItem("Bookmarks", "", 'b', func() {
468 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
470 AddItem("Browse Tracker", "", 't', func() {
471 ui.trackerList = ui.getTrackerList()
472 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
474 AddItem("Settings", "", 's', func() {
475 ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, true)
477 AddItem("Quit", "", 'q', func() {
481 ui.Pages.AddPage("home", home, true, true)
483 // App level input capture
484 ui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
485 if event.Key() == tcell.KeyCtrlC {
486 ui.HLClient.Logger.Infow("Exiting")
491 if event.Key() == tcell.KeyCtrlL {
492 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
493 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
494 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
495 if key == tcell.KeyEscape {
496 ui.Pages.RemovePage("logs")
500 ui.Pages.AddPage("logs", ui.HLClient.DebugBuf.TextView, true, true)
505 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {