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 --- 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 ++++++++++++++++++++-- 7 files changed, 490 insertions(+), 154 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/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') -- cgit v1.2.3