aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTulir Asokan <tulir@maunium.net>2018-04-09 23:45:54 +0300
committerTulir Asokan <tulir@maunium.net>2018-04-09 23:45:54 +0300
commiteda2b575f06e72040ebf82d24a7ec1ac84b7948c (patch)
treefe02378ebd00443cb675675ddade335ceab25cd1
parent2ba2fde3966211845b1117c85b27e3c947b6307f (diff)
Refactor UI to use interfaces everywhere
-rw-r--r--config/config.go2
-rw-r--r--config/session.go2
-rw-r--r--debug/debug.go (renamed from ui/debug/debug.go)99
-rw-r--r--debug/doc.go2
-rw-r--r--gomuks.go37
-rw-r--r--interface/ui.go79
-rw-r--r--matrix/matrix.go32
-rw-r--r--ui/debug/doc.go2
-rw-r--r--ui/message-view.go (renamed from ui/widget/message-view.go)108
-rw-r--r--ui/messages/doc.go2
-rw-r--r--ui/messages/message.go (renamed from ui/debug/external.go)34
-rw-r--r--ui/messages/meta.go70
-rw-r--r--ui/messages/textmessage.go295
-rw-r--r--ui/room-list.go (renamed from ui/widget/room-list.go)7
-rw-r--r--ui/room-view.go (renamed from ui/widget/room-view.go)46
-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/view-login.go2
-rw-r--r--ui/view-main.go78
-rw-r--r--ui/widget/util.go14
21 files changed, 630 insertions, 588 deletions
diff --git a/config/config.go b/config/config.go
index 4ad6793..9179a58 100644
--- a/config/config.go
+++ b/config/config.go
@@ -23,7 +23,7 @@ import (
"path/filepath"
"gopkg.in/yaml.v2"
- "maunium.net/go/gomuks/ui/debug"
+ "maunium.net/go/gomuks/debug"
)
// Config contains the main config of gomuks.
diff --git a/config/session.go b/config/session.go
index 337a14b..d12bc20 100644
--- a/config/session.go
+++ b/config/session.go
@@ -24,7 +24,7 @@ import (
"maunium.net/go/gomatrix"
"maunium.net/go/gomuks/matrix/pushrules"
"maunium.net/go/gomuks/matrix/rooms"
- "maunium.net/go/gomuks/ui/debug"
+ "maunium.net/go/gomuks/debug"
)
type Session struct {
diff --git a/ui/debug/debug.go b/debug/debug.go
index 3f47980..9af3d9d 100644
--- a/ui/debug/debug.go
+++ b/debug/debug.go
@@ -18,104 +18,43 @@ package debug
import (
"fmt"
+ "io"
"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")
+var writer io.Writer
- return &Pane{
- TextView: pane,
- Height: 35,
- Width: 80,
- num: 0,
+func init() {
+ var err error
+ writer, err = os.OpenFile("/tmp/gomuks-debug.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
+ if err != nil {
+ writer = nil
}
}
-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
+func Printf(text string, args ...interface{}) {
+ if writer != nil {
+ fmt.Fprintf(writer, time.Now().Format("[2006-01-02 15:04:05] "))
+ fmt.Fprintf(writer, text+"\n", args...)
}
- 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 writer != nil {
+ fmt.Fprintf(writer, time.Now().Format("[2006-01-02 15:04:05] "))
+ fmt.Fprintln(writer, text...)
}
}
-func Print(text ...interface{}) {
- if RedirectAllExt {
- ExtPrint(text...)
- } else if Default != nil {
- Default.Print(text...)
+func PrintStack() {
+ if writer != nil {
+ data := debug.Stack()
+ writer.Write(data)
}
}
diff --git a/debug/doc.go b/debug/doc.go
new file mode 100644
index 0000000..253441c
--- /dev/null
+++ b/debug/doc.go
@@ -0,0 +1,2 @@
+// Package debug contains utilities to log debug messages and display panics nicely.
+package debug
diff --git a/gomuks.go b/gomuks.go
index 60a4d2b..5edae3a 100644
--- a/gomuks.go
+++ b/gomuks.go
@@ -23,10 +23,10 @@ import (
"time"
"maunium.net/go/gomuks/config"
+ "maunium.net/go/gomuks/debug"
"maunium.net/go/gomuks/interface"
"maunium.net/go/gomuks/matrix"
"maunium.net/go/gomuks/ui"
- "maunium.net/go/gomuks/ui/debug"
"maunium.net/go/tview"
)
@@ -35,7 +35,6 @@ type Gomuks struct {
app *tview.Application
ui *ui.GomuksUI
matrix *matrix.Container
- debug *debug.Pane
debugMode bool
config *config.Config
stop chan bool
@@ -43,19 +42,14 @@ type Gomuks struct {
// NewGomuks creates a new Gomuks instance with everything initialized,
// but does not start it.
-func NewGomuks(enableDebug, forceExternalDebug bool) *Gomuks {
+func NewGomuks(enableDebug bool) *Gomuks {
configDir := filepath.Join(os.Getenv("HOME"), ".config/gomuks")
gmx := &Gomuks{
- app: tview.NewApplication(),
- stop: make(chan bool, 1),
+ app: tview.NewApplication(),
+ stop: make(chan bool, 1),
+ debugMode: enableDebug,
}
- gmx.debug = debug.NewPane()
- gmx.debug.SetChangedFunc(func() {
- gmx.ui.Render()
- })
- debug.Default = gmx.debug
-
gmx.config = config.NewConfig(configDir)
gmx.ui = ui.NewGomuksUI(gmx)
gmx.matrix = matrix.NewContainer(gmx)
@@ -68,15 +62,6 @@ func NewGomuks(enableDebug, forceExternalDebug bool) *Gomuks {
_ = gmx.matrix.InitClient()
main := gmx.ui.InitViews()
- if enableDebug {
- debug.EnableExternal()
- if forceExternalDebug {
- debug.RedirectAllExt = true
- } else {
- main = gmx.debug.Wrap(main, debug.Right)
- }
- gmx.debugMode = true
- }
gmx.app.SetRoot(main, true)
return gmx
@@ -85,10 +70,10 @@ func NewGomuks(enableDebug, forceExternalDebug bool) *Gomuks {
// Save saves the active session and message history.
func (gmx *Gomuks) Save() {
if gmx.config.Session != nil {
- gmx.debug.Print("Saving session...")
+ debug.Print("Saving session...")
_ = gmx.config.Session.Save()
}
- gmx.debug.Print("Saving history...")
+ debug.Print("Saving history...")
gmx.ui.MainView().SaveAllHistory()
}
@@ -112,9 +97,9 @@ func (gmx *Gomuks) StartAutosave() {
// Stop stops the Matrix syncer, the tview app and the autosave goroutine,
// then saves everything and calls os.Exit(0).
func (gmx *Gomuks) Stop() {
- gmx.debug.Print("Disconnecting from Matrix...")
+ debug.Print("Disconnecting from Matrix...")
gmx.matrix.Stop()
- gmx.debug.Print("Cleaning up UI...")
+ debug.Print("Cleaning up UI...")
gmx.app.Stop()
gmx.stop <- true
gmx.Save()
@@ -170,8 +155,8 @@ func (gmx *Gomuks) UI() ifc.GomuksUI {
}
func main() {
- debugVar := os.Getenv("DEBUG")
- NewGomuks(len(debugVar) > 0, debugVar == "ext").Start()
+ enableDebug := len(os.Getenv("DEBUG")) > 0
+ NewGomuks(enableDebug).Start()
// We use os.Exit() everywhere, so exiting by returning from Start() shouldn't happen.
time.Sleep(5 * time.Second)
diff --git a/interface/ui.go b/interface/ui.go
index c0ddf53..0ddbde7 100644
--- a/interface/ui.go
+++ b/interface/ui.go
@@ -17,11 +17,12 @@
package ifc
import (
+ "time"
+
+ "github.com/gdamore/tcell"
"maunium.net/go/gomatrix"
"maunium.net/go/gomuks/matrix/pushrules"
"maunium.net/go/gomuks/matrix/rooms"
- "maunium.net/go/gomuks/ui/types"
- "maunium.net/go/gomuks/ui/widget"
"maunium.net/go/tview"
)
@@ -42,7 +43,7 @@ type GomuksUI interface {
}
type MainView interface {
- GetRoom(roomID string) *widget.RoomView
+ GetRoom(roomID string) RoomView
HasRoom(roomID string) bool
AddRoom(roomID string)
RemoveRoom(roomID string)
@@ -50,11 +51,75 @@ type MainView interface {
SaveAllHistory()
SetTyping(roomID string, users []string)
- AddServiceMessage(roomID *widget.RoomView, message string)
- ProcessMessageEvent(roomView *widget.RoomView, evt *gomatrix.Event) *types.Message
- ProcessMembershipEvent(roomView *widget.RoomView, evt *gomatrix.Event) *types.Message
- NotifyMessage(room *rooms.Room, message *types.Message, should pushrules.PushActionArrayShould)
+ ProcessMessageEvent(roomView RoomView, evt *gomatrix.Event) Message
+ ProcessMembershipEvent(roomView RoomView, evt *gomatrix.Event) Message
+ NotifyMessage(room *rooms.Room, message Message, should pushrules.PushActionArrayShould)
}
type LoginView interface {
}
+
+type MessageDirection int
+
+const (
+ AppendMessage MessageDirection = iota
+ PrependMessage
+ IgnoreMessage
+)
+
+type RoomView interface {
+ MxRoom() *rooms.Room
+ SaveHistory(dir string) error
+ LoadHistory(dir string) (int, error)
+
+ SetStatus(status string)
+ SetTyping(users []string)
+ UpdateUserList()
+
+ NewMessage(id, sender, msgtype, text string, timestamp time.Time) Message
+ NewTempMessage(msgtype, text string) Message
+ AddMessage(message Message, direction MessageDirection)
+ AddServiceMessage(message string)
+}
+
+type MessageMeta interface {
+ Sender() string
+ SenderColor() tcell.Color
+ TextColor() tcell.Color
+ TimestampColor() tcell.Color
+ Timestamp() string
+ Date() string
+ CopyFrom(from MessageMeta)
+}
+
+// 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
+)
+
+type Message interface {
+ MessageMeta
+
+ SetIsHighlight(isHighlight bool)
+ IsHighlight() bool
+
+ SetIsService(isService bool)
+ IsService() bool
+
+ SetID(id string)
+ ID() string
+
+ SetType(msgtype string)
+ Type() string
+
+ SetText(text string)
+ Text() string
+
+ SetState(state MessageState)
+ State() MessageState
+}
diff --git a/matrix/matrix.go b/matrix/matrix.go
index 7391ca0..6fff6d8 100644
--- a/matrix/matrix.go
+++ b/matrix/matrix.go
@@ -22,13 +22,14 @@ import (
"strings"
"time"
- "maunium.net/go/gomatrix"
- "maunium.net/go/gomuks/config"
- "maunium.net/go/gomuks/interface"
- "maunium.net/go/gomuks/matrix/pushrules"
- "maunium.net/go/gomuks/matrix/rooms"
- "maunium.net/go/gomuks/ui/debug"
- "maunium.net/go/gomuks/ui/widget"
+
+"maunium.net/go/gomatrix"
+"maunium.net/go/gomuks/config"
+"maunium.net/go/gomuks/debug"
+"maunium.net/go/gomuks/interface"
+"maunium.net/go/gomuks/matrix/pushrules"
+"maunium.net/go/gomuks/matrix/rooms"
+
)
// Container is a wrapper for a gomatrix Client and some other stuff.
@@ -224,10 +225,10 @@ func (c *Container) HandleMessage(evt *gomatrix.Event) {
message := mainView.ProcessMessageEvent(roomView, evt)
if message != nil {
if c.syncer.FirstSyncDone {
- pushRules := c.PushRules().GetActions(roomView.Room, evt).Should()
- mainView.NotifyMessage(roomView.Room, message, pushRules)
+ pushRules := c.PushRules().GetActions(roomView.MxRoom(), evt).Should()
+ mainView.NotifyMessage(roomView.MxRoom(), message, pushRules)
}
- roomView.AddMessage(message, widget.AppendMessage)
+ roomView.AddMessage(message, ifc.AppendMessage)
c.ui.Render()
}
}
@@ -255,8 +256,7 @@ func (c *Container) processOwnMembershipChange(evt *gomatrix.Event) {
if evt.Unsigned.PrevContent != nil {
prevMembership, _ = evt.Unsigned.PrevContent["membership"].(string)
}
- const Hour = 1 * 60 * 60 * 1000
- if membership == prevMembership || evt.Unsigned.Age > Hour {
+ if membership == prevMembership {
return
}
switch membership {
@@ -282,15 +282,15 @@ func (c *Container) HandleMembership(evt *gomatrix.Event) {
message := mainView.ProcessMembershipEvent(roomView, evt)
if message != nil {
// TODO this shouldn't be necessary
- roomView.Room.UpdateState(evt)
+ roomView.MxRoom().UpdateState(evt)
// TODO This should probably also be in a different place
roomView.UpdateUserList()
if c.syncer.FirstSyncDone {
- pushRules := c.PushRules().GetActions(roomView.Room, evt).Should()
- mainView.NotifyMessage(roomView.Room, message, pushRules)
+ pushRules := c.PushRules().GetActions(roomView.MxRoom(), evt).Should()
+ mainView.NotifyMessage(roomView.MxRoom(), message, pushRules)
}
- roomView.AddMessage(message, widget.AppendMessage)
+ roomView.AddMessage(message, ifc.AppendMessage)
c.ui.Render()
}
}
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/widget/message-view.go b/ui/message-view.go
index f0bdbad..f9d477b 100644
--- a/ui/widget/message-view.go
+++ b/ui/message-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 (
"encoding/gob"
@@ -24,8 +24,10 @@ import (
"time"
"github.com/gdamore/tcell"
- "maunium.net/go/gomuks/ui/debug"
- "maunium.net/go/gomuks/ui/types"
+ "maunium.net/go/gomuks/debug"
+ "maunium.net/go/gomuks/interface"
+ "maunium.net/go/gomuks/ui/messages"
+ "maunium.net/go/gomuks/ui/widget"
"maunium.net/go/tview"
)
@@ -44,11 +46,11 @@ type MessageView struct {
prevHeight int
prevMsgCount int
- messageIDs map[string]*types.Message
- messages []*types.Message
+ messageIDs map[string]messages.UIMessage
+ messages []messages.UIMessage
textBuffer []string
- metaBuffer []types.MessageMeta
+ metaBuffer []ifc.MessageMeta
}
func NewMessageView() *MessageView {
@@ -60,10 +62,10 @@ func NewMessageView() *MessageView {
TimestampWidth: 8,
ScrollOffset: 0,
- messages: make([]*types.Message, 0),
- messageIDs: make(map[string]*types.Message),
+ messages: make([]messages.UIMessage, 0),
+ messageIDs: make(map[string]messages.UIMessage),
textBuffer: make([]string, 0),
- metaBuffer: make([]types.MessageMeta, 0),
+ metaBuffer: make([]ifc.MessageMeta, 0),
widestSender: 5,
prevWidth: -1,
@@ -72,11 +74,11 @@ func NewMessageView() *MessageView {
}
}
-func (view *MessageView) NewMessage(id, sender, msgtype, text string, timestamp time.Time) *types.Message {
- return types.NewMessage(id, sender, msgtype, text,
+func (view *MessageView) NewMessage(id, sender, msgtype, text string, timestamp time.Time) messages.UIMessage {
+ return messages.NewMessage(id, sender, msgtype, text,
timestamp.Format(view.TimestampFormat),
timestamp.Format(view.DateFormat),
- GetHashColor(sender))
+ widget.GetHashColor(sender))
}
func (view *MessageView) SaveHistory(path string) error {
@@ -112,7 +114,7 @@ func (view *MessageView) LoadHistory(path string) (int, error) {
}
for _, message := range view.messages {
- view.updateWidestSender(message.Sender)
+ view.updateWidestSender(message.Sender())
}
return len(view.messages), nil
@@ -127,57 +129,61 @@ func (view *MessageView) updateWidestSender(sender string) {
}
}
-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) 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(message *types.Message, direction MessageDirection) {
- if message == nil {
+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
}
- msg, messageExists := view.messageIDs[message.ID]
+ msg, messageExists := view.messageIDs[message.ID()]
if msg != nil && messageExists {
- message.CopyTo(msg)
+ msg.CopyFrom(message)
message = msg
- direction = IgnoreMessage
+ direction = ifc.IgnoreMessage
}
- view.updateWidestSender(message.Sender)
+ view.updateWidestSender(message.Sender())
_, _, width, _ := view.GetInnerRect()
width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap
message.CalculateBuffer(width)
- if direction == AppendMessage {
+ if direction == ifc.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...)
+ } else if direction == ifc.PrependMessage {
+ view.messages = append([]messages.UIMessage{message}, view.messages...)
}
- view.messageIDs[message.ID] = message
+ view.messageIDs[message.ID()] = message
}
-func (view *MessageView) appendBuffer(message *types.Message) {
+func (view *MessageView) appendBuffer(message messages.UIMessage) {
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})
+ if prevMeta != nil && prevMeta.Date() != message.Date() {
+ view.textBuffer = append(view.textBuffer, fmt.Sprintf("Date changed to %s", message.Date()))
+ view.metaBuffer = append(view.metaBuffer, &messages.BasicMeta{BTextColor: tcell.ColorGreen})
}
}
@@ -195,7 +201,7 @@ func (view *MessageView) recalculateBuffers() {
recalculateMessageBuffers := width != view.prevWidth
if height != view.prevHeight || recalculateMessageBuffers || len(view.messages) != view.prevMsgCount {
view.textBuffer = []string{}
- view.metaBuffer = []types.MessageMeta{}
+ view.metaBuffer = []ifc.MessageMeta{}
view.prevMsgCount = 0
for _, message := range view.messages {
if recalculateMessageBuffers {
@@ -280,7 +286,7 @@ func (view *MessageView) Draw(screen tcell.Screen) {
view.recalculateBuffers()
if len(view.textBuffer) == 0 {
- writeLineSimple(screen, "It's quite empty in here.", x, y+height)
+ widget.WriteLineSimple(screen, "It's quite empty in here.", x, y+height)
return
}
@@ -294,11 +300,11 @@ func (view *MessageView) Draw(screen tcell.Screen) {
if view.LoadingMessages {
message = "Loading more messages..."
}
- writeLineSimpleColor(screen, message, messageX, y, tcell.ColorGreen)
+ widget.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))
+ debug.Printf("Unexpected text/meta buffer length mismatch: %d != %d.", len(view.textBuffer), len(view.metaBuffer))
return
}
@@ -313,7 +319,7 @@ func (view *MessageView) Draw(screen tcell.Screen) {
scrollBarPos = height - int(math.Round(float64(view.ScrollOffset)/contentHeight*viewportHeight))
}
- var prevMeta types.MessageMeta
+ var prevMeta ifc.MessageMeta
firstLine := true
skippedLines := 0
@@ -338,17 +344,17 @@ func (view *MessageView) Draw(screen tcell.Screen) {
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 len(meta.Timestamp()) > 0 {
+ widget.WriteLineSimpleColor(screen, meta.Timestamp(), x, y+line, meta.TimestampColor())
}
- if prevMeta == nil || meta.GetSender() != prevMeta.GetSender() {
- writeLineColor(
- screen, tview.AlignRight, meta.GetSender(),
+ if prevMeta == nil || meta.Sender() != prevMeta.Sender() {
+ widget.WriteLineColor(
+ screen, tview.AlignRight, meta.Sender(),
usernameX, y+line, view.widestSender,
- meta.GetSenderColor())
+ meta.SenderColor())
}
prevMeta = meta
}
- writeLineSimpleColor(screen, text, messageX, y+line, meta.GetTextColor())
+ widget.WriteLineSimpleColor(screen, text, messageX, y+line, meta.TextColor())
}
}
diff --git a/ui/messages/doc.go b/ui/messages/doc.go
new file mode 100644
index 0000000..7c3c077
--- /dev/null
+++ b/ui/messages/doc.go
@@ -0,0 +1,2 @@
+// Package types contains common type definitions used by the UI.
+package messages
diff --git a/ui/debug/external.go b/ui/messages/message.go
index faabbcc..ef0966c 100644
--- a/ui/debug/external.go
+++ b/ui/messages/message.go
@@ -14,32 +14,16 @@
// 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"
-)
+import "maunium.net/go/gomuks/interface"
-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
- }
-}
-
-func ExtPrintf(text string, args ...interface{}) {
- if writer != nil {
- fmt.Fprintf(writer, text+"\n", args...)
- }
-}
-
-func ExtPrint(text ...interface{}) {
- if writer != nil {
- fmt.Fprintln(writer, text...)
- }
+ CalculateBuffer(width int)
+ RecalculateBuffer()
+ Buffer() []string
+ Height() int
}
diff --git a/ui/messages/meta.go b/ui/messages/meta.go
new file mode 100644
index 0000000..8cb6c1b
--- /dev/null
+++ b/ui/messages/meta.go
@@ -0,0 +1,70 @@
+// gomuks - A terminal Matrix client written in Go.
+// Copyright (C) 2018 Tulir Asokan
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package messages
+
+import (
+ "github.com/gdamore/tcell"
+ "maunium.net/go/gomuks/interface"
+)
+
+// BasicMeta is a simple variable store implementation of MessageMeta.
+type BasicMeta struct {
+ BSender, BTimestamp, BDate string
+ BSenderColor, BTextColor, BTimestampColor tcell.Color
+}
+
+// Sender gets the string that should be displayed as the sender of this message.
+func (meta *BasicMeta) Sender() string {
+ return meta.BSender
+}
+
+// SenderColor returns the color the name of the sender should be shown in.
+func (meta *BasicMeta) SenderColor() tcell.Color {
+ return meta.BSenderColor
+}
+
+// Timestamp returns the formatted time when the message was sent.
+func (meta *BasicMeta) Timestamp() string {
+ return meta.BTimestamp
+}
+
+// Date returns the formatted date when the message was sent.
+func (meta *BasicMeta) Date() string {
+ return meta.BDate
+}
+
+// TextColor returns the color the actual content of the message should be shown in.
+func (meta *BasicMeta) TextColor() tcell.Color {
+ return meta.BTextColor
+}
+
+// TimestampColor returns the color the timestamp should be shown in.
+//
+// This usually does not apply to the date, as it is rendered separately from the message.
+func (meta *BasicMeta) TimestampColor() tcell.Color {
+ return meta.BTimestampColor
+}
+
+// CopyFrom replaces the content of this meta object with the content of the given object.
+func (meta *BasicMeta) CopyFrom(from ifc.MessageMeta) {
+ meta.BSender = from.Sender()
+ meta.BTimestamp = from.Timestamp()
+ meta.BDate = from.Date()
+ meta.BSenderColor = from.SenderColor()
+ meta.BTextColor = from.TextColor()
+ meta.BTimestampColor = from.TimestampColor()
+}
diff --git a/ui/messages/textmessage.go b/ui/messages/textmessage.go
new file mode 100644
index 0000000..1f37418
--- /dev/null
+++ b/ui/messages/textmessage.go
@@ -0,0 +1,295 @@
+// gomuks - A terminal Matrix client written in Go.
+// Copyright (C) 2018 Tulir Asokan
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package messages
+
+import (
+ "encoding/gob"
+ "fmt"
+ "regexp"
+ "strings"
+
+ "github.com/gdamore/tcell"
+ "github.com/mattn/go-runewidth"
+ "maunium.net/go/gomuks/interface"
+)
+
+func init() {
+ gob.Register(&UITextMessage{})
+}
+
+type UITextMessage struct {
+ MsgID string
+ MsgType string
+ MsgSender string
+ MsgSenderColor tcell.Color
+ MsgTimestamp string
+ MsgDate string
+ MsgText string
+ MsgState ifc.MessageState
+ MsgIsHighlight bool
+ MsgIsService bool
+ buffer []string
+ prevBufferWidth int
+}
+
+// NewMessage creates a new Message object with the provided values and the default state.
+func NewMessage(id, sender, msgtype, text, timestamp, date string, senderColor tcell.Color) UIMessage {
+ return &UITextMessage{
+ MsgSender: sender,
+ MsgTimestamp: timestamp,
+ MsgDate: date,
+ MsgSenderColor: senderColor,
+ MsgType: msgtype,
+ MsgText: text,
+ MsgID: id,
+ prevBufferWidth: 0,
+ MsgState: ifc.MessageStateDefault,
+ MsgIsHighlight: false,
+ MsgIsService: false,
+ }
+}
+
+// CopyFrom replaces the content of this message object with the content of the given object.
+func (msg *UITextMessage) CopyFrom(from ifc.MessageMeta) {
+ msg.MsgSender = from.Sender()
+ msg.MsgTimestamp = from.Timestamp()
+ msg.MsgDate = from.Date()
+ msg.MsgSenderColor = from.SenderColor()
+
+ fromMsg, ok := from.(UIMessage)
+ if ok {
+ msg.MsgID = fromMsg.ID()
+ msg.MsgType = fromMsg.Type()
+ msg.MsgText = fromMsg.Text()
+ msg.MsgState = fromMsg.State()
+ msg.MsgIsService = fromMsg.IsService()
+ msg.MsgIsHighlight = fromMsg.IsHighlight()
+
+ msg.RecalculateBuffer()
+ }
+}
+
+// Sender gets the string that should be displayed as the sender of this message.
+//
+// If the message is being sent, the sender is "Sending...".
+// If sending has failed, the sender is "Error".
+// If the message is an emote, the sender is blank.
+// In any other case, the sender is the display name of the user who sent the message.
+func (msg *UITextMessage) Sender() string {
+ switch msg.MsgState {
+ case ifc.MessageStateSending:
+ return "Sending..."
+ case ifc.MessageStateFailed:
+ return "Error"
+ }
+ switch msg.MsgType {
+ case "m.emote":
+ // Emotes don't show a separate sender, it's included in the buffer.
+ return ""
+ default:
+ return msg.MsgSender
+ }
+}
+
+func (msg *UITextMessage) getStateSpecificColor() tcell.Color {
+ switch msg.MsgState {
+ case ifc.MessageStateSending:
+ return tcell.ColorGray
+ case ifc.MessageStateFailed:
+ return tcell.ColorRed
+ case ifc.MessageStateDefault:
+ fallthrough
+ default:
+ return tcell.ColorDefault
+ }
+}
+
+// SenderColor returns the color the name of the sender should be shown in.
+//
+// If the message is being sent, the color is gray.
+// If sending has failed, the color is red.
+//
+// In any other case, the color is whatever is specified in the Message struct.
+// Usually that means it is the hash-based color of the sender (see ui/widget/color.go)
+func (msg *UITextMessage) SenderColor() tcell.Color {
+ stateColor := msg.getStateSpecificColor()
+ switch {
+ case stateColor != tcell.ColorDefault:
+ return stateColor
+ case msg.MsgIsService:
+ return tcell.ColorGray
+ default:
+ return msg.MsgSenderColor
+ }
+}
+
+// TextColor returns the color the actual content of the message should be shown in.
+func (msg *UITextMessage) TextColor() tcell.Color {
+ stateColor := msg.getStateSpecificColor()
+ switch {
+ case stateColor != tcell.ColorDefault:
+ return stateColor
+ case msg.MsgIsService:
+ return tcell.ColorGray
+ case msg.MsgIsHighlight:
+ return tcell.ColorYellow
+ case msg.MsgType == "m.room.member":
+ return tcell.ColorGreen
+ default:
+ return tcell.ColorDefault
+ }
+}
+
+// TimestampColor returns the color the timestamp should be shown in.
+//
+// As with SenderColor(), messages being sent and messages that failed to be sent are
+// gray and red respectively.
+//
+// However, other messages are the default color instead of a color stored in the struct.
+func (msg *UITextMessage) TimestampColor() tcell.Color {
+ return msg.getStateSpecificColor()
+}
+
+// RecalculateBuffer calculates the buffer again with the previously provided width.
+func (msg *UITextMessage) RecalculateBuffer() {
+ msg.CalculateBuffer(msg.prevBufferWidth)
+}
+
+// Buffer returns the computed text buffer.
+//
+// The buffer contains the text of the message split into lines with a maximum
+// width of whatever was provided to CalculateBuffer().
+//
+// N.B. This will NOT automatically calculate the buffer if it hasn't been
+// calculated already, as that requires the target width.
+func (msg *UITextMessage) Buffer() []string {
+ return msg.buffer
+}
+
+// Height returns the number of rows in the computed buffer (see Buffer()).
+func (msg *UITextMessage) Height() int {
+ return len(msg.buffer)
+}
+
+// Timestamp returns the formatted time when the message was sent.
+func (msg *UITextMessage) Timestamp() string {
+ return msg.MsgTimestamp
+}
+
+// Date returns the formatted date when the message was sent.
+func (msg *UITextMessage) Date() string {
+ return msg.MsgDate
+}
+
+func (msg *UITextMessage) ID() string {
+ return msg.MsgID
+}
+
+func (msg *UITextMessage) SetID(id string) {
+ msg.MsgID = id
+}
+
+func (msg *UITextMessage) Type() string {
+ return msg.MsgType
+}
+
+func (msg *UITextMessage) SetType(msgtype string) {
+ msg.MsgType = msgtype
+}
+
+func (msg *UITextMessage) Text() string {
+ return msg.MsgText
+}
+
+func (msg *UITextMessage) SetText(text string) {
+ msg.MsgText = text
+}
+
+func (msg *UITextMessage) State() ifc.MessageState {
+ return msg.MsgState
+}
+
+func (msg *UITextMessage) SetState(state ifc.MessageState) {
+ msg.MsgState = state
+}
+
+func (msg *UITextMessage) IsHighlight() bool {
+ return msg.MsgIsHighlight
+}
+
+func (msg *UITextMessage) SetIsHighlight(isHighlight bool) {
+ msg.MsgIsHighlight = isHighlight
+}
+
+func (msg *UITextMessage) IsService() bool {
+ return msg.MsgIsService
+}
+
+func (msg *UITextMessage) SetIsService(isService bool) {
+ msg.MsgIsService = isService
+}
+
+// Regular expressions used to split lines when calculating the buffer.
+//
+// From tview/textview.go
+var (
+ boundaryPattern = regexp.MustCompile("([[:punct:]]\\s*|\\s+)")
+ spacePattern = regexp.MustCompile(`\s+`)
+)
+
+// CalculateBuffer generates the internal buffer for this message that consists
+// of the text of this message split into lines at most as wide as the width
+// parameter.
+func (msg *UITextMessage) CalculateBuffer(width int) {
+ if width < 2 {
+ return
+ }
+
+ msg.buffer = []string{}
+ text := msg.MsgText
+ if msg.MsgType == "m.emote" {
+ text = fmt.Sprintf("* %s %s", msg.MsgSender, msg.MsgText)
+ }
+
+ forcedLinebreaks := strings.Split(text, "\n")
+ newlines := 0
+ for _, str := range forcedLinebreaks {
+ if len(str) == 0 && newlines < 1 {
+ msg.buffer = append(msg.buffer, "")
+ newlines++
+ } else {
+ newlines = 0
+ }
+ // From tview/textview.go#reindexBuffer()
+ for len(str) > 0 {
+ extract := runewidth.Truncate(str, width, "")
+ if len(extract) < len(str) {
+ if spaces := spacePattern.FindStringIndex(str[len(extract):]); spaces != nil && spaces[0] == 0 {
+ extract = str[:len(extract)+spaces[1]]
+ }
+
+ matches := boundaryPattern.FindAllStringIndex(extract, -1)
+ if len(matches) > 0 {
+ extract = extract[:matches[len(matches)-1][1]]
+ }
+ }
+ msg.buffer = append(msg.buffer, extract)
+ str = str[len(extract):]
+ }
+ }
+ msg.prevBufferWidth = width
+}
diff --git a/ui/widget/room-list.go b/ui/room-list.go
index d2fb543..2ff3ed7 100644
--- a/ui/widget/room-list.go
+++ b/ui/room-list.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"
@@ -22,6 +22,7 @@ import (
"github.com/gdamore/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..24325b4 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"
@@ -24,8 +24,10 @@ import (
"time"
"github.com/gdamore/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
}
@@ -48,8 +50,8 @@ func NewRoomView(room *rooms.Room) *RoomView {
content: NewMessageView(),
status: tview.NewTextView(),
userList: tview.NewTextView(),
- ulBorder: NewBorder(),
- input: NewAdvancedInputField(),
+ ulBorder: widget.NewBorder(),
+ input: widget.NewAdvancedInputField(),
Room: room,
}
@@ -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,7 +255,7 @@ 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
@@ -257,19 +263,29 @@ func (view *RoomView) NewMessage(id, sender, msgtype, text string, timestamp tim
return view.content.NewMessage(id, sender, msgtype, text, timestamp)
}
-func (view *RoomView) NewTempMessage(msgtype, text string) *types.Message {
+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) 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/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..b3b5b82 100644
--- a/ui/view-main.go
+++ b/ui/view-main.go
@@ -27,12 +27,11 @@ import (
"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/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/widget"
"maunium.net/go/tview"
)
@@ -40,9 +39,9 @@ import (
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 +56,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 +80,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 +100,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 +117,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,23 +131,23 @@ 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)
}
-func (view *MainView) sendTempMessage(roomView *widget.RoomView, tempMessage *types.Message) {
+func (view *MainView) sendTempMessage(roomView *RoomView, tempMessage ifc.Message) {
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(), tempMessage.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 {
@@ -169,16 +168,16 @@ func (view *MainView) HandleCommand(roomView *widget.RoomView, command string, a
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,7 +219,7 @@ 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 {
+func (view *MainView) MouseEventHandler(roomView *RoomView, event *tcell.EventMouse) *tcell.EventMouse {
if event.Buttons() == tcell.ButtonNone {
return event
}
@@ -300,7 +299,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).
@@ -319,7 +318,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 +351,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 +362,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 +384,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 +405,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.Text(), 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 +440,20 @@ 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
+ var message ifc.Message
if evt.Type == "m.room.message" {
message = view.ProcessMessageEvent(roomView, &evt)
} else if evt.Type == "m.room.member" {
message = view.ProcessMembershipEvent(roomView, &evt)
}
if message != nil {
- roomView.AddMessage(message, widget.PrependMessage)
+ roomView.AddMessage(message, ifc.PrependMessage)
}
}
err = roomView.SaveHistory(view.config.HistoryDir)
@@ -476,7 +464,7 @@ func (view *MainView) LoadHistory(room string, initial bool) {
view.parent.Render()
}
-func (view *MainView) ProcessMessageEvent(room *widget.RoomView, evt *gomatrix.Event) (message *types.Message) {
+func (view *MainView) ProcessMessageEvent(room ifc.RoomView, evt *gomatrix.Event) ifc.Message {
text, _ := evt.Content["body"].(string)
msgtype, _ := evt.Content["msgtype"].(string)
return room.NewMessage(evt.ID, evt.Sender, msgtype, text, unixToTime(evt.Timestamp))
@@ -519,14 +507,12 @@ func (view *MainView) getMembershipEventContent(evt *gomatrix.Event) (sender, te
return
}
-func (view *MainView) ProcessMembershipEvent(room *widget.RoomView, evt *gomatrix.Event) (message *types.Message) {
+func (view *MainView) ProcessMembershipEvent(room ifc.RoomView, evt *gomatrix.Event) ifc.Message {
sender, text := view.getMembershipEventContent(evt)
if len(text) == 0 {
- return
+ return nil
}
- message = room.NewMessage(evt.ID, sender, "m.room.member", text, unixToTime(evt.Timestamp))
- message.TextColor = tcell.ColorGreen
- return
+ return room.NewMessage(evt.ID, sender, "m.room.member", text, unixToTime(evt.Timestamp))
}
func unixToTime(unix int64) time.Time {
diff --git a/ui/widget/util.go b/ui/widget/util.go
index 0888210..bd80903 100644
--- a/ui/widget/util.go
+++ b/ui/widget/util.go
@@ -22,19 +22,19 @@ import (
"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)