From eda2b575f06e72040ebf82d24a7ec1ac84b7948c Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 9 Apr 2018 23:45:54 +0300 Subject: Refactor UI to use interfaces everywhere --- ui/messages/doc.go | 2 + ui/messages/message.go | 29 +++++ ui/messages/meta.go | 70 +++++++++++ ui/messages/textmessage.go | 295 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 396 insertions(+) create mode 100644 ui/messages/doc.go create mode 100644 ui/messages/message.go create mode 100644 ui/messages/meta.go create mode 100644 ui/messages/textmessage.go (limited to 'ui/messages') diff --git a/ui/messages/doc.go b/ui/messages/doc.go new file mode 100644 index 0000000..7c3c077 --- /dev/null +++ b/ui/messages/doc.go @@ -0,0 +1,2 @@ +// Package types contains common type definitions used by the UI. +package messages diff --git a/ui/messages/message.go b/ui/messages/message.go new file mode 100644 index 0000000..ef0966c --- /dev/null +++ b/ui/messages/message.go @@ -0,0 +1,29 @@ +// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package messages + +import "maunium.net/go/gomuks/interface" + +// Message is a wrapper for the content and metadata of a Matrix message intended to be displayed. +type UIMessage interface { + ifc.Message + + CalculateBuffer(width int) + RecalculateBuffer() + Buffer() []string + Height() int +} diff --git a/ui/messages/meta.go b/ui/messages/meta.go new file mode 100644 index 0000000..8cb6c1b --- /dev/null +++ b/ui/messages/meta.go @@ -0,0 +1,70 @@ +// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package messages + +import ( + "github.com/gdamore/tcell" + "maunium.net/go/gomuks/interface" +) + +// BasicMeta is a simple variable store implementation of MessageMeta. +type BasicMeta struct { + BSender, BTimestamp, BDate string + BSenderColor, BTextColor, BTimestampColor tcell.Color +} + +// Sender gets the string that should be displayed as the sender of this message. +func (meta *BasicMeta) Sender() string { + return meta.BSender +} + +// SenderColor returns the color the name of the sender should be shown in. +func (meta *BasicMeta) SenderColor() tcell.Color { + return meta.BSenderColor +} + +// Timestamp returns the formatted time when the message was sent. +func (meta *BasicMeta) Timestamp() string { + return meta.BTimestamp +} + +// Date returns the formatted date when the message was sent. +func (meta *BasicMeta) Date() string { + return meta.BDate +} + +// TextColor returns the color the actual content of the message should be shown in. +func (meta *BasicMeta) TextColor() tcell.Color { + return meta.BTextColor +} + +// TimestampColor returns the color the timestamp should be shown in. +// +// This usually does not apply to the date, as it is rendered separately from the message. +func (meta *BasicMeta) TimestampColor() tcell.Color { + return meta.BTimestampColor +} + +// CopyFrom replaces the content of this meta object with the content of the given object. +func (meta *BasicMeta) CopyFrom(from ifc.MessageMeta) { + meta.BSender = from.Sender() + meta.BTimestamp = from.Timestamp() + meta.BDate = from.Date() + meta.BSenderColor = from.SenderColor() + meta.BTextColor = from.TextColor() + meta.BTimestampColor = from.TimestampColor() +} diff --git a/ui/messages/textmessage.go b/ui/messages/textmessage.go new file mode 100644 index 0000000..1f37418 --- /dev/null +++ b/ui/messages/textmessage.go @@ -0,0 +1,295 @@ +// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package messages + +import ( + "encoding/gob" + "fmt" + "regexp" + "strings" + + "github.com/gdamore/tcell" + "github.com/mattn/go-runewidth" + "maunium.net/go/gomuks/interface" +) + +func init() { + gob.Register(&UITextMessage{}) +} + +type UITextMessage struct { + MsgID string + MsgType string + MsgSender string + MsgSenderColor tcell.Color + MsgTimestamp string + MsgDate string + MsgText string + MsgState ifc.MessageState + MsgIsHighlight bool + MsgIsService bool + buffer []string + prevBufferWidth int +} + +// 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 { + return &UITextMessage{ + MsgSender: sender, + MsgTimestamp: timestamp, + MsgDate: date, + MsgSenderColor: senderColor, + MsgType: msgtype, + MsgText: text, + MsgID: id, + prevBufferWidth: 0, + MsgState: ifc.MessageStateDefault, + MsgIsHighlight: false, + MsgIsService: false, + } +} + +// CopyFrom replaces the content of this message object with the content of the given object. +func (msg *UITextMessage) CopyFrom(from ifc.MessageMeta) { + msg.MsgSender = from.Sender() + msg.MsgTimestamp = from.Timestamp() + msg.MsgDate = from.Date() + msg.MsgSenderColor = from.SenderColor() + + fromMsg, ok := from.(UIMessage) + if ok { + msg.MsgID = fromMsg.ID() + msg.MsgType = fromMsg.Type() + msg.MsgText = fromMsg.Text() + msg.MsgState = fromMsg.State() + msg.MsgIsService = fromMsg.IsService() + msg.MsgIsHighlight = fromMsg.IsHighlight() + + msg.RecalculateBuffer() + } +} + +// Sender gets the string that should be displayed as the sender of this message. +// +// If the message is being sent, the sender is "Sending...". +// If sending has failed, the sender is "Error". +// If the message is an emote, the sender is blank. +// In any other case, the sender is the display name of the user who sent the message. +func (msg *UITextMessage) Sender() string { + switch msg.MsgState { + case ifc.MessageStateSending: + return "Sending..." + case ifc.MessageStateFailed: + return "Error" + } + switch msg.MsgType { + case "m.emote": + // Emotes don't show a separate sender, it's included in the buffer. + return "" + default: + return msg.MsgSender + } +} + +func (msg *UITextMessage) getStateSpecificColor() tcell.Color { + switch msg.MsgState { + case ifc.MessageStateSending: + return tcell.ColorGray + case ifc.MessageStateFailed: + return tcell.ColorRed + case ifc.MessageStateDefault: + fallthrough + default: + return tcell.ColorDefault + } +} + +// SenderColor returns the color the name of the sender should be shown in. +// +// If the message is being sent, the color is gray. +// If sending has failed, the color is red. +// +// In any other case, the color is whatever is specified in the Message struct. +// Usually that means it is the hash-based color of the sender (see ui/widget/color.go) +func (msg *UITextMessage) SenderColor() tcell.Color { + stateColor := msg.getStateSpecificColor() + switch { + case stateColor != tcell.ColorDefault: + return stateColor + case msg.MsgIsService: + return tcell.ColorGray + default: + return msg.MsgSenderColor + } +} + +// TextColor returns the color the actual content of the message should be shown in. +func (msg *UITextMessage) TextColor() tcell.Color { + stateColor := msg.getStateSpecificColor() + switch { + case stateColor != tcell.ColorDefault: + return stateColor + case msg.MsgIsService: + return tcell.ColorGray + case msg.MsgIsHighlight: + return tcell.ColorYellow + case msg.MsgType == "m.room.member": + return tcell.ColorGreen + default: + return tcell.ColorDefault + } +} + +// TimestampColor returns the color the timestamp should be shown in. +// +// As with SenderColor(), messages being sent and messages that failed to be sent are +// gray and red respectively. +// +// However, other messages are the default color instead of a color stored in the struct. +func (msg *UITextMessage) TimestampColor() tcell.Color { + return msg.getStateSpecificColor() +} + +// RecalculateBuffer calculates the buffer again with the previously provided width. +func (msg *UITextMessage) RecalculateBuffer() { + msg.CalculateBuffer(msg.prevBufferWidth) +} + +// Buffer returns the computed text buffer. +// +// The buffer contains the text of the message split into lines with a maximum +// width of whatever was provided to CalculateBuffer(). +// +// N.B. This will NOT automatically calculate the buffer if it hasn't been +// calculated already, as that requires the target width. +func (msg *UITextMessage) Buffer() []string { + return msg.buffer +} + +// Height returns the number of rows in the computed buffer (see Buffer()). +func (msg *UITextMessage) Height() int { + return len(msg.buffer) +} + +// Timestamp returns the formatted time when the message was sent. +func (msg *UITextMessage) Timestamp() string { + return msg.MsgTimestamp +} + +// Date returns the formatted date when the message was sent. +func (msg *UITextMessage) Date() string { + return msg.MsgDate +} + +func (msg *UITextMessage) ID() string { + return msg.MsgID +} + +func (msg *UITextMessage) SetID(id string) { + msg.MsgID = id +} + +func (msg *UITextMessage) Type() string { + return msg.MsgType +} + +func (msg *UITextMessage) SetType(msgtype string) { + msg.MsgType = msgtype +} + +func (msg *UITextMessage) Text() string { + return msg.MsgText +} + +func (msg *UITextMessage) SetText(text string) { + msg.MsgText = text +} + +func (msg *UITextMessage) State() ifc.MessageState { + return msg.MsgState +} + +func (msg *UITextMessage) SetState(state ifc.MessageState) { + msg.MsgState = state +} + +func (msg *UITextMessage) IsHighlight() bool { + return msg.MsgIsHighlight +} + +func (msg *UITextMessage) SetIsHighlight(isHighlight bool) { + msg.MsgIsHighlight = isHighlight +} + +func (msg *UITextMessage) IsService() bool { + return msg.MsgIsService +} + +func (msg *UITextMessage) SetIsService(isService bool) { + msg.MsgIsService = isService +} + +// Regular expressions used to split lines when calculating the buffer. +// +// From tview/textview.go +var ( + boundaryPattern = regexp.MustCompile("([[:punct:]]\\s*|\\s+)") + spacePattern = regexp.MustCompile(`\s+`) +) + +// CalculateBuffer generates the internal buffer for this message that consists +// of the text of this message split into lines at most as wide as the width +// parameter. +func (msg *UITextMessage) CalculateBuffer(width int) { + if width < 2 { + return + } + + msg.buffer = []string{} + text := msg.MsgText + if msg.MsgType == "m.emote" { + text = fmt.Sprintf("* %s %s", msg.MsgSender, msg.MsgText) + } + + forcedLinebreaks := strings.Split(text, "\n") + newlines := 0 + for _, str := range forcedLinebreaks { + if len(str) == 0 && newlines < 1 { + msg.buffer = append(msg.buffer, "") + newlines++ + } else { + newlines = 0 + } + // From tview/textview.go#reindexBuffer() + for len(str) > 0 { + extract := runewidth.Truncate(str, width, "") + if len(extract) < len(str) { + if spaces := spacePattern.FindStringIndex(str[len(extract):]); spaces != nil && spaces[0] == 0 { + extract = str[:len(extract)+spaces[1]] + } + + matches := boundaryPattern.FindAllStringIndex(extract, -1) + if len(matches) > 0 { + extract = extract[:matches[len(matches)-1][1]] + } + } + msg.buffer = append(msg.buffer, extract) + str = str[len(extract):] + } + } + msg.prevBufferWidth = width +} -- cgit v1.2.3-70-g09d2 From ee67c1446cbb3c446d59d4ebd9657a25bf0b702d Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 10 Apr 2018 16:07:16 +0300 Subject: Convert message buffer to use custom colorable strings --- interface/ui.go | 5 +- ui/message-view.go | 88 ++++++++++++++++--------- ui/messages/message.go | 158 ++++++++++++++++++++++++++++++++++++++++++++- ui/messages/meta.go | 21 ++++-- ui/messages/textmessage.go | 56 +++++++++------- 5 files changed, 264 insertions(+), 64 deletions(-) (limited to 'ui/messages') diff --git a/interface/ui.go b/interface/ui.go index 0ddbde7..2d27ea8 100644 --- a/interface/ui.go +++ b/interface/ui.go @@ -87,8 +87,9 @@ type MessageMeta interface { SenderColor() tcell.Color TextColor() tcell.Color TimestampColor() tcell.Color - Timestamp() string - Date() string + Timestamp() time.Time + FormatTime() string + FormatDate() string CopyFrom(from MessageMeta) } diff --git a/ui/message-view.go b/ui/message-view.go index f9d477b..7b18ad8 100644 --- a/ui/message-view.go +++ b/ui/message-view.go @@ -49,22 +49,20 @@ type MessageView struct { messageIDs map[string]messages.UIMessage messages []messages.UIMessage - textBuffer []string + textBuffer []messages.UIString metaBuffer []ifc.MessageMeta } func NewMessageView() *MessageView { return &MessageView{ - Box: tview.NewBox(), - MaxSenderWidth: 15, - DateFormat: "January _2, 2006", - TimestampFormat: "15:04:05", - TimestampWidth: 8, - ScrollOffset: 0, + Box: tview.NewBox(), + MaxSenderWidth: 15, + TimestampWidth: len(messages.TimeFormat), + ScrollOffset: 0, messages: make([]messages.UIMessage, 0), messageIDs: make(map[string]messages.UIMessage), - textBuffer: make([]string, 0), + textBuffer: make([]messages.UIString, 0), metaBuffer: make([]ifc.MessageMeta, 0), widestSender: 5, @@ -75,10 +73,7 @@ func NewMessageView() *MessageView { } func (view *MessageView) NewMessage(id, sender, msgtype, text string, timestamp time.Time) messages.UIMessage { - return messages.NewMessage(id, sender, msgtype, text, - timestamp.Format(view.TimestampFormat), - timestamp.Format(view.DateFormat), - widget.GetHashColor(sender)) + return messages.NewMessage(id, sender, msgtype, text, timestamp, widget.GetHashColor(sender)) } func (view *MessageView) SaveHistory(path string) error { @@ -152,10 +147,10 @@ func (view *MessageView) AddMessage(ifcMessage ifc.Message, direction ifc.Messag return } - msg, messageExists := view.messageIDs[message.ID()] - if msg != nil && messageExists { - msg.CopyFrom(message) - message = msg + oldMsg, messageExists := view.messageIDs[message.ID()] + if messageExists { + oldMsg.CopyFrom(message) + message = oldMsg direction = ifc.IgnoreMessage } @@ -173,6 +168,8 @@ func (view *MessageView) AddMessage(ifcMessage ifc.Message, direction ifc.Messag view.appendBuffer(message) } else if direction == ifc.PrependMessage { view.messages = append([]messages.UIMessage{message}, view.messages...) + } else { + view.replaceBuffer(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) { if len(view.metaBuffer) > 0 { prevMeta := view.metaBuffer[len(view.metaBuffer)-1] - if prevMeta != nil && prevMeta.Date() != message.Date() { - view.textBuffer = append(view.textBuffer, fmt.Sprintf("Date changed to %s", message.Date())) - view.metaBuffer = append(view.metaBuffer, &messages.BasicMeta{BTextColor: tcell.ColorGreen}) + if prevMeta != nil && prevMeta.FormatDate() != message.FormatDate() { + view.textBuffer = append(view.textBuffer, messages.NewColorUIString( + 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++ } +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() { _, _, width, height := view.GetInnerRect() width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap recalculateMessageBuffers := width != view.prevWidth if height != view.prevHeight || recalculateMessageBuffers || len(view.messages) != view.prevMsgCount { - view.textBuffer = []string{} + view.textBuffer = []messages.UIString{} view.metaBuffer = []ifc.MessageMeta{} view.prevMsgCount = 0 for _, message := range view.messages { @@ -219,7 +248,7 @@ const PaddingAtTop = 5 func (view *MessageView) AddScrollOffset(diff int) { _, _, _, height := view.GetInnerRect() - totalHeight := len(view.textBuffer) + totalHeight := view.TotalHeight() if diff >= 0 && view.ScrollOffset+diff >= totalHeight-height+PaddingAtTop { view.ScrollOffset = totalHeight - height + PaddingAtTop } else { @@ -285,7 +314,7 @@ func (view *MessageView) Draw(screen tcell.Screen) { x, y, _, height := view.GetInnerRect() view.recalculateBuffers() - if len(view.textBuffer) == 0 { + if view.TotalHeight() == 0 { widget.WriteLineSimple(screen, "It's quite empty in here.", x, y+height) return } @@ -294,7 +323,7 @@ func (view *MessageView) Draw(screen tcell.Screen) { messageX := usernameX + view.widestSender + SenderMessageGap separatorX := usernameX + view.widestSender + SenderSeparatorGap - indexOffset := len(view.textBuffer) - view.ScrollOffset - height + indexOffset := view.TotalHeight() - view.ScrollOffset - height if indexOffset <= -PaddingAtTop { message := "Scroll up to load more messages." 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. { viewportHeight := float64(height) - contentHeight := float64(len(view.textBuffer)) + contentHeight := float64(view.TotalHeight()) scrollBarHeight = int(math.Ceil(viewportHeight / (contentHeight / viewportHeight))) @@ -328,12 +357,12 @@ func (view *MessageView) Draw(screen tcell.Screen) { if index < 0 { skippedLines++ continue - } else if index >= len(view.textBuffer) { + } else if index >= view.TotalHeight() { break } 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 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] if meta != prevMeta { - if len(meta.Timestamp()) > 0 { - widget.WriteLineSimpleColor(screen, meta.Timestamp(), x, y+line, meta.TimestampColor()) + if len(meta.FormatTime()) > 0 { + widget.WriteLineSimpleColor(screen, meta.FormatTime(), x, y+line, meta.TimestampColor()) } if prevMeta == nil || meta.Sender() != prevMeta.Sender() { widget.WriteLineColor( @@ -355,6 +384,7 @@ func (view *MessageView) Draw(screen tcell.Screen) { } prevMeta = meta } - widget.WriteLineSimpleColor(screen, text, messageX, y+line, meta.TextColor()) + + text.Draw(screen, messageX, y+line) } } diff --git a/ui/messages/message.go b/ui/messages/message.go index ef0966c..f9ad1f7 100644 --- a/ui/messages/message.go +++ b/ui/messages/message.go @@ -16,7 +16,13 @@ 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. type UIMessage interface { @@ -24,6 +30,154 @@ type UIMessage interface { CalculateBuffer(width int) RecalculateBuffer() - Buffer() []string + Buffer() []UIString 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" diff --git a/ui/messages/meta.go b/ui/messages/meta.go index 8cb6c1b..3f9c9ab 100644 --- a/ui/messages/meta.go +++ b/ui/messages/meta.go @@ -17,13 +17,16 @@ package messages import ( + "time" + "github.com/gdamore/tcell" "maunium.net/go/gomuks/interface" ) // BasicMeta is a simple variable store implementation of MessageMeta. type BasicMeta struct { - BSender, BTimestamp, BDate string + BSender string + BTimestamp time.Time BSenderColor, BTextColor, BTimestampColor tcell.Color } @@ -37,14 +40,19 @@ func (meta *BasicMeta) SenderColor() tcell.Color { return meta.BSenderColor } -// Timestamp returns the formatted time when the message was sent. -func (meta *BasicMeta) Timestamp() string { +// Timestamp returns the full time when the message was sent. +func (meta *BasicMeta) Timestamp() time.Time { return meta.BTimestamp } -// Date returns the formatted date when the message was sent. -func (meta *BasicMeta) Date() string { - return meta.BDate +// FormatTime returns the formatted time when the message was sent. +func (meta *BasicMeta) FormatTime() string { + 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. @@ -63,7 +71,6 @@ func (meta *BasicMeta) TimestampColor() tcell.Color { func (meta *BasicMeta) CopyFrom(from ifc.MessageMeta) { meta.BSender = from.Sender() meta.BTimestamp = from.Timestamp() - meta.BDate = from.Date() meta.BSenderColor = from.SenderColor() meta.BTextColor = from.TextColor() meta.BTimestampColor = from.TimestampColor() diff --git a/ui/messages/textmessage.go b/ui/messages/textmessage.go index 1f37418..1a53c2b 100644 --- a/ui/messages/textmessage.go +++ b/ui/messages/textmessage.go @@ -20,10 +20,9 @@ import ( "encoding/gob" "fmt" "regexp" - "strings" + "time" "github.com/gdamore/tcell" - "github.com/mattn/go-runewidth" "maunium.net/go/gomuks/interface" ) @@ -36,22 +35,20 @@ type UITextMessage struct { MsgType string MsgSender string MsgSenderColor tcell.Color - MsgTimestamp string - MsgDate string + MsgTimestamp time.Time MsgText string MsgState ifc.MessageState MsgIsHighlight bool MsgIsService bool - buffer []string + buffer []UIString prevBufferWidth int } // 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{ MsgSender: sender, MsgTimestamp: timestamp, - MsgDate: date, MsgSenderColor: senderColor, MsgType: msgtype, 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. func (msg *UITextMessage) CopyFrom(from ifc.MessageMeta) { msg.MsgSender = from.Sender() - msg.MsgTimestamp = from.Timestamp() - msg.MsgDate = from.Date() msg.MsgSenderColor = from.SenderColor() fromMsg, ok := from.(UIMessage) if ok { + msg.MsgSender = fromMsg.RealSender() msg.MsgID = fromMsg.ID() msg.MsgType = fromMsg.Type() + msg.MsgTimestamp = fromMsg.Timestamp() msg.MsgText = fromMsg.Text() msg.MsgState = fromMsg.State() msg.MsgIsService = fromMsg.IsService() msg.MsgIsHighlight = fromMsg.IsHighlight() + msg.buffer = nil 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 { switch msg.MsgState { 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 // calculated already, as that requires the target width. -func (msg *UITextMessage) Buffer() []string { +func (msg *UITextMessage) Buffer() []UIString { return msg.buffer } @@ -185,14 +187,19 @@ func (msg *UITextMessage) Height() int { return len(msg.buffer) } -// Timestamp returns the formatted time when the message was sent. -func (msg *UITextMessage) Timestamp() string { +// Timestamp returns the full timestamp when the message was sent. +func (msg *UITextMessage) Timestamp() time.Time { return msg.MsgTimestamp } -// Date returns the formatted date when the message was sent. -func (msg *UITextMessage) Date() string { - return msg.MsgDate +// FormatTime returns the formatted time when the message was sent. +func (msg *UITextMessage) FormatTime() string { + 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 { @@ -259,30 +266,31 @@ func (msg *UITextMessage) CalculateBuffer(width int) { return } - msg.buffer = []string{} - text := msg.MsgText + msg.buffer = []UIString{} + text := NewColorUIString(msg.Text(), msg.TextColor()) 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 for _, str := range forcedLinebreaks { if len(str) == 0 && newlines < 1 { - msg.buffer = append(msg.buffer, "") + msg.buffer = append(msg.buffer, UIString{}) newlines++ } else { newlines = 0 } - // From tview/textview.go#reindexBuffer() + // Mostly from tview/textview.go#reindexBuffer() for len(str) > 0 { - extract := runewidth.Truncate(str, width, "") + extract := str.Truncate(width) 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]] } - matches := boundaryPattern.FindAllStringIndex(extract, -1) + matches := boundaryPattern.FindAllStringIndex(extract.String(), -1) if len(matches) > 0 { extract = extract[:matches[len(matches)-1][1]] } -- cgit v1.2.3-70-g09d2 From 8270bc0322ac262f4b48c92d5fad25cf9634f1bb Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 10 Apr 2018 19:31:28 +0300 Subject: Move event parsing to ui/messages and add image displaying --- config/config.go | 2 + interface/matrix.go | 1 + interface/ui.go | 5 +- matrix/matrix.go | 76 ++++++++++++++++++++--- ui/message-view.go | 28 ++++++--- ui/messages/cell.go | 51 +++++++++++++++ ui/messages/doc.go | 2 +- ui/messages/imagemessage.go | 113 ++++++++++++++++++++++++++++++++++ ui/messages/message.go | 147 -------------------------------------------- ui/messages/parser.go | 120 ++++++++++++++++++++++++++++++++++++ ui/messages/string.go | 138 +++++++++++++++++++++++++++++++++++++++++ ui/messages/textmessage.go | 73 ++++++++++++++++++++-- ui/room-view.go | 2 +- ui/view-main.go | 67 ++------------------ 14 files changed, 585 insertions(+), 240 deletions(-) create mode 100644 ui/messages/cell.go create mode 100644 ui/messages/imagemessage.go create mode 100644 ui/messages/parser.go create mode 100644 ui/messages/string.go (limited to 'ui/messages') diff --git a/config/config.go b/config/config.go index 9179a58..85160c6 100644 --- a/config/config.go +++ b/config/config.go @@ -33,6 +33,7 @@ type Config struct { Dir string `yaml:"-"` HistoryDir string `yaml:"history_dir"` + MediaDir string `yaml:"media_dir"` Session *Session `yaml:"-"` } @@ -41,6 +42,7 @@ func NewConfig(dir string) *Config { return &Config{ Dir: dir, HistoryDir: filepath.Join(dir, "history"), + MediaDir: filepath.Join(dir, "media"), } } diff --git a/interface/matrix.go b/interface/matrix.go index f811dff..b7b52c3 100644 --- a/interface/matrix.go +++ b/interface/matrix.go @@ -34,4 +34,5 @@ type MatrixContainer interface { LeaveRoom(roomID string) error GetHistory(roomID, prevBatch string, limit int) ([]gomatrix.Event, string, error) GetRoom(roomID string) *rooms.Room + Download(mxcURL string) ([]byte, error) } diff --git a/interface/ui.go b/interface/ui.go index 2d27ea8..df92308 100644 --- a/interface/ui.go +++ b/interface/ui.go @@ -51,8 +51,9 @@ type MainView interface { SaveAllHistory() SetTyping(roomID string, users []string) - ProcessMessageEvent(roomView RoomView, evt *gomatrix.Event) Message - ProcessMembershipEvent(roomView RoomView, evt *gomatrix.Event) Message + ParseEvent(roomView RoomView, evt *gomatrix.Event) Message + //ProcessMessageEvent(roomView RoomView, evt *gomatrix.Event) Message + //ProcessMembershipEvent(roomView RoomView, evt *gomatrix.Event) Message NotifyMessage(room *rooms.Room, message Message, should pushrules.PushActionArrayShould) } diff --git a/matrix/matrix.go b/matrix/matrix.go index 6fff6d8..29419b0 100644 --- a/matrix/matrix.go +++ b/matrix/matrix.go @@ -17,19 +17,25 @@ package matrix import ( + "bytes" "encoding/json" "fmt" + "io" + "io/ioutil" + "net/url" + "os" + "path" + "path/filepath" + "regexp" "strings" "time" - -"maunium.net/go/gomatrix" -"maunium.net/go/gomuks/config" -"maunium.net/go/gomuks/debug" -"maunium.net/go/gomuks/interface" -"maunium.net/go/gomuks/matrix/pushrules" -"maunium.net/go/gomuks/matrix/rooms" - + "maunium.net/go/gomatrix" + "maunium.net/go/gomuks/config" + "maunium.net/go/gomuks/debug" + "maunium.net/go/gomuks/interface" + "maunium.net/go/gomuks/matrix/pushrules" + "maunium.net/go/gomuks/matrix/rooms" ) // Container is a wrapper for a gomatrix Client and some other stuff. @@ -222,7 +228,7 @@ func (c *Container) HandleMessage(evt *gomatrix.Event) { return } - message := mainView.ProcessMessageEvent(roomView, evt) + message := mainView.ParseEvent(roomView, evt) if message != nil { if c.syncer.FirstSyncDone { pushRules := c.PushRules().GetActions(roomView.MxRoom(), evt).Should() @@ -279,7 +285,7 @@ func (c *Container) HandleMembership(evt *gomatrix.Event) { return } - message := mainView.ProcessMembershipEvent(roomView, evt) + message := mainView.ParseEvent(roomView, evt) if message != nil { // TODO this shouldn't be necessary roomView.MxRoom().UpdateState(evt) @@ -403,3 +409,53 @@ func (c *Container) GetRoom(roomID string) *rooms.Room { } return room } + +var mxcRegex = regexp.MustCompile("mxc://(.+)/(.+)") + +func (c *Container) Download(mxcURL string) ([]byte, error) { + parts := mxcRegex.FindStringSubmatch(mxcURL) + if parts == nil || len(parts) != 3 { + debug.Print(parts) + return nil, fmt.Errorf("invalid matrix content URL") + } + hs := parts[1] + id := parts[2] + + cacheFile := c.getCachePath(hs, id) + if _, err := os.Stat(cacheFile); err != nil { + data, err := ioutil.ReadFile(cacheFile) + if err == nil { + return data, nil + } + } + + dlURL, _ := url.Parse(c.client.HomeserverURL.String()) + dlURL.Path = path.Join(dlURL.Path, "/_matrix/media/v1/download", hs, id) + + resp, err := c.client.Client.Get(dlURL.String()) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var buf bytes.Buffer + _, err = io.Copy(&buf, resp.Body) + if err != nil { + return nil, err + } + + data := buf.Bytes() + + err = ioutil.WriteFile(cacheFile, data, 0600) + return data, err +} +func (c *Container) getCachePath(homeserver, fileID string) string { + dir := filepath.Join(c.config.MediaDir, homeserver) + + err := os.MkdirAll(dir, 0700) + if err != nil { + return "" + } + + return filepath.Join(dir, fileID) +} diff --git a/ui/message-view.go b/ui/message-view.go index 7b18ad8..dd3edb5 100644 --- a/ui/message-view.go +++ b/ui/message-view.go @@ -21,7 +21,6 @@ import ( "fmt" "math" "os" - "time" "github.com/gdamore/tcell" "maunium.net/go/gomuks/debug" @@ -72,10 +71,6 @@ func NewMessageView() *MessageView { } } -func (view *MessageView) NewMessage(id, sender, msgtype, text string, timestamp time.Time) messages.UIMessage { - return messages.NewMessage(id, sender, msgtype, text, timestamp, widget.GetHashColor(sender)) -} - func (view *MessageView) SaveHistory(path string) error { file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0600) if err != nil { @@ -102,14 +97,23 @@ func (view *MessageView) LoadHistory(path string) (int, error) { } defer file.Close() + var msgs []messages.UIMessage + dec := gob.NewDecoder(file) - err = dec.Decode(&view.messages) + err = dec.Decode(&msgs) if err != nil { return -1, err } - for _, message := range view.messages { - view.updateWidestSender(message.Sender()) + view.messages = make([]messages.UIMessage, len(msgs)) + indexOffset := 0 + for index, message := range msgs { + if message != nil { + view.messages[index-indexOffset] = message + view.updateWidestSender(message.Sender()) + } else { + indexOffset++ + } } return len(view.messages), nil @@ -213,7 +217,7 @@ func (view *MessageView) replaceBuffer(message messages.UIMessage) { } view.textBuffer = append(append(view.textBuffer[0:start], message.Buffer()...), view.textBuffer[end:]...) - if len(message.Buffer()) != end - start + 1 { + if len(message.Buffer()) != end-start+1 { debug.Print(end, "-", start, "!=", len(message.Buffer())) metaBuffer := view.metaBuffer[0:start] for range message.Buffer() { @@ -232,7 +236,11 @@ func (view *MessageView) recalculateBuffers() { view.textBuffer = []messages.UIString{} view.metaBuffer = []ifc.MessageMeta{} view.prevMsgCount = 0 - for _, message := range view.messages { + for i, message := range view.messages { + if message == nil { + debug.Print("O.o found nil message at", i) + break + } if recalculateMessageBuffers { message.CalculateBuffer(width) } diff --git a/ui/messages/cell.go b/ui/messages/cell.go new file mode 100644 index 0000000..a919da7 --- /dev/null +++ b/ui/messages/cell.go @@ -0,0 +1,51 @@ +// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package messages + +import ( + "github.com/gdamore/tcell" + "github.com/mattn/go-runewidth" +) + +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 +} diff --git a/ui/messages/doc.go b/ui/messages/doc.go index 7c3c077..289c308 100644 --- a/ui/messages/doc.go +++ b/ui/messages/doc.go @@ -1,2 +1,2 @@ -// Package types contains common type definitions used by the UI. +// Package messages contains different message types and code to generate and render them. package messages diff --git a/ui/messages/imagemessage.go b/ui/messages/imagemessage.go new file mode 100644 index 0000000..53c0588 --- /dev/null +++ b/ui/messages/imagemessage.go @@ -0,0 +1,113 @@ +// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package messages + +import ( + "bytes" + "encoding/gob" + "time" + + "image/color" + + "github.com/gdamore/tcell" + "maunium.net/go/gomuks/debug" + "maunium.net/go/gomuks/interface" + "maunium.net/go/gomuks/ui/widget" + "maunium.net/go/pixterm/ansimage" +) + +func init() { + gob.Register(&UIImageMessage{}) +} + +type UIImageMessage struct { + UITextMessage + data []byte +} + +// NewImageMessage creates a new UIImageMessage object with the provided values and the default state. +func NewImageMessage(id, sender, msgtype string, data []byte, timestamp time.Time) UIMessage { + return &UIImageMessage{ + UITextMessage{ + MsgSender: sender, + MsgTimestamp: timestamp, + MsgSenderColor: widget.GetHashColor(sender), + MsgType: msgtype, + MsgID: id, + prevBufferWidth: 0, + MsgState: ifc.MessageStateDefault, + MsgIsHighlight: false, + MsgIsService: false, + }, + data, + } +} + +// CopyFrom replaces the content of this message object with the content of the given object. +func (msg *UIImageMessage) CopyFrom(from ifc.MessageMeta) { + msg.MsgSender = from.Sender() + msg.MsgSenderColor = from.SenderColor() + + fromMsg, ok := from.(UIMessage) + if ok { + msg.MsgSender = fromMsg.RealSender() + msg.MsgID = fromMsg.ID() + msg.MsgType = fromMsg.Type() + msg.MsgTimestamp = fromMsg.Timestamp() + msg.MsgState = fromMsg.State() + msg.MsgIsService = fromMsg.IsService() + msg.MsgIsHighlight = fromMsg.IsHighlight() + msg.buffer = nil + + fromImgMsg, ok := from.(*UIImageMessage) + if ok { + msg.data = fromImgMsg.data + } + + msg.RecalculateBuffer() + } +} + +// CalculateBuffer generates the internal buffer for this message that consists +// of the text of this message split into lines at most as wide as the width +// parameter. +func (msg *UIImageMessage) CalculateBuffer(width int) { + if width < 2 { + return + } + + image, err := ansimage.NewScaledFromReader(bytes.NewReader(msg.data), -1, width, color.Black, ansimage.ScaleModeResize, ansimage.NoDithering) + if err != nil { + msg.buffer = []UIString{NewColorUIString("Failed to display image", tcell.ColorRed)} + debug.Print("Failed to display image:", err) + return + } + + msg.buffer = make([]UIString, image.Height()) + pixels := image.Pixmap() + for row, pixelRow := range pixels { + msg.buffer[row] = make(UIString, len(pixelRow)) + for column, pixel := range pixelRow { + pixelColor := tcell.NewRGBColor(int32(pixel.R), int32(pixel.G), int32(pixel.B)) + msg.buffer[row][column] = Cell{ + Char: ' ', + Style: tcell.StyleDefault.Background(pixelColor), + } + } + } + msg.prevBufferWidth = width +} diff --git a/ui/messages/message.go b/ui/messages/message.go index f9ad1f7..f116d84 100644 --- a/ui/messages/message.go +++ b/ui/messages/message.go @@ -17,10 +17,6 @@ package messages import ( - "strings" - - "github.com/gdamore/tcell" - "github.com/mattn/go-runewidth" "maunium.net/go/gomuks/interface" ) @@ -36,148 +32,5 @@ type UIMessage interface { 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" diff --git a/ui/messages/parser.go b/ui/messages/parser.go new file mode 100644 index 0000000..4300c86 --- /dev/null +++ b/ui/messages/parser.go @@ -0,0 +1,120 @@ +// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package messages + +import ( + "fmt" + "time" + + "github.com/gdamore/tcell" + "maunium.net/go/gomatrix" + "maunium.net/go/gomuks/debug" + "maunium.net/go/gomuks/interface" + "maunium.net/go/gomuks/matrix/rooms" + "maunium.net/go/gomuks/ui/widget" +) + +func ParseEvent(mx ifc.MatrixContainer, room *rooms.Room, evt *gomatrix.Event) UIMessage { + member := room.GetMember(evt.Sender) + if member != nil { + evt.Sender = member.DisplayName + } + switch evt.Type { + case "m.room.message": + return ParseMessage(mx, evt) + case "m.room.member": + return ParseMembershipEvent(evt) + } + return nil +} + +func unixToTime(unix int64) time.Time { + timestamp := time.Now() + if unix != 0 { + timestamp = time.Unix(unix/1000, unix%1000*1000) + } + return timestamp +} + +func ParseMessage(mx ifc.MatrixContainer, evt *gomatrix.Event) UIMessage { + msgtype, _ := evt.Content["msgtype"].(string) + ts := unixToTime(evt.Timestamp) + switch msgtype { + case "m.text", "m.notice": + text, _ := evt.Content["body"].(string) + return NewTextMessage(evt.ID, evt.Sender, msgtype, text, ts) + case "m.image": + url, _ := evt.Content["url"].(string) + data, err := mx.Download(url) + if err != nil { + debug.Printf("Failed to download %s: %v", url, err) + } + return NewImageMessage(evt.ID, evt.Sender, msgtype, data, ts) + } + return nil +} + +func getMembershipEventContent(evt *gomatrix.Event) (sender string, text UIString) { + membership, _ := evt.Content["membership"].(string) + displayname, _ := evt.Content["displayname"].(string) + if len(displayname) == 0 { + displayname = *evt.StateKey + } + prevMembership := "leave" + prevDisplayname := "" + if evt.Unsigned.PrevContent != nil { + prevMembership, _ = evt.Unsigned.PrevContent["membership"].(string) + prevDisplayname, _ = evt.Unsigned.PrevContent["displayname"].(string) + } + + if membership != prevMembership { + switch membership { + case "invite": + sender = "---" + text = NewColorUIString(fmt.Sprintf("%s invited %s.", evt.Sender, displayname), tcell.ColorYellow) + text.Colorize(0, len(evt.Sender), widget.GetHashColor(evt.Sender)) + text.Colorize(len(evt.Sender)+len(" invited "), len(displayname), widget.GetHashColor(displayname)) + case "join": + sender = "-->" + text = NewColorUIString(fmt.Sprintf("%s joined the room.", displayname), tcell.ColorGreen) + text.Colorize(0, len(displayname), widget.GetHashColor(displayname)) + case "leave": + sender = "<--" + if evt.Sender != *evt.StateKey { + reason, _ := evt.Content["reason"].(string) + text = NewColorUIString(fmt.Sprintf("%s kicked %s: %s", evt.Sender, displayname, reason), tcell.ColorRed) + text.Colorize(0, len(evt.Sender), widget.GetHashColor(evt.Sender)) + text.Colorize(len(evt.Sender)+len(" kicked "), len(displayname), widget.GetHashColor(displayname)) + } else { + text = NewColorUIString(fmt.Sprintf("%s left the room.", displayname), tcell.ColorRed) + text.Colorize(0, len(displayname), widget.GetHashColor(displayname)) + } + } + } else if displayname != prevDisplayname { + sender = "---" + text = NewColorUIString(fmt.Sprintf("%s changed their display name to %s.", prevDisplayname, displayname), tcell.ColorYellow) + text.Colorize(0, len(prevDisplayname), widget.GetHashColor(prevDisplayname)) + text.Colorize(len(prevDisplayname)+len(" changed their display name to "), len(displayname), widget.GetHashColor(displayname)) + } + return +} + +func ParseMembershipEvent(evt *gomatrix.Event) UIMessage { + sender, text := getMembershipEventContent(evt) + ts := unixToTime(evt.Timestamp) + return NewExpandedTextMessage(evt.ID, sender, "m.room.membership", text, ts) +} diff --git a/ui/messages/string.go b/ui/messages/string.go new file mode 100644 index 0000000..7c3143b --- /dev/null +++ b/ui/messages/string.go @@ -0,0 +1,138 @@ +// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package messages + +import ( + "strings" + + "github.com/gdamore/tcell" + "github.com/mattn/go-runewidth" +) + +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, length int, color tcell.Color) { + for i := from; i < from+length; 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] +} diff --git a/ui/messages/textmessage.go b/ui/messages/textmessage.go index 1a53c2b..8ad3168 100644 --- a/ui/messages/textmessage.go +++ b/ui/messages/textmessage.go @@ -24,10 +24,67 @@ import ( "github.com/gdamore/tcell" "maunium.net/go/gomuks/interface" + "maunium.net/go/gomuks/ui/widget" ) func init() { gob.Register(&UITextMessage{}) + gob.Register(&UIExpandedTextMessage{}) +} + +type UIExpandedTextMessage struct { + UITextMessage + MsgUIStringText UIString +} + +// NewExpandedTextMessage creates a new UIExpandedTextMessage object with the provided values and the default state. +func NewExpandedTextMessage(id, sender, msgtype string, text UIString, timestamp time.Time) UIMessage { + return &UIExpandedTextMessage{ + UITextMessage{ + MsgSender: sender, + MsgTimestamp: timestamp, + MsgSenderColor: widget.GetHashColor(sender), + MsgType: msgtype, + MsgText: text.String(), + MsgID: id, + prevBufferWidth: 0, + MsgState: ifc.MessageStateDefault, + MsgIsHighlight: false, + MsgIsService: false, + }, + text, + } +} + +func (msg *UIExpandedTextMessage) GetUIStringText() UIString { + return msg.MsgUIStringText +} + +// CopyFrom replaces the content of this message object with the content of the given object. +func (msg *UIExpandedTextMessage) CopyFrom(from ifc.MessageMeta) { + msg.MsgSender = from.Sender() + msg.MsgSenderColor = from.SenderColor() + + fromMsg, ok := from.(UIMessage) + if ok { + msg.MsgSender = fromMsg.RealSender() + msg.MsgID = fromMsg.ID() + msg.MsgType = fromMsg.Type() + msg.MsgTimestamp = fromMsg.Timestamp() + msg.MsgState = fromMsg.State() + msg.MsgIsService = fromMsg.IsService() + msg.MsgIsHighlight = fromMsg.IsHighlight() + msg.buffer = nil + + fromExpandedMsg, ok := from.(*UIExpandedTextMessage) + if ok { + msg.MsgUIStringText = fromExpandedMsg.MsgUIStringText + } else { + msg.MsgUIStringText = NewColorUIString(fromMsg.Text(), from.TextColor()) + } + + msg.RecalculateBuffer() + } } type UITextMessage struct { @@ -44,12 +101,12 @@ type UITextMessage struct { prevBufferWidth int } -// NewMessage creates a new Message object with the provided values and the default state. -func NewMessage(id, sender, msgtype, text string, timestamp time.Time, senderColor tcell.Color) UIMessage { +// NewTextMessage creates a new UITextMessage object with the provided values and the default state. +func NewTextMessage(id, sender, msgtype, text string, timestamp time.Time) UIMessage { return &UITextMessage{ MsgSender: sender, MsgTimestamp: timestamp, - MsgSenderColor: senderColor, + MsgSenderColor: widget.GetHashColor(sender), MsgType: msgtype, MsgText: text, MsgID: id, @@ -250,6 +307,10 @@ func (msg *UITextMessage) SetIsService(isService bool) { msg.MsgIsService = isService } +func (msg *UITextMessage) GetUIStringText() UIString { + return NewColorUIString(msg.Text(), msg.TextColor()) +} + // Regular expressions used to split lines when calculating the buffer. // // From tview/textview.go @@ -267,10 +328,10 @@ func (msg *UITextMessage) CalculateBuffer(width int) { } msg.buffer = []UIString{} - text := NewColorUIString(msg.Text(), msg.TextColor()) + text := msg.GetUIStringText() if msg.MsgType == "m.emote" { - text = NewColorUIString(fmt.Sprintf("* %s %s", msg.MsgSender, msg.MsgText), msg.TextColor()) - text.Colorize(2, 2+len(msg.MsgSender), msg.SenderColor()) + text = NewColorUIString(fmt.Sprintf("* %s %s", msg.MsgSender, text.String()), msg.TextColor()) + text.Colorize(2, len(msg.MsgSender), msg.SenderColor()) } forcedLinebreaks := text.Split('\n') diff --git a/ui/room-view.go b/ui/room-view.go index 24325b4..749a432 100644 --- a/ui/room-view.go +++ b/ui/room-view.go @@ -260,7 +260,7 @@ func (view *RoomView) newUIMessage(id, sender, msgtype, text string, timestamp t if member != nil { sender = member.DisplayName } - return view.content.NewMessage(id, sender, msgtype, text, timestamp) + return messages.NewTextMessage(id, sender, msgtype, text, timestamp) } func (view *RoomView) NewMessage(id, sender, msgtype, text string, timestamp time.Time) ifc.Message { diff --git a/ui/view-main.go b/ui/view-main.go index b3b5b82..a97f9b2 100644 --- a/ui/view-main.go +++ b/ui/view-main.go @@ -32,6 +32,7 @@ import ( "maunium.net/go/gomuks/matrix/pushrules" "maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/notification" + "maunium.net/go/gomuks/ui/messages" "maunium.net/go/gomuks/ui/widget" "maunium.net/go/tview" ) @@ -446,12 +447,7 @@ func (view *MainView) LoadHistory(room string, initial bool) { } roomView.Room.PrevBatch = prevBatch for _, evt := range history { - var message ifc.Message - if evt.Type == "m.room.message" { - message = view.ProcessMessageEvent(roomView, &evt) - } else if evt.Type == "m.room.member" { - message = view.ProcessMembershipEvent(roomView, &evt) - } + message := view.ParseEvent(roomView, &evt) if message != nil { roomView.AddMessage(message, ifc.PrependMessage) } @@ -464,61 +460,6 @@ func (view *MainView) LoadHistory(room string, initial bool) { view.parent.Render() } -func (view *MainView) ProcessMessageEvent(room ifc.RoomView, evt *gomatrix.Event) ifc.Message { - text, _ := evt.Content["body"].(string) - msgtype, _ := evt.Content["msgtype"].(string) - return room.NewMessage(evt.ID, evt.Sender, msgtype, text, unixToTime(evt.Timestamp)) -} - -func (view *MainView) getMembershipEventContent(evt *gomatrix.Event) (sender, text string) { - membership, _ := evt.Content["membership"].(string) - displayname, _ := evt.Content["displayname"].(string) - if len(displayname) == 0 { - displayname = *evt.StateKey - } - prevMembership := "leave" - prevDisplayname := "" - if evt.Unsigned.PrevContent != nil { - prevMembership, _ = evt.Unsigned.PrevContent["membership"].(string) - prevDisplayname, _ = evt.Unsigned.PrevContent["displayname"].(string) - } - - if membership != prevMembership { - switch membership { - case "invite": - sender = "---" - text = fmt.Sprintf("%s invited %s.", evt.Sender, displayname) - case "join": - sender = "-->" - text = fmt.Sprintf("%s joined the room.", displayname) - case "leave": - sender = "<--" - if evt.Sender != *evt.StateKey { - reason, _ := evt.Content["reason"].(string) - text = fmt.Sprintf("%s kicked %s: %s", evt.Sender, displayname, reason) - } else { - text = fmt.Sprintf("%s left the room.", displayname) - } - } - } else if displayname != prevDisplayname { - sender = "---" - text = fmt.Sprintf("%s changed their display name to %s.", prevDisplayname, displayname) - } - return -} - -func (view *MainView) ProcessMembershipEvent(room ifc.RoomView, evt *gomatrix.Event) ifc.Message { - sender, text := view.getMembershipEventContent(evt) - if len(text) == 0 { - return nil - } - return room.NewMessage(evt.ID, sender, "m.room.member", text, unixToTime(evt.Timestamp)) -} - -func unixToTime(unix int64) time.Time { - timestamp := time.Now() - if unix != 0 { - timestamp = time.Unix(unix/1000, unix%1000*1000) - } - return timestamp +func (view *MainView) ParseEvent(roomView ifc.RoomView, evt *gomatrix.Event) ifc.Message { + return messages.ParseEvent(view.matrix, roomView.MxRoom(), evt) } -- cgit v1.2.3-70-g09d2 From ff7ee333a1028850337aa332532cbc23c7bdde14 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 11 Apr 2018 17:57:15 +0300 Subject: Rename UIString to TString, move ansimage to lib/ and switch to tcell fork --- interface/matrix.go | 2 +- interface/ui.go | 2 +- lib/ansimage/ansimage.go | 287 +++++++++++++++++++++++++++++++++++++++ matrix/matrix.go | 25 ++-- ui/message-view.go | 12 +- ui/messages/cell.go | 51 ------- ui/messages/imagemessage.go | 26 ++-- ui/messages/message.go | 3 +- ui/messages/meta.go | 2 +- ui/messages/parser.go | 19 +-- ui/messages/string.go | 138 ------------------- ui/messages/textmessage.go | 31 +++-- ui/messages/tstring/cell.go | 51 +++++++ ui/messages/tstring/string.go | 138 +++++++++++++++++++ ui/room-list.go | 2 +- ui/room-view.go | 2 +- ui/ui.go | 2 +- ui/view-main.go | 7 +- ui/widget/advanced-inputfield.go | 2 +- ui/widget/border.go | 2 +- ui/widget/color.go | 2 +- ui/widget/util.go | 2 +- 22 files changed, 549 insertions(+), 259 deletions(-) create mode 100644 lib/ansimage/ansimage.go delete mode 100644 ui/messages/cell.go delete mode 100644 ui/messages/string.go create mode 100644 ui/messages/tstring/cell.go create mode 100644 ui/messages/tstring/string.go (limited to 'ui/messages') diff --git a/interface/matrix.go b/interface/matrix.go index b7b52c3..cf011cf 100644 --- a/interface/matrix.go +++ b/interface/matrix.go @@ -34,5 +34,5 @@ type MatrixContainer interface { LeaveRoom(roomID string) error GetHistory(roomID, prevBatch string, limit int) ([]gomatrix.Event, string, error) GetRoom(roomID string) *rooms.Room - Download(mxcURL string) ([]byte, error) + Download(mxcURL string) ([]byte, string, error) } diff --git a/interface/ui.go b/interface/ui.go index df92308..8d3836d 100644 --- a/interface/ui.go +++ b/interface/ui.go @@ -19,7 +19,7 @@ package ifc import ( "time" - "github.com/gdamore/tcell" + "maunium.net/go/tcell" "maunium.net/go/gomatrix" "maunium.net/go/gomuks/matrix/pushrules" "maunium.net/go/gomuks/matrix/rooms" diff --git a/lib/ansimage/ansimage.go b/lib/ansimage/ansimage.go new file mode 100644 index 0000000..a41c833 --- /dev/null +++ b/lib/ansimage/ansimage.go @@ -0,0 +1,287 @@ +// ___ _____ ____ +// / _ \/ _/ |/_/ /____ ______ _ +// / ___// /_> =2") + + // ErrOutOfBounds happens when ANSI-pixel coordinates are out of ANSImage bounds. + ErrOutOfBounds = errors.New("ANSImage: out of bounds") +) + +// ANSIpixel represents a pixel of an ANSImage. +type ANSIpixel struct { + Brightness uint8 + R, G, B uint8 + upper bool + source *ANSImage +} + +// ANSImage represents an image encoded in ANSI escape codes. +type ANSImage struct { + h, w int + maxprocs int + bgR uint8 + bgG uint8 + bgB uint8 + pixmap [][]*ANSIpixel +} + +func (ai *ANSImage) Pixmap() [][]*ANSIpixel { + return ai.pixmap +} + +// Height gets total rows of ANSImage. +func (ai *ANSImage) Height() int { + return ai.h +} + +// Width gets total columns of ANSImage. +func (ai *ANSImage) Width() int { + return ai.w +} + +// SetMaxProcs sets the maximum number of parallel goroutines to render the ANSImage +// (user should manually sets `runtime.GOMAXPROCS(max)` before to this change takes effect). +func (ai *ANSImage) SetMaxProcs(max int) { + ai.maxprocs = max +} + +// GetMaxProcs gets the maximum number of parallels goroutines to render the ANSImage. +func (ai *ANSImage) GetMaxProcs() int { + return ai.maxprocs +} + +// SetAt sets ANSI-pixel color (RBG) and brightness in coordinates (y,x). +func (ai *ANSImage) SetAt(y, x int, r, g, b, brightness uint8) error { + if y >= 0 && y < ai.h && x >= 0 && x < ai.w { + ai.pixmap[y][x].R = r + ai.pixmap[y][x].G = g + ai.pixmap[y][x].B = b + ai.pixmap[y][x].Brightness = brightness + ai.pixmap[y][x].upper = y%2 == 0 + return nil + } + return ErrOutOfBounds +} + +// GetAt gets ANSI-pixel in coordinates (y,x). +func (ai *ANSImage) GetAt(y, x int) (*ANSIpixel, error) { + if y >= 0 && y < ai.h && x >= 0 && x < ai.w { + return &ANSIpixel{ + R: ai.pixmap[y][x].R, + G: ai.pixmap[y][x].G, + B: ai.pixmap[y][x].B, + Brightness: ai.pixmap[y][x].Brightness, + upper: ai.pixmap[y][x].upper, + source: ai.pixmap[y][x].source, + }, + nil + } + return nil, ErrOutOfBounds +} + +// Render returns the ANSI-compatible string form of ANSImage. +// (Nice info for ANSI True Colour - https://gist.github.com/XVilka/8346728) +func (ai *ANSImage) Render() []tstring.TString { + type renderData struct { + row int + render tstring.TString + } + + rows := make([]tstring.TString, ai.h/2) + for y := 0; y < ai.h; y += ai.maxprocs { + ch := make(chan renderData, ai.maxprocs) + for n, r := 0, y+1; (n <= ai.maxprocs) && (2*r+1 < ai.h); n, r = n+1, y+n+1 { + go func(r, y int) { + str := make(tstring.TString, ai.w) + for x := 0; x < ai.w; x++ { + topPixel := ai.pixmap[y][x] + topColor := tcell.NewRGBColor(int32(topPixel.R), int32(topPixel.G), int32(topPixel.B)) + + bottomPixel := ai.pixmap[y+1][x] + bottomColor := tcell.NewRGBColor(int32(bottomPixel.R), int32(bottomPixel.G), int32(bottomPixel.B)) + + str[x] = tstring.Cell{ + Char: 'â–„', + Style: tcell.StyleDefault.Background(topColor).Foreground(bottomColor), + } + } + ch <- renderData{row: r, render: str} + }(r, 2*r) + } + for n, r := 0, y+1; (n <= ai.maxprocs) && (2*r+1 < ai.h); n, r = n+1, y+n+1 { + data := <-ch + rows[data.row] = data.render + } + } + return rows +} + +// New creates a new empty ANSImage ready to draw on it. +func New(h, w int, bg color.Color) (*ANSImage, error) { + if h%2 != 0 { + return nil, ErrHeightNonMoT + } + + if h < 2 || w < 2 { + return nil, ErrInvalidBoundsMoT + } + + r, g, b, _ := bg.RGBA() + ansimage := &ANSImage{ + h: h, w: w, + maxprocs: 1, + bgR: uint8(r), + bgG: uint8(g), + bgB: uint8(b), + pixmap: nil, + } + + ansimage.pixmap = func() [][]*ANSIpixel { + v := make([][]*ANSIpixel, h) + for y := 0; y < h; y++ { + v[y] = make([]*ANSIpixel, w) + for x := 0; x < w; x++ { + v[y][x] = &ANSIpixel{ + R: 0, + G: 0, + B: 0, + Brightness: 0, + source: ansimage, + upper: y%2 == 0, + } + } + } + return v + }() + + return ansimage, nil +} + +// NewFromReader creates a new ANSImage from an io.Reader. +// Background color is used to fill when image has transparency or dithering mode is enabled +// Dithering mode is used to specify the way that ANSImage render ANSI-pixels (char/block elements). +func NewFromReader(reader io.Reader, bg color.Color) (*ANSImage, error) { + img, _, err := image.Decode(reader) + if err != nil { + return nil, err + } + + return createANSImage(img, bg) +} + +// NewScaledFromReader creates a new scaled ANSImage from an io.Reader. +// Background color is used to fill when image has transparency or dithering mode is enabled +// Dithering mode is used to specify the way that ANSImage render ANSI-pixels (char/block elements). +func NewScaledFromReader(reader io.Reader, y, x int, bg color.Color) (*ANSImage, error) { + img, _, err := image.Decode(reader) + if err != nil { + return nil, err + } + + img = imaging.Resize(img, x, y, imaging.Lanczos) + + return createANSImage(img, bg) +} + +// NewFromFile creates a new ANSImage from a file. +// Background color is used to fill when image has transparency or dithering mode is enabled +// Dithering mode is used to specify the way that ANSImage render ANSI-pixels (char/block elements). +func NewFromFile(name string, bg color.Color) (*ANSImage, error) { + reader, err := os.Open(name) + if err != nil { + return nil, err + } + defer reader.Close() + return NewFromReader(reader, bg) +} + +// NewScaledFromFile creates a new scaled ANSImage from a file. +// Background color is used to fill when image has transparency or dithering mode is enabled +// Dithering mode is used to specify the way that ANSImage render ANSI-pixels (char/block elements). +func NewScaledFromFile(name string, y, x int, bg color.Color) (*ANSImage, error) { + reader, err := os.Open(name) + if err != nil { + return nil, err + } + defer reader.Close() + return NewScaledFromReader(reader, y, x, bg) +} + +// createANSImage loads data from an image and returns an ANSImage. +// Background color is used to fill when image has transparency or dithering mode is enabled +// Dithering mode is used to specify the way that ANSImage render ANSI-pixels (char/block elements). +func createANSImage(img image.Image, bg color.Color) (*ANSImage, error) { + var rgbaOut *image.RGBA + bounds := img.Bounds() + + // do compositing only if background color has no transparency (thank you @disq for the idea!) + // (info - http://stackoverflow.com/questions/36595687/transparent-pixel-color-go-lang-image) + if _, _, _, a := bg.RGBA(); a >= 0xffff { + rgbaOut = image.NewRGBA(bounds) + draw.Draw(rgbaOut, bounds, image.NewUniform(bg), image.ZP, draw.Src) + draw.Draw(rgbaOut, bounds, img, image.ZP, draw.Over) + } else { + if v, ok := img.(*image.RGBA); ok { + rgbaOut = v + } else { + rgbaOut = image.NewRGBA(bounds) + draw.Draw(rgbaOut, bounds, img, image.ZP, draw.Src) + } + } + + yMin, xMin := bounds.Min.Y, bounds.Min.X + yMax, xMax := bounds.Max.Y, bounds.Max.X + + // always sets an even number of ANSIPixel rows... + yMax = yMax - yMax%2 // one for upper pixel and another for lower pixel --> without dithering + + ansimage, err := New(yMax, xMax, bg) + if err != nil { + return nil, err + } + + for y := yMin; y < yMax; y++ { + for x := xMin; x < xMax; x++ { + v := rgbaOut.RGBAAt(x, y) + if err := ansimage.SetAt(y, x, v.R, v.G, v.B, 0); err != nil { + return nil, err + } + } + } + + return ansimage, nil +} diff --git a/matrix/matrix.go b/matrix/matrix.go index 29419b0..b4d5775 100644 --- a/matrix/matrix.go +++ b/matrix/matrix.go @@ -22,6 +22,7 @@ import ( "fmt" "io" "io/ioutil" + "net/http" "net/url" "os" "path" @@ -412,42 +413,44 @@ func (c *Container) GetRoom(roomID string) *rooms.Room { var mxcRegex = regexp.MustCompile("mxc://(.+)/(.+)") -func (c *Container) Download(mxcURL string) ([]byte, error) { +func (c *Container) Download(mxcURL string) (data []byte, fullID string, err error) { parts := mxcRegex.FindStringSubmatch(mxcURL) if parts == nil || len(parts) != 3 { - debug.Print(parts) - return nil, fmt.Errorf("invalid matrix content URL") + err = fmt.Errorf("invalid matrix content URL") + return } hs := parts[1] id := parts[2] + fullID = fmt.Sprintf("%s/%s", hs, id) cacheFile := c.getCachePath(hs, id) - if _, err := os.Stat(cacheFile); err != nil { - data, err := ioutil.ReadFile(cacheFile) + if _, err = os.Stat(cacheFile); err != nil { + data, err = ioutil.ReadFile(cacheFile) if err == nil { - return data, nil + return } } dlURL, _ := url.Parse(c.client.HomeserverURL.String()) dlURL.Path = path.Join(dlURL.Path, "/_matrix/media/v1/download", hs, id) - resp, err := c.client.Client.Get(dlURL.String()) + var resp *http.Response + resp, err = c.client.Client.Get(dlURL.String()) if err != nil { - return nil, err + return } defer resp.Body.Close() var buf bytes.Buffer _, err = io.Copy(&buf, resp.Body) if err != nil { - return nil, err + return } - data := buf.Bytes() + data = buf.Bytes() err = ioutil.WriteFile(cacheFile, data, 0600) - return data, err + return } func (c *Container) getCachePath(homeserver, fileID string) string { dir := filepath.Join(c.config.MediaDir, homeserver) diff --git a/ui/message-view.go b/ui/message-view.go index dd3edb5..87dc993 100644 --- a/ui/message-view.go +++ b/ui/message-view.go @@ -22,7 +22,8 @@ import ( "math" "os" - "github.com/gdamore/tcell" + "maunium.net/go/gomuks/ui/messages/tstring" + "maunium.net/go/tcell" "maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/ui/messages" @@ -48,7 +49,7 @@ type MessageView struct { messageIDs map[string]messages.UIMessage messages []messages.UIMessage - textBuffer []messages.UIString + textBuffer []tstring.TString metaBuffer []ifc.MessageMeta } @@ -61,7 +62,7 @@ func NewMessageView() *MessageView { messages: make([]messages.UIMessage, 0), messageIDs: make(map[string]messages.UIMessage), - textBuffer: make([]messages.UIString, 0), + textBuffer: make([]tstring.TString, 0), metaBuffer: make([]ifc.MessageMeta, 0), widestSender: 5, @@ -183,7 +184,7 @@ func (view *MessageView) appendBuffer(message messages.UIMessage) { if len(view.metaBuffer) > 0 { prevMeta := view.metaBuffer[len(view.metaBuffer)-1] if prevMeta != nil && prevMeta.FormatDate() != message.FormatDate() { - view.textBuffer = append(view.textBuffer, messages.NewColorUIString( + view.textBuffer = append(view.textBuffer, tstring.NewColorTString( fmt.Sprintf("Date changed to %s", message.FormatDate()), tcell.ColorGreen)) view.metaBuffer = append(view.metaBuffer, &messages.BasicMeta{ @@ -218,7 +219,6 @@ func (view *MessageView) replaceBuffer(message messages.UIMessage) { 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) @@ -233,7 +233,7 @@ func (view *MessageView) recalculateBuffers() { width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap recalculateMessageBuffers := width != view.prevWidth if height != view.prevHeight || recalculateMessageBuffers || len(view.messages) != view.prevMsgCount { - view.textBuffer = []messages.UIString{} + view.textBuffer = []tstring.TString{} view.metaBuffer = []ifc.MessageMeta{} view.prevMsgCount = 0 for i, message := range view.messages { diff --git a/ui/messages/cell.go b/ui/messages/cell.go deleted file mode 100644 index a919da7..0000000 --- a/ui/messages/cell.go +++ /dev/null @@ -1,51 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2018 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package messages - -import ( - "github.com/gdamore/tcell" - "github.com/mattn/go-runewidth" -) - -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 -} diff --git a/ui/messages/imagemessage.go b/ui/messages/imagemessage.go index 53c0588..7129e5a 100644 --- a/ui/messages/imagemessage.go +++ b/ui/messages/imagemessage.go @@ -23,11 +23,12 @@ import ( "image/color" - "github.com/gdamore/tcell" "maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/interface" + "maunium.net/go/gomuks/ui/messages/tstring" "maunium.net/go/gomuks/ui/widget" - "maunium.net/go/pixterm/ansimage" + "maunium.net/go/gomuks/lib/ansimage" + "maunium.net/go/tcell" ) func init() { @@ -36,11 +37,12 @@ func init() { type UIImageMessage struct { UITextMessage + Path string data []byte } // NewImageMessage creates a new UIImageMessage object with the provided values and the default state. -func NewImageMessage(id, sender, msgtype string, data []byte, timestamp time.Time) UIMessage { +func NewImageMessage(id, sender, msgtype string, path string, data []byte, timestamp time.Time) UIMessage { return &UIImageMessage{ UITextMessage{ MsgSender: sender, @@ -53,6 +55,7 @@ func NewImageMessage(id, sender, msgtype string, data []byte, timestamp time.Tim MsgIsHighlight: false, MsgIsService: false, }, + path, data, } } @@ -90,24 +93,13 @@ func (msg *UIImageMessage) CalculateBuffer(width int) { return } - image, err := ansimage.NewScaledFromReader(bytes.NewReader(msg.data), -1, width, color.Black, ansimage.ScaleModeResize, ansimage.NoDithering) + image, err := ansimage.NewScaledFromReader(bytes.NewReader(msg.data), 0, width, color.Black) if err != nil { - msg.buffer = []UIString{NewColorUIString("Failed to display image", tcell.ColorRed)} + msg.buffer = []tstring.TString{tstring.NewColorTString("Failed to display image", tcell.ColorRed)} debug.Print("Failed to display image:", err) return } - msg.buffer = make([]UIString, image.Height()) - pixels := image.Pixmap() - for row, pixelRow := range pixels { - msg.buffer[row] = make(UIString, len(pixelRow)) - for column, pixel := range pixelRow { - pixelColor := tcell.NewRGBColor(int32(pixel.R), int32(pixel.G), int32(pixel.B)) - msg.buffer[row][column] = Cell{ - Char: ' ', - Style: tcell.StyleDefault.Background(pixelColor), - } - } - } + msg.buffer = image.Render() msg.prevBufferWidth = width } diff --git a/ui/messages/message.go b/ui/messages/message.go index f116d84..ac118ad 100644 --- a/ui/messages/message.go +++ b/ui/messages/message.go @@ -18,6 +18,7 @@ package messages import ( "maunium.net/go/gomuks/interface" + "maunium.net/go/gomuks/ui/messages/tstring" ) // Message is a wrapper for the content and metadata of a Matrix message intended to be displayed. @@ -26,7 +27,7 @@ type UIMessage interface { CalculateBuffer(width int) RecalculateBuffer() - Buffer() []UIString + Buffer() []tstring.TString Height() int RealSender() string diff --git a/ui/messages/meta.go b/ui/messages/meta.go index 3f9c9ab..7e2f29f 100644 --- a/ui/messages/meta.go +++ b/ui/messages/meta.go @@ -19,7 +19,7 @@ package messages import ( "time" - "github.com/gdamore/tcell" + "maunium.net/go/tcell" "maunium.net/go/gomuks/interface" ) diff --git a/ui/messages/parser.go b/ui/messages/parser.go index 4300c86..22ba4e7 100644 --- a/ui/messages/parser.go +++ b/ui/messages/parser.go @@ -20,12 +20,13 @@ import ( "fmt" "time" - "github.com/gdamore/tcell" "maunium.net/go/gomatrix" "maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/matrix/rooms" + "maunium.net/go/gomuks/ui/messages/tstring" "maunium.net/go/gomuks/ui/widget" + "maunium.net/go/tcell" ) func ParseEvent(mx ifc.MatrixContainer, room *rooms.Room, evt *gomatrix.Event) UIMessage { @@ -59,16 +60,16 @@ func ParseMessage(mx ifc.MatrixContainer, evt *gomatrix.Event) UIMessage { return NewTextMessage(evt.ID, evt.Sender, msgtype, text, ts) case "m.image": url, _ := evt.Content["url"].(string) - data, err := mx.Download(url) + data, path, err := mx.Download(url) if err != nil { debug.Printf("Failed to download %s: %v", url, err) } - return NewImageMessage(evt.ID, evt.Sender, msgtype, data, ts) + return NewImageMessage(evt.ID, evt.Sender, msgtype, path, data, ts) } return nil } -func getMembershipEventContent(evt *gomatrix.Event) (sender string, text UIString) { +func getMembershipEventContent(evt *gomatrix.Event) (sender string, text tstring.TString) { membership, _ := evt.Content["membership"].(string) displayname, _ := evt.Content["displayname"].(string) if len(displayname) == 0 { @@ -85,28 +86,28 @@ func getMembershipEventContent(evt *gomatrix.Event) (sender string, text UIStrin switch membership { case "invite": sender = "---" - text = NewColorUIString(fmt.Sprintf("%s invited %s.", evt.Sender, displayname), tcell.ColorYellow) + text = tstring.NewColorTString(fmt.Sprintf("%s invited %s.", evt.Sender, displayname), tcell.ColorYellow) text.Colorize(0, len(evt.Sender), widget.GetHashColor(evt.Sender)) text.Colorize(len(evt.Sender)+len(" invited "), len(displayname), widget.GetHashColor(displayname)) case "join": sender = "-->" - text = NewColorUIString(fmt.Sprintf("%s joined the room.", displayname), tcell.ColorGreen) + text = tstring.NewColorTString(fmt.Sprintf("%s joined the room.", displayname), tcell.ColorGreen) text.Colorize(0, len(displayname), widget.GetHashColor(displayname)) case "leave": sender = "<--" if evt.Sender != *evt.StateKey { reason, _ := evt.Content["reason"].(string) - text = NewColorUIString(fmt.Sprintf("%s kicked %s: %s", evt.Sender, displayname, reason), tcell.ColorRed) + text = tstring.NewColorTString(fmt.Sprintf("%s kicked %s: %s", evt.Sender, displayname, reason), tcell.ColorRed) text.Colorize(0, len(evt.Sender), widget.GetHashColor(evt.Sender)) text.Colorize(len(evt.Sender)+len(" kicked "), len(displayname), widget.GetHashColor(displayname)) } else { - text = NewColorUIString(fmt.Sprintf("%s left the room.", displayname), tcell.ColorRed) + text = tstring.NewColorTString(fmt.Sprintf("%s left the room.", displayname), tcell.ColorRed) text.Colorize(0, len(displayname), widget.GetHashColor(displayname)) } } } else if displayname != prevDisplayname { sender = "---" - text = NewColorUIString(fmt.Sprintf("%s changed their display name to %s.", prevDisplayname, displayname), tcell.ColorYellow) + text = tstring.NewColorTString(fmt.Sprintf("%s changed their display name to %s.", prevDisplayname, displayname), tcell.ColorYellow) text.Colorize(0, len(prevDisplayname), widget.GetHashColor(prevDisplayname)) text.Colorize(len(prevDisplayname)+len(" changed their display name to "), len(displayname), widget.GetHashColor(displayname)) } diff --git a/ui/messages/string.go b/ui/messages/string.go deleted file mode 100644 index 7c3143b..0000000 --- a/ui/messages/string.go +++ /dev/null @@ -1,138 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2018 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package messages - -import ( - "strings" - - "github.com/gdamore/tcell" - "github.com/mattn/go-runewidth" -) - -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, length int, color tcell.Color) { - for i := from; i < from+length; 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] -} diff --git a/ui/messages/textmessage.go b/ui/messages/textmessage.go index 8ad3168..12b098d 100644 --- a/ui/messages/textmessage.go +++ b/ui/messages/textmessage.go @@ -22,7 +22,8 @@ import ( "regexp" "time" - "github.com/gdamore/tcell" + "maunium.net/go/gomuks/ui/messages/tstring" + "maunium.net/go/tcell" "maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/ui/widget" ) @@ -34,11 +35,11 @@ func init() { type UIExpandedTextMessage struct { UITextMessage - MsgUIStringText UIString + MsgTStringText tstring.TString } // NewExpandedTextMessage creates a new UIExpandedTextMessage object with the provided values and the default state. -func NewExpandedTextMessage(id, sender, msgtype string, text UIString, timestamp time.Time) UIMessage { +func NewExpandedTextMessage(id, sender, msgtype string, text tstring.TString, timestamp time.Time) UIMessage { return &UIExpandedTextMessage{ UITextMessage{ MsgSender: sender, @@ -56,8 +57,8 @@ func NewExpandedTextMessage(id, sender, msgtype string, text UIString, timestamp } } -func (msg *UIExpandedTextMessage) GetUIStringText() UIString { - return msg.MsgUIStringText +func (msg *UIExpandedTextMessage) GetTStringText() tstring.TString { + return msg.MsgTStringText } // CopyFrom replaces the content of this message object with the content of the given object. @@ -78,9 +79,9 @@ func (msg *UIExpandedTextMessage) CopyFrom(from ifc.MessageMeta) { fromExpandedMsg, ok := from.(*UIExpandedTextMessage) if ok { - msg.MsgUIStringText = fromExpandedMsg.MsgUIStringText + msg.MsgTStringText = fromExpandedMsg.MsgTStringText } else { - msg.MsgUIStringText = NewColorUIString(fromMsg.Text(), from.TextColor()) + msg.MsgTStringText = tstring.NewColorTString(fromMsg.Text(), from.TextColor()) } msg.RecalculateBuffer() @@ -97,7 +98,7 @@ type UITextMessage struct { MsgState ifc.MessageState MsgIsHighlight bool MsgIsService bool - buffer []UIString + buffer []tstring.TString prevBufferWidth int } @@ -235,7 +236,7 @@ func (msg *UITextMessage) RecalculateBuffer() { // // N.B. This will NOT automatically calculate the buffer if it hasn't been // calculated already, as that requires the target width. -func (msg *UITextMessage) Buffer() []UIString { +func (msg *UITextMessage) Buffer() []tstring.TString { return msg.buffer } @@ -307,8 +308,8 @@ func (msg *UITextMessage) SetIsService(isService bool) { msg.MsgIsService = isService } -func (msg *UITextMessage) GetUIStringText() UIString { - return NewColorUIString(msg.Text(), msg.TextColor()) +func (msg *UITextMessage) GetTStringText() tstring.TString { + return tstring.NewColorTString(msg.Text(), msg.TextColor()) } // Regular expressions used to split lines when calculating the buffer. @@ -327,10 +328,10 @@ func (msg *UITextMessage) CalculateBuffer(width int) { return } - msg.buffer = []UIString{} - text := msg.GetUIStringText() + msg.buffer = []tstring.TString{} + text := msg.GetTStringText() if msg.MsgType == "m.emote" { - text = NewColorUIString(fmt.Sprintf("* %s %s", msg.MsgSender, text.String()), msg.TextColor()) + text = tstring.NewColorTString(fmt.Sprintf("* %s %s", msg.MsgSender, text.String()), msg.TextColor()) text.Colorize(2, len(msg.MsgSender), msg.SenderColor()) } @@ -338,7 +339,7 @@ func (msg *UITextMessage) CalculateBuffer(width int) { newlines := 0 for _, str := range forcedLinebreaks { if len(str) == 0 && newlines < 1 { - msg.buffer = append(msg.buffer, UIString{}) + msg.buffer = append(msg.buffer, tstring.TString{}) newlines++ } else { newlines = 0 diff --git a/ui/messages/tstring/cell.go b/ui/messages/tstring/cell.go new file mode 100644 index 0000000..8a400ee --- /dev/null +++ b/ui/messages/tstring/cell.go @@ -0,0 +1,51 @@ +// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package tstring + +import ( + "maunium.net/go/tcell" + "github.com/mattn/go-runewidth" +) + +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 +} diff --git a/ui/messages/tstring/string.go b/ui/messages/tstring/string.go new file mode 100644 index 0000000..ad0b2c8 --- /dev/null +++ b/ui/messages/tstring/string.go @@ -0,0 +1,138 @@ +// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package tstring + +import ( + "strings" + + "maunium.net/go/tcell" + "github.com/mattn/go-runewidth" +) + +type TString []Cell + +func NewTString(str string) TString { + newStr := make([]Cell, len(str)) + for i, char := range str { + newStr[i] = NewCell(char) + } + return newStr +} + +func NewColorTString(str string, color tcell.Color) TString { + newStr := make([]Cell, len(str)) + for i, char := range str { + newStr[i] = NewColorCell(char, color) + } + return newStr +} + +func NewStyleTString(str string, style tcell.Style) TString { + newStr := make([]Cell, len(str)) + for i, char := range str { + newStr[i] = NewStyleCell(char, style) + } + return newStr +} + +func (str TString) Colorize(from, length int, color tcell.Color) { + for i := from; i < from+length; i++ { + str[i].Style = str[i].Style.Foreground(color) + } +} + +func (str TString) Draw(screen tcell.Screen, x, y int) { + offsetX := 0 + for _, cell := range str { + offsetX += cell.Draw(screen, x+offsetX, y) + } +} + +func (str TString) RuneWidth() (width int) { + for _, cell := range str { + width += runewidth.RuneWidth(cell.Char) + } + return width +} + +func (str TString) 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 TString) Truncate(w int) TString { + 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 TString) IndexFrom(r rune, from int) int { + for i := from; i < len(str); i++ { + if str[i].Char == r { + return i + } + } + return -1 +} + +func (str TString) Index(r rune) int { + return str.IndexFrom(r, 0) +} + +func (str TString) Count(r rune) (counter int) { + index := 0 + for { + index = str.IndexFrom(r, index) + if index < 0 { + break + } + index++ + counter++ + } + return +} + +func (str TString) Split(sep rune) []TString { + a := make([]TString, 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] +} diff --git a/ui/room-list.go b/ui/room-list.go index 2ff3ed7..b85ed24 100644 --- a/ui/room-list.go +++ b/ui/room-list.go @@ -20,7 +20,7 @@ import ( "fmt" "strconv" - "github.com/gdamore/tcell" + "maunium.net/go/tcell" "maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/ui/widget" "maunium.net/go/tview" diff --git a/ui/room-view.go b/ui/room-view.go index 749a432..b7c9a34 100644 --- a/ui/room-view.go +++ b/ui/room-view.go @@ -23,7 +23,7 @@ import ( "strings" "time" - "github.com/gdamore/tcell" + "maunium.net/go/tcell" "maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/ui/messages" diff --git a/ui/ui.go b/ui/ui.go index 16321b8..b5f847d 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -17,7 +17,7 @@ package ui import ( - "github.com/gdamore/tcell" + "maunium.net/go/tcell" "maunium.net/go/gomuks/interface" "maunium.net/go/tview" ) diff --git a/ui/view-main.go b/ui/view-main.go index 3a6c38d..056f8a0 100644 --- a/ui/view-main.go +++ b/ui/view-main.go @@ -23,7 +23,7 @@ import ( "time" "unicode" - "github.com/gdamore/tcell" + "maunium.net/go/tcell" "github.com/mattn/go-runewidth" "maunium.net/go/gomatrix" "maunium.net/go/gomuks/config" @@ -230,6 +230,11 @@ func (view *MainView) MouseEventHandler(roomView *RoomView, event *tcell.EventMo x, y := event.Position() switch event.Buttons() { + case tcell.Button1: + mx, my, mw, mh := msgView.GetRect() + if x >= mx && y >= my && x < mx+mw && y < my+mh { + debug.Print("Message view clicked") + } case tcell.WheelUp: if msgView.IsAtTop() { go view.LoadHistory(roomView.Room.ID, false) diff --git a/ui/widget/advanced-inputfield.go b/ui/widget/advanced-inputfield.go index f74ce29..7e01478 100644 --- a/ui/widget/advanced-inputfield.go +++ b/ui/widget/advanced-inputfield.go @@ -24,7 +24,7 @@ import ( "strings" "unicode/utf8" - "github.com/gdamore/tcell" + "maunium.net/go/tcell" "github.com/mattn/go-runewidth" "github.com/zyedidia/clipboard" "maunium.net/go/tview" diff --git a/ui/widget/border.go b/ui/widget/border.go index 7c42f3d..b3eb65d 100644 --- a/ui/widget/border.go +++ b/ui/widget/border.go @@ -17,7 +17,7 @@ package widget import ( - "github.com/gdamore/tcell" + "maunium.net/go/tcell" "maunium.net/go/tview" ) diff --git a/ui/widget/color.go b/ui/widget/color.go index 12ee791..c4f1abf 100644 --- a/ui/widget/color.go +++ b/ui/widget/color.go @@ -21,7 +21,7 @@ import ( "hash/fnv" "sort" - "github.com/gdamore/tcell" + "maunium.net/go/tcell" ) var colorNames []string diff --git a/ui/widget/util.go b/ui/widget/util.go index bd80903..920afad 100644 --- a/ui/widget/util.go +++ b/ui/widget/util.go @@ -17,7 +17,7 @@ package widget import ( - "github.com/gdamore/tcell" + "maunium.net/go/tcell" "github.com/mattn/go-runewidth" "maunium.net/go/tview" ) -- cgit v1.2.3-70-g09d2 From 92a2428865dfde80af6f9ffba0f59567b63230b6 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 11 Apr 2018 19:20:40 +0300 Subject: Allow clicking images and load images from cache --- interface/matrix.go | 3 +- interface/ui.go | 2 +- matrix/matrix.go | 12 +++--- ui/message-view.go | 28 ++++++++++++- ui/messages/expandedtextmessage.go | 86 ++++++++++++++++++++++++++++++++++++++ ui/messages/imagemessage.go | 42 ++++++++++++++++--- ui/messages/message.go | 1 + ui/messages/parser.go | 10 ++--- ui/messages/textmessage.go | 62 ++------------------------- ui/room-view.go | 4 +- ui/view-main.go | 20 ++++----- 11 files changed, 181 insertions(+), 89 deletions(-) create mode 100644 ui/messages/expandedtextmessage.go (limited to 'ui/messages') diff --git a/interface/matrix.go b/interface/matrix.go index cf011cf..3a1ec14 100644 --- a/interface/matrix.go +++ b/interface/matrix.go @@ -34,5 +34,6 @@ type MatrixContainer interface { LeaveRoom(roomID string) error GetHistory(roomID, prevBatch string, limit int) ([]gomatrix.Event, string, error) GetRoom(roomID string) *rooms.Room - Download(mxcURL string) ([]byte, string, error) + Download(mxcURL string) ([]byte, string, string, error) + GetCachePath(homeserver, fileID string) string } diff --git a/interface/ui.go b/interface/ui.go index 8d3836d..fed1a73 100644 --- a/interface/ui.go +++ b/interface/ui.go @@ -71,7 +71,7 @@ const ( type RoomView interface { MxRoom() *rooms.Room SaveHistory(dir string) error - LoadHistory(dir string) (int, error) + LoadHistory(gmx Gomuks, dir string) (int, error) SetStatus(status string) SetTyping(users []string) diff --git a/matrix/matrix.go b/matrix/matrix.go index b4d5775..48bb57f 100644 --- a/matrix/matrix.go +++ b/matrix/matrix.go @@ -413,17 +413,16 @@ func (c *Container) GetRoom(roomID string) *rooms.Room { var mxcRegex = regexp.MustCompile("mxc://(.+)/(.+)") -func (c *Container) Download(mxcURL string) (data []byte, fullID string, err error) { +func (c *Container) Download(mxcURL string) (data []byte, hs, id string, err error) { parts := mxcRegex.FindStringSubmatch(mxcURL) if parts == nil || len(parts) != 3 { err = fmt.Errorf("invalid matrix content URL") return } - hs := parts[1] - id := parts[2] - fullID = fmt.Sprintf("%s/%s", hs, id) + hs = parts[1] + id = parts[2] - cacheFile := c.getCachePath(hs, id) + cacheFile := c.GetCachePath(hs, id) if _, err = os.Stat(cacheFile); err != nil { data, err = ioutil.ReadFile(cacheFile) if err == nil { @@ -452,7 +451,8 @@ func (c *Container) Download(mxcURL string) (data []byte, fullID string, err err err = ioutil.WriteFile(cacheFile, data, 0600) return } -func (c *Container) getCachePath(homeserver, fileID string) string { + +func (c *Container) GetCachePath(homeserver, fileID string) string { dir := filepath.Join(c.config.MediaDir, homeserver) err := os.MkdirAll(dir, 0700) diff --git a/ui/message-view.go b/ui/message-view.go index 87dc993..5584d3e 100644 --- a/ui/message-view.go +++ b/ui/message-view.go @@ -22,6 +22,7 @@ import ( "math" "os" + "maunium.net/go/gomuks/lib/open" "maunium.net/go/gomuks/ui/messages/tstring" "maunium.net/go/tcell" "maunium.net/go/gomuks/debug" @@ -88,7 +89,7 @@ func (view *MessageView) SaveHistory(path string) error { return nil } -func (view *MessageView) LoadHistory(path string) (int, error) { +func (view *MessageView) LoadHistory(gmx ifc.Gomuks, path string) (int, error) { file, err := os.OpenFile(path, os.O_RDONLY, 0600) if err != nil { if os.IsNotExist(err) { @@ -112,6 +113,7 @@ func (view *MessageView) LoadHistory(path string) (int, error) { if message != nil { view.messages[index-indexOffset] = message view.updateWidestSender(message.Sender()) + message.RegisterGomuks(gmx) } else { indexOffset++ } @@ -251,6 +253,30 @@ func (view *MessageView) recalculateBuffers() { } } +func (view *MessageView) HandleClick(x, y int, button tcell.ButtonMask) { + if button != tcell.Button1 { + return + } + + _, _, _, height := view.GetRect() + line := view.TotalHeight() - view.ScrollOffset - height + y + if line < 0 || line >= view.TotalHeight() { + return + } + + message := view.metaBuffer[line] + imageMessage, ok := message.(*messages.UIImageMessage) + if !ok { + uiMessage, ok := message.(messages.UIMessage) + if ok { + debug.Print("Message clicked:", uiMessage.Text()) + } + return + } + + open.Open(imageMessage.Path()) +} + const PaddingAtTop = 5 func (view *MessageView) AddScrollOffset(diff int) { diff --git a/ui/messages/expandedtextmessage.go b/ui/messages/expandedtextmessage.go new file mode 100644 index 0000000..1d6030a --- /dev/null +++ b/ui/messages/expandedtextmessage.go @@ -0,0 +1,86 @@ +// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package messages + +import ( + "encoding/gob" + "time" + + "maunium.net/go/gomuks/interface" + "maunium.net/go/gomuks/ui/messages/tstring" + "maunium.net/go/gomuks/ui/widget" +) + +func init() { + gob.Register(&UITextMessage{}) + gob.Register(&UIExpandedTextMessage{}) +} + +type UIExpandedTextMessage struct { + UITextMessage + MsgTStringText tstring.TString +} + +// NewExpandedTextMessage creates a new UIExpandedTextMessage object with the provided values and the default state. +func NewExpandedTextMessage(id, sender, msgtype string, text tstring.TString, timestamp time.Time) UIMessage { + return &UIExpandedTextMessage{ + UITextMessage{ + MsgSender: sender, + MsgTimestamp: timestamp, + MsgSenderColor: widget.GetHashColor(sender), + MsgType: msgtype, + MsgText: text.String(), + MsgID: id, + prevBufferWidth: 0, + MsgState: ifc.MessageStateDefault, + MsgIsHighlight: false, + MsgIsService: false, + }, + text, + } +} + +func (msg *UIExpandedTextMessage) GetTStringText() tstring.TString { + return msg.MsgTStringText +} + +// CopyFrom replaces the content of this message object with the content of the given object. +func (msg *UIExpandedTextMessage) CopyFrom(from ifc.MessageMeta) { + msg.MsgSender = from.Sender() + msg.MsgSenderColor = from.SenderColor() + + fromMsg, ok := from.(UIMessage) + if ok { + msg.MsgSender = fromMsg.RealSender() + msg.MsgID = fromMsg.ID() + msg.MsgType = fromMsg.Type() + msg.MsgTimestamp = fromMsg.Timestamp() + msg.MsgState = fromMsg.State() + msg.MsgIsService = fromMsg.IsService() + msg.MsgIsHighlight = fromMsg.IsHighlight() + msg.buffer = nil + + fromExpandedMsg, ok := from.(*UIExpandedTextMessage) + if ok { + msg.MsgTStringText = fromExpandedMsg.MsgTStringText + } else { + msg.MsgTStringText = tstring.NewColorTString(fromMsg.Text(), from.TextColor()) + } + + msg.RecalculateBuffer() + } +} diff --git a/ui/messages/imagemessage.go b/ui/messages/imagemessage.go index 7129e5a..5282d44 100644 --- a/ui/messages/imagemessage.go +++ b/ui/messages/imagemessage.go @@ -19,15 +19,16 @@ package messages import ( "bytes" "encoding/gob" + "fmt" "time" "image/color" "maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/interface" + "maunium.net/go/gomuks/lib/ansimage" "maunium.net/go/gomuks/ui/messages/tstring" "maunium.net/go/gomuks/ui/widget" - "maunium.net/go/gomuks/lib/ansimage" "maunium.net/go/tcell" ) @@ -37,12 +38,15 @@ func init() { type UIImageMessage struct { UITextMessage - Path string - data []byte + Homeserver string + FileID string + data []byte + + gmx ifc.Gomuks } // NewImageMessage creates a new UIImageMessage object with the provided values and the default state. -func NewImageMessage(id, sender, msgtype string, path string, data []byte, timestamp time.Time) UIMessage { +func NewImageMessage(gmx ifc.Gomuks, id, sender, msgtype, homeserver, fileID string, data []byte, timestamp time.Time) UIMessage { return &UIImageMessage{ UITextMessage{ MsgSender: sender, @@ -55,11 +59,39 @@ func NewImageMessage(id, sender, msgtype string, path string, data []byte, times MsgIsHighlight: false, MsgIsService: false, }, - path, + homeserver, + fileID, data, + gmx, + } +} + +func (msg *UIImageMessage) RegisterGomuks(gmx ifc.Gomuks) { + msg.gmx = gmx + + debug.Print(len(msg.data), msg.data) + if len(msg.data) == 0 { + go func() { + defer gmx.Recover() + msg.updateData() + }() } } +func (msg *UIImageMessage) updateData() { + debug.Print("Loading image:", msg.Homeserver, msg.FileID) + data, _, _, err := msg.gmx.Matrix().Download(fmt.Sprintf("mxc://%s/%s", msg.Homeserver, msg.FileID)) + if err != nil { + debug.Print("Failed to download image %s/%s: %v", msg.Homeserver, msg.FileID, err) + return + } + msg.data = data +} + +func (msg *UIImageMessage) Path() string { + return msg.gmx.Matrix().GetCachePath(msg.Homeserver, msg.FileID) +} + // CopyFrom replaces the content of this message object with the content of the given object. func (msg *UIImageMessage) CopyFrom(from ifc.MessageMeta) { msg.MsgSender = from.Sender() diff --git a/ui/messages/message.go b/ui/messages/message.go index ac118ad..6ebfb6d 100644 --- a/ui/messages/message.go +++ b/ui/messages/message.go @@ -31,6 +31,7 @@ type UIMessage interface { Height() int RealSender() string + RegisterGomuks(gmx ifc.Gomuks) } const DateFormat = "January _2, 2006" diff --git a/ui/messages/parser.go b/ui/messages/parser.go index 22ba4e7..af76bf3 100644 --- a/ui/messages/parser.go +++ b/ui/messages/parser.go @@ -29,14 +29,14 @@ import ( "maunium.net/go/tcell" ) -func ParseEvent(mx ifc.MatrixContainer, room *rooms.Room, evt *gomatrix.Event) UIMessage { +func ParseEvent(gmx ifc.Gomuks, room *rooms.Room, evt *gomatrix.Event) UIMessage { member := room.GetMember(evt.Sender) if member != nil { evt.Sender = member.DisplayName } switch evt.Type { case "m.room.message": - return ParseMessage(mx, evt) + return ParseMessage(gmx, evt) case "m.room.member": return ParseMembershipEvent(evt) } @@ -51,7 +51,7 @@ func unixToTime(unix int64) time.Time { return timestamp } -func ParseMessage(mx ifc.MatrixContainer, evt *gomatrix.Event) UIMessage { +func ParseMessage(gmx ifc.Gomuks, evt *gomatrix.Event) UIMessage { msgtype, _ := evt.Content["msgtype"].(string) ts := unixToTime(evt.Timestamp) switch msgtype { @@ -60,11 +60,11 @@ func ParseMessage(mx ifc.MatrixContainer, evt *gomatrix.Event) UIMessage { return NewTextMessage(evt.ID, evt.Sender, msgtype, text, ts) case "m.image": url, _ := evt.Content["url"].(string) - data, path, err := mx.Download(url) + data, hs, id, err := gmx.Matrix().Download(url) if err != nil { debug.Printf("Failed to download %s: %v", url, err) } - return NewImageMessage(evt.ID, evt.Sender, msgtype, path, data, ts) + return NewImageMessage(gmx, evt.ID, evt.Sender, msgtype, hs, id, data, ts) } return nil } diff --git a/ui/messages/textmessage.go b/ui/messages/textmessage.go index 12b098d..4587d49 100644 --- a/ui/messages/textmessage.go +++ b/ui/messages/textmessage.go @@ -22,70 +22,14 @@ import ( "regexp" "time" - "maunium.net/go/gomuks/ui/messages/tstring" - "maunium.net/go/tcell" "maunium.net/go/gomuks/interface" + "maunium.net/go/gomuks/ui/messages/tstring" "maunium.net/go/gomuks/ui/widget" + "maunium.net/go/tcell" ) func init() { gob.Register(&UITextMessage{}) - gob.Register(&UIExpandedTextMessage{}) -} - -type UIExpandedTextMessage struct { - UITextMessage - MsgTStringText tstring.TString -} - -// NewExpandedTextMessage creates a new UIExpandedTextMessage object with the provided values and the default state. -func NewExpandedTextMessage(id, sender, msgtype string, text tstring.TString, timestamp time.Time) UIMessage { - return &UIExpandedTextMessage{ - UITextMessage{ - MsgSender: sender, - MsgTimestamp: timestamp, - MsgSenderColor: widget.GetHashColor(sender), - MsgType: msgtype, - MsgText: text.String(), - MsgID: id, - prevBufferWidth: 0, - MsgState: ifc.MessageStateDefault, - MsgIsHighlight: false, - MsgIsService: false, - }, - text, - } -} - -func (msg *UIExpandedTextMessage) GetTStringText() tstring.TString { - return msg.MsgTStringText -} - -// CopyFrom replaces the content of this message object with the content of the given object. -func (msg *UIExpandedTextMessage) CopyFrom(from ifc.MessageMeta) { - msg.MsgSender = from.Sender() - msg.MsgSenderColor = from.SenderColor() - - fromMsg, ok := from.(UIMessage) - if ok { - msg.MsgSender = fromMsg.RealSender() - msg.MsgID = fromMsg.ID() - msg.MsgType = fromMsg.Type() - msg.MsgTimestamp = fromMsg.Timestamp() - msg.MsgState = fromMsg.State() - msg.MsgIsService = fromMsg.IsService() - msg.MsgIsHighlight = fromMsg.IsHighlight() - msg.buffer = nil - - fromExpandedMsg, ok := from.(*UIExpandedTextMessage) - if ok { - msg.MsgTStringText = fromExpandedMsg.MsgTStringText - } else { - msg.MsgTStringText = tstring.NewColorTString(fromMsg.Text(), from.TextColor()) - } - - msg.RecalculateBuffer() - } } type UITextMessage struct { @@ -118,6 +62,8 @@ func NewTextMessage(id, sender, msgtype, text string, timestamp time.Time) UIMes } } +func (msg *UITextMessage) RegisterGomuks(gmx ifc.Gomuks) {} + // CopyFrom replaces the content of this message object with the content of the given object. func (msg *UITextMessage) CopyFrom(from ifc.MessageMeta) { msg.MsgSender = from.Sender() diff --git a/ui/room-view.go b/ui/room-view.go index b7c9a34..ba3b004 100644 --- a/ui/room-view.go +++ b/ui/room-view.go @@ -81,8 +81,8 @@ func (view *RoomView) SaveHistory(dir string) error { return view.MessageView().SaveHistory(view.logPath(dir)) } -func (view *RoomView) LoadHistory(dir string) (int, error) { - return view.MessageView().LoadHistory(view.logPath(dir)) +func (view *RoomView) LoadHistory(gmx ifc.Gomuks, dir string) (int, error) { + return view.MessageView().LoadHistory(gmx, view.logPath(dir)) } func (view *RoomView) SetTabCompleteFunc(fn func(room *RoomView, text string, cursorOffset int) string) *RoomView { diff --git a/ui/view-main.go b/ui/view-main.go index 056f8a0..b21b3f4 100644 --- a/ui/view-main.go +++ b/ui/view-main.go @@ -23,7 +23,6 @@ import ( "time" "unicode" - "maunium.net/go/tcell" "github.com/mattn/go-runewidth" "maunium.net/go/gomatrix" "maunium.net/go/gomuks/config" @@ -34,6 +33,7 @@ import ( "maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/ui/messages" "maunium.net/go/gomuks/ui/widget" + "maunium.net/go/tcell" "maunium.net/go/tview" ) @@ -221,7 +221,7 @@ func (view *MainView) KeyEventHandler(roomView *RoomView, key *tcell.EventKey) * const WheelScrollOffsetDiff = 3 func (view *MainView) MouseEventHandler(roomView *RoomView, event *tcell.EventMouse) *tcell.EventMouse { - if event.Buttons() == tcell.ButtonNone { + if event.Buttons() == tcell.ButtonNone || event.HasMotion() { return event } view.BumpFocus() @@ -230,11 +230,6 @@ func (view *MainView) MouseEventHandler(roomView *RoomView, event *tcell.EventMo x, y := event.Position() switch event.Buttons() { - case tcell.Button1: - mx, my, mw, mh := msgView.GetRect() - if x >= mx && y >= my && x < mx+mw && y < my+mh { - debug.Print("Message view clicked") - } case tcell.WheelUp: if msgView.IsAtTop() { go view.LoadHistory(roomView.Room.ID, false) @@ -252,7 +247,12 @@ func (view *MainView) MouseEventHandler(roomView *RoomView, event *tcell.EventMo roomView.Room.MarkRead() } default: - debug.Print("Mouse event received:", event.Buttons(), event.Modifiers(), x, y) + mx, my, mw, mh := msgView.GetRect() + if x >= mx && y >= my && x < mx+mw && y < my+mh { + msgView.HandleClick(x-mx, y-my, event.Buttons()) + } else { + debug.Print("Mouse event received:", event.Buttons(), event.Modifiers(), x, y) + } return event } @@ -315,7 +315,7 @@ func (view *MainView) addRoom(index int, room string) { view.roomView.AddPage(room, roomView, true, false) roomView.UpdateUserList() - count, err := roomView.LoadHistory(view.config.HistoryDir) + count, err := roomView.LoadHistory(view.gmx, view.config.HistoryDir) if err != nil { debug.Printf("Failed to load history of %s: %v", roomView.Room.GetTitle(), err) } else if count <= 0 { @@ -466,5 +466,5 @@ func (view *MainView) LoadHistory(room string, initial bool) { } func (view *MainView) ParseEvent(roomView ifc.RoomView, evt *gomatrix.Event) ifc.Message { - return messages.ParseEvent(view.matrix, roomView.MxRoom(), evt) + return messages.ParseEvent(view.gmx, roomView.MxRoom(), evt) } -- cgit v1.2.3-70-g09d2 From 7e5e802e90e38e7b1de025e792e228ac7b329a53 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 11 Apr 2018 19:36:15 +0300 Subject: Cache TString version of UITextMessage.MsgText --- ui/messages/textmessage.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) (limited to 'ui/messages') diff --git a/ui/messages/textmessage.go b/ui/messages/textmessage.go index 4587d49..f630ef0 100644 --- a/ui/messages/textmessage.go +++ b/ui/messages/textmessage.go @@ -42,6 +42,7 @@ type UITextMessage struct { MsgState ifc.MessageState MsgIsHighlight bool MsgIsService bool + text tstring.TString buffer []tstring.TString prevBufferWidth int } @@ -220,6 +221,7 @@ func (msg *UITextMessage) Type() string { func (msg *UITextMessage) SetType(msgtype string) { msg.MsgType = msgtype + msg.text = nil } func (msg *UITextMessage) Text() string { @@ -228,6 +230,7 @@ func (msg *UITextMessage) Text() string { func (msg *UITextMessage) SetText(text string) { msg.MsgText = text + msg.text = nil } func (msg *UITextMessage) State() ifc.MessageState { @@ -236,6 +239,7 @@ func (msg *UITextMessage) State() ifc.MessageState { func (msg *UITextMessage) SetState(state ifc.MessageState) { msg.MsgState = state + msg.text = nil } func (msg *UITextMessage) IsHighlight() bool { @@ -244,6 +248,7 @@ func (msg *UITextMessage) IsHighlight() bool { func (msg *UITextMessage) SetIsHighlight(isHighlight bool) { msg.MsgIsHighlight = isHighlight + msg.text = nil } func (msg *UITextMessage) IsService() bool { @@ -252,10 +257,14 @@ func (msg *UITextMessage) IsService() bool { func (msg *UITextMessage) SetIsService(isService bool) { msg.MsgIsService = isService + msg.text = nil } func (msg *UITextMessage) GetTStringText() tstring.TString { - return tstring.NewColorTString(msg.Text(), msg.TextColor()) + if msg.text == nil || len(msg.text) == 0 { + msg.text = tstring.NewColorTString(msg.Text(), msg.TextColor()) + } + return msg.text } // Regular expressions used to split lines when calculating the buffer. @@ -278,7 +287,7 @@ func (msg *UITextMessage) CalculateBuffer(width int) { text := msg.GetTStringText() if msg.MsgType == "m.emote" { text = tstring.NewColorTString(fmt.Sprintf("* %s %s", msg.MsgSender, text.String()), msg.TextColor()) - text.Colorize(2, len(msg.MsgSender), msg.SenderColor()) + text.Colorize(0, len(msg.MsgSender), msg.SenderColor()) } forcedLinebreaks := text.Split('\n') -- cgit v1.2.3-70-g09d2 From 69c163cfe9d353060896403a3e844848d4fd54eb Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 13 Apr 2018 21:25:45 +0300 Subject: Unknown changes that possibly fix and/or break things --- interface/ui.go | 3 +- lib/ansimage/ansimage.go | 10 +- ui/message-view.go | 4 +- ui/messages/base.go | 236 ++++++++++++++++++++++++++++ ui/messages/expandedtextmessage.go | 71 ++++----- ui/messages/imagemessage.go | 66 ++++---- ui/messages/parser.go | 4 +- ui/messages/textbase.go | 84 ++++++++++ ui/messages/textmessage.go | 304 ++++++------------------------------- ui/view-main.go | 13 +- 10 files changed, 434 insertions(+), 361 deletions(-) create mode 100644 ui/messages/base.go create mode 100644 ui/messages/textbase.go (limited to 'ui/messages') diff --git a/interface/ui.go b/interface/ui.go index fed1a73..e6071fc 100644 --- a/interface/ui.go +++ b/interface/ui.go @@ -119,8 +119,7 @@ type Message interface { SetType(msgtype string) Type() string - SetText(text string) - Text() string + NotificationContent() string SetState(state MessageState) State() MessageState diff --git a/lib/ansimage/ansimage.go b/lib/ansimage/ansimage.go index a41c833..b117b3d 100644 --- a/lib/ansimage/ansimage.go +++ b/lib/ansimage/ansimage.go @@ -124,8 +124,8 @@ func (ai *ANSImage) Render() []tstring.TString { rows := make([]tstring.TString, ai.h/2) for y := 0; y < ai.h; y += ai.maxprocs { ch := make(chan renderData, ai.maxprocs) - for n, r := 0, y+1; (n <= ai.maxprocs) && (2*r+1 < ai.h); n, r = n+1, y+n+1 { - go func(r, y int) { + for n, row := 0, y; (n <= ai.maxprocs) && (2*row+1 < ai.h); n, row = n+1, y+n { + go func(row, y int) { str := make(tstring.TString, ai.w) for x := 0; x < ai.w; x++ { topPixel := ai.pixmap[y][x] @@ -139,10 +139,10 @@ func (ai *ANSImage) Render() []tstring.TString { Style: tcell.StyleDefault.Background(topColor).Foreground(bottomColor), } } - ch <- renderData{row: r, render: str} - }(r, 2*r) + ch <- renderData{row: row, render: str} + }(row, 2*row) } - for n, r := 0, y+1; (n <= ai.maxprocs) && (2*r+1 < ai.h); n, r = n+1, y+n+1 { + for n, row := 0, y; (n <= ai.maxprocs) && (2*row+1 < ai.h); n, row = n+1, y+n { data := <-ch rows[data.row] = data.render } diff --git a/ui/message-view.go b/ui/message-view.go index 5584d3e..9216aed 100644 --- a/ui/message-view.go +++ b/ui/message-view.go @@ -265,11 +265,11 @@ func (view *MessageView) HandleClick(x, y int, button tcell.ButtonMask) { } message := view.metaBuffer[line] - imageMessage, ok := message.(*messages.UIImageMessage) + imageMessage, ok := message.(*messages.ImageMessage) if !ok { uiMessage, ok := message.(messages.UIMessage) if ok { - debug.Print("Message clicked:", uiMessage.Text()) + debug.Print("Message clicked:", uiMessage.NotificationContent()) } return } diff --git a/ui/messages/base.go b/ui/messages/base.go new file mode 100644 index 0000000..deb153f --- /dev/null +++ b/ui/messages/base.go @@ -0,0 +1,236 @@ +// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package messages + +import ( + "encoding/gob" + "time" + + "maunium.net/go/gomuks/interface" + "maunium.net/go/gomuks/ui/messages/tstring" + "maunium.net/go/gomuks/ui/widget" + "maunium.net/go/tcell" +) + +func init() { + gob.Register(&BaseMessage{}) +} + +type BaseMessage struct { + MsgID string + MsgType string + MsgSender string + MsgSenderColor tcell.Color + MsgTimestamp time.Time + MsgState ifc.MessageState + MsgIsHighlight bool + MsgIsService bool + buffer []tstring.TString + prevBufferWidth int +} + +func newBaseMessage(id, sender, msgtype string, timestamp time.Time) BaseMessage { + return BaseMessage{ + MsgSender: sender, + MsgTimestamp: timestamp, + MsgSenderColor: widget.GetHashColor(sender), + MsgType: msgtype, + MsgID: id, + prevBufferWidth: 0, + MsgState: ifc.MessageStateDefault, + MsgIsHighlight: false, + MsgIsService: false, + } +} + +func (msg *BaseMessage) RegisterGomuks(gmx ifc.Gomuks) {} + +// CopyFrom replaces the content of this message object with the content of the given object. +func (msg *BaseMessage) CopyFrom(from ifc.MessageMeta) { + msg.MsgSender = from.Sender() + msg.MsgSenderColor = from.SenderColor() + + fromMsg, ok := from.(UIMessage) + if ok { + msg.MsgSender = fromMsg.RealSender() + msg.MsgID = fromMsg.ID() + msg.MsgType = fromMsg.Type() + msg.MsgTimestamp = fromMsg.Timestamp() + msg.MsgState = fromMsg.State() + msg.MsgIsService = fromMsg.IsService() + msg.MsgIsHighlight = fromMsg.IsHighlight() + msg.buffer = nil + } +} + +// Sender gets the string that should be displayed as the sender of this message. +// +// If the message is being sent, the sender is "Sending...". +// If sending has failed, the sender is "Error". +// If the message is an emote, the sender is blank. +// In any other case, the sender is the display name of the user who sent the message. +func (msg *BaseMessage) Sender() string { + switch msg.MsgState { + case ifc.MessageStateSending: + return "Sending..." + case ifc.MessageStateFailed: + return "Error" + } + switch msg.MsgType { + case "m.emote": + // Emotes don't show a separate sender, it's included in the buffer. + return "" + default: + return msg.MsgSender + } +} + +func (msg *BaseMessage) RealSender() string { + return msg.MsgSender +} + +func (msg *BaseMessage) getStateSpecificColor() tcell.Color { + switch msg.MsgState { + case ifc.MessageStateSending: + return tcell.ColorGray + case ifc.MessageStateFailed: + return tcell.ColorRed + case ifc.MessageStateDefault: + fallthrough + default: + return tcell.ColorDefault + } +} + +// SenderColor returns the color the name of the sender should be shown in. +// +// If the message is being sent, the color is gray. +// If sending has failed, the color is red. +// +// In any other case, the color is whatever is specified in the Message struct. +// Usually that means it is the hash-based color of the sender (see ui/widget/color.go) +func (msg *BaseMessage) SenderColor() tcell.Color { + stateColor := msg.getStateSpecificColor() + switch { + case stateColor != tcell.ColorDefault: + return stateColor + case msg.MsgIsService: + return tcell.ColorGray + default: + return msg.MsgSenderColor + } +} + +// TextColor returns the color the actual content of the message should be shown in. +func (msg *BaseMessage) TextColor() tcell.Color { + stateColor := msg.getStateSpecificColor() + switch { + case stateColor != tcell.ColorDefault: + return stateColor + case msg.MsgIsService: + fallthrough + case msg.MsgType == "m.notice": + return tcell.ColorGray + case msg.MsgIsHighlight: + return tcell.ColorYellow + case msg.MsgType == "m.room.member": + return tcell.ColorGreen + default: + return tcell.ColorDefault + } +} + +// TimestampColor returns the color the timestamp should be shown in. +// +// As with SenderColor(), messages being sent and messages that failed to be sent are +// gray and red respectively. +// +// However, other messages are the default color instead of a color stored in the struct. +func (msg *BaseMessage) TimestampColor() tcell.Color { + return msg.getStateSpecificColor() +} + +// Buffer returns the computed text buffer. +// +// The buffer contains the text of the message split into lines with a maximum +// width of whatever was provided to CalculateBuffer(). +// +// N.B. This will NOT automatically calculate the buffer if it hasn't been +// calculated already, as that requires the target width. +func (msg *BaseMessage) Buffer() []tstring.TString { + return msg.buffer +} + +// Height returns the number of rows in the computed buffer (see Buffer()). +func (msg *BaseMessage) Height() int { + return len(msg.buffer) +} + +// Timestamp returns the full timestamp when the message was sent. +func (msg *BaseMessage) Timestamp() time.Time { + return msg.MsgTimestamp +} + +// FormatTime returns the formatted time when the message was sent. +func (msg *BaseMessage) FormatTime() string { + return msg.MsgTimestamp.Format(TimeFormat) +} + +// FormatDate returns the formatted date when the message was sent. +func (msg *BaseMessage) FormatDate() string { + return msg.MsgTimestamp.Format(DateFormat) +} + +func (msg *BaseMessage) ID() string { + return msg.MsgID +} + +func (msg *BaseMessage) SetID(id string) { + msg.MsgID = id +} + +func (msg *BaseMessage) Type() string { + return msg.MsgType +} + +func (msg *BaseMessage) SetType(msgtype string) { + msg.MsgType = msgtype +} + +func (msg *BaseMessage) State() ifc.MessageState { + return msg.MsgState +} + +func (msg *BaseMessage) SetState(state ifc.MessageState) { + msg.MsgState = state +} + +func (msg *BaseMessage) IsHighlight() bool { + return msg.MsgIsHighlight +} + +func (msg *BaseMessage) SetIsHighlight(isHighlight bool) { + msg.MsgIsHighlight = isHighlight +} + +func (msg *BaseMessage) IsService() bool { + return msg.MsgIsService +} + +func (msg *BaseMessage) SetIsService(isService bool) { + msg.MsgIsService = isService +} diff --git a/ui/messages/expandedtextmessage.go b/ui/messages/expandedtextmessage.go index 1d6030a..3ee15ad 100644 --- a/ui/messages/expandedtextmessage.go +++ b/ui/messages/expandedtextmessage.go @@ -22,65 +22,50 @@ import ( "maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/ui/messages/tstring" - "maunium.net/go/gomuks/ui/widget" ) func init() { - gob.Register(&UITextMessage{}) - gob.Register(&UIExpandedTextMessage{}) + gob.Register(&ExpandedTextMessage{}) } -type UIExpandedTextMessage struct { - UITextMessage - MsgTStringText tstring.TString +type ExpandedTextMessage struct { + BaseTextMessage + MsgText tstring.TString } -// NewExpandedTextMessage creates a new UIExpandedTextMessage object with the provided values and the default state. +// NewExpandedTextMessage creates a new ExpandedTextMessage object with the provided values and the default state. func NewExpandedTextMessage(id, sender, msgtype string, text tstring.TString, timestamp time.Time) UIMessage { - return &UIExpandedTextMessage{ - UITextMessage{ - MsgSender: sender, - MsgTimestamp: timestamp, - MsgSenderColor: widget.GetHashColor(sender), - MsgType: msgtype, - MsgText: text.String(), - MsgID: id, - prevBufferWidth: 0, - MsgState: ifc.MessageStateDefault, - MsgIsHighlight: false, - MsgIsService: false, - }, - text, + return &ExpandedTextMessage{ + BaseTextMessage: newBaseTextMessage(id, sender, msgtype, timestamp), + MsgText: text, } } -func (msg *UIExpandedTextMessage) GetTStringText() tstring.TString { - return msg.MsgTStringText +func (msg *ExpandedTextMessage) GenerateText() tstring.TString { + return msg.MsgText } // CopyFrom replaces the content of this message object with the content of the given object. -func (msg *UIExpandedTextMessage) CopyFrom(from ifc.MessageMeta) { - msg.MsgSender = from.Sender() - msg.MsgSenderColor = from.SenderColor() +func (msg *ExpandedTextMessage) CopyFrom(from ifc.MessageMeta) { + msg.BaseTextMessage.CopyFrom(from) - fromMsg, ok := from.(UIMessage) + fromExpandedMsg, ok := from.(*ExpandedTextMessage) if ok { - msg.MsgSender = fromMsg.RealSender() - msg.MsgID = fromMsg.ID() - msg.MsgType = fromMsg.Type() - msg.MsgTimestamp = fromMsg.Timestamp() - msg.MsgState = fromMsg.State() - msg.MsgIsService = fromMsg.IsService() - msg.MsgIsHighlight = fromMsg.IsHighlight() - msg.buffer = nil + msg.MsgText = fromExpandedMsg.MsgText + } - fromExpandedMsg, ok := from.(*UIExpandedTextMessage) - if ok { - msg.MsgTStringText = fromExpandedMsg.MsgTStringText - } else { - msg.MsgTStringText = tstring.NewColorTString(fromMsg.Text(), from.TextColor()) - } + msg.RecalculateBuffer() +} - msg.RecalculateBuffer() - } +func (msg *ExpandedTextMessage) NotificationContent() string { + return msg.MsgText.String() +} + +func (msg *ExpandedTextMessage) CalculateBuffer(width int) { + msg.BaseTextMessage.calculateBufferWithText(msg.MsgText, width) +} + +// RecalculateBuffer calculates the buffer again with the previously provided width. +func (msg *ExpandedTextMessage) RecalculateBuffer() { + msg.CalculateBuffer(msg.prevBufferWidth) } diff --git a/ui/messages/imagemessage.go b/ui/messages/imagemessage.go index 5282d44..2fbf6ae 100644 --- a/ui/messages/imagemessage.go +++ b/ui/messages/imagemessage.go @@ -28,16 +28,15 @@ import ( "maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/lib/ansimage" "maunium.net/go/gomuks/ui/messages/tstring" - "maunium.net/go/gomuks/ui/widget" "maunium.net/go/tcell" ) func init() { - gob.Register(&UIImageMessage{}) + gob.Register(&ImageMessage{}) } -type UIImageMessage struct { - UITextMessage +type ImageMessage struct { + BaseMessage Homeserver string FileID string data []byte @@ -45,20 +44,10 @@ type UIImageMessage struct { gmx ifc.Gomuks } -// NewImageMessage creates a new UIImageMessage object with the provided values and the default state. +// NewImageMessage creates a new ImageMessage object with the provided values and the default state. func NewImageMessage(gmx ifc.Gomuks, id, sender, msgtype, homeserver, fileID string, data []byte, timestamp time.Time) UIMessage { - return &UIImageMessage{ - UITextMessage{ - MsgSender: sender, - MsgTimestamp: timestamp, - MsgSenderColor: widget.GetHashColor(sender), - MsgType: msgtype, - MsgID: id, - prevBufferWidth: 0, - MsgState: ifc.MessageStateDefault, - MsgIsHighlight: false, - MsgIsService: false, - }, + return &ImageMessage{ + newBaseMessage(id, sender, msgtype, timestamp), homeserver, fileID, data, @@ -66,7 +55,7 @@ func NewImageMessage(gmx ifc.Gomuks, id, sender, msgtype, homeserver, fileID str } } -func (msg *UIImageMessage) RegisterGomuks(gmx ifc.Gomuks) { +func (msg *ImageMessage) RegisterGomuks(gmx ifc.Gomuks) { msg.gmx = gmx debug.Print(len(msg.data), msg.data) @@ -78,7 +67,11 @@ func (msg *UIImageMessage) RegisterGomuks(gmx ifc.Gomuks) { } } -func (msg *UIImageMessage) updateData() { +func (msg *ImageMessage) NotificationContent() string { + return "Sent an image" +} + +func (msg *ImageMessage) updateData() { debug.Print("Loading image:", msg.Homeserver, msg.FileID) data, _, _, err := msg.gmx.Matrix().Download(fmt.Sprintf("mxc://%s/%s", msg.Homeserver, msg.FileID)) if err != nil { @@ -88,39 +81,26 @@ func (msg *UIImageMessage) updateData() { msg.data = data } -func (msg *UIImageMessage) Path() string { +func (msg *ImageMessage) Path() string { return msg.gmx.Matrix().GetCachePath(msg.Homeserver, msg.FileID) } // CopyFrom replaces the content of this message object with the content of the given object. -func (msg *UIImageMessage) CopyFrom(from ifc.MessageMeta) { - msg.MsgSender = from.Sender() - msg.MsgSenderColor = from.SenderColor() +func (msg *ImageMessage) CopyFrom(from ifc.MessageMeta) { + msg.BaseMessage.CopyFrom(from) - fromMsg, ok := from.(UIMessage) + fromImgMsg, ok := from.(*ImageMessage) if ok { - msg.MsgSender = fromMsg.RealSender() - msg.MsgID = fromMsg.ID() - msg.MsgType = fromMsg.Type() - msg.MsgTimestamp = fromMsg.Timestamp() - msg.MsgState = fromMsg.State() - msg.MsgIsService = fromMsg.IsService() - msg.MsgIsHighlight = fromMsg.IsHighlight() - msg.buffer = nil - - fromImgMsg, ok := from.(*UIImageMessage) - if ok { - msg.data = fromImgMsg.data - } - - msg.RecalculateBuffer() + msg.data = fromImgMsg.data } + + msg.RecalculateBuffer() } // CalculateBuffer generates the internal buffer for this message that consists // of the text of this message split into lines at most as wide as the width // parameter. -func (msg *UIImageMessage) CalculateBuffer(width int) { +func (msg *ImageMessage) CalculateBuffer(width int) { if width < 2 { return } @@ -135,3 +115,9 @@ func (msg *UIImageMessage) CalculateBuffer(width int) { msg.buffer = image.Render() msg.prevBufferWidth = width } + +// RecalculateBuffer calculates the buffer again with the previously provided width. +func (msg *ImageMessage) RecalculateBuffer() { + msg.CalculateBuffer(msg.prevBufferWidth) +} + diff --git a/ui/messages/parser.go b/ui/messages/parser.go index af76bf3..263bced 100644 --- a/ui/messages/parser.go +++ b/ui/messages/parser.go @@ -55,7 +55,7 @@ func ParseMessage(gmx ifc.Gomuks, evt *gomatrix.Event) UIMessage { msgtype, _ := evt.Content["msgtype"].(string) ts := unixToTime(evt.Timestamp) switch msgtype { - case "m.text", "m.notice": + case "m.text", "m.notice", "m.emote": text, _ := evt.Content["body"].(string) return NewTextMessage(evt.ID, evt.Sender, msgtype, text, ts) case "m.image": @@ -86,7 +86,7 @@ func getMembershipEventContent(evt *gomatrix.Event) (sender string, text tstring switch membership { case "invite": sender = "---" - text = tstring.NewColorTString(fmt.Sprintf("%s invited %s.", evt.Sender, displayname), tcell.ColorYellow) + text = tstring.NewColorTString(fmt.Sprintf("%s invited %s.", evt.Sender, displayname), tcell.ColorGreen) text.Colorize(0, len(evt.Sender), widget.GetHashColor(evt.Sender)) text.Colorize(len(evt.Sender)+len(" invited "), len(displayname), widget.GetHashColor(displayname)) case "join": diff --git a/ui/messages/textbase.go b/ui/messages/textbase.go new file mode 100644 index 0000000..d7eb16c --- /dev/null +++ b/ui/messages/textbase.go @@ -0,0 +1,84 @@ +// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package messages + +import ( + "encoding/gob" + "regexp" + "time" + + "maunium.net/go/gomuks/ui/messages/tstring" +) + +func init() { + gob.Register(BaseTextMessage{}) +} + +type BaseTextMessage struct { + BaseMessage +} + +func newBaseTextMessage(id, sender, msgtype string, timestamp time.Time) BaseTextMessage { + return BaseTextMessage{newBaseMessage(id, sender, msgtype, timestamp)} +} + +// Regular expressions used to split lines when calculating the buffer. +// +// From tview/textview.go +var ( + boundaryPattern = regexp.MustCompile("([[:punct:]]\\s*|\\s+)") + spacePattern = regexp.MustCompile(`\s+`) +) + +// CalculateBuffer generates the internal buffer for this message that consists +// of the text of this message split into lines at most as wide as the width +// parameter. +func (msg *BaseTextMessage) calculateBufferWithText(text tstring.TString, width int) { + if width < 2 { + return + } + + msg.buffer = []tstring.TString{} + + forcedLinebreaks := text.Split('\n') + newlines := 0 + for _, str := range forcedLinebreaks { + if len(str) == 0 && newlines < 1 { + msg.buffer = append(msg.buffer, tstring.TString{}) + newlines++ + } else { + newlines = 0 + } + // Mostly from tview/textview.go#reindexBuffer() + for len(str) > 0 { + extract := str.Truncate(width) + if len(extract) < len(str) { + if spaces := spacePattern.FindStringIndex(str[len(extract):].String()); spaces != nil && spaces[0] == 0 { + extract = str[:len(extract)+spaces[1]] + } + + matches := boundaryPattern.FindAllStringIndex(extract.String(), -1) + if len(matches) > 0 { + extract = extract[:matches[len(matches)-1][1]] + } + } + msg.buffer = append(msg.buffer, extract) + str = str[len(extract):] + } + } + msg.prevBufferWidth = width +} diff --git a/ui/messages/textmessage.go b/ui/messages/textmessage.go index f630ef0..4c99e5b 100644 --- a/ui/messages/textmessage.go +++ b/ui/messages/textmessage.go @@ -19,302 +19,84 @@ package messages import ( "encoding/gob" "fmt" - "regexp" "time" "maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/ui/messages/tstring" - "maunium.net/go/gomuks/ui/widget" - "maunium.net/go/tcell" ) func init() { - gob.Register(&UITextMessage{}) + gob.Register(&TextMessage{}) } -type UITextMessage struct { - MsgID string - MsgType string - MsgSender string - MsgSenderColor tcell.Color - MsgTimestamp time.Time - MsgText string - MsgState ifc.MessageState - MsgIsHighlight bool - MsgIsService bool - text tstring.TString - buffer []tstring.TString - prevBufferWidth int +type TextMessage struct { + BaseTextMessage + cache tstring.TString + MsgText string } // NewTextMessage creates a new UITextMessage object with the provided values and the default state. func NewTextMessage(id, sender, msgtype, text string, timestamp time.Time) UIMessage { - return &UITextMessage{ - MsgSender: sender, - MsgTimestamp: timestamp, - MsgSenderColor: widget.GetHashColor(sender), - MsgType: msgtype, + return &TextMessage{ + BaseTextMessage: newBaseTextMessage(id, sender, msgtype, timestamp), MsgText: text, - MsgID: id, - prevBufferWidth: 0, - MsgState: ifc.MessageStateDefault, - MsgIsHighlight: false, - MsgIsService: false, } } -func (msg *UITextMessage) RegisterGomuks(gmx ifc.Gomuks) {} - -// CopyFrom replaces the content of this message object with the content of the given object. -func (msg *UITextMessage) CopyFrom(from ifc.MessageMeta) { - msg.MsgSender = from.Sender() - msg.MsgSenderColor = from.SenderColor() - - fromMsg, ok := from.(UIMessage) - if ok { - msg.MsgSender = fromMsg.RealSender() - msg.MsgID = fromMsg.ID() - msg.MsgType = fromMsg.Type() - msg.MsgTimestamp = fromMsg.Timestamp() - msg.MsgText = fromMsg.Text() - msg.MsgState = fromMsg.State() - msg.MsgIsService = fromMsg.IsService() - msg.MsgIsHighlight = fromMsg.IsHighlight() - msg.buffer = nil - - msg.RecalculateBuffer() - } -} - -// Sender gets the string that should be displayed as the sender of this message. -// -// If the message is being sent, the sender is "Sending...". -// If sending has failed, the sender is "Error". -// If the message is an emote, the sender is blank. -// In any other case, the sender is the display name of the user who sent the message. -func (msg *UITextMessage) Sender() string { - switch msg.MsgState { - case ifc.MessageStateSending: - return "Sending..." - case ifc.MessageStateFailed: - return "Error" - } - switch msg.MsgType { - case "m.emote": - // Emotes don't show a separate sender, it's included in the buffer. - return "" - default: - return msg.MsgSender - } -} - -func (msg *UITextMessage) RealSender() string { - return msg.MsgSender -} - -func (msg *UITextMessage) getStateSpecificColor() tcell.Color { - switch msg.MsgState { - case ifc.MessageStateSending: - return tcell.ColorGray - case ifc.MessageStateFailed: - return tcell.ColorRed - case ifc.MessageStateDefault: - fallthrough - default: - return tcell.ColorDefault +func (msg *TextMessage) getCache() tstring.TString { + if msg.cache == nil { + switch msg.MsgType { + case "m.emote": + msg.cache = tstring.NewColorTString(fmt.Sprintf("* %s %s", msg.MsgSender, msg.MsgText), msg.TextColor()) + msg.cache.Colorize(0, len(msg.MsgSender)+2, msg.SenderColor()) + default: + msg.cache = tstring.NewColorTString(msg.MsgText, msg.TextColor()) + } } + return msg.cache } -// SenderColor returns the color the name of the sender should be shown in. -// -// If the message is being sent, the color is gray. -// If sending has failed, the color is red. -// -// In any other case, the color is whatever is specified in the Message struct. -// Usually that means it is the hash-based color of the sender (see ui/widget/color.go) -func (msg *UITextMessage) SenderColor() tcell.Color { - stateColor := msg.getStateSpecificColor() - switch { - case stateColor != tcell.ColorDefault: - return stateColor - case msg.MsgIsService: - return tcell.ColorGray - default: - return msg.MsgSenderColor - } -} +// CopyFrom replaces the content of this message object with the content of the given object. +func (msg *TextMessage) CopyFrom(from ifc.MessageMeta) { + msg.BaseTextMessage.CopyFrom(from) -// TextColor returns the color the actual content of the message should be shown in. -func (msg *UITextMessage) TextColor() tcell.Color { - stateColor := msg.getStateSpecificColor() - switch { - case stateColor != tcell.ColorDefault: - return stateColor - case msg.MsgIsService: - return tcell.ColorGray - case msg.MsgIsHighlight: - return tcell.ColorYellow - case msg.MsgType == "m.room.member": - return tcell.ColorGreen - default: - return tcell.ColorDefault + fromTextMsg, ok := from.(*TextMessage) + if ok { + msg.MsgText = fromTextMsg.MsgText } -} - -// TimestampColor returns the color the timestamp should be shown in. -// -// As with SenderColor(), messages being sent and messages that failed to be sent are -// gray and red respectively. -// -// However, other messages are the default color instead of a color stored in the struct. -func (msg *UITextMessage) TimestampColor() tcell.Color { - return msg.getStateSpecificColor() -} - -// RecalculateBuffer calculates the buffer again with the previously provided width. -func (msg *UITextMessage) RecalculateBuffer() { - msg.CalculateBuffer(msg.prevBufferWidth) -} - -// Buffer returns the computed text buffer. -// -// The buffer contains the text of the message split into lines with a maximum -// width of whatever was provided to CalculateBuffer(). -// -// N.B. This will NOT automatically calculate the buffer if it hasn't been -// calculated already, as that requires the target width. -func (msg *UITextMessage) Buffer() []tstring.TString { - return msg.buffer -} -// Height returns the number of rows in the computed buffer (see Buffer()). -func (msg *UITextMessage) Height() int { - return len(msg.buffer) + msg.cache = nil + msg.RecalculateBuffer() } - -// Timestamp returns the full timestamp when the message was sent. -func (msg *UITextMessage) Timestamp() time.Time { - return msg.MsgTimestamp -} - -// FormatTime returns the formatted time when the message was sent. -func (msg *UITextMessage) FormatTime() string { - 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 *TextMessage) SetType(msgtype string) { + msg.BaseTextMessage.SetType(msgtype) + msg.cache = nil } -func (msg *UITextMessage) ID() string { - return msg.MsgID +func (msg *TextMessage) SetState(state ifc.MessageState) { + msg.BaseTextMessage.SetState(state) + msg.cache = nil } -func (msg *UITextMessage) SetID(id string) { - msg.MsgID = id +func (msg *TextMessage) SetIsHighlight(isHighlight bool) { + msg.BaseTextMessage.SetIsHighlight(isHighlight) + msg.cache = nil } -func (msg *UITextMessage) Type() string { - return msg.MsgType +func (msg *TextMessage) SetIsService(isService bool) { + msg.BaseTextMessage.SetIsService(isService) + msg.cache = nil } -func (msg *UITextMessage) SetType(msgtype string) { - msg.MsgType = msgtype - msg.text = nil -} - -func (msg *UITextMessage) Text() string { +func (msg *TextMessage) NotificationContent() string { return msg.MsgText } -func (msg *UITextMessage) SetText(text string) { - msg.MsgText = text - msg.text = nil -} - -func (msg *UITextMessage) State() ifc.MessageState { - return msg.MsgState +func (msg *TextMessage) CalculateBuffer(width int) { + msg.BaseTextMessage.calculateBufferWithText(msg.getCache(), width) } -func (msg *UITextMessage) SetState(state ifc.MessageState) { - msg.MsgState = state - msg.text = nil -} - -func (msg *UITextMessage) IsHighlight() bool { - return msg.MsgIsHighlight -} - -func (msg *UITextMessage) SetIsHighlight(isHighlight bool) { - msg.MsgIsHighlight = isHighlight - msg.text = nil -} - -func (msg *UITextMessage) IsService() bool { - return msg.MsgIsService -} - -func (msg *UITextMessage) SetIsService(isService bool) { - msg.MsgIsService = isService - msg.text = nil -} - -func (msg *UITextMessage) GetTStringText() tstring.TString { - if msg.text == nil || len(msg.text) == 0 { - msg.text = tstring.NewColorTString(msg.Text(), msg.TextColor()) - } - return msg.text -} - -// Regular expressions used to split lines when calculating the buffer. -// -// From tview/textview.go -var ( - boundaryPattern = regexp.MustCompile("([[:punct:]]\\s*|\\s+)") - spacePattern = regexp.MustCompile(`\s+`) -) - -// CalculateBuffer generates the internal buffer for this message that consists -// of the text of this message split into lines at most as wide as the width -// parameter. -func (msg *UITextMessage) CalculateBuffer(width int) { - if width < 2 { - return - } - - msg.buffer = []tstring.TString{} - text := msg.GetTStringText() - if msg.MsgType == "m.emote" { - text = tstring.NewColorTString(fmt.Sprintf("* %s %s", msg.MsgSender, text.String()), msg.TextColor()) - text.Colorize(0, len(msg.MsgSender), msg.SenderColor()) - } - - forcedLinebreaks := text.Split('\n') - newlines := 0 - for _, str := range forcedLinebreaks { - if len(str) == 0 && newlines < 1 { - msg.buffer = append(msg.buffer, tstring.TString{}) - newlines++ - } else { - newlines = 0 - } - // Mostly from tview/textview.go#reindexBuffer() - for len(str) > 0 { - extract := str.Truncate(width) - if len(extract) < len(str) { - if spaces := spacePattern.FindStringIndex(str[len(extract):].String()); spaces != nil && spaces[0] == 0 { - extract = str[:len(extract)+spaces[1]] - } - - matches := boundaryPattern.FindAllStringIndex(extract.String(), -1) - if len(matches) > 0 { - extract = extract[:matches[len(matches)-1][1]] - } - } - msg.buffer = append(msg.buffer, extract) - str = str[len(extract):] - } - } - msg.prevBufferWidth = width +// RecalculateBuffer calculates the buffer again with the previously provided width. +func (msg *TextMessage) RecalculateBuffer() { + msg.CalculateBuffer(msg.prevBufferWidth) } diff --git a/ui/view-main.go b/ui/view-main.go index b21b3f4..e5850d3 100644 --- a/ui/view-main.go +++ b/ui/view-main.go @@ -134,12 +134,12 @@ func (view *MainView) InputSubmit(roomView *RoomView, text string) { func (view *MainView) SendMessage(roomView *RoomView, text string) { tempMessage := roomView.NewTempMessage("m.text", text) - go view.sendTempMessage(roomView, tempMessage) + go view.sendTempMessage(roomView, tempMessage, text) } -func (view *MainView) sendTempMessage(roomView *RoomView, tempMessage ifc.Message) { +func (view *MainView) sendTempMessage(roomView *RoomView, tempMessage ifc.Message, text string) { defer view.gmx.Recover() - eventID, err := view.matrix.SendMessage(roomView.Room.ID, tempMessage.Type(), tempMessage.Text()) + eventID, err := view.matrix.SendMessage(roomView.Room.ID, tempMessage.Type(), text) if err != nil { tempMessage.SetState(ifc.MessageStateFailed) roomView.SetStatus(fmt.Sprintf("Failed to send message: %s", err)) @@ -153,8 +153,9 @@ func (view *MainView) HandleCommand(roomView *RoomView, command string, args []s debug.Print("Handling command", command, args) switch command { case "/me": - tempMessage := roomView.NewTempMessage("m.emote", strings.Join(args, " ")) - go view.sendTempMessage(roomView, tempMessage) + text := strings.Join(args, " ") + tempMessage := roomView.NewTempMessage("m.emote", text) + go view.sendTempMessage(roomView, tempMessage, text) view.parent.Render() case "/quit": view.gmx.Stop() @@ -411,7 +412,7 @@ func (view *MainView) NotifyMessage(room *rooms.Room, message ifc.Message, shoul if shouldNotify && !isFocused { // Push rules say notify and the terminal is not focused, send desktop notification. shouldPlaySound := should.PlaySound && should.SoundName == "default" - sendNotification(room, message.Sender(), message.Text(), should.Highlight, shouldPlaySound) + sendNotification(room, message.Sender(), message.NotificationContent(), should.Highlight, shouldPlaySound) } message.SetIsHighlight(should.Highlight) -- cgit v1.2.3-70-g09d2 From e7bf5bd59fc0a43172b6ab5b338e1d60bd4b3bbb Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 14 Apr 2018 00:34:25 +0300 Subject: Add basic HTML rendering (ref #16) --- matrix/rooms/room.go | 6 +- ui/messages/base.go | 4 +- ui/messages/htmlparser.go | 136 ++++++++++++++++++++++++++++++++++++++++++ ui/messages/parser.go | 10 +++- ui/messages/tstring/string.go | 33 +++++++++- ui/view-main.go | 4 +- 6 files changed, 179 insertions(+), 14 deletions(-) create mode 100644 ui/messages/htmlparser.go (limited to 'ui/messages') diff --git a/matrix/rooms/room.go b/matrix/rooms/room.go index 7dd2af4..2e12a28 100644 --- a/matrix/rooms/room.go +++ b/matrix/rooms/room.go @@ -94,11 +94,7 @@ func (room *Room) UpdateState(event *gomatrix.Event) { room.memberCache = nil room.firstMemberCache = "" fallthrough - case "m.room.name": - fallthrough - case "m.room.canonical_alias": - fallthrough - case "m.room.alias": + case "m.room.name", "m.room.canonical_alias", "m.room.alias": room.nameCache = "" case "m.room.topic": room.topicCache = "" diff --git a/ui/messages/base.go b/ui/messages/base.go index deb153f..aed7903 100644 --- a/ui/messages/base.go +++ b/ui/messages/base.go @@ -141,9 +141,7 @@ func (msg *BaseMessage) TextColor() tcell.Color { switch { case stateColor != tcell.ColorDefault: return stateColor - case msg.MsgIsService: - fallthrough - case msg.MsgType == "m.notice": + case msg.MsgIsService, msg.MsgType == "m.notice": return tcell.ColorGray case msg.MsgIsHighlight: return tcell.ColorYellow diff --git a/ui/messages/htmlparser.go b/ui/messages/htmlparser.go new file mode 100644 index 0000000..0475e7a --- /dev/null +++ b/ui/messages/htmlparser.go @@ -0,0 +1,136 @@ +// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package messages + +import ( + "strings" + + "golang.org/x/net/html" + "maunium.net/go/gomatrix" + "maunium.net/go/gomuks/debug" + "maunium.net/go/gomuks/ui/messages/tstring" + "maunium.net/go/tcell" +) + +// TagArray is a reversed queue for remembering what HTML tags are open. +type TagArray []string + +// Pushb converts the given byte array into a string and calls Push(). +func (ta *TagArray) Pushb(tag []byte) { + ta.Push(string(tag)) +} + +// Popb converts the given byte array into a string and calls Pop(). +func (ta *TagArray) Popb(tag []byte) { + ta.Pop(string(tag)) +} + +// Hasb converts the given byte array into a string and calls Has(). +func (ta *TagArray) Hasb(tag []byte) { + ta.Has(string(tag)) +} + +// HasAfterb converts the given byte array into a string and calls HasAfter(). +func (ta *TagArray) HasAfterb(tag []byte, after int) { + ta.HasAfter(string(tag), after) +} + +// Push adds the given tag to the array. +func (ta *TagArray) Push(tag string) { + *ta = append(*ta, "") + copy((*ta)[1:], *ta) + (*ta)[0] = tag +} + +// Pop removes the given tag from the array. +func (ta *TagArray) Pop(tag string) { + if (*ta)[0] == tag { + // This is the default case and is lighter than append(), so we handle it separately. + *ta = (*ta)[1:] + } else if index := ta.Has(tag); index != -1 { + *ta = append((*ta)[:index], (*ta)[index+1:]...) + } +} + +// Has returns the first index where the given tag is, or -1 if it's not in the list. +func (ta *TagArray) Has(tag string) int { + return ta.HasAfter(tag, -1) +} + +// HasAfter returns the first index after the given index where the given tag is, +// or -1 if the given tag is not on the list after the given index. +func (ta *TagArray) HasAfter(tag string, after int) int { + for i := after + 1; i < len(*ta); i++ { + if (*ta)[i] == tag { + return i + } + } + return -1 +} + +// ParseHTMLMessage parses a HTML-formatted Matrix event into a UIMessage. +func ParseHTMLMessage(evt *gomatrix.Event) tstring.TString { + //textData, _ := evt.Content["body"].(string) + htmlData, _ := evt.Content["formatted_body"].(string) + + z := html.NewTokenizer(strings.NewReader(htmlData)) + text := tstring.NewTString("") + + openTags := &TagArray{} + +Loop: + for { + tt := z.Next() + switch tt { + case html.ErrorToken: + break Loop + case html.TextToken: + style := tcell.StyleDefault + for _, tag := range *openTags { + switch tag { + case "b", "strong": + style = style.Bold(true) + case "i", "em": + style = style.Italic(true) + case "s", "del": + style = style.Strikethrough(true) + case "u", "ins": + style = style.Underline(true) + } + } + text = text.AppendStyle(string(z.Text()), style) + case html.SelfClosingTagToken, html.StartTagToken: + tagb, _ := z.TagName() + tag := string(tagb) + switch tag { + case "br": + debug.Print("BR found") + debug.Print(text.String()) + text = text.Append("\n") + default: + if tt == html.StartTagToken { + openTags.Push(tag) + } + } + case html.EndTagToken: + tagb, _ := z.TagName() + openTags.Popb(tagb) + } + } + + return text +} diff --git a/ui/messages/parser.go b/ui/messages/parser.go index 263bced..d8069c6 100644 --- a/ui/messages/parser.go +++ b/ui/messages/parser.go @@ -56,8 +56,14 @@ func ParseMessage(gmx ifc.Gomuks, evt *gomatrix.Event) UIMessage { ts := unixToTime(evt.Timestamp) switch msgtype { case "m.text", "m.notice", "m.emote": - text, _ := evt.Content["body"].(string) - return NewTextMessage(evt.ID, evt.Sender, msgtype, text, ts) + format, hasFormat := evt.Content["format"].(string) + if hasFormat && format == "org.matrix.custom.html" { + text := ParseHTMLMessage(evt) + return NewExpandedTextMessage(evt.ID, evt.Sender, msgtype, text, ts) + } else { + text, _ := evt.Content["body"].(string) + return NewTextMessage(evt.ID, evt.Sender, msgtype, text, ts) + } case "m.image": url, _ := evt.Content["url"].(string) data, hs, id, err := gmx.Matrix().Download(url) diff --git a/ui/messages/tstring/string.go b/ui/messages/tstring/string.go index ad0b2c8..d1ad446 100644 --- a/ui/messages/tstring/string.go +++ b/ui/messages/tstring/string.go @@ -19,8 +19,8 @@ package tstring import ( "strings" - "maunium.net/go/tcell" "github.com/mattn/go-runewidth" + "maunium.net/go/tcell" ) type TString []Cell @@ -49,6 +49,37 @@ func NewStyleTString(str string, style tcell.Style) TString { return newStr } +func (str TString) AppendTString(data TString) TString { + return append(str, data...) +} + +func (str TString) Append(data string) TString { + newStr := make(TString, len(str)+len(data)) + copy(newStr, str) + for i, char := range data { + newStr[i+len(str)] = NewCell(char) + } + return newStr +} + +func (str TString) AppendColor(data string, color tcell.Color) TString { + newStr := make(TString, len(str)+len(data)) + copy(newStr, str) + for i, char := range data { + newStr[i+len(str)] = NewColorCell(char, color) + } + return newStr +} + +func (str TString) AppendStyle(data string, style tcell.Style) TString { + newStr := make(TString, len(str)+len(data)) + copy(newStr, str) + for i, char := range data { + newStr[i+len(str)] = NewStyleCell(char, style) + } + return newStr +} + func (str TString) Colorize(from, length int, color tcell.Color) { for i := from; i < from+length; i++ { str[i].Style = str[i].Style.Foreground(color) diff --git a/ui/view-main.go b/ui/view-main.go index e5850d3..ccb3cc1 100644 --- a/ui/view-main.go +++ b/ui/view-main.go @@ -164,9 +164,7 @@ func (view *MainView) HandleCommand(roomView *RoomView, command string, args []s view.gmx.Stop() case "/panic": panic("This is a test panic.") - case "/part": - fallthrough - case "/leave": + case "/part", "/leave": debug.Print("Leave room result:", view.matrix.LeaveRoom(roomView.Room.ID)) case "/join": if len(args) == 0 { -- cgit v1.2.3-70-g09d2 From 782ba0657a0bddc6ccb31b1792f3fbf4500a0087 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 14 Apr 2018 11:44:07 +0300 Subject: Make HTML rendering more advanced Also add Python-like HTML parser thing in lib/htmlparser --- lib/htmlparser/htmlparser.go | 108 +++++++++++++++++++++ ui/messages/htmlparser.go | 218 ++++++++++++++++++++++++++---------------- ui/messages/htmltagarray.go | 118 +++++++++++++++++++++++ ui/messages/parser.go | 6 +- ui/messages/tstring/string.go | 4 + 5 files changed, 367 insertions(+), 87 deletions(-) create mode 100644 lib/htmlparser/htmlparser.go create mode 100644 ui/messages/htmltagarray.go (limited to 'ui/messages') diff --git a/lib/htmlparser/htmlparser.go b/lib/htmlparser/htmlparser.go new file mode 100644 index 0000000..fb4e012 --- /dev/null +++ b/lib/htmlparser/htmlparser.go @@ -0,0 +1,108 @@ +// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package htmlparser + +import ( + "io" + "strings" + + "golang.org/x/net/html" +) + +type HTMLProcessor interface { + Preprocess() + HandleStartTag(tagName string, attrs map[string]string) + HandleSelfClosingTag(tagName string, attrs map[string]string) + HandleText(text string) + HandleEndTag(tagName string) + ReceiveError(err error) + Postprocess() +} + +type HTMLParser struct { + *html.Tokenizer + processor HTMLProcessor +} + +func NewHTMLParserFromTokenizer(z *html.Tokenizer, processor HTMLProcessor) HTMLParser { + return HTMLParser{ + z, + processor, + } +} + +func NewHTMLParserFromReader(reader io.Reader, processor HTMLProcessor) HTMLParser { + return NewHTMLParserFromTokenizer(html.NewTokenizer(reader), processor) +} + +func NewHTMLParserFromString(html string, processor HTMLProcessor) HTMLParser { + return NewHTMLParserFromReader(strings.NewReader(html), processor) +} + +var SelfClosingTags = []string{"img", "br", "hr", "area", "base", "basefont", "input", "link", "meta"} + +func (parser HTMLParser) mapAttrs() map[string]string { + attrs := make(map[string]string) + hasMore := true + for hasMore { + var key, val []byte + key, val, hasMore = parser.TagAttr() + attrs[string(key)] = string(val) + } + return attrs +} + +func (parser HTMLParser) isSelfClosing(tag string) bool { + for _, selfClosingTag := range SelfClosingTags { + if tag == selfClosingTag { + return true + } + } + return false +} + +func (parser HTMLParser) Process() { + parser.processor.Preprocess() +Loop: + for { + tt := parser.Next() + switch tt { + case html.ErrorToken: + parser.processor.ReceiveError(parser.Err()) + break Loop + case html.TextToken: + parser.processor.HandleText(string(parser.Text())) + case html.StartTagToken, html.SelfClosingTagToken: + tagb, _ := parser.TagName() + attrs := parser.mapAttrs() + tag := string(tagb) + + selfClosing := tt == html.SelfClosingTagToken || parser.isSelfClosing(tag) + + if selfClosing { + parser.processor.HandleSelfClosingTag(tag, attrs) + } else { + parser.processor.HandleStartTag(tag, attrs) + } + case html.EndTagToken: + tagb, _ := parser.TagName() + parser.processor.HandleEndTag(string(tagb)) + } + } + + parser.processor.Postprocess() +} diff --git a/ui/messages/htmlparser.go b/ui/messages/htmlparser.go index 0475e7a..aa6211e 100644 --- a/ui/messages/htmlparser.go +++ b/ui/messages/htmlparser.go @@ -17,120 +17,170 @@ package messages import ( + "fmt" + "io" + "math" + "regexp" "strings" - "golang.org/x/net/html" "maunium.net/go/gomatrix" "maunium.net/go/gomuks/debug" + "maunium.net/go/gomuks/lib/htmlparser" + "maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/ui/messages/tstring" + "maunium.net/go/gomuks/ui/widget" "maunium.net/go/tcell" ) -// TagArray is a reversed queue for remembering what HTML tags are open. -type TagArray []string +var matrixToURL = regexp.MustCompile("^(?:https?://)?(?:www\\.)?matrix\\.to/#/([#@!].*)") -// Pushb converts the given byte array into a string and calls Push(). -func (ta *TagArray) Pushb(tag []byte) { - ta.Push(string(tag)) +type MatrixHTMLProcessor struct { + text tstring.TString + + indent string + listType string + lineIsNew bool + openTags *TagArray + + room *rooms.Room } -// Popb converts the given byte array into a string and calls Pop(). -func (ta *TagArray) Popb(tag []byte) { - ta.Pop(string(tag)) +func (parser *MatrixHTMLProcessor) newline() { + if !parser.lineIsNew { + parser.text = parser.text.Append("\n" + parser.indent) + parser.lineIsNew = true + } } -// Hasb converts the given byte array into a string and calls Has(). -func (ta *TagArray) Hasb(tag []byte) { - ta.Has(string(tag)) +func (parser *MatrixHTMLProcessor) Preprocess() {} + +func (parser *MatrixHTMLProcessor) HandleText(text string) { + style := tcell.StyleDefault + for _, tag := range *parser.openTags { + switch tag.Tag { + case "b", "strong": + style = style.Bold(true) + case "i", "em": + style = style.Italic(true) + case "s", "del": + style = style.Strikethrough(true) + case "u", "ins": + style = style.Underline(true) + case "a": + tag.Text += text + return + } + } + + if parser.openTags.Has("pre", "code") { + text = strings.Replace(text, "\n", "", -1) + } + parser.text = parser.text.AppendStyle(text, style) + parser.lineIsNew = false } -// HasAfterb converts the given byte array into a string and calls HasAfter(). -func (ta *TagArray) HasAfterb(tag []byte, after int) { - ta.HasAfter(string(tag), after) +func (parser *MatrixHTMLProcessor) HandleStartTag(tagName string, attrs map[string]string) { + tag := &TagWithMeta{Tag: tagName} + switch tag.Tag { + case "h1", "h2", "h3", "h4", "h5", "h6": + length := int(tag.Tag[1] - '0') + parser.text = parser.text.Append(strings.Repeat("#", length) + " ") + parser.lineIsNew = false + case "a": + tag.Meta, _ = attrs["href"] + case "ol", "ul": + parser.listType = tag.Tag + case "li": + indentSize := 2 + if parser.listType == "ol" { + list := parser.openTags.Get(parser.listType) + list.Counter++ + parser.text = parser.text.Append(fmt.Sprintf("%d. ", list.Counter)) + indentSize = int(math.Log10(float64(list.Counter))+1) + len(". ") + } else { + parser.text = parser.text.Append("* ") + } + parser.indent += strings.Repeat(" ", indentSize) + parser.lineIsNew = false + case "blockquote": + parser.indent += "> " + parser.text = parser.text.Append("> ") + parser.lineIsNew = false + } + parser.openTags.PushMeta(tag) } -// Push adds the given tag to the array. -func (ta *TagArray) Push(tag string) { - *ta = append(*ta, "") - copy((*ta)[1:], *ta) - (*ta)[0] = tag +func (parser *MatrixHTMLProcessor) HandleSelfClosingTag(tagName string, attrs map[string]string) { + if tagName == "br" { + parser.newline() + } } -// Pop removes the given tag from the array. -func (ta *TagArray) Pop(tag string) { - if (*ta)[0] == tag { - // This is the default case and is lighter than append(), so we handle it separately. - *ta = (*ta)[1:] - } else if index := ta.Has(tag); index != -1 { - *ta = append((*ta)[:index], (*ta)[index+1:]...) +func (parser *MatrixHTMLProcessor) HandleEndTag(tagName string) { + tag := parser.openTags.Pop(tagName) + + switch tag.Tag { + case "li", "blockquote": + indentSize := 2 + if tag.Tag == "li" && parser.listType == "ol" { + list := parser.openTags.Get(parser.listType) + indentSize = int(math.Log10(float64(list.Counter))+1) + len(". ") + } + if len(parser.indent) >= indentSize { + parser.indent = parser.indent[0 : len(parser.indent)-indentSize] + } + // TODO this newline is sometimes not good + parser.newline() + case "a": + match := matrixToURL.FindStringSubmatch(tag.Meta) + if len(match) == 2 { + pillTarget := match[1] + if pillTarget[0] == '@' { + if member := parser.room.GetMember(pillTarget); member != nil { + parser.text = parser.text.AppendColor(member.DisplayName, widget.GetHashColor(member.DisplayName)) + } else { + parser.text = parser.text.Append(pillTarget) + } + } else { + parser.text = parser.text.Append(pillTarget) + } + } else { + // TODO make text clickable rather than printing URL + parser.text = parser.text.Append(fmt.Sprintf("%s (%s)", tag.Text, tag.Meta)) + } + parser.lineIsNew = false + case "p", "pre", "ol", "ul", "h1", "h2", "h3", "h4", "h5", "h6", "div": + // parser.newline() } } -// Has returns the first index where the given tag is, or -1 if it's not in the list. -func (ta *TagArray) Has(tag string) int { - return ta.HasAfter(tag, -1) +func (parser *MatrixHTMLProcessor) ReceiveError(err error) { + if err != io.EOF { + debug.Print("Unexpected error parsing HTML:", err) + } } -// HasAfter returns the first index after the given index where the given tag is, -// or -1 if the given tag is not on the list after the given index. -func (ta *TagArray) HasAfter(tag string, after int) int { - for i := after + 1; i < len(*ta); i++ { - if (*ta)[i] == tag { - return i - } +func (parser *MatrixHTMLProcessor) Postprocess() { + if len(parser.text) > 0 && parser.text[len(parser.text)-1].Char == '\n' { + parser.text = parser.text[:len(parser.text)-1] } - return -1 } // ParseHTMLMessage parses a HTML-formatted Matrix event into a UIMessage. -func ParseHTMLMessage(evt *gomatrix.Event) tstring.TString { - //textData, _ := evt.Content["body"].(string) +func ParseHTMLMessage(room *rooms.Room, evt *gomatrix.Event) tstring.TString { htmlData, _ := evt.Content["formatted_body"].(string) - z := html.NewTokenizer(strings.NewReader(htmlData)) - text := tstring.NewTString("") - - openTags := &TagArray{} - -Loop: - for { - tt := z.Next() - switch tt { - case html.ErrorToken: - break Loop - case html.TextToken: - style := tcell.StyleDefault - for _, tag := range *openTags { - switch tag { - case "b", "strong": - style = style.Bold(true) - case "i", "em": - style = style.Italic(true) - case "s", "del": - style = style.Strikethrough(true) - case "u", "ins": - style = style.Underline(true) - } - } - text = text.AppendStyle(string(z.Text()), style) - case html.SelfClosingTagToken, html.StartTagToken: - tagb, _ := z.TagName() - tag := string(tagb) - switch tag { - case "br": - debug.Print("BR found") - debug.Print(text.String()) - text = text.Append("\n") - default: - if tt == html.StartTagToken { - openTags.Push(tag) - } - } - case html.EndTagToken: - tagb, _ := z.TagName() - openTags.Popb(tagb) - } + processor := &MatrixHTMLProcessor{ + room: room, + text: tstring.NewBlankTString(), + indent: "", + listType: "", + lineIsNew: true, + openTags: &TagArray{}, } - return text + parser := htmlparser.NewHTMLParserFromString(htmlData, processor) + parser.Process() + + return processor.text } diff --git a/ui/messages/htmltagarray.go b/ui/messages/htmltagarray.go new file mode 100644 index 0000000..597f0c7 --- /dev/null +++ b/ui/messages/htmltagarray.go @@ -0,0 +1,118 @@ +// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package messages + +// TagWithMeta is an open HTML tag with some metadata (e.g. list index, a href value). +type TagWithMeta struct { + Tag string + Counter int + Meta string + Text string +} + +// BlankTag is a blank TagWithMeta object. +var BlankTag = &TagWithMeta{} + +// TagArray is a reversed queue for remembering what HTML tags are open. +type TagArray []*TagWithMeta + +// Pushb converts the given byte array into a string and calls Push(). +func (ta *TagArray) Pushb(tag []byte) { + ta.Push(string(tag)) +} + +// Popb converts the given byte array into a string and calls Pop(). +func (ta *TagArray) Popb(tag []byte) *TagWithMeta { + return ta.Pop(string(tag)) +} + +// Indexb converts the given byte array into a string and calls Index(). +func (ta *TagArray) Indexb(tag []byte) { + ta.Index(string(tag)) +} + +// IndexAfterb converts the given byte array into a string and calls IndexAfter(). +func (ta *TagArray) IndexAfterb(tag []byte, after int) { + ta.IndexAfter(string(tag), after) +} + +// Push adds the given tag to the array. +func (ta *TagArray) Push(tag string) { + ta.PushMeta(&TagWithMeta{Tag: tag}) +} + +// Push adds the given tag to the array. +func (ta *TagArray) PushMeta(tag *TagWithMeta) { + *ta = append(*ta, BlankTag) + copy((*ta)[1:], *ta) + (*ta)[0] = tag +} + +// Pop removes the given tag from the array. +func (ta *TagArray) Pop(tag string) (removed *TagWithMeta) { + if (*ta)[0].Tag == tag { + // This is the default case and is lighter than append(), so we handle it separately. + removed = (*ta)[0] + *ta = (*ta)[1:] + } else if index := ta.Index(tag); index != -1 { + removed = (*ta)[index] + *ta = append((*ta)[:index], (*ta)[index+1:]...) + } + return +} + +// Index returns the first index where the given tag is, or -1 if it's not in the list. +func (ta *TagArray) Index(tag string) int { + return ta.IndexAfter(tag, -1) +} + +// IndexAfter returns the first index after the given index where the given tag is, +// or -1 if the given tag is not on the list after the given index. +func (ta *TagArray) IndexAfter(tag string, after int) int { + for i := after + 1; i < len(*ta); i++ { + if (*ta)[i].Tag == tag { + return i + } + } + return -1 +} + +// Get returns the first occurrence of the given tag, or nil if it's not in the list. +func (ta *TagArray) Get(tag string) *TagWithMeta { + return ta.GetAfter(tag, -1) +} + +// IndexAfter returns the first occurrence of the given tag, or nil if the given +// tag is not on the list after the given index. +func (ta *TagArray) GetAfter(tag string, after int) *TagWithMeta { + for i := after + 1; i < len(*ta); i++ { + if (*ta)[i].Tag == tag { + return (*ta)[i] + } + } + return nil +} + +// Has returns whether or not the list has at least one of the given tags. +func (ta *TagArray) Has(tags ...string) bool { + for _, tag := range tags { + if index := ta.Index(tag); index != -1 { + return true + } + } + return false +} diff --git a/ui/messages/parser.go b/ui/messages/parser.go index d8069c6..80ce5d6 100644 --- a/ui/messages/parser.go +++ b/ui/messages/parser.go @@ -36,7 +36,7 @@ func ParseEvent(gmx ifc.Gomuks, room *rooms.Room, evt *gomatrix.Event) UIMessage } switch evt.Type { case "m.room.message": - return ParseMessage(gmx, evt) + return ParseMessage(gmx, room, evt) case "m.room.member": return ParseMembershipEvent(evt) } @@ -51,14 +51,14 @@ func unixToTime(unix int64) time.Time { return timestamp } -func ParseMessage(gmx ifc.Gomuks, evt *gomatrix.Event) UIMessage { +func ParseMessage(gmx ifc.Gomuks, room *rooms.Room, evt *gomatrix.Event) UIMessage { msgtype, _ := evt.Content["msgtype"].(string) ts := unixToTime(evt.Timestamp) switch msgtype { case "m.text", "m.notice", "m.emote": format, hasFormat := evt.Content["format"].(string) if hasFormat && format == "org.matrix.custom.html" { - text := ParseHTMLMessage(evt) + text := ParseHTMLMessage(room, evt) return NewExpandedTextMessage(evt.ID, evt.Sender, msgtype, text, ts) } else { text, _ := evt.Content["body"].(string) diff --git a/ui/messages/tstring/string.go b/ui/messages/tstring/string.go index d1ad446..a87d16a 100644 --- a/ui/messages/tstring/string.go +++ b/ui/messages/tstring/string.go @@ -25,6 +25,10 @@ import ( type TString []Cell +func NewBlankTString() TString { + return make([]Cell, 0) +} + func NewTString(str string) TString { newStr := make([]Cell, len(str)) for i, char := range str { -- cgit v1.2.3-70-g09d2 From f5530ff99c62d0d97dfae352e922f8327ff418c1 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 14 Apr 2018 11:50:18 +0300 Subject: Move message parsing to parser subpackage --- ui/messages/htmlparser.go | 186 ------------------------------------- ui/messages/htmltagarray.go | 118 ----------------------- ui/messages/parser.go | 127 ------------------------- ui/messages/parser/htmlparser.go | 186 +++++++++++++++++++++++++++++++++++++ ui/messages/parser/htmltagarray.go | 118 +++++++++++++++++++++++ ui/messages/parser/parser.go | 128 +++++++++++++++++++++++++ ui/view-main.go | 4 +- 7 files changed, 434 insertions(+), 433 deletions(-) delete mode 100644 ui/messages/htmlparser.go delete mode 100644 ui/messages/htmltagarray.go delete mode 100644 ui/messages/parser.go create mode 100644 ui/messages/parser/htmlparser.go create mode 100644 ui/messages/parser/htmltagarray.go create mode 100644 ui/messages/parser/parser.go (limited to 'ui/messages') diff --git a/ui/messages/htmlparser.go b/ui/messages/htmlparser.go deleted file mode 100644 index aa6211e..0000000 --- a/ui/messages/htmlparser.go +++ /dev/null @@ -1,186 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2018 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package messages - -import ( - "fmt" - "io" - "math" - "regexp" - "strings" - - "maunium.net/go/gomatrix" - "maunium.net/go/gomuks/debug" - "maunium.net/go/gomuks/lib/htmlparser" - "maunium.net/go/gomuks/matrix/rooms" - "maunium.net/go/gomuks/ui/messages/tstring" - "maunium.net/go/gomuks/ui/widget" - "maunium.net/go/tcell" -) - -var matrixToURL = regexp.MustCompile("^(?:https?://)?(?:www\\.)?matrix\\.to/#/([#@!].*)") - -type MatrixHTMLProcessor struct { - text tstring.TString - - indent string - listType string - lineIsNew bool - openTags *TagArray - - room *rooms.Room -} - -func (parser *MatrixHTMLProcessor) newline() { - if !parser.lineIsNew { - parser.text = parser.text.Append("\n" + parser.indent) - parser.lineIsNew = true - } -} - -func (parser *MatrixHTMLProcessor) Preprocess() {} - -func (parser *MatrixHTMLProcessor) HandleText(text string) { - style := tcell.StyleDefault - for _, tag := range *parser.openTags { - switch tag.Tag { - case "b", "strong": - style = style.Bold(true) - case "i", "em": - style = style.Italic(true) - case "s", "del": - style = style.Strikethrough(true) - case "u", "ins": - style = style.Underline(true) - case "a": - tag.Text += text - return - } - } - - if parser.openTags.Has("pre", "code") { - text = strings.Replace(text, "\n", "", -1) - } - parser.text = parser.text.AppendStyle(text, style) - parser.lineIsNew = false -} - -func (parser *MatrixHTMLProcessor) HandleStartTag(tagName string, attrs map[string]string) { - tag := &TagWithMeta{Tag: tagName} - switch tag.Tag { - case "h1", "h2", "h3", "h4", "h5", "h6": - length := int(tag.Tag[1] - '0') - parser.text = parser.text.Append(strings.Repeat("#", length) + " ") - parser.lineIsNew = false - case "a": - tag.Meta, _ = attrs["href"] - case "ol", "ul": - parser.listType = tag.Tag - case "li": - indentSize := 2 - if parser.listType == "ol" { - list := parser.openTags.Get(parser.listType) - list.Counter++ - parser.text = parser.text.Append(fmt.Sprintf("%d. ", list.Counter)) - indentSize = int(math.Log10(float64(list.Counter))+1) + len(". ") - } else { - parser.text = parser.text.Append("* ") - } - parser.indent += strings.Repeat(" ", indentSize) - parser.lineIsNew = false - case "blockquote": - parser.indent += "> " - parser.text = parser.text.Append("> ") - parser.lineIsNew = false - } - parser.openTags.PushMeta(tag) -} - -func (parser *MatrixHTMLProcessor) HandleSelfClosingTag(tagName string, attrs map[string]string) { - if tagName == "br" { - parser.newline() - } -} - -func (parser *MatrixHTMLProcessor) HandleEndTag(tagName string) { - tag := parser.openTags.Pop(tagName) - - switch tag.Tag { - case "li", "blockquote": - indentSize := 2 - if tag.Tag == "li" && parser.listType == "ol" { - list := parser.openTags.Get(parser.listType) - indentSize = int(math.Log10(float64(list.Counter))+1) + len(". ") - } - if len(parser.indent) >= indentSize { - parser.indent = parser.indent[0 : len(parser.indent)-indentSize] - } - // TODO this newline is sometimes not good - parser.newline() - case "a": - match := matrixToURL.FindStringSubmatch(tag.Meta) - if len(match) == 2 { - pillTarget := match[1] - if pillTarget[0] == '@' { - if member := parser.room.GetMember(pillTarget); member != nil { - parser.text = parser.text.AppendColor(member.DisplayName, widget.GetHashColor(member.DisplayName)) - } else { - parser.text = parser.text.Append(pillTarget) - } - } else { - parser.text = parser.text.Append(pillTarget) - } - } else { - // TODO make text clickable rather than printing URL - parser.text = parser.text.Append(fmt.Sprintf("%s (%s)", tag.Text, tag.Meta)) - } - parser.lineIsNew = false - case "p", "pre", "ol", "ul", "h1", "h2", "h3", "h4", "h5", "h6", "div": - // parser.newline() - } -} - -func (parser *MatrixHTMLProcessor) ReceiveError(err error) { - if err != io.EOF { - debug.Print("Unexpected error parsing HTML:", err) - } -} - -func (parser *MatrixHTMLProcessor) Postprocess() { - if len(parser.text) > 0 && parser.text[len(parser.text)-1].Char == '\n' { - parser.text = parser.text[:len(parser.text)-1] - } -} - -// ParseHTMLMessage parses a HTML-formatted Matrix event into a UIMessage. -func ParseHTMLMessage(room *rooms.Room, evt *gomatrix.Event) tstring.TString { - htmlData, _ := evt.Content["formatted_body"].(string) - - processor := &MatrixHTMLProcessor{ - room: room, - text: tstring.NewBlankTString(), - indent: "", - listType: "", - lineIsNew: true, - openTags: &TagArray{}, - } - - parser := htmlparser.NewHTMLParserFromString(htmlData, processor) - parser.Process() - - return processor.text -} diff --git a/ui/messages/htmltagarray.go b/ui/messages/htmltagarray.go deleted file mode 100644 index 597f0c7..0000000 --- a/ui/messages/htmltagarray.go +++ /dev/null @@ -1,118 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2018 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package messages - -// TagWithMeta is an open HTML tag with some metadata (e.g. list index, a href value). -type TagWithMeta struct { - Tag string - Counter int - Meta string - Text string -} - -// BlankTag is a blank TagWithMeta object. -var BlankTag = &TagWithMeta{} - -// TagArray is a reversed queue for remembering what HTML tags are open. -type TagArray []*TagWithMeta - -// Pushb converts the given byte array into a string and calls Push(). -func (ta *TagArray) Pushb(tag []byte) { - ta.Push(string(tag)) -} - -// Popb converts the given byte array into a string and calls Pop(). -func (ta *TagArray) Popb(tag []byte) *TagWithMeta { - return ta.Pop(string(tag)) -} - -// Indexb converts the given byte array into a string and calls Index(). -func (ta *TagArray) Indexb(tag []byte) { - ta.Index(string(tag)) -} - -// IndexAfterb converts the given byte array into a string and calls IndexAfter(). -func (ta *TagArray) IndexAfterb(tag []byte, after int) { - ta.IndexAfter(string(tag), after) -} - -// Push adds the given tag to the array. -func (ta *TagArray) Push(tag string) { - ta.PushMeta(&TagWithMeta{Tag: tag}) -} - -// Push adds the given tag to the array. -func (ta *TagArray) PushMeta(tag *TagWithMeta) { - *ta = append(*ta, BlankTag) - copy((*ta)[1:], *ta) - (*ta)[0] = tag -} - -// Pop removes the given tag from the array. -func (ta *TagArray) Pop(tag string) (removed *TagWithMeta) { - if (*ta)[0].Tag == tag { - // This is the default case and is lighter than append(), so we handle it separately. - removed = (*ta)[0] - *ta = (*ta)[1:] - } else if index := ta.Index(tag); index != -1 { - removed = (*ta)[index] - *ta = append((*ta)[:index], (*ta)[index+1:]...) - } - return -} - -// Index returns the first index where the given tag is, or -1 if it's not in the list. -func (ta *TagArray) Index(tag string) int { - return ta.IndexAfter(tag, -1) -} - -// IndexAfter returns the first index after the given index where the given tag is, -// or -1 if the given tag is not on the list after the given index. -func (ta *TagArray) IndexAfter(tag string, after int) int { - for i := after + 1; i < len(*ta); i++ { - if (*ta)[i].Tag == tag { - return i - } - } - return -1 -} - -// Get returns the first occurrence of the given tag, or nil if it's not in the list. -func (ta *TagArray) Get(tag string) *TagWithMeta { - return ta.GetAfter(tag, -1) -} - -// IndexAfter returns the first occurrence of the given tag, or nil if the given -// tag is not on the list after the given index. -func (ta *TagArray) GetAfter(tag string, after int) *TagWithMeta { - for i := after + 1; i < len(*ta); i++ { - if (*ta)[i].Tag == tag { - return (*ta)[i] - } - } - return nil -} - -// Has returns whether or not the list has at least one of the given tags. -func (ta *TagArray) Has(tags ...string) bool { - for _, tag := range tags { - if index := ta.Index(tag); index != -1 { - return true - } - } - return false -} diff --git a/ui/messages/parser.go b/ui/messages/parser.go deleted file mode 100644 index 80ce5d6..0000000 --- a/ui/messages/parser.go +++ /dev/null @@ -1,127 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2018 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package messages - -import ( - "fmt" - "time" - - "maunium.net/go/gomatrix" - "maunium.net/go/gomuks/debug" - "maunium.net/go/gomuks/interface" - "maunium.net/go/gomuks/matrix/rooms" - "maunium.net/go/gomuks/ui/messages/tstring" - "maunium.net/go/gomuks/ui/widget" - "maunium.net/go/tcell" -) - -func ParseEvent(gmx ifc.Gomuks, room *rooms.Room, evt *gomatrix.Event) UIMessage { - member := room.GetMember(evt.Sender) - if member != nil { - evt.Sender = member.DisplayName - } - switch evt.Type { - case "m.room.message": - return ParseMessage(gmx, room, evt) - case "m.room.member": - return ParseMembershipEvent(evt) - } - return nil -} - -func unixToTime(unix int64) time.Time { - timestamp := time.Now() - if unix != 0 { - timestamp = time.Unix(unix/1000, unix%1000*1000) - } - return timestamp -} - -func ParseMessage(gmx ifc.Gomuks, room *rooms.Room, evt *gomatrix.Event) UIMessage { - msgtype, _ := evt.Content["msgtype"].(string) - ts := unixToTime(evt.Timestamp) - switch msgtype { - case "m.text", "m.notice", "m.emote": - format, hasFormat := evt.Content["format"].(string) - if hasFormat && format == "org.matrix.custom.html" { - text := ParseHTMLMessage(room, evt) - return NewExpandedTextMessage(evt.ID, evt.Sender, msgtype, text, ts) - } else { - text, _ := evt.Content["body"].(string) - return NewTextMessage(evt.ID, evt.Sender, msgtype, text, ts) - } - case "m.image": - url, _ := evt.Content["url"].(string) - data, hs, id, err := gmx.Matrix().Download(url) - if err != nil { - debug.Printf("Failed to download %s: %v", url, err) - } - return NewImageMessage(gmx, evt.ID, evt.Sender, msgtype, hs, id, data, ts) - } - return nil -} - -func getMembershipEventContent(evt *gomatrix.Event) (sender string, text tstring.TString) { - membership, _ := evt.Content["membership"].(string) - displayname, _ := evt.Content["displayname"].(string) - if len(displayname) == 0 { - displayname = *evt.StateKey - } - prevMembership := "leave" - prevDisplayname := "" - if evt.Unsigned.PrevContent != nil { - prevMembership, _ = evt.Unsigned.PrevContent["membership"].(string) - prevDisplayname, _ = evt.Unsigned.PrevContent["displayname"].(string) - } - - if membership != prevMembership { - switch membership { - case "invite": - sender = "---" - text = tstring.NewColorTString(fmt.Sprintf("%s invited %s.", evt.Sender, displayname), tcell.ColorGreen) - text.Colorize(0, len(evt.Sender), widget.GetHashColor(evt.Sender)) - text.Colorize(len(evt.Sender)+len(" invited "), len(displayname), widget.GetHashColor(displayname)) - case "join": - sender = "-->" - text = tstring.NewColorTString(fmt.Sprintf("%s joined the room.", displayname), tcell.ColorGreen) - text.Colorize(0, len(displayname), widget.GetHashColor(displayname)) - case "leave": - sender = "<--" - if evt.Sender != *evt.StateKey { - reason, _ := evt.Content["reason"].(string) - text = tstring.NewColorTString(fmt.Sprintf("%s kicked %s: %s", evt.Sender, displayname, reason), tcell.ColorRed) - text.Colorize(0, len(evt.Sender), widget.GetHashColor(evt.Sender)) - text.Colorize(len(evt.Sender)+len(" kicked "), len(displayname), widget.GetHashColor(displayname)) - } else { - text = tstring.NewColorTString(fmt.Sprintf("%s left the room.", displayname), tcell.ColorRed) - text.Colorize(0, len(displayname), widget.GetHashColor(displayname)) - } - } - } else if displayname != prevDisplayname { - sender = "---" - text = tstring.NewColorTString(fmt.Sprintf("%s changed their display name to %s.", prevDisplayname, displayname), tcell.ColorYellow) - text.Colorize(0, len(prevDisplayname), widget.GetHashColor(prevDisplayname)) - text.Colorize(len(prevDisplayname)+len(" changed their display name to "), len(displayname), widget.GetHashColor(displayname)) - } - return -} - -func ParseMembershipEvent(evt *gomatrix.Event) UIMessage { - sender, text := getMembershipEventContent(evt) - ts := unixToTime(evt.Timestamp) - return NewExpandedTextMessage(evt.ID, sender, "m.room.membership", text, ts) -} diff --git a/ui/messages/parser/htmlparser.go b/ui/messages/parser/htmlparser.go new file mode 100644 index 0000000..cb8f254 --- /dev/null +++ b/ui/messages/parser/htmlparser.go @@ -0,0 +1,186 @@ +// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package parser + +import ( + "fmt" + "io" + "math" + "regexp" + "strings" + + "maunium.net/go/gomatrix" + "maunium.net/go/gomuks/debug" + "maunium.net/go/gomuks/lib/htmlparser" + "maunium.net/go/gomuks/matrix/rooms" + "maunium.net/go/gomuks/ui/messages/tstring" + "maunium.net/go/gomuks/ui/widget" + "maunium.net/go/tcell" +) + +var matrixToURL = regexp.MustCompile("^(?:https?://)?(?:www\\.)?matrix\\.to/#/([#@!].*)") + +type MatrixHTMLProcessor struct { + text tstring.TString + + indent string + listType string + lineIsNew bool + openTags *TagArray + + room *rooms.Room +} + +func (parser *MatrixHTMLProcessor) newline() { + if !parser.lineIsNew { + parser.text = parser.text.Append("\n" + parser.indent) + parser.lineIsNew = true + } +} + +func (parser *MatrixHTMLProcessor) Preprocess() {} + +func (parser *MatrixHTMLProcessor) HandleText(text string) { + style := tcell.StyleDefault + for _, tag := range *parser.openTags { + switch tag.Tag { + case "b", "strong": + style = style.Bold(true) + case "i", "em": + style = style.Italic(true) + case "s", "del": + style = style.Strikethrough(true) + case "u", "ins": + style = style.Underline(true) + case "a": + tag.Text += text + return + } + } + + if parser.openTags.Has("pre", "code") { + text = strings.Replace(text, "\n", "", -1) + } + parser.text = parser.text.AppendStyle(text, style) + parser.lineIsNew = false +} + +func (parser *MatrixHTMLProcessor) HandleStartTag(tagName string, attrs map[string]string) { + tag := &TagWithMeta{Tag: tagName} + switch tag.Tag { + case "h1", "h2", "h3", "h4", "h5", "h6": + length := int(tag.Tag[1] - '0') + parser.text = parser.text.Append(strings.Repeat("#", length) + " ") + parser.lineIsNew = false + case "a": + tag.Meta, _ = attrs["href"] + case "ol", "ul": + parser.listType = tag.Tag + case "li": + indentSize := 2 + if parser.listType == "ol" { + list := parser.openTags.Get(parser.listType) + list.Counter++ + parser.text = parser.text.Append(fmt.Sprintf("%d. ", list.Counter)) + indentSize = int(math.Log10(float64(list.Counter))+1) + len(". ") + } else { + parser.text = parser.text.Append("* ") + } + parser.indent += strings.Repeat(" ", indentSize) + parser.lineIsNew = false + case "blockquote": + parser.indent += "> " + parser.text = parser.text.Append("> ") + parser.lineIsNew = false + } + parser.openTags.PushMeta(tag) +} + +func (parser *MatrixHTMLProcessor) HandleSelfClosingTag(tagName string, attrs map[string]string) { + if tagName == "br" { + parser.newline() + } +} + +func (parser *MatrixHTMLProcessor) HandleEndTag(tagName string) { + tag := parser.openTags.Pop(tagName) + + switch tag.Tag { + case "li", "blockquote": + indentSize := 2 + if tag.Tag == "li" && parser.listType == "ol" { + list := parser.openTags.Get(parser.listType) + indentSize = int(math.Log10(float64(list.Counter))+1) + len(". ") + } + if len(parser.indent) >= indentSize { + parser.indent = parser.indent[0 : len(parser.indent)-indentSize] + } + // TODO this newline is sometimes not good + parser.newline() + case "a": + match := matrixToURL.FindStringSubmatch(tag.Meta) + if len(match) == 2 { + pillTarget := match[1] + if pillTarget[0] == '@' { + if member := parser.room.GetMember(pillTarget); member != nil { + parser.text = parser.text.AppendColor(member.DisplayName, widget.GetHashColor(member.DisplayName)) + } else { + parser.text = parser.text.Append(pillTarget) + } + } else { + parser.text = parser.text.Append(pillTarget) + } + } else { + // TODO make text clickable rather than printing URL + parser.text = parser.text.Append(fmt.Sprintf("%s (%s)", tag.Text, tag.Meta)) + } + parser.lineIsNew = false + case "p", "pre", "ol", "ul", "h1", "h2", "h3", "h4", "h5", "h6", "div": + // parser.newline() + } +} + +func (parser *MatrixHTMLProcessor) ReceiveError(err error) { + if err != io.EOF { + debug.Print("Unexpected error parsing HTML:", err) + } +} + +func (parser *MatrixHTMLProcessor) Postprocess() { + if len(parser.text) > 0 && parser.text[len(parser.text)-1].Char == '\n' { + parser.text = parser.text[:len(parser.text)-1] + } +} + +// ParseHTMLMessage parses a HTML-formatted Matrix event into a UIMessage. +func ParseHTMLMessage(room *rooms.Room, evt *gomatrix.Event) tstring.TString { + htmlData, _ := evt.Content["formatted_body"].(string) + + processor := &MatrixHTMLProcessor{ + room: room, + text: tstring.NewBlankTString(), + indent: "", + listType: "", + lineIsNew: true, + openTags: &TagArray{}, + } + + parser := htmlparser.NewHTMLParserFromString(htmlData, processor) + parser.Process() + + return processor.text +} diff --git a/ui/messages/parser/htmltagarray.go b/ui/messages/parser/htmltagarray.go new file mode 100644 index 0000000..4cd4245 --- /dev/null +++ b/ui/messages/parser/htmltagarray.go @@ -0,0 +1,118 @@ +// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package parser + +// TagWithMeta is an open HTML tag with some metadata (e.g. list index, a href value). +type TagWithMeta struct { + Tag string + Counter int + Meta string + Text string +} + +// BlankTag is a blank TagWithMeta object. +var BlankTag = &TagWithMeta{} + +// TagArray is a reversed queue for remembering what HTML tags are open. +type TagArray []*TagWithMeta + +// Pushb converts the given byte array into a string and calls Push(). +func (ta *TagArray) Pushb(tag []byte) { + ta.Push(string(tag)) +} + +// Popb converts the given byte array into a string and calls Pop(). +func (ta *TagArray) Popb(tag []byte) *TagWithMeta { + return ta.Pop(string(tag)) +} + +// Indexb converts the given byte array into a string and calls Index(). +func (ta *TagArray) Indexb(tag []byte) { + ta.Index(string(tag)) +} + +// IndexAfterb converts the given byte array into a string and calls IndexAfter(). +func (ta *TagArray) IndexAfterb(tag []byte, after int) { + ta.IndexAfter(string(tag), after) +} + +// Push adds the given tag to the array. +func (ta *TagArray) Push(tag string) { + ta.PushMeta(&TagWithMeta{Tag: tag}) +} + +// Push adds the given tag to the array. +func (ta *TagArray) PushMeta(tag *TagWithMeta) { + *ta = append(*ta, BlankTag) + copy((*ta)[1:], *ta) + (*ta)[0] = tag +} + +// Pop removes the given tag from the array. +func (ta *TagArray) Pop(tag string) (removed *TagWithMeta) { + if (*ta)[0].Tag == tag { + // This is the default case and is lighter than append(), so we handle it separately. + removed = (*ta)[0] + *ta = (*ta)[1:] + } else if index := ta.Index(tag); index != -1 { + removed = (*ta)[index] + *ta = append((*ta)[:index], (*ta)[index+1:]...) + } + return +} + +// Index returns the first index where the given tag is, or -1 if it's not in the list. +func (ta *TagArray) Index(tag string) int { + return ta.IndexAfter(tag, -1) +} + +// IndexAfter returns the first index after the given index where the given tag is, +// or -1 if the given tag is not on the list after the given index. +func (ta *TagArray) IndexAfter(tag string, after int) int { + for i := after + 1; i < len(*ta); i++ { + if (*ta)[i].Tag == tag { + return i + } + } + return -1 +} + +// Get returns the first occurrence of the given tag, or nil if it's not in the list. +func (ta *TagArray) Get(tag string) *TagWithMeta { + return ta.GetAfter(tag, -1) +} + +// IndexAfter returns the first occurrence of the given tag, or nil if the given +// tag is not on the list after the given index. +func (ta *TagArray) GetAfter(tag string, after int) *TagWithMeta { + for i := after + 1; i < len(*ta); i++ { + if (*ta)[i].Tag == tag { + return (*ta)[i] + } + } + return nil +} + +// Has returns whether or not the list has at least one of the given tags. +func (ta *TagArray) Has(tags ...string) bool { + for _, tag := range tags { + if index := ta.Index(tag); index != -1 { + return true + } + } + return false +} diff --git a/ui/messages/parser/parser.go b/ui/messages/parser/parser.go new file mode 100644 index 0000000..939dd10 --- /dev/null +++ b/ui/messages/parser/parser.go @@ -0,0 +1,128 @@ +// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package parser + +import ( + "fmt" + "time" + + "maunium.net/go/gomatrix" + "maunium.net/go/gomuks/debug" + "maunium.net/go/gomuks/interface" + "maunium.net/go/gomuks/matrix/rooms" + "maunium.net/go/gomuks/ui/messages" + "maunium.net/go/gomuks/ui/messages/tstring" + "maunium.net/go/gomuks/ui/widget" + "maunium.net/go/tcell" +) + +func ParseEvent(gmx ifc.Gomuks, room *rooms.Room, evt *gomatrix.Event) messages.UIMessage { + member := room.GetMember(evt.Sender) + if member != nil { + evt.Sender = member.DisplayName + } + switch evt.Type { + case "m.room.message": + return ParseMessage(gmx, room, evt) + case "m.room.member": + return ParseMembershipEvent(evt) + } + return nil +} + +func unixToTime(unix int64) time.Time { + timestamp := time.Now() + if unix != 0 { + timestamp = time.Unix(unix/1000, unix%1000*1000) + } + return timestamp +} + +func ParseMessage(gmx ifc.Gomuks, room *rooms.Room, evt *gomatrix.Event) messages.UIMessage { + msgtype, _ := evt.Content["msgtype"].(string) + ts := unixToTime(evt.Timestamp) + switch msgtype { + case "m.text", "m.notice", "m.emote": + format, hasFormat := evt.Content["format"].(string) + if hasFormat && format == "org.matrix.custom.html" { + text := ParseHTMLMessage(room, evt) + return messages.NewExpandedTextMessage(evt.ID, evt.Sender, msgtype, text, ts) + } else { + text, _ := evt.Content["body"].(string) + return messages.NewTextMessage(evt.ID, evt.Sender, msgtype, text, ts) + } + case "m.image": + url, _ := evt.Content["url"].(string) + data, hs, id, err := gmx.Matrix().Download(url) + if err != nil { + debug.Printf("Failed to download %s: %v", url, err) + } + return messages.NewImageMessage(gmx, evt.ID, evt.Sender, msgtype, hs, id, data, ts) + } + return nil +} + +func getMembershipEventContent(evt *gomatrix.Event) (sender string, text tstring.TString) { + membership, _ := evt.Content["membership"].(string) + displayname, _ := evt.Content["displayname"].(string) + if len(displayname) == 0 { + displayname = *evt.StateKey + } + prevMembership := "leave" + prevDisplayname := "" + if evt.Unsigned.PrevContent != nil { + prevMembership, _ = evt.Unsigned.PrevContent["membership"].(string) + prevDisplayname, _ = evt.Unsigned.PrevContent["displayname"].(string) + } + + if membership != prevMembership { + switch membership { + case "invite": + sender = "---" + text = tstring.NewColorTString(fmt.Sprintf("%s invited %s.", evt.Sender, displayname), tcell.ColorGreen) + text.Colorize(0, len(evt.Sender), widget.GetHashColor(evt.Sender)) + text.Colorize(len(evt.Sender)+len(" invited "), len(displayname), widget.GetHashColor(displayname)) + case "join": + sender = "-->" + text = tstring.NewColorTString(fmt.Sprintf("%s joined the room.", displayname), tcell.ColorGreen) + text.Colorize(0, len(displayname), widget.GetHashColor(displayname)) + case "leave": + sender = "<--" + if evt.Sender != *evt.StateKey { + reason, _ := evt.Content["reason"].(string) + text = tstring.NewColorTString(fmt.Sprintf("%s kicked %s: %s", evt.Sender, displayname, reason), tcell.ColorRed) + text.Colorize(0, len(evt.Sender), widget.GetHashColor(evt.Sender)) + text.Colorize(len(evt.Sender)+len(" kicked "), len(displayname), widget.GetHashColor(displayname)) + } else { + text = tstring.NewColorTString(fmt.Sprintf("%s left the room.", displayname), tcell.ColorRed) + text.Colorize(0, len(displayname), widget.GetHashColor(displayname)) + } + } + } else if displayname != prevDisplayname { + sender = "---" + text = tstring.NewColorTString(fmt.Sprintf("%s changed their display name to %s.", prevDisplayname, displayname), tcell.ColorYellow) + text.Colorize(0, len(prevDisplayname), widget.GetHashColor(prevDisplayname)) + text.Colorize(len(prevDisplayname)+len(" changed their display name to "), len(displayname), widget.GetHashColor(displayname)) + } + return +} + +func ParseMembershipEvent(evt *gomatrix.Event) messages.UIMessage { + sender, text := getMembershipEventContent(evt) + ts := unixToTime(evt.Timestamp) + return messages.NewExpandedTextMessage(evt.ID, sender, "m.room.membership", text, ts) +} diff --git a/ui/view-main.go b/ui/view-main.go index ccb3cc1..50304fc 100644 --- a/ui/view-main.go +++ b/ui/view-main.go @@ -31,7 +31,7 @@ import ( "maunium.net/go/gomuks/lib/notification" "maunium.net/go/gomuks/matrix/pushrules" "maunium.net/go/gomuks/matrix/rooms" - "maunium.net/go/gomuks/ui/messages" + "maunium.net/go/gomuks/ui/messages/parser" "maunium.net/go/gomuks/ui/widget" "maunium.net/go/tcell" "maunium.net/go/tview" @@ -465,5 +465,5 @@ func (view *MainView) LoadHistory(room string, initial bool) { } func (view *MainView) ParseEvent(roomView ifc.RoomView, evt *gomatrix.Event) ifc.Message { - return messages.ParseEvent(view.gmx, roomView.MxRoom(), evt) + return parser.ParseEvent(view.gmx, roomView.MxRoom(), evt) } -- cgit v1.2.3-70-g09d2 From 85757c387e346ad45105dd124e7e87e044f6b77c Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 14 Apr 2018 11:50:57 +0300 Subject: Fix typo in newline stripping --- ui/messages/parser/htmlparser.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ui/messages') diff --git a/ui/messages/parser/htmlparser.go b/ui/messages/parser/htmlparser.go index cb8f254..9ca707f 100644 --- a/ui/messages/parser/htmlparser.go +++ b/ui/messages/parser/htmlparser.go @@ -72,7 +72,7 @@ func (parser *MatrixHTMLProcessor) HandleText(text string) { } } - if parser.openTags.Has("pre", "code") { + if !parser.openTags.Has("pre", "code") { text = strings.Replace(text, "\n", "", -1) } parser.text = parser.text.AppendStyle(text, style) -- cgit v1.2.3-70-g09d2