Implement theming for several components

This commit is contained in:
erroneousboat 2017-12-17 12:22:04 +01:00
parent 6ebeb7b5fc
commit d170057b37
9 changed files with 226 additions and 212 deletions

View File

@ -1,6 +1,8 @@
package components package components
import ( import (
"fmt"
"html"
"strings" "strings"
"github.com/erroneousboat/termui" "github.com/erroneousboat/termui"
@ -13,8 +15,82 @@ const (
IconGroup = "☰" IconGroup = "☰"
IconIM = "●" IconIM = "●"
IconNotification = "*" 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 // Channels is the definition of a Channels component
type Channels struct { type Channels struct {
List *termui.List List *termui.List
@ -51,6 +127,7 @@ func (c *Channels) Buffer() termui.Buffer {
break break
} }
// Set the visible cursor
var cells []termui.Cell var cells []termui.Cell
if y == c.CursorPosition { if y == c.CursorPosition {
cells = termui.DefaultTxBuilder.Build( cells = termui.DefaultTxBuilder.Build(

View File

@ -16,25 +16,29 @@ type Message struct {
Time time.Time Time time.Time
Name string Name string
Content string Content string
StyleTime string
StyleName string
StyleText string
} }
func (m Message) ToString(stlTime string, stlName string, stlContent string) string { func (m Message) ToString() string {
if (m.Time != time.Time{} && m.Name != "") { if (m.Time != time.Time{} && m.Name != "") {
return html.UnescapeString( return html.UnescapeString(
fmt.Sprintf( fmt.Sprintf(
"[[%s]](%s) [<%s>](%s) [%s](%s)", "[[%s]](%s) [<%s>](%s) [%s](%s)",
m.Time.Format("15:04"), m.Time.Format("15:04"),
stlTime, m.StyleTime,
m.Name, m.Name,
stlName, m.StyleName,
m.Content, m.Content,
stlContent, m.StyleText,
), ),
) )
} else { } else {
return html.UnescapeString( return html.UnescapeString(
fmt.Sprintf("[%s](%s)", m.Content, stlContent), fmt.Sprintf("[%s](%s)", m.Content, m.StyleText),
) )
} }
} }

View File

@ -1,6 +1,14 @@
package components 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 // Mode is the definition of Mode component
type Mode struct { type Mode struct {
@ -10,10 +18,11 @@ type Mode struct {
// CreateMode is the constructor of the Mode struct // CreateMode is the constructor of the Mode struct
func CreateModeComponent() *Mode { func CreateModeComponent() *Mode {
mode := &Mode{ mode := &Mode{
Par: termui.NewPar("NORMAL"), Par: termui.NewPar(CommandMode),
} }
mode.Par.Height = 3 mode.Par.Height = 3
mode.SetCommandMode()
return mode return mode
} }
@ -82,16 +91,16 @@ func (m *Mode) SetY(y int) {
} }
func (m *Mode) SetInsertMode() { func (m *Mode) SetInsertMode() {
m.Par.Text = "INSERT" m.Par.Text = InsertMode
termui.Render(m) termui.Render(m)
} }
func (m *Mode) SetCommandMode() { func (m *Mode) SetCommandMode() {
m.Par.Text = "NORMAL" m.Par.Text = CommandMode
termui.Render(m) termui.Render(m)
} }
func (m *Mode) SetSearchMode() { func (m *Mode) SetSearchMode() {
m.Par.Text = "SEARCH" m.Par.Text = SearchMode
termui.Render(m) termui.Render(m)
} }

View File

@ -4,12 +4,13 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"os" "os"
"github.com/erroneousboat/termui"
) )
// Config is the definition of a Config struct // Config is the definition of a Config struct
type Config struct { type Config struct {
SlackToken string `json:"slack_token"` SlackToken string `json:"slack_token"`
// Theme string `json:"theme"`
SidebarWidth int `json:"sidebar_width"` SidebarWidth int `json:"sidebar_width"`
MainWidth int `json:"-"` MainWidth int `json:"-"`
KeyMap map[string]keyMapping `json:"key_map"` KeyMap map[string]keyMapping `json:"key_map"`
@ -20,8 +21,41 @@ type keyMapping map[string]string
// NewConfig loads the config file and returns a Config struct // NewConfig loads the config file and returns a Config struct
func NewConfig(filepath string) (*Config, error) { func NewConfig(filepath string) (*Config, error) {
cfg := Config{ cfg := getDefaultConfig()
// Theme: "dark",
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, SidebarWidth: 1,
MainWidth: 11, MainWidth: 11,
KeyMap: map[string]keyMapping{ KeyMap: map[string]keyMapping{
@ -63,48 +97,24 @@ func NewConfig(filepath string) (*Config, error) {
}, },
}, },
Theme: Theme{ Theme: Theme{
Message: Message{ View: View{
Time: "fg-red,fg-bold", Fg: "white",
Name: "fg-blue,fg-bold", Bg: "default",
Content: "", BorderFg: "white",
LabelFg: "green,bold",
ParFg: "white",
ParLabelFg: "white",
}, },
Channel: Channel{ Channel: Channel{
Prefix: "", Prefix: "",
Icon: "fg-red", Icon: "fg-green,fg-bold",
Name: "", 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
} }

View File

@ -1,18 +1,28 @@
package config package config
type Theme struct { type Theme struct {
Message Message `json:"message"` View View `json:"view"`
Channel Channel `json:"channel"` 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 { type Message struct {
Time string `json:"time"` Time string `json:"time"`
Name string `json:"name"` Name string `json:"name"`
Content string `json:"content"` Text string `json:"text"`
} }
type Channel struct { type Channel struct {
Prefix string `json:"prefix"` Prefix string `json:"prefix"`
Icon string `json:"icon"` Icon string `json:"icon"`
Name string `json:"name"` Text string `json:"text"`
} }

View File

@ -110,11 +110,7 @@ func messageHandler(ctx *context.AppContext) {
// when attachments are added to message // when attachments are added to message
for i := len(msg) - 1; i >= 0; i-- { for i := len(msg) - 1; i >= 0; i-- {
ctx.View.Chat.AddMessage( ctx.View.Chat.AddMessage(
msg[i].ToString( msg[i].ToString(),
ctx.Config.Theme.Message.Time,
ctx.Config.Theme.Message.Name,
ctx.Config.Theme.Message.Content,
),
) )
} }
@ -272,14 +268,7 @@ func actionGetMessages(ctx *context.AppContext) {
var strMsgs []string var strMsgs []string
for _, msg := range msgs { for _, msg := range msgs {
strMsgs = append( strMsgs = append(strMsgs, msg.ToString())
strMsgs,
msg.ToString(
ctx.Config.Theme.Message.Time,
ctx.Config.Theme.Message.Name,
ctx.Config.Theme.Message.Content,
),
)
} }
ctx.View.Chat.SetMessages(strMsgs) ctx.View.Chat.SetMessages(strMsgs)
@ -350,14 +339,7 @@ func actionChangeChannel(ctx *context.AppContext) {
var strMsgs []string var strMsgs []string
for _, msg := range msgs { for _, msg := range msgs {
strMsgs = append( strMsgs = append(strMsgs, msg.ToString())
strMsgs,
msg.ToString(
ctx.Config.Theme.Message.Time,
ctx.Config.Theme.Message.Name,
ctx.Config.Theme.Message.Content,
),
)
} }
// Set messages for the channel // Set messages for the channel

View File

@ -1,76 +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(stlPrefix string, stlIcon string, stlName string) string {
var prefix string
if c.Notification {
prefix = components.IconNotification
} else {
prefix = " "
}
var icon string
switch c.Type {
case ChannelTypeChannel:
icon = components.IconChannel
case ChannelTypeGroup:
icon = components.IconGroup
case ChannelTypeIM:
switch c.Presence {
case PresenceActive:
icon = components.IconOnline
case PresenceAway:
icon = components.IconOffline
default:
icon = components.IconIM
}
}
label := fmt.Sprintf(
"[%s](%s) [%s](%s) [%s](%s)",
prefix, stlPrefix,
icon, stlIcon,
c.Name, stlName,
)
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

@ -26,7 +26,7 @@ type SlackService struct {
Client *slack.Client Client *slack.Client
RTM *slack.RTM RTM *slack.RTM
SlackChannels []interface{} SlackChannels []interface{}
Channels []Channel Channels []components.ChannelItem
UserCache map[string]string UserCache map[string]string
CurrentUserID string CurrentUserID string
CurrentUsername string CurrentUsername string
@ -79,23 +79,27 @@ func NewSlackService(config *config.Config) (*SlackService, error) {
// an []interface as well as to a []Channel which will give us easy access // an []interface as well as to a []Channel which will give us easy access
// to the id and name of the Channel. // to the id and name of the Channel.
func (s *SlackService) GetChannels() []string { func (s *SlackService) GetChannels() []string {
var chans []Channel var chans []components.ChannelItem
// Channel // Channel
slackChans, err := s.Client.GetChannels(true) slackChans, err := s.Client.GetChannels(true)
if err != nil { if err != nil {
chans = append(chans, Channel{}) chans = append(chans, components.ChannelItem{})
} }
for _, chn := range slackChans { for _, chn := range slackChans {
if chn.IsMember { if chn.IsMember {
s.SlackChannels = append(s.SlackChannels, chn) s.SlackChannels = append(s.SlackChannels, chn)
chans = append( chans = append(
chans, Channel{ chans, components.ChannelItem{
ID: chn.ID, ID: chn.ID,
Name: chn.Name, Name: chn.Name,
Topic: chn.Topic.Value, Topic: chn.Topic.Value,
Type: ChannelTypeChannel, Type: components.ChannelTypeChannel,
UserID: "", UserID: "",
StylePrefix: s.Config.Theme.Channel.Prefix,
StyleIcon: s.Config.Theme.Channel.Icon,
StyleText: s.Config.Theme.Channel.Text,
}, },
) )
} }
@ -104,17 +108,20 @@ func (s *SlackService) GetChannels() []string {
// Groups // Groups
slackGroups, err := s.Client.GetGroups(true) slackGroups, err := s.Client.GetGroups(true)
if err != nil { if err != nil {
chans = append(chans, Channel{}) chans = append(chans, components.ChannelItem{})
} }
for _, grp := range slackGroups { for _, grp := range slackGroups {
s.SlackChannels = append(s.SlackChannels, grp) s.SlackChannels = append(s.SlackChannels, grp)
chans = append( chans = append(
chans, Channel{ chans, components.ChannelItem{
ID: grp.ID, ID: grp.ID,
Name: grp.Name, Name: grp.Name,
Topic: grp.Topic.Value, Topic: grp.Topic.Value,
Type: ChannelTypeGroup, Type: components.ChannelTypeGroup,
UserID: "", UserID: "",
StylePrefix: s.Config.Theme.Channel.Prefix,
StyleIcon: s.Config.Theme.Channel.Icon,
StyleText: s.Config.Theme.Channel.Text,
}, },
) )
} }
@ -122,7 +129,7 @@ func (s *SlackService) GetChannels() []string {
// IM // IM
slackIM, err := s.Client.GetIMChannels() slackIM, err := s.Client.GetIMChannels()
if err != nil { if err != nil {
chans = append(chans, Channel{}) chans = append(chans, components.ChannelItem{})
} }
for _, im := range slackIM { for _, im := range slackIM {
@ -138,13 +145,16 @@ func (s *SlackService) GetChannels() []string {
if ok { if ok {
chans = append( chans = append(
chans, chans,
Channel{ components.ChannelItem{
ID: im.ID, ID: im.ID,
Name: name, Name: name,
Topic: "", Topic: "",
Type: ChannelTypeIM, Type: components.ChannelTypeIM,
UserID: im.User, UserID: im.User,
Presence: presence, 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) s.SlackChannels = append(s.SlackChannels, im)
@ -155,14 +165,7 @@ func (s *SlackService) GetChannels() []string {
var channels []string var channels []string
for _, chn := range s.Channels { for _, chn := range s.Channels {
channels = append( channels = append(channels, chn.ToString())
channels,
chn.ToString(
s.Config.Theme.Channel.Prefix,
s.Config.Theme.Channel.Icon,
s.Config.Theme.Channel.Name,
),
)
} }
return channels return channels
} }
@ -171,14 +174,7 @@ func (s *SlackService) GetChannels() []string {
func (s *SlackService) ChannelsToString() []string { func (s *SlackService) ChannelsToString() []string {
var channels []string var channels []string
for _, chn := range s.Channels { for _, chn := range s.Channels {
channels = append( channels = append(channels, chn.ToString())
channels,
chn.ToString(
s.Config.Theme.Channel.Prefix,
s.Config.Theme.Channel.Icon,
s.Config.Theme.Channel.Name,
),
)
} }
return channels return channels
} }
@ -376,7 +372,7 @@ func (s *SlackService) CreateMessage(message slack.Message) []components.Message
// When there are attachments append them // When there are attachments append them
if len(message.Attachments) > 0 { if len(message.Attachments) > 0 {
msgs = append(msgs, createMessageFromAttachments(message.Attachments)...) msgs = append(msgs, s.CreateMessageFromAttachments(message.Attachments)...)
} }
// Parse time // Parse time
@ -388,9 +384,12 @@ func (s *SlackService) CreateMessage(message slack.Message) []components.Message
// Format message // Format message
msg := components.Message{ msg := components.Message{
Time: time.Unix(intTime, 0), Time: time.Unix(intTime, 0),
Name: name, Name: name,
Content: parseMessage(s, message.Text), 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) msgs = append(msgs, msg)
@ -441,7 +440,7 @@ func (s *SlackService) CreateMessageFromMessageEvent(message *slack.MessageEvent
// When there are attachments append them // When there are attachments append them
if len(message.Attachments) > 0 { if len(message.Attachments) > 0 {
msgs = append(msgs, createMessageFromAttachments(message.Attachments)...) msgs = append(msgs, s.CreateMessageFromAttachments(message.Attachments)...)
} }
// Parse time // Parse time
@ -453,9 +452,12 @@ func (s *SlackService) CreateMessageFromMessageEvent(message *slack.MessageEvent
// Format message // Format message
msg := components.Message{ msg := components.Message{
Time: time.Unix(intTime, 0), Time: time.Unix(intTime, 0),
Name: name, Name: name,
Content: parseMessage(s, message.Text), 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) msgs = append(msgs, msg)
@ -538,9 +540,9 @@ 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. // values of Attachments from a Message.
func createMessageFromAttachments(atts []slack.Attachment) []components.Message { func (s *SlackService) CreateMessageFromAttachments(atts []slack.Attachment) []components.Message {
var msgs []components.Message var msgs []components.Message
for _, att := range atts { for _, att := range atts {
for i := len(att.Fields) - 1; i >= 0; i-- { for i := len(att.Fields) - 1; i >= 0; i-- {
@ -550,6 +552,9 @@ func createMessageFromAttachments(atts []slack.Attachment) []components.Message
att.Fields[i].Title, att.Fields[i].Title,
att.Fields[i].Value, att.Fields[i].Value,
), ),
StyleTime: s.Config.Theme.Message.Time,
StyleName: s.Config.Theme.Message.Name,
StyleText: s.Config.Theme.Message.Text,
}, },
) )
} }

View File

@ -39,14 +39,7 @@ func CreateView(config *config.Config, svc *service.SlackService) *View {
var strMsgs []string var strMsgs []string
for _, msg := range msgs { for _, msg := range msgs {
strMsgs = append( strMsgs = append(strMsgs, msg.ToString())
strMsgs,
msg.ToString(
config.Theme.Message.Time,
config.Theme.Message.Name,
config.Theme.Message.Content,
),
)
} }
chat.SetMessages(strMsgs) chat.SetMessages(strMsgs)