diff --git a/components/channels.go b/components/channels.go index 73f5413..df089b0 100644 --- a/components/channels.go +++ b/components/channels.go @@ -1,6 +1,8 @@ package components import ( + "fmt" + "html" "strings" "github.com/erroneousboat/termui" @@ -13,8 +15,82 @@ const ( IconGroup = "☰" IconIM = "●" IconNotification = "*" + + PresenceAway = "away" + PresenceActive = "active" + + ChannelTypeChannel = "channel" + ChannelTypeGroup = "group" + ChannelTypeIM = "im" ) +type ChannelItem struct { + ID string + Name string + Topic string + Type string + UserID string + Presence string + Notification bool + + StylePrefix string + StyleIcon string + StyleText string +} + +// ToString will set the label of the channel, how it will be +// displayed on screen. Based on the type, different icons are +// shown, as well as an optional notification icon. +func (c ChannelItem) ToString() string { + var prefix string + if c.Notification { + prefix = IconNotification + } else { + prefix = " " + } + + var icon string + switch c.Type { + case ChannelTypeChannel: + icon = IconChannel + case ChannelTypeGroup: + icon = IconGroup + case ChannelTypeIM: + switch c.Presence { + case PresenceActive: + icon = IconOnline + case PresenceAway: + icon = IconOffline + default: + icon = IconIM + } + } + + label := fmt.Sprintf( + "[%s](%s) [%s](%s) [%s](%s)", + prefix, c.StylePrefix, + icon, c.StyleIcon, + c.Name, c.StyleText, + ) + + return label +} + +// GetChannelName will return a formatted representation of the +// name of the channel +func (c ChannelItem) GetChannelName() string { + var channelName string + if c.Topic != "" { + channelName = fmt.Sprintf("%s - %s", + html.UnescapeString(c.Name), + html.UnescapeString(c.Topic), + ) + } else { + channelName = c.Name + } + return channelName +} + // Channels is the definition of a Channels component type Channels struct { List *termui.List @@ -51,6 +127,7 @@ func (c *Channels) Buffer() termui.Buffer { break } + // Set the visible cursor var cells []termui.Cell if y == c.CursorPosition { cells = termui.DefaultTxBuilder.Build( diff --git a/components/chat.go b/components/chat.go index b772e36..3fbafec 100644 --- a/components/chat.go +++ b/components/chat.go @@ -16,17 +16,31 @@ type Message struct { Time time.Time Name string Content string + + StyleTime string + StyleName string + StyleText string } func (m Message) ToString() string { - return html.UnescapeString( - fmt.Sprintf( - "[%s] <%s> %s", - m.Time.Format("15:04"), - m.Name, - m.Content, - ), - ) + if (m.Time != time.Time{} && m.Name != "") { + + return html.UnescapeString( + fmt.Sprintf( + "[[%s]](%s) [<%s>](%s) [%s](%s)", + m.Time.Format("15:04"), + m.StyleTime, + m.Name, + m.StyleName, + m.Content, + m.StyleText, + ), + ) + } else { + return html.UnescapeString( + fmt.Sprintf("[%s](%s)", m.Content, m.StyleText), + ) + } } // Chat is the definition of a Chat component diff --git a/components/mode.go b/components/mode.go index 799a5eb..36cf8ca 100644 --- a/components/mode.go +++ b/components/mode.go @@ -1,6 +1,14 @@ package components -import "github.com/erroneousboat/termui" +import ( + "github.com/erroneousboat/termui" +) + +const ( + CommandMode = "NORMAL" + InsertMode = "INSERT" + SearchMode = "SEARCH" +) // Mode is the definition of Mode component type Mode struct { @@ -10,10 +18,11 @@ type Mode struct { // CreateMode is the constructor of the Mode struct func CreateModeComponent() *Mode { mode := &Mode{ - Par: termui.NewPar("NORMAL"), + Par: termui.NewPar(CommandMode), } mode.Par.Height = 3 + mode.SetCommandMode() return mode } @@ -82,16 +91,16 @@ func (m *Mode) SetY(y int) { } func (m *Mode) SetInsertMode() { - m.Par.Text = "INSERT" + m.Par.Text = InsertMode termui.Render(m) } func (m *Mode) SetCommandMode() { - m.Par.Text = "NORMAL" + m.Par.Text = CommandMode termui.Render(m) } func (m *Mode) SetSearchMode() { - m.Par.Text = "SEARCH" + m.Par.Text = SearchMode termui.Render(m) } diff --git a/config/config.go b/config/config.go index cd7f0d3..b9b430a 100644 --- a/config/config.go +++ b/config/config.go @@ -11,18 +11,51 @@ import ( // Config is the definition of a Config struct type Config struct { SlackToken string `json:"slack_token"` - Theme string `json:"theme"` SidebarWidth int `json:"sidebar_width"` MainWidth int `json:"-"` KeyMap map[string]keyMapping `json:"key_map"` + Theme Theme `json:"theme"` } type keyMapping map[string]string // NewConfig loads the config file and returns a Config struct func NewConfig(filepath string) (*Config, error) { - cfg := Config{ - Theme: "dark", + cfg := getDefaultConfig() + + file, err := os.Open(filepath) + if err != nil { + return &cfg, err + } + + if err := json.NewDecoder(file).Decode(&cfg); err != nil { + return &cfg, err + } + + if cfg.SlackToken == "" { + return &cfg, errors.New("couldn't find 'slack_token' parameter") + } + + if cfg.SidebarWidth < 1 || cfg.SidebarWidth > 11 { + return &cfg, errors.New("please specify the 'sidebar_width' between 1 and 11") + } + + cfg.MainWidth = 12 - cfg.SidebarWidth + + termui.ColorMap = map[string]termui.Attribute{ + "fg": termui.StringToAttribute(cfg.Theme.View.Fg), + "bg": termui.StringToAttribute(cfg.Theme.View.Bg), + "border.fg": termui.StringToAttribute(cfg.Theme.View.BorderFg), + "label.fg": termui.StringToAttribute(cfg.Theme.View.LabelFg), + "par.fg": termui.StringToAttribute(cfg.Theme.View.ParFg), + "par.label.bg": termui.StringToAttribute(cfg.Theme.View.ParLabelFg), + } + + return &cfg, nil +} + +func getDefaultConfig() Config { + return Config{ SidebarWidth: 1, MainWidth: 11, KeyMap: map[string]keyMapping{ @@ -63,37 +96,25 @@ func NewConfig(filepath string) (*Config, error) { "": "space", }, }, + Theme: Theme{ + View: View{ + Fg: "white", + Bg: "default", + BorderFg: "white", + LabelFg: "green,bold", + ParFg: "white", + ParLabelFg: "white", + }, + Channel: Channel{ + Prefix: "", + Icon: "fg-green,fg-bold", + Text: "fg-blue,fg-bold", + }, + Message: Message{ + Time: "fg-red,fg-bold", + Name: "fg-blue,fg-bold", + Text: "", + }, + }, } - - file, err := os.Open(filepath) - if err != nil { - return &cfg, err - } - - if err := json.NewDecoder(file).Decode(&cfg); err != nil { - return &cfg, err - } - - if cfg.SlackToken == "" { - return &cfg, errors.New("couldn't find 'slack_token' parameter") - } - - if cfg.SidebarWidth < 1 || cfg.SidebarWidth > 11 { - return &cfg, errors.New("please specify the 'sidebar_width' between 1 and 11") - } - - cfg.MainWidth = 12 - cfg.SidebarWidth - - if cfg.Theme == "light" { - termui.ColorMap = map[string]termui.Attribute{ - "fg": termui.ColorBlack, - "bg": termui.ColorWhite, - "border.fg": termui.ColorBlack, - "label.fg": termui.ColorBlue, - "par.fg": termui.ColorYellow, - "par.label.bg": termui.ColorWhite, - } - } - - return &cfg, nil } diff --git a/config/theme.go b/config/theme.go new file mode 100644 index 0000000..0b113d9 --- /dev/null +++ b/config/theme.go @@ -0,0 +1,28 @@ +package config + +type Theme struct { + View View `json:"view"` + Channel Channel `json:"channel"` + Message Message `json:"message"` +} + +type View struct { + Fg string `json:"fg"` + Bg string `json:"bg"` + BorderFg string `json:"border_fg"` + LabelFg string `json:"border_fg"` + ParFg string `json:"par_fg"` + ParLabelFg string `json:"par_label_fg"` +} + +type Message struct { + Time string `json:"time"` + Name string `json:"name"` + Text string `json:"text"` +} + +type Channel struct { + Prefix string `json:"prefix"` + Icon string `json:"icon"` + Text string `json:"text"` +} diff --git a/context/context.go b/context/context.go index ae985c3..8638771 100644 --- a/context/context.go +++ b/context/context.go @@ -44,13 +44,13 @@ func CreateAppContext(flgConfig string, flgDebug bool) (*AppContext, error) { } // Create Service - svc, err := service.NewSlackService(config.SlackToken) + svc, err := service.NewSlackService(config) if err != nil { return nil, err } // Create the main view - view := views.CreateView(svc) + view := views.CreateView(config, svc) // Setup the interface if flgDebug { diff --git a/handlers/event.go b/handlers/event.go index fd1121d..e49eb8d 100644 --- a/handlers/event.go +++ b/handlers/event.go @@ -109,7 +109,9 @@ func messageHandler(ctx *context.AppContext) { // reverse order of messages, mainly done // when attachments are added to message for i := len(msg) - 1; i >= 0; i-- { - ctx.View.Chat.AddMessage(msg[i]) + ctx.View.Chat.AddMessage( + msg[i].ToString(), + ) } termui.Render(ctx.View.Chat) @@ -259,11 +261,17 @@ func actionSearchMode(ctx *context.AppContext) { } func actionGetMessages(ctx *context.AppContext) { - messages := ctx.Service.GetMessages( + msgs := ctx.Service.GetMessages( ctx.Service.Channels[ctx.View.Channels.SelectedChannel], ctx.View.Chat.GetMaxItems(), ) - ctx.View.Chat.SetMessages(messages) + + var strMsgs []string + for _, msg := range msgs { + strMsgs = append(strMsgs, msg.ToString()) + } + + ctx.View.Chat.SetMessages(strMsgs) termui.Render(ctx.View.Chat) } @@ -324,13 +332,18 @@ func actionChangeChannel(ctx *context.AppContext) { // Get messages of the SelectedChannel, and get the count of messages // that fit into the Chat component - messages := ctx.Service.GetMessages( + msgs := ctx.Service.GetMessages( ctx.Service.GetSlackChannel(ctx.View.Channels.SelectedChannel), ctx.View.Chat.GetMaxItems(), ) + var strMsgs []string + for _, msg := range msgs { + strMsgs = append(strMsgs, msg.ToString()) + } + // Set messages for the channel - ctx.View.Chat.SetMessages(messages) + ctx.View.Chat.SetMessages(strMsgs) // FIXME // Set channel name for the Chat pane diff --git a/service/channel.go b/service/channel.go deleted file mode 100644 index bd5a2b7..0000000 --- a/service/channel.go +++ /dev/null @@ -1,71 +0,0 @@ -package service - -import ( - "fmt" - "html" - - "github.com/erroneousboat/slack-term/components" -) - -const ( - PresenceAway = "away" - PresenceActive = "active" -) - -type Channel struct { - ID string - Name string - Topic string - Type string - UserID string - Presence string - Notification bool -} - -// ToString will set the label of the channel, how it will be -// displayed on screen. Based on the type, different icons are -// shown, as well as an optional notification icon. -func (c Channel) ToString() string { - var prefix string - if c.Notification { - prefix = components.IconNotification - } else { - prefix = " " - } - - var label string - switch c.Type { - case ChannelTypeChannel: - label = fmt.Sprintf("%s %s %s", prefix, components.IconChannel, c.Name) - case ChannelTypeGroup: - label = fmt.Sprintf("%s %s %s", prefix, components.IconGroup, c.Name) - case ChannelTypeIM: - var icon string - switch c.Presence { - case PresenceActive: - icon = components.IconOnline - case PresenceAway: - icon = components.IconOffline - default: - icon = components.IconIM - } - label = fmt.Sprintf("%s %s %s", prefix, icon, c.Name) - } - - return label -} - -// GetChannelName will return a formatted representation of the -// name of the channel -func (c Channel) GetChannelName() string { - var channelName string - if c.Topic != "" { - channelName = fmt.Sprintf("%s - %s", - html.UnescapeString(c.Name), - html.UnescapeString(c.Topic), - ) - } else { - channelName = c.Name - } - return channelName -} diff --git a/service/slack.go b/service/slack.go index 98e4d58..56e6383 100644 --- a/service/slack.go +++ b/service/slack.go @@ -22,10 +22,11 @@ const ( ) type SlackService struct { + Config *config.Config Client *slack.Client RTM *slack.RTM SlackChannels []interface{} - Channels []Channel + Channels []components.ChannelItem UserCache map[string]string CurrentUserID string CurrentUsername string @@ -33,9 +34,10 @@ type SlackService struct { // NewSlackService is the constructor for the SlackService and will initialize // the RTM and a Client -func NewSlackService(token string) (*SlackService, error) { +func NewSlackService(config *config.Config) (*SlackService, error) { svc := &SlackService{ - Client: slack.New(token), + Config: config, + Client: slack.New(config.SlackToken), UserCache: make(map[string]string), } @@ -77,23 +79,27 @@ func NewSlackService(token string) (*SlackService, error) { // an []interface as well as to a []Channel which will give us easy access // to the id and name of the Channel. func (s *SlackService) GetChannels() []string { - var chans []Channel + var chans []components.ChannelItem // Channel slackChans, err := s.Client.GetChannels(true) if err != nil { - chans = append(chans, Channel{}) + chans = append(chans, components.ChannelItem{}) } + for _, chn := range slackChans { if chn.IsMember { s.SlackChannels = append(s.SlackChannels, chn) chans = append( - chans, Channel{ - ID: chn.ID, - Name: chn.Name, - Topic: chn.Topic.Value, - Type: ChannelTypeChannel, - UserID: "", + chans, components.ChannelItem{ + ID: chn.ID, + Name: chn.Name, + Topic: chn.Topic.Value, + Type: components.ChannelTypeChannel, + UserID: "", + StylePrefix: s.Config.Theme.Channel.Prefix, + StyleIcon: s.Config.Theme.Channel.Icon, + StyleText: s.Config.Theme.Channel.Text, }, ) } @@ -102,17 +108,20 @@ func (s *SlackService) GetChannels() []string { // Groups slackGroups, err := s.Client.GetGroups(true) if err != nil { - chans = append(chans, Channel{}) + chans = append(chans, components.ChannelItem{}) } for _, grp := range slackGroups { s.SlackChannels = append(s.SlackChannels, grp) chans = append( - chans, Channel{ - ID: grp.ID, - Name: grp.Name, - Topic: grp.Topic.Value, - Type: ChannelTypeGroup, - UserID: "", + chans, components.ChannelItem{ + ID: grp.ID, + Name: grp.Name, + Topic: grp.Topic.Value, + Type: components.ChannelTypeGroup, + UserID: "", + StylePrefix: s.Config.Theme.Channel.Prefix, + StyleIcon: s.Config.Theme.Channel.Icon, + StyleText: s.Config.Theme.Channel.Text, }, ) } @@ -120,7 +129,7 @@ func (s *SlackService) GetChannels() []string { // IM slackIM, err := s.Client.GetIMChannels() if err != nil { - chans = append(chans, Channel{}) + chans = append(chans, components.ChannelItem{}) } for _, im := range slackIM { @@ -136,13 +145,16 @@ func (s *SlackService) GetChannels() []string { if ok { chans = append( chans, - Channel{ - ID: im.ID, - Name: name, - Topic: "", - Type: ChannelTypeIM, - UserID: im.User, - Presence: presence, + components.ChannelItem{ + ID: im.ID, + Name: name, + Topic: "", + Type: components.ChannelTypeIM, + UserID: im.User, + Presence: presence, + StylePrefix: s.Config.Theme.Channel.Prefix, + StyleIcon: s.Config.Theme.Channel.Icon, + StyleText: s.Config.Theme.Channel.Text, }, ) s.SlackChannels = append(s.SlackChannels, im) @@ -271,7 +283,7 @@ func (s *SlackService) SendMessage(channelID int, message string) { // GetMessages will get messages for a channel, group or im channel delimited // by a count. -func (s *SlackService) GetMessages(channel interface{}, count int) []string { +func (s *SlackService) GetMessages(channel interface{}, count int) []components.Message { // https://api.slack.com/methods/channels.history historyParams := slack.HistoryParameters{ Count: count, @@ -301,7 +313,7 @@ func (s *SlackService) GetMessages(channel interface{}, count int) []string { } // Construct the messages - var messages []string + var messages []components.Message for _, message := range history.Messages { msg := s.CreateMessage(message) messages = append(messages, msg...) @@ -309,7 +321,7 @@ func (s *SlackService) GetMessages(channel interface{}, count int) []string { // Reverse the order of the messages, we want the newest in // the last place - var messagesReversed []string + var messagesReversed []components.Message for i := len(messages) - 1; i >= 0; i-- { messagesReversed = append(messagesReversed, messages[i]) } @@ -324,8 +336,8 @@ func (s *SlackService) GetMessages(channel interface{}, count int) []string { // // This returns an array of string because we will try to uncover attachments // associated with messages. -func (s *SlackService) CreateMessage(message slack.Message) []string { - var msgs []string +func (s *SlackService) CreateMessage(message slack.Message) []components.Message { + var msgs []components.Message var name string // Get username from cache @@ -360,7 +372,7 @@ func (s *SlackService) CreateMessage(message slack.Message) []string { // When there are attachments append them if len(message.Attachments) > 0 { - msgs = append(msgs, createMessageFromAttachments(message.Attachments)...) + msgs = append(msgs, s.CreateMessageFromAttachments(message.Attachments)...) } // Parse time @@ -372,19 +384,22 @@ func (s *SlackService) CreateMessage(message slack.Message) []string { // Format message msg := components.Message{ - Time: time.Unix(intTime, 0), - Name: name, - Content: parseMessage(s, message.Text), + Time: time.Unix(intTime, 0), + Name: name, + Content: parseMessage(s, message.Text), + StyleTime: s.Config.Theme.Message.Time, + StyleName: s.Config.Theme.Message.Name, + StyleText: s.Config.Theme.Message.Text, } - msgs = append(msgs, msg.ToString()) + msgs = append(msgs, msg) return msgs } -func (s *SlackService) CreateMessageFromMessageEvent(message *slack.MessageEvent) []string { +func (s *SlackService) CreateMessageFromMessageEvent(message *slack.MessageEvent) []components.Message { - var msgs []string + var msgs []components.Message var name string // Append (edited) when an edited message is received @@ -425,7 +440,7 @@ func (s *SlackService) CreateMessageFromMessageEvent(message *slack.MessageEvent // When there are attachments append them if len(message.Attachments) > 0 { - msgs = append(msgs, createMessageFromAttachments(message.Attachments)...) + msgs = append(msgs, s.CreateMessageFromAttachments(message.Attachments)...) } // Parse time @@ -437,12 +452,15 @@ func (s *SlackService) CreateMessageFromMessageEvent(message *slack.MessageEvent // Format message msg := components.Message{ - Time: time.Unix(intTime, 0), - Name: name, - Content: parseMessage(s, message.Text), + Time: time.Unix(intTime, 0), + Name: name, + Content: parseMessage(s, message.Text), + StyleTime: s.Config.Theme.Message.Time, + StyleName: s.Config.Theme.Message.Name, + StyleText: s.Config.Theme.Message.Text, } - msgs = append(msgs, msg.ToString()) + msgs = append(msgs, msg) return msgs } @@ -522,27 +540,37 @@ func parseEmoji(msg string) string { ) } -// createMessageFromAttachments will construct a array of string of the Field +// CreateMessageFromAttachments will construct a array of string of the Field // values of Attachments from a Message. -func createMessageFromAttachments(atts []slack.Attachment) []string { - var msgs []string +func (s *SlackService) CreateMessageFromAttachments(atts []slack.Attachment) []components.Message { + var msgs []components.Message for _, att := range atts { for i := len(att.Fields) - 1; i >= 0; i-- { - msgs = append(msgs, - fmt.Sprintf( + msgs = append(msgs, components.Message{ + Content: fmt.Sprintf( "%s %s", att.Fields[i].Title, att.Fields[i].Value, ), + StyleTime: s.Config.Theme.Message.Time, + StyleName: s.Config.Theme.Message.Name, + StyleText: s.Config.Theme.Message.Text, + }, ) } if att.Text != "" { - msgs = append(msgs, att.Text) + msgs = append( + msgs, + components.Message{Content: fmt.Sprintf("%s", att.Text)}, + ) } if att.Title != "" { - msgs = append(msgs, att.Title) + msgs = append( + msgs, + components.Message{Content: fmt.Sprintf("%s", att.Title)}, + ) } } diff --git a/views/view.go b/views/view.go index baf5949..3361616 100644 --- a/views/view.go +++ b/views/view.go @@ -4,10 +4,12 @@ import ( "github.com/erroneousboat/termui" "github.com/erroneousboat/slack-term/components" + "github.com/erroneousboat/slack-term/config" "github.com/erroneousboat/slack-term/service" ) type View struct { + Config *config.Config Input *components.Input Chat *components.Chat Channels *components.Channels @@ -15,7 +17,7 @@ type View struct { Debug *components.Debug } -func CreateView(svc *service.SlackService) *View { +func CreateView(config *config.Config, svc *service.SlackService) *View { // Create Input component input := components.CreateInputComponent() @@ -30,11 +32,17 @@ func CreateView(svc *service.SlackService) *View { chat := components.CreateChatComponent(input.Par.Height) // Chat: fill the component - slackMsgs := svc.GetMessages( + msgs := svc.GetMessages( svc.GetSlackChannel(channels.SelectedChannel), chat.GetMaxItems(), ) - chat.SetMessages(slackMsgs) + + var strMsgs []string + for _, msg := range msgs { + strMsgs = append(strMsgs, msg.ToString()) + } + + chat.SetMessages(strMsgs) chat.SetBorderLabel(svc.Channels[channels.SelectedChannel].GetChannelName()) // Debug: create the component @@ -44,6 +52,7 @@ func CreateView(svc *service.SlackService) *View { mode := components.CreateModeComponent() view := &View{ + Config: config, Input: input, Channels: channels, Chat: chat,