396 lines
9.1 KiB
Go
Raw Normal View History

2016-09-25 22:34:02 +02:00
package components
import (
"fmt"
"html"
2016-09-30 12:09:03 +02:00
2017-09-23 13:56:45 +02:00
"github.com/erroneousboat/termui"
"github.com/lithammer/fuzzysearch/fuzzy"
2016-09-25 22:34:02 +02:00
)
const (
IconOnline = "●"
IconOffline = "○"
IconChannel = "#"
IconGroup = "☰"
IconIM = "●"
2018-10-13 15:32:21 +02:00
IconMpIM = "☰"
IconNotification = "*"
PresenceAway = "away"
PresenceActive = "active"
ChannelTypeChannel = "channel"
ChannelTypeGroup = "group"
ChannelTypeIM = "im"
ChannelTypeMpIM = "mpim"
)
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
2018-09-02 10:14:34 +02:00
case ChannelTypeMpIM:
icon = IconMpIM
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
}
2016-10-02 14:53:00 +02:00
// Channels is the definition of a Channels component
2016-09-25 22:34:02 +02:00
type Channels struct {
ChannelItems []ChannelItem
2016-09-27 22:05:44 +02:00
List *termui.List
SelectedChannel int // index of which channel is selected from the List
Offset int // from what offset are channels rendered
CursorPosition int // the y position of the 'cursor'
2017-12-17 13:48:20 +01:00
SearchMatches []int // index of the search matches
SearchPosition int // current position of a search match
2016-09-27 22:05:44 +02:00
}
2016-10-01 12:48:15 +02:00
// CreateChannels is the constructor for the Channels component
2019-03-16 14:42:27 +01:00
func CreateChannelsComponent(height int) *Channels {
2016-09-25 22:34:02 +02:00
channels := &Channels{
List: termui.NewList(),
}
channels.List.BorderLabel = "Channels"
2019-03-16 14:42:27 +01:00
channels.List.Height = height
2016-09-25 22:34:02 +02:00
2016-09-28 22:10:04 +02:00
channels.SelectedChannel = 0
2016-10-01 12:48:15 +02:00
channels.Offset = 0
channels.CursorPosition = channels.List.InnerBounds().Min.Y
2016-09-27 22:05:44 +02:00
2016-09-25 22:34:02 +02:00
return channels
}
// Buffer implements interface termui.Bufferer
func (c *Channels) Buffer() termui.Buffer {
buf := c.List.Buffer()
2016-09-27 22:05:44 +02:00
for i, item := range c.ChannelItems[c.Offset:] {
2016-10-01 12:48:15 +02:00
y := c.List.InnerBounds().Min.Y + i
if y > c.List.InnerBounds().Max.Y-1 {
break
}
// Set the visible cursor
2016-09-27 22:05:44 +02:00
var cells []termui.Cell
2016-10-01 12:48:15 +02:00
if y == c.CursorPosition {
2016-09-27 22:05:44 +02:00
cells = termui.DefaultTxBuilder.Build(
item.ToString(), c.List.ItemBgColor, c.List.ItemFgColor)
2016-09-27 22:05:44 +02:00
} else {
cells = termui.DefaultTxBuilder.Build(
item.ToString(), c.List.ItemFgColor, c.List.ItemBgColor)
2016-09-27 22:05:44 +02:00
}
2018-10-13 15:32:21 +02:00
// Append ellipsis when overflows
2016-09-27 22:05:44 +02:00
cells = termui.DTrimTxCls(cells, c.List.InnerWidth())
2019-03-16 14:42:27 +01:00
x := c.List.InnerBounds().Min.X
2016-09-27 22:05:44 +02:00
for _, cell := range cells {
2019-03-16 14:42:27 +01:00
buf.Set(x, y, cell)
x += cell.Width()
2016-09-27 22:05:44 +02:00
}
2016-10-01 12:48:15 +02:00
// When not at the end of the pane fill it up empty characters
2019-04-06 17:51:19 +02:00
for x < c.List.InnerBounds().Max.X {
2016-10-01 12:48:15 +02:00
if y == c.CursorPosition {
2019-04-06 17:51:19 +02:00
buf.Set(x, y,
2016-10-01 12:48:15 +02:00
termui.Cell{
2016-10-11 19:28:37 +02:00
Ch: ' ',
Fg: c.List.ItemBgColor,
Bg: c.List.ItemFgColor,
2016-10-01 12:48:15 +02:00
},
)
} else {
2016-10-11 19:28:37 +02:00
buf.Set(
2019-04-06 17:51:19 +02:00
x, y,
2016-10-11 19:28:37 +02:00
termui.Cell{
Ch: ' ',
Fg: c.List.ItemFgColor,
Bg: c.List.ItemBgColor,
},
)
2016-10-01 12:48:15 +02:00
}
x++
}
2016-09-27 22:05:44 +02:00
}
2016-09-25 22:34:02 +02:00
return buf
}
// GetHeight implements interface termui.GridBufferer
func (c *Channels) GetHeight() int {
return c.List.Block.GetHeight()
}
// SetWidth implements interface termui.GridBufferer
func (c *Channels) SetWidth(w int) {
c.List.SetWidth(w)
}
// SetX implements interface termui.GridBufferer
func (c *Channels) SetX(x int) {
c.List.SetX(x)
}
// SetY implements interface termui.GridBufferer
func (c *Channels) SetY(y int) {
c.List.SetY(y)
}
func (c *Channels) SetChannels(channels []ChannelItem) {
c.ChannelItems = channels
}
func (c *Channels) MarkAsRead(channelID int) {
c.ChannelItems[channelID].Notification = false
}
func (c *Channels) MarkAsUnread(channelID string) {
index := c.FindChannel(channelID)
c.ChannelItems[index].Notification = true
}
func (c *Channels) SetPresence(channelID string, presence string) {
index := c.FindChannel(channelID)
c.ChannelItems[index].Presence = presence
}
func (c *Channels) FindChannel(channelID string) int {
var index int
for i, channel := range c.ChannelItems {
if channel.ID == channelID {
index = i
break
}
}
return index
2016-09-25 22:34:02 +02:00
}
2016-09-27 22:05:44 +02:00
2016-09-30 12:09:03 +02:00
// SetSelectedChannel sets the SelectedChannel given the index
func (c *Channels) SetSelectedChannel(index int) {
c.SelectedChannel = index
2016-09-27 22:05:44 +02:00
}
// Get SelectedChannel returns the ChannelItem that is currently selected
func (c *Channels) GetSelectedChannel() ChannelItem {
return c.ChannelItems[c.SelectedChannel]
}
2016-09-30 12:09:03 +02:00
// MoveCursorUp will decrease the SelectedChannel by 1
2016-09-27 22:05:44 +02:00
func (c *Channels) MoveCursorUp() {
if c.SelectedChannel > 0 {
c.SetSelectedChannel(c.SelectedChannel - 1)
2016-10-01 12:48:15 +02:00
c.ScrollUp()
2016-09-27 22:05:44 +02:00
}
}
2016-09-30 12:09:03 +02:00
// MoveCursorDown will increase the SelectedChannel by 1
2016-09-27 22:05:44 +02:00
func (c *Channels) MoveCursorDown() {
if c.SelectedChannel < len(c.ChannelItems)-1 {
2016-09-27 22:05:44 +02:00
c.SetSelectedChannel(c.SelectedChannel + 1)
2016-10-01 12:48:15 +02:00
c.ScrollDown()
2016-09-27 22:05:44 +02:00
}
}
// MoveCursorTop will move the cursor to the top of the channels
func (c *Channels) MoveCursorTop() {
c.SetSelectedChannel(0)
c.CursorPosition = c.List.InnerBounds().Min.Y
c.Offset = 0
}
// MoveCursorBottom will move the cursor to the bottom of the channels
func (c *Channels) MoveCursorBottom() {
c.SetSelectedChannel(len(c.ChannelItems) - 1)
offset := len(c.ChannelItems) - (c.List.InnerBounds().Max.Y - 1)
if offset < 0 {
c.Offset = 0
c.CursorPosition = c.SelectedChannel + 1
} else {
c.Offset = offset
c.CursorPosition = c.List.InnerBounds().Max.Y - 1
}
}
2016-10-02 16:07:35 +02:00
// ScrollUp enables us to scroll through the channel list when it overflows
2016-10-01 12:48:15 +02:00
func (c *Channels) ScrollUp() {
2017-07-15 23:56:39 +02:00
// Is cursor at the top of the channel view?
2016-10-01 12:48:15 +02:00
if c.CursorPosition == c.List.InnerBounds().Min.Y {
if c.Offset > 0 {
c.Offset--
}
} else {
c.CursorPosition--
}
}
2016-10-02 16:07:35 +02:00
// ScrollDown enables us to scroll through the channel list when it overflows
2016-10-01 12:48:15 +02:00
func (c *Channels) ScrollDown() {
2017-07-15 23:56:39 +02:00
// Is the cursor at the bottom of the channel view?
2016-10-01 12:48:15 +02:00
if c.CursorPosition == c.List.InnerBounds().Max.Y-1 {
if c.Offset < len(c.ChannelItems)-1 {
2016-10-01 12:48:15 +02:00
c.Offset++
}
} else {
c.CursorPosition++
}
}
2017-07-15 23:56:39 +02:00
// Search will search through the channels to find a channel,
// when a match has been found the selected channel will then
// be the channel that has been found
func (c *Channels) Search(term string) {
2017-12-17 13:48:20 +01:00
c.SearchMatches = make([]int, 0)
2017-07-15 23:56:39 +02:00
targets := make([]string, 0)
for _, c := range c.ChannelItems {
2018-09-15 12:42:15 +02:00
targets = append(targets, c.Name)
}
matches := fuzzy.Find(term, targets)
2017-07-15 23:56:39 +02:00
2017-12-17 13:48:20 +01:00
for _, m := range matches {
for i, item := range c.ChannelItems {
if m == item.Name {
2017-12-17 13:48:20 +01:00
c.SearchMatches = append(c.SearchMatches, i)
break
}
}
}
2017-07-15 23:56:39 +02:00
2017-12-17 14:25:39 +01:00
if len(c.SearchMatches) > 0 {
c.GotoPositionSearch(0)
2017-12-17 14:25:39 +01:00
c.SearchPosition = 0
}
2017-12-17 13:48:20 +01:00
}
2017-07-15 23:56:39 +02:00
// GotoPosition is used by to automatically scroll to a specific
// location in the channels component
func (c *Channels) GotoPosition(newPos int) {
2017-07-15 23:56:39 +02:00
2017-12-17 13:48:20 +01:00
// Is the new position in range of the current view?
minRange := c.Offset
maxRange := c.Offset + (c.List.InnerBounds().Max.Y - 2)
2017-07-15 23:56:39 +02:00
2017-12-17 13:48:20 +01:00
if newPos < minRange {
// newPos is above, we need to scroll up.
c.SetSelectedChannel(newPos)
2017-07-15 23:56:39 +02:00
2017-12-17 13:48:20 +01:00
// How much do we need to scroll to get it into range?
c.Offset = c.Offset - (minRange - newPos)
} else if newPos > maxRange {
// newPos is below, we need to scroll down
c.SetSelectedChannel(newPos)
// How much do we need to scroll to get it into range?
c.Offset = c.Offset + (newPos - maxRange)
} else {
// newPos is inside range
c.SetSelectedChannel(newPos)
}
// Set cursor to correct position
c.CursorPosition = (newPos - c.Offset) + 1
}
// GotoPosition is used by the search functionality to automatically
// scroll to a specific location in the channels component
func (c *Channels) GotoPositionSearch(position int) {
newPos := c.SearchMatches[position]
c.GotoPosition(newPos)
}
2017-12-17 13:48:20 +01:00
// SearchNext allows us to cycle through the c.SearchMatches
func (c *Channels) SearchNext() {
newPosition := c.SearchPosition + 1
if newPosition <= len(c.SearchMatches)-1 {
c.GotoPositionSearch(newPosition)
2017-12-17 13:48:20 +01:00
c.SearchPosition = newPosition
}
}
// SearchPrev allows us to cycle through the c.SearchMatches
func (c *Channels) SearchPrev() {
newPosition := c.SearchPosition - 1
if newPosition >= 0 {
c.GotoPositionSearch(newPosition)
2017-12-17 13:48:20 +01:00
c.SearchPosition = newPosition
2017-07-15 23:56:39 +02:00
}
}
// Jump to the first channel with a notification
func (c *Channels) Jump() {
for i, channel := range c.ChannelItems {
if channel.Notification {
c.GotoPosition(i)
break
}
}
}