@ -22,9 +22,12 @@ var (
channels [ ] keybase . Channel
channels [ ] keybase . Channel
stream = false
stream = false
lastMessage keybase . ChatAPI
lastMessage keybase . ChatAPI
lastChat = ""
g * gocui . Gui
g * gocui . Gui
)
)
var config * Config
func main ( ) {
func main ( ) {
if ! k . LoggedIn {
if ! k . LoggedIn {
fmt . Println ( "You are not logged in." )
fmt . Println ( "You are not logged in." )
@ -37,6 +40,7 @@ func main() {
}
}
defer g . Close ( )
defer g . Close ( )
g . SetManagerFunc ( layout )
g . SetManagerFunc ( layout )
RunCommand ( "config" , "load" )
go populateList ( )
go populateList ( )
go updateChatWindow ( )
go updateChatWindow ( )
if len ( os . Args ) > 1 {
if len ( os . Args ) > 1 {
@ -81,7 +85,7 @@ func layout(g *gocui.Gui) error {
chatView . Autoscroll = true
chatView . Autoscroll = true
chatView . Wrap = true
chatView . Wrap = true
welcomeText := basicStyle . stylize ( "Welcome $USER!\n\nYour chats will appear here.\nSupported commands are as follows:\n" )
welcomeText := basicStyle . stylize ( "Welcome $USER!\n\nYour chats will appear here.\nSupported commands are as follows:\n" )
welcomeText = welcomeText . replace ( "$USER" , mentionColor . stylize ( k . Username ) )
welcomeText = welcomeText . replace ( "$USER" , config . Colors . Message . Mention . stylize ( k . Username ) )
fmt . Fprintln ( chatView , welcomeText . string ( ) )
fmt . Fprintln ( chatView , welcomeText . string ( ) )
RunCommand ( "help" )
RunCommand ( "help" )
}
}
@ -94,7 +98,7 @@ func layout(g *gocui.Gui) error {
}
}
inputView . Editable = true
inputView . Editable = true
inputView . Wrap = 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
g . Cursor = true
}
}
if listView , err4 := g . SetView ( "List" , 0 , 0 , maxX / 2 - maxX / 3 - 1 , maxY - 1 , 0 ) ; err4 != nil {
if listView , err4 := g . SetView ( "List" , 0 , 0 , maxX / 2 - maxX / 3 - 1 , maxY - 1 , 0 ) ; err4 != nil {
@ -106,7 +110,68 @@ func layout(g *gocui.Gui) error {
}
}
return nil
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 {
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 ,
if err := g . SetKeybinding ( "" , gocui . KeyCtrlC , gocui . ModNone ,
func ( g * gocui . Gui , v * gocui . View ) error {
func ( g * gocui . Gui , v * gocui . View ) error {
input , err := getInputString ( "Input" )
input , err := getInputString ( "Input" )
@ -121,6 +186,13 @@ func initKeybindings() error {
} ) ; err != nil {
} ) ; err != nil {
return err
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 ,
if err := g . SetKeybinding ( "Edit" , gocui . KeyCtrlC , gocui . ModNone ,
func ( g * gocui . Gui , v * gocui . View ) error {
func ( g * gocui . Gui , v * gocui . View ) error {
popupView ( "Chat" )
popupView ( "Chat" )
@ -254,7 +326,7 @@ func printError(message string) {
printErrorF ( message )
printErrorF ( message )
}
}
func printErrorF ( message string , parts ... StyledString ) {
func printErrorF ( message string , parts ... StyledString ) {
printToView ( "Feed" , errorCol or. sprintf ( removeFormatting ( message ) , parts ... ) . string ( ) )
printToView ( "Feed" , config . Colors . Feed . Err or. sprintf ( removeFormatting ( message ) , parts ... ) . string ( ) )
}
}
// this removes formatting
// this removes formatting
@ -264,15 +336,19 @@ func printInfo(message string) {
// this removes formatting
// this removes formatting
func printInfoF ( message string , parts ... StyledString ) {
func printInfoF ( message string , parts ... StyledString ) {
printToView ( "Feed" , feedColor . sprintf ( removeFormatting ( message ) , parts ... ) . string ( ) )
printToView ( "Feed" , config . Colors . Feed . Basic . sprintf ( removeFormatting ( message ) , parts ... ) . string ( ) )
}
}
func printToView ( viewName string , message string ) {
func printToView ( viewName string , message string ) {
g . Update ( func ( g * gocui . Gui ) error {
g . Update ( func ( g * gocui . Gui ) error {
updatingView , err := g . View ( viewName )
updatingView , err := g . View ( viewName )
if err != nil {
if err != nil {
return err
return err
} else {
if config . Basics . UnicodeEmojis {
message = emojiUnicodeConvert ( message )
}
fmt . Fprintf ( updatingView , "%s\n" , message )
}
}
fmt . Fprintf ( updatingView , "%s\n" , message )
return nil
return nil
} )
} )
}
}
@ -336,7 +412,7 @@ func populateChat() {
}
}
}
}
printToView ( "Chat" , actuallyPrintMe )
printToView ( "Chat" , actuallyPrintMe )
go populateList ( )
}
}
func populateList ( ) {
func populateList ( ) {
_ , maxY := g . Size ( )
_ , maxY := g . Size ( )
@ -344,10 +420,10 @@ func populateList() {
log . Printf ( "%+v" , err )
log . Printf ( "%+v" , err )
} else {
} else {
clearView ( "List" )
clearView ( "List" )
var textBase = channelsColor . stylize ( "" )
var textBase = config . Colors . Channels . Basic . stylize ( "" )
var recentPMs = textBase . append ( channelsHeaderColo r . stylize ( "---[PMs]---\n" ) )
var recentPMs = textBase . append ( config . Colors . C hannels . Header . stylize ( "---[PMs]---\n" ) )
var recentPMsCount = 0
var recentPMsCount = 0
var recentChannels = textBase . append ( channelsHeaderColo r . stylize ( "---[Teams]---\n" ) )
var recentChannels = textBase . append ( config . Colors . C hannels . Header . stylize ( "---[Teams]---\n" ) )
var recentChannelsCount = 0
var recentChannelsCount = 0
for _ , s := range testVar . Result . Conversations {
for _ , s := range testVar . Result . Conversations {
channels = append ( channels , s . Channel )
channels = append ( channels , s . Channel )
@ -356,7 +432,7 @@ func populateList() {
if recentChannelsCount <= ( ( maxY - 2 ) / 3 ) {
if recentChannelsCount <= ( ( maxY - 2 ) / 3 ) {
channel := fmt . Sprintf ( "%s\n\t#%s\n" , s . Channel . Name , s . Channel . TopicName )
channel := fmt . Sprintf ( "%s\n\t#%s\n" , s . Channel . Name , s . Channel . TopicName )
if s . Unread {
if s . Unread {
recentChannels = recentChannels . append ( channelUnreadColor . stylize ( "*" + channel ) )
recentChannels = recentChannels . append ( config . Colors . C hannels . Unread . stylize ( "*" + channel ) )
} else {
} else {
recentChannels = recentChannels . appendString ( channel )
recentChannels = recentChannels . appendString ( channel )
}
}
@ -366,7 +442,7 @@ func populateList() {
if recentPMsCount <= ( ( maxY - 2 ) / 3 ) {
if recentPMsCount <= ( ( maxY - 2 ) / 3 ) {
pmName := fmt . Sprintf ( "%s\n" , cleanChannelName ( s . Channel . Name ) )
pmName := fmt . Sprintf ( "%s\n" , cleanChannelName ( s . Channel . Name ) )
if s . Unread {
if s . Unread {
recentPMs = recentPMs . append ( channelUnreadColor . stylize ( "*" + pmName ) )
recentPMs = recentPMs . append ( config . Colors . C hannels . Unread . stylize ( "*" + pmName ) )
} else {
} else {
recentPMs = recentPMs . appendString ( pmName )
recentPMs = recentPMs . appendString ( pmName )
}
}
@ -383,35 +459,35 @@ func populateList() {
// Formatting
// Formatting
func formatMessageBody ( body string ) StyledString {
func formatMessageBody ( body string ) StyledString {
output := messageBodyColor . stylize ( body )
output := config . Colors . Message . Body . stylize ( body )
output = colorReplaceMentionMe ( output )
output = colorReplaceMentionMe ( output )
output = output . colorRegex ( ` _[^_]*_ ` , messageBodyColor . withItalic ( ) )
output = output . colorRegex ( ` _[^_]*_ ` , config . Colors . Message . Body . withItalic ( ) )
output = output . colorRegex ( ` ~[^~]*~ ` , messageBodyColor . withStrikethrough ( ) )
output = output . colorRegex ( ` ~[^~]*~ ` , config . Colors . Message . Body . withStrikethrough ( ) )
output = output . colorRegex ( ` @[\w_]*(\.[\w_]+)* ` , messageLinkKeybaseColor )
output = output . colorRegex ( ` @[\w_]*(\.[\w_]+)* ` , config . Colors . Message . LinkKeybase )
// TODO change how bold, italic etc works, so it uses boldOn boldOff ([1m and [22m)
// TODO change how bold, italic etc works, so it uses boldOn boldOff ([1m and [22m)
output = output . colorRegex ( ` \*[^\*]*\* ` , messageBodyColor . withBold ( ) )
output = output . colorRegex ( ` \*[^\*]*\* ` , config . Colors . Message . Body . withBold ( ) )
output = output . replaceString ( "```" , "<code>" )
output = output . replaceString ( "```" , "\n <code>\n " )
// TODO make background color cover whole line
// TODO make background color cover whole line
output = output . colorRegex ( "<code>(.*\n)*<code>" , messageCodeColor )
output = output . colorRegex ( "<code>(.*\n)*<code>" , config . Colors . Message . Code )
output = output . colorRegex ( "`[^`]*`" , messageCodeColor )
output = output . colorRegex ( "`[^`]*`" , config . Colors . Message . Code )
// mention URL
// mention URL
output = output . colorRegex ( ` (https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=] { 1,256}\.[a-zA-Z0-9()] { 1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)) ` , messageLinkURLColor )
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
return output
}
}
// TODO use this more
// TODO use this more
func formatChannel ( ch keybase . Channel ) StyledString {
func formatChannel ( ch keybase . Channel ) StyledString {
return messageLinkKeybaseColor . stylize ( fmt . Sprintf ( "@%s#%s" , ch . Name , ch . TopicName ) )
return config . Colors . Message . LinkKeybase . stylize ( fmt . Sprintf ( "@%s#%s" , ch . Name , ch . TopicName ) )
}
}
func colorReplaceMentionMe ( msg StyledString ) StyledString {
func colorReplaceMentionMe ( msg StyledString ) StyledString {
return msg . colorRegex ( "(@?" + k . Username + ")" , mentionColor )
return msg . colorRegex ( ` (@?\b ` + k . Username + ` \b) ` , config . Colors . Message . Mention )
}
}
func colorUsername ( username string ) StyledString {
func colorUsername ( username string ) StyledString {
var color = messageSenderDefaultColor
var color = config . Colors . Message . SenderDefault
if username == k . Username {
if username == k . Username {
color = mentionColor
color = config . Colors . Message . Mention
}
}
return color . stylize ( username )
return color . stylize ( username )
}
}
@ -422,27 +498,27 @@ func cleanChannelName(c string) string {
}
}
func formatMessage ( api keybase . ChatAPI , formatString string ) string {
func formatMessage ( api keybase . ChatAPI , formatString string ) string {
ret := messageHeaderColo r. stylize ( "" )
ret := config . Colors . Message . Heade r. stylize ( "" )
msgType := api . Msg . Content . Type
msgType := api . Msg . Content . Type
switch msgType {
switch msgType {
case "text" , "attachment" :
case "text" , "attachment" :
ret = messageHeaderColo r. stylize ( formatString )
ret = config . Colors . Message . Heade r. stylize ( formatString )
tm := time . Unix ( int64 ( api . Msg . SentAt ) , 0 )
tm := time . Unix ( int64 ( api . Msg . SentAt ) , 0 )
var msg = formatMessageBody ( api . Msg . Content . Text . Body )
var msg = formatMessageBody ( api . Msg . Content . Text . Body )
if msgType == "attachment" {
if msgType == "attachment" {
msg = messageBodyColor . stylize ( "$TITLE\n$FILE" )
msg = config . Colors . Message . Body . stylize ( "$TITLE\n$FILE" )
attachment := api . Msg . Content . Attachment
attachment := api . Msg . Content . Attachment
msg = msg . replaceString ( "$TITLE" , attachment . Object . Title )
msg = msg . replaceString ( "$TITLE" , attachment . Object . Title )
msg = msg . replace ( "$FILE" , messageAttachmentColor . stylize ( fmt . Sprintf ( "[Attachment: %s]" , attachment . Object . Filename ) ) )
msg = msg . replace ( "$FILE" , config . Colors . Message . Attachment . stylize ( fmt . Sprintf ( "[Attachment: %s]" , attachment . Object . Filename ) ) )
}
}
user := colorUsername ( api . Msg . Sender . Username )
user := colorUsername ( api . Msg . Sender . Username )
device := messageSenderDeviceColor . stylize ( api . Msg . Sender . DeviceName )
device := config . Colors . Message . SenderDevice . stylize ( api . Msg . Sender . DeviceName )
msgID := messageIDColor . stylize ( fmt . Sprintf ( "%d" , api . Msg . ID ) )
msgID := config . Colors . Message . ID . stylize ( fmt . Sprintf ( "%d" , api . Msg . ID ) )
date := messageTimeColor . stylize ( tm . Format ( d ateFormat) )
date := config . Colors . Message . Time . stylize ( tm . Format ( config . Formatting . D ateFormat) )
msgTime := messageTimeColor . stylize ( tm . Format ( timeFormat ) )
msgTime := config . Colors . Message . Time . stylize ( tm . Format ( config . Forma tting . T imeFormat) )
channelName := messageIDColor . stylize ( fmt . Sprintf ( "@%s#%s" , api . Msg . Channel . Name , api . Msg . Channel . TopicName ) )
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 ( "$MSG" , msg )
ret = ret . replace ( "$USER" , user )
ret = ret . replace ( "$USER" , user )
ret = ret . replace ( "$DEVICE" , device )
ret = ret . replace ( "$DEVICE" , device )
@ -454,9 +530,9 @@ func formatMessage(api keybase.ChatAPI, formatString string) string {
return ret . string ( )
return ret . string ( )
}
}
func formatOutput ( api keybase . ChatAPI ) string {
func formatOutput ( api keybase . ChatAPI ) string {
format := outputFormat
format := c onfig . Formatting . O utputFormat
if stream {
if stream {
format = outputStreamFormat
format = c onfig . Formatting . O utputStreamFormat
}
}
return formatMessage ( api , format )
return formatMessage ( api , format )
}
}
@ -484,7 +560,7 @@ func handleMessage(api keybase.ChatAPI) {
if m . Text == k . Username {
if m . Text == k . Username {
// We are in a team
// We are in a team
if topicName != channel . TopicName {
if topicName != channel . TopicName {
printInfo ( formatMessage ( api , mentionFormat ) )
printInfo ( formatMessage ( api , config . For matting . OutputM entionFormat) )
fmt . Print ( "\a" )
fmt . Print ( "\a" )
}
}
@ -493,7 +569,7 @@ func handleMessage(api keybase.ChatAPI) {
}
}
} else {
} else {
if msgSender != channel . Name {
if msgSender != channel . Name {
printInfo ( formatMessage ( api , p mFormat) )
printInfo ( formatMessage ( api , config . For matting . PM Format) )
fmt . Print ( "\a" )
fmt . Print ( "\a" )
}
}
@ -511,7 +587,7 @@ func handleMessage(api keybase.ChatAPI) {
if api . Msg . Channel . MembersType == keybase . TEAM {
if api . Msg . Channel . MembersType == keybase . TEAM {
printToView ( "Chat" , formatOutput ( api ) )
printToView ( "Chat" , formatOutput ( api ) )
} else {
} else {
printToView ( "Chat" , formatMessage ( api , p mFormat) )
printToView ( "Chat" , formatMessage ( api , config . For matting . PM Format) )
}
}
}
}
} else {
} else {
@ -545,8 +621,8 @@ func handleInput(viewName string) error {
if inputString == "" {
if inputString == "" {
return nil
return nil
}
}
if strings . HasPrefix ( inputString , cmdPrefix ) {
if strings . HasPrefix ( inputString , config . Basics . C mdPrefix ) {
cmd := deleteEmpty ( strings . Split ( inputString [ len ( cmdPrefix ) : ] , " " ) )
cmd := deleteEmpty ( strings . Split ( inputString [ len ( config . Basics . C mdPrefix ) : ] , " " ) )
if len ( cmd ) < 1 {
if len ( cmd ) < 1 {
return nil
return nil
}
}
@ -565,6 +641,7 @@ func handleInput(viewName string) error {
cmd [ 0 ] = inputString [ : 1 ]
cmd [ 0 ] = inputString [ : 1 ]
RunCommand ( cmd ... )
RunCommand ( cmd ... )
} else {
} else {
inputString = resolveRootEmojis ( inputString )
go sendChat ( inputString )
go sendChat ( inputString )
}
}
// restore any tab completion view titles on input commit
// restore any tab completion view titles on input commit
@ -576,6 +653,7 @@ func handleInput(viewName string) error {
return nil
return nil
}
}
func sendChat ( message string ) {
func sendChat ( message string ) {
autoScrollView ( "Chat" )
chat := k . NewChat ( channel )
chat := k . NewChat ( channel )
_ , err := chat . Send ( message )
_ , err := chat . Send ( message )
if err != nil {
if err != nil {