package tui import ( "fmt" "regexp" "sort" "strings" "time" "github.com/gdamore/tcell/v2" "github.com/mrusme/superhighway84/models" "github.com/rivo/tview" ) var HEADER_LOGO = `[white] _ _ _ __ [-][hotpink]____ __ _ __ ___ ____[-] [teal] / / / // [-][hotpink]/ __/_ _____ ___ ____/ / (_)__ _/ / _ _____ ___ __( _ )/ / /[-] [teal] _\ _\_\_\\_[-][fuchsia]\ \/ // / _ \/ -_) __/ _ \/ / _ \/ _ \ |/|/ / _ \/ // / _ /_ _/[-] [darkcyan] / / / // [-][hotpink]/___/\_,_/ .__/\__/_/ /_//_/_/\_, /_//_/__,__/\_,_/\_, /\___/ /_/[-] [dimgray]%s[-] [hotpink] /_/ /___/ /___/[-] [yellow]%s[-] ` var STATS_TEMPLATE = `[gray]⦿ %d PEERS[-] [yellow]▲ %.2f[-] [gray]MB/s[-] [teal]▼ %.2f[-] [gray]MB/s[-] [yellow]▲ %.2f[-] [gray]MB[-] [teal]▼ %.2f[-] [gray]MB[-] ` var INFO_TEMPLATE = "%s" const ( COLOR_SUBJECT_UNREAD = "teal" COLOR_SUBJECT_READ = "white" ) var HELP_TEMPLATE = ` HELP! Default shortcuts: %s ` type GroupMapEntry struct { Index int } type Mainscreen struct { T *TUI Canvas *tview.Grid Header *tview.TextView Stats *tview.TextView Info *tview.TextView Footer *tview.TextView Groups *tview.List Articles *tview.List Preview *tview.TextView CurrentGroupSelected int CurrentArticleSelected int GroupsMap map[string]GroupMapEntry GroupsList []string ArticlesList []*models.Article MarkTimer *time.Timer ArticlesListView int8 } func(t *TUI) NewMainscreen() (*Mainscreen) { mainscreen := new(Mainscreen) mainscreen.T = t mainscreen.Groups = tview.NewList(). SetWrapAround(false). ShowSecondaryText(false). SetHighlightFullLine(true). SetMainTextColor(tcell.ColorWhite). SetSelectedBackgroundColor(tcell.ColorHotPink). SetSelectedTextColor(tcell.ColorWhite). SetSecondaryTextColor(tcell.ColorGray). SetChangedFunc(mainscreen.changeHandler("group")). SetSelectedFunc(mainscreen.selectHandler("group")) mainscreen.Groups. SetBorder(true). SetBorderAttributes(tcell.AttrNone). SetBorderColor(tcell.ColorTeal) mainscreen.Articles = tview.NewList(). SetWrapAround(false). ShowSecondaryText(true). SetHighlightFullLine(true). SetMainTextColor(tcell.ColorTeal). SetSelectedBackgroundColor(tcell.ColorHotPink). SetSelectedTextColor(tcell.ColorWhite). SetSecondaryTextColor(tcell.ColorGray). SetChangedFunc(mainscreen.changeHandler("article")). SetSelectedFunc(mainscreen.selectHandler("article")) mainscreen.Articles. SetBorder(true). SetBorderAttributes(tcell.AttrNone). SetBorderColor(tcell.ColorTeal) mainscreen.Preview = tview.NewTextView(). SetText(""). SetTextColor(tcell.ColorWhite). SetDynamicColors(true) mainscreen.Preview. SetBorder(true). SetBorderColor(tcell.ColorTeal) mainscreen.Header = tview.NewTextView(). SetText(HEADER_LOGO). SetTextColor(tcell.ColorHotPink). SetDynamicColors(true) mainscreen.Header.SetBorder(false) mainscreen.Stats = tview.NewTextView(). SetText(""). SetTextColor(tcell.ColorHotPink). SetDynamicColors(true) mainscreen.Stats.SetBorder(false) mainscreen.Info = tview.NewTextView(). SetText(""). SetTextColor(tcell.ColorHotPink). SetDynamicColors(true) mainscreen.Info.SetBorder(false). SetBorderPadding(0, 0, 1, 1) mainscreen.Footer = tview.NewTextView(). SetText("It really whips the llama's ass"). SetTextColor(tcell.ColorHotPink). SetTextAlign(tview.AlignRight) mainscreen.Footer.SetBorder(false). SetBorderPadding(0, 0, 1, 1) topRowGrid := tview.NewGrid(). SetColumns(30, 0, 14). AddItem(mainscreen.Header, 0, 0, 1, 2, 0, 0, false). AddItem(mainscreen.Stats, 0, 2, 1, 1, 0, 0, false) midRowGrid := tview.NewGrid(). SetColumns(-1, -5). // Group takes ~1/5 of the horizontal space available SetRows(-2, -3). // Preview is ~1/3 bigger than Articles AddItem(mainscreen.Groups, 0, 0, 2, 1, 0, 0, false). AddItem(mainscreen.Articles, 0, 1, 1, 1, 0, 0, false). AddItem(mainscreen.Preview, 1, 1, 1, 1, 0, 0, false) bottomRowGrid := tview.NewGrid(). SetColumns(5, 0, 0). AddItem(mainscreen.Info, 0, 0, 1, 1, 0, 0, false). AddItem(mainscreen.Footer, 0, 1, 1, 2, 0, 0, false) mainscreen.Canvas = tview.NewGrid(). SetRows(5, 0, 1). SetBorders(false). AddItem(topRowGrid, 0, 0, 1, 1, 0, 0, false). AddItem(midRowGrid, 1, 0, 1, 1, 0, 0, false). AddItem(bottomRowGrid, 2, 0, 1, 1, 0, 0, false) mainscreen.ArticlesListView = mainscreen.T.Config.ArticlesListView return mainscreen } func (mainscreen *Mainscreen) SetFooter(text string) { mainscreen.Footer.SetText(text) } func (mainscreen *Mainscreen) SetStats(stats map[string]int64) { peers := stats["peers"] totalIn := float64(stats["total_in"]) / 1024.0 / 1024.0 totalOut := float64(stats["total_out"]) / 1024.0 / 1024.0 rateIn := float64(stats["rate_in"]) / 1024.0 / 1024.0 rateOut := float64(stats["rate_out"]) / 1024.0 / 1024.0 mainscreen.Stats.SetText( fmt.Sprintf(STATS_TEMPLATE, peers, rateOut, rateIn, totalOut, totalIn, ), ) } func (mainscreen *Mainscreen) SetInfo(info map[string]string) { refresh := info["refresh"] mainscreen.Info.SetText( fmt.Sprintf(INFO_TEMPLATE, refresh, ), ) } func (mainscreen *Mainscreen) SetVersion(version string, versionLatest string) { v := version if version == "0.0.0" { v = "DeLorean @ 1.21 Gigawatts" } l := "" if versionLatest != version && version != "0.0.0" { l = fmt.Sprintf("%s update available!", versionLatest) } mainscreen.Header.SetText( fmt.Sprintf(HEADER_LOGO, v, l, ), ) } func (mainscreen *Mainscreen) GetCanvas() (tview.Primitive) { return mainscreen.Canvas } func (mainscreen *Mainscreen) GetDefaultFocus() (tview.Primitive) { return mainscreen.Articles } func(mainscreen *Mainscreen) addNodeToArticlesList(view int8, level int, articlesNode *[]*models.Article, selectedGroup int, previousGroupsList []string) { // fmt.Fprintf(os.Stderr, "%s Node has %d items\n", strings.Repeat(" ", level * 3), len(*articlesNode)) for i := 0; i < len(*articlesNode); i++ { article := (*articlesNode)[i] // fmt.Fprintf(os.Stderr, "%s Item has ID %s and is in reply of ID %s and has %d replies\n", strings.Repeat(" ", level * 3), article.ID, article.InReplyToID, len(article.Replies)) if selectedGroup == 0 || (selectedGroup != 0 && article.Newsgroup == previousGroupsList[selectedGroup]) { prefix := "" if view == 0 && level > 0 { if i < (len(*articlesNode) - 1) || len(article.Replies) > 0 { prefix = "[gray]├[-]" } else { prefix = "[gray]└[-]" } } prefixSub := " " if view == 0 && (len(article.Replies) > 0 || (level > 0 && i < (len(*articlesNode) - 1))) { prefixSub = "[gray]│[-]" } subjectColor := COLOR_SUBJECT_UNREAD if article.Read == true { subjectColor = COLOR_SUBJECT_READ } mainscreen.Articles.AddItem( fmt.Sprintf( "%s%s[%s]%s[-]", prefix, strings.Repeat(" ", level), subjectColor, article.Subject, ), fmt.Sprintf( "%s%s in %s by [darkgray]%s[-]", prefixSub, strings.Repeat(" ", level), article.Newsgroup, article.From, ), 0, nil) mainscreen.ArticlesList = append(mainscreen.ArticlesList, article) if view == 0 && len(article.Replies) > 0 { mainscreen.addNodeToArticlesList(view, (level + 1), &article.Replies, selectedGroup, previousGroupsList) } } if _, ok := mainscreen.GroupsMap[article.Newsgroup]; !ok { mainscreen.GroupsList = append(mainscreen.GroupsList, article.Newsgroup) mainscreen.GroupsMap[article.Newsgroup] = GroupMapEntry{ Index: 0, } } } } func(mainscreen *Mainscreen) Refresh() { selectedGroup := mainscreen.CurrentGroupSelected selectedArticle := mainscreen.CurrentArticleSelected previewLine, previewCol := mainscreen.Preview.GetScrollOffset() previousGroupsList := mainscreen.GroupsList mainscreen.GroupsList = []string{} // previousGroupsMap := mainscreen.GroupsMap mainscreen.GroupsMap = make(map[string]GroupMapEntry) mainscreen.Groups.Clear() mainscreen.ArticlesList = []*models.Article{} mainscreen.Articles.Clear() mainscreen.GroupsList = append(mainscreen.GroupsList, "*") mainscreen.GroupsMap["*"] = GroupMapEntry{ Index: 0, } var articlesSource *[]*models.Article switch(mainscreen.ArticlesListView) { case 0: articlesSource = mainscreen.T.ArticlesRoots case 1: articlesSource = mainscreen.T.ArticlesDatasource } mainscreen.addNodeToArticlesList(mainscreen.ArticlesListView, 0, articlesSource, selectedGroup, previousGroupsList) sort.Strings(mainscreen.GroupsList) for idx, group := range mainscreen.GroupsList { mainscreen.GroupsMap[group] = GroupMapEntry{ Index: idx, } mainscreen.Groups.AddItem(group, "", 0, nil) } mainscreen.Groups.SetCurrentItem(selectedGroup) mainscreen.Articles.SetCurrentItem(selectedArticle) mainscreen.changeHandler("group")(selectedGroup, "", "", 0) mainscreen.changeHandler("article")(selectedArticle, "", "", 0) mainscreen.Preview.ScrollTo(previewLine, previewCol) } func (mainscreen *Mainscreen) HandleInput(event *tcell.EventKey) (*tcell.EventKey) { action := mainscreen.T.getInputEvent(event) switch action { case "focus-groups": mainscreen.T.App.SetFocus(mainscreen.Groups) return nil case "focus-preview": mainscreen.T.App.SetFocus(mainscreen.Preview) return nil case "focus-articles": mainscreen.T.App.SetFocus(mainscreen.Articles) return nil case "article-new": mainscreen.submitNewArticle(mainscreen.GroupsList[mainscreen.CurrentGroupSelected]) return nil case "article-reply": mainscreen.replyToArticle(mainscreen.ArticlesList[mainscreen.CurrentArticleSelected]) return nil case "article-mark-all-read": mainscreen.markAllAsRead() return nil case "additional-key-down": mainscreen.T.App.QueueEvent(tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone)) return nil case "additional-key-up": mainscreen.T.App.QueueEvent(tcell.NewEventKey(tcell.KeyUp, 0, tcell.ModNone)) return nil case "additional-key-left": mainscreen.T.App.QueueEvent(tcell.NewEventKey(tcell.KeyLeft, 0, tcell.ModNone)) return nil case "additional-key-right": mainscreen.T.App.QueueEvent(tcell.NewEventKey(tcell.KeyRight, 0, tcell.ModNone)) return nil case "additional-key-home": mainscreen.T.App.QueueEvent(tcell.NewEventKey(tcell.KeyHome, 0, tcell.ModNone)) case "additional-key-end": mainscreen.T.App.QueueEvent(tcell.NewEventKey(tcell.KeyEnd, 0, tcell.ModNone)) case "help": mainscreen.showHelp() } return event } func(mainscreen *Mainscreen) changeHandler(item string)(func(int, string, string, rune)) { return func(index int, text string, secondaryText string, shortcut rune) { switch(item) { case "group": mainscreen.CurrentGroupSelected = index case "article": if index < 0 || index >= len(mainscreen.ArticlesList) { return } mainscreen.CurrentArticleSelected = index mainscreen.renderPreview(mainscreen.ArticlesList[index]) if mainscreen.MarkTimer != nil { mainscreen.MarkTimer.Stop() } mainscreen.MarkTimer = time.AfterFunc(time.Second * 2, func() { mainscreen.markAsRead(index, mainscreen.ArticlesList[index]) }) } } } func(mainscreen *Mainscreen) selectHandler(item string)(func(int, string, string, rune)) { return func(index int, text string, secondaryText string, shortcut rune) { switch(item) { case "group": mainscreen.Refresh() case "article": mainscreen.markAsRead(index, mainscreen.ArticlesList[index]) mainscreen.T.OpenArticle(mainscreen.ArticlesList[index], true) } } } func(mainscreen *Mainscreen) renderPreview(article *models.Article) { var m *regexp.Regexp body := article.Body // Removing GPG/PGP stuff until there is a prober validation for it m = regexp.MustCompile(`(?m)^(> ){0,1}-----BEGIN PGP SIGNED MESSAGE-----\n(> ){0,1}Hash:(.*)(\n( >){0,1}){1,2}`) body = m.ReplaceAllString(body, "") m = regexp.MustCompile(`(?sm)^(> ){0,1}-----BEGIN PGP SIGNATURE-----.*-----END PGP SIGNATURE-----$`) body = m.ReplaceAllString(body, "") // End GPG/PGP stuff m = regexp.MustCompile(`(?m)^>(.*)(\n){0,1}`) body = m.ReplaceAllString(body, "[gray]> $1[-]\n") mainscreen.Preview.SetText(fmt.Sprintf( "[gray]Date:[-] [darkgray]%s[-]\n[gray]Newsgroup:[-] [darkgray]%s[-]\n\n\n%s", MillisecondsToDate(article.Date), article.Newsgroup, body, )) mainscreen.Preview.ScrollToBeginning() } func(mainscreen *Mainscreen) markAsRead(index int, article *models.Article) { if article.Read == true { return } article.Read = true mainText, secondaryText := mainscreen.Articles.GetItemText(index) updatedMainText := strings.Replace( mainText, fmt.Sprintf("[%s]", COLOR_SUBJECT_UNREAD), fmt.Sprintf("[%s]", COLOR_SUBJECT_READ), 1, ) mainscreen.Articles.SetItemText(index, updatedMainText, secondaryText) mainscreen.T.Cache.StoreArticle(article) } func(mainscreen *Mainscreen) markAllAsRead() { for i := 0; i < len(mainscreen.ArticlesList); i++ { mainscreen.markAsRead(i, mainscreen.ArticlesList[i]) } } func(mainscreen *Mainscreen) submitNewArticle(group string) { newArticle := models.NewArticle() newArticle.Subject = "" newArticle.Newsgroup = group newArticle.From = mainscreen.T.Config.Profile.From newArticle.Organization = mainscreen.T.Config.Profile.Organization newArticle.Body = "" updatedNewArticle, err := mainscreen.T.OpenArticle(newArticle, false) if err != nil { mainscreen.T.ShowErrorModal(err.Error()) return } if strings.TrimSpace(updatedNewArticle.Body) == strings.TrimSpace(newArticle.Body) { return } if valid, err := updatedNewArticle.IsValid(); valid == false { mainscreen.T.ShowErrorModal(err.Error()) return } mainscreen.T.ShowModal( "Do you want to submit this new article?", map[string]ModalButton{ "(Y)es": { Rune: 'y', Callback: func() { if mainscreen.T.CallbackSubmitArticle != nil { mainscreen.T.CallbackSubmitArticle(&updatedNewArticle) } return }, }, "(N)ope": { Rune: 'n', Callback: func() { return }, }, }) } func(mainscreen *Mainscreen) replyToArticle(article *models.Article) { newArticle := models.NewArticle() newArticle.Subject = fmt.Sprintf("Re: %s", article.Subject) newArticle.InReplyToID = article.ID newArticle.Newsgroup = article.Newsgroup newArticle.From = mainscreen.T.Config.Profile.From newArticle.Organization = mainscreen.T.Config.Profile.Organization newArticle.Body = fmt.Sprintf("\nOn %s %s wrote:\n> %s", MillisecondsToDate(article.Date), article.From, strings.Replace(article.Body, "\n", "\n> ", -1)) updatedNewArticle, err := mainscreen.T.OpenArticle(newArticle, false) if err != nil { mainscreen.T.ShowErrorModal(err.Error()) return } if strings.TrimSpace(updatedNewArticle.Body) == strings.TrimSpace(newArticle.Body) { return } if valid, err := updatedNewArticle.IsValid(); valid == false { mainscreen.T.ShowErrorModal(err.Error()) return } mainscreen.T.ShowModal( "Do you want to submit this reply?", map[string]ModalButton{ "(Y)es": { Rune: 'y', Callback: func() { if mainscreen.T.CallbackSubmitArticle != nil { err = mainscreen.T.CallbackSubmitArticle(&updatedNewArticle) } return }, }, "(N)ope": { Rune: 'n', Callback: func() { return }, }, }, ) if err != nil { mainscreen.T.ShowErrorModal(err.Error()) return } return } func(mainscreen *Mainscreen) showHelp() { helpMessage := fmt.Sprintf(HELP_TEMPLATE, mainscreen.T.Config.ShortcutsReference) mainscreen.T.ShowHelpModal(helpMessage) return }