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 ] ▲ % .2 f [ - ] [ gray ] MB / s [ - ]
[ teal ] ▼ % .2 f [ - ] [ gray ] MB / s [ - ]
[ yellow ] ▲ % .2 f [ - ] [ gray ] MB [ - ]
[ teal ] ▼ % .2 f [ - ] [ 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
}