From eda2b575f06e72040ebf82d24a7ec1ac84b7948c Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 9 Apr 2018 23:45:54 +0300 Subject: [PATCH] Refactor UI to use interfaces everywhere --- config/config.go | 2 +- config/session.go | 2 +- {ui/debug => debug}/debug.go | 101 ++---- debug/doc.go | 2 + gomuks.go | 37 +-- interface/ui.go | 79 ++++- matrix/matrix.go | 32 +- ui/debug/doc.go | 2 - ui/{widget => }/message-view.go | 108 ++++--- ui/messages/doc.go | 2 + ui/{debug/external.go => messages/message.go} | 34 +- ui/messages/meta.go | 70 +++++ ui/messages/textmessage.go | 295 ++++++++++++++++++ ui/{widget => }/room-list.go | 7 +- ui/{widget => }/room-view.go | 46 ++- ui/types/doc.go | 2 - ui/types/message.go | 234 -------------- ui/types/meta.go | 71 ----- ui/view-login.go | 2 +- ui/view-main.go | 78 ++--- ui/widget/util.go | 14 +- 21 files changed, 631 insertions(+), 589 deletions(-) rename {ui/debug => debug}/debug.go (59%) create mode 100644 debug/doc.go delete mode 100644 ui/debug/doc.go rename ui/{widget => }/message-view.go (73%) create mode 100644 ui/messages/doc.go rename ui/{debug/external.go => messages/message.go} (62%) create mode 100644 ui/messages/meta.go create mode 100644 ui/messages/textmessage.go rename ui/{widget => }/room-list.go (93%) rename ui/{widget => }/room-view.go (85%) delete mode 100644 ui/types/doc.go delete mode 100644 ui/types/message.go delete mode 100644 ui/types/meta.go 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 similarity index 59% rename from ui/debug/debug.go rename to 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{}) -} +var writer io.Writer -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 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 - } - 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...) + if writer != nil { + fmt.Fprintf(writer, time.Now().Format("[2006-01-02 15:04:05] ")) + fmt.Fprintf(writer, text+"\n", args...) } } func Print(text ...interface{}) { - if RedirectAllExt { - ExtPrint(text...) - } else if Default != nil { - Default.Print(text...) + if writer != nil { + fmt.Fprintf(writer, time.Now().Format("[2006-01-02 15:04:05] ")) + fmt.Fprintln(writer, 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 similarity index 73% rename from ui/widget/message-view.go rename to 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 . -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 similarity index 62% rename from ui/debug/external.go rename to 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 . -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 . + +package messages + +import ( + "github.com/gdamore/tcell" + "maunium.net/go/gomuks/interface" +) + +// BasicMeta is a simple variable store implementation of MessageMeta. +type BasicMeta struct { + BSender, BTimestamp, BDate string + BSenderColor, BTextColor, BTimestampColor tcell.Color +} + +// Sender gets the string that should be displayed as the sender of this message. +func (meta *BasicMeta) Sender() string { + return meta.BSender +} + +// SenderColor returns the color the name of the sender should be shown in. +func (meta *BasicMeta) SenderColor() tcell.Color { + return meta.BSenderColor +} + +// Timestamp returns the formatted time when the message was sent. +func (meta *BasicMeta) Timestamp() string { + return meta.BTimestamp +} + +// Date returns the formatted date when the message was sent. +func (meta *BasicMeta) Date() string { + return meta.BDate +} + +// TextColor returns the color the actual content of the message should be shown in. +func (meta *BasicMeta) TextColor() tcell.Color { + return meta.BTextColor +} + +// TimestampColor returns the color the timestamp should be shown in. +// +// This usually does not apply to the date, as it is rendered separately from the message. +func (meta *BasicMeta) TimestampColor() tcell.Color { + return meta.BTimestampColor +} + +// CopyFrom replaces the content of this meta object with the content of the given object. +func (meta *BasicMeta) CopyFrom(from ifc.MessageMeta) { + meta.BSender = from.Sender() + meta.BTimestamp = from.Timestamp() + meta.BDate = from.Date() + meta.BSenderColor = from.SenderColor() + meta.BTextColor = from.TextColor() + meta.BTimestampColor = from.TimestampColor() +} diff --git a/ui/messages/textmessage.go b/ui/messages/textmessage.go new file mode 100644 index 0000000..1f37418 --- /dev/null +++ b/ui/messages/textmessage.go @@ -0,0 +1,295 @@ +// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package messages + +import ( + "encoding/gob" + "fmt" + "regexp" + "strings" + + "github.com/gdamore/tcell" + "github.com/mattn/go-runewidth" + "maunium.net/go/gomuks/interface" +) + +func init() { + gob.Register(&UITextMessage{}) +} + +type UITextMessage struct { + MsgID string + MsgType string + MsgSender string + MsgSenderColor tcell.Color + MsgTimestamp string + MsgDate string + MsgText string + MsgState ifc.MessageState + MsgIsHighlight bool + MsgIsService bool + buffer []string + prevBufferWidth int +} + +// NewMessage creates a new Message object with the provided values and the default state. +func NewMessage(id, sender, msgtype, text, timestamp, date string, senderColor tcell.Color) UIMessage { + return &UITextMessage{ + MsgSender: sender, + MsgTimestamp: timestamp, + MsgDate: date, + MsgSenderColor: senderColor, + MsgType: msgtype, + MsgText: text, + MsgID: id, + prevBufferWidth: 0, + MsgState: ifc.MessageStateDefault, + MsgIsHighlight: false, + MsgIsService: false, + } +} + +// CopyFrom replaces the content of this message object with the content of the given object. +func (msg *UITextMessage) CopyFrom(from ifc.MessageMeta) { + msg.MsgSender = from.Sender() + msg.MsgTimestamp = from.Timestamp() + msg.MsgDate = from.Date() + msg.MsgSenderColor = from.SenderColor() + + fromMsg, ok := from.(UIMessage) + if ok { + msg.MsgID = fromMsg.ID() + msg.MsgType = fromMsg.Type() + msg.MsgText = fromMsg.Text() + msg.MsgState = fromMsg.State() + msg.MsgIsService = fromMsg.IsService() + msg.MsgIsHighlight = fromMsg.IsHighlight() + + msg.RecalculateBuffer() + } +} + +// Sender gets the string that should be displayed as the sender of this message. +// +// If the message is being sent, the sender is "Sending...". +// If sending has failed, the sender is "Error". +// If the message is an emote, the sender is blank. +// In any other case, the sender is the display name of the user who sent the message. +func (msg *UITextMessage) Sender() string { + switch msg.MsgState { + case ifc.MessageStateSending: + return "Sending..." + case ifc.MessageStateFailed: + return "Error" + } + switch msg.MsgType { + case "m.emote": + // Emotes don't show a separate sender, it's included in the buffer. + return "" + default: + return msg.MsgSender + } +} + +func (msg *UITextMessage) getStateSpecificColor() tcell.Color { + switch msg.MsgState { + case ifc.MessageStateSending: + return tcell.ColorGray + case ifc.MessageStateFailed: + return tcell.ColorRed + case ifc.MessageStateDefault: + fallthrough + default: + return tcell.ColorDefault + } +} + +// SenderColor returns the color the name of the sender should be shown in. +// +// If the message is being sent, the color is gray. +// If sending has failed, the color is red. +// +// In any other case, the color is whatever is specified in the Message struct. +// Usually that means it is the hash-based color of the sender (see ui/widget/color.go) +func (msg *UITextMessage) SenderColor() tcell.Color { + stateColor := msg.getStateSpecificColor() + switch { + case stateColor != tcell.ColorDefault: + return stateColor + case msg.MsgIsService: + return tcell.ColorGray + default: + return msg.MsgSenderColor + } +} + +// TextColor returns the color the actual content of the message should be shown in. +func (msg *UITextMessage) TextColor() tcell.Color { + stateColor := msg.getStateSpecificColor() + switch { + case stateColor != tcell.ColorDefault: + return stateColor + case msg.MsgIsService: + return tcell.ColorGray + case msg.MsgIsHighlight: + return tcell.ColorYellow + case msg.MsgType == "m.room.member": + return tcell.ColorGreen + default: + return tcell.ColorDefault + } +} + +// TimestampColor returns the color the timestamp should be shown in. +// +// As with SenderColor(), messages being sent and messages that failed to be sent are +// gray and red respectively. +// +// However, other messages are the default color instead of a color stored in the struct. +func (msg *UITextMessage) TimestampColor() tcell.Color { + return msg.getStateSpecificColor() +} + +// RecalculateBuffer calculates the buffer again with the previously provided width. +func (msg *UITextMessage) RecalculateBuffer() { + msg.CalculateBuffer(msg.prevBufferWidth) +} + +// Buffer returns the computed text buffer. +// +// The buffer contains the text of the message split into lines with a maximum +// width of whatever was provided to CalculateBuffer(). +// +// N.B. This will NOT automatically calculate the buffer if it hasn't been +// calculated already, as that requires the target width. +func (msg *UITextMessage) Buffer() []string { + return msg.buffer +} + +// Height returns the number of rows in the computed buffer (see Buffer()). +func (msg *UITextMessage) Height() int { + return len(msg.buffer) +} + +// Timestamp returns the formatted time when the message was sent. +func (msg *UITextMessage) Timestamp() string { + return msg.MsgTimestamp +} + +// Date returns the formatted date when the message was sent. +func (msg *UITextMessage) Date() string { + return msg.MsgDate +} + +func (msg *UITextMessage) ID() string { + return msg.MsgID +} + +func (msg *UITextMessage) SetID(id string) { + msg.MsgID = id +} + +func (msg *UITextMessage) Type() string { + return msg.MsgType +} + +func (msg *UITextMessage) SetType(msgtype string) { + msg.MsgType = msgtype +} + +func (msg *UITextMessage) Text() string { + return msg.MsgText +} + +func (msg *UITextMessage) SetText(text string) { + msg.MsgText = text +} + +func (msg *UITextMessage) State() ifc.MessageState { + return msg.MsgState +} + +func (msg *UITextMessage) SetState(state ifc.MessageState) { + msg.MsgState = state +} + +func (msg *UITextMessage) IsHighlight() bool { + return msg.MsgIsHighlight +} + +func (msg *UITextMessage) SetIsHighlight(isHighlight bool) { + msg.MsgIsHighlight = isHighlight +} + +func (msg *UITextMessage) IsService() bool { + return msg.MsgIsService +} + +func (msg *UITextMessage) SetIsService(isService bool) { + msg.MsgIsService = isService +} + +// Regular expressions used to split lines when calculating the buffer. +// +// From tview/textview.go +var ( + boundaryPattern = regexp.MustCompile("([[:punct:]]\\s*|\\s+)") + spacePattern = regexp.MustCompile(`\s+`) +) + +// CalculateBuffer generates the internal buffer for this message that consists +// of the text of this message split into lines at most as wide as the width +// parameter. +func (msg *UITextMessage) CalculateBuffer(width int) { + if width < 2 { + return + } + + msg.buffer = []string{} + text := msg.MsgText + if msg.MsgType == "m.emote" { + text = fmt.Sprintf("* %s %s", msg.MsgSender, msg.MsgText) + } + + forcedLinebreaks := strings.Split(text, "\n") + newlines := 0 + for _, str := range forcedLinebreaks { + if len(str) == 0 && newlines < 1 { + msg.buffer = append(msg.buffer, "") + newlines++ + } else { + newlines = 0 + } + // From tview/textview.go#reindexBuffer() + for len(str) > 0 { + extract := runewidth.Truncate(str, width, "") + if len(extract) < len(str) { + if spaces := spacePattern.FindStringIndex(str[len(extract):]); spaces != nil && spaces[0] == 0 { + extract = str[:len(extract)+spaces[1]] + } + + matches := boundaryPattern.FindAllStringIndex(extract, -1) + if len(matches) > 0 { + extract = extract[:matches[len(matches)-1][1]] + } + } + msg.buffer = append(msg.buffer, extract) + str = str[len(extract):] + } + } + msg.prevBufferWidth = width +} diff --git a/ui/widget/room-list.go b/ui/room-list.go similarity index 93% rename from ui/widget/room-list.go rename to 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 . -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 similarity index 85% rename from ui/widget/room-view.go rename to 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 . -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 . - -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 . - -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 ") + roomView.AddServiceMessage("Usage: /join ") 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)