Convert message buffer to use custom colorable strings

This commit is contained in:
Tulir Asokan 2018-04-10 16:07:16 +03:00
parent b6e58e83a8
commit ee67c1446c
5 changed files with 264 additions and 64 deletions

View File

@ -87,8 +87,9 @@ type MessageMeta interface {
SenderColor() tcell.Color SenderColor() tcell.Color
TextColor() tcell.Color TextColor() tcell.Color
TimestampColor() tcell.Color TimestampColor() tcell.Color
Timestamp() string Timestamp() time.Time
Date() string FormatTime() string
FormatDate() string
CopyFrom(from MessageMeta) CopyFrom(from MessageMeta)
} }

View File

@ -49,22 +49,20 @@ type MessageView struct {
messageIDs map[string]messages.UIMessage messageIDs map[string]messages.UIMessage
messages []messages.UIMessage messages []messages.UIMessage
textBuffer []string textBuffer []messages.UIString
metaBuffer []ifc.MessageMeta metaBuffer []ifc.MessageMeta
} }
func NewMessageView() *MessageView { func NewMessageView() *MessageView {
return &MessageView{ return &MessageView{
Box: tview.NewBox(), Box: tview.NewBox(),
MaxSenderWidth: 15, MaxSenderWidth: 15,
DateFormat: "January _2, 2006", TimestampWidth: len(messages.TimeFormat),
TimestampFormat: "15:04:05", ScrollOffset: 0,
TimestampWidth: 8,
ScrollOffset: 0,
messages: make([]messages.UIMessage, 0), messages: make([]messages.UIMessage, 0),
messageIDs: make(map[string]messages.UIMessage), messageIDs: make(map[string]messages.UIMessage),
textBuffer: make([]string, 0), textBuffer: make([]messages.UIString, 0),
metaBuffer: make([]ifc.MessageMeta, 0), metaBuffer: make([]ifc.MessageMeta, 0),
widestSender: 5, widestSender: 5,
@ -75,10 +73,7 @@ func NewMessageView() *MessageView {
} }
func (view *MessageView) NewMessage(id, sender, msgtype, text string, timestamp time.Time) messages.UIMessage { func (view *MessageView) NewMessage(id, sender, msgtype, text string, timestamp time.Time) messages.UIMessage {
return messages.NewMessage(id, sender, msgtype, text, return messages.NewMessage(id, sender, msgtype, text, timestamp, widget.GetHashColor(sender))
timestamp.Format(view.TimestampFormat),
timestamp.Format(view.DateFormat),
widget.GetHashColor(sender))
} }
func (view *MessageView) SaveHistory(path string) error { func (view *MessageView) SaveHistory(path string) error {
@ -152,10 +147,10 @@ func (view *MessageView) AddMessage(ifcMessage ifc.Message, direction ifc.Messag
return return
} }
msg, messageExists := view.messageIDs[message.ID()] oldMsg, messageExists := view.messageIDs[message.ID()]
if msg != nil && messageExists { if messageExists {
msg.CopyFrom(message) oldMsg.CopyFrom(message)
message = msg message = oldMsg
direction = ifc.IgnoreMessage direction = ifc.IgnoreMessage
} }
@ -173,6 +168,8 @@ func (view *MessageView) AddMessage(ifcMessage ifc.Message, direction ifc.Messag
view.appendBuffer(message) view.appendBuffer(message)
} else if direction == ifc.PrependMessage { } else if direction == ifc.PrependMessage {
view.messages = append([]messages.UIMessage{message}, view.messages...) view.messages = append([]messages.UIMessage{message}, view.messages...)
} else {
view.replaceBuffer(message)
} }
view.messageIDs[message.ID()] = message view.messageIDs[message.ID()] = message
@ -181,9 +178,12 @@ func (view *MessageView) AddMessage(ifcMessage ifc.Message, direction ifc.Messag
func (view *MessageView) appendBuffer(message messages.UIMessage) { func (view *MessageView) appendBuffer(message messages.UIMessage) {
if len(view.metaBuffer) > 0 { if len(view.metaBuffer) > 0 {
prevMeta := view.metaBuffer[len(view.metaBuffer)-1] prevMeta := view.metaBuffer[len(view.metaBuffer)-1]
if prevMeta != nil && prevMeta.Date() != message.Date() { if prevMeta != nil && prevMeta.FormatDate() != message.FormatDate() {
view.textBuffer = append(view.textBuffer, fmt.Sprintf("Date changed to %s", message.Date())) view.textBuffer = append(view.textBuffer, messages.NewColorUIString(
view.metaBuffer = append(view.metaBuffer, &messages.BasicMeta{BTextColor: tcell.ColorGreen}) fmt.Sprintf("Date changed to %s", message.FormatDate()),
tcell.ColorGreen))
view.metaBuffer = append(view.metaBuffer, &messages.BasicMeta{
BTimestampColor: tcell.ColorDefault, BTextColor: tcell.ColorGreen})
} }
} }
@ -194,13 +194,42 @@ func (view *MessageView) appendBuffer(message messages.UIMessage) {
view.prevMsgCount++ view.prevMsgCount++
} }
func (view *MessageView) replaceBuffer(message messages.UIMessage) {
start := -1
end := -1
for index, meta := range view.metaBuffer {
if meta == message {
if start == -1 {
start = index
}
end = index
} else if start != -1 {
break
}
}
if len(view.textBuffer) > end {
end++
}
view.textBuffer = append(append(view.textBuffer[0:start], message.Buffer()...), view.textBuffer[end:]...)
if len(message.Buffer()) != end - start + 1 {
debug.Print(end, "-", start, "!=", len(message.Buffer()))
metaBuffer := view.metaBuffer[0:start]
for range message.Buffer() {
metaBuffer = append(metaBuffer, message)
}
view.metaBuffer = append(metaBuffer, view.metaBuffer[end:]...)
}
}
func (view *MessageView) recalculateBuffers() { func (view *MessageView) recalculateBuffers() {
_, _, width, height := view.GetInnerRect() _, _, width, height := view.GetInnerRect()
width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap
recalculateMessageBuffers := width != view.prevWidth recalculateMessageBuffers := width != view.prevWidth
if height != view.prevHeight || recalculateMessageBuffers || len(view.messages) != view.prevMsgCount { if height != view.prevHeight || recalculateMessageBuffers || len(view.messages) != view.prevMsgCount {
view.textBuffer = []string{} view.textBuffer = []messages.UIString{}
view.metaBuffer = []ifc.MessageMeta{} view.metaBuffer = []ifc.MessageMeta{}
view.prevMsgCount = 0 view.prevMsgCount = 0
for _, message := range view.messages { for _, message := range view.messages {
@ -219,7 +248,7 @@ const PaddingAtTop = 5
func (view *MessageView) AddScrollOffset(diff int) { func (view *MessageView) AddScrollOffset(diff int) {
_, _, _, height := view.GetInnerRect() _, _, _, height := view.GetInnerRect()
totalHeight := len(view.textBuffer) totalHeight := view.TotalHeight()
if diff >= 0 && view.ScrollOffset+diff >= totalHeight-height+PaddingAtTop { if diff >= 0 && view.ScrollOffset+diff >= totalHeight-height+PaddingAtTop {
view.ScrollOffset = totalHeight - height + PaddingAtTop view.ScrollOffset = totalHeight - height + PaddingAtTop
} else { } else {
@ -285,7 +314,7 @@ func (view *MessageView) Draw(screen tcell.Screen) {
x, y, _, height := view.GetInnerRect() x, y, _, height := view.GetInnerRect()
view.recalculateBuffers() view.recalculateBuffers()
if len(view.textBuffer) == 0 { if view.TotalHeight() == 0 {
widget.WriteLineSimple(screen, "It's quite empty in here.", x, y+height) widget.WriteLineSimple(screen, "It's quite empty in here.", x, y+height)
return return
} }
@ -294,7 +323,7 @@ func (view *MessageView) Draw(screen tcell.Screen) {
messageX := usernameX + view.widestSender + SenderMessageGap messageX := usernameX + view.widestSender + SenderMessageGap
separatorX := usernameX + view.widestSender + SenderSeparatorGap separatorX := usernameX + view.widestSender + SenderSeparatorGap
indexOffset := len(view.textBuffer) - view.ScrollOffset - height indexOffset := view.TotalHeight() - view.ScrollOffset - height
if indexOffset <= -PaddingAtTop { if indexOffset <= -PaddingAtTop {
message := "Scroll up to load more messages." message := "Scroll up to load more messages."
if view.LoadingMessages { if view.LoadingMessages {
@ -312,7 +341,7 @@ func (view *MessageView) Draw(screen tcell.Screen) {
// Black magic (aka math) used to figure out where the scroll bar should be put. // Black magic (aka math) used to figure out where the scroll bar should be put.
{ {
viewportHeight := float64(height) viewportHeight := float64(height)
contentHeight := float64(len(view.textBuffer)) contentHeight := float64(view.TotalHeight())
scrollBarHeight = int(math.Ceil(viewportHeight / (contentHeight / viewportHeight))) scrollBarHeight = int(math.Ceil(viewportHeight / (contentHeight / viewportHeight)))
@ -328,12 +357,12 @@ func (view *MessageView) Draw(screen tcell.Screen) {
if index < 0 { if index < 0 {
skippedLines++ skippedLines++
continue continue
} else if index >= len(view.textBuffer) { } else if index >= view.TotalHeight() {
break break
} }
showScrollbar := line-skippedLines >= scrollBarPos-scrollBarHeight && line-skippedLines < scrollBarPos showScrollbar := line-skippedLines >= scrollBarPos-scrollBarHeight && line-skippedLines < scrollBarPos
isTop := firstLine && view.ScrollOffset+height >= len(view.textBuffer) isTop := firstLine && view.ScrollOffset+height >= view.TotalHeight()
isBottom := line == height-1 && view.ScrollOffset == 0 isBottom := line == height-1 && view.ScrollOffset == 0
borderChar, borderStyle := getScrollbarStyle(showScrollbar, isTop, isBottom) borderChar, borderStyle := getScrollbarStyle(showScrollbar, isTop, isBottom)
@ -344,8 +373,8 @@ func (view *MessageView) Draw(screen tcell.Screen) {
text, meta := view.textBuffer[index], view.metaBuffer[index] text, meta := view.textBuffer[index], view.metaBuffer[index]
if meta != prevMeta { if meta != prevMeta {
if len(meta.Timestamp()) > 0 { if len(meta.FormatTime()) > 0 {
widget.WriteLineSimpleColor(screen, meta.Timestamp(), x, y+line, meta.TimestampColor()) widget.WriteLineSimpleColor(screen, meta.FormatTime(), x, y+line, meta.TimestampColor())
} }
if prevMeta == nil || meta.Sender() != prevMeta.Sender() { if prevMeta == nil || meta.Sender() != prevMeta.Sender() {
widget.WriteLineColor( widget.WriteLineColor(
@ -355,6 +384,7 @@ func (view *MessageView) Draw(screen tcell.Screen) {
} }
prevMeta = meta prevMeta = meta
} }
widget.WriteLineSimpleColor(screen, text, messageX, y+line, meta.TextColor())
text.Draw(screen, messageX, y+line)
} }
} }

View File

@ -16,7 +16,13 @@
package messages package messages
import "maunium.net/go/gomuks/interface" import (
"strings"
"github.com/gdamore/tcell"
"github.com/mattn/go-runewidth"
"maunium.net/go/gomuks/interface"
)
// Message is a wrapper for the content and metadata of a Matrix message intended to be displayed. // Message is a wrapper for the content and metadata of a Matrix message intended to be displayed.
type UIMessage interface { type UIMessage interface {
@ -24,6 +30,154 @@ type UIMessage interface {
CalculateBuffer(width int) CalculateBuffer(width int)
RecalculateBuffer() RecalculateBuffer()
Buffer() []string Buffer() []UIString
Height() int Height() int
RealSender() string
} }
type Cell struct {
Char rune
Style tcell.Style
}
func NewStyleCell(char rune, style tcell.Style) Cell {
return Cell{char, style}
}
func NewColorCell(char rune, color tcell.Color) Cell {
return Cell{char, tcell.StyleDefault.Foreground(color)}
}
func NewCell(char rune) Cell {
return Cell{char, tcell.StyleDefault}
}
func (cell Cell) RuneWidth() int {
return runewidth.RuneWidth(cell.Char)
}
func (cell Cell) Draw(screen tcell.Screen, x, y int) (chWidth int) {
chWidth = cell.RuneWidth()
for runeWidthOffset := 0; runeWidthOffset < chWidth; runeWidthOffset++ {
screen.SetContent(x+runeWidthOffset, y, cell.Char, nil, cell.Style)
}
return
}
type UIString []Cell
func NewUIString(str string) UIString {
newStr := make([]Cell, len(str))
for i, char := range str {
newStr[i] = NewCell(char)
}
return newStr
}
func NewColorUIString(str string, color tcell.Color) UIString {
newStr := make([]Cell, len(str))
for i, char := range str {
newStr[i] = NewColorCell(char, color)
}
return newStr
}
func NewStyleUIString(str string, style tcell.Style) UIString {
newStr := make([]Cell, len(str))
for i, char := range str {
newStr[i] = NewStyleCell(char, style)
}
return newStr
}
func (str UIString) Colorize(from, to int, color tcell.Color) {
for i := from; i < to; i++ {
str[i].Style = str[i].Style.Foreground(color)
}
}
func (str UIString) Draw(screen tcell.Screen, x, y int) {
offsetX := 0
for _, cell := range str {
offsetX += cell.Draw(screen, x+offsetX, y)
}
}
func (str UIString) RuneWidth() (width int) {
for _, cell := range str {
width += runewidth.RuneWidth(cell.Char)
}
return width
}
func (str UIString) String() string {
var buf strings.Builder
for _, cell := range str {
buf.WriteRune(cell.Char)
}
return buf.String()
}
// Truncate return string truncated with w cells
func (str UIString) Truncate(w int) UIString {
if str.RuneWidth() <= w {
return str[:]
}
width := 0
i := 0
for ; i < len(str); i++ {
cw := runewidth.RuneWidth(str[i].Char)
if width+cw > w {
break
}
width += cw
}
return str[0:i]
}
func (str UIString) IndexFrom(r rune, from int) int {
for i := from; i < len(str); i++ {
if str[i].Char == r {
return i
}
}
return -1
}
func (str UIString) Index(r rune) int {
return str.IndexFrom(r, 0)
}
func (str UIString) Count(r rune) (counter int) {
index := 0
for {
index = str.IndexFrom(r, index)
if index < 0 {
break
}
index++
counter++
}
return
}
func (str UIString) Split(sep rune) []UIString {
a := make([]UIString, str.Count(sep)+1)
i := 0
orig := str
for {
m := orig.Index(sep)
if m < 0 {
break
}
a[i] = orig[:m]
orig = orig[m+1:]
i++
}
a[i] = orig
return a[:i+1]
}
const DateFormat = "January _2, 2006"
const TimeFormat = "15:04:05"

View File

@ -17,13 +17,16 @@
package messages package messages
import ( import (
"time"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/interface"
) )
// BasicMeta is a simple variable store implementation of MessageMeta. // BasicMeta is a simple variable store implementation of MessageMeta.
type BasicMeta struct { type BasicMeta struct {
BSender, BTimestamp, BDate string BSender string
BTimestamp time.Time
BSenderColor, BTextColor, BTimestampColor tcell.Color BSenderColor, BTextColor, BTimestampColor tcell.Color
} }
@ -37,14 +40,19 @@ func (meta *BasicMeta) SenderColor() tcell.Color {
return meta.BSenderColor return meta.BSenderColor
} }
// Timestamp returns the formatted time when the message was sent. // Timestamp returns the full time when the message was sent.
func (meta *BasicMeta) Timestamp() string { func (meta *BasicMeta) Timestamp() time.Time {
return meta.BTimestamp return meta.BTimestamp
} }
// Date returns the formatted date when the message was sent. // FormatTime returns the formatted time when the message was sent.
func (meta *BasicMeta) Date() string { func (meta *BasicMeta) FormatTime() string {
return meta.BDate return meta.BTimestamp.Format(TimeFormat)
}
// FormatDate returns the formatted date when the message was sent.
func (meta *BasicMeta) FormatDate() string {
return meta.BTimestamp.Format(DateFormat)
} }
// TextColor returns the color the actual content of the message should be shown in. // TextColor returns the color the actual content of the message should be shown in.
@ -63,7 +71,6 @@ func (meta *BasicMeta) TimestampColor() tcell.Color {
func (meta *BasicMeta) CopyFrom(from ifc.MessageMeta) { func (meta *BasicMeta) CopyFrom(from ifc.MessageMeta) {
meta.BSender = from.Sender() meta.BSender = from.Sender()
meta.BTimestamp = from.Timestamp() meta.BTimestamp = from.Timestamp()
meta.BDate = from.Date()
meta.BSenderColor = from.SenderColor() meta.BSenderColor = from.SenderColor()
meta.BTextColor = from.TextColor() meta.BTextColor = from.TextColor()
meta.BTimestampColor = from.TimestampColor() meta.BTimestampColor = from.TimestampColor()

View File

@ -20,10 +20,9 @@ import (
"encoding/gob" "encoding/gob"
"fmt" "fmt"
"regexp" "regexp"
"strings" "time"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"github.com/mattn/go-runewidth"
"maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/interface"
) )
@ -36,22 +35,20 @@ type UITextMessage struct {
MsgType string MsgType string
MsgSender string MsgSender string
MsgSenderColor tcell.Color MsgSenderColor tcell.Color
MsgTimestamp string MsgTimestamp time.Time
MsgDate string
MsgText string MsgText string
MsgState ifc.MessageState MsgState ifc.MessageState
MsgIsHighlight bool MsgIsHighlight bool
MsgIsService bool MsgIsService bool
buffer []string buffer []UIString
prevBufferWidth int prevBufferWidth int
} }
// NewMessage creates a new Message object with the provided values and the default state. // NewMessage creates a new Message object with the provided values and the default state.
func NewMessage(id, sender, msgtype, text, timestamp, date string, senderColor tcell.Color) UIMessage { func NewMessage(id, sender, msgtype, text string, timestamp time.Time, senderColor tcell.Color) UIMessage {
return &UITextMessage{ return &UITextMessage{
MsgSender: sender, MsgSender: sender,
MsgTimestamp: timestamp, MsgTimestamp: timestamp,
MsgDate: date,
MsgSenderColor: senderColor, MsgSenderColor: senderColor,
MsgType: msgtype, MsgType: msgtype,
MsgText: text, MsgText: text,
@ -66,18 +63,19 @@ func NewMessage(id, sender, msgtype, text, timestamp, date string, senderColor t
// CopyFrom replaces the content of this message object with the content of the given object. // CopyFrom replaces the content of this message object with the content of the given object.
func (msg *UITextMessage) CopyFrom(from ifc.MessageMeta) { func (msg *UITextMessage) CopyFrom(from ifc.MessageMeta) {
msg.MsgSender = from.Sender() msg.MsgSender = from.Sender()
msg.MsgTimestamp = from.Timestamp()
msg.MsgDate = from.Date()
msg.MsgSenderColor = from.SenderColor() msg.MsgSenderColor = from.SenderColor()
fromMsg, ok := from.(UIMessage) fromMsg, ok := from.(UIMessage)
if ok { if ok {
msg.MsgSender = fromMsg.RealSender()
msg.MsgID = fromMsg.ID() msg.MsgID = fromMsg.ID()
msg.MsgType = fromMsg.Type() msg.MsgType = fromMsg.Type()
msg.MsgTimestamp = fromMsg.Timestamp()
msg.MsgText = fromMsg.Text() msg.MsgText = fromMsg.Text()
msg.MsgState = fromMsg.State() msg.MsgState = fromMsg.State()
msg.MsgIsService = fromMsg.IsService() msg.MsgIsService = fromMsg.IsService()
msg.MsgIsHighlight = fromMsg.IsHighlight() msg.MsgIsHighlight = fromMsg.IsHighlight()
msg.buffer = nil
msg.RecalculateBuffer() msg.RecalculateBuffer()
} }
@ -105,6 +103,10 @@ func (msg *UITextMessage) Sender() string {
} }
} }
func (msg *UITextMessage) RealSender() string {
return msg.MsgSender
}
func (msg *UITextMessage) getStateSpecificColor() tcell.Color { func (msg *UITextMessage) getStateSpecificColor() tcell.Color {
switch msg.MsgState { switch msg.MsgState {
case ifc.MessageStateSending: case ifc.MessageStateSending:
@ -176,7 +178,7 @@ func (msg *UITextMessage) RecalculateBuffer() {
// //
// N.B. This will NOT automatically calculate the buffer if it hasn't been // N.B. This will NOT automatically calculate the buffer if it hasn't been
// calculated already, as that requires the target width. // calculated already, as that requires the target width.
func (msg *UITextMessage) Buffer() []string { func (msg *UITextMessage) Buffer() []UIString {
return msg.buffer return msg.buffer
} }
@ -185,14 +187,19 @@ func (msg *UITextMessage) Height() int {
return len(msg.buffer) return len(msg.buffer)
} }
// Timestamp returns the formatted time when the message was sent. // Timestamp returns the full timestamp when the message was sent.
func (msg *UITextMessage) Timestamp() string { func (msg *UITextMessage) Timestamp() time.Time {
return msg.MsgTimestamp return msg.MsgTimestamp
} }
// Date returns the formatted date when the message was sent. // FormatTime returns the formatted time when the message was sent.
func (msg *UITextMessage) Date() string { func (msg *UITextMessage) FormatTime() string {
return msg.MsgDate return msg.MsgTimestamp.Format(TimeFormat)
}
// FormatDate returns the formatted date when the message was sent.
func (msg *UITextMessage) FormatDate() string {
return msg.MsgTimestamp.Format(DateFormat)
} }
func (msg *UITextMessage) ID() string { func (msg *UITextMessage) ID() string {
@ -259,30 +266,31 @@ func (msg *UITextMessage) CalculateBuffer(width int) {
return return
} }
msg.buffer = []string{} msg.buffer = []UIString{}
text := msg.MsgText text := NewColorUIString(msg.Text(), msg.TextColor())
if msg.MsgType == "m.emote" { if msg.MsgType == "m.emote" {
text = fmt.Sprintf("* %s %s", msg.MsgSender, msg.MsgText) text = NewColorUIString(fmt.Sprintf("* %s %s", msg.MsgSender, msg.MsgText), msg.TextColor())
text.Colorize(2, 2+len(msg.MsgSender), msg.SenderColor())
} }
forcedLinebreaks := strings.Split(text, "\n") forcedLinebreaks := text.Split('\n')
newlines := 0 newlines := 0
for _, str := range forcedLinebreaks { for _, str := range forcedLinebreaks {
if len(str) == 0 && newlines < 1 { if len(str) == 0 && newlines < 1 {
msg.buffer = append(msg.buffer, "") msg.buffer = append(msg.buffer, UIString{})
newlines++ newlines++
} else { } else {
newlines = 0 newlines = 0
} }
// From tview/textview.go#reindexBuffer() // Mostly from tview/textview.go#reindexBuffer()
for len(str) > 0 { for len(str) > 0 {
extract := runewidth.Truncate(str, width, "") extract := str.Truncate(width)
if len(extract) < len(str) { if len(extract) < len(str) {
if spaces := spacePattern.FindStringIndex(str[len(extract):]); spaces != nil && spaces[0] == 0 { if spaces := spacePattern.FindStringIndex(str[len(extract):].String()); spaces != nil && spaces[0] == 0 {
extract = str[:len(extract)+spaces[1]] extract = str[:len(extract)+spaces[1]]
} }
matches := boundaryPattern.FindAllStringIndex(extract, -1) matches := boundaryPattern.FindAllStringIndex(extract.String(), -1)
if len(matches) > 0 { if len(matches) > 0 {
extract = extract[:matches[len(matches)-1][1]] extract = extract[:matches[len(matches)-1][1]]
} }