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("Name", server, 0, func(textToCheck string, lastChar rune) bool {
225 AddInputField("Server", server, 0, nil, nil).
226 AddInputField("Login", login, 0, nil, nil).
227 AddPasswordField("Password", password, 0, '*', nil).
228 AddCheckbox("Save", save, func(checked bool) {
229 ui.HLClient.Logger.Infow("saving bookmark")
230 // TODO: Implement bookmark saving
232 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())
233 out, err := yaml.Marshal(ui.HLClient.Pref)
238 err = os.WriteFile(ui.HLClient.cfgPath, out, 0666)
242 // Pref := ui.HLClient.Pref
244 AddButton("Cancel", func() {
245 ui.Pages.SwitchToPage(backPage)
247 AddButton("Connect", func() {
248 srvAddr := joinServerForm.GetFormItem(0).(*tview.InputField).GetText()
249 loginInput := joinServerForm.GetFormItem(1).(*tview.InputField).GetText()
250 err := ui.joinServer(
253 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
256 name = fmt.Sprintf("%s@%s", loginInput, srvAddr)
258 ui.HLClient.serverName = name
261 ui.HLClient.Logger.Errorw("login error", "err", err)
262 loginErrModal := tview.NewModal().
263 AddButtons([]string{"Oh no"}).
264 SetText(err.Error()).
265 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
266 ui.Pages.SwitchToPage(backPage)
269 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
273 if joinServerForm.GetFormItem(3).(*tview.Checkbox).IsChecked() {
274 // TODO: implement bookmark saving
278 joinServerForm.Box.SetBorder(true).SetTitle("| Connect |")
279 joinServerForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
280 if event.Key() == tcell.KeyEscape {
281 ui.Pages.SwitchToPage(backPage)
287 joinServerForm.SetFocus(5)
290 joinServerPage := tview.NewFlex().
291 AddItem(nil, 0, 1, false).
292 AddItem(tview.NewFlex().
293 SetDirection(tview.FlexRow).
294 AddItem(nil, 0, 1, false).
295 AddItem(joinServerForm, 14, 1, true).
296 AddItem(nil, 0, 1, false), 40, 1, true).
297 AddItem(nil, 0, 1, false)
299 return joinServerPage
302 func (ui *UI) renderServerUI() *tview.Flex {
303 ui.chatBox.SetText("") // clear any previously existing chatbox text
304 commandList := tview.NewTextView().SetDynamicColors(true)
306 SetText("[yellow]^n[-::]: Read News [yellow]^p[-::]: Post News\n[yellow]^l[-::]: View Logs [yellow]^f[-::]: View Files\n").
308 SetTitle("| Keyboard Shortcuts| ")
310 modal := tview.NewModal().
311 SetText("Disconnect from the server?").
312 AddButtons([]string{"Cancel", "Exit"}).
314 modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
315 if buttonIndex == 1 {
316 _ = ui.HLClient.Disconnect()
317 ui.Pages.RemovePage(pageServerUI)
318 ui.Pages.SwitchToPage("home")
320 ui.Pages.HidePage("modal")
324 serverUI := tview.NewFlex().
325 AddItem(tview.NewFlex().
326 SetDirection(tview.FlexRow).
327 AddItem(commandList, 4, 0, false).
328 AddItem(ui.chatBox, 0, 8, false).
329 AddItem(ui.chatInput, 3, 0, true), 0, 1, true).
330 AddItem(ui.userList, 25, 1, false)
331 serverUI.SetBorder(true).SetTitle("| Mobius - Connected to " + ui.HLClient.serverName + " |").SetTitleAlign(tview.AlignLeft)
332 serverUI.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
333 if event.Key() == tcell.KeyEscape {
334 ui.Pages.AddPage("modal", modal, false, true)
338 if event.Key() == tcell.KeyCtrlF {
339 if err := ui.HLClient.Send(*NewTransaction(TranGetFileNameList, nil)); err != nil {
340 ui.HLClient.Logger.Errorw("err", "err", err)
345 if event.Key() == tcell.KeyCtrlN {
346 if err := ui.HLClient.Send(*NewTransaction(TranGetMsgs, nil)); err != nil {
347 ui.HLClient.Logger.Errorw("err", "err", err)
352 if event.Key() == tcell.KeyCtrlP {
354 newsFlex := tview.NewFlex()
355 newsFlex.SetBorderPadding(0, 0, 1, 1)
356 newsPostTextArea := tview.NewTextView()
357 newsPostTextArea.SetBackgroundColor(tcell.ColorDarkSlateGrey)
358 newsPostTextArea.SetChangedFunc(func() {
359 ui.App.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
362 newsPostForm := tview.NewForm().
363 SetButtonsAlign(tview.AlignRight).
364 // AddButton("Cancel", nil). // TODO: implement cancel button behavior
365 AddButton("Send", nil)
366 newsPostForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
368 case tcell.KeyEscape:
369 ui.Pages.RemovePage("newsInput")
371 ui.App.SetFocus(newsPostTextArea)
373 newsText := strings.ReplaceAll(newsPostTextArea.GetText(true), "\n", "\r")
374 if len(newsText) == 0 {
377 err := ui.HLClient.Send(
378 *NewTransaction(TranOldPostNews, nil,
379 NewField(FieldData, []byte(newsText)),
383 ui.HLClient.Logger.Errorw("Error posting news", "err", err)
384 // TODO: display errModal to user
386 ui.Pages.RemovePage("newsInput")
393 SetDirection(tview.FlexRow).
395 SetTitle("| Post Message |")
397 newsPostTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
399 case tcell.KeyEscape:
400 ui.Pages.RemovePage("newsInput")
402 ui.App.SetFocus(newsPostForm)
404 _, _ = fmt.Fprintf(newsPostTextArea, "\n")
406 const windowsBackspaceRune = 8
407 const macBackspaceRune = 127
408 switch event.Rune() {
409 case macBackspaceRune, windowsBackspaceRune:
410 curTxt := newsPostTextArea.GetText(true)
412 curTxt = curTxt[:len(curTxt)-1]
413 newsPostTextArea.SetText(curTxt)
416 _, _ = fmt.Fprintf(newsPostTextArea, string(event.Rune()))
423 newsFlex.AddItem(newsPostTextArea, 10, 0, true)
424 newsFlex.AddItem(newsPostForm, 3, 0, false)
426 newsPostPage := tview.NewFlex().
427 AddItem(nil, 0, 1, false).
428 AddItem(tview.NewFlex().
429 SetDirection(tview.FlexRow).
430 AddItem(nil, 0, 1, false).
431 AddItem(newsFlex, 15, 1, true).
432 // AddItem(newsPostForm, 3, 0, false).
433 AddItem(nil, 0, 1, false), 40, 1, false).
434 AddItem(nil, 0, 1, false)
436 ui.Pages.AddPage("newsInput", newsPostPage, true, true)
437 ui.App.SetFocus(newsPostTextArea)
445 func (ui *UI) Start() {
446 home := tview.NewFlex().SetDirection(tview.FlexRow)
447 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
448 mainMenu := tview.NewList()
450 bannerItem := tview.NewTextView().
451 SetText(randomBanner()).
452 SetDynamicColors(true).
453 SetTextAlign(tview.AlignCenter)
456 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
458 home.AddItem(tview.NewFlex().
459 AddItem(nil, 0, 1, false).
460 AddItem(mainMenu, 0, 1, true).
461 AddItem(nil, 0, 1, false),
465 mainMenu.AddItem("Join Server", "", 'j', func() {
466 joinServerPage := ui.renderJoinServerForm("", "", GuestAccount, "", "home", false, false)
467 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
469 AddItem("Bookmarks", "", 'b', func() {
470 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
472 AddItem("Browse Tracker", "", 't', func() {
473 ui.trackerList = ui.getTrackerList()
474 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
476 AddItem("Settings", "", 's', func() {
477 ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, true)
479 AddItem("Quit", "", 'q', func() {
483 ui.Pages.AddPage("home", home, true, true)
485 // App level input capture
486 ui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
487 if event.Key() == tcell.KeyCtrlC {
488 ui.HLClient.Logger.Infow("Exiting")
493 if event.Key() == tcell.KeyCtrlL {
494 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
495 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
496 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
497 if key == tcell.KeyEscape {
498 ui.Pages.RemovePage("logs")
502 ui.Pages.AddPage("logs", ui.HLClient.DebugBuf.TextView, true, true)
507 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {