aboutsummaryrefslogtreecommitdiff
path: root/ui
diff options
context:
space:
mode:
Diffstat (limited to 'ui')
-rw-r--r--ui/debug/debug.go158
-rw-r--r--ui/debug/doc.go2
-rw-r--r--ui/message-view.go464
-rw-r--r--ui/messages/base.go234
-rw-r--r--ui/messages/doc.go2
-rw-r--r--ui/messages/expandedtextmessage.go71
-rw-r--r--ui/messages/imagemessage.go123
-rw-r--r--ui/messages/message.go (renamed from ui/debug/external.go)35
-rw-r--r--ui/messages/meta.go77
-rw-r--r--ui/messages/parser/htmlparser.go186
-rw-r--r--ui/messages/parser/htmltagarray.go118
-rw-r--r--ui/messages/parser/parser.go128
-rw-r--r--ui/messages/textbase.go84
-rw-r--r--ui/messages/textmessage.go102
-rw-r--r--ui/messages/tstring/cell.go51
-rw-r--r--ui/messages/tstring/string.go173
-rw-r--r--ui/room-list.go (renamed from ui/widget/room-list.go)9
-rw-r--r--ui/room-view.go (renamed from ui/widget/room-view.go)56
-rw-r--r--ui/types/doc.go2
-rw-r--r--ui/types/message.go234
-rw-r--r--ui/types/meta.go71
-rw-r--r--ui/ui.go2
-rw-r--r--ui/view-login.go2
-rw-r--r--ui/view-main.go163
-rw-r--r--ui/widget/advanced-inputfield.go2
-rw-r--r--ui/widget/border.go2
-rw-r--r--ui/widget/color.go2
-rw-r--r--ui/widget/message-view.go354
-rw-r--r--ui/widget/util.go16
29 files changed, 1929 insertions, 994 deletions
diff --git a/ui/debug/debug.go b/ui/debug/debug.go
deleted file mode 100644
index 3f47980..0000000
--- a/ui/debug/debug.go
+++ /dev/null
@@ -1,158 +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 <http://www.gnu.org/licenses/>.
-
-package debug
-
-import (
- "fmt"
- "io/ioutil"
- "os"
- "time"
-
- "runtime/debug"
-
- "maunium.net/go/tview"
-)
-
-type Printer interface {
- Printf(text string, args ...interface{})
- Print(text ...interface{})
-}
-
-type Pane struct {
- *tview.TextView
- Height int
- Width int
- num int
-}
-
-var Default Printer
-var RedirectAllExt bool
-
-func NewPane() *Pane {
- pane := tview.NewTextView()
- pane.
- SetScrollable(true).
- SetWrap(true).
- SetBorder(true).
- SetTitle("Debug output")
- fmt.Fprintln(pane, "[0] Debug pane initialized")
-
- return &Pane{
- TextView: pane,
- Height: 35,
- Width: 80,
- num: 0,
- }
-}
-
-func (db *Pane) Printf(text string, args ...interface{}) {
- db.WriteString(fmt.Sprintf(text, args...) + "\n")
-}
-
-func (db *Pane) Print(text ...interface{}) {
- db.WriteString(fmt.Sprintln(text...))
-}
-
-func (db *Pane) WriteString(text string) {
- db.num++
- fmt.Fprintf(db, "[%d] %s", db.num, text)
-}
-
-type PaneSide int
-
-const (
- Top PaneSide = iota
- Bottom
- Left
- Right
-)
-
-func (db *Pane) Wrap(main tview.Primitive, side PaneSide) tview.Primitive {
- rows, columns := []int{0}, []int{0}
- mainRow, mainColumn, paneRow, paneColumn := 0, 0, 0, 0
- switch side {
- case Top:
- rows = []int{db.Height, 0}
- mainRow = 1
- case Bottom:
- rows = []int{0, db.Height}
- paneRow = 1
- case Left:
- columns = []int{db.Width, 0}
- mainColumn = 1
- case Right:
- columns = []int{0, db.Width}
- paneColumn = 1
- }
- return tview.NewGrid().SetRows(rows...).SetColumns(columns...).
- AddItem(main, mainRow, mainColumn, 1, 1, 1, 1, true).
- AddItem(db, paneRow, paneColumn, 1, 1, 1, 1, false)
-}
-
-func Printf(text string, args ...interface{}) {
- if RedirectAllExt {
- ExtPrintf(text, args...)
- } else if Default != nil {
- Default.Printf(text, args...)
- }
-}
-
-func Print(text ...interface{}) {
- if RedirectAllExt {
- ExtPrint(text...)
- } else if Default != nil {
- Default.Print(text...)
- }
-}
-
-const Oops = ` __________
-< Oh noes! >
- ‾‾‾\‾‾‾‾‾‾
- \ ^__^
- \ (XX)\_______
- (__)\ )\/\
- U ||----W |
- || ||`
-
-func PrettyPanic() {
- fmt.Println(Oops)
- fmt.Println("")
- fmt.Println("A fatal error has occurred.")
- fmt.Println("")
- traceFile := fmt.Sprintf("/tmp/gomuks-panic-%s.txt", time.Now().Format("2006-01-02--15-04-05"))
- data := debug.Stack()
- err := ioutil.WriteFile(traceFile, data, 0644)
- if err != nil {
- fmt.Println("Saving the stack trace to", traceFile, "failed:")
- fmt.Println("--------------------------------------------------------------------------------")
- fmt.Println(err)
- fmt.Println("--------------------------------------------------------------------------------")
- fmt.Println("")
- fmt.Println("You can file an issue at https://github.com/tulir/gomuks/issues.")
- fmt.Println("Please provide the file save error (above) and the stack trace of the original error (below) when filing an issue.")
- fmt.Println("")
- fmt.Println("--------------------------------------------------------------------------------")
- debug.PrintStack()
- fmt.Println("--------------------------------------------------------------------------------")
- } else {
- fmt.Println("The stack trace has been saved to", traceFile)
- fmt.Println("")
- fmt.Println("You can file an issue at https://github.com/tulir/gomuks/issues.")
- fmt.Println("Please provide the contents of that file when filing an issue.")
- }
- os.Exit(1)
-}
diff --git a/ui/debug/doc.go b/ui/debug/doc.go
deleted file mode 100644
index a321689..0000000
--- a/ui/debug/doc.go
+++ /dev/null
@@ -1,2 +0,0 @@
-// Package debug contains utilities to display debug messages while running an interactive tview program.
-package debug
diff --git a/ui/message-view.go b/ui/message-view.go
new file mode 100644
index 0000000..80add59
--- /dev/null
+++ b/ui/message-view.go
@@ -0,0 +1,464 @@
+// 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 ui
+
+import (
+ "encoding/gob"
+ "fmt"
+ "math"
+ "os"
+
+ "maunium.net/go/gomuks/debug"
+ "maunium.net/go/gomuks/interface"
+ "maunium.net/go/gomuks/lib/open"
+ "maunium.net/go/gomuks/ui/messages"
+ "maunium.net/go/gomuks/ui/messages/tstring"
+ "maunium.net/go/gomuks/ui/widget"
+ "maunium.net/go/tcell"
+ "maunium.net/go/tview"
+)
+
+type MessageView struct {
+ *tview.Box
+
+ parent *RoomView
+
+ ScrollOffset int
+ MaxSenderWidth int
+ DateFormat string
+ TimestampFormat string
+ TimestampWidth int
+ LoadingMessages bool
+
+ widestSender int
+ prevWidth int
+ prevHeight int
+ prevMsgCount int
+
+ messageIDs map[string]messages.UIMessage
+ messages []messages.UIMessage
+
+ textBuffer []tstring.TString
+ metaBuffer []ifc.MessageMeta
+}
+
+func NewMessageView(parent *RoomView) *MessageView {
+ return &MessageView{
+ Box: tview.NewBox(),
+ parent: parent,
+
+ MaxSenderWidth: 15,
+ TimestampWidth: len(messages.TimeFormat),
+ ScrollOffset: 0,
+
+ messages: make([]messages.UIMessage, 0),
+ messageIDs: make(map[string]messages.UIMessage),
+ textBuffer: make([]tstring.TString, 0),
+ metaBuffer: make([]ifc.MessageMeta, 0),
+
+ widestSender: 5,
+ prevWidth: -1,
+ prevHeight: -1,
+ prevMsgCount: -1,
+ }
+}
+
+func (view *MessageView) SaveHistory(path string) error {
+ file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0600)
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+
+ enc := gob.NewEncoder(file)
+ err = enc.Encode(view.messages)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+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) {
+ return 0, nil
+ }
+ return -1, err
+ }
+ defer file.Close()
+
+ var msgs []messages.UIMessage
+
+ dec := gob.NewDecoder(file)
+ err = dec.Decode(&msgs)
+ if err != nil {
+ return -1, err
+ }
+
+ 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())
+ message.RegisterGomuks(gmx)
+ } else {
+ indexOffset++
+ }
+ }
+
+ return len(view.messages), nil
+}
+
+func (view *MessageView) updateWidestSender(sender string) {
+ if len(sender) > view.widestSender {
+ view.widestSender = len(sender)
+ if view.widestSender > view.MaxSenderWidth {
+ view.widestSender = view.MaxSenderWidth
+ }
+ }
+}
+
+func (view *MessageView) UpdateMessageID(ifcMessage ifc.Message, newID string) {
+ message, ok := ifcMessage.(messages.UIMessage)
+ if !ok {
+ debug.Print("[Warning] Passed non-UIMessage ifc.Message object to UpdateMessageID().")
+ debug.PrintStack()
+ return
+ }
+ delete(view.messageIDs, message.ID())
+ message.SetID(newID)
+ view.messageIDs[message.ID()] = message
+}
+
+func (view *MessageView) AddMessage(ifcMessage ifc.Message, direction ifc.MessageDirection) {
+ if ifcMessage == nil {
+ return
+ }
+ message, ok := ifcMessage.(messages.UIMessage)
+ if !ok {
+ debug.Print("[Warning] Passed non-UIMessage ifc.Message object to AddMessage().")
+ debug.PrintStack()
+ return
+ }
+
+ oldMsg, messageExists := view.messageIDs[message.ID()]
+ if messageExists {
+ oldMsg.CopyFrom(message)
+ message = oldMsg
+ direction = ifc.IgnoreMessage
+ }
+
+ view.updateWidestSender(message.Sender())
+
+ _, _, width, _ := view.GetInnerRect()
+ width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap
+ message.CalculateBuffer(width)
+
+ if direction == ifc.AppendMessage {
+ if view.ScrollOffset > 0 {
+ view.ScrollOffset += message.Height()
+ }
+ view.messages = append(view.messages, message)
+ 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
+}
+
+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, tstring.NewColorTString(
+ fmt.Sprintf("Date changed to %s", message.FormatDate()),
+ tcell.ColorGreen))
+ view.metaBuffer = append(view.metaBuffer, &messages.BasicMeta{
+ BTimestampColor: tcell.ColorDefault, BTextColor: tcell.ColorGreen})
+ }
+ }
+
+ view.textBuffer = append(view.textBuffer, message.Buffer()...)
+ for range message.Buffer() {
+ view.metaBuffer = append(view.metaBuffer, message)
+ }
+ 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 {
+ 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 = []tstring.TString{}
+ view.metaBuffer = []ifc.MessageMeta{}
+ view.prevMsgCount = 0
+ for i, message := range view.messages {
+ if message == nil {
+ debug.Print("O.o found nil message at", i)
+ break
+ }
+ if recalculateMessageBuffers {
+ message.CalculateBuffer(width)
+ }
+ view.appendBuffer(message)
+ }
+ view.prevHeight = height
+ view.prevWidth = width
+ }
+}
+
+func (view *MessageView) HandleClick(x, y int, button tcell.ButtonMask) bool {
+ if button != tcell.Button1 {
+ return false
+ }
+
+ _, _, _, height := view.GetRect()
+ line := view.TotalHeight() - view.ScrollOffset - height + y
+ if line < 0 || line >= view.TotalHeight() {
+ return false
+ }
+
+ message := view.metaBuffer[line]
+ var prevMessage ifc.MessageMeta
+ if line > 0 {
+ prevMessage = view.metaBuffer[line-1]
+ }
+
+ usernameX := view.TimestampWidth + TimestampSenderGap
+ messageX := usernameX + view.widestSender + SenderMessageGap
+ if x >= messageX {
+ switch message := message.(type) {
+ case *messages.ImageMessage:
+ open.Open(message.Path())
+ case messages.UIMessage:
+ debug.Print("Message clicked:", message.NotificationContent())
+ }
+ } else if x >= usernameX {
+ uiMessage, ok := message.(messages.UIMessage)
+ if !ok {
+ return false
+ }
+
+ prevUIMessage, _ := prevMessage.(messages.UIMessage)
+ if prevUIMessage != nil && prevUIMessage.Sender() == uiMessage.Sender() {
+ return false
+ }
+
+ sender := []rune(uiMessage.Sender())
+ if len(sender) == 0 {
+ return false
+ }
+
+ cursorPos := view.parent.input.GetCursorOffset()
+ text := []rune(view.parent.input.GetText())
+ var newText []rune
+ if cursorPos == 0 {
+ newText = append(sender, ':', ' ')
+ newText = append(newText, text...)
+ } else {
+ newText = append(text[0:cursorPos], sender...)
+ newText = append(newText, ' ')
+ newText = append(newText, text[cursorPos:]...)
+ }
+ view.parent.input.SetText(string(newText))
+ view.parent.input.SetCursorOffset(cursorPos + len(newText) - len(text))
+ return true
+ }
+ return false
+}
+
+const PaddingAtTop = 5
+
+func (view *MessageView) AddScrollOffset(diff int) {
+ _, _, _, height := view.GetInnerRect()
+
+ totalHeight := view.TotalHeight()
+ if diff >= 0 && view.ScrollOffset+diff >= totalHeight-height+PaddingAtTop {
+ view.ScrollOffset = totalHeight - height + PaddingAtTop
+ } else {
+ view.ScrollOffset += diff
+ }
+
+ if view.ScrollOffset > totalHeight-height+PaddingAtTop {
+ view.ScrollOffset = totalHeight - height + PaddingAtTop
+ }
+ if view.ScrollOffset < 0 {
+ view.ScrollOffset = 0
+ }
+}
+
+func (view *MessageView) Height() int {
+ _, _, _, height := view.GetInnerRect()
+ return height
+}
+
+func (view *MessageView) TotalHeight() int {
+ return len(view.textBuffer)
+}
+
+func (view *MessageView) IsAtTop() bool {
+ _, _, _, height := view.GetInnerRect()
+ totalHeight := len(view.textBuffer)
+ return view.ScrollOffset >= totalHeight-height+PaddingAtTop
+}
+
+const (
+ TimestampSenderGap = 1
+ SenderSeparatorGap = 1
+ SenderMessageGap = 3
+)
+
+func getScrollbarStyle(scrollbarHere, isTop, isBottom bool) (char rune, style tcell.Style) {
+ char = '│'
+ style = tcell.StyleDefault
+ if scrollbarHere {
+ style = style.Foreground(tcell.ColorGreen)
+ }
+ if isTop {
+ if scrollbarHere {
+ char = '╥'
+ } else {
+ char = '┬'
+ }
+ } else if isBottom {
+ if scrollbarHere {
+ char = '╨'
+ } else {
+ char = '┴'
+ }
+ } else if scrollbarHere {
+ char = '║'
+ }
+ return
+}
+
+func (view *MessageView) Draw(screen tcell.Screen) {
+ view.Box.Draw(screen)
+
+ x, y, _, height := view.GetInnerRect()
+ view.recalculateBuffers()
+
+ if view.TotalHeight() == 0 {
+ widget.WriteLineSimple(screen, "It's quite empty in here.", x, y+height)
+ return
+ }
+
+ usernameX := x + view.TimestampWidth + TimestampSenderGap
+ messageX := usernameX + view.widestSender + SenderMessageGap
+ separatorX := usernameX + view.widestSender + SenderSeparatorGap
+
+ indexOffset := view.TotalHeight() - view.ScrollOffset - height
+ if indexOffset <= -PaddingAtTop {
+ message := "Scroll up to load more messages."
+ if view.LoadingMessages {
+ message = "Loading more messages..."
+ }
+ widget.WriteLineSimpleColor(screen, message, messageX, y, tcell.ColorGreen)
+ }
+
+ if len(view.textBuffer) != len(view.metaBuffer) {
+ debug.Printf("Unexpected text/meta buffer length mismatch: %d != %d.", len(view.textBuffer), len(view.metaBuffer))
+ return
+ }
+
+ var scrollBarHeight, scrollBarPos int
+ // Black magic (aka math) used to figure out where the scroll bar should be put.
+ {
+ viewportHeight := float64(height)
+ contentHeight := float64(view.TotalHeight())
+
+ scrollBarHeight = int(math.Ceil(viewportHeight / (contentHeight / viewportHeight)))
+
+ scrollBarPos = height - int(math.Round(float64(view.ScrollOffset)/contentHeight*viewportHeight))
+ }
+
+ var prevMeta ifc.MessageMeta
+ firstLine := true
+ skippedLines := 0
+
+ for line := 0; line < height; line++ {
+ index := indexOffset + line
+ if index < 0 {
+ skippedLines++
+ continue
+ } else if index >= view.TotalHeight() {
+ break
+ }
+
+ showScrollbar := line-skippedLines >= scrollBarPos-scrollBarHeight && line-skippedLines < scrollBarPos
+ isTop := firstLine && view.ScrollOffset+height >= view.TotalHeight()
+ isBottom := line == height-1 && view.ScrollOffset == 0
+
+ borderChar, borderStyle := getScrollbarStyle(showScrollbar, isTop, isBottom)
+
+ firstLine = false
+
+ screen.SetContent(separatorX, y+line, borderChar, nil, borderStyle)
+
+ text, meta := view.textBuffer[index], view.metaBuffer[index]
+ if meta != prevMeta {
+ if len(meta.FormatTime()) > 0 {
+ widget.WriteLineSimpleColor(screen, meta.FormatTime(), x, y+line, meta.TimestampColor())
+ }
+ if prevMeta == nil || meta.Sender() != prevMeta.Sender() {
+ widget.WriteLineColor(
+ screen, tview.AlignRight, meta.Sender(),
+ usernameX, y+line, view.widestSender,
+ meta.SenderColor())
+ }
+ prevMeta = meta
+ }
+
+ text.Draw(screen, messageX, y+line)
+ }
+}
diff --git a/ui/messages/base.go b/ui/messages/base.go
new file mode 100644
index 0000000..aed7903
--- /dev/null
+++ b/ui/messages/base.go
@@ -0,0 +1,234 @@
+// 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 (
+ "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, 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/doc.go b/ui/messages/doc.go
new file mode 100644
index 0000000..289c308
--- /dev/null
+++ b/ui/messages/doc.go
@@ -0,0 +1,2 @@
+// Package messages contains different message types and code to generate and render them.
+package messages
diff --git a/ui/messages/expandedtextmessage.go b/ui/messages/expandedtextmessage.go
new file mode 100644
index 0000000..3ee15ad
--- /dev/null
+++ b/ui/messages/expandedtextmessage.go
@@ -0,0 +1,71 @@
+// 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 (
+ "encoding/gob"
+ "time"
+
+ "maunium.net/go/gomuks/interface"
+ "maunium.net/go/gomuks/ui/messages/tstring"
+)
+
+func init() {
+ gob.Register(&ExpandedTextMessage{})
+}
+
+type ExpandedTextMessage struct {
+ BaseTextMessage
+ MsgText tstring.TString
+}
+
+// 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 &ExpandedTextMessage{
+ BaseTextMessage: newBaseTextMessage(id, sender, msgtype, timestamp),
+ MsgText: text,
+ }
+}
+
+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 *ExpandedTextMessage) CopyFrom(from ifc.MessageMeta) {
+ msg.BaseTextMessage.CopyFrom(from)
+
+ fromExpandedMsg, ok := from.(*ExpandedTextMessage)
+ if ok {
+ msg.MsgText = fromExpandedMsg.MsgText
+ }
+
+ 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
new file mode 100644
index 0000000..2fbf6ae
--- /dev/null
+++ b/ui/messages/imagemessage.go
@@ -0,0 +1,123 @@
+// 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"
+ "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/tcell"
+)
+
+func init() {
+ gob.Register(&ImageMessage{})
+}
+
+type ImageMessage struct {
+ BaseMessage
+ Homeserver string
+ FileID string
+ data []byte
+
+ gmx ifc.Gomuks
+}
+
+// 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 &ImageMessage{
+ newBaseMessage(id, sender, msgtype, timestamp),
+ homeserver,
+ fileID,
+ data,
+ gmx,
+ }
+}
+
+func (msg *ImageMessage) 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 *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 {
+ debug.Print("Failed to download image %s/%s: %v", msg.Homeserver, msg.FileID, err)
+ return
+ }
+ msg.data = data
+}
+
+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 *ImageMessage) CopyFrom(from ifc.MessageMeta) {
+ msg.BaseMessage.CopyFrom(from)
+
+ fromImgMsg, ok := from.(*ImageMessage)
+ 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 *ImageMessage) CalculateBuffer(width int) {
+ if width < 2 {
+ return
+ }
+
+ image, err := ansimage.NewScaledFromReader(bytes.NewReader(msg.data), 0, width, color.Black)
+ if err != nil {
+ msg.buffer = []tstring.TString{tstring.NewColorTString("Failed to display image", tcell.ColorRed)}
+ debug.Print("Failed to display image:", err)
+ return
+ }
+
+ 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/debug/external.go b/ui/messages/message.go
index faabbcc..6ebfb6d 100644
--- a/ui/debug/external.go
+++ b/ui/messages/message.go
@@ -14,32 +14,25 @@
// 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 debug
+package messages
import (
- "fmt"
- "io"
- "os"
+ "maunium.net/go/gomuks/interface"
+ "maunium.net/go/gomuks/ui/messages/tstring"
)
-var writer io.Writer
+// Message is a wrapper for the content and metadata of a Matrix message intended to be displayed.
+type UIMessage interface {
+ ifc.Message
-func EnableExternal() {
- var err error
- writer, err = os.OpenFile("/tmp/gomuks-debug.log", os.O_WRONLY|os.O_APPEND, 0644)
- if err != nil {
- writer = nil
- }
-}
+ CalculateBuffer(width int)
+ RecalculateBuffer()
+ Buffer() []tstring.TString
+ Height() int
-func ExtPrintf(text string, args ...interface{}) {
- if writer != nil {
- fmt.Fprintf(writer, text+"\n", args...)
- }
+ RealSender() string
+ RegisterGomuks(gmx ifc.Gomuks)
}
-func ExtPrint(text ...interface{}) {
- if writer != nil {
- fmt.Fprintln(writer, text...)
- }
-}
+const DateFormat = "January _2, 2006"
+const TimeFormat = "15:04:05"
diff --git a/ui/messages/meta.go b/ui/messages/meta.go
new file mode 100644
index 0000000..7e2f29f
--- /dev/null
+++ b/ui/messages/meta.go
@@ -0,0 +1,77 @@
+// 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 (
+ "time"
+
+ "maunium.net/go/tcell"
+ "maunium.net/go/gomuks/interface"
+)
+
+// BasicMeta is a simple variable store implementation of MessageMeta.
+type BasicMeta struct {
+ BSender string
+ BTimestamp time.Time
+ 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 full time when the message was sent.
+func (meta *BasicMeta) Timestamp() time.Time {
+ return meta.BTimestamp
+}
+
+// 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.
+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.BSenderColor = from.SenderColor()
+ meta.BTextColor = from.TextColor()
+ meta.BTimestampColor = from.TimestampColor()
+}
diff --git a/ui/messages/parser/htmlparser.go b/ui/messages/parser/htmlparser.go
new file mode 100644
index 0000000..9ca707f
--- /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 <http://www.gnu.org/licenses/>.
+
+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 <http://www.gnu.org/licenses/>.
+
+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 <http://www.gnu.org/licenses/>.
+
+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/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 <http://www.gnu.org/licenses/>.
+
+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
new file mode 100644
index 0000000..4c99e5b
--- /dev/null
+++ b/ui/messages/textmessage.go
@@ -0,0 +1,102 @@
+// 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 (
+ "encoding/gob"
+ "fmt"
+ "time"
+
+ "maunium.net/go/gomuks/interface"
+ "maunium.net/go/gomuks/ui/messages/tstring"
+)
+
+func init() {
+ gob.Register(&TextMessage{})
+}
+
+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 &TextMessage{
+ BaseTextMessage: newBaseTextMessage(id, sender, msgtype, timestamp),
+ MsgText: text,
+ }
+}
+
+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
+}
+
+// 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)
+
+ fromTextMsg, ok := from.(*TextMessage)
+ if ok {
+ msg.MsgText = fromTextMsg.MsgText
+ }
+
+ msg.cache = nil
+ msg.RecalculateBuffer()
+}
+func (msg *TextMessage) SetType(msgtype string) {
+ msg.BaseTextMessage.SetType(msgtype)
+ msg.cache = nil
+}
+
+func (msg *TextMessage) SetState(state ifc.MessageState) {
+ msg.BaseTextMessage.SetState(state)
+ msg.cache = nil
+}
+
+func (msg *TextMessage) SetIsHighlight(isHighlight bool) {
+ msg.BaseTextMessage.SetIsHighlight(isHighlight)
+ msg.cache = nil
+}
+
+func (msg *TextMessage) SetIsService(isService bool) {
+ msg.BaseTextMessage.SetIsService(isService)
+ msg.cache = nil
+}
+
+func (msg *TextMessage) NotificationContent() string {
+ return msg.MsgText
+}
+
+func (msg *TextMessage) CalculateBuffer(width int) {
+ msg.BaseTextMessage.calculateBufferWithText(msg.getCache(), width)
+}
+
+// RecalculateBuffer calculates the buffer again with the previously provided width.
+func (msg *TextMessage) RecalculateBuffer() {
+ msg.CalculateBuffer(msg.prevBufferWidth)
+}
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 <http://www.gnu.org/licenses/>.
+
+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..a87d16a
--- /dev/null
+++ b/ui/messages/tstring/string.go
@@ -0,0 +1,173 @@
+// 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 tstring
+
+import (
+ "strings"
+
+ "github.com/mattn/go-runewidth"
+ "maunium.net/go/tcell"
+)
+
+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 {
+ 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) 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)
+ }
+}
+
+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/widget/room-list.go b/ui/room-list.go
index d2fb543..b85ed24 100644
--- a/ui/widget/room-list.go
+++ b/ui/room-list.go
@@ -14,14 +14,15 @@
// 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 widget
+package ui
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"
)
@@ -126,11 +127,11 @@ func (list *RoomList) Draw(screen tcell.Screen) {
unreadMessageCount += "!"
}
unreadMessageCount = fmt.Sprintf("(%s)", unreadMessageCount)
- writeLine(screen, tview.AlignRight, unreadMessageCount, x+lineWidth-6, y, 6, style)
+ widget.WriteLine(screen, tview.AlignRight, unreadMessageCount, x+lineWidth-6, y, 6, style)
lineWidth -= len(unreadMessageCount) + 1
}
- writeLine(screen, tview.AlignLeft, text, x, y, lineWidth, style)
+ widget.WriteLine(screen, tview.AlignLeft, text, x, y, lineWidth, style)
y++
if y >= bottomLimit {
diff --git a/ui/widget/room-view.go b/ui/room-view.go
index 4bab779..d7824fe 100644
--- a/ui/widget/room-view.go
+++ b/ui/room-view.go
@@ -14,7 +14,7 @@
// 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 widget
+package ui
import (
"fmt"
@@ -23,9 +23,11 @@ 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/types"
+ "maunium.net/go/gomuks/ui/messages"
+ "maunium.net/go/gomuks/ui/widget"
"maunium.net/go/tview"
)
@@ -36,8 +38,8 @@ type RoomView struct {
content *MessageView
status *tview.TextView
userList *tview.TextView
- ulBorder *Border
- input *AdvancedInputField
+ ulBorder *widget.Border
+ input *widget.AdvancedInputField
Room *rooms.Room
}
@@ -45,13 +47,13 @@ func NewRoomView(room *rooms.Room) *RoomView {
view := &RoomView{
Box: tview.NewBox(),
topic: tview.NewTextView(),
- content: NewMessageView(),
status: tview.NewTextView(),
userList: tview.NewTextView(),
- ulBorder: NewBorder(),
- input: NewAdvancedInputField(),
+ ulBorder: widget.NewBorder(),
+ input: widget.NewAdvancedInputField(),
Room: room,
}
+ view.content = NewMessageView(view)
view.input.
SetFieldBackgroundColor(tcell.ColorDefault).
@@ -79,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 {
@@ -129,7 +131,7 @@ func (view *RoomView) GetInputText() string {
return view.input.GetText()
}
-func (view *RoomView) GetInputField() *AdvancedInputField {
+func (view *RoomView) GetInputField() *widget.AdvancedInputField {
return view.input
}
@@ -230,15 +232,19 @@ func (view *RoomView) MessageView() *MessageView {
return view.content
}
+func (view *RoomView) MxRoom() *rooms.Room {
+ return view.Room
+}
+
func (view *RoomView) UpdateUserList() {
var joined strings.Builder
var invited strings.Builder
for _, user := range view.Room.GetMembers() {
if user.Membership == "join" {
- joined.WriteString(AddHashColor(user.DisplayName))
+ joined.WriteString(widget.AddHashColor(user.DisplayName))
joined.WriteRune('\n')
} else if user.Membership == "invite" {
- invited.WriteString(AddHashColor(user.DisplayName))
+ invited.WriteString(widget.AddHashColor(user.DisplayName))
invited.WriteRune('\n')
}
}
@@ -249,27 +255,37 @@ func (view *RoomView) UpdateUserList() {
}
}
-func (view *RoomView) NewMessage(id, sender, msgtype, text string, timestamp time.Time) *types.Message {
+func (view *RoomView) newUIMessage(id, sender, msgtype, text string, timestamp time.Time) messages.UIMessage {
member := view.Room.GetMember(sender)
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 {
+ return view.newUIMessage(id, sender, msgtype, text, timestamp)
}
-func (view *RoomView) NewTempMessage(msgtype, text string) *types.Message {
+func (view *RoomView) NewTempMessage(msgtype, text string) ifc.Message {
now := time.Now()
id := strconv.FormatInt(now.UnixNano(), 10)
sender := ""
if ownerMember := view.Room.GetSessionOwner(); ownerMember != nil {
sender = ownerMember.DisplayName
}
- message := view.NewMessage(id, sender, msgtype, text, now)
- message.State = types.MessageStateSending
- view.AddMessage(message, AppendMessage)
+ message := view.newUIMessage(id, sender, msgtype, text, now)
+ message.SetState(ifc.MessageStateSending)
+ view.AddMessage(message, ifc.AppendMessage)
return message
}
-func (view *RoomView) AddMessage(message *types.Message, direction MessageDirection) {
+func (view *RoomView) AddServiceMessage(text string) {
+ message := view.newUIMessage("", "*", "gomuks.service", text, time.Now())
+ message.SetIsService(true)
+ view.AddMessage(message, ifc.AppendMessage)
+}
+
+func (view *RoomView) AddMessage(message ifc.Message, direction ifc.MessageDirection) {
view.content.AddMessage(message, direction)
}
diff --git a/ui/types/doc.go b/ui/types/doc.go
deleted file mode 100644
index 5bc229c..0000000
--- a/ui/types/doc.go
+++ /dev/null
@@ -1,2 +0,0 @@
-// Package types contains common type definitions used mostly by the UI, but also other parts of gomuks.
-package types
diff --git a/ui/types/message.go b/ui/types/message.go
deleted file mode 100644
index fa3b6ef..0000000
--- a/ui/types/message.go
+++ /dev/null
@@ -1,234 +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 <http://www.gnu.org/licenses/>.
-
-package types
-
-import (
- "fmt"
- "regexp"
- "strings"
-
- "github.com/gdamore/tcell"
- "github.com/mattn/go-runewidth"
-)
-
-// MessageState is an enum to specify if a Message is being sent, failed to send or was successfully sent.
-type MessageState int
-
-// Allowed MessageStates.
-const (
- MessageStateSending MessageState = iota
- MessageStateDefault
- MessageStateFailed
-)
-
-// Message is a wrapper for the content and metadata of a Matrix message intended to be displayed.
-type Message struct {
- ID string
- Type string
- Sender string
- SenderColor tcell.Color
- TextColor tcell.Color
- Timestamp string
- Date string
- Text string
- State MessageState
- 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) *Message {
- return &Message{
- Sender: sender,
- Timestamp: timestamp,
- Date: date,
- SenderColor: senderColor,
- TextColor: tcell.ColorDefault,
- Type: msgtype,
- Text: text,
- ID: id,
- prevBufferWidth: 0,
- State: MessageStateDefault,
- }
-}
-
-// CopyTo copies the content of this message to the given message.
-func (message *Message) CopyTo(to *Message) {
- to.ID = message.ID
- to.Type = message.Type
- to.Sender = message.Sender
- to.SenderColor = message.SenderColor
- to.TextColor = message.TextColor
- to.Timestamp = message.Timestamp
- to.Date = message.Date
- to.Text = message.Text
- to.State = message.State
- to.RecalculateBuffer()
-}
-
-// GetSender 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 (message *Message) GetSender() string {
- switch message.State {
- case MessageStateSending:
- return "Sending..."
- case MessageStateFailed:
- return "Error"
- }
- switch message.Type {
- case "m.emote":
- // Emotes don't show a separate sender, it's included in the buffer.
- return ""
- default:
- return message.Sender
- }
-}
-
-func (message *Message) getStateSpecificColor() tcell.Color {
- switch message.State {
- case MessageStateSending:
- return tcell.ColorGray
- case MessageStateFailed:
- return tcell.ColorRed
- case MessageStateDefault:
- fallthrough
- default:
- return tcell.ColorDefault
- }
-}
-
-// GetSenderColor 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 (message *Message) GetSenderColor() (color tcell.Color) {
- color = message.getStateSpecificColor()
- if color == tcell.ColorDefault {
- color = message.SenderColor
- }
- return
-}
-
-// GetTextColor returns the color the actual content of the message should be shown in.
-//
-// This returns the same colors as GetSenderColor(), but takes the default color from a different variable.
-func (message *Message) GetTextColor() (color tcell.Color) {
- color = message.getStateSpecificColor()
- if color == tcell.ColorDefault {
- color = message.TextColor
- }
- return
-}
-
-// GetTimestampColor returns the color the timestamp should be shown in.
-//
-// As with GetSenderColor(), 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 (message *Message) GetTimestampColor() tcell.Color {
- return message.getStateSpecificColor()
-}
-
-// RecalculateBuffer calculates the buffer again with the previously provided width.
-func (message *Message) RecalculateBuffer() {
- message.CalculateBuffer(message.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 (message *Message) Buffer() []string {
- return message.buffer
-}
-
-// Height returns the number of rows in the computed buffer (see Buffer()).
-func (message *Message) Height() int {
- return len(message.buffer)
-}
-
-// GetTimestamp returns the formatted time when the message was sent.
-func (message *Message) GetTimestamp() string {
- return message.Timestamp
-}
-
-// GetDate returns the formatted date when the message was sent.
-func (message *Message) GetDate() string {
- return message.Date
-}
-
-// 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 (message *Message) CalculateBuffer(width int) {
- if width < 2 {
- return
- }
-
- message.buffer = []string{}
- text := message.Text
- if message.Type == "m.emote" {
- text = fmt.Sprintf("* %s %s", message.Sender, message.Text)
- }
-
- forcedLinebreaks := strings.Split(text, "\n")
- newlines := 0
- for _, str := range forcedLinebreaks {
- if len(str) == 0 && newlines < 1 {
- message.buffer = append(message.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]]
- }
- }
- message.buffer = append(message.buffer, extract)
- str = str[len(extract):]
- }
- }
- message.prevBufferWidth = width
-}
diff --git a/ui/types/meta.go b/ui/types/meta.go
deleted file mode 100644
index fdc6dba..0000000
--- a/ui/types/meta.go
+++ /dev/null
@@ -1,71 +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 <http://www.gnu.org/licenses/>.
-
-package types
-
-import (
- "github.com/gdamore/tcell"
-)
-
-// MessageMeta is an interface to get the metadata of a message.
-//
-// See BasicMeta for a simple implementation and documentation of methods.
-type MessageMeta interface {
- GetSender() string
- GetSenderColor() tcell.Color
- GetTextColor() tcell.Color
- GetTimestampColor() tcell.Color
- GetTimestamp() string
- GetDate() string
-}
-
-// BasicMeta is a simple variable store implementation of MessageMeta.
-type BasicMeta struct {
- Sender, Timestamp, Date string
- SenderColor, TextColor, TimestampColor tcell.Color
-}
-
-// GetSender gets the string that should be displayed as the sender of this message.
-func (meta *BasicMeta) GetSender() string {
- return meta.Sender
-}
-
-// GetSenderColor returns the color the name of the sender should be shown in.
-func (meta *BasicMeta) GetSenderColor() tcell.Color {
- return meta.SenderColor
-}
-
-// GetTimestamp returns the formatted time when the message was sent.
-func (meta *BasicMeta) GetTimestamp() string {
- return meta.Timestamp
-}
-
-// GetDate returns the formatted date when the message was sent.
-func (meta *BasicMeta) GetDate() string {
- return meta.Date
-}
-
-// GetTextColor returns the color the actual content of the message should be shown in.
-func (meta *BasicMeta) GetTextColor() tcell.Color {
- return meta.TextColor
-}
-
-// GetTimestampColor 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) GetTimestampColor() tcell.Color {
- return meta.TimestampColor
-}
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-login.go b/ui/view-login.go
index ff0e44e..8343aaa 100644
--- a/ui/view-login.go
+++ b/ui/view-login.go
@@ -19,7 +19,7 @@ package ui
import (
"maunium.net/go/gomuks/config"
"maunium.net/go/gomuks/interface"
- "maunium.net/go/gomuks/ui/debug"
+ "maunium.net/go/gomuks/debug"
"maunium.net/go/gomuks/ui/widget"
"maunium.net/go/tview"
)
diff --git a/ui/view-main.go b/ui/view-main.go
index fd05492..d4ffd39 100644
--- a/ui/view-main.go
+++ b/ui/view-main.go
@@ -23,26 +23,26 @@ import (
"time"
"unicode"
- "github.com/gdamore/tcell"
"github.com/mattn/go-runewidth"
"maunium.net/go/gomatrix"
"maunium.net/go/gomuks/config"
+ "maunium.net/go/gomuks/debug"
"maunium.net/go/gomuks/interface"
+ "maunium.net/go/gomuks/lib/notification"
"maunium.net/go/gomuks/matrix/pushrules"
"maunium.net/go/gomuks/matrix/rooms"
- "maunium.net/go/gomuks/notification"
- "maunium.net/go/gomuks/ui/debug"
- "maunium.net/go/gomuks/ui/types"
+ "maunium.net/go/gomuks/ui/messages/parser"
"maunium.net/go/gomuks/ui/widget"
+ "maunium.net/go/tcell"
"maunium.net/go/tview"
)
type MainView struct {
*tview.Flex
- roomList *widget.RoomList
+ roomList *RoomList
roomView *tview.Pages
- rooms map[string]*widget.RoomView
+ rooms map[string]*RoomView
currentRoomIndex int
roomIDs []string
@@ -57,9 +57,9 @@ type MainView struct {
func (ui *GomuksUI) NewMainView() tview.Primitive {
mainView := &MainView{
Flex: tview.NewFlex(),
- roomList: widget.NewRoomList(),
+ roomList: NewRoomList(),
roomView: tview.NewPages(),
- rooms: make(map[string]*widget.RoomView),
+ rooms: make(map[string]*RoomView),
matrix: ui.gmx.Matrix(),
gmx: ui.gmx,
@@ -81,7 +81,7 @@ func (view *MainView) BumpFocus() {
view.lastFocusTime = time.Now()
}
-func (view *MainView) InputChanged(roomView *widget.RoomView, text string) {
+func (view *MainView) InputChanged(roomView *RoomView, text string) {
if len(text) == 0 {
go view.matrix.SendTyping(roomView.Room.ID, false)
} else if text[0] != '/' {
@@ -101,7 +101,7 @@ func findWordToTabComplete(text string) string {
return output
}
-func (view *MainView) InputTabComplete(roomView *widget.RoomView, text string, cursorOffset int) string {
+func (view *MainView) InputTabComplete(roomView *RoomView, text string, cursorOffset int) string {
str := runewidth.Truncate(text, cursorOffset, "")
word := findWordToTabComplete(str)
userCompletions := roomView.AutocompleteUser(word)
@@ -118,7 +118,7 @@ func (view *MainView) InputTabComplete(roomView *widget.RoomView, text string, c
return text
}
-func (view *MainView) InputSubmit(roomView *widget.RoomView, text string) {
+func (view *MainView) InputSubmit(roomView *RoomView, text string) {
if len(text) == 0 {
return
} else if text[0] == '/' {
@@ -132,29 +132,30 @@ func (view *MainView) InputSubmit(roomView *widget.RoomView, text string) {
roomView.SetInputText("")
}
-func (view *MainView) SendMessage(roomView *widget.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 *widget.RoomView, tempMessage *types.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.State = types.MessageStateFailed
+ tempMessage.SetState(ifc.MessageStateFailed)
roomView.SetStatus(fmt.Sprintf("Failed to send message: %s", err))
} else {
roomView.MessageView().UpdateMessageID(tempMessage, eventID)
}
}
-func (view *MainView) HandleCommand(roomView *widget.RoomView, command string, args []string) {
+func (view *MainView) HandleCommand(roomView *RoomView, command string, args []string) {
defer view.gmx.Recover()
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()
@@ -163,22 +164,20 @@ func (view *MainView) HandleCommand(roomView *widget.RoomView, command string, a
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 {
- view.AddServiceMessage(roomView, "Usage: /join <room>")
+ roomView.AddServiceMessage("Usage: /join <room>")
break
}
debug.Print("Join room result:", view.matrix.JoinRoom(args[0]))
default:
- view.AddServiceMessage(roomView, "Unknown command.")
+ roomView.AddServiceMessage("Unknown command.")
}
}
-func (view *MainView) KeyEventHandler(roomView *widget.RoomView, key *tcell.EventKey) *tcell.EventKey {
+func (view *MainView) KeyEventHandler(roomView *RoomView, key *tcell.EventKey) *tcell.EventKey {
view.BumpFocus()
k := key.Key()
@@ -220,8 +219,8 @@ func (view *MainView) KeyEventHandler(roomView *widget.RoomView, key *tcell.Even
const WheelScrollOffsetDiff = 3
-func (view *MainView) MouseEventHandler(roomView *widget.RoomView, event *tcell.EventMouse) *tcell.EventMouse {
- if event.Buttons() == tcell.ButtonNone {
+func (view *MainView) MouseEventHandler(roomView *RoomView, event *tcell.EventMouse) *tcell.EventMouse {
+ if event.Buttons() == tcell.ButtonNone || event.HasMotion() {
return event
}
view.BumpFocus()
@@ -247,7 +246,14 @@ func (view *MainView) MouseEventHandler(roomView *widget.RoomView, event *tcell.
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 {
+ if msgView.HandleClick(x-mx, y-my, event.Buttons()) {
+ view.parent.Render()
+ }
+ } else {
+ debug.Print("Mouse event received:", event.Buttons(), event.Modifiers(), x, y)
+ }
return event
}
@@ -300,7 +306,7 @@ func (view *MainView) addRoom(index int, room string) {
view.roomList.Add(roomStore)
if !view.roomView.HasPage(room) {
- roomView := widget.NewRoomView(roomStore).
+ roomView := NewRoomView(roomStore).
SetInputSubmitFunc(view.InputSubmit).
SetInputChangedFunc(view.InputChanged).
SetTabCompleteFunc(view.InputTabComplete).
@@ -310,7 +316,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 {
@@ -319,7 +325,7 @@ func (view *MainView) addRoom(index int, room string) {
}
}
-func (view *MainView) GetRoom(id string) *widget.RoomView {
+func (view *MainView) GetRoom(id string) ifc.RoomView {
return view.rooms[id]
}
@@ -352,7 +358,7 @@ func (view *MainView) RemoveRoom(room string) {
} else {
removeIndex = sort.StringSlice(view.roomIDs).Search(room)
}
- view.roomList.Remove(roomView.Room)
+ view.roomList.Remove(roomView.MxRoom())
view.roomIDs = append(view.roomIDs[:removeIndex], view.roomIDs[removeIndex+1:]...)
view.roomView.RemovePage(room)
delete(view.rooms, room)
@@ -363,7 +369,7 @@ func (view *MainView) SetRooms(rooms []string) {
view.roomIDs = rooms
view.roomList.Clear()
view.roomView.Clear()
- view.rooms = make(map[string]*widget.RoomView)
+ view.rooms = make(map[string]*RoomView)
for index, room := range rooms {
view.addRoom(index, room)
}
@@ -385,14 +391,14 @@ func sendNotification(room *rooms.Room, sender, text string, critical, sound boo
notification.Send(sender, text, critical, sound)
}
-func (view *MainView) NotifyMessage(room *rooms.Room, message *types.Message, should pushrules.PushActionArrayShould) {
+func (view *MainView) NotifyMessage(room *rooms.Room, message ifc.Message, should pushrules.PushActionArrayShould) {
// Whether or not the room where the message came is the currently shown room.
isCurrent := room.ID == view.CurrentRoomID()
// Whether or not the terminal window is focused.
isFocused := view.lastFocusTime.Add(30 * time.Second).Before(time.Now())
// Whether or not the push rules say this message should be notified about.
- shouldNotify := (should.Notify || !should.NotifySpecified) && message.Sender != view.config.Session.UserID
+ shouldNotify := (should.Notify || !should.NotifySpecified) && message.Sender() != view.config.Session.UserID
if !isCurrent {
// The message is not in the current room, show new message status in room list.
@@ -406,21 +412,10 @@ func (view *MainView) NotifyMessage(room *rooms.Room, message *types.Message, sh
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)
}
- if should.Highlight {
- // Message is highlight, set color.
- message.TextColor = tcell.ColorYellow
- }
-}
-
-func (view *MainView) AddServiceMessage(roomView *widget.RoomView, text string) {
- message := roomView.NewMessage("", "*", "gomuks.service", text, time.Now())
- message.TextColor = tcell.ColorGray
- message.SenderColor = tcell.ColorGray
- roomView.AddMessage(message, widget.AppendMessage)
- view.parent.Render()
+ message.SetIsHighlight(should.Highlight)
}
func (view *MainView) LoadHistory(room string, initial bool) {
@@ -452,20 +447,15 @@ func (view *MainView) LoadHistory(room string, initial bool) {
}
history, prevBatch, err := view.matrix.GetHistory(roomView.Room.ID, batch, 50)
if err != nil {
- view.AddServiceMessage(roomView, "Failed to fetch history")
+ roomView.AddServiceMessage("Failed to fetch history")
debug.Print("Failed to fetch history for", roomView.Room.ID, err)
return
}
roomView.Room.PrevBatch = prevBatch
for _, evt := range history {
- var message *types.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, widget.PrependMessage)
+ roomView.AddMessage(message, ifc.PrependMessage)
}
}
err = roomView.SaveHistory(view.config.HistoryDir)
@@ -476,63 +466,6 @@ func (view *MainView) LoadHistory(room string, initial bool) {
view.parent.Render()
}
-func (view *MainView) ProcessMessageEvent(room *widget.RoomView, evt *gomatrix.Event) (message *types.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 *widget.RoomView, evt *gomatrix.Event) (message *types.Message) {
- sender, text := view.getMembershipEventContent(evt)
- if len(text) == 0 {
- return
- }
- message = room.NewMessage(evt.ID, sender, "m.room.member", text, unixToTime(evt.Timestamp))
- message.TextColor = tcell.ColorGreen
- return
-}
-
-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 parser.ParseEvent(view.gmx, roomView.MxRoom(), evt)
}
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/message-view.go b/ui/widget/message-view.go
deleted file mode 100644
index f0bdbad..0000000
--- a/ui/widget/message-view.go
+++ /dev/null
@@ -1,354 +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 <http://www.gnu.org/licenses/>.
-
-package widget
-
-import (
- "encoding/gob"
- "fmt"
- "math"
- "os"
- "time"
-
- "github.com/gdamore/tcell"
- "maunium.net/go/gomuks/ui/debug"
- "maunium.net/go/gomuks/ui/types"
- "maunium.net/go/tview"
-)
-
-type MessageView struct {
- *tview.Box
-
- ScrollOffset int
- MaxSenderWidth int
- DateFormat string
- TimestampFormat string
- TimestampWidth int
- LoadingMessages bool
-
- widestSender int
- prevWidth int
- prevHeight int
- prevMsgCount int
-
- messageIDs map[string]*types.Message
- messages []*types.Message
-
- textBuffer []string
- metaBuffer []types.MessageMeta
-}
-
-func NewMessageView() *MessageView {
- return &MessageView{
- Box: tview.NewBox(),
- MaxSenderWidth: 15,
- DateFormat: "January _2, 2006",
- TimestampFormat: "15:04:05",
- TimestampWidth: 8,
- ScrollOffset: 0,
-
- messages: make([]*types.Message, 0),
- messageIDs: make(map[string]*types.Message),
- textBuffer: make([]string, 0),
- metaBuffer: make([]types.MessageMeta, 0),
-
- widestSender: 5,
- prevWidth: -1,
- prevHeight: -1,
- prevMsgCount: -1,
- }
-}
-
-func (view *MessageView) NewMessage(id, sender, msgtype, text string, timestamp time.Time) *types.Message {
- return types.NewMessage(id, sender, msgtype, text,
- timestamp.Format(view.TimestampFormat),
- timestamp.Format(view.DateFormat),
- GetHashColor(sender))
-}
-
-func (view *MessageView) SaveHistory(path string) error {
- file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0600)
- if err != nil {
- return err
- }
- defer file.Close()
-
- enc := gob.NewEncoder(file)
- err = enc.Encode(view.messages)
- if err != nil {
- return err
- }
-
- return nil
-}
-
-func (view *MessageView) LoadHistory(path string) (int, error) {
- file, err := os.OpenFile(path, os.O_RDONLY, 0600)
- if err != nil {
- if os.IsNotExist(err) {
- return 0, nil
- }
- return -1, err
- }
- defer file.Close()
-
- dec := gob.NewDecoder(file)
- err = dec.Decode(&view.messages)
- if err != nil {
- return -1, err
- }
-
- for _, message := range view.messages {
- view.updateWidestSender(message.Sender)
- }
-
- return len(view.messages), nil
-}
-
-func (view *MessageView) updateWidestSender(sender string) {
- if len(sender) > view.widestSender {
- view.widestSender = len(sender)
- if view.widestSender > view.MaxSenderWidth {
- view.widestSender = view.MaxSenderWidth
- }
- }
-}
-
-type MessageDirection int
-
-const (
- AppendMessage MessageDirection = iota
- PrependMessage
- IgnoreMessage
-)
-
-func (view *MessageView) UpdateMessageID(message *types.Message, newID string) {
- delete(view.messageIDs, message.ID)
- message.ID = newID
- view.messageIDs[message.ID] = message
-}
-
-func (view *MessageView) AddMessage(message *types.Message, direction MessageDirection) {
- if message == nil {
- return
- }
-
- msg, messageExists := view.messageIDs[message.ID]
- if msg != nil && messageExists {
- message.CopyTo(msg)
- message = msg
- direction = IgnoreMessage
- }
-
- view.updateWidestSender(message.Sender)
-
- _, _, width, _ := view.GetInnerRect()
- width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap
- message.CalculateBuffer(width)
-
- if direction == AppendMessage {
- if view.ScrollOffset > 0 {
- view.ScrollOffset += message.Height()
- }
- view.messages = append(view.messages, message)
- view.appendBuffer(message)
- } else if direction == PrependMessage {
- view.messages = append([]*types.Message{message}, view.messages...)
- }
-
- view.messageIDs[message.ID] = message
-}
-
-func (view *MessageView) appendBuffer(message *types.Message) {
- if len(view.metaBuffer) > 0 {
- prevMeta := view.metaBuffer[len(view.metaBuffer)-1]
- if prevMeta != nil && prevMeta.GetDate() != message.Date {
- view.textBuffer = append(view.textBuffer, fmt.Sprintf("Date changed to %s", message.Date))
- view.metaBuffer = append(view.metaBuffer, &types.BasicMeta{TextColor: tcell.ColorGreen})
- }
- }
-
- view.textBuffer = append(view.textBuffer, message.Buffer()...)
- for range message.Buffer() {
- view.metaBuffer = append(view.metaBuffer, message)
- }
- view.prevMsgCount++
-}
-
-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.metaBuffer = []types.MessageMeta{}
- view.prevMsgCount = 0
- for _, message := range view.messages {
- if recalculateMessageBuffers {
- message.CalculateBuffer(width)
- }
- view.appendBuffer(message)
- }
- view.prevHeight = height
- view.prevWidth = width
- }
-}
-
-const PaddingAtTop = 5
-
-func (view *MessageView) AddScrollOffset(diff int) {
- _, _, _, height := view.GetInnerRect()
-
- totalHeight := len(view.textBuffer)
- if diff >= 0 && view.ScrollOffset+diff >= totalHeight-height+PaddingAtTop {
- view.ScrollOffset = totalHeight - height + PaddingAtTop
- } else {
- view.ScrollOffset += diff
- }
-
- if view.ScrollOffset > totalHeight-height+PaddingAtTop {
- view.ScrollOffset = totalHeight - height + PaddingAtTop
- }
- if view.ScrollOffset < 0 {
- view.ScrollOffset = 0
- }
-}
-
-func (view *MessageView) Height() int {
- _, _, _, height := view.GetInnerRect()
- return height
-}
-
-func (view *MessageView) TotalHeight() int {
- return len(view.textBuffer)
-}
-
-func (view *MessageView) IsAtTop() bool {
- _, _, _, height := view.GetInnerRect()
- totalHeight := len(view.textBuffer)
- return view.ScrollOffset >= totalHeight-height+PaddingAtTop
-}
-
-const (
- TimestampSenderGap = 1
- SenderSeparatorGap = 1
- SenderMessageGap = 3
-)
-
-func getScrollbarStyle(scrollbarHere, isTop, isBottom bool) (char rune, style tcell.Style) {
- char = '│'
- style = tcell.StyleDefault
- if scrollbarHere {
- style = style.Foreground(tcell.ColorGreen)
- }
- if isTop {
- if scrollbarHere {
- char = '╥'
- } else {
- char = '┬'
- }
- } else if isBottom {
- if scrollbarHere {
- char = '╨'
- } else {
- char = '┴'
- }
- } else if scrollbarHere {
- char = '║'
- }
- return
-}
-
-func (view *MessageView) Draw(screen tcell.Screen) {
- view.Box.Draw(screen)
-
- x, y, _, height := view.GetInnerRect()
- view.recalculateBuffers()
-
- if len(view.textBuffer) == 0 {
- writeLineSimple(screen, "It's quite empty in here.", x, y+height)
- return
- }
-
- usernameX := x + view.TimestampWidth + TimestampSenderGap
- messageX := usernameX + view.widestSender + SenderMessageGap
- separatorX := usernameX + view.widestSender + SenderSeparatorGap
-
- indexOffset := len(view.textBuffer) - view.ScrollOffset - height
- if indexOffset <= -PaddingAtTop {
- message := "Scroll up to load more messages."
- if view.LoadingMessages {
- message = "Loading more messages..."
- }
- writeLineSimpleColor(screen, message, messageX, y, tcell.ColorGreen)
- }
-
- if len(view.textBuffer) != len(view.metaBuffer) {
- debug.ExtPrintf("Unexpected text/meta buffer length mismatch: %d != %d.", len(view.textBuffer), len(view.metaBuffer))
- return
- }
-
- var scrollBarHeight, scrollBarPos int
- // Black magic (aka math) used to figure out where the scroll bar should be put.
- {
- viewportHeight := float64(height)
- contentHeight := float64(len(view.textBuffer))
-
- scrollBarHeight = int(math.Ceil(viewportHeight / (contentHeight / viewportHeight)))
-
- scrollBarPos = height - int(math.Round(float64(view.ScrollOffset)/contentHeight*viewportHeight))
- }
-
- var prevMeta types.MessageMeta
- firstLine := true
- skippedLines := 0
-
- for line := 0; line < height; line++ {
- index := indexOffset + line
- if index < 0 {
- skippedLines++
- continue
- } else if index >= len(view.textBuffer) {
- break
- }
-
- showScrollbar := line-skippedLines >= scrollBarPos-scrollBarHeight && line-skippedLines < scrollBarPos
- isTop := firstLine && view.ScrollOffset+height >= len(view.textBuffer)
- isBottom := line == height-1 && view.ScrollOffset == 0
-
- borderChar, borderStyle := getScrollbarStyle(showScrollbar, isTop, isBottom)
-
- firstLine = false
-
- screen.SetContent(separatorX, y+line, borderChar, nil, borderStyle)
-
- text, meta := view.textBuffer[index], view.metaBuffer[index]
- if meta != prevMeta {
- if len(meta.GetTimestamp()) > 0 {
- writeLineSimpleColor(screen, meta.GetTimestamp(), x, y+line, meta.GetTimestampColor())
- }
- if prevMeta == nil || meta.GetSender() != prevMeta.GetSender() {
- writeLineColor(
- screen, tview.AlignRight, meta.GetSender(),
- usernameX, y+line, view.widestSender,
- meta.GetSenderColor())
- }
- prevMeta = meta
- }
- writeLineSimpleColor(screen, text, messageX, y+line, meta.GetTextColor())
- }
-}
diff --git a/ui/widget/util.go b/ui/widget/util.go
index 0888210..920afad 100644
--- a/ui/widget/util.go
+++ b/ui/widget/util.go
@@ -17,24 +17,24 @@
package widget
import (
- "github.com/gdamore/tcell"
+ "maunium.net/go/tcell"
"github.com/mattn/go-runewidth"
"maunium.net/go/tview"
)
-func writeLineSimple(screen tcell.Screen, line string, x, y int) {
- writeLine(screen, tview.AlignLeft, line, x, y, 1<<30, tcell.StyleDefault)
+func WriteLineSimple(screen tcell.Screen, line string, x, y int) {
+ WriteLine(screen, tview.AlignLeft, line, x, y, 1<<30, tcell.StyleDefault)
}
-func writeLineSimpleColor(screen tcell.Screen, line string, x, y int, color tcell.Color) {
- writeLine(screen, tview.AlignLeft, line, x, y, 1<<30, tcell.StyleDefault.Foreground(color))
+func WriteLineSimpleColor(screen tcell.Screen, line string, x, y int, color tcell.Color) {
+ WriteLine(screen, tview.AlignLeft, line, x, y, 1<<30, tcell.StyleDefault.Foreground(color))
}
-func writeLineColor(screen tcell.Screen, align int, line string, x, y, maxWidth int, color tcell.Color) {
- writeLine(screen, align, line, x, y, maxWidth, tcell.StyleDefault.Foreground(color))
+func WriteLineColor(screen tcell.Screen, align int, line string, x, y, maxWidth int, color tcell.Color) {
+ WriteLine(screen, align, line, x, y, maxWidth, tcell.StyleDefault.Foreground(color))
}
-func writeLine(screen tcell.Screen, align int, line string, x, y, maxWidth int, style tcell.Style) {
+func WriteLine(screen tcell.Screen, align int, line string, x, y, maxWidth int, style tcell.Style) {
offsetX := 0
if align == tview.AlignRight {
offsetX = maxWidth - runewidth.StringWidth(line)