396 lines
9.1 KiB
Go
396 lines
9.1 KiB
Go
package components
|
|
|
|
import (
|
|
"fmt"
|
|
"html"
|
|
|
|
"github.com/erroneousboat/termui"
|
|
"github.com/renstrom/fuzzysearch/fuzzy"
|
|
)
|
|
|
|
const (
|
|
IconOnline = "●"
|
|
IconOffline = "○"
|
|
IconChannel = "#"
|
|
IconGroup = "☰"
|
|
IconIM = "●"
|
|
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
|
|
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
|
|
}
|
|
|
|
// Channels is the definition of a Channels component
|
|
type Channels struct {
|
|
ChannelItems []ChannelItem
|
|
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'
|
|
|
|
SearchMatches []int // index of the search matches
|
|
SearchPosition int // current position of a search match
|
|
}
|
|
|
|
// CreateChannels is the constructor for the Channels component
|
|
func CreateChannelsComponent(height int) *Channels {
|
|
channels := &Channels{
|
|
List: termui.NewList(),
|
|
}
|
|
|
|
channels.List.BorderLabel = "Channels"
|
|
channels.List.Height = height
|
|
|
|
channels.SelectedChannel = 0
|
|
channels.Offset = 0
|
|
channels.CursorPosition = channels.List.InnerBounds().Min.Y
|
|
|
|
return channels
|
|
}
|
|
|
|
// Buffer implements interface termui.Bufferer
|
|
func (c *Channels) Buffer() termui.Buffer {
|
|
buf := c.List.Buffer()
|
|
|
|
for i, item := range c.ChannelItems[c.Offset:] {
|
|
|
|
y := c.List.InnerBounds().Min.Y + i
|
|
|
|
if y > c.List.InnerBounds().Max.Y-1 {
|
|
break
|
|
}
|
|
|
|
// Set the visible cursor
|
|
var cells []termui.Cell
|
|
if y == c.CursorPosition {
|
|
cells = termui.DefaultTxBuilder.Build(
|
|
item.ToString(), c.List.ItemBgColor, c.List.ItemFgColor)
|
|
} else {
|
|
cells = termui.DefaultTxBuilder.Build(
|
|
item.ToString(), c.List.ItemFgColor, c.List.ItemBgColor)
|
|
}
|
|
|
|
// Append ellipsis when overflows
|
|
cells = termui.DTrimTxCls(cells, c.List.InnerWidth())
|
|
|
|
x := c.List.InnerBounds().Min.X
|
|
for _, cell := range cells {
|
|
buf.Set(x, y, cell)
|
|
x += cell.Width()
|
|
}
|
|
|
|
// When not at the end of the pane fill it up empty characters
|
|
for x < c.List.InnerBounds().Max.X {
|
|
if y == c.CursorPosition {
|
|
buf.Set(x, y,
|
|
termui.Cell{
|
|
Ch: ' ',
|
|
Fg: c.List.ItemBgColor,
|
|
Bg: c.List.ItemFgColor,
|
|
},
|
|
)
|
|
} else {
|
|
buf.Set(
|
|
x, y,
|
|
termui.Cell{
|
|
Ch: ' ',
|
|
Fg: c.List.ItemFgColor,
|
|
Bg: c.List.ItemBgColor,
|
|
},
|
|
)
|
|
}
|
|
x++
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// SetSelectedChannel sets the SelectedChannel given the index
|
|
func (c *Channels) SetSelectedChannel(index int) {
|
|
c.SelectedChannel = index
|
|
}
|
|
|
|
// Get SelectedChannel returns the ChannelItem that is currently selected
|
|
func (c *Channels) GetSelectedChannel() ChannelItem {
|
|
return c.ChannelItems[c.SelectedChannel]
|
|
}
|
|
|
|
// MoveCursorUp will decrease the SelectedChannel by 1
|
|
func (c *Channels) MoveCursorUp() {
|
|
if c.SelectedChannel > 0 {
|
|
c.SetSelectedChannel(c.SelectedChannel - 1)
|
|
c.ScrollUp()
|
|
}
|
|
}
|
|
|
|
// MoveCursorDown will increase the SelectedChannel by 1
|
|
func (c *Channels) MoveCursorDown() {
|
|
if c.SelectedChannel < len(c.ChannelItems)-1 {
|
|
c.SetSelectedChannel(c.SelectedChannel + 1)
|
|
c.ScrollDown()
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
// ScrollUp enables us to scroll through the channel list when it overflows
|
|
func (c *Channels) ScrollUp() {
|
|
// Is cursor at the top of the channel view?
|
|
if c.CursorPosition == c.List.InnerBounds().Min.Y {
|
|
if c.Offset > 0 {
|
|
c.Offset--
|
|
}
|
|
} else {
|
|
c.CursorPosition--
|
|
}
|
|
}
|
|
|
|
// ScrollDown enables us to scroll through the channel list when it overflows
|
|
func (c *Channels) ScrollDown() {
|
|
// Is the cursor at the bottom of the channel view?
|
|
if c.CursorPosition == c.List.InnerBounds().Max.Y-1 {
|
|
if c.Offset < len(c.ChannelItems)-1 {
|
|
c.Offset++
|
|
}
|
|
} else {
|
|
c.CursorPosition++
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
c.SearchMatches = make([]int, 0)
|
|
|
|
targets := make([]string, 0)
|
|
for _, c := range c.ChannelItems {
|
|
targets = append(targets, c.Name)
|
|
}
|
|
|
|
matches := fuzzy.Find(term, targets)
|
|
|
|
for _, m := range matches {
|
|
for i, item := range c.ChannelItems {
|
|
if m == item.Name {
|
|
c.SearchMatches = append(c.SearchMatches, i)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(c.SearchMatches) > 0 {
|
|
c.GotoPositionSearch(0)
|
|
c.SearchPosition = 0
|
|
}
|
|
}
|
|
|
|
// GotoPosition is used by to automatically scroll to a specific
|
|
// location in the channels component
|
|
func (c *Channels) GotoPosition(newPos int) {
|
|
|
|
// Is the new position in range of the current view?
|
|
minRange := c.Offset
|
|
maxRange := c.Offset + (c.List.InnerBounds().Max.Y - 2)
|
|
|
|
if newPos < minRange {
|
|
// newPos is above, we need to scroll up.
|
|
c.SetSelectedChannel(newPos)
|
|
|
|
// 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)
|
|
}
|
|
|
|
// 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)
|
|
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)
|
|
c.SearchPosition = newPosition
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
}
|