@ -22,9 +22,12 @@ var (
@@ -22,9 +22,12 @@ var (
channels [ ] keybase . Channel
stream = false
lastMessage keybase . ChatAPI
lastChat = ""
g * gocui . Gui
)
var config * Config
func main ( ) {
if ! k . LoggedIn {
fmt . Println ( "You are not logged in." )
@ -37,6 +40,7 @@ func main() {
@@ -37,6 +40,7 @@ func main() {
}
defer g . Close ( )
g . SetManagerFunc ( layout )
RunCommand ( "config" , "load" )
go populateList ( )
go updateChatWindow ( )
if len ( os . Args ) > 1 {
@ -72,7 +76,7 @@ func layout(g *gocui.Gui) error {
@@ -72,7 +76,7 @@ func layout(g *gocui.Gui) error {
feedView . Autoscroll = true
feedView . Wrap = true
feedView . Title = "Feed Window"
fmt . Fprintln ( feedView , "Feed Window - If you are mentioned or receive a PM it will show here" )
printInfo ( "Feed Window - If you are mentioned or receive a PM it will show here" )
}
if chatView , err2 := g . SetView ( "Chat" , maxX / 2 - maxX / 3 , maxY / 5 + 1 , maxX - 1 , maxY - 5 , 0 ) ; err2 != nil {
if ! gocui . IsUnknownView ( err2 ) {
@ -80,7 +84,9 @@ func layout(g *gocui.Gui) error {
@@ -80,7 +84,9 @@ func layout(g *gocui.Gui) error {
}
chatView . Autoscroll = true
chatView . Wrap = true
fmt . Fprintf ( chatView , "Welcome %s!\n\nYour chats will appear here.\nSupported commands are as follows:\n\n" , k . Username )
welcomeText := basicStyle . stylize ( "Welcome $USER!\n\nYour chats will appear here.\nSupported commands are as follows:\n" )
welcomeText = welcomeText . replace ( "$USER" , config . Colors . Message . Mention . stylize ( k . Username ) )
fmt . Fprintln ( chatView , welcomeText . string ( ) )
RunCommand ( "help" )
}
if inputView , err3 := g . SetView ( "Input" , maxX / 2 - maxX / 3 , maxY - 4 , maxX - 1 , maxY - 1 , 0 ) ; err3 != nil {
@ -92,7 +98,7 @@ func layout(g *gocui.Gui) error {
@@ -92,7 +98,7 @@ func layout(g *gocui.Gui) error {
}
inputView . Editable = true
inputView . Wrap = true
inputView . Title = fmt . Sprintf ( " Not in a chat - write `%sj` to join" , cmdPrefix )
inputView . Title = fmt . Sprintf ( " Not in a chat - write `%sj` to join" , config . Basics . C mdPrefix )
g . Cursor = true
}
if listView , err4 := g . SetView ( "List" , 0 , 0 , maxX / 2 - maxX / 3 - 1 , maxY - 1 , 0 ) ; err4 != nil {
@ -104,7 +110,68 @@ func layout(g *gocui.Gui) error {
@@ -104,7 +110,68 @@ func layout(g *gocui.Gui) error {
}
return nil
}
func scrollViewUp ( v * gocui . View ) error {
scrollView ( v , - 1 )
return nil
}
func scrollViewDown ( v * gocui . View ) error {
scrollView ( v , 1 )
return nil
}
func scrollView ( v * gocui . View , delta int ) error {
if v != nil {
_ , y := v . Size ( )
ox , oy := v . Origin ( )
if oy + delta > strings . Count ( v . ViewBuffer ( ) , "\n" ) - y {
v . Autoscroll = true
} else {
v . Autoscroll = false
if err := v . SetOrigin ( ox , oy + delta ) ; err != nil {
return err
}
}
}
return nil
}
func autoScrollView ( vn string ) error {
v , err := g . View ( vn )
if err != nil {
return err
} else if v != nil {
v . Autoscroll = true
}
return nil
}
func initKeybindings ( ) error {
if err := g . SetKeybinding ( "" , gocui . KeyPgup , gocui . ModNone ,
func ( g * gocui . Gui , v * gocui . View ) error {
cv , _ := g . View ( "Chat" )
err := scrollViewUp ( cv )
if err != nil {
return err
}
return nil
} ) ; err != nil {
return err
}
if err := g . SetKeybinding ( "" , gocui . KeyPgdn , gocui . ModNone ,
func ( g * gocui . Gui , v * gocui . View ) error {
cv , _ := g . View ( "Chat" )
err := scrollViewDown ( cv )
if err != nil {
return err
}
return nil
} ) ; err != nil {
return err
}
if err := g . SetKeybinding ( "" , gocui . KeyEsc , gocui . ModNone ,
func ( g * gocui . Gui , v * gocui . View ) error {
autoScrollView ( "Chat" )
return nil
} ) ; err != nil {
return err
}
if err := g . SetKeybinding ( "" , gocui . KeyCtrlC , gocui . ModNone ,
func ( g * gocui . Gui , v * gocui . View ) error {
input , err := getInputString ( "Input" )
@ -119,6 +186,13 @@ func initKeybindings() error {
@@ -119,6 +186,13 @@ func initKeybindings() error {
} ) ; err != nil {
return err
}
if err := g . SetKeybinding ( "" , gocui . KeyCtrlZ , gocui . ModNone ,
func ( g * gocui . Gui , v * gocui . View ) error {
cmdJoin ( [ ] string { "/join" , lastChat } )
return nil
} ) ; err != nil {
return err
}
if err := g . SetKeybinding ( "Edit" , gocui . KeyCtrlC , gocui . ModNone ,
func ( g * gocui . Gui , v * gocui . View ) error {
popupView ( "Chat" )
@ -176,20 +250,19 @@ func getViewTitle(viewName string) string {
@@ -176,20 +250,19 @@ func getViewTitle(viewName string) string {
view , err := g . View ( viewName )
if err != nil {
// in case there is active tab completion, filter that to just the view title and not the completion options.
printToView ( "Feed" , fmt . Sprintf ( "Error getting view title: %s" , err ) )
printError ( fmt . Sprintf ( "Error getting view title: %s" , err ) )
return ""
}
return strings . Split ( view . Title , "||" ) [ 0 ]
}
func popupView ( viewName string ) {
_ , err := g . SetCurrentView ( viewName )
if err != nil {
printToView ( "Feed" , fmt . Sprintf ( "%+v" , err ) )
printError ( fmt . Sprintf ( "%+v" , err ) )
}
_ , err = g . SetViewOnTop ( viewName )
if err != nil {
printToView ( "Feed" , fmt . Sprintf ( "%+v" , err ) )
printError ( fmt . Sprintf ( "%+v" , err ) )
}
g . Update ( func ( g * gocui . Gui ) error {
updatingView , err := g . View ( viewName )
@ -247,13 +320,35 @@ func writeToView(viewName string, message string) {
@@ -247,13 +320,35 @@ func writeToView(viewName string, message string) {
return nil
} )
}
// this removes formatting
func printError ( message string ) {
printErrorF ( message )
}
func printErrorF ( message string , parts ... StyledString ) {
printToView ( "Feed" , config . Colors . Feed . Error . sprintf ( removeFormatting ( message ) , parts ... ) . string ( ) )
}
// this removes formatting
func printInfo ( message string ) {
printInfoF ( message )
}
// this removes formatting
func printInfoF ( message string , parts ... StyledString ) {
printToView ( "Feed" , config . Colors . Feed . Basic . sprintf ( removeFormatting ( message ) , parts ... ) . string ( ) )
}
func printToView ( viewName string , message string ) {
g . Update ( func ( g * gocui . Gui ) error {
updatingView , err := g . View ( viewName )
if err != nil {
return err
} else {
if config . Basics . UnicodeEmojis {
message = emojiUnicodeConvert ( message )
}
fmt . Fprintf ( updatingView , "%s\n" , message )
}
return nil
} )
}
@ -287,13 +382,12 @@ func populateChat() {
@@ -287,13 +382,12 @@ func populateChat() {
chat = k . NewChat ( channel )
_ , err2 := chat . Read ( 2 )
if err2 != nil {
printToView ( "Feed" , fmt . Sprintf ( "%+v" , err ) )
printError ( fmt . Sprintf ( "%+v" , err ) )
return
}
go populateChat ( )
go generateChannelTabCompletionSlice ( )
return
}
var printMe [ ] string
var actuallyPrintMe string
@ -318,82 +412,129 @@ func populateChat() {
@@ -318,82 +412,129 @@ func populateChat() {
}
}
printToView ( "Chat" , actuallyPrintMe )
go populateList ( )
}
func populateList ( ) {
_ , maxY := g . Size ( )
if testVar , err := k . ChatList ( ) ; err != nil {
log . Printf ( "%+v" , err )
} else {
clearView ( "List" )
var recentPMs = fmt . Sprintf ( "%s---[PMs]---%s\n" , channelsHeaderColor , channelsColor )
var textBase = config . Colors . Channels . Basic . stylize ( "" )
var recentPMs = textBase . append ( config . Colors . Channels . Header . stylize ( "---[PMs]---\n" ) )
var recentPMsCount = 0
var recentChannels = fmt . Sprintf ( "%s---[Teams]---%s\n" , channelsHeaderColor , channelsColor )
var recentChannels = textBase . append ( config . Colors . Channels . Header . stylize ( "---[Teams]---\n" ) )
var recentChannelsCount = 0
for _ , s := range testVar . Result . Conversations {
channels = append ( channels , s . Channel )
if s . Channel . MembersType == keybase . TEAM {
recentChannelsCount ++
if recentChannelsCount <= ( ( maxY - 2 ) / 3 ) {
channel := fmt . Sprintf ( "%s\n\t#%s\n" , s . Channel . Name , s . Channel . TopicName )
if s . Unread {
recentChannels += fmt . Sprintf ( "%s*" , color ( 0 ) )
recentChannels = recentChannels . append ( config . Colors . Channels . Unread . stylize ( "*" + channel ) )
} else {
recentChannels = recentChannels . appendString ( channel )
}
recentChannels += fmt . Sprintf ( "%s\n\t#%s\n%s" , s . Channel . Name , s . Channel . TopicName , channelsColor )
}
} else {
recentPMsCount ++
if recentPMsCount <= ( ( maxY - 2 ) / 3 ) {
pmName := fmt . Sprintf ( "%s\n" , cleanChannelName ( s . Channel . Name ) )
if s . Unread {
recentChannels += fmt . Sprintf ( "%s*" , color ( 0 ) )
recentPMs = recentPMs . append ( config . Colors . Channels . Unread . stylize ( "*" + pmName ) )
} else {
recentPMs = recentPMs . appendString ( pmName )
}
recentPMs += fmt . Sprintf ( "%s\n%s" , cleanChannelName ( s . Channel . Name ) , channelsColor )
}
}
}
time . Sleep ( 1 * time . Millisecond )
printToView ( "List" , fmt . Sprintf ( "%s%s%s%s " , channelsColor , recentPMs , recentChannels , noColor ) )
go generateRecentTabCompletionSlice ( )
printToView ( "List" , fmt . Sprintf ( "%s%s" , recentPMs . string ( ) , recentChannels . string ( ) ) )
generateRecentTabCompletionSlice ( )
}
}
// End update/populate views automatically
// Formatting
func formatMessageBody ( body string ) StyledString {
output := config . Colors . Message . Body . stylize ( body )
output = colorReplaceMentionMe ( output )
output = output . colorRegex ( ` _[^_]*_ ` , config . Colors . Message . Body . withItalic ( ) )
output = output . colorRegex ( ` ~[^~]*~ ` , config . Colors . Message . Body . withStrikethrough ( ) )
output = output . colorRegex ( ` @[\w_]*(\.[\w_]+)* ` , config . Colors . Message . LinkKeybase )
// TODO change how bold, italic etc works, so it uses boldOn boldOff ([1m and [22m)
output = output . colorRegex ( ` \*[^\*]*\* ` , config . Colors . Message . Body . withBold ( ) )
output = output . replaceString ( "```" , "\n<code>\n" )
// TODO make background color cover whole line
output = output . colorRegex ( "<code>(.*\n)*<code>" , config . Colors . Message . Code )
output = output . colorRegex ( "`[^`]*`" , config . Colors . Message . Code )
// mention URL
output = output . colorRegex ( ` (https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=] { 1,256}\.[a-zA-Z0-9()] { 1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)) ` , config . Colors . Message . LinkURL )
return output
}
// TODO use this more
func formatChannel ( ch keybase . Channel ) StyledString {
return config . Colors . Message . LinkKeybase . stylize ( fmt . Sprintf ( "@%s#%s" , ch . Name , ch . TopicName ) )
}
func colorReplaceMentionMe ( msg StyledString ) StyledString {
return msg . colorRegex ( ` (@?\b ` + k . Username + ` \b) ` , config . Colors . Message . Mention )
}
func colorUsername ( username string ) StyledString {
var color = config . Colors . Message . SenderDefault
if username == k . Username {
color = config . Colors . Message . Mention
}
return color . stylize ( username )
}
func cleanChannelName ( c string ) string {
newChannelName := strings . Replace ( c , fmt . Sprintf ( "%s," , k . Username ) , "" , 1 )
return strings . Replace ( newChannelName , fmt . Sprintf ( ",%s" , k . Username ) , "" , 1 )
}
func formatOutput ( api keybase . ChatAPI ) string {
ret := ""
func formatMessage ( api keybase . ChatAPI , formatString string ) string {
ret := config . Colors . Message . Header . stylize ( "" )
msgType := api . Msg . Content . Type
switch msgType {
case "text" , "attachment" :
var c = messageHeaderColor
ret = colorText ( outputFormat , c , noColor )
ret = config . Colors . Message . Header . stylize ( formatString )
tm := time . Unix ( int64 ( api . Msg . SentAt ) , 0 )
var msg = api . Msg . Content . Text . Body
// mention teams or users
msg = colorRegex ( msg , ` (@\w*(\.\w+)*) ` , messageLinkColor , messageBodyColor )
// mention URL
msg = colorRegex ( msg , ` (https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=] { 1,256}\.[a-zA-Z0-9()] { 1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)) ` , messageLinkColor , messageBodyColor )
msg = colorText ( colorReplaceMentionMe ( msg , messageBodyColor ) , messageBodyColor , c )
var msg = formatMessageBody ( api . Msg . Content . Text . Body )
if msgType == "attachment" {
msg = fmt . Sprintf ( "%s\n%s" , api . Msg . Content . Attachment . Object . Title , colorText ( fmt . Sprintf ( "[Attachment: %s]" , api . Msg . Content . Attachment . Object . Filename ) , messageAttachmentColor , c ) )
}
user := colorUsername ( api . Msg . Sender . Username , c )
device := colorText ( api . Msg . Sender . DeviceName , messageSenderDeviceColor , c )
msgID := colorText ( fmt . Sprintf ( "%d" , api . Msg . ID ) , messageIdColor , c )
ts := colorText ( tm . Format ( timeFormat ) , messageTimeColor , c )
ret = strings . Replace ( ret , "$MSG" , msg , 1 )
ret = strings . Replace ( ret , "$USER" , user , 1 )
ret = strings . Replace ( ret , "$DEVICE" , device , 1 )
ret = strings . Replace ( ret , "$ID" , msgID , 1 )
ret = strings . Replace ( ret , "$TIME" , ts , 1 )
ret = strings . Replace ( ret , "$DATE" , colorText ( tm . Format ( dateFormat ) , messageTimeColor , c ) , 1 )
ret = strings . Replace ( ret , "```" , fmt . Sprintf ( "\n<code>\n" ) , - 1 )
msg = config . Colors . Message . Body . stylize ( "$TITLE\n$FILE" )
attachment := api . Msg . Content . Attachment
msg = msg . replaceString ( "$TITLE" , attachment . Object . Title )
msg = msg . replace ( "$FILE" , config . Colors . Message . Attachment . stylize ( fmt . Sprintf ( "[Attachment: %s]" , attachment . Object . Filename ) ) )
}
user := colorUsername ( api . Msg . Sender . Username )
device := config . Colors . Message . SenderDevice . stylize ( api . Msg . Sender . DeviceName )
msgID := config . Colors . Message . ID . stylize ( fmt . Sprintf ( "%d" , api . Msg . ID ) )
date := config . Colors . Message . Time . stylize ( tm . Format ( config . Formatting . DateFormat ) )
msgTime := config . Colors . Message . Time . stylize ( tm . Format ( config . Formatting . TimeFormat ) )
channelName := config . Colors . Message . ID . stylize ( fmt . Sprintf ( "@%s#%s" , api . Msg . Channel . Name , api . Msg . Channel . TopicName ) )
ret = ret . replace ( "$MSG" , msg )
ret = ret . replace ( "$USER" , user )
ret = ret . replace ( "$DEVICE" , device )
ret = ret . replace ( "$ID" , msgID )
ret = ret . replace ( "$TIME" , msgTime )
ret = ret . replace ( "$DATE" , date )
ret = ret . replace ( "$TEAM" , channelName )
}
return ret . string ( )
}
func formatOutput ( api keybase . ChatAPI ) string {
format := config . Formatting . OutputFormat
if stream {
format = config . Formatting . OutputStreamFormat
}
return ret
return formatMessage ( api , format )
}
// End formatting
@ -410,9 +551,7 @@ func handleMessage(api keybase.ChatAPI) {
@@ -410,9 +551,7 @@ func handleMessage(api keybase.ChatAPI) {
}
if api . Msg . Content . Type == "text" || api . Msg . Content . Type == "attachment" {
go populateList ( )
msgBody := api . Msg . Content . Text . Body
msgSender := api . Msg . Sender . Username
channelName := api . Msg . Channel . Name
if ! stream {
if msgSender != k . Username {
if api . Msg . Channel . MembersType == keybase . TEAM {
@ -421,7 +560,7 @@ func handleMessage(api keybase.ChatAPI) {
@@ -421,7 +560,7 @@ func handleMessage(api keybase.ChatAPI) {
if m . Text == k . Username {
// We are in a team
if topicName != channel . TopicName {
printToView ( "Feed" , fmt . Sprintf ( "[ %s#%s ] %s: %s" , channelName , topicName , msgSender , msgBody ) )
printInfo ( formatMessage ( api , config . Formatting . OutputMentionFormat ) )
fmt . Print ( "\a" )
}
@ -430,7 +569,7 @@ func handleMessage(api keybase.ChatAPI) {
@@ -430,7 +569,7 @@ func handleMessage(api keybase.ChatAPI) {
}
} else {
if msgSender != channel . Name {
printToView ( "Feed" , fmt . Sprintf ( "PM from @%s: %s" , cleanChannelName ( channelName ) , msgBody ) )
printInfo ( formatMessage ( api , config . Formatting . PMFormat ) )
fmt . Print ( "\a" )
}
@ -446,10 +585,9 @@ func handleMessage(api keybase.ChatAPI) {
@@ -446,10 +585,9 @@ func handleMessage(api keybase.ChatAPI) {
}
} else {
if api . Msg . Channel . MembersType == keybase . TEAM {
topicName := api . Msg . Channel . TopicName
printToView ( "Chat" , fmt . Sprintf ( "@%s#%s [%s]: %s" , channelName , topicName , msgSender , msgBody ) )
printToView ( "Chat" , formatOutput ( api ) )
} else {
printToView ( "Chat" , fmt . Sprintf ( "PM @%s [%s]: %s" , cleanChannelName ( channelName ) , msgSender , msgBody ) )
printToView ( "Chat" , formatMessage ( api , config . Formatting . PMFormat ) )
}
}
} else {
@ -483,8 +621,8 @@ func handleInput(viewName string) error {
@@ -483,8 +621,8 @@ func handleInput(viewName string) error {
if inputString == "" {
return nil
}
if strings . HasPrefix ( inputString , cmdPrefix ) {
cmd := deleteEmpty ( strings . Split ( inputString [ len ( cmdPrefix ) : ] , " " ) )
if strings . HasPrefix ( inputString , config . Basics . C mdPrefix ) {
cmd := deleteEmpty ( strings . Split ( inputString [ len ( config . Basics . C mdPrefix ) : ] , " " ) )
if len ( cmd ) < 1 {
return nil
}
@ -494,7 +632,7 @@ func handleInput(viewName string) error {
@@ -494,7 +632,7 @@ func handleInput(viewName string) error {
} else if cmd [ 0 ] == "q" || cmd [ 0 ] == "quit" {
return gocui . ErrQuit
} else {
printToView ( "Feed" , fmt . Sprintf ( "Command '%s' not recognized" , cmd [ 0 ] ) )
printError ( fmt . Sprintf ( "Command '%s' not recognized" , cmd [ 0 ] ) )
return nil
}
}
@ -503,6 +641,7 @@ func handleInput(viewName string) error {
@@ -503,6 +641,7 @@ func handleInput(viewName string) error {
cmd [ 0 ] = inputString [ : 1 ]
RunCommand ( cmd ... )
} else {
inputString = resolveRootEmojis ( inputString )
go sendChat ( inputString )
}
// restore any tab completion view titles on input commit
@ -514,10 +653,11 @@ func handleInput(viewName string) error {
@@ -514,10 +653,11 @@ func handleInput(viewName string) error {
return nil
}
func sendChat ( message string ) {
autoScrollView ( "Chat" )
chat := k . NewChat ( channel )
_ , err := chat . Send ( message )
if err != nil {
printToView ( "Feed" , fmt . Sprintf ( "There was an error %+v" , err ) )
printError ( fmt . Sprintf ( "There was an error %+v" , err ) )
}
}