Refactor UI to use interfaces everywhere

This commit is contained in:
Tulir Asokan 2018-04-09 23:45:54 +03:00
parent 2ba2fde396
commit eda2b575f0
21 changed files with 631 additions and 589 deletions

View File

@ -23,7 +23,7 @@ import (
"path/filepath" "path/filepath"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
"maunium.net/go/gomuks/ui/debug" "maunium.net/go/gomuks/debug"
) )
// Config contains the main config of gomuks. // Config contains the main config of gomuks.

View File

@ -24,7 +24,7 @@ import (
"maunium.net/go/gomatrix" "maunium.net/go/gomatrix"
"maunium.net/go/gomuks/matrix/pushrules" "maunium.net/go/gomuks/matrix/pushrules"
"maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/matrix/rooms"
"maunium.net/go/gomuks/ui/debug" "maunium.net/go/gomuks/debug"
) )
type Session struct { type Session struct {

View File

@ -18,104 +18,43 @@ package debug
import ( import (
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"os" "os"
"time" "time"
"runtime/debug" "runtime/debug"
"maunium.net/go/tview"
) )
type Printer interface { var writer io.Writer
Printf(text string, args ...interface{})
Print(text ...interface{}) 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
} }
type Pane struct {
*tview.TextView
Height int
Width int
num int
}
var Default Printer
var RedirectAllExt bool
func NewPane() *Pane {
pane := tview.NewTextView()
pane.
SetScrollable(true).
SetWrap(true).
SetBorder(true).
SetTitle("Debug output")
fmt.Fprintln(pane, "[0] Debug pane initialized")
return &Pane{
TextView: pane,
Height: 35,
Width: 80,
num: 0,
}
}
func (db *Pane) Printf(text string, args ...interface{}) {
db.WriteString(fmt.Sprintf(text, args...) + "\n")
}
func (db *Pane) Print(text ...interface{}) {
db.WriteString(fmt.Sprintln(text...))
}
func (db *Pane) WriteString(text string) {
db.num++
fmt.Fprintf(db, "[%d] %s", db.num, text)
}
type PaneSide int
const (
Top PaneSide = iota
Bottom
Left
Right
)
func (db *Pane) Wrap(main tview.Primitive, side PaneSide) tview.Primitive {
rows, columns := []int{0}, []int{0}
mainRow, mainColumn, paneRow, paneColumn := 0, 0, 0, 0
switch side {
case Top:
rows = []int{db.Height, 0}
mainRow = 1
case Bottom:
rows = []int{0, db.Height}
paneRow = 1
case Left:
columns = []int{db.Width, 0}
mainColumn = 1
case Right:
columns = []int{0, db.Width}
paneColumn = 1
}
return tview.NewGrid().SetRows(rows...).SetColumns(columns...).
AddItem(main, mainRow, mainColumn, 1, 1, 1, 1, true).
AddItem(db, paneRow, paneColumn, 1, 1, 1, 1, false)
} }
func Printf(text string, args ...interface{}) { func Printf(text string, args ...interface{}) {
if RedirectAllExt { if writer != nil {
ExtPrintf(text, args...) fmt.Fprintf(writer, time.Now().Format("[2006-01-02 15:04:05] "))
} else if Default != nil { fmt.Fprintf(writer, text+"\n", args...)
Default.Printf(text, args...)
} }
} }
func Print(text ...interface{}) { func Print(text ...interface{}) {
if RedirectAllExt { if writer != nil {
ExtPrint(text...) fmt.Fprintf(writer, time.Now().Format("[2006-01-02 15:04:05] "))
} else if Default != nil { fmt.Fprintln(writer, text...)
Default.Print(text...) }
}
func PrintStack() {
if writer != nil {
data := debug.Stack()
writer.Write(data)
} }
} }

2
debug/doc.go Normal file
View File

@ -0,0 +1,2 @@
// Package debug contains utilities to log debug messages and display panics nicely.
package debug

View File

@ -23,10 +23,10 @@ import (
"time" "time"
"maunium.net/go/gomuks/config" "maunium.net/go/gomuks/config"
"maunium.net/go/gomuks/debug"
"maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/interface"
"maunium.net/go/gomuks/matrix" "maunium.net/go/gomuks/matrix"
"maunium.net/go/gomuks/ui" "maunium.net/go/gomuks/ui"
"maunium.net/go/gomuks/ui/debug"
"maunium.net/go/tview" "maunium.net/go/tview"
) )
@ -35,7 +35,6 @@ type Gomuks struct {
app *tview.Application app *tview.Application
ui *ui.GomuksUI ui *ui.GomuksUI
matrix *matrix.Container matrix *matrix.Container
debug *debug.Pane
debugMode bool debugMode bool
config *config.Config config *config.Config
stop chan bool stop chan bool
@ -43,19 +42,14 @@ type Gomuks struct {
// NewGomuks creates a new Gomuks instance with everything initialized, // NewGomuks creates a new Gomuks instance with everything initialized,
// but does not start it. // but does not start it.
func NewGomuks(enableDebug, forceExternalDebug bool) *Gomuks { func NewGomuks(enableDebug bool) *Gomuks {
configDir := filepath.Join(os.Getenv("HOME"), ".config/gomuks") configDir := filepath.Join(os.Getenv("HOME"), ".config/gomuks")
gmx := &Gomuks{ gmx := &Gomuks{
app: tview.NewApplication(), app: tview.NewApplication(),
stop: make(chan bool, 1), 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.config = config.NewConfig(configDir)
gmx.ui = ui.NewGomuksUI(gmx) gmx.ui = ui.NewGomuksUI(gmx)
gmx.matrix = matrix.NewContainer(gmx) gmx.matrix = matrix.NewContainer(gmx)
@ -68,15 +62,6 @@ func NewGomuks(enableDebug, forceExternalDebug bool) *Gomuks {
_ = gmx.matrix.InitClient() _ = gmx.matrix.InitClient()
main := gmx.ui.InitViews() 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) gmx.app.SetRoot(main, true)
return gmx return gmx
@ -85,10 +70,10 @@ func NewGomuks(enableDebug, forceExternalDebug bool) *Gomuks {
// Save saves the active session and message history. // Save saves the active session and message history.
func (gmx *Gomuks) Save() { func (gmx *Gomuks) Save() {
if gmx.config.Session != nil { if gmx.config.Session != nil {
gmx.debug.Print("Saving session...") debug.Print("Saving session...")
_ = gmx.config.Session.Save() _ = gmx.config.Session.Save()
} }
gmx.debug.Print("Saving history...") debug.Print("Saving history...")
gmx.ui.MainView().SaveAllHistory() gmx.ui.MainView().SaveAllHistory()
} }
@ -112,9 +97,9 @@ func (gmx *Gomuks) StartAutosave() {
// Stop stops the Matrix syncer, the tview app and the autosave goroutine, // Stop stops the Matrix syncer, the tview app and the autosave goroutine,
// then saves everything and calls os.Exit(0). // then saves everything and calls os.Exit(0).
func (gmx *Gomuks) Stop() { func (gmx *Gomuks) Stop() {
gmx.debug.Print("Disconnecting from Matrix...") debug.Print("Disconnecting from Matrix...")
gmx.matrix.Stop() gmx.matrix.Stop()
gmx.debug.Print("Cleaning up UI...") debug.Print("Cleaning up UI...")
gmx.app.Stop() gmx.app.Stop()
gmx.stop <- true gmx.stop <- true
gmx.Save() gmx.Save()
@ -170,8 +155,8 @@ func (gmx *Gomuks) UI() ifc.GomuksUI {
} }
func main() { func main() {
debugVar := os.Getenv("DEBUG") enableDebug := len(os.Getenv("DEBUG")) > 0
NewGomuks(len(debugVar) > 0, debugVar == "ext").Start() NewGomuks(enableDebug).Start()
// We use os.Exit() everywhere, so exiting by returning from Start() shouldn't happen. // We use os.Exit() everywhere, so exiting by returning from Start() shouldn't happen.
time.Sleep(5 * time.Second) time.Sleep(5 * time.Second)

View File

@ -17,11 +17,12 @@
package ifc package ifc
import ( import (
"time"
"github.com/gdamore/tcell"
"maunium.net/go/gomatrix" "maunium.net/go/gomatrix"
"maunium.net/go/gomuks/matrix/pushrules" "maunium.net/go/gomuks/matrix/pushrules"
"maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/matrix/rooms"
"maunium.net/go/gomuks/ui/types"
"maunium.net/go/gomuks/ui/widget"
"maunium.net/go/tview" "maunium.net/go/tview"
) )
@ -42,7 +43,7 @@ type GomuksUI interface {
} }
type MainView interface { type MainView interface {
GetRoom(roomID string) *widget.RoomView GetRoom(roomID string) RoomView
HasRoom(roomID string) bool HasRoom(roomID string) bool
AddRoom(roomID string) AddRoom(roomID string)
RemoveRoom(roomID string) RemoveRoom(roomID string)
@ -50,11 +51,75 @@ type MainView interface {
SaveAllHistory() SaveAllHistory()
SetTyping(roomID string, users []string) SetTyping(roomID string, users []string)
AddServiceMessage(roomID *widget.RoomView, message string) ProcessMessageEvent(roomView RoomView, evt *gomatrix.Event) Message
ProcessMessageEvent(roomView *widget.RoomView, evt *gomatrix.Event) *types.Message ProcessMembershipEvent(roomView RoomView, evt *gomatrix.Event) Message
ProcessMembershipEvent(roomView *widget.RoomView, evt *gomatrix.Event) *types.Message NotifyMessage(room *rooms.Room, message Message, should pushrules.PushActionArrayShould)
NotifyMessage(room *rooms.Room, message *types.Message, should pushrules.PushActionArrayShould)
} }
type LoginView interface { 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
}

View File

@ -22,13 +22,14 @@ import (
"strings" "strings"
"time" "time"
"maunium.net/go/gomatrix" "maunium.net/go/gomatrix"
"maunium.net/go/gomuks/config" "maunium.net/go/gomuks/config"
"maunium.net/go/gomuks/debug"
"maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/interface"
"maunium.net/go/gomuks/matrix/pushrules" "maunium.net/go/gomuks/matrix/pushrules"
"maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/matrix/rooms"
"maunium.net/go/gomuks/ui/debug"
"maunium.net/go/gomuks/ui/widget"
) )
// Container is a wrapper for a gomatrix Client and some other stuff. // 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) message := mainView.ProcessMessageEvent(roomView, evt)
if message != nil { if message != nil {
if c.syncer.FirstSyncDone { if c.syncer.FirstSyncDone {
pushRules := c.PushRules().GetActions(roomView.Room, evt).Should() pushRules := c.PushRules().GetActions(roomView.MxRoom(), evt).Should()
mainView.NotifyMessage(roomView.Room, message, pushRules) mainView.NotifyMessage(roomView.MxRoom(), message, pushRules)
} }
roomView.AddMessage(message, widget.AppendMessage) roomView.AddMessage(message, ifc.AppendMessage)
c.ui.Render() c.ui.Render()
} }
} }
@ -255,8 +256,7 @@ func (c *Container) processOwnMembershipChange(evt *gomatrix.Event) {
if evt.Unsigned.PrevContent != nil { if evt.Unsigned.PrevContent != nil {
prevMembership, _ = evt.Unsigned.PrevContent["membership"].(string) prevMembership, _ = evt.Unsigned.PrevContent["membership"].(string)
} }
const Hour = 1 * 60 * 60 * 1000 if membership == prevMembership {
if membership == prevMembership || evt.Unsigned.Age > Hour {
return return
} }
switch membership { switch membership {
@ -282,15 +282,15 @@ func (c *Container) HandleMembership(evt *gomatrix.Event) {
message := mainView.ProcessMembershipEvent(roomView, evt) message := mainView.ProcessMembershipEvent(roomView, evt)
if message != nil { if message != nil {
// TODO this shouldn't be necessary // TODO this shouldn't be necessary
roomView.Room.UpdateState(evt) roomView.MxRoom().UpdateState(evt)
// TODO This should probably also be in a different place // TODO This should probably also be in a different place
roomView.UpdateUserList() roomView.UpdateUserList()
if c.syncer.FirstSyncDone { if c.syncer.FirstSyncDone {
pushRules := c.PushRules().GetActions(roomView.Room, evt).Should() pushRules := c.PushRules().GetActions(roomView.MxRoom(), evt).Should()
mainView.NotifyMessage(roomView.Room, message, pushRules) mainView.NotifyMessage(roomView.MxRoom(), message, pushRules)
} }
roomView.AddMessage(message, widget.AppendMessage) roomView.AddMessage(message, ifc.AppendMessage)
c.ui.Render() c.ui.Render()
} }
} }

View File

@ -1,2 +0,0 @@
// Package debug contains utilities to display debug messages while running an interactive tview program.
package debug

View File

@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
package widget package ui
import ( import (
"encoding/gob" "encoding/gob"
@ -24,8 +24,10 @@ import (
"time" "time"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"maunium.net/go/gomuks/ui/debug" "maunium.net/go/gomuks/debug"
"maunium.net/go/gomuks/ui/types" "maunium.net/go/gomuks/interface"
"maunium.net/go/gomuks/ui/messages"
"maunium.net/go/gomuks/ui/widget"
"maunium.net/go/tview" "maunium.net/go/tview"
) )
@ -44,11 +46,11 @@ type MessageView struct {
prevHeight int prevHeight int
prevMsgCount int prevMsgCount int
messageIDs map[string]*types.Message messageIDs map[string]messages.UIMessage
messages []*types.Message messages []messages.UIMessage
textBuffer []string textBuffer []string
metaBuffer []types.MessageMeta metaBuffer []ifc.MessageMeta
} }
func NewMessageView() *MessageView { func NewMessageView() *MessageView {
@ -60,10 +62,10 @@ func NewMessageView() *MessageView {
TimestampWidth: 8, TimestampWidth: 8,
ScrollOffset: 0, ScrollOffset: 0,
messages: make([]*types.Message, 0), messages: make([]messages.UIMessage, 0),
messageIDs: make(map[string]*types.Message), messageIDs: make(map[string]messages.UIMessage),
textBuffer: make([]string, 0), textBuffer: make([]string, 0),
metaBuffer: make([]types.MessageMeta, 0), metaBuffer: make([]ifc.MessageMeta, 0),
widestSender: 5, widestSender: 5,
prevWidth: -1, prevWidth: -1,
@ -72,11 +74,11 @@ func NewMessageView() *MessageView {
} }
} }
func (view *MessageView) NewMessage(id, sender, msgtype, text string, timestamp time.Time) *types.Message { func (view *MessageView) NewMessage(id, sender, msgtype, text string, timestamp time.Time) messages.UIMessage {
return types.NewMessage(id, sender, msgtype, text, return messages.NewMessage(id, sender, msgtype, text,
timestamp.Format(view.TimestampFormat), timestamp.Format(view.TimestampFormat),
timestamp.Format(view.DateFormat), timestamp.Format(view.DateFormat),
GetHashColor(sender)) widget.GetHashColor(sender))
} }
func (view *MessageView) SaveHistory(path string) error { func (view *MessageView) SaveHistory(path string) error {
@ -112,7 +114,7 @@ func (view *MessageView) LoadHistory(path string) (int, error) {
} }
for _, message := range view.messages { for _, message := range view.messages {
view.updateWidestSender(message.Sender) view.updateWidestSender(message.Sender())
} }
return len(view.messages), nil return len(view.messages), nil
@ -127,57 +129,61 @@ func (view *MessageView) updateWidestSender(sender string) {
} }
} }
type MessageDirection int func (view *MessageView) UpdateMessageID(ifcMessage ifc.Message, newID string) {
message, ok := ifcMessage.(messages.UIMessage)
const ( if !ok {
AppendMessage MessageDirection = iota debug.Print("[Warning] Passed non-UIMessage ifc.Message object to UpdateMessageID().")
PrependMessage debug.PrintStack()
IgnoreMessage return
) }
delete(view.messageIDs, message.ID())
func (view *MessageView) UpdateMessageID(message *types.Message, newID string) { message.SetID(newID)
delete(view.messageIDs, message.ID) view.messageIDs[message.ID()] = message
message.ID = newID
view.messageIDs[message.ID] = message
} }
func (view *MessageView) AddMessage(message *types.Message, direction MessageDirection) { func (view *MessageView) AddMessage(ifcMessage ifc.Message, direction ifc.MessageDirection) {
if message == nil { 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 return
} }
msg, messageExists := view.messageIDs[message.ID] msg, messageExists := view.messageIDs[message.ID()]
if msg != nil && messageExists { if msg != nil && messageExists {
message.CopyTo(msg) msg.CopyFrom(message)
message = msg message = msg
direction = IgnoreMessage direction = ifc.IgnoreMessage
} }
view.updateWidestSender(message.Sender) view.updateWidestSender(message.Sender())
_, _, width, _ := view.GetInnerRect() _, _, width, _ := view.GetInnerRect()
width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap
message.CalculateBuffer(width) message.CalculateBuffer(width)
if direction == AppendMessage { if direction == ifc.AppendMessage {
if view.ScrollOffset > 0 { if view.ScrollOffset > 0 {
view.ScrollOffset += message.Height() view.ScrollOffset += message.Height()
} }
view.messages = append(view.messages, message) view.messages = append(view.messages, message)
view.appendBuffer(message) view.appendBuffer(message)
} else if direction == PrependMessage { } else if direction == ifc.PrependMessage {
view.messages = append([]*types.Message{message}, view.messages...) 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 { if len(view.metaBuffer) > 0 {
prevMeta := view.metaBuffer[len(view.metaBuffer)-1] prevMeta := view.metaBuffer[len(view.metaBuffer)-1]
if prevMeta != nil && prevMeta.GetDate() != message.Date { if prevMeta != nil && prevMeta.Date() != message.Date() {
view.textBuffer = append(view.textBuffer, fmt.Sprintf("Date changed to %s", message.Date)) view.textBuffer = append(view.textBuffer, fmt.Sprintf("Date changed to %s", message.Date()))
view.metaBuffer = append(view.metaBuffer, &types.BasicMeta{TextColor: tcell.ColorGreen}) view.metaBuffer = append(view.metaBuffer, &messages.BasicMeta{BTextColor: tcell.ColorGreen})
} }
} }
@ -195,7 +201,7 @@ func (view *MessageView) recalculateBuffers() {
recalculateMessageBuffers := width != view.prevWidth recalculateMessageBuffers := width != view.prevWidth
if height != view.prevHeight || recalculateMessageBuffers || len(view.messages) != view.prevMsgCount { if height != view.prevHeight || recalculateMessageBuffers || len(view.messages) != view.prevMsgCount {
view.textBuffer = []string{} view.textBuffer = []string{}
view.metaBuffer = []types.MessageMeta{} view.metaBuffer = []ifc.MessageMeta{}
view.prevMsgCount = 0 view.prevMsgCount = 0
for _, message := range view.messages { for _, message := range view.messages {
if recalculateMessageBuffers { if recalculateMessageBuffers {
@ -280,7 +286,7 @@ func (view *MessageView) Draw(screen tcell.Screen) {
view.recalculateBuffers() view.recalculateBuffers()
if len(view.textBuffer) == 0 { 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 return
} }
@ -294,11 +300,11 @@ func (view *MessageView) Draw(screen tcell.Screen) {
if view.LoadingMessages { if view.LoadingMessages {
message = "Loading more messages..." 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) { 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 return
} }
@ -313,7 +319,7 @@ func (view *MessageView) Draw(screen tcell.Screen) {
scrollBarPos = height - int(math.Round(float64(view.ScrollOffset)/contentHeight*viewportHeight)) scrollBarPos = height - int(math.Round(float64(view.ScrollOffset)/contentHeight*viewportHeight))
} }
var prevMeta types.MessageMeta var prevMeta ifc.MessageMeta
firstLine := true firstLine := true
skippedLines := 0 skippedLines := 0
@ -338,17 +344,17 @@ func (view *MessageView) Draw(screen tcell.Screen) {
text, meta := view.textBuffer[index], view.metaBuffer[index] text, meta := view.textBuffer[index], view.metaBuffer[index]
if meta != prevMeta { if meta != prevMeta {
if len(meta.GetTimestamp()) > 0 { if len(meta.Timestamp()) > 0 {
writeLineSimpleColor(screen, meta.GetTimestamp(), x, y+line, meta.GetTimestampColor()) widget.WriteLineSimpleColor(screen, meta.Timestamp(), x, y+line, meta.TimestampColor())
} }
if prevMeta == nil || meta.GetSender() != prevMeta.GetSender() { if prevMeta == nil || meta.Sender() != prevMeta.Sender() {
writeLineColor( widget.WriteLineColor(
screen, tview.AlignRight, meta.GetSender(), screen, tview.AlignRight, meta.Sender(),
usernameX, y+line, view.widestSender, usernameX, y+line, view.widestSender,
meta.GetSenderColor()) meta.SenderColor())
} }
prevMeta = meta prevMeta = meta
} }
writeLineSimpleColor(screen, text, messageX, y+line, meta.GetTextColor()) widget.WriteLineSimpleColor(screen, text, messageX, y+line, meta.TextColor())
} }
} }

2
ui/messages/doc.go Normal file
View File

@ -0,0 +1,2 @@
// Package types contains common type definitions used by the UI.
package messages

View File

@ -14,32 +14,16 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
package debug package messages
import ( import "maunium.net/go/gomuks/interface"
"fmt"
"io"
"os"
)
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() { CalculateBuffer(width int)
var err error RecalculateBuffer()
writer, err = os.OpenFile("/tmp/gomuks-debug.log", os.O_WRONLY|os.O_APPEND, 0644) Buffer() []string
if err != nil { Height() int
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...)
}
} }

70
ui/messages/meta.go Normal file
View File

@ -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()
}

295
ui/messages/textmessage.go Normal file
View File

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

View File

@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
package widget package ui
import ( import (
"fmt" "fmt"
@ -22,6 +22,7 @@ import (
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/matrix/rooms"
"maunium.net/go/gomuks/ui/widget"
"maunium.net/go/tview" "maunium.net/go/tview"
) )
@ -126,11 +127,11 @@ func (list *RoomList) Draw(screen tcell.Screen) {
unreadMessageCount += "!" unreadMessageCount += "!"
} }
unreadMessageCount = fmt.Sprintf("(%s)", 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 lineWidth -= len(unreadMessageCount) + 1
} }
writeLine(screen, tview.AlignLeft, text, x, y, lineWidth, style) widget.WriteLine(screen, tview.AlignLeft, text, x, y, lineWidth, style)
y++ y++
if y >= bottomLimit { if y >= bottomLimit {

View File

@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
package widget package ui
import ( import (
"fmt" "fmt"
@ -24,8 +24,10 @@ import (
"time" "time"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"maunium.net/go/gomuks/interface"
"maunium.net/go/gomuks/matrix/rooms" "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" "maunium.net/go/tview"
) )
@ -36,8 +38,8 @@ type RoomView struct {
content *MessageView content *MessageView
status *tview.TextView status *tview.TextView
userList *tview.TextView userList *tview.TextView
ulBorder *Border ulBorder *widget.Border
input *AdvancedInputField input *widget.AdvancedInputField
Room *rooms.Room Room *rooms.Room
} }
@ -48,8 +50,8 @@ func NewRoomView(room *rooms.Room) *RoomView {
content: NewMessageView(), content: NewMessageView(),
status: tview.NewTextView(), status: tview.NewTextView(),
userList: tview.NewTextView(), userList: tview.NewTextView(),
ulBorder: NewBorder(), ulBorder: widget.NewBorder(),
input: NewAdvancedInputField(), input: widget.NewAdvancedInputField(),
Room: room, Room: room,
} }
@ -129,7 +131,7 @@ func (view *RoomView) GetInputText() string {
return view.input.GetText() return view.input.GetText()
} }
func (view *RoomView) GetInputField() *AdvancedInputField { func (view *RoomView) GetInputField() *widget.AdvancedInputField {
return view.input return view.input
} }
@ -230,15 +232,19 @@ func (view *RoomView) MessageView() *MessageView {
return view.content return view.content
} }
func (view *RoomView) MxRoom() *rooms.Room {
return view.Room
}
func (view *RoomView) UpdateUserList() { func (view *RoomView) UpdateUserList() {
var joined strings.Builder var joined strings.Builder
var invited strings.Builder var invited strings.Builder
for _, user := range view.Room.GetMembers() { for _, user := range view.Room.GetMembers() {
if user.Membership == "join" { if user.Membership == "join" {
joined.WriteString(AddHashColor(user.DisplayName)) joined.WriteString(widget.AddHashColor(user.DisplayName))
joined.WriteRune('\n') joined.WriteRune('\n')
} else if user.Membership == "invite" { } else if user.Membership == "invite" {
invited.WriteString(AddHashColor(user.DisplayName)) invited.WriteString(widget.AddHashColor(user.DisplayName))
invited.WriteRune('\n') 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) member := view.Room.GetMember(sender)
if member != nil { if member != nil {
sender = member.DisplayName 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) 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() now := time.Now()
id := strconv.FormatInt(now.UnixNano(), 10) id := strconv.FormatInt(now.UnixNano(), 10)
sender := "" sender := ""
if ownerMember := view.Room.GetSessionOwner(); ownerMember != nil { if ownerMember := view.Room.GetSessionOwner(); ownerMember != nil {
sender = ownerMember.DisplayName sender = ownerMember.DisplayName
} }
message := view.NewMessage(id, sender, msgtype, text, now) message := view.newUIMessage(id, sender, msgtype, text, now)
message.State = types.MessageStateSending message.SetState(ifc.MessageStateSending)
view.AddMessage(message, AppendMessage) view.AddMessage(message, ifc.AppendMessage)
return message 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) view.content.AddMessage(message, direction)
} }

View File

@ -1,2 +0,0 @@
// Package types contains common type definitions used mostly by the UI, but also other parts of gomuks.
package types

View File

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

View File

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

View File

@ -19,7 +19,7 @@ package ui
import ( import (
"maunium.net/go/gomuks/config" "maunium.net/go/gomuks/config"
"maunium.net/go/gomuks/interface" "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/gomuks/ui/widget"
"maunium.net/go/tview" "maunium.net/go/tview"
) )

View File

@ -27,12 +27,11 @@ import (
"github.com/mattn/go-runewidth" "github.com/mattn/go-runewidth"
"maunium.net/go/gomatrix" "maunium.net/go/gomatrix"
"maunium.net/go/gomuks/config" "maunium.net/go/gomuks/config"
"maunium.net/go/gomuks/debug"
"maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/interface"
"maunium.net/go/gomuks/matrix/pushrules" "maunium.net/go/gomuks/matrix/pushrules"
"maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/matrix/rooms"
"maunium.net/go/gomuks/notification" "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/gomuks/ui/widget"
"maunium.net/go/tview" "maunium.net/go/tview"
) )
@ -40,9 +39,9 @@ import (
type MainView struct { type MainView struct {
*tview.Flex *tview.Flex
roomList *widget.RoomList roomList *RoomList
roomView *tview.Pages roomView *tview.Pages
rooms map[string]*widget.RoomView rooms map[string]*RoomView
currentRoomIndex int currentRoomIndex int
roomIDs []string roomIDs []string
@ -57,9 +56,9 @@ type MainView struct {
func (ui *GomuksUI) NewMainView() tview.Primitive { func (ui *GomuksUI) NewMainView() tview.Primitive {
mainView := &MainView{ mainView := &MainView{
Flex: tview.NewFlex(), Flex: tview.NewFlex(),
roomList: widget.NewRoomList(), roomList: NewRoomList(),
roomView: tview.NewPages(), roomView: tview.NewPages(),
rooms: make(map[string]*widget.RoomView), rooms: make(map[string]*RoomView),
matrix: ui.gmx.Matrix(), matrix: ui.gmx.Matrix(),
gmx: ui.gmx, gmx: ui.gmx,
@ -81,7 +80,7 @@ func (view *MainView) BumpFocus() {
view.lastFocusTime = time.Now() 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 { if len(text) == 0 {
go view.matrix.SendTyping(roomView.Room.ID, false) go view.matrix.SendTyping(roomView.Room.ID, false)
} else if text[0] != '/' { } else if text[0] != '/' {
@ -101,7 +100,7 @@ func findWordToTabComplete(text string) string {
return output 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, "") str := runewidth.Truncate(text, cursorOffset, "")
word := findWordToTabComplete(str) word := findWordToTabComplete(str)
userCompletions := roomView.AutocompleteUser(word) userCompletions := roomView.AutocompleteUser(word)
@ -118,7 +117,7 @@ func (view *MainView) InputTabComplete(roomView *widget.RoomView, text string, c
return text return text
} }
func (view *MainView) InputSubmit(roomView *widget.RoomView, text string) { func (view *MainView) InputSubmit(roomView *RoomView, text string) {
if len(text) == 0 { if len(text) == 0 {
return return
} else if text[0] == '/' { } else if text[0] == '/' {
@ -132,23 +131,23 @@ func (view *MainView) InputSubmit(roomView *widget.RoomView, text string) {
roomView.SetInputText("") 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) tempMessage := roomView.NewTempMessage("m.text", text)
go view.sendTempMessage(roomView, tempMessage) 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() 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 { if err != nil {
tempMessage.State = types.MessageStateFailed tempMessage.SetState(ifc.MessageStateFailed)
roomView.SetStatus(fmt.Sprintf("Failed to send message: %s", err)) roomView.SetStatus(fmt.Sprintf("Failed to send message: %s", err))
} else { } else {
roomView.MessageView().UpdateMessageID(tempMessage, eventID) 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() defer view.gmx.Recover()
debug.Print("Handling command", command, args) debug.Print("Handling command", command, args)
switch command { 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)) debug.Print("Leave room result:", view.matrix.LeaveRoom(roomView.Room.ID))
case "/join": case "/join":
if len(args) == 0 { if len(args) == 0 {
view.AddServiceMessage(roomView, "Usage: /join <room>") roomView.AddServiceMessage("Usage: /join <room>")
break break
} }
debug.Print("Join room result:", view.matrix.JoinRoom(args[0])) debug.Print("Join room result:", view.matrix.JoinRoom(args[0]))
default: 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() view.BumpFocus()
k := key.Key() k := key.Key()
@ -220,7 +219,7 @@ func (view *MainView) KeyEventHandler(roomView *widget.RoomView, key *tcell.Even
const WheelScrollOffsetDiff = 3 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 { if event.Buttons() == tcell.ButtonNone {
return event return event
} }
@ -300,7 +299,7 @@ func (view *MainView) addRoom(index int, room string) {
view.roomList.Add(roomStore) view.roomList.Add(roomStore)
if !view.roomView.HasPage(room) { if !view.roomView.HasPage(room) {
roomView := widget.NewRoomView(roomStore). roomView := NewRoomView(roomStore).
SetInputSubmitFunc(view.InputSubmit). SetInputSubmitFunc(view.InputSubmit).
SetInputChangedFunc(view.InputChanged). SetInputChangedFunc(view.InputChanged).
SetTabCompleteFunc(view.InputTabComplete). 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] return view.rooms[id]
} }
@ -352,7 +351,7 @@ func (view *MainView) RemoveRoom(room string) {
} else { } else {
removeIndex = sort.StringSlice(view.roomIDs).Search(room) 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.roomIDs = append(view.roomIDs[:removeIndex], view.roomIDs[removeIndex+1:]...)
view.roomView.RemovePage(room) view.roomView.RemovePage(room)
delete(view.rooms, room) delete(view.rooms, room)
@ -363,7 +362,7 @@ func (view *MainView) SetRooms(rooms []string) {
view.roomIDs = rooms view.roomIDs = rooms
view.roomList.Clear() view.roomList.Clear()
view.roomView.Clear() view.roomView.Clear()
view.rooms = make(map[string]*widget.RoomView) view.rooms = make(map[string]*RoomView)
for index, room := range rooms { for index, room := range rooms {
view.addRoom(index, room) 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) 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. // Whether or not the room where the message came is the currently shown room.
isCurrent := room.ID == view.CurrentRoomID() isCurrent := room.ID == view.CurrentRoomID()
// Whether or not the terminal window is focused. // Whether or not the terminal window is focused.
isFocused := view.lastFocusTime.Add(30 * time.Second).Before(time.Now()) isFocused := view.lastFocusTime.Add(30 * time.Second).Before(time.Now())
// Whether or not the push rules say this message should be notified about. // 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 { if !isCurrent {
// The message is not in the current room, show new message status in room list. // 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 { if shouldNotify && !isFocused {
// Push rules say notify and the terminal is not focused, send desktop notification. // Push rules say notify and the terminal is not focused, send desktop notification.
shouldPlaySound := should.PlaySound && should.SoundName == "default" 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.SetIsHighlight(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()
} }
func (view *MainView) LoadHistory(room string, initial bool) { 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) history, prevBatch, err := view.matrix.GetHistory(roomView.Room.ID, batch, 50)
if err != nil { 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) debug.Print("Failed to fetch history for", roomView.Room.ID, err)
return return
} }
roomView.Room.PrevBatch = prevBatch roomView.Room.PrevBatch = prevBatch
for _, evt := range history { for _, evt := range history {
var message *types.Message var message ifc.Message
if evt.Type == "m.room.message" { if evt.Type == "m.room.message" {
message = view.ProcessMessageEvent(roomView, &evt) message = view.ProcessMessageEvent(roomView, &evt)
} else if evt.Type == "m.room.member" { } else if evt.Type == "m.room.member" {
message = view.ProcessMembershipEvent(roomView, &evt) message = view.ProcessMembershipEvent(roomView, &evt)
} }
if message != nil { if message != nil {
roomView.AddMessage(message, widget.PrependMessage) roomView.AddMessage(message, ifc.PrependMessage)
} }
} }
err = roomView.SaveHistory(view.config.HistoryDir) err = roomView.SaveHistory(view.config.HistoryDir)
@ -476,7 +464,7 @@ func (view *MainView) LoadHistory(room string, initial bool) {
view.parent.Render() 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) text, _ := evt.Content["body"].(string)
msgtype, _ := evt.Content["msgtype"].(string) msgtype, _ := evt.Content["msgtype"].(string)
return room.NewMessage(evt.ID, evt.Sender, msgtype, text, unixToTime(evt.Timestamp)) 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 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) sender, text := view.getMembershipEventContent(evt)
if len(text) == 0 { if len(text) == 0 {
return return nil
} }
message = room.NewMessage(evt.ID, sender, "m.room.member", text, unixToTime(evt.Timestamp)) return room.NewMessage(evt.ID, sender, "m.room.member", text, unixToTime(evt.Timestamp))
message.TextColor = tcell.ColorGreen
return
} }
func unixToTime(unix int64) time.Time { func unixToTime(unix int64) time.Time {

View File

@ -22,19 +22,19 @@ import (
"maunium.net/go/tview" "maunium.net/go/tview"
) )
func writeLineSimple(screen tcell.Screen, line string, x, y int) { func WriteLineSimple(screen tcell.Screen, line string, x, y int) {
writeLine(screen, tview.AlignLeft, line, x, y, 1<<30, tcell.StyleDefault) WriteLine(screen, tview.AlignLeft, line, x, y, 1<<30, tcell.StyleDefault)
} }
func writeLineSimpleColor(screen tcell.Screen, line string, x, y int, color tcell.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)) 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) { 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)) 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 offsetX := 0
if align == tview.AlignRight { if align == tview.AlignRight {
offsetX = maxWidth - runewidth.StringWidth(line) offsetX = maxWidth - runewidth.StringWidth(line)