Merge branch 'color-messages' into v0.3.0

* color-messages:
  Implement theming for several components
  Start with theming functionality
  Start with adding colors for messages
This commit is contained in:
erroneousboat 2017-12-17 14:28:22 +01:00
commit f0df903d9d
10 changed files with 307 additions and 179 deletions

View File

@ -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(

View File

@ -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

View File

@ -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)
}

View File

@ -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>": "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
}

28
config/theme.go Normal file
View File

@ -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"`
}

View File

@ -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 {

View File

@ -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

View File

@ -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
}

View File

@ -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)},
)
}
}

View File

@ -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,