Move event parsing to ui/messages and add image displaying

This commit is contained in:
Tulir Asokan
2018-04-10 19:31:28 +03:00
parent ee67c1446c
commit 8270bc0322
14 changed files with 585 additions and 240 deletions

51
ui/messages/cell.go Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
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
}

View File

@ -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

113
ui/messages/imagemessage.go Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
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
}

View File

@ -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"

120
ui/messages/parser.go Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
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)
}

138
ui/messages/string.go Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
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]
}

View File

@ -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')