Merge pull request #18 from tulir/ui-refactor
Refactor UI to use interfaces and add advanced message rendering
This commit is contained in:
		@@ -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.
 | 
			
		||||
@@ -33,6 +33,7 @@ type Config struct {
 | 
			
		||||
 | 
			
		||||
	Dir        string   `yaml:"-"`
 | 
			
		||||
	HistoryDir string   `yaml:"history_dir"`
 | 
			
		||||
	MediaDir   string   `yaml:"media_dir"`
 | 
			
		||||
	Session    *Session `yaml:"-"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -41,6 +42,7 @@ func NewConfig(dir string) *Config {
 | 
			
		||||
	return &Config{
 | 
			
		||||
		Dir:        dir,
 | 
			
		||||
		HistoryDir: filepath.Join(dir, "history"),
 | 
			
		||||
		MediaDir:   filepath.Join(dir, "media"),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -17,105 +17,45 @@
 | 
			
		||||
package debug
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"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)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -128,14 +68,18 @@ const Oops = ` __________
 | 
			
		||||
          U  ||----W |
 | 
			
		||||
             ||     ||`
 | 
			
		||||
 | 
			
		||||
func PrettyPanic() {
 | 
			
		||||
func PrettyPanic(panic interface{}) {
 | 
			
		||||
	fmt.Println(Oops)
 | 
			
		||||
	fmt.Println("")
 | 
			
		||||
	fmt.Println("A fatal error has occurred.")
 | 
			
		||||
	fmt.Println("")
 | 
			
		||||
	traceFile := fmt.Sprintf("/tmp/gomuks-panic-%s.txt", time.Now().Format("2006-01-02--15-04-05"))
 | 
			
		||||
	data := debug.Stack()
 | 
			
		||||
	err := ioutil.WriteFile(traceFile, data, 0644)
 | 
			
		||||
 | 
			
		||||
	var buf bytes.Buffer
 | 
			
		||||
	fmt.Fprintln(&buf, panic)
 | 
			
		||||
	buf.Write(debug.Stack())
 | 
			
		||||
	err := ioutil.WriteFile(traceFile, buf.Bytes(), 0644)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Println("Saving the stack trace to", traceFile, "failed:")
 | 
			
		||||
		fmt.Println("--------------------------------------------------------------------------------")
 | 
			
		||||
@@ -146,6 +90,7 @@ func PrettyPanic() {
 | 
			
		||||
		fmt.Println("Please provide the file save error (above) and the stack trace of the original error (below) when filing an issue.")
 | 
			
		||||
		fmt.Println("")
 | 
			
		||||
		fmt.Println("--------------------------------------------------------------------------------")
 | 
			
		||||
		fmt.Println(panic)
 | 
			
		||||
		debug.PrintStack()
 | 
			
		||||
		fmt.Println("--------------------------------------------------------------------------------")
 | 
			
		||||
	} else {
 | 
			
		||||
							
								
								
									
										2
									
								
								debug/doc.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								debug/doc.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
			
		||||
// Package debug contains utilities to log debug messages and display panics nicely.
 | 
			
		||||
package debug
 | 
			
		||||
							
								
								
									
										39
									
								
								gomuks.go
									
									
									
									
									
								
							
							
						
						
									
										39
									
								
								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()
 | 
			
		||||
@@ -132,7 +117,7 @@ func (gmx *Gomuks) Recover() {
 | 
			
		||||
		if gmx.debugMode {
 | 
			
		||||
			panic(p)
 | 
			
		||||
		} else {
 | 
			
		||||
			debug.PrettyPanic()
 | 
			
		||||
			debug.PrettyPanic(p)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -34,4 +34,6 @@ type MatrixContainer interface {
 | 
			
		||||
	LeaveRoom(roomID string) error
 | 
			
		||||
	GetHistory(roomID, prevBatch string, limit int) ([]gomatrix.Event, string, error)
 | 
			
		||||
	GetRoom(roomID string) *rooms.Room
 | 
			
		||||
	Download(mxcURL string) ([]byte, string, string, error)
 | 
			
		||||
	GetCachePath(homeserver, fileID string) string
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -17,11 +17,12 @@
 | 
			
		||||
package ifc
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"maunium.net/go/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,76 @@ 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)
 | 
			
		||||
	ParseEvent(roomView RoomView, evt *gomatrix.Event) Message
 | 
			
		||||
	//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(gmx Gomuks, 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() time.Time
 | 
			
		||||
	FormatTime() string
 | 
			
		||||
	FormatDate() 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
 | 
			
		||||
 | 
			
		||||
	NotificationContent() string
 | 
			
		||||
 | 
			
		||||
	SetState(state MessageState)
 | 
			
		||||
	State() MessageState
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										373
									
								
								lib/ansimage/LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										373
									
								
								lib/ansimage/LICENSE
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,373 @@
 | 
			
		||||
Mozilla Public License Version 2.0
 | 
			
		||||
==================================
 | 
			
		||||
 | 
			
		||||
1. Definitions
 | 
			
		||||
--------------
 | 
			
		||||
 | 
			
		||||
1.1. "Contributor"
 | 
			
		||||
    means each individual or legal entity that creates, contributes to
 | 
			
		||||
    the creation of, or owns Covered Software.
 | 
			
		||||
 | 
			
		||||
1.2. "Contributor Version"
 | 
			
		||||
    means the combination of the Contributions of others (if any) used
 | 
			
		||||
    by a Contributor and that particular Contributor's Contribution.
 | 
			
		||||
 | 
			
		||||
1.3. "Contribution"
 | 
			
		||||
    means Covered Software of a particular Contributor.
 | 
			
		||||
 | 
			
		||||
1.4. "Covered Software"
 | 
			
		||||
    means Source Code Form to which the initial Contributor has attached
 | 
			
		||||
    the notice in Exhibit A, the Executable Form of such Source Code
 | 
			
		||||
    Form, and Modifications of such Source Code Form, in each case
 | 
			
		||||
    including portions thereof.
 | 
			
		||||
 | 
			
		||||
1.5. "Incompatible With Secondary Licenses"
 | 
			
		||||
    means
 | 
			
		||||
 | 
			
		||||
    (a) that the initial Contributor has attached the notice described
 | 
			
		||||
        in Exhibit B to the Covered Software; or
 | 
			
		||||
 | 
			
		||||
    (b) that the Covered Software was made available under the terms of
 | 
			
		||||
        version 1.1 or earlier of the License, but not also under the
 | 
			
		||||
        terms of a Secondary License.
 | 
			
		||||
 | 
			
		||||
1.6. "Executable Form"
 | 
			
		||||
    means any form of the work other than Source Code Form.
 | 
			
		||||
 | 
			
		||||
1.7. "Larger Work"
 | 
			
		||||
    means a work that combines Covered Software with other material, in 
 | 
			
		||||
    a separate file or files, that is not Covered Software.
 | 
			
		||||
 | 
			
		||||
1.8. "License"
 | 
			
		||||
    means this document.
 | 
			
		||||
 | 
			
		||||
1.9. "Licensable"
 | 
			
		||||
    means having the right to grant, to the maximum extent possible,
 | 
			
		||||
    whether at the time of the initial grant or subsequently, any and
 | 
			
		||||
    all of the rights conveyed by this License.
 | 
			
		||||
 | 
			
		||||
1.10. "Modifications"
 | 
			
		||||
    means any of the following:
 | 
			
		||||
 | 
			
		||||
    (a) any file in Source Code Form that results from an addition to,
 | 
			
		||||
        deletion from, or modification of the contents of Covered
 | 
			
		||||
        Software; or
 | 
			
		||||
 | 
			
		||||
    (b) any new file in Source Code Form that contains any Covered
 | 
			
		||||
        Software.
 | 
			
		||||
 | 
			
		||||
1.11. "Patent Claims" of a Contributor
 | 
			
		||||
    means any patent claim(s), including without limitation, method,
 | 
			
		||||
    process, and apparatus claims, in any patent Licensable by such
 | 
			
		||||
    Contributor that would be infringed, but for the grant of the
 | 
			
		||||
    License, by the making, using, selling, offering for sale, having
 | 
			
		||||
    made, import, or transfer of either its Contributions or its
 | 
			
		||||
    Contributor Version.
 | 
			
		||||
 | 
			
		||||
1.12. "Secondary License"
 | 
			
		||||
    means either the GNU General Public License, Version 2.0, the GNU
 | 
			
		||||
    Lesser General Public License, Version 2.1, the GNU Affero General
 | 
			
		||||
    Public License, Version 3.0, or any later versions of those
 | 
			
		||||
    licenses.
 | 
			
		||||
 | 
			
		||||
1.13. "Source Code Form"
 | 
			
		||||
    means the form of the work preferred for making modifications.
 | 
			
		||||
 | 
			
		||||
1.14. "You" (or "Your")
 | 
			
		||||
    means an individual or a legal entity exercising rights under this
 | 
			
		||||
    License. For legal entities, "You" includes any entity that
 | 
			
		||||
    controls, is controlled by, or is under common control with You. For
 | 
			
		||||
    purposes of this definition, "control" means (a) the power, direct
 | 
			
		||||
    or indirect, to cause the direction or management of such entity,
 | 
			
		||||
    whether by contract or otherwise, or (b) ownership of more than
 | 
			
		||||
    fifty percent (50%) of the outstanding shares or beneficial
 | 
			
		||||
    ownership of such entity.
 | 
			
		||||
 | 
			
		||||
2. License Grants and Conditions
 | 
			
		||||
--------------------------------
 | 
			
		||||
 | 
			
		||||
2.1. Grants
 | 
			
		||||
 | 
			
		||||
Each Contributor hereby grants You a world-wide, royalty-free,
 | 
			
		||||
non-exclusive license:
 | 
			
		||||
 | 
			
		||||
(a) under intellectual property rights (other than patent or trademark)
 | 
			
		||||
    Licensable by such Contributor to use, reproduce, make available,
 | 
			
		||||
    modify, display, perform, distribute, and otherwise exploit its
 | 
			
		||||
    Contributions, either on an unmodified basis, with Modifications, or
 | 
			
		||||
    as part of a Larger Work; and
 | 
			
		||||
 | 
			
		||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
 | 
			
		||||
    for sale, have made, import, and otherwise transfer either its
 | 
			
		||||
    Contributions or its Contributor Version.
 | 
			
		||||
 | 
			
		||||
2.2. Effective Date
 | 
			
		||||
 | 
			
		||||
The licenses granted in Section 2.1 with respect to any Contribution
 | 
			
		||||
become effective for each Contribution on the date the Contributor first
 | 
			
		||||
distributes such Contribution.
 | 
			
		||||
 | 
			
		||||
2.3. Limitations on Grant Scope
 | 
			
		||||
 | 
			
		||||
The licenses granted in this Section 2 are the only rights granted under
 | 
			
		||||
this License. No additional rights or licenses will be implied from the
 | 
			
		||||
distribution or licensing of Covered Software under this License.
 | 
			
		||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
 | 
			
		||||
Contributor:
 | 
			
		||||
 | 
			
		||||
(a) for any code that a Contributor has removed from Covered Software;
 | 
			
		||||
    or
 | 
			
		||||
 | 
			
		||||
(b) for infringements caused by: (i) Your and any other third party's
 | 
			
		||||
    modifications of Covered Software, or (ii) the combination of its
 | 
			
		||||
    Contributions with other software (except as part of its Contributor
 | 
			
		||||
    Version); or
 | 
			
		||||
 | 
			
		||||
(c) under Patent Claims infringed by Covered Software in the absence of
 | 
			
		||||
    its Contributions.
 | 
			
		||||
 | 
			
		||||
This License does not grant any rights in the trademarks, service marks,
 | 
			
		||||
or logos of any Contributor (except as may be necessary to comply with
 | 
			
		||||
the notice requirements in Section 3.4).
 | 
			
		||||
 | 
			
		||||
2.4. Subsequent Licenses
 | 
			
		||||
 | 
			
		||||
No Contributor makes additional grants as a result of Your choice to
 | 
			
		||||
distribute the Covered Software under a subsequent version of this
 | 
			
		||||
License (see Section 10.2) or under the terms of a Secondary License (if
 | 
			
		||||
permitted under the terms of Section 3.3).
 | 
			
		||||
 | 
			
		||||
2.5. Representation
 | 
			
		||||
 | 
			
		||||
Each Contributor represents that the Contributor believes its
 | 
			
		||||
Contributions are its original creation(s) or it has sufficient rights
 | 
			
		||||
to grant the rights to its Contributions conveyed by this License.
 | 
			
		||||
 | 
			
		||||
2.6. Fair Use
 | 
			
		||||
 | 
			
		||||
This License is not intended to limit any rights You have under
 | 
			
		||||
applicable copyright doctrines of fair use, fair dealing, or other
 | 
			
		||||
equivalents.
 | 
			
		||||
 | 
			
		||||
2.7. Conditions
 | 
			
		||||
 | 
			
		||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
 | 
			
		||||
in Section 2.1.
 | 
			
		||||
 | 
			
		||||
3. Responsibilities
 | 
			
		||||
-------------------
 | 
			
		||||
 | 
			
		||||
3.1. Distribution of Source Form
 | 
			
		||||
 | 
			
		||||
All distribution of Covered Software in Source Code Form, including any
 | 
			
		||||
Modifications that You create or to which You contribute, must be under
 | 
			
		||||
the terms of this License. You must inform recipients that the Source
 | 
			
		||||
Code Form of the Covered Software is governed by the terms of this
 | 
			
		||||
License, and how they can obtain a copy of this License. You may not
 | 
			
		||||
attempt to alter or restrict the recipients' rights in the Source Code
 | 
			
		||||
Form.
 | 
			
		||||
 | 
			
		||||
3.2. Distribution of Executable Form
 | 
			
		||||
 | 
			
		||||
If You distribute Covered Software in Executable Form then:
 | 
			
		||||
 | 
			
		||||
(a) such Covered Software must also be made available in Source Code
 | 
			
		||||
    Form, as described in Section 3.1, and You must inform recipients of
 | 
			
		||||
    the Executable Form how they can obtain a copy of such Source Code
 | 
			
		||||
    Form by reasonable means in a timely manner, at a charge no more
 | 
			
		||||
    than the cost of distribution to the recipient; and
 | 
			
		||||
 | 
			
		||||
(b) You may distribute such Executable Form under the terms of this
 | 
			
		||||
    License, or sublicense it under different terms, provided that the
 | 
			
		||||
    license for the Executable Form does not attempt to limit or alter
 | 
			
		||||
    the recipients' rights in the Source Code Form under this License.
 | 
			
		||||
 | 
			
		||||
3.3. Distribution of a Larger Work
 | 
			
		||||
 | 
			
		||||
You may create and distribute a Larger Work under terms of Your choice,
 | 
			
		||||
provided that You also comply with the requirements of this License for
 | 
			
		||||
the Covered Software. If the Larger Work is a combination of Covered
 | 
			
		||||
Software with a work governed by one or more Secondary Licenses, and the
 | 
			
		||||
Covered Software is not Incompatible With Secondary Licenses, this
 | 
			
		||||
License permits You to additionally distribute such Covered Software
 | 
			
		||||
under the terms of such Secondary License(s), so that the recipient of
 | 
			
		||||
the Larger Work may, at their option, further distribute the Covered
 | 
			
		||||
Software under the terms of either this License or such Secondary
 | 
			
		||||
License(s).
 | 
			
		||||
 | 
			
		||||
3.4. Notices
 | 
			
		||||
 | 
			
		||||
You may not remove or alter the substance of any license notices
 | 
			
		||||
(including copyright notices, patent notices, disclaimers of warranty,
 | 
			
		||||
or limitations of liability) contained within the Source Code Form of
 | 
			
		||||
the Covered Software, except that You may alter any license notices to
 | 
			
		||||
the extent required to remedy known factual inaccuracies.
 | 
			
		||||
 | 
			
		||||
3.5. Application of Additional Terms
 | 
			
		||||
 | 
			
		||||
You may choose to offer, and to charge a fee for, warranty, support,
 | 
			
		||||
indemnity or liability obligations to one or more recipients of Covered
 | 
			
		||||
Software. However, You may do so only on Your own behalf, and not on
 | 
			
		||||
behalf of any Contributor. You must make it absolutely clear that any
 | 
			
		||||
such warranty, support, indemnity, or liability obligation is offered by
 | 
			
		||||
You alone, and You hereby agree to indemnify every Contributor for any
 | 
			
		||||
liability incurred by such Contributor as a result of warranty, support,
 | 
			
		||||
indemnity or liability terms You offer. You may include additional
 | 
			
		||||
disclaimers of warranty and limitations of liability specific to any
 | 
			
		||||
jurisdiction.
 | 
			
		||||
 | 
			
		||||
4. Inability to Comply Due to Statute or Regulation
 | 
			
		||||
---------------------------------------------------
 | 
			
		||||
 | 
			
		||||
If it is impossible for You to comply with any of the terms of this
 | 
			
		||||
License with respect to some or all of the Covered Software due to
 | 
			
		||||
statute, judicial order, or regulation then You must: (a) comply with
 | 
			
		||||
the terms of this License to the maximum extent possible; and (b)
 | 
			
		||||
describe the limitations and the code they affect. Such description must
 | 
			
		||||
be placed in a text file included with all distributions of the Covered
 | 
			
		||||
Software under this License. Except to the extent prohibited by statute
 | 
			
		||||
or regulation, such description must be sufficiently detailed for a
 | 
			
		||||
recipient of ordinary skill to be able to understand it.
 | 
			
		||||
 | 
			
		||||
5. Termination
 | 
			
		||||
--------------
 | 
			
		||||
 | 
			
		||||
5.1. The rights granted under this License will terminate automatically
 | 
			
		||||
if You fail to comply with any of its terms. However, if You become
 | 
			
		||||
compliant, then the rights granted under this License from a particular
 | 
			
		||||
Contributor are reinstated (a) provisionally, unless and until such
 | 
			
		||||
Contributor explicitly and finally terminates Your grants, and (b) on an
 | 
			
		||||
ongoing basis, if such Contributor fails to notify You of the
 | 
			
		||||
non-compliance by some reasonable means prior to 60 days after You have
 | 
			
		||||
come back into compliance. Moreover, Your grants from a particular
 | 
			
		||||
Contributor are reinstated on an ongoing basis if such Contributor
 | 
			
		||||
notifies You of the non-compliance by some reasonable means, this is the
 | 
			
		||||
first time You have received notice of non-compliance with this License
 | 
			
		||||
from such Contributor, and You become compliant prior to 30 days after
 | 
			
		||||
Your receipt of the notice.
 | 
			
		||||
 | 
			
		||||
5.2. If You initiate litigation against any entity by asserting a patent
 | 
			
		||||
infringement claim (excluding declaratory judgment actions,
 | 
			
		||||
counter-claims, and cross-claims) alleging that a Contributor Version
 | 
			
		||||
directly or indirectly infringes any patent, then the rights granted to
 | 
			
		||||
You by any and all Contributors for the Covered Software under Section
 | 
			
		||||
2.1 of this License shall terminate.
 | 
			
		||||
 | 
			
		||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
 | 
			
		||||
end user license agreements (excluding distributors and resellers) which
 | 
			
		||||
have been validly granted by You or Your distributors under this License
 | 
			
		||||
prior to termination shall survive termination.
 | 
			
		||||
 | 
			
		||||
************************************************************************
 | 
			
		||||
*                                                                      *
 | 
			
		||||
*  6. Disclaimer of Warranty                                           *
 | 
			
		||||
*  -------------------------                                           *
 | 
			
		||||
*                                                                      *
 | 
			
		||||
*  Covered Software is provided under this License on an "as is"       *
 | 
			
		||||
*  basis, without warranty of any kind, either expressed, implied, or  *
 | 
			
		||||
*  statutory, including, without limitation, warranties that the       *
 | 
			
		||||
*  Covered Software is free of defects, merchantable, fit for a        *
 | 
			
		||||
*  particular purpose or non-infringing. The entire risk as to the     *
 | 
			
		||||
*  quality and performance of the Covered Software is with You.        *
 | 
			
		||||
*  Should any Covered Software prove defective in any respect, You     *
 | 
			
		||||
*  (not any Contributor) assume the cost of any necessary servicing,   *
 | 
			
		||||
*  repair, or correction. This disclaimer of warranty constitutes an   *
 | 
			
		||||
*  essential part of this License. No use of any Covered Software is   *
 | 
			
		||||
*  authorized under this License except under this disclaimer.         *
 | 
			
		||||
*                                                                      *
 | 
			
		||||
************************************************************************
 | 
			
		||||
 | 
			
		||||
************************************************************************
 | 
			
		||||
*                                                                      *
 | 
			
		||||
*  7. Limitation of Liability                                          *
 | 
			
		||||
*  --------------------------                                          *
 | 
			
		||||
*                                                                      *
 | 
			
		||||
*  Under no circumstances and under no legal theory, whether tort      *
 | 
			
		||||
*  (including negligence), contract, or otherwise, shall any           *
 | 
			
		||||
*  Contributor, or anyone who distributes Covered Software as          *
 | 
			
		||||
*  permitted above, be liable to You for any direct, indirect,         *
 | 
			
		||||
*  special, incidental, or consequential damages of any character      *
 | 
			
		||||
*  including, without limitation, damages for lost profits, loss of    *
 | 
			
		||||
*  goodwill, work stoppage, computer failure or malfunction, or any    *
 | 
			
		||||
*  and all other commercial damages or losses, even if such party      *
 | 
			
		||||
*  shall have been informed of the possibility of such damages. This   *
 | 
			
		||||
*  limitation of liability shall not apply to liability for death or   *
 | 
			
		||||
*  personal injury resulting from such party's negligence to the       *
 | 
			
		||||
*  extent applicable law prohibits such limitation. Some               *
 | 
			
		||||
*  jurisdictions do not allow the exclusion or limitation of           *
 | 
			
		||||
*  incidental or consequential damages, so this exclusion and          *
 | 
			
		||||
*  limitation may not apply to You.                                    *
 | 
			
		||||
*                                                                      *
 | 
			
		||||
************************************************************************
 | 
			
		||||
 | 
			
		||||
8. Litigation
 | 
			
		||||
-------------
 | 
			
		||||
 | 
			
		||||
Any litigation relating to this License may be brought only in the
 | 
			
		||||
courts of a jurisdiction where the defendant maintains its principal
 | 
			
		||||
place of business and such litigation shall be governed by laws of that
 | 
			
		||||
jurisdiction, without reference to its conflict-of-law provisions.
 | 
			
		||||
Nothing in this Section shall prevent a party's ability to bring
 | 
			
		||||
cross-claims or counter-claims.
 | 
			
		||||
 | 
			
		||||
9. Miscellaneous
 | 
			
		||||
----------------
 | 
			
		||||
 | 
			
		||||
This License represents the complete agreement concerning the subject
 | 
			
		||||
matter hereof. If any provision of this License is held to be
 | 
			
		||||
unenforceable, such provision shall be reformed only to the extent
 | 
			
		||||
necessary to make it enforceable. Any law or regulation which provides
 | 
			
		||||
that the language of a contract shall be construed against the drafter
 | 
			
		||||
shall not be used to construe this License against a Contributor.
 | 
			
		||||
 | 
			
		||||
10. Versions of the License
 | 
			
		||||
---------------------------
 | 
			
		||||
 | 
			
		||||
10.1. New Versions
 | 
			
		||||
 | 
			
		||||
Mozilla Foundation is the license steward. Except as provided in Section
 | 
			
		||||
10.3, no one other than the license steward has the right to modify or
 | 
			
		||||
publish new versions of this License. Each version will be given a
 | 
			
		||||
distinguishing version number.
 | 
			
		||||
 | 
			
		||||
10.2. Effect of New Versions
 | 
			
		||||
 | 
			
		||||
You may distribute the Covered Software under the terms of the version
 | 
			
		||||
of the License under which You originally received the Covered Software,
 | 
			
		||||
or under the terms of any subsequent version published by the license
 | 
			
		||||
steward.
 | 
			
		||||
 | 
			
		||||
10.3. Modified Versions
 | 
			
		||||
 | 
			
		||||
If you create software not governed by this License, and you want to
 | 
			
		||||
create a new license for such software, you may create and use a
 | 
			
		||||
modified version of this License if you rename the license and remove
 | 
			
		||||
any references to the name of the license steward (except to note that
 | 
			
		||||
such modified license differs from this License).
 | 
			
		||||
 | 
			
		||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
 | 
			
		||||
Licenses
 | 
			
		||||
 | 
			
		||||
If You choose to distribute Source Code Form that is Incompatible With
 | 
			
		||||
Secondary Licenses under the terms of this version of the License, the
 | 
			
		||||
notice described in Exhibit B of this License must be attached.
 | 
			
		||||
 | 
			
		||||
Exhibit A - Source Code Form License Notice
 | 
			
		||||
-------------------------------------------
 | 
			
		||||
 | 
			
		||||
  This Source Code Form is subject to the terms of the Mozilla Public
 | 
			
		||||
  License, v. 2.0. If a copy of the MPL was not distributed with this
 | 
			
		||||
  file, You can obtain one at http://mozilla.org/MPL/2.0/.
 | 
			
		||||
 | 
			
		||||
If it is not possible or desirable to put the notice in a particular
 | 
			
		||||
file, then You may include the notice in a location (such as a LICENSE
 | 
			
		||||
file in a relevant directory) where a recipient would be likely to look
 | 
			
		||||
for such a notice.
 | 
			
		||||
 | 
			
		||||
You may add additional accurate notices of copyright ownership.
 | 
			
		||||
 | 
			
		||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
 | 
			
		||||
---------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
  This Source Code Form is "Incompatible With Secondary Licenses", as
 | 
			
		||||
  defined by the Mozilla Public License, v. 2.0.
 | 
			
		||||
							
								
								
									
										287
									
								
								lib/ansimage/ansimage.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										287
									
								
								lib/ansimage/ansimage.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,287 @@
 | 
			
		||||
//       ___  _____  ____
 | 
			
		||||
//      / _ \/  _/ |/_/ /____ ______ _
 | 
			
		||||
//     / ___// /_>  </ __/ -_) __/  ' \
 | 
			
		||||
//    /_/  /___/_/|_|\__/\__/_/ /_/_/_/
 | 
			
		||||
//
 | 
			
		||||
//    Copyright 2017 Eliuk Blau
 | 
			
		||||
//
 | 
			
		||||
//    This Source Code Form is subject to the terms of the Mozilla Public
 | 
			
		||||
//    License, v. 2.0. If a copy of the MPL was not distributed with this
 | 
			
		||||
//    file, You can obtain one at http://mozilla.org/MPL/2.0/.
 | 
			
		||||
 | 
			
		||||
package ansimage
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"image"
 | 
			
		||||
	"image/color"
 | 
			
		||||
	"image/draw"
 | 
			
		||||
	_ "image/gif"  // initialize decoder
 | 
			
		||||
	_ "image/jpeg" // initialize decoder
 | 
			
		||||
	_ "image/png"  // initialize decoder
 | 
			
		||||
	"io"
 | 
			
		||||
	"os"
 | 
			
		||||
 | 
			
		||||
	"github.com/disintegration/imaging"
 | 
			
		||||
	_ "golang.org/x/image/bmp"  // initialize decoder
 | 
			
		||||
	_ "golang.org/x/image/tiff" // initialize decoder
 | 
			
		||||
	_ "golang.org/x/image/webp" // initialize decoder
 | 
			
		||||
	"maunium.net/go/gomuks/ui/messages/tstring"
 | 
			
		||||
	"maunium.net/go/tcell"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	// ErrHeightNonMoT happens when ANSImage height is not a Multiple of Two value.
 | 
			
		||||
	ErrHeightNonMoT = errors.New("ANSImage: height must be a Multiple of Two value")
 | 
			
		||||
 | 
			
		||||
	// ErrInvalidBoundsMoT happens when ANSImage height or width are invalid values (Multiple of Two).
 | 
			
		||||
	ErrInvalidBoundsMoT = errors.New("ANSImage: height or width must be >=2")
 | 
			
		||||
 | 
			
		||||
	// ErrOutOfBounds happens when ANSI-pixel coordinates are out of ANSImage bounds.
 | 
			
		||||
	ErrOutOfBounds = errors.New("ANSImage: out of bounds")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// ANSIpixel represents a pixel of an ANSImage.
 | 
			
		||||
type ANSIpixel struct {
 | 
			
		||||
	Brightness uint8
 | 
			
		||||
	R, G, B    uint8
 | 
			
		||||
	upper      bool
 | 
			
		||||
	source     *ANSImage
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ANSImage represents an image encoded in ANSI escape codes.
 | 
			
		||||
type ANSImage struct {
 | 
			
		||||
	h, w     int
 | 
			
		||||
	maxprocs int
 | 
			
		||||
	bgR      uint8
 | 
			
		||||
	bgG      uint8
 | 
			
		||||
	bgB      uint8
 | 
			
		||||
	pixmap   [][]*ANSIpixel
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ai *ANSImage) Pixmap() [][]*ANSIpixel {
 | 
			
		||||
	return ai.pixmap
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Height gets total rows of ANSImage.
 | 
			
		||||
func (ai *ANSImage) Height() int {
 | 
			
		||||
	return ai.h
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Width gets total columns of ANSImage.
 | 
			
		||||
func (ai *ANSImage) Width() int {
 | 
			
		||||
	return ai.w
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetMaxProcs sets the maximum number of parallel goroutines to render the ANSImage
 | 
			
		||||
// (user should manually sets `runtime.GOMAXPROCS(max)` before to this change takes effect).
 | 
			
		||||
func (ai *ANSImage) SetMaxProcs(max int) {
 | 
			
		||||
	ai.maxprocs = max
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetMaxProcs gets the maximum number of parallels goroutines to render the ANSImage.
 | 
			
		||||
func (ai *ANSImage) GetMaxProcs() int {
 | 
			
		||||
	return ai.maxprocs
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetAt sets ANSI-pixel color (RBG) and brightness in coordinates (y,x).
 | 
			
		||||
func (ai *ANSImage) SetAt(y, x int, r, g, b, brightness uint8) error {
 | 
			
		||||
	if y >= 0 && y < ai.h && x >= 0 && x < ai.w {
 | 
			
		||||
		ai.pixmap[y][x].R = r
 | 
			
		||||
		ai.pixmap[y][x].G = g
 | 
			
		||||
		ai.pixmap[y][x].B = b
 | 
			
		||||
		ai.pixmap[y][x].Brightness = brightness
 | 
			
		||||
		ai.pixmap[y][x].upper = y%2 == 0
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	return ErrOutOfBounds
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetAt gets ANSI-pixel in coordinates (y,x).
 | 
			
		||||
func (ai *ANSImage) GetAt(y, x int) (*ANSIpixel, error) {
 | 
			
		||||
	if y >= 0 && y < ai.h && x >= 0 && x < ai.w {
 | 
			
		||||
		return &ANSIpixel{
 | 
			
		||||
			R:          ai.pixmap[y][x].R,
 | 
			
		||||
			G:          ai.pixmap[y][x].G,
 | 
			
		||||
			B:          ai.pixmap[y][x].B,
 | 
			
		||||
			Brightness: ai.pixmap[y][x].Brightness,
 | 
			
		||||
			upper:      ai.pixmap[y][x].upper,
 | 
			
		||||
			source:     ai.pixmap[y][x].source,
 | 
			
		||||
		},
 | 
			
		||||
			nil
 | 
			
		||||
	}
 | 
			
		||||
	return nil, ErrOutOfBounds
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Render returns the ANSI-compatible string form of ANSImage.
 | 
			
		||||
// (Nice info for ANSI True Colour - https://gist.github.com/XVilka/8346728)
 | 
			
		||||
func (ai *ANSImage) Render() []tstring.TString {
 | 
			
		||||
	type renderData struct {
 | 
			
		||||
		row    int
 | 
			
		||||
		render tstring.TString
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rows := make([]tstring.TString, ai.h/2)
 | 
			
		||||
	for y := 0; y < ai.h; y += ai.maxprocs {
 | 
			
		||||
		ch := make(chan renderData, ai.maxprocs)
 | 
			
		||||
		for n, row := 0, y; (n <= ai.maxprocs) && (2*row+1 < ai.h); n, row = n+1, y+n {
 | 
			
		||||
			go func(row, y int) {
 | 
			
		||||
				str := make(tstring.TString, ai.w)
 | 
			
		||||
				for x := 0; x < ai.w; x++ {
 | 
			
		||||
					topPixel := ai.pixmap[y][x]
 | 
			
		||||
					topColor := tcell.NewRGBColor(int32(topPixel.R), int32(topPixel.G), int32(topPixel.B))
 | 
			
		||||
 | 
			
		||||
					bottomPixel := ai.pixmap[y+1][x]
 | 
			
		||||
					bottomColor := tcell.NewRGBColor(int32(bottomPixel.R), int32(bottomPixel.G), int32(bottomPixel.B))
 | 
			
		||||
 | 
			
		||||
					str[x] = tstring.Cell{
 | 
			
		||||
						Char:  '▄',
 | 
			
		||||
						Style: tcell.StyleDefault.Background(topColor).Foreground(bottomColor),
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				ch <- renderData{row: row, render: str}
 | 
			
		||||
			}(row, 2*row)
 | 
			
		||||
		}
 | 
			
		||||
		for n, row := 0, y; (n <= ai.maxprocs) && (2*row+1 < ai.h); n, row = n+1, y+n {
 | 
			
		||||
			data := <-ch
 | 
			
		||||
			rows[data.row] = data.render
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return rows
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// New creates a new empty ANSImage ready to draw on it.
 | 
			
		||||
func New(h, w int, bg color.Color) (*ANSImage, error) {
 | 
			
		||||
	if h%2 != 0 {
 | 
			
		||||
		return nil, ErrHeightNonMoT
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if h < 2 || w < 2 {
 | 
			
		||||
		return nil, ErrInvalidBoundsMoT
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	r, g, b, _ := bg.RGBA()
 | 
			
		||||
	ansimage := &ANSImage{
 | 
			
		||||
		h:        h, w: w,
 | 
			
		||||
		maxprocs: 1,
 | 
			
		||||
		bgR:      uint8(r),
 | 
			
		||||
		bgG:      uint8(g),
 | 
			
		||||
		bgB:      uint8(b),
 | 
			
		||||
		pixmap:   nil,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ansimage.pixmap = func() [][]*ANSIpixel {
 | 
			
		||||
		v := make([][]*ANSIpixel, h)
 | 
			
		||||
		for y := 0; y < h; y++ {
 | 
			
		||||
			v[y] = make([]*ANSIpixel, w)
 | 
			
		||||
			for x := 0; x < w; x++ {
 | 
			
		||||
				v[y][x] = &ANSIpixel{
 | 
			
		||||
					R:          0,
 | 
			
		||||
					G:          0,
 | 
			
		||||
					B:          0,
 | 
			
		||||
					Brightness: 0,
 | 
			
		||||
					source:     ansimage,
 | 
			
		||||
					upper:      y%2 == 0,
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return v
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	return ansimage, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewFromReader creates a new ANSImage from an io.Reader.
 | 
			
		||||
// Background color is used to fill when image has transparency or dithering mode is enabled
 | 
			
		||||
// Dithering mode is used to specify the way that ANSImage render ANSI-pixels (char/block elements).
 | 
			
		||||
func NewFromReader(reader io.Reader, bg color.Color) (*ANSImage, error) {
 | 
			
		||||
	img, _, err := image.Decode(reader)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return createANSImage(img, bg)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewScaledFromReader creates a new scaled ANSImage from an io.Reader.
 | 
			
		||||
// Background color is used to fill when image has transparency or dithering mode is enabled
 | 
			
		||||
// Dithering mode is used to specify the way that ANSImage render ANSI-pixels (char/block elements).
 | 
			
		||||
func NewScaledFromReader(reader io.Reader, y, x int, bg color.Color) (*ANSImage, error) {
 | 
			
		||||
	img, _, err := image.Decode(reader)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	img = imaging.Resize(img, x, y, imaging.Lanczos)
 | 
			
		||||
 | 
			
		||||
	return createANSImage(img, bg)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewFromFile creates a new ANSImage from a file.
 | 
			
		||||
// Background color is used to fill when image has transparency or dithering mode is enabled
 | 
			
		||||
// Dithering mode is used to specify the way that ANSImage render ANSI-pixels (char/block elements).
 | 
			
		||||
func NewFromFile(name string, bg color.Color) (*ANSImage, error) {
 | 
			
		||||
	reader, err := os.Open(name)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	defer reader.Close()
 | 
			
		||||
	return NewFromReader(reader, bg)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewScaledFromFile creates a new scaled ANSImage from a file.
 | 
			
		||||
// Background color is used to fill when image has transparency or dithering mode is enabled
 | 
			
		||||
// Dithering mode is used to specify the way that ANSImage render ANSI-pixels (char/block elements).
 | 
			
		||||
func NewScaledFromFile(name string, y, x int, bg color.Color) (*ANSImage, error) {
 | 
			
		||||
	reader, err := os.Open(name)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	defer reader.Close()
 | 
			
		||||
	return NewScaledFromReader(reader, y, x, bg)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// createANSImage loads data from an image and returns an ANSImage.
 | 
			
		||||
// Background color is used to fill when image has transparency or dithering mode is enabled
 | 
			
		||||
// Dithering mode is used to specify the way that ANSImage render ANSI-pixels (char/block elements).
 | 
			
		||||
func createANSImage(img image.Image, bg color.Color) (*ANSImage, error) {
 | 
			
		||||
	var rgbaOut *image.RGBA
 | 
			
		||||
	bounds := img.Bounds()
 | 
			
		||||
 | 
			
		||||
	// do compositing only if background color has no transparency (thank you @disq for the idea!)
 | 
			
		||||
	// (info - http://stackoverflow.com/questions/36595687/transparent-pixel-color-go-lang-image)
 | 
			
		||||
	if _, _, _, a := bg.RGBA(); a >= 0xffff {
 | 
			
		||||
		rgbaOut = image.NewRGBA(bounds)
 | 
			
		||||
		draw.Draw(rgbaOut, bounds, image.NewUniform(bg), image.ZP, draw.Src)
 | 
			
		||||
		draw.Draw(rgbaOut, bounds, img, image.ZP, draw.Over)
 | 
			
		||||
	} else {
 | 
			
		||||
		if v, ok := img.(*image.RGBA); ok {
 | 
			
		||||
			rgbaOut = v
 | 
			
		||||
		} else {
 | 
			
		||||
			rgbaOut = image.NewRGBA(bounds)
 | 
			
		||||
			draw.Draw(rgbaOut, bounds, img, image.ZP, draw.Src)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	yMin, xMin := bounds.Min.Y, bounds.Min.X
 | 
			
		||||
	yMax, xMax := bounds.Max.Y, bounds.Max.X
 | 
			
		||||
 | 
			
		||||
	// always sets an even number of ANSIPixel rows...
 | 
			
		||||
	yMax = yMax - yMax%2 // one for upper pixel and another for lower pixel --> without dithering
 | 
			
		||||
 | 
			
		||||
	ansimage, err := New(yMax, xMax, bg)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for y := yMin; y < yMax; y++ {
 | 
			
		||||
		for x := xMin; x < xMax; x++ {
 | 
			
		||||
			v := rgbaOut.RGBAAt(x, y)
 | 
			
		||||
			if err := ansimage.SetAt(y, x, v.R, v.G, v.B, 0); err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return ansimage, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										12
									
								
								lib/ansimage/doc.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								lib/ansimage/doc.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
// Package ansimage is a simplified version of the ansimage package
 | 
			
		||||
// in https://github.com/eliukblau/pixterm focused in rendering images
 | 
			
		||||
// to a tcell-based TUI app.
 | 
			
		||||
//
 | 
			
		||||
//       ___  _____  ____
 | 
			
		||||
//      / _ \/  _/ |/_/ /____ ______ _
 | 
			
		||||
//     / ___// /_>  </ __/ -_) __/  ' \
 | 
			
		||||
//    /_/  /___/_/|_|\__/\__/_/ /_/_/_/
 | 
			
		||||
//
 | 
			
		||||
// This package is licensed under the Mozilla Public License v2.0.
 | 
			
		||||
package ansimage
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										3
									
								
								lib/htmlparser/doc.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								lib/htmlparser/doc.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
// Package htmlparser contains a HTML parsing system similar to html.parser.HTMLParser in Python 3.
 | 
			
		||||
// The parser uses x/net/html.Tokenizer in the background.
 | 
			
		||||
package htmlparser
 | 
			
		||||
							
								
								
									
										142
									
								
								lib/htmlparser/htmlparser.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								lib/htmlparser/htmlparser.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,142 @@
 | 
			
		||||
// 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 htmlparser
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"io"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/net/html"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// HTMLProcessor contains the functions to process parsed HTML data.
 | 
			
		||||
type HTMLProcessor interface {
 | 
			
		||||
	// Preprocess is called before the parsing is started.
 | 
			
		||||
	Preprocess()
 | 
			
		||||
 | 
			
		||||
	// HandleStartTag is called with the tag name and attributes when
 | 
			
		||||
	// the parser encounters a StartTagToken, except if the tag is
 | 
			
		||||
	// always self-closing.
 | 
			
		||||
	HandleStartTag(tagName string, attrs map[string]string)
 | 
			
		||||
	// HandleSelfClosingTag is called with the tag name and attributes
 | 
			
		||||
	// when the parser encounters a SelfClosingTagToken OR a StartTagToken
 | 
			
		||||
	// with a tag that's always self-closing.
 | 
			
		||||
	HandleSelfClosingTag(tagName string, attrs map[string]string)
 | 
			
		||||
	// HandleText is called with the text when the parser encounters
 | 
			
		||||
	// a TextToken.
 | 
			
		||||
	HandleText(text string)
 | 
			
		||||
	// HandleEndTag is called with the tag name when the parser encounters
 | 
			
		||||
	// an EndTagToken.
 | 
			
		||||
	HandleEndTag(tagName string)
 | 
			
		||||
 | 
			
		||||
	// ReceiveError is called with the error when the parser encounters
 | 
			
		||||
	// an ErrorToken that IS NOT io.EOF.
 | 
			
		||||
	ReceiveError(err error)
 | 
			
		||||
 | 
			
		||||
	// Postprocess is called after parsing is completed successfully.
 | 
			
		||||
	// An unsuccessful parsing will trigger a ReceiveError() call.
 | 
			
		||||
	Postprocess()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// HTMLParser wraps a net/html.Tokenizer and a HTMLProcessor to call
 | 
			
		||||
// the HTMLProcessor with data from the Tokenizer.
 | 
			
		||||
type HTMLParser struct {
 | 
			
		||||
	*html.Tokenizer
 | 
			
		||||
	processor HTMLProcessor
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewHTMLParserFromTokenizer creates a new HTMLParser from an existing html Tokenizer.
 | 
			
		||||
func NewHTMLParserFromTokenizer(z *html.Tokenizer, processor HTMLProcessor) HTMLParser {
 | 
			
		||||
	return HTMLParser{
 | 
			
		||||
		z,
 | 
			
		||||
		processor,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewHTMLParserFromReader creates a Tokenizer with the given io.Reader and
 | 
			
		||||
// then uses that to create a new HTMLParser.
 | 
			
		||||
func NewHTMLParserFromReader(reader io.Reader, processor HTMLProcessor) HTMLParser {
 | 
			
		||||
	return NewHTMLParserFromTokenizer(html.NewTokenizer(reader), processor)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewHTMLParserFromString creates a Tokenizer with a reader of the given
 | 
			
		||||
// string and then uses that to create a new HTMLParser.
 | 
			
		||||
func NewHTMLParserFromString(html string, processor HTMLProcessor) HTMLParser {
 | 
			
		||||
	return NewHTMLParserFromReader(strings.NewReader(html), processor)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SelfClosingTags is the list of tags that always call
 | 
			
		||||
// HTMLProcessor.HandleSelfClosingTag() even if it is encountered
 | 
			
		||||
// as a html.StartTagToken rather than html.SelfClosingTagToken.
 | 
			
		||||
var SelfClosingTags = []string{"img", "br", "hr", "area", "base", "basefont", "input", "link", "meta"}
 | 
			
		||||
 | 
			
		||||
func (parser HTMLParser) mapAttrs() map[string]string {
 | 
			
		||||
	attrs := make(map[string]string)
 | 
			
		||||
	hasMore := true
 | 
			
		||||
	for hasMore {
 | 
			
		||||
		var key, val []byte
 | 
			
		||||
		key, val, hasMore = parser.TagAttr()
 | 
			
		||||
		attrs[string(key)] = string(val)
 | 
			
		||||
	}
 | 
			
		||||
	return attrs
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (parser HTMLParser) isSelfClosing(tag string) bool {
 | 
			
		||||
	for _, selfClosingTag := range SelfClosingTags {
 | 
			
		||||
		if tag == selfClosingTag {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Process parses the HTML using the tokenizer in this parser and
 | 
			
		||||
// calls the appropriate functions of the HTML processor.
 | 
			
		||||
func (parser HTMLParser) Process() {
 | 
			
		||||
	parser.processor.Preprocess()
 | 
			
		||||
Loop:
 | 
			
		||||
	for {
 | 
			
		||||
		tt := parser.Next()
 | 
			
		||||
		switch tt {
 | 
			
		||||
		case html.ErrorToken:
 | 
			
		||||
			if parser.Err() != io.EOF {
 | 
			
		||||
				parser.processor.ReceiveError(parser.Err())
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			break Loop
 | 
			
		||||
		case html.TextToken:
 | 
			
		||||
			parser.processor.HandleText(string(parser.Text()))
 | 
			
		||||
		case html.StartTagToken, html.SelfClosingTagToken:
 | 
			
		||||
			tagb, _ := parser.TagName()
 | 
			
		||||
			attrs := parser.mapAttrs()
 | 
			
		||||
			tag := string(tagb)
 | 
			
		||||
 | 
			
		||||
			selfClosing := tt == html.SelfClosingTagToken || parser.isSelfClosing(tag)
 | 
			
		||||
 | 
			
		||||
			if selfClosing {
 | 
			
		||||
				parser.processor.HandleSelfClosingTag(tag, attrs)
 | 
			
		||||
			} else {
 | 
			
		||||
				parser.processor.HandleStartTag(tag, attrs)
 | 
			
		||||
			}
 | 
			
		||||
		case html.EndTagToken:
 | 
			
		||||
			tagb, _ := parser.TagName()
 | 
			
		||||
			parser.processor.HandleEndTag(string(tagb))
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	parser.processor.Postprocess()
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										4
									
								
								lib/open/doc.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								lib/open/doc.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
// Package open contains a simple cross-platform way to open files in the program the OS wants to use.
 | 
			
		||||
//
 | 
			
		||||
// Based on https://github.com/skratchdot/open-golang
 | 
			
		||||
package open
 | 
			
		||||
							
								
								
									
										27
									
								
								lib/open/open.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								lib/open/open.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
// +build !windows,!darwin
 | 
			
		||||
 | 
			
		||||
// 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 open
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os/exec"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func Open(input string) error {
 | 
			
		||||
	return exec.Command("xdg-open", input).Start()
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										25
									
								
								lib/open/open_darwin.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								lib/open/open_darwin.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
			
		||||
// 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 open
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os/exec"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func Open(input string) error {
 | 
			
		||||
	return exec.Command("open", input).Start()
 | 
			
		||||
}
 | 
			
		||||
@@ -14,32 +14,16 @@
 | 
			
		||||
// You should have received a copy of the GNU General Public License
 | 
			
		||||
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
package debug
 | 
			
		||||
package open
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/exec"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var writer io.Writer
 | 
			
		||||
const FileProtocolHandler = "url.dll,FileProtocolHandler"
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
var RunDLL32 = filepath.Join(os.Getenv("SYSTEMROOT"), "System32", "rundll32.exe")
 | 
			
		||||
 | 
			
		||||
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...)
 | 
			
		||||
	}
 | 
			
		||||
func Open(input string) error {
 | 
			
		||||
	return exec.Command(RunDLL32, FileProtocolHandler, input).Start()
 | 
			
		||||
}
 | 
			
		||||
@@ -17,18 +17,26 @@
 | 
			
		||||
package matrix
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"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/ui/debug"
 | 
			
		||||
	"maunium.net/go/gomuks/ui/widget"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Container is a wrapper for a gomatrix Client and some other stuff.
 | 
			
		||||
@@ -221,13 +229,13 @@ func (c *Container) HandleMessage(evt *gomatrix.Event) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	message := mainView.ProcessMessageEvent(roomView, evt)
 | 
			
		||||
	message := mainView.ParseEvent(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 +263,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 {
 | 
			
		||||
@@ -279,18 +286,18 @@ func (c *Container) HandleMembership(evt *gomatrix.Event) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	message := mainView.ProcessMembershipEvent(roomView, evt)
 | 
			
		||||
	message := mainView.ParseEvent(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()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -403,3 +410,55 @@ func (c *Container) GetRoom(roomID string) *rooms.Room {
 | 
			
		||||
	}
 | 
			
		||||
	return room
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var mxcRegex = regexp.MustCompile("mxc://(.+)/(.+)")
 | 
			
		||||
 | 
			
		||||
func (c *Container) Download(mxcURL string) (data []byte, hs, id string, err error) {
 | 
			
		||||
	parts := mxcRegex.FindStringSubmatch(mxcURL)
 | 
			
		||||
	if parts == nil || len(parts) != 3 {
 | 
			
		||||
		err = fmt.Errorf("invalid matrix content URL")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	hs = parts[1]
 | 
			
		||||
	id = parts[2]
 | 
			
		||||
 | 
			
		||||
	cacheFile := c.GetCachePath(hs, id)
 | 
			
		||||
	if _, err = os.Stat(cacheFile); err != nil {
 | 
			
		||||
		data, err = ioutil.ReadFile(cacheFile)
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dlURL, _ := url.Parse(c.client.HomeserverURL.String())
 | 
			
		||||
	dlURL.Path = path.Join(dlURL.Path, "/_matrix/media/v1/download", hs, id)
 | 
			
		||||
 | 
			
		||||
	var resp *http.Response
 | 
			
		||||
	resp, err = c.client.Client.Get(dlURL.String())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	defer resp.Body.Close()
 | 
			
		||||
 | 
			
		||||
	var buf bytes.Buffer
 | 
			
		||||
	_, err = io.Copy(&buf, resp.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	data = buf.Bytes()
 | 
			
		||||
 | 
			
		||||
	err = ioutil.WriteFile(cacheFile, data, 0600)
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Container) GetCachePath(homeserver, fileID string) string {
 | 
			
		||||
	dir := filepath.Join(c.config.MediaDir, homeserver)
 | 
			
		||||
 | 
			
		||||
	err := os.MkdirAll(dir, 0700)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return ""
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return filepath.Join(dir, fileID)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -94,11 +94,7 @@ func (room *Room) UpdateState(event *gomatrix.Event) {
 | 
			
		||||
		room.memberCache = nil
 | 
			
		||||
		room.firstMemberCache = ""
 | 
			
		||||
		fallthrough
 | 
			
		||||
	case "m.room.name":
 | 
			
		||||
		fallthrough
 | 
			
		||||
	case "m.room.canonical_alias":
 | 
			
		||||
		fallthrough
 | 
			
		||||
	case "m.room.alias":
 | 
			
		||||
	case "m.room.name", "m.room.canonical_alias", "m.room.alias":
 | 
			
		||||
		room.nameCache = ""
 | 
			
		||||
	case "m.room.topic":
 | 
			
		||||
		room.topicCache = ""
 | 
			
		||||
 
 | 
			
		||||
@@ -1,2 +0,0 @@
 | 
			
		||||
// Package debug contains utilities to display debug messages while running an interactive tview program.
 | 
			
		||||
package debug
 | 
			
		||||
							
								
								
									
										464
									
								
								ui/message-view.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										464
									
								
								ui/message-view.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,464 @@
 | 
			
		||||
// gomuks - A terminal Matrix client written in Go.
 | 
			
		||||
// Copyright (C) 2018 Tulir Asokan
 | 
			
		||||
//
 | 
			
		||||
// This program is free software: you can redistribute it and/or modify
 | 
			
		||||
// it under the terms of the GNU General Public License as published by
 | 
			
		||||
// the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
// (at your option) any later version.
 | 
			
		||||
//
 | 
			
		||||
// This program is distributed in the hope that it will be useful,
 | 
			
		||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
// GNU General Public License for more details.
 | 
			
		||||
//
 | 
			
		||||
// You should have received a copy of the GNU General Public License
 | 
			
		||||
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
package ui
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/gob"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"math"
 | 
			
		||||
	"os"
 | 
			
		||||
 | 
			
		||||
	"maunium.net/go/gomuks/debug"
 | 
			
		||||
	"maunium.net/go/gomuks/interface"
 | 
			
		||||
	"maunium.net/go/gomuks/lib/open"
 | 
			
		||||
	"maunium.net/go/gomuks/ui/messages"
 | 
			
		||||
	"maunium.net/go/gomuks/ui/messages/tstring"
 | 
			
		||||
	"maunium.net/go/gomuks/ui/widget"
 | 
			
		||||
	"maunium.net/go/tcell"
 | 
			
		||||
	"maunium.net/go/tview"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type MessageView struct {
 | 
			
		||||
	*tview.Box
 | 
			
		||||
 | 
			
		||||
	parent *RoomView
 | 
			
		||||
 | 
			
		||||
	ScrollOffset    int
 | 
			
		||||
	MaxSenderWidth  int
 | 
			
		||||
	DateFormat      string
 | 
			
		||||
	TimestampFormat string
 | 
			
		||||
	TimestampWidth  int
 | 
			
		||||
	LoadingMessages bool
 | 
			
		||||
 | 
			
		||||
	widestSender int
 | 
			
		||||
	prevWidth    int
 | 
			
		||||
	prevHeight   int
 | 
			
		||||
	prevMsgCount int
 | 
			
		||||
 | 
			
		||||
	messageIDs map[string]messages.UIMessage
 | 
			
		||||
	messages   []messages.UIMessage
 | 
			
		||||
 | 
			
		||||
	textBuffer []tstring.TString
 | 
			
		||||
	metaBuffer []ifc.MessageMeta
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewMessageView(parent *RoomView) *MessageView {
 | 
			
		||||
	return &MessageView{
 | 
			
		||||
		Box:    tview.NewBox(),
 | 
			
		||||
		parent: parent,
 | 
			
		||||
 | 
			
		||||
		MaxSenderWidth: 15,
 | 
			
		||||
		TimestampWidth: len(messages.TimeFormat),
 | 
			
		||||
		ScrollOffset:   0,
 | 
			
		||||
 | 
			
		||||
		messages:   make([]messages.UIMessage, 0),
 | 
			
		||||
		messageIDs: make(map[string]messages.UIMessage),
 | 
			
		||||
		textBuffer: make([]tstring.TString, 0),
 | 
			
		||||
		metaBuffer: make([]ifc.MessageMeta, 0),
 | 
			
		||||
 | 
			
		||||
		widestSender: 5,
 | 
			
		||||
		prevWidth:    -1,
 | 
			
		||||
		prevHeight:   -1,
 | 
			
		||||
		prevMsgCount: -1,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MessageView) SaveHistory(path string) error {
 | 
			
		||||
	file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0600)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer file.Close()
 | 
			
		||||
 | 
			
		||||
	enc := gob.NewEncoder(file)
 | 
			
		||||
	err = enc.Encode(view.messages)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MessageView) LoadHistory(gmx ifc.Gomuks, path string) (int, error) {
 | 
			
		||||
	file, err := os.OpenFile(path, os.O_RDONLY, 0600)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if os.IsNotExist(err) {
 | 
			
		||||
			return 0, nil
 | 
			
		||||
		}
 | 
			
		||||
		return -1, err
 | 
			
		||||
	}
 | 
			
		||||
	defer file.Close()
 | 
			
		||||
 | 
			
		||||
	var msgs []messages.UIMessage
 | 
			
		||||
 | 
			
		||||
	dec := gob.NewDecoder(file)
 | 
			
		||||
	err = dec.Decode(&msgs)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return -1, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	view.messages = make([]messages.UIMessage, len(msgs))
 | 
			
		||||
	indexOffset := 0
 | 
			
		||||
	for index, message := range msgs {
 | 
			
		||||
		if message != nil {
 | 
			
		||||
			view.messages[index-indexOffset] = message
 | 
			
		||||
			view.updateWidestSender(message.Sender())
 | 
			
		||||
			message.RegisterGomuks(gmx)
 | 
			
		||||
		} else {
 | 
			
		||||
			indexOffset++
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return len(view.messages), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MessageView) updateWidestSender(sender string) {
 | 
			
		||||
	if len(sender) > view.widestSender {
 | 
			
		||||
		view.widestSender = len(sender)
 | 
			
		||||
		if view.widestSender > view.MaxSenderWidth {
 | 
			
		||||
			view.widestSender = view.MaxSenderWidth
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MessageView) UpdateMessageID(ifcMessage ifc.Message, newID string) {
 | 
			
		||||
	message, ok := ifcMessage.(messages.UIMessage)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		debug.Print("[Warning] Passed non-UIMessage ifc.Message object to UpdateMessageID().")
 | 
			
		||||
		debug.PrintStack()
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	delete(view.messageIDs, message.ID())
 | 
			
		||||
	message.SetID(newID)
 | 
			
		||||
	view.messageIDs[message.ID()] = message
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MessageView) AddMessage(ifcMessage ifc.Message, direction ifc.MessageDirection) {
 | 
			
		||||
	if ifcMessage == nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	message, ok := ifcMessage.(messages.UIMessage)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		debug.Print("[Warning] Passed non-UIMessage ifc.Message object to AddMessage().")
 | 
			
		||||
		debug.PrintStack()
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	oldMsg, messageExists := view.messageIDs[message.ID()]
 | 
			
		||||
	if messageExists {
 | 
			
		||||
		oldMsg.CopyFrom(message)
 | 
			
		||||
		message = oldMsg
 | 
			
		||||
		direction = ifc.IgnoreMessage
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	view.updateWidestSender(message.Sender())
 | 
			
		||||
 | 
			
		||||
	_, _, width, _ := view.GetInnerRect()
 | 
			
		||||
	width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap
 | 
			
		||||
	message.CalculateBuffer(width)
 | 
			
		||||
 | 
			
		||||
	if direction == ifc.AppendMessage {
 | 
			
		||||
		if view.ScrollOffset > 0 {
 | 
			
		||||
			view.ScrollOffset += message.Height()
 | 
			
		||||
		}
 | 
			
		||||
		view.messages = append(view.messages, message)
 | 
			
		||||
		view.appendBuffer(message)
 | 
			
		||||
	} else if direction == ifc.PrependMessage {
 | 
			
		||||
		view.messages = append([]messages.UIMessage{message}, view.messages...)
 | 
			
		||||
	} else {
 | 
			
		||||
		view.replaceBuffer(message)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	view.messageIDs[message.ID()] = message
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MessageView) appendBuffer(message messages.UIMessage) {
 | 
			
		||||
	if len(view.metaBuffer) > 0 {
 | 
			
		||||
		prevMeta := view.metaBuffer[len(view.metaBuffer)-1]
 | 
			
		||||
		if prevMeta != nil && prevMeta.FormatDate() != message.FormatDate() {
 | 
			
		||||
			view.textBuffer = append(view.textBuffer, tstring.NewColorTString(
 | 
			
		||||
				fmt.Sprintf("Date changed to %s", message.FormatDate()),
 | 
			
		||||
				tcell.ColorGreen))
 | 
			
		||||
			view.metaBuffer = append(view.metaBuffer, &messages.BasicMeta{
 | 
			
		||||
				BTimestampColor: tcell.ColorDefault, BTextColor: tcell.ColorGreen})
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	view.textBuffer = append(view.textBuffer, message.Buffer()...)
 | 
			
		||||
	for range message.Buffer() {
 | 
			
		||||
		view.metaBuffer = append(view.metaBuffer, message)
 | 
			
		||||
	}
 | 
			
		||||
	view.prevMsgCount++
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MessageView) replaceBuffer(message messages.UIMessage) {
 | 
			
		||||
	start := -1
 | 
			
		||||
	end := -1
 | 
			
		||||
	for index, meta := range view.metaBuffer {
 | 
			
		||||
		if meta == message {
 | 
			
		||||
			if start == -1 {
 | 
			
		||||
				start = index
 | 
			
		||||
			}
 | 
			
		||||
			end = index
 | 
			
		||||
		} else if start != -1 {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(view.textBuffer) > end {
 | 
			
		||||
		end++
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	view.textBuffer = append(append(view.textBuffer[0:start], message.Buffer()...), view.textBuffer[end:]...)
 | 
			
		||||
	if len(message.Buffer()) != end-start+1 {
 | 
			
		||||
		metaBuffer := view.metaBuffer[0:start]
 | 
			
		||||
		for range message.Buffer() {
 | 
			
		||||
			metaBuffer = append(metaBuffer, message)
 | 
			
		||||
		}
 | 
			
		||||
		view.metaBuffer = append(metaBuffer, view.metaBuffer[end:]...)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MessageView) recalculateBuffers() {
 | 
			
		||||
	_, _, width, height := view.GetInnerRect()
 | 
			
		||||
 | 
			
		||||
	width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap
 | 
			
		||||
	recalculateMessageBuffers := width != view.prevWidth
 | 
			
		||||
	if height != view.prevHeight || recalculateMessageBuffers || len(view.messages) != view.prevMsgCount {
 | 
			
		||||
		view.textBuffer = []tstring.TString{}
 | 
			
		||||
		view.metaBuffer = []ifc.MessageMeta{}
 | 
			
		||||
		view.prevMsgCount = 0
 | 
			
		||||
		for i, message := range view.messages {
 | 
			
		||||
			if message == nil {
 | 
			
		||||
				debug.Print("O.o found nil message at", i)
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
			if recalculateMessageBuffers {
 | 
			
		||||
				message.CalculateBuffer(width)
 | 
			
		||||
			}
 | 
			
		||||
			view.appendBuffer(message)
 | 
			
		||||
		}
 | 
			
		||||
		view.prevHeight = height
 | 
			
		||||
		view.prevWidth = width
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MessageView) HandleClick(x, y int, button tcell.ButtonMask) bool {
 | 
			
		||||
	if button != tcell.Button1 {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, _, _, height := view.GetRect()
 | 
			
		||||
	line := view.TotalHeight() - view.ScrollOffset - height + y
 | 
			
		||||
	if line < 0 || line >= view.TotalHeight() {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	message := view.metaBuffer[line]
 | 
			
		||||
	var prevMessage ifc.MessageMeta
 | 
			
		||||
	if line > 0 {
 | 
			
		||||
		prevMessage = view.metaBuffer[line-1]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	usernameX := view.TimestampWidth + TimestampSenderGap
 | 
			
		||||
	messageX := usernameX + view.widestSender + SenderMessageGap
 | 
			
		||||
	if x >= messageX {
 | 
			
		||||
		switch message := message.(type) {
 | 
			
		||||
		case *messages.ImageMessage:
 | 
			
		||||
			open.Open(message.Path())
 | 
			
		||||
		case messages.UIMessage:
 | 
			
		||||
			debug.Print("Message clicked:", message.NotificationContent())
 | 
			
		||||
		}
 | 
			
		||||
	} else if x >= usernameX {
 | 
			
		||||
		uiMessage, ok := message.(messages.UIMessage)
 | 
			
		||||
		if !ok {
 | 
			
		||||
			return false
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		prevUIMessage, _ := prevMessage.(messages.UIMessage)
 | 
			
		||||
		if prevUIMessage != nil && prevUIMessage.Sender() == uiMessage.Sender() {
 | 
			
		||||
			return false
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		sender := []rune(uiMessage.Sender())
 | 
			
		||||
		if len(sender) == 0 {
 | 
			
		||||
			return false
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		cursorPos := view.parent.input.GetCursorOffset()
 | 
			
		||||
		text := []rune(view.parent.input.GetText())
 | 
			
		||||
		var newText []rune
 | 
			
		||||
		if cursorPos == 0 {
 | 
			
		||||
			newText = append(sender, ':', ' ')
 | 
			
		||||
			newText = append(newText, text...)
 | 
			
		||||
		} else {
 | 
			
		||||
			newText = append(text[0:cursorPos], sender...)
 | 
			
		||||
			newText = append(newText, ' ')
 | 
			
		||||
			newText = append(newText, text[cursorPos:]...)
 | 
			
		||||
		}
 | 
			
		||||
		view.parent.input.SetText(string(newText))
 | 
			
		||||
		view.parent.input.SetCursorOffset(cursorPos + len(newText) - len(text))
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const PaddingAtTop = 5
 | 
			
		||||
 | 
			
		||||
func (view *MessageView) AddScrollOffset(diff int) {
 | 
			
		||||
	_, _, _, height := view.GetInnerRect()
 | 
			
		||||
 | 
			
		||||
	totalHeight := view.TotalHeight()
 | 
			
		||||
	if diff >= 0 && view.ScrollOffset+diff >= totalHeight-height+PaddingAtTop {
 | 
			
		||||
		view.ScrollOffset = totalHeight - height + PaddingAtTop
 | 
			
		||||
	} else {
 | 
			
		||||
		view.ScrollOffset += diff
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if view.ScrollOffset > totalHeight-height+PaddingAtTop {
 | 
			
		||||
		view.ScrollOffset = totalHeight - height + PaddingAtTop
 | 
			
		||||
	}
 | 
			
		||||
	if view.ScrollOffset < 0 {
 | 
			
		||||
		view.ScrollOffset = 0
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MessageView) Height() int {
 | 
			
		||||
	_, _, _, height := view.GetInnerRect()
 | 
			
		||||
	return height
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MessageView) TotalHeight() int {
 | 
			
		||||
	return len(view.textBuffer)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MessageView) IsAtTop() bool {
 | 
			
		||||
	_, _, _, height := view.GetInnerRect()
 | 
			
		||||
	totalHeight := len(view.textBuffer)
 | 
			
		||||
	return view.ScrollOffset >= totalHeight-height+PaddingAtTop
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	TimestampSenderGap = 1
 | 
			
		||||
	SenderSeparatorGap = 1
 | 
			
		||||
	SenderMessageGap   = 3
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func getScrollbarStyle(scrollbarHere, isTop, isBottom bool) (char rune, style tcell.Style) {
 | 
			
		||||
	char = '│'
 | 
			
		||||
	style = tcell.StyleDefault
 | 
			
		||||
	if scrollbarHere {
 | 
			
		||||
		style = style.Foreground(tcell.ColorGreen)
 | 
			
		||||
	}
 | 
			
		||||
	if isTop {
 | 
			
		||||
		if scrollbarHere {
 | 
			
		||||
			char = '╥'
 | 
			
		||||
		} else {
 | 
			
		||||
			char = '┬'
 | 
			
		||||
		}
 | 
			
		||||
	} else if isBottom {
 | 
			
		||||
		if scrollbarHere {
 | 
			
		||||
			char = '╨'
 | 
			
		||||
		} else {
 | 
			
		||||
			char = '┴'
 | 
			
		||||
		}
 | 
			
		||||
	} else if scrollbarHere {
 | 
			
		||||
		char = '║'
 | 
			
		||||
	}
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MessageView) Draw(screen tcell.Screen) {
 | 
			
		||||
	view.Box.Draw(screen)
 | 
			
		||||
 | 
			
		||||
	x, y, _, height := view.GetInnerRect()
 | 
			
		||||
	view.recalculateBuffers()
 | 
			
		||||
 | 
			
		||||
	if view.TotalHeight() == 0 {
 | 
			
		||||
		widget.WriteLineSimple(screen, "It's quite empty in here.", x, y+height)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	usernameX := x + view.TimestampWidth + TimestampSenderGap
 | 
			
		||||
	messageX := usernameX + view.widestSender + SenderMessageGap
 | 
			
		||||
	separatorX := usernameX + view.widestSender + SenderSeparatorGap
 | 
			
		||||
 | 
			
		||||
	indexOffset := view.TotalHeight() - view.ScrollOffset - height
 | 
			
		||||
	if indexOffset <= -PaddingAtTop {
 | 
			
		||||
		message := "Scroll up to load more messages."
 | 
			
		||||
		if view.LoadingMessages {
 | 
			
		||||
			message = "Loading more messages..."
 | 
			
		||||
		}
 | 
			
		||||
		widget.WriteLineSimpleColor(screen, message, messageX, y, tcell.ColorGreen)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(view.textBuffer) != len(view.metaBuffer) {
 | 
			
		||||
		debug.Printf("Unexpected text/meta buffer length mismatch: %d != %d.", len(view.textBuffer), len(view.metaBuffer))
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var scrollBarHeight, scrollBarPos int
 | 
			
		||||
	// Black magic (aka math) used to figure out where the scroll bar should be put.
 | 
			
		||||
	{
 | 
			
		||||
		viewportHeight := float64(height)
 | 
			
		||||
		contentHeight := float64(view.TotalHeight())
 | 
			
		||||
 | 
			
		||||
		scrollBarHeight = int(math.Ceil(viewportHeight / (contentHeight / viewportHeight)))
 | 
			
		||||
 | 
			
		||||
		scrollBarPos = height - int(math.Round(float64(view.ScrollOffset)/contentHeight*viewportHeight))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var prevMeta ifc.MessageMeta
 | 
			
		||||
	firstLine := true
 | 
			
		||||
	skippedLines := 0
 | 
			
		||||
 | 
			
		||||
	for line := 0; line < height; line++ {
 | 
			
		||||
		index := indexOffset + line
 | 
			
		||||
		if index < 0 {
 | 
			
		||||
			skippedLines++
 | 
			
		||||
			continue
 | 
			
		||||
		} else if index >= view.TotalHeight() {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		showScrollbar := line-skippedLines >= scrollBarPos-scrollBarHeight && line-skippedLines < scrollBarPos
 | 
			
		||||
		isTop := firstLine && view.ScrollOffset+height >= view.TotalHeight()
 | 
			
		||||
		isBottom := line == height-1 && view.ScrollOffset == 0
 | 
			
		||||
 | 
			
		||||
		borderChar, borderStyle := getScrollbarStyle(showScrollbar, isTop, isBottom)
 | 
			
		||||
 | 
			
		||||
		firstLine = false
 | 
			
		||||
 | 
			
		||||
		screen.SetContent(separatorX, y+line, borderChar, nil, borderStyle)
 | 
			
		||||
 | 
			
		||||
		text, meta := view.textBuffer[index], view.metaBuffer[index]
 | 
			
		||||
		if meta != prevMeta {
 | 
			
		||||
			if len(meta.FormatTime()) > 0 {
 | 
			
		||||
				widget.WriteLineSimpleColor(screen, meta.FormatTime(), x, y+line, meta.TimestampColor())
 | 
			
		||||
			}
 | 
			
		||||
			if prevMeta == nil || meta.Sender() != prevMeta.Sender() {
 | 
			
		||||
				widget.WriteLineColor(
 | 
			
		||||
					screen, tview.AlignRight, meta.Sender(),
 | 
			
		||||
					usernameX, y+line, view.widestSender,
 | 
			
		||||
					meta.SenderColor())
 | 
			
		||||
			}
 | 
			
		||||
			prevMeta = meta
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		text.Draw(screen, messageX, y+line)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										234
									
								
								ui/messages/base.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										234
									
								
								ui/messages/base.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,234 @@
 | 
			
		||||
// gomuks - A terminal Matrix client written in Go.
 | 
			
		||||
// Copyright (C) 2018 Tulir Asokan
 | 
			
		||||
//
 | 
			
		||||
// This program is free software: you can redistribute it and/or modify
 | 
			
		||||
// it under the terms of the GNU General Public License as published by
 | 
			
		||||
// the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
// (at your option) any later version.
 | 
			
		||||
//
 | 
			
		||||
// This program is distributed in the hope that it will be useful,
 | 
			
		||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
// GNU General Public License for more details.
 | 
			
		||||
//
 | 
			
		||||
// You should have received a copy of the GNU General Public License
 | 
			
		||||
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
package messages
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/gob"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"maunium.net/go/gomuks/interface"
 | 
			
		||||
	"maunium.net/go/gomuks/ui/messages/tstring"
 | 
			
		||||
	"maunium.net/go/gomuks/ui/widget"
 | 
			
		||||
	"maunium.net/go/tcell"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	gob.Register(&BaseMessage{})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type BaseMessage struct {
 | 
			
		||||
	MsgID           string
 | 
			
		||||
	MsgType         string
 | 
			
		||||
	MsgSender       string
 | 
			
		||||
	MsgSenderColor  tcell.Color
 | 
			
		||||
	MsgTimestamp    time.Time
 | 
			
		||||
	MsgState        ifc.MessageState
 | 
			
		||||
	MsgIsHighlight  bool
 | 
			
		||||
	MsgIsService    bool
 | 
			
		||||
	buffer          []tstring.TString
 | 
			
		||||
	prevBufferWidth int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func newBaseMessage(id, sender, msgtype string, timestamp time.Time) BaseMessage {
 | 
			
		||||
	return BaseMessage{
 | 
			
		||||
		MsgSender:       sender,
 | 
			
		||||
		MsgTimestamp:    timestamp,
 | 
			
		||||
		MsgSenderColor:  widget.GetHashColor(sender),
 | 
			
		||||
		MsgType:         msgtype,
 | 
			
		||||
		MsgID:           id,
 | 
			
		||||
		prevBufferWidth: 0,
 | 
			
		||||
		MsgState:        ifc.MessageStateDefault,
 | 
			
		||||
		MsgIsHighlight:  false,
 | 
			
		||||
		MsgIsService:    false,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (msg *BaseMessage) RegisterGomuks(gmx ifc.Gomuks) {}
 | 
			
		||||
 | 
			
		||||
// CopyFrom replaces the content of this message object with the content of the given object.
 | 
			
		||||
func (msg *BaseMessage) CopyFrom(from ifc.MessageMeta) {
 | 
			
		||||
	msg.MsgSender = from.Sender()
 | 
			
		||||
	msg.MsgSenderColor = from.SenderColor()
 | 
			
		||||
 | 
			
		||||
	fromMsg, ok := from.(UIMessage)
 | 
			
		||||
	if ok {
 | 
			
		||||
		msg.MsgSender = fromMsg.RealSender()
 | 
			
		||||
		msg.MsgID = fromMsg.ID()
 | 
			
		||||
		msg.MsgType = fromMsg.Type()
 | 
			
		||||
		msg.MsgTimestamp = fromMsg.Timestamp()
 | 
			
		||||
		msg.MsgState = fromMsg.State()
 | 
			
		||||
		msg.MsgIsService = fromMsg.IsService()
 | 
			
		||||
		msg.MsgIsHighlight = fromMsg.IsHighlight()
 | 
			
		||||
		msg.buffer = nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Sender gets the string that should be displayed as the sender of this message.
 | 
			
		||||
//
 | 
			
		||||
// If the message is being sent, the sender is "Sending...".
 | 
			
		||||
// If sending has failed, the sender is "Error".
 | 
			
		||||
// If the message is an emote, the sender is blank.
 | 
			
		||||
// In any other case, the sender is the display name of the user who sent the message.
 | 
			
		||||
func (msg *BaseMessage) Sender() string {
 | 
			
		||||
	switch msg.MsgState {
 | 
			
		||||
	case ifc.MessageStateSending:
 | 
			
		||||
		return "Sending..."
 | 
			
		||||
	case ifc.MessageStateFailed:
 | 
			
		||||
		return "Error"
 | 
			
		||||
	}
 | 
			
		||||
	switch msg.MsgType {
 | 
			
		||||
	case "m.emote":
 | 
			
		||||
		// Emotes don't show a separate sender, it's included in the buffer.
 | 
			
		||||
		return ""
 | 
			
		||||
	default:
 | 
			
		||||
		return msg.MsgSender
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (msg *BaseMessage) RealSender() string {
 | 
			
		||||
	return msg.MsgSender
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (msg *BaseMessage) getStateSpecificColor() tcell.Color {
 | 
			
		||||
	switch msg.MsgState {
 | 
			
		||||
	case ifc.MessageStateSending:
 | 
			
		||||
		return tcell.ColorGray
 | 
			
		||||
	case ifc.MessageStateFailed:
 | 
			
		||||
		return tcell.ColorRed
 | 
			
		||||
	case ifc.MessageStateDefault:
 | 
			
		||||
		fallthrough
 | 
			
		||||
	default:
 | 
			
		||||
		return tcell.ColorDefault
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SenderColor returns the color the name of the sender should be shown in.
 | 
			
		||||
//
 | 
			
		||||
// If the message is being sent, the color is gray.
 | 
			
		||||
// If sending has failed, the color is red.
 | 
			
		||||
//
 | 
			
		||||
// In any other case, the color is whatever is specified in the Message struct.
 | 
			
		||||
// Usually that means it is the hash-based color of the sender (see ui/widget/color.go)
 | 
			
		||||
func (msg *BaseMessage) SenderColor() tcell.Color {
 | 
			
		||||
	stateColor := msg.getStateSpecificColor()
 | 
			
		||||
	switch {
 | 
			
		||||
	case stateColor != tcell.ColorDefault:
 | 
			
		||||
		return stateColor
 | 
			
		||||
	case msg.MsgIsService:
 | 
			
		||||
		return tcell.ColorGray
 | 
			
		||||
	default:
 | 
			
		||||
		return msg.MsgSenderColor
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TextColor returns the color the actual content of the message should be shown in.
 | 
			
		||||
func (msg *BaseMessage) TextColor() tcell.Color {
 | 
			
		||||
	stateColor := msg.getStateSpecificColor()
 | 
			
		||||
	switch {
 | 
			
		||||
	case stateColor != tcell.ColorDefault:
 | 
			
		||||
		return stateColor
 | 
			
		||||
	case msg.MsgIsService, msg.MsgType == "m.notice":
 | 
			
		||||
		return tcell.ColorGray
 | 
			
		||||
	case msg.MsgIsHighlight:
 | 
			
		||||
		return tcell.ColorYellow
 | 
			
		||||
	case msg.MsgType == "m.room.member":
 | 
			
		||||
		return tcell.ColorGreen
 | 
			
		||||
	default:
 | 
			
		||||
		return tcell.ColorDefault
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TimestampColor returns the color the timestamp should be shown in.
 | 
			
		||||
//
 | 
			
		||||
// As with SenderColor(), messages being sent and messages that failed to be sent are
 | 
			
		||||
// gray and red respectively.
 | 
			
		||||
//
 | 
			
		||||
// However, other messages are the default color instead of a color stored in the struct.
 | 
			
		||||
func (msg *BaseMessage) TimestampColor() tcell.Color {
 | 
			
		||||
	return msg.getStateSpecificColor()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Buffer returns the computed text buffer.
 | 
			
		||||
//
 | 
			
		||||
// The buffer contains the text of the message split into lines with a maximum
 | 
			
		||||
// width of whatever was provided to CalculateBuffer().
 | 
			
		||||
//
 | 
			
		||||
// N.B. This will NOT automatically calculate the buffer if it hasn't been
 | 
			
		||||
//      calculated already, as that requires the target width.
 | 
			
		||||
func (msg *BaseMessage) Buffer() []tstring.TString {
 | 
			
		||||
	return msg.buffer
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Height returns the number of rows in the computed buffer (see Buffer()).
 | 
			
		||||
func (msg *BaseMessage) Height() int {
 | 
			
		||||
	return len(msg.buffer)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Timestamp returns the full timestamp when the message was sent.
 | 
			
		||||
func (msg *BaseMessage) Timestamp() time.Time {
 | 
			
		||||
	return msg.MsgTimestamp
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FormatTime returns the formatted time when the message was sent.
 | 
			
		||||
func (msg *BaseMessage) FormatTime() string {
 | 
			
		||||
	return msg.MsgTimestamp.Format(TimeFormat)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FormatDate returns the formatted date when the message was sent.
 | 
			
		||||
func (msg *BaseMessage) FormatDate() string {
 | 
			
		||||
	return msg.MsgTimestamp.Format(DateFormat)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (msg *BaseMessage) ID() string {
 | 
			
		||||
	return msg.MsgID
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (msg *BaseMessage) SetID(id string) {
 | 
			
		||||
	msg.MsgID = id
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (msg *BaseMessage) Type() string {
 | 
			
		||||
	return msg.MsgType
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (msg *BaseMessage) SetType(msgtype string) {
 | 
			
		||||
	msg.MsgType = msgtype
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (msg *BaseMessage) State() ifc.MessageState {
 | 
			
		||||
	return msg.MsgState
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (msg *BaseMessage) SetState(state ifc.MessageState) {
 | 
			
		||||
	msg.MsgState = state
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (msg *BaseMessage) IsHighlight() bool {
 | 
			
		||||
	return msg.MsgIsHighlight
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (msg *BaseMessage) SetIsHighlight(isHighlight bool) {
 | 
			
		||||
	msg.MsgIsHighlight = isHighlight
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (msg *BaseMessage) IsService() bool {
 | 
			
		||||
	return msg.MsgIsService
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (msg *BaseMessage) SetIsService(isService bool) {
 | 
			
		||||
	msg.MsgIsService = isService
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										2
									
								
								ui/messages/doc.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								ui/messages/doc.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
			
		||||
// Package messages contains different message types and code to generate and render them.
 | 
			
		||||
package messages
 | 
			
		||||
							
								
								
									
										71
									
								
								ui/messages/expandedtextmessage.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								ui/messages/expandedtextmessage.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,71 @@
 | 
			
		||||
// gomuks - A terminal Matrix client written in Go.
 | 
			
		||||
// Copyright (C) 2018 Tulir Asokan
 | 
			
		||||
//
 | 
			
		||||
// This program is free software: you can redistribute it and/or modify
 | 
			
		||||
// it under the terms of the GNU General Public License as published by
 | 
			
		||||
// the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
// (at your option) any later version.
 | 
			
		||||
//
 | 
			
		||||
// This program is distributed in the hope that it will be useful,
 | 
			
		||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
// GNU General Public License for more details.
 | 
			
		||||
//
 | 
			
		||||
// You should have received a copy of the GNU General Public License
 | 
			
		||||
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
package messages
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/gob"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"maunium.net/go/gomuks/interface"
 | 
			
		||||
	"maunium.net/go/gomuks/ui/messages/tstring"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	gob.Register(&ExpandedTextMessage{})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ExpandedTextMessage struct {
 | 
			
		||||
	BaseTextMessage
 | 
			
		||||
	MsgText tstring.TString
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewExpandedTextMessage creates a new ExpandedTextMessage object with the provided values and the default state.
 | 
			
		||||
func NewExpandedTextMessage(id, sender, msgtype string, text tstring.TString, timestamp time.Time) UIMessage {
 | 
			
		||||
	return &ExpandedTextMessage{
 | 
			
		||||
		BaseTextMessage: newBaseTextMessage(id, sender, msgtype, timestamp),
 | 
			
		||||
		MsgText: text,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (msg *ExpandedTextMessage) GenerateText() tstring.TString {
 | 
			
		||||
	return msg.MsgText
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CopyFrom replaces the content of this message object with the content of the given object.
 | 
			
		||||
func (msg *ExpandedTextMessage) CopyFrom(from ifc.MessageMeta) {
 | 
			
		||||
	msg.BaseTextMessage.CopyFrom(from)
 | 
			
		||||
 | 
			
		||||
	fromExpandedMsg, ok := from.(*ExpandedTextMessage)
 | 
			
		||||
	if ok {
 | 
			
		||||
		msg.MsgText = fromExpandedMsg.MsgText
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	msg.RecalculateBuffer()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (msg *ExpandedTextMessage) NotificationContent() string {
 | 
			
		||||
	return msg.MsgText.String()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (msg *ExpandedTextMessage) CalculateBuffer(width int) {
 | 
			
		||||
	msg.BaseTextMessage.calculateBufferWithText(msg.MsgText, width)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RecalculateBuffer calculates the buffer again with the previously provided width.
 | 
			
		||||
func (msg *ExpandedTextMessage) RecalculateBuffer() {
 | 
			
		||||
	msg.CalculateBuffer(msg.prevBufferWidth)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										123
									
								
								ui/messages/imagemessage.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								ui/messages/imagemessage.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,123 @@
 | 
			
		||||
// gomuks - A terminal Matrix client written in Go.
 | 
			
		||||
// Copyright (C) 2018 Tulir Asokan
 | 
			
		||||
//
 | 
			
		||||
// This program is free software: you can redistribute it and/or modify
 | 
			
		||||
// it under the terms of the GNU General Public License as published by
 | 
			
		||||
// the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
// (at your option) any later version.
 | 
			
		||||
//
 | 
			
		||||
// This program is distributed in the hope that it will be useful,
 | 
			
		||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
// GNU General Public License for more details.
 | 
			
		||||
//
 | 
			
		||||
// You should have received a copy of the GNU General Public License
 | 
			
		||||
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
package messages
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/gob"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"image/color"
 | 
			
		||||
 | 
			
		||||
	"maunium.net/go/gomuks/debug"
 | 
			
		||||
	"maunium.net/go/gomuks/interface"
 | 
			
		||||
	"maunium.net/go/gomuks/lib/ansimage"
 | 
			
		||||
	"maunium.net/go/gomuks/ui/messages/tstring"
 | 
			
		||||
	"maunium.net/go/tcell"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	gob.Register(&ImageMessage{})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ImageMessage struct {
 | 
			
		||||
	BaseMessage
 | 
			
		||||
	Homeserver string
 | 
			
		||||
	FileID     string
 | 
			
		||||
	data       []byte
 | 
			
		||||
 | 
			
		||||
	gmx ifc.Gomuks
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewImageMessage creates a new ImageMessage object with the provided values and the default state.
 | 
			
		||||
func NewImageMessage(gmx ifc.Gomuks, id, sender, msgtype, homeserver, fileID string, data []byte, timestamp time.Time) UIMessage {
 | 
			
		||||
	return &ImageMessage{
 | 
			
		||||
		newBaseMessage(id, sender, msgtype, timestamp),
 | 
			
		||||
		homeserver,
 | 
			
		||||
		fileID,
 | 
			
		||||
		data,
 | 
			
		||||
		gmx,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (msg *ImageMessage) RegisterGomuks(gmx ifc.Gomuks) {
 | 
			
		||||
	msg.gmx = gmx
 | 
			
		||||
 | 
			
		||||
	debug.Print(len(msg.data), msg.data)
 | 
			
		||||
	if len(msg.data) == 0 {
 | 
			
		||||
		go func() {
 | 
			
		||||
			defer gmx.Recover()
 | 
			
		||||
			msg.updateData()
 | 
			
		||||
		}()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (msg *ImageMessage) NotificationContent() string {
 | 
			
		||||
	return "Sent an image"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (msg *ImageMessage) updateData() {
 | 
			
		||||
	debug.Print("Loading image:", msg.Homeserver, msg.FileID)
 | 
			
		||||
	data, _, _, err := msg.gmx.Matrix().Download(fmt.Sprintf("mxc://%s/%s", msg.Homeserver, msg.FileID))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		debug.Print("Failed to download image %s/%s: %v", msg.Homeserver, msg.FileID, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	msg.data = data
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (msg *ImageMessage) Path() string {
 | 
			
		||||
	return msg.gmx.Matrix().GetCachePath(msg.Homeserver, msg.FileID)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CopyFrom replaces the content of this message object with the content of the given object.
 | 
			
		||||
func (msg *ImageMessage) CopyFrom(from ifc.MessageMeta) {
 | 
			
		||||
	msg.BaseMessage.CopyFrom(from)
 | 
			
		||||
 | 
			
		||||
	fromImgMsg, ok := from.(*ImageMessage)
 | 
			
		||||
	if ok {
 | 
			
		||||
		msg.data = fromImgMsg.data
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	msg.RecalculateBuffer()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CalculateBuffer generates the internal buffer for this message that consists
 | 
			
		||||
// of the text of this message split into lines at most as wide as the width
 | 
			
		||||
// parameter.
 | 
			
		||||
func (msg *ImageMessage) CalculateBuffer(width int) {
 | 
			
		||||
	if width < 2 {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	image, err := ansimage.NewScaledFromReader(bytes.NewReader(msg.data), 0, width, color.Black)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		msg.buffer = []tstring.TString{tstring.NewColorTString("Failed to display image", tcell.ColorRed)}
 | 
			
		||||
		debug.Print("Failed to display image:", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	msg.buffer = image.Render()
 | 
			
		||||
	msg.prevBufferWidth = width
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RecalculateBuffer calculates the buffer again with the previously provided width.
 | 
			
		||||
func (msg *ImageMessage) RecalculateBuffer() {
 | 
			
		||||
	msg.CalculateBuffer(msg.prevBufferWidth)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										38
									
								
								ui/messages/message.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								ui/messages/message.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
			
		||||
// 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 (
 | 
			
		||||
	"maunium.net/go/gomuks/interface"
 | 
			
		||||
	"maunium.net/go/gomuks/ui/messages/tstring"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Message is a wrapper for the content and metadata of a Matrix message intended to be displayed.
 | 
			
		||||
type UIMessage interface {
 | 
			
		||||
	ifc.Message
 | 
			
		||||
 | 
			
		||||
	CalculateBuffer(width int)
 | 
			
		||||
	RecalculateBuffer()
 | 
			
		||||
	Buffer() []tstring.TString
 | 
			
		||||
	Height() int
 | 
			
		||||
 | 
			
		||||
	RealSender() string
 | 
			
		||||
	RegisterGomuks(gmx ifc.Gomuks)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const DateFormat = "January _2, 2006"
 | 
			
		||||
const TimeFormat = "15:04:05"
 | 
			
		||||
							
								
								
									
										77
									
								
								ui/messages/meta.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								ui/messages/meta.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,77 @@
 | 
			
		||||
// gomuks - A terminal Matrix client written in Go.
 | 
			
		||||
// Copyright (C) 2018 Tulir Asokan
 | 
			
		||||
//
 | 
			
		||||
// This program is free software: you can redistribute it and/or modify
 | 
			
		||||
// it under the terms of the GNU General Public License as published by
 | 
			
		||||
// the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
// (at your option) any later version.
 | 
			
		||||
//
 | 
			
		||||
// This program is distributed in the hope that it will be useful,
 | 
			
		||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
// GNU General Public License for more details.
 | 
			
		||||
//
 | 
			
		||||
// You should have received a copy of the GNU General Public License
 | 
			
		||||
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
package messages
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"maunium.net/go/tcell"
 | 
			
		||||
	"maunium.net/go/gomuks/interface"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// BasicMeta is a simple variable store implementation of MessageMeta.
 | 
			
		||||
type BasicMeta struct {
 | 
			
		||||
	BSender string
 | 
			
		||||
	BTimestamp time.Time
 | 
			
		||||
	BSenderColor, BTextColor, BTimestampColor tcell.Color
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Sender gets the string that should be displayed as the sender of this message.
 | 
			
		||||
func (meta *BasicMeta) Sender() string {
 | 
			
		||||
	return meta.BSender
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SenderColor returns the color the name of the sender should be shown in.
 | 
			
		||||
func (meta *BasicMeta) SenderColor() tcell.Color {
 | 
			
		||||
	return meta.BSenderColor
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Timestamp returns the full time when the message was sent.
 | 
			
		||||
func (meta *BasicMeta) Timestamp() time.Time {
 | 
			
		||||
	return meta.BTimestamp
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FormatTime returns the formatted time when the message was sent.
 | 
			
		||||
func (meta *BasicMeta) FormatTime() string {
 | 
			
		||||
	return meta.BTimestamp.Format(TimeFormat)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FormatDate returns the formatted date when the message was sent.
 | 
			
		||||
func (meta *BasicMeta) FormatDate() string {
 | 
			
		||||
	return meta.BTimestamp.Format(DateFormat)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TextColor returns the color the actual content of the message should be shown in.
 | 
			
		||||
func (meta *BasicMeta) TextColor() tcell.Color {
 | 
			
		||||
	return meta.BTextColor
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TimestampColor returns the color the timestamp should be shown in.
 | 
			
		||||
//
 | 
			
		||||
// This usually does not apply to the date, as it is rendered separately from the message.
 | 
			
		||||
func (meta *BasicMeta) TimestampColor() tcell.Color {
 | 
			
		||||
	return meta.BTimestampColor
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CopyFrom replaces the content of this meta object with the content of the given object.
 | 
			
		||||
func (meta *BasicMeta) CopyFrom(from ifc.MessageMeta) {
 | 
			
		||||
	meta.BSender = from.Sender()
 | 
			
		||||
	meta.BTimestamp = from.Timestamp()
 | 
			
		||||
	meta.BSenderColor = from.SenderColor()
 | 
			
		||||
	meta.BTextColor = from.TextColor()
 | 
			
		||||
	meta.BTimestampColor = from.TimestampColor()
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										186
									
								
								ui/messages/parser/htmlparser.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										186
									
								
								ui/messages/parser/htmlparser.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,186 @@
 | 
			
		||||
// gomuks - A terminal Matrix client written in Go.
 | 
			
		||||
// Copyright (C) 2018 Tulir Asokan
 | 
			
		||||
//
 | 
			
		||||
// This program is free software: you can redistribute it and/or modify
 | 
			
		||||
// it under the terms of the GNU General Public License as published by
 | 
			
		||||
// the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
// (at your option) any later version.
 | 
			
		||||
//
 | 
			
		||||
// This program is distributed in the hope that it will be useful,
 | 
			
		||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
// GNU General Public License for more details.
 | 
			
		||||
//
 | 
			
		||||
// You should have received a copy of the GNU General Public License
 | 
			
		||||
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
package parser
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"math"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"maunium.net/go/gomatrix"
 | 
			
		||||
	"maunium.net/go/gomuks/debug"
 | 
			
		||||
	"maunium.net/go/gomuks/lib/htmlparser"
 | 
			
		||||
	"maunium.net/go/gomuks/matrix/rooms"
 | 
			
		||||
	"maunium.net/go/gomuks/ui/messages/tstring"
 | 
			
		||||
	"maunium.net/go/gomuks/ui/widget"
 | 
			
		||||
	"maunium.net/go/tcell"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var matrixToURL = regexp.MustCompile("^(?:https?://)?(?:www\\.)?matrix\\.to/#/([#@!].*)")
 | 
			
		||||
 | 
			
		||||
type MatrixHTMLProcessor struct {
 | 
			
		||||
	text tstring.TString
 | 
			
		||||
 | 
			
		||||
	indent    string
 | 
			
		||||
	listType  string
 | 
			
		||||
	lineIsNew bool
 | 
			
		||||
	openTags  *TagArray
 | 
			
		||||
 | 
			
		||||
	room *rooms.Room
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (parser *MatrixHTMLProcessor) newline() {
 | 
			
		||||
	if !parser.lineIsNew {
 | 
			
		||||
		parser.text = parser.text.Append("\n" + parser.indent)
 | 
			
		||||
		parser.lineIsNew = true
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (parser *MatrixHTMLProcessor) Preprocess() {}
 | 
			
		||||
 | 
			
		||||
func (parser *MatrixHTMLProcessor) HandleText(text string) {
 | 
			
		||||
	style := tcell.StyleDefault
 | 
			
		||||
	for _, tag := range *parser.openTags {
 | 
			
		||||
		switch tag.Tag {
 | 
			
		||||
		case "b", "strong":
 | 
			
		||||
			style = style.Bold(true)
 | 
			
		||||
		case "i", "em":
 | 
			
		||||
			style = style.Italic(true)
 | 
			
		||||
		case "s", "del":
 | 
			
		||||
			style = style.Strikethrough(true)
 | 
			
		||||
		case "u", "ins":
 | 
			
		||||
			style = style.Underline(true)
 | 
			
		||||
		case "a":
 | 
			
		||||
			tag.Text += text
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !parser.openTags.Has("pre", "code") {
 | 
			
		||||
		text = strings.Replace(text, "\n", "", -1)
 | 
			
		||||
	}
 | 
			
		||||
	parser.text = parser.text.AppendStyle(text, style)
 | 
			
		||||
	parser.lineIsNew = false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (parser *MatrixHTMLProcessor) HandleStartTag(tagName string, attrs map[string]string) {
 | 
			
		||||
	tag := &TagWithMeta{Tag: tagName}
 | 
			
		||||
	switch tag.Tag {
 | 
			
		||||
	case "h1", "h2", "h3", "h4", "h5", "h6":
 | 
			
		||||
		length := int(tag.Tag[1] - '0')
 | 
			
		||||
		parser.text = parser.text.Append(strings.Repeat("#", length) + " ")
 | 
			
		||||
		parser.lineIsNew = false
 | 
			
		||||
	case "a":
 | 
			
		||||
		tag.Meta, _ = attrs["href"]
 | 
			
		||||
	case "ol", "ul":
 | 
			
		||||
		parser.listType = tag.Tag
 | 
			
		||||
	case "li":
 | 
			
		||||
		indentSize := 2
 | 
			
		||||
		if parser.listType == "ol" {
 | 
			
		||||
			list := parser.openTags.Get(parser.listType)
 | 
			
		||||
			list.Counter++
 | 
			
		||||
			parser.text = parser.text.Append(fmt.Sprintf("%d. ", list.Counter))
 | 
			
		||||
			indentSize = int(math.Log10(float64(list.Counter))+1) + len(". ")
 | 
			
		||||
		} else {
 | 
			
		||||
			parser.text = parser.text.Append("* ")
 | 
			
		||||
		}
 | 
			
		||||
		parser.indent += strings.Repeat(" ", indentSize)
 | 
			
		||||
		parser.lineIsNew = false
 | 
			
		||||
	case "blockquote":
 | 
			
		||||
		parser.indent += "> "
 | 
			
		||||
		parser.text = parser.text.Append("> ")
 | 
			
		||||
		parser.lineIsNew = false
 | 
			
		||||
	}
 | 
			
		||||
	parser.openTags.PushMeta(tag)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (parser *MatrixHTMLProcessor) HandleSelfClosingTag(tagName string, attrs map[string]string) {
 | 
			
		||||
	if tagName == "br" {
 | 
			
		||||
		parser.newline()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (parser *MatrixHTMLProcessor) HandleEndTag(tagName string) {
 | 
			
		||||
	tag := parser.openTags.Pop(tagName)
 | 
			
		||||
 | 
			
		||||
	switch tag.Tag {
 | 
			
		||||
	case "li", "blockquote":
 | 
			
		||||
		indentSize := 2
 | 
			
		||||
		if tag.Tag == "li" && parser.listType == "ol" {
 | 
			
		||||
			list := parser.openTags.Get(parser.listType)
 | 
			
		||||
			indentSize = int(math.Log10(float64(list.Counter))+1) + len(". ")
 | 
			
		||||
		}
 | 
			
		||||
		if len(parser.indent) >= indentSize {
 | 
			
		||||
			parser.indent = parser.indent[0 : len(parser.indent)-indentSize]
 | 
			
		||||
		}
 | 
			
		||||
		// TODO this newline is sometimes not good
 | 
			
		||||
		parser.newline()
 | 
			
		||||
	case "a":
 | 
			
		||||
		match := matrixToURL.FindStringSubmatch(tag.Meta)
 | 
			
		||||
		if len(match) == 2 {
 | 
			
		||||
			pillTarget := match[1]
 | 
			
		||||
			if pillTarget[0] == '@' {
 | 
			
		||||
				if member := parser.room.GetMember(pillTarget); member != nil {
 | 
			
		||||
					parser.text = parser.text.AppendColor(member.DisplayName, widget.GetHashColor(member.DisplayName))
 | 
			
		||||
				} else {
 | 
			
		||||
					parser.text = parser.text.Append(pillTarget)
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				parser.text = parser.text.Append(pillTarget)
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			// TODO make text clickable rather than printing URL
 | 
			
		||||
			parser.text = parser.text.Append(fmt.Sprintf("%s (%s)", tag.Text, tag.Meta))
 | 
			
		||||
		}
 | 
			
		||||
		parser.lineIsNew = false
 | 
			
		||||
	case "p", "pre", "ol", "ul", "h1", "h2", "h3", "h4", "h5", "h6", "div":
 | 
			
		||||
		// parser.newline()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (parser *MatrixHTMLProcessor) ReceiveError(err error) {
 | 
			
		||||
	if err != io.EOF {
 | 
			
		||||
		debug.Print("Unexpected error parsing HTML:", err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (parser *MatrixHTMLProcessor) Postprocess() {
 | 
			
		||||
	if len(parser.text) > 0 && parser.text[len(parser.text)-1].Char == '\n' {
 | 
			
		||||
		parser.text = parser.text[:len(parser.text)-1]
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ParseHTMLMessage parses a HTML-formatted Matrix event into a UIMessage.
 | 
			
		||||
func ParseHTMLMessage(room *rooms.Room, evt *gomatrix.Event) tstring.TString {
 | 
			
		||||
	htmlData, _ := evt.Content["formatted_body"].(string)
 | 
			
		||||
 | 
			
		||||
	processor := &MatrixHTMLProcessor{
 | 
			
		||||
		room:      room,
 | 
			
		||||
		text:      tstring.NewBlankTString(),
 | 
			
		||||
		indent:    "",
 | 
			
		||||
		listType:  "",
 | 
			
		||||
		lineIsNew: true,
 | 
			
		||||
		openTags:  &TagArray{},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	parser := htmlparser.NewHTMLParserFromString(htmlData, processor)
 | 
			
		||||
	parser.Process()
 | 
			
		||||
 | 
			
		||||
	return processor.text
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										118
									
								
								ui/messages/parser/htmltagarray.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								ui/messages/parser/htmltagarray.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,118 @@
 | 
			
		||||
// gomuks - A terminal Matrix client written in Go.
 | 
			
		||||
// Copyright (C) 2018 Tulir Asokan
 | 
			
		||||
//
 | 
			
		||||
// This program is free software: you can redistribute it and/or modify
 | 
			
		||||
// it under the terms of the GNU General Public License as published by
 | 
			
		||||
// the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
// (at your option) any later version.
 | 
			
		||||
//
 | 
			
		||||
// This program is distributed in the hope that it will be useful,
 | 
			
		||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
// GNU General Public License for more details.
 | 
			
		||||
//
 | 
			
		||||
// You should have received a copy of the GNU General Public License
 | 
			
		||||
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
package parser
 | 
			
		||||
 | 
			
		||||
// TagWithMeta is an open HTML tag with some metadata (e.g. list index, a href value).
 | 
			
		||||
type TagWithMeta struct {
 | 
			
		||||
	Tag     string
 | 
			
		||||
	Counter int
 | 
			
		||||
	Meta    string
 | 
			
		||||
	Text    string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// BlankTag is a blank TagWithMeta object.
 | 
			
		||||
var BlankTag = &TagWithMeta{}
 | 
			
		||||
 | 
			
		||||
// TagArray is a reversed queue for remembering what HTML tags are open.
 | 
			
		||||
type TagArray []*TagWithMeta
 | 
			
		||||
 | 
			
		||||
// Pushb converts the given byte array into a string and calls Push().
 | 
			
		||||
func (ta *TagArray) Pushb(tag []byte) {
 | 
			
		||||
	ta.Push(string(tag))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Popb converts the given byte array into a string and calls Pop().
 | 
			
		||||
func (ta *TagArray) Popb(tag []byte) *TagWithMeta {
 | 
			
		||||
	return ta.Pop(string(tag))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Indexb converts the given byte array into a string and calls Index().
 | 
			
		||||
func (ta *TagArray) Indexb(tag []byte) {
 | 
			
		||||
	ta.Index(string(tag))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IndexAfterb converts the given byte array into a string and calls IndexAfter().
 | 
			
		||||
func (ta *TagArray) IndexAfterb(tag []byte, after int) {
 | 
			
		||||
	ta.IndexAfter(string(tag), after)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Push adds the given tag to the array.
 | 
			
		||||
func (ta *TagArray) Push(tag string) {
 | 
			
		||||
	ta.PushMeta(&TagWithMeta{Tag: tag})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Push adds the given tag to the array.
 | 
			
		||||
func (ta *TagArray) PushMeta(tag *TagWithMeta) {
 | 
			
		||||
	*ta = append(*ta, BlankTag)
 | 
			
		||||
	copy((*ta)[1:], *ta)
 | 
			
		||||
	(*ta)[0] = tag
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Pop removes the given tag from the array.
 | 
			
		||||
func (ta *TagArray) Pop(tag string) (removed *TagWithMeta) {
 | 
			
		||||
	if (*ta)[0].Tag == tag {
 | 
			
		||||
		// This is the default case and is lighter than append(), so we handle it separately.
 | 
			
		||||
		removed = (*ta)[0]
 | 
			
		||||
		*ta = (*ta)[1:]
 | 
			
		||||
	} else if index := ta.Index(tag); index != -1 {
 | 
			
		||||
		removed = (*ta)[index]
 | 
			
		||||
		*ta = append((*ta)[:index], (*ta)[index+1:]...)
 | 
			
		||||
	}
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Index returns the first index where the given tag is, or -1 if it's not in the list.
 | 
			
		||||
func (ta *TagArray) Index(tag string) int {
 | 
			
		||||
	return ta.IndexAfter(tag, -1)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IndexAfter returns the first index after the given index where the given tag is,
 | 
			
		||||
// or -1 if the given tag is not on the list after the given index.
 | 
			
		||||
func (ta *TagArray) IndexAfter(tag string, after int) int {
 | 
			
		||||
	for i := after + 1; i < len(*ta); i++ {
 | 
			
		||||
		if (*ta)[i].Tag == tag {
 | 
			
		||||
			return i
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return -1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Get returns the first occurrence of the given tag, or nil if it's not in the list.
 | 
			
		||||
func (ta *TagArray) Get(tag string) *TagWithMeta {
 | 
			
		||||
	return ta.GetAfter(tag, -1)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IndexAfter returns the first occurrence of the given tag, or nil if the given
 | 
			
		||||
// tag is not on the list after the given index.
 | 
			
		||||
func (ta *TagArray) GetAfter(tag string, after int) *TagWithMeta {
 | 
			
		||||
	for i := after + 1; i < len(*ta); i++ {
 | 
			
		||||
		if (*ta)[i].Tag == tag {
 | 
			
		||||
			return (*ta)[i]
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Has returns whether or not the list has at least one of the given tags.
 | 
			
		||||
func (ta *TagArray) Has(tags ...string) bool {
 | 
			
		||||
	for _, tag := range tags {
 | 
			
		||||
		if index := ta.Index(tag); index != -1 {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										128
									
								
								ui/messages/parser/parser.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								ui/messages/parser/parser.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,128 @@
 | 
			
		||||
// gomuks - A terminal Matrix client written in Go.
 | 
			
		||||
// Copyright (C) 2018 Tulir Asokan
 | 
			
		||||
//
 | 
			
		||||
// This program is free software: you can redistribute it and/or modify
 | 
			
		||||
// it under the terms of the GNU General Public License as published by
 | 
			
		||||
// the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
// (at your option) any later version.
 | 
			
		||||
//
 | 
			
		||||
// This program is distributed in the hope that it will be useful,
 | 
			
		||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
// GNU General Public License for more details.
 | 
			
		||||
//
 | 
			
		||||
// You should have received a copy of the GNU General Public License
 | 
			
		||||
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
package parser
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"maunium.net/go/gomatrix"
 | 
			
		||||
	"maunium.net/go/gomuks/debug"
 | 
			
		||||
	"maunium.net/go/gomuks/interface"
 | 
			
		||||
	"maunium.net/go/gomuks/matrix/rooms"
 | 
			
		||||
	"maunium.net/go/gomuks/ui/messages"
 | 
			
		||||
	"maunium.net/go/gomuks/ui/messages/tstring"
 | 
			
		||||
	"maunium.net/go/gomuks/ui/widget"
 | 
			
		||||
	"maunium.net/go/tcell"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func ParseEvent(gmx ifc.Gomuks, room *rooms.Room, evt *gomatrix.Event) messages.UIMessage {
 | 
			
		||||
	member := room.GetMember(evt.Sender)
 | 
			
		||||
	if member != nil {
 | 
			
		||||
		evt.Sender = member.DisplayName
 | 
			
		||||
	}
 | 
			
		||||
	switch evt.Type {
 | 
			
		||||
	case "m.room.message":
 | 
			
		||||
		return ParseMessage(gmx, room, evt)
 | 
			
		||||
	case "m.room.member":
 | 
			
		||||
		return ParseMembershipEvent(evt)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func unixToTime(unix int64) time.Time {
 | 
			
		||||
	timestamp := time.Now()
 | 
			
		||||
	if unix != 0 {
 | 
			
		||||
		timestamp = time.Unix(unix/1000, unix%1000*1000)
 | 
			
		||||
	}
 | 
			
		||||
	return timestamp
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ParseMessage(gmx ifc.Gomuks, room *rooms.Room, evt *gomatrix.Event) messages.UIMessage {
 | 
			
		||||
	msgtype, _ := evt.Content["msgtype"].(string)
 | 
			
		||||
	ts := unixToTime(evt.Timestamp)
 | 
			
		||||
	switch msgtype {
 | 
			
		||||
	case "m.text", "m.notice", "m.emote":
 | 
			
		||||
		format, hasFormat := evt.Content["format"].(string)
 | 
			
		||||
		if hasFormat && format == "org.matrix.custom.html" {
 | 
			
		||||
			text := ParseHTMLMessage(room, evt)
 | 
			
		||||
			return messages.NewExpandedTextMessage(evt.ID, evt.Sender, msgtype, text, ts)
 | 
			
		||||
		} else {
 | 
			
		||||
			text, _ := evt.Content["body"].(string)
 | 
			
		||||
			return messages.NewTextMessage(evt.ID, evt.Sender, msgtype, text, ts)
 | 
			
		||||
		}
 | 
			
		||||
	case "m.image":
 | 
			
		||||
		url, _ := evt.Content["url"].(string)
 | 
			
		||||
		data, hs, id, err := gmx.Matrix().Download(url)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			debug.Printf("Failed to download %s: %v", url, err)
 | 
			
		||||
		}
 | 
			
		||||
		return messages.NewImageMessage(gmx, evt.ID, evt.Sender, msgtype, hs, id, data, ts)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getMembershipEventContent(evt *gomatrix.Event) (sender string, text tstring.TString) {
 | 
			
		||||
	membership, _ := evt.Content["membership"].(string)
 | 
			
		||||
	displayname, _ := evt.Content["displayname"].(string)
 | 
			
		||||
	if len(displayname) == 0 {
 | 
			
		||||
		displayname = *evt.StateKey
 | 
			
		||||
	}
 | 
			
		||||
	prevMembership := "leave"
 | 
			
		||||
	prevDisplayname := ""
 | 
			
		||||
	if evt.Unsigned.PrevContent != nil {
 | 
			
		||||
		prevMembership, _ = evt.Unsigned.PrevContent["membership"].(string)
 | 
			
		||||
		prevDisplayname, _ = evt.Unsigned.PrevContent["displayname"].(string)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if membership != prevMembership {
 | 
			
		||||
		switch membership {
 | 
			
		||||
		case "invite":
 | 
			
		||||
			sender = "---"
 | 
			
		||||
			text = tstring.NewColorTString(fmt.Sprintf("%s invited %s.", evt.Sender, displayname), tcell.ColorGreen)
 | 
			
		||||
			text.Colorize(0, len(evt.Sender), widget.GetHashColor(evt.Sender))
 | 
			
		||||
			text.Colorize(len(evt.Sender)+len(" invited "), len(displayname), widget.GetHashColor(displayname))
 | 
			
		||||
		case "join":
 | 
			
		||||
			sender = "-->"
 | 
			
		||||
			text = tstring.NewColorTString(fmt.Sprintf("%s joined the room.", displayname), tcell.ColorGreen)
 | 
			
		||||
			text.Colorize(0, len(displayname), widget.GetHashColor(displayname))
 | 
			
		||||
		case "leave":
 | 
			
		||||
			sender = "<--"
 | 
			
		||||
			if evt.Sender != *evt.StateKey {
 | 
			
		||||
				reason, _ := evt.Content["reason"].(string)
 | 
			
		||||
				text = tstring.NewColorTString(fmt.Sprintf("%s kicked %s: %s", evt.Sender, displayname, reason), tcell.ColorRed)
 | 
			
		||||
				text.Colorize(0, len(evt.Sender), widget.GetHashColor(evt.Sender))
 | 
			
		||||
				text.Colorize(len(evt.Sender)+len(" kicked "), len(displayname), widget.GetHashColor(displayname))
 | 
			
		||||
			} else {
 | 
			
		||||
				text = tstring.NewColorTString(fmt.Sprintf("%s left the room.", displayname), tcell.ColorRed)
 | 
			
		||||
				text.Colorize(0, len(displayname), widget.GetHashColor(displayname))
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	} else if displayname != prevDisplayname {
 | 
			
		||||
		sender = "---"
 | 
			
		||||
		text = tstring.NewColorTString(fmt.Sprintf("%s changed their display name to %s.", prevDisplayname, displayname), tcell.ColorYellow)
 | 
			
		||||
		text.Colorize(0, len(prevDisplayname), widget.GetHashColor(prevDisplayname))
 | 
			
		||||
		text.Colorize(len(prevDisplayname)+len(" changed their display name to "), len(displayname), widget.GetHashColor(displayname))
 | 
			
		||||
	}
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ParseMembershipEvent(evt *gomatrix.Event) messages.UIMessage {
 | 
			
		||||
	sender, text := getMembershipEventContent(evt)
 | 
			
		||||
	ts := unixToTime(evt.Timestamp)
 | 
			
		||||
	return messages.NewExpandedTextMessage(evt.ID, sender, "m.room.membership", text, ts)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										84
									
								
								ui/messages/textbase.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								ui/messages/textbase.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,84 @@
 | 
			
		||||
// gomuks - A terminal Matrix client written in Go.
 | 
			
		||||
// Copyright (C) 2018 Tulir Asokan
 | 
			
		||||
//
 | 
			
		||||
// This program is free software: you can redistribute it and/or modify
 | 
			
		||||
// it under the terms of the GNU General Public License as published by
 | 
			
		||||
// the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
// (at your option) any later version.
 | 
			
		||||
//
 | 
			
		||||
// This program is distributed in the hope that it will be useful,
 | 
			
		||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
// GNU General Public License for more details.
 | 
			
		||||
//
 | 
			
		||||
// You should have received a copy of the GNU General Public License
 | 
			
		||||
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
package messages
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/gob"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"maunium.net/go/gomuks/ui/messages/tstring"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	gob.Register(BaseTextMessage{})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type BaseTextMessage struct {
 | 
			
		||||
	BaseMessage
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func newBaseTextMessage(id, sender, msgtype string, timestamp time.Time) BaseTextMessage {
 | 
			
		||||
	return BaseTextMessage{newBaseMessage(id, sender, msgtype, timestamp)}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Regular expressions used to split lines when calculating the buffer.
 | 
			
		||||
//
 | 
			
		||||
// From tview/textview.go
 | 
			
		||||
var (
 | 
			
		||||
	boundaryPattern = regexp.MustCompile("([[:punct:]]\\s*|\\s+)")
 | 
			
		||||
	spacePattern    = regexp.MustCompile(`\s+`)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// CalculateBuffer generates the internal buffer for this message that consists
 | 
			
		||||
// of the text of this message split into lines at most as wide as the width
 | 
			
		||||
// parameter.
 | 
			
		||||
func (msg *BaseTextMessage) calculateBufferWithText(text tstring.TString, width int) {
 | 
			
		||||
	if width < 2 {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	msg.buffer = []tstring.TString{}
 | 
			
		||||
 | 
			
		||||
	forcedLinebreaks := text.Split('\n')
 | 
			
		||||
	newlines := 0
 | 
			
		||||
	for _, str := range forcedLinebreaks {
 | 
			
		||||
		if len(str) == 0 && newlines < 1 {
 | 
			
		||||
			msg.buffer = append(msg.buffer, tstring.TString{})
 | 
			
		||||
			newlines++
 | 
			
		||||
		} else {
 | 
			
		||||
			newlines = 0
 | 
			
		||||
		}
 | 
			
		||||
		// Mostly from tview/textview.go#reindexBuffer()
 | 
			
		||||
		for len(str) > 0 {
 | 
			
		||||
			extract := str.Truncate(width)
 | 
			
		||||
			if len(extract) < len(str) {
 | 
			
		||||
				if spaces := spacePattern.FindStringIndex(str[len(extract):].String()); spaces != nil && spaces[0] == 0 {
 | 
			
		||||
					extract = str[:len(extract)+spaces[1]]
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				matches := boundaryPattern.FindAllStringIndex(extract.String(), -1)
 | 
			
		||||
				if len(matches) > 0 {
 | 
			
		||||
					extract = extract[:matches[len(matches)-1][1]]
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			msg.buffer = append(msg.buffer, extract)
 | 
			
		||||
			str = str[len(extract):]
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	msg.prevBufferWidth = width
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										102
									
								
								ui/messages/textmessage.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								ui/messages/textmessage.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,102 @@
 | 
			
		||||
// gomuks - A terminal Matrix client written in Go.
 | 
			
		||||
// Copyright (C) 2018 Tulir Asokan
 | 
			
		||||
//
 | 
			
		||||
// This program is free software: you can redistribute it and/or modify
 | 
			
		||||
// it under the terms of the GNU General Public License as published by
 | 
			
		||||
// the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
// (at your option) any later version.
 | 
			
		||||
//
 | 
			
		||||
// This program is distributed in the hope that it will be useful,
 | 
			
		||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
// GNU General Public License for more details.
 | 
			
		||||
//
 | 
			
		||||
// You should have received a copy of the GNU General Public License
 | 
			
		||||
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
package messages
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/gob"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"maunium.net/go/gomuks/interface"
 | 
			
		||||
	"maunium.net/go/gomuks/ui/messages/tstring"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	gob.Register(&TextMessage{})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type TextMessage struct {
 | 
			
		||||
	BaseTextMessage
 | 
			
		||||
	cache   tstring.TString
 | 
			
		||||
	MsgText string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewTextMessage creates a new UITextMessage object with the provided values and the default state.
 | 
			
		||||
func NewTextMessage(id, sender, msgtype, text string, timestamp time.Time) UIMessage {
 | 
			
		||||
	return &TextMessage{
 | 
			
		||||
		BaseTextMessage: newBaseTextMessage(id, sender, msgtype, timestamp),
 | 
			
		||||
		MsgText:         text,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (msg *TextMessage) getCache() tstring.TString {
 | 
			
		||||
	if msg.cache == nil {
 | 
			
		||||
		switch msg.MsgType {
 | 
			
		||||
		case "m.emote":
 | 
			
		||||
			msg.cache = tstring.NewColorTString(fmt.Sprintf("* %s %s", msg.MsgSender, msg.MsgText), msg.TextColor())
 | 
			
		||||
			msg.cache.Colorize(0, len(msg.MsgSender)+2, msg.SenderColor())
 | 
			
		||||
		default:
 | 
			
		||||
			msg.cache = tstring.NewColorTString(msg.MsgText, msg.TextColor())
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return msg.cache
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CopyFrom replaces the content of this message object with the content of the given object.
 | 
			
		||||
func (msg *TextMessage) CopyFrom(from ifc.MessageMeta) {
 | 
			
		||||
	msg.BaseTextMessage.CopyFrom(from)
 | 
			
		||||
 | 
			
		||||
	fromTextMsg, ok := from.(*TextMessage)
 | 
			
		||||
	if ok {
 | 
			
		||||
		msg.MsgText = fromTextMsg.MsgText
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	msg.cache = nil
 | 
			
		||||
	msg.RecalculateBuffer()
 | 
			
		||||
}
 | 
			
		||||
func (msg *TextMessage) SetType(msgtype string) {
 | 
			
		||||
	msg.BaseTextMessage.SetType(msgtype)
 | 
			
		||||
	msg.cache = nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (msg *TextMessage) SetState(state ifc.MessageState) {
 | 
			
		||||
	msg.BaseTextMessage.SetState(state)
 | 
			
		||||
	msg.cache = nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (msg *TextMessage) SetIsHighlight(isHighlight bool) {
 | 
			
		||||
	msg.BaseTextMessage.SetIsHighlight(isHighlight)
 | 
			
		||||
	msg.cache = nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (msg *TextMessage) SetIsService(isService bool) {
 | 
			
		||||
	msg.BaseTextMessage.SetIsService(isService)
 | 
			
		||||
	msg.cache = nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (msg *TextMessage) NotificationContent() string {
 | 
			
		||||
	return msg.MsgText
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (msg *TextMessage) CalculateBuffer(width int) {
 | 
			
		||||
	msg.BaseTextMessage.calculateBufferWithText(msg.getCache(), width)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RecalculateBuffer calculates the buffer again with the previously provided width.
 | 
			
		||||
func (msg *TextMessage) RecalculateBuffer() {
 | 
			
		||||
	msg.CalculateBuffer(msg.prevBufferWidth)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										51
									
								
								ui/messages/tstring/cell.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								ui/messages/tstring/cell.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,51 @@
 | 
			
		||||
// gomuks - A terminal Matrix client written in Go.
 | 
			
		||||
// Copyright (C) 2018 Tulir Asokan
 | 
			
		||||
//
 | 
			
		||||
// This program is free software: you can redistribute it and/or modify
 | 
			
		||||
// it under the terms of the GNU General Public License as published by
 | 
			
		||||
// the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
// (at your option) any later version.
 | 
			
		||||
//
 | 
			
		||||
// This program is distributed in the hope that it will be useful,
 | 
			
		||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
// GNU General Public License for more details.
 | 
			
		||||
//
 | 
			
		||||
// You should have received a copy of the GNU General Public License
 | 
			
		||||
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
package tstring
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"maunium.net/go/tcell"
 | 
			
		||||
	"github.com/mattn/go-runewidth"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Cell struct {
 | 
			
		||||
	Char  rune
 | 
			
		||||
	Style tcell.Style
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewStyleCell(char rune, style tcell.Style) Cell {
 | 
			
		||||
	return Cell{char, style}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewColorCell(char rune, color tcell.Color) Cell {
 | 
			
		||||
	return Cell{char, tcell.StyleDefault.Foreground(color)}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewCell(char rune) Cell {
 | 
			
		||||
	return Cell{char, tcell.StyleDefault}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (cell Cell) RuneWidth() int {
 | 
			
		||||
	return runewidth.RuneWidth(cell.Char)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (cell Cell) Draw(screen tcell.Screen, x, y int) (chWidth int) {
 | 
			
		||||
	chWidth = cell.RuneWidth()
 | 
			
		||||
	for runeWidthOffset := 0; runeWidthOffset < chWidth; runeWidthOffset++ {
 | 
			
		||||
		screen.SetContent(x+runeWidthOffset, y, cell.Char, nil, cell.Style)
 | 
			
		||||
	}
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										173
									
								
								ui/messages/tstring/string.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								ui/messages/tstring/string.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,173 @@
 | 
			
		||||
// gomuks - A terminal Matrix client written in Go.
 | 
			
		||||
// Copyright (C) 2018 Tulir Asokan
 | 
			
		||||
//
 | 
			
		||||
// This program is free software: you can redistribute it and/or modify
 | 
			
		||||
// it under the terms of the GNU General Public License as published by
 | 
			
		||||
// the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
// (at your option) any later version.
 | 
			
		||||
//
 | 
			
		||||
// This program is distributed in the hope that it will be useful,
 | 
			
		||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
// GNU General Public License for more details.
 | 
			
		||||
//
 | 
			
		||||
// You should have received a copy of the GNU General Public License
 | 
			
		||||
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
package tstring
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/mattn/go-runewidth"
 | 
			
		||||
	"maunium.net/go/tcell"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type TString []Cell
 | 
			
		||||
 | 
			
		||||
func NewBlankTString() TString {
 | 
			
		||||
	return make([]Cell, 0)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewTString(str string) TString {
 | 
			
		||||
	newStr := make([]Cell, len(str))
 | 
			
		||||
	for i, char := range str {
 | 
			
		||||
		newStr[i] = NewCell(char)
 | 
			
		||||
	}
 | 
			
		||||
	return newStr
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewColorTString(str string, color tcell.Color) TString {
 | 
			
		||||
	newStr := make([]Cell, len(str))
 | 
			
		||||
	for i, char := range str {
 | 
			
		||||
		newStr[i] = NewColorCell(char, color)
 | 
			
		||||
	}
 | 
			
		||||
	return newStr
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewStyleTString(str string, style tcell.Style) TString {
 | 
			
		||||
	newStr := make([]Cell, len(str))
 | 
			
		||||
	for i, char := range str {
 | 
			
		||||
		newStr[i] = NewStyleCell(char, style)
 | 
			
		||||
	}
 | 
			
		||||
	return newStr
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (str TString) AppendTString(data TString) TString {
 | 
			
		||||
	return append(str, data...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (str TString) Append(data string) TString {
 | 
			
		||||
	newStr := make(TString, len(str)+len(data))
 | 
			
		||||
	copy(newStr, str)
 | 
			
		||||
	for i, char := range data {
 | 
			
		||||
		newStr[i+len(str)] = NewCell(char)
 | 
			
		||||
	}
 | 
			
		||||
	return newStr
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (str TString) AppendColor(data string, color tcell.Color) TString {
 | 
			
		||||
	newStr := make(TString, len(str)+len(data))
 | 
			
		||||
	copy(newStr, str)
 | 
			
		||||
	for i, char := range data {
 | 
			
		||||
		newStr[i+len(str)] = NewColorCell(char, color)
 | 
			
		||||
	}
 | 
			
		||||
	return newStr
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (str TString) AppendStyle(data string, style tcell.Style) TString {
 | 
			
		||||
	newStr := make(TString, len(str)+len(data))
 | 
			
		||||
	copy(newStr, str)
 | 
			
		||||
	for i, char := range data {
 | 
			
		||||
		newStr[i+len(str)] = NewStyleCell(char, style)
 | 
			
		||||
	}
 | 
			
		||||
	return newStr
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (str TString) Colorize(from, length int, color tcell.Color) {
 | 
			
		||||
	for i := from; i < from+length; i++ {
 | 
			
		||||
		str[i].Style = str[i].Style.Foreground(color)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (str TString) Draw(screen tcell.Screen, x, y int) {
 | 
			
		||||
	offsetX := 0
 | 
			
		||||
	for _, cell := range str {
 | 
			
		||||
		offsetX += cell.Draw(screen, x+offsetX, y)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (str TString) RuneWidth() (width int) {
 | 
			
		||||
	for _, cell := range str {
 | 
			
		||||
		width += runewidth.RuneWidth(cell.Char)
 | 
			
		||||
	}
 | 
			
		||||
	return width
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (str TString) String() string {
 | 
			
		||||
	var buf strings.Builder
 | 
			
		||||
	for _, cell := range str {
 | 
			
		||||
		buf.WriteRune(cell.Char)
 | 
			
		||||
	}
 | 
			
		||||
	return buf.String()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Truncate return string truncated with w cells
 | 
			
		||||
func (str TString) Truncate(w int) TString {
 | 
			
		||||
	if str.RuneWidth() <= w {
 | 
			
		||||
		return str[:]
 | 
			
		||||
	}
 | 
			
		||||
	width := 0
 | 
			
		||||
	i := 0
 | 
			
		||||
	for ; i < len(str); i++ {
 | 
			
		||||
		cw := runewidth.RuneWidth(str[i].Char)
 | 
			
		||||
		if width+cw > w {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
		width += cw
 | 
			
		||||
	}
 | 
			
		||||
	return str[0:i]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (str TString) IndexFrom(r rune, from int) int {
 | 
			
		||||
	for i := from; i < len(str); i++ {
 | 
			
		||||
		if str[i].Char == r {
 | 
			
		||||
			return i
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return -1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (str TString) Index(r rune) int {
 | 
			
		||||
	return str.IndexFrom(r, 0)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (str TString) Count(r rune) (counter int) {
 | 
			
		||||
	index := 0
 | 
			
		||||
	for {
 | 
			
		||||
		index = str.IndexFrom(r, index)
 | 
			
		||||
		if index < 0 {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
		index++
 | 
			
		||||
		counter++
 | 
			
		||||
	}
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (str TString) Split(sep rune) []TString {
 | 
			
		||||
	a := make([]TString, str.Count(sep)+1)
 | 
			
		||||
	i := 0
 | 
			
		||||
	orig := str
 | 
			
		||||
	for {
 | 
			
		||||
		m := orig.Index(sep)
 | 
			
		||||
		if m < 0 {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
		a[i] = orig[:m]
 | 
			
		||||
		orig = orig[m+1:]
 | 
			
		||||
		i++
 | 
			
		||||
	}
 | 
			
		||||
	a[i] = orig
 | 
			
		||||
	return a[:i+1]
 | 
			
		||||
}
 | 
			
		||||
@@ -14,14 +14,15 @@
 | 
			
		||||
// You should have received a copy of the GNU General Public License
 | 
			
		||||
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
package widget
 | 
			
		||||
package ui
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strconv"
 | 
			
		||||
 | 
			
		||||
	"github.com/gdamore/tcell"
 | 
			
		||||
	"maunium.net/go/tcell"
 | 
			
		||||
	"maunium.net/go/gomuks/matrix/rooms"
 | 
			
		||||
	"maunium.net/go/gomuks/ui/widget"
 | 
			
		||||
	"maunium.net/go/tview"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -126,11 +127,11 @@ func (list *RoomList) Draw(screen tcell.Screen) {
 | 
			
		||||
				unreadMessageCount += "!"
 | 
			
		||||
			}
 | 
			
		||||
			unreadMessageCount = fmt.Sprintf("(%s)", unreadMessageCount)
 | 
			
		||||
			writeLine(screen, tview.AlignRight, unreadMessageCount, x+lineWidth-6, y, 6, style)
 | 
			
		||||
			widget.WriteLine(screen, tview.AlignRight, unreadMessageCount, x+lineWidth-6, y, 6, style)
 | 
			
		||||
			lineWidth -= len(unreadMessageCount) + 1
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		writeLine(screen, tview.AlignLeft, text, x, y, lineWidth, style)
 | 
			
		||||
		widget.WriteLine(screen, tview.AlignLeft, text, x, y, lineWidth, style)
 | 
			
		||||
 | 
			
		||||
		y++
 | 
			
		||||
		if y >= bottomLimit {
 | 
			
		||||
@@ -14,7 +14,7 @@
 | 
			
		||||
// You should have received a copy of the GNU General Public License
 | 
			
		||||
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
package widget
 | 
			
		||||
package ui
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
@@ -23,9 +23,11 @@ import (
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/gdamore/tcell"
 | 
			
		||||
	"maunium.net/go/tcell"
 | 
			
		||||
	"maunium.net/go/gomuks/interface"
 | 
			
		||||
	"maunium.net/go/gomuks/matrix/rooms"
 | 
			
		||||
	"maunium.net/go/gomuks/ui/types"
 | 
			
		||||
	"maunium.net/go/gomuks/ui/messages"
 | 
			
		||||
	"maunium.net/go/gomuks/ui/widget"
 | 
			
		||||
	"maunium.net/go/tview"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -36,8 +38,8 @@ type RoomView struct {
 | 
			
		||||
	content  *MessageView
 | 
			
		||||
	status   *tview.TextView
 | 
			
		||||
	userList *tview.TextView
 | 
			
		||||
	ulBorder *Border
 | 
			
		||||
	input    *AdvancedInputField
 | 
			
		||||
	ulBorder *widget.Border
 | 
			
		||||
	input    *widget.AdvancedInputField
 | 
			
		||||
	Room     *rooms.Room
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -45,13 +47,13 @@ func NewRoomView(room *rooms.Room) *RoomView {
 | 
			
		||||
	view := &RoomView{
 | 
			
		||||
		Box:      tview.NewBox(),
 | 
			
		||||
		topic:    tview.NewTextView(),
 | 
			
		||||
		content:  NewMessageView(),
 | 
			
		||||
		status:   tview.NewTextView(),
 | 
			
		||||
		userList: tview.NewTextView(),
 | 
			
		||||
		ulBorder: NewBorder(),
 | 
			
		||||
		input:    NewAdvancedInputField(),
 | 
			
		||||
		ulBorder: widget.NewBorder(),
 | 
			
		||||
		input:    widget.NewAdvancedInputField(),
 | 
			
		||||
		Room:     room,
 | 
			
		||||
	}
 | 
			
		||||
	view.content = NewMessageView(view)
 | 
			
		||||
 | 
			
		||||
	view.input.
 | 
			
		||||
		SetFieldBackgroundColor(tcell.ColorDefault).
 | 
			
		||||
@@ -79,8 +81,8 @@ func (view *RoomView) SaveHistory(dir string) error {
 | 
			
		||||
	return view.MessageView().SaveHistory(view.logPath(dir))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *RoomView) LoadHistory(dir string) (int, error) {
 | 
			
		||||
	return view.MessageView().LoadHistory(view.logPath(dir))
 | 
			
		||||
func (view *RoomView) LoadHistory(gmx ifc.Gomuks, dir string) (int, error) {
 | 
			
		||||
	return view.MessageView().LoadHistory(gmx, view.logPath(dir))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *RoomView) SetTabCompleteFunc(fn func(room *RoomView, text string, cursorOffset int) string) *RoomView {
 | 
			
		||||
@@ -129,7 +131,7 @@ func (view *RoomView) GetInputText() string {
 | 
			
		||||
	return view.input.GetText()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *RoomView) GetInputField() *AdvancedInputField {
 | 
			
		||||
func (view *RoomView) GetInputField() *widget.AdvancedInputField {
 | 
			
		||||
	return view.input
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -230,15 +232,19 @@ func (view *RoomView) MessageView() *MessageView {
 | 
			
		||||
	return view.content
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *RoomView) MxRoom() *rooms.Room {
 | 
			
		||||
	return view.Room
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *RoomView) UpdateUserList() {
 | 
			
		||||
	var joined strings.Builder
 | 
			
		||||
	var invited strings.Builder
 | 
			
		||||
	for _, user := range view.Room.GetMembers() {
 | 
			
		||||
		if user.Membership == "join" {
 | 
			
		||||
			joined.WriteString(AddHashColor(user.DisplayName))
 | 
			
		||||
			joined.WriteString(widget.AddHashColor(user.DisplayName))
 | 
			
		||||
			joined.WriteRune('\n')
 | 
			
		||||
		} else if user.Membership == "invite" {
 | 
			
		||||
			invited.WriteString(AddHashColor(user.DisplayName))
 | 
			
		||||
			invited.WriteString(widget.AddHashColor(user.DisplayName))
 | 
			
		||||
			invited.WriteRune('\n')
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
@@ -249,27 +255,37 @@ func (view *RoomView) UpdateUserList() {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *RoomView) NewMessage(id, sender, msgtype, text string, timestamp time.Time) *types.Message {
 | 
			
		||||
func (view *RoomView) newUIMessage(id, sender, msgtype, text string, timestamp time.Time) messages.UIMessage {
 | 
			
		||||
	member := view.Room.GetMember(sender)
 | 
			
		||||
	if member != nil {
 | 
			
		||||
		sender = member.DisplayName
 | 
			
		||||
	}
 | 
			
		||||
	return view.content.NewMessage(id, sender, msgtype, text, timestamp)
 | 
			
		||||
	return messages.NewTextMessage(id, sender, msgtype, text, timestamp)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *RoomView) 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)
 | 
			
		||||
}
 | 
			
		||||
@@ -1,2 +0,0 @@
 | 
			
		||||
// Package types contains common type definitions used mostly by the UI, but also other parts of gomuks.
 | 
			
		||||
package types
 | 
			
		||||
@@ -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
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										2
									
								
								ui/ui.go
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								ui/ui.go
									
									
									
									
									
								
							@@ -17,7 +17,7 @@
 | 
			
		||||
package ui
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/gdamore/tcell"
 | 
			
		||||
	"maunium.net/go/tcell"
 | 
			
		||||
	"maunium.net/go/gomuks/interface"
 | 
			
		||||
	"maunium.net/go/tview"
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -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"
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										163
									
								
								ui/view-main.go
									
									
									
									
									
								
							
							
						
						
									
										163
									
								
								ui/view-main.go
									
									
									
									
									
								
							@@ -23,26 +23,26 @@ import (
 | 
			
		||||
	"time"
 | 
			
		||||
	"unicode"
 | 
			
		||||
 | 
			
		||||
	"github.com/gdamore/tcell"
 | 
			
		||||
	"github.com/mattn/go-runewidth"
 | 
			
		||||
	"maunium.net/go/gomatrix"
 | 
			
		||||
	"maunium.net/go/gomuks/config"
 | 
			
		||||
	"maunium.net/go/gomuks/debug"
 | 
			
		||||
	"maunium.net/go/gomuks/interface"
 | 
			
		||||
	"maunium.net/go/gomuks/lib/notification"
 | 
			
		||||
	"maunium.net/go/gomuks/matrix/pushrules"
 | 
			
		||||
	"maunium.net/go/gomuks/matrix/rooms"
 | 
			
		||||
	"maunium.net/go/gomuks/notification"
 | 
			
		||||
	"maunium.net/go/gomuks/ui/debug"
 | 
			
		||||
	"maunium.net/go/gomuks/ui/types"
 | 
			
		||||
	"maunium.net/go/gomuks/ui/messages/parser"
 | 
			
		||||
	"maunium.net/go/gomuks/ui/widget"
 | 
			
		||||
	"maunium.net/go/tcell"
 | 
			
		||||
	"maunium.net/go/tview"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type MainView struct {
 | 
			
		||||
	*tview.Flex
 | 
			
		||||
 | 
			
		||||
	roomList         *widget.RoomList
 | 
			
		||||
	roomList         *RoomList
 | 
			
		||||
	roomView         *tview.Pages
 | 
			
		||||
	rooms            map[string]*widget.RoomView
 | 
			
		||||
	rooms            map[string]*RoomView
 | 
			
		||||
	currentRoomIndex int
 | 
			
		||||
	roomIDs          []string
 | 
			
		||||
 | 
			
		||||
@@ -57,9 +57,9 @@ type MainView struct {
 | 
			
		||||
func (ui *GomuksUI) NewMainView() tview.Primitive {
 | 
			
		||||
	mainView := &MainView{
 | 
			
		||||
		Flex:     tview.NewFlex(),
 | 
			
		||||
		roomList: widget.NewRoomList(),
 | 
			
		||||
		roomList: NewRoomList(),
 | 
			
		||||
		roomView: tview.NewPages(),
 | 
			
		||||
		rooms:    make(map[string]*widget.RoomView),
 | 
			
		||||
		rooms:    make(map[string]*RoomView),
 | 
			
		||||
 | 
			
		||||
		matrix: ui.gmx.Matrix(),
 | 
			
		||||
		gmx:    ui.gmx,
 | 
			
		||||
@@ -81,7 +81,7 @@ func (view *MainView) BumpFocus() {
 | 
			
		||||
	view.lastFocusTime = time.Now()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MainView) InputChanged(roomView *widget.RoomView, text string) {
 | 
			
		||||
func (view *MainView) InputChanged(roomView *RoomView, text string) {
 | 
			
		||||
	if len(text) == 0 {
 | 
			
		||||
		go view.matrix.SendTyping(roomView.Room.ID, false)
 | 
			
		||||
	} else if text[0] != '/' {
 | 
			
		||||
@@ -101,7 +101,7 @@ func findWordToTabComplete(text string) string {
 | 
			
		||||
	return output
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MainView) InputTabComplete(roomView *widget.RoomView, text string, cursorOffset int) string {
 | 
			
		||||
func (view *MainView) InputTabComplete(roomView *RoomView, text string, cursorOffset int) string {
 | 
			
		||||
	str := runewidth.Truncate(text, cursorOffset, "")
 | 
			
		||||
	word := findWordToTabComplete(str)
 | 
			
		||||
	userCompletions := roomView.AutocompleteUser(word)
 | 
			
		||||
@@ -118,7 +118,7 @@ func (view *MainView) InputTabComplete(roomView *widget.RoomView, text string, c
 | 
			
		||||
	return text
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MainView) InputSubmit(roomView *widget.RoomView, text string) {
 | 
			
		||||
func (view *MainView) InputSubmit(roomView *RoomView, text string) {
 | 
			
		||||
	if len(text) == 0 {
 | 
			
		||||
		return
 | 
			
		||||
	} else if text[0] == '/' {
 | 
			
		||||
@@ -132,29 +132,30 @@ func (view *MainView) InputSubmit(roomView *widget.RoomView, text string) {
 | 
			
		||||
	roomView.SetInputText("")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MainView) SendMessage(roomView *widget.RoomView, text string) {
 | 
			
		||||
func (view *MainView) SendMessage(roomView *RoomView, text string) {
 | 
			
		||||
	tempMessage := roomView.NewTempMessage("m.text", text)
 | 
			
		||||
	go view.sendTempMessage(roomView, tempMessage)
 | 
			
		||||
	go view.sendTempMessage(roomView, tempMessage, text)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MainView) sendTempMessage(roomView *widget.RoomView, tempMessage *types.Message) {
 | 
			
		||||
func (view *MainView) sendTempMessage(roomView *RoomView, tempMessage ifc.Message, text string) {
 | 
			
		||||
	defer view.gmx.Recover()
 | 
			
		||||
	eventID, err := view.matrix.SendMessage(roomView.Room.ID, tempMessage.Type, tempMessage.Text)
 | 
			
		||||
	eventID, err := view.matrix.SendMessage(roomView.Room.ID, tempMessage.Type(), text)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		tempMessage.State = types.MessageStateFailed
 | 
			
		||||
		tempMessage.SetState(ifc.MessageStateFailed)
 | 
			
		||||
		roomView.SetStatus(fmt.Sprintf("Failed to send message: %s", err))
 | 
			
		||||
	} else {
 | 
			
		||||
		roomView.MessageView().UpdateMessageID(tempMessage, eventID)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MainView) HandleCommand(roomView *widget.RoomView, command string, args []string) {
 | 
			
		||||
func (view *MainView) HandleCommand(roomView *RoomView, command string, args []string) {
 | 
			
		||||
	defer view.gmx.Recover()
 | 
			
		||||
	debug.Print("Handling command", command, args)
 | 
			
		||||
	switch command {
 | 
			
		||||
	case "/me":
 | 
			
		||||
		tempMessage := roomView.NewTempMessage("m.emote", strings.Join(args, " "))
 | 
			
		||||
		go view.sendTempMessage(roomView, tempMessage)
 | 
			
		||||
		text := strings.Join(args, " ")
 | 
			
		||||
		tempMessage := roomView.NewTempMessage("m.emote", text)
 | 
			
		||||
		go view.sendTempMessage(roomView, tempMessage, text)
 | 
			
		||||
		view.parent.Render()
 | 
			
		||||
	case "/quit":
 | 
			
		||||
		view.gmx.Stop()
 | 
			
		||||
@@ -163,22 +164,20 @@ func (view *MainView) HandleCommand(roomView *widget.RoomView, command string, a
 | 
			
		||||
		view.gmx.Stop()
 | 
			
		||||
	case "/panic":
 | 
			
		||||
		panic("This is a test panic.")
 | 
			
		||||
	case "/part":
 | 
			
		||||
		fallthrough
 | 
			
		||||
	case "/leave":
 | 
			
		||||
	case "/part", "/leave":
 | 
			
		||||
		debug.Print("Leave room result:", view.matrix.LeaveRoom(roomView.Room.ID))
 | 
			
		||||
	case "/join":
 | 
			
		||||
		if len(args) == 0 {
 | 
			
		||||
			view.AddServiceMessage(roomView, "Usage: /join <room>")
 | 
			
		||||
			roomView.AddServiceMessage("Usage: /join <room>")
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
		debug.Print("Join room result:", view.matrix.JoinRoom(args[0]))
 | 
			
		||||
	default:
 | 
			
		||||
		view.AddServiceMessage(roomView, "Unknown command.")
 | 
			
		||||
		roomView.AddServiceMessage("Unknown command.")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MainView) KeyEventHandler(roomView *widget.RoomView, key *tcell.EventKey) *tcell.EventKey {
 | 
			
		||||
func (view *MainView) KeyEventHandler(roomView *RoomView, key *tcell.EventKey) *tcell.EventKey {
 | 
			
		||||
	view.BumpFocus()
 | 
			
		||||
 | 
			
		||||
	k := key.Key()
 | 
			
		||||
@@ -220,8 +219,8 @@ func (view *MainView) KeyEventHandler(roomView *widget.RoomView, key *tcell.Even
 | 
			
		||||
 | 
			
		||||
const WheelScrollOffsetDiff = 3
 | 
			
		||||
 | 
			
		||||
func (view *MainView) MouseEventHandler(roomView *widget.RoomView, event *tcell.EventMouse) *tcell.EventMouse {
 | 
			
		||||
	if event.Buttons() == tcell.ButtonNone {
 | 
			
		||||
func (view *MainView) MouseEventHandler(roomView *RoomView, event *tcell.EventMouse) *tcell.EventMouse {
 | 
			
		||||
	if event.Buttons() == tcell.ButtonNone || event.HasMotion() {
 | 
			
		||||
		return event
 | 
			
		||||
	}
 | 
			
		||||
	view.BumpFocus()
 | 
			
		||||
@@ -247,7 +246,14 @@ func (view *MainView) MouseEventHandler(roomView *widget.RoomView, event *tcell.
 | 
			
		||||
			roomView.Room.MarkRead()
 | 
			
		||||
		}
 | 
			
		||||
	default:
 | 
			
		||||
		debug.Print("Mouse event received:", event.Buttons(), event.Modifiers(), x, y)
 | 
			
		||||
		mx, my, mw, mh := msgView.GetRect()
 | 
			
		||||
		if x >= mx && y >= my && x < mx+mw && y < my+mh {
 | 
			
		||||
			if msgView.HandleClick(x-mx, y-my, event.Buttons()) {
 | 
			
		||||
				view.parent.Render()
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			debug.Print("Mouse event received:", event.Buttons(), event.Modifiers(), x, y)
 | 
			
		||||
		}
 | 
			
		||||
		return event
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -300,7 +306,7 @@ func (view *MainView) addRoom(index int, room string) {
 | 
			
		||||
 | 
			
		||||
	view.roomList.Add(roomStore)
 | 
			
		||||
	if !view.roomView.HasPage(room) {
 | 
			
		||||
		roomView := widget.NewRoomView(roomStore).
 | 
			
		||||
		roomView := NewRoomView(roomStore).
 | 
			
		||||
			SetInputSubmitFunc(view.InputSubmit).
 | 
			
		||||
			SetInputChangedFunc(view.InputChanged).
 | 
			
		||||
			SetTabCompleteFunc(view.InputTabComplete).
 | 
			
		||||
@@ -310,7 +316,7 @@ func (view *MainView) addRoom(index int, room string) {
 | 
			
		||||
		view.roomView.AddPage(room, roomView, true, false)
 | 
			
		||||
		roomView.UpdateUserList()
 | 
			
		||||
 | 
			
		||||
		count, err := roomView.LoadHistory(view.config.HistoryDir)
 | 
			
		||||
		count, err := roomView.LoadHistory(view.gmx, view.config.HistoryDir)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			debug.Printf("Failed to load history of %s: %v", roomView.Room.GetTitle(), err)
 | 
			
		||||
		} else if count <= 0 {
 | 
			
		||||
@@ -319,7 +325,7 @@ func (view *MainView) addRoom(index int, room string) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MainView) GetRoom(id string) *widget.RoomView {
 | 
			
		||||
func (view *MainView) GetRoom(id string) ifc.RoomView {
 | 
			
		||||
	return view.rooms[id]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -352,7 +358,7 @@ func (view *MainView) RemoveRoom(room string) {
 | 
			
		||||
	} else {
 | 
			
		||||
		removeIndex = sort.StringSlice(view.roomIDs).Search(room)
 | 
			
		||||
	}
 | 
			
		||||
	view.roomList.Remove(roomView.Room)
 | 
			
		||||
	view.roomList.Remove(roomView.MxRoom())
 | 
			
		||||
	view.roomIDs = append(view.roomIDs[:removeIndex], view.roomIDs[removeIndex+1:]...)
 | 
			
		||||
	view.roomView.RemovePage(room)
 | 
			
		||||
	delete(view.rooms, room)
 | 
			
		||||
@@ -363,7 +369,7 @@ func (view *MainView) SetRooms(rooms []string) {
 | 
			
		||||
	view.roomIDs = rooms
 | 
			
		||||
	view.roomList.Clear()
 | 
			
		||||
	view.roomView.Clear()
 | 
			
		||||
	view.rooms = make(map[string]*widget.RoomView)
 | 
			
		||||
	view.rooms = make(map[string]*RoomView)
 | 
			
		||||
	for index, room := range rooms {
 | 
			
		||||
		view.addRoom(index, room)
 | 
			
		||||
	}
 | 
			
		||||
@@ -385,14 +391,14 @@ func sendNotification(room *rooms.Room, sender, text string, critical, sound boo
 | 
			
		||||
	notification.Send(sender, text, critical, sound)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MainView) NotifyMessage(room *rooms.Room, message *types.Message, should pushrules.PushActionArrayShould) {
 | 
			
		||||
func (view *MainView) NotifyMessage(room *rooms.Room, message ifc.Message, should pushrules.PushActionArrayShould) {
 | 
			
		||||
	// Whether or not the room where the message came is the currently shown room.
 | 
			
		||||
	isCurrent := room.ID == view.CurrentRoomID()
 | 
			
		||||
	// Whether or not the terminal window is focused.
 | 
			
		||||
	isFocused := view.lastFocusTime.Add(30 * time.Second).Before(time.Now())
 | 
			
		||||
 | 
			
		||||
	// Whether or not the push rules say this message should be notified about.
 | 
			
		||||
	shouldNotify := (should.Notify || !should.NotifySpecified) && message.Sender != view.config.Session.UserID
 | 
			
		||||
	shouldNotify := (should.Notify || !should.NotifySpecified) && message.Sender() != view.config.Session.UserID
 | 
			
		||||
 | 
			
		||||
	if !isCurrent {
 | 
			
		||||
		// The message is not in the current room, show new message status in room list.
 | 
			
		||||
@@ -406,21 +412,10 @@ func (view *MainView) NotifyMessage(room *rooms.Room, message *types.Message, sh
 | 
			
		||||
	if shouldNotify && !isFocused {
 | 
			
		||||
		// Push rules say notify and the terminal is not focused, send desktop notification.
 | 
			
		||||
		shouldPlaySound := should.PlaySound && should.SoundName == "default"
 | 
			
		||||
		sendNotification(room, message.Sender, message.Text, should.Highlight, shouldPlaySound)
 | 
			
		||||
		sendNotification(room, message.Sender(), message.NotificationContent(), should.Highlight, shouldPlaySound)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if should.Highlight {
 | 
			
		||||
		// Message is highlight, set color.
 | 
			
		||||
		message.TextColor = tcell.ColorYellow
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MainView) AddServiceMessage(roomView *widget.RoomView, text string) {
 | 
			
		||||
	message := roomView.NewMessage("", "*", "gomuks.service", text, time.Now())
 | 
			
		||||
	message.TextColor = tcell.ColorGray
 | 
			
		||||
	message.SenderColor = tcell.ColorGray
 | 
			
		||||
	roomView.AddMessage(message, widget.AppendMessage)
 | 
			
		||||
	view.parent.Render()
 | 
			
		||||
	message.SetIsHighlight(should.Highlight)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MainView) LoadHistory(room string, initial bool) {
 | 
			
		||||
@@ -452,20 +447,15 @@ func (view *MainView) LoadHistory(room string, initial bool) {
 | 
			
		||||
	}
 | 
			
		||||
	history, prevBatch, err := view.matrix.GetHistory(roomView.Room.ID, batch, 50)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		view.AddServiceMessage(roomView, "Failed to fetch history")
 | 
			
		||||
		roomView.AddServiceMessage("Failed to fetch history")
 | 
			
		||||
		debug.Print("Failed to fetch history for", roomView.Room.ID, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	roomView.Room.PrevBatch = prevBatch
 | 
			
		||||
	for _, evt := range history {
 | 
			
		||||
		var message *types.Message
 | 
			
		||||
		if evt.Type == "m.room.message" {
 | 
			
		||||
			message = view.ProcessMessageEvent(roomView, &evt)
 | 
			
		||||
		} else if evt.Type == "m.room.member" {
 | 
			
		||||
			message = view.ProcessMembershipEvent(roomView, &evt)
 | 
			
		||||
		}
 | 
			
		||||
		message := view.ParseEvent(roomView, &evt)
 | 
			
		||||
		if message != nil {
 | 
			
		||||
			roomView.AddMessage(message, widget.PrependMessage)
 | 
			
		||||
			roomView.AddMessage(message, ifc.PrependMessage)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	err = roomView.SaveHistory(view.config.HistoryDir)
 | 
			
		||||
@@ -476,63 +466,6 @@ func (view *MainView) LoadHistory(room string, initial bool) {
 | 
			
		||||
	view.parent.Render()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MainView) ProcessMessageEvent(room *widget.RoomView, evt *gomatrix.Event) (message *types.Message) {
 | 
			
		||||
	text, _ := evt.Content["body"].(string)
 | 
			
		||||
	msgtype, _ := evt.Content["msgtype"].(string)
 | 
			
		||||
	return room.NewMessage(evt.ID, evt.Sender, msgtype, text, unixToTime(evt.Timestamp))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MainView) getMembershipEventContent(evt *gomatrix.Event) (sender, text string) {
 | 
			
		||||
	membership, _ := evt.Content["membership"].(string)
 | 
			
		||||
	displayname, _ := evt.Content["displayname"].(string)
 | 
			
		||||
	if len(displayname) == 0 {
 | 
			
		||||
		displayname = *evt.StateKey
 | 
			
		||||
	}
 | 
			
		||||
	prevMembership := "leave"
 | 
			
		||||
	prevDisplayname := ""
 | 
			
		||||
	if evt.Unsigned.PrevContent != nil {
 | 
			
		||||
		prevMembership, _ = evt.Unsigned.PrevContent["membership"].(string)
 | 
			
		||||
		prevDisplayname, _ = evt.Unsigned.PrevContent["displayname"].(string)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if membership != prevMembership {
 | 
			
		||||
		switch membership {
 | 
			
		||||
		case "invite":
 | 
			
		||||
			sender = "---"
 | 
			
		||||
			text = fmt.Sprintf("%s invited %s.", evt.Sender, displayname)
 | 
			
		||||
		case "join":
 | 
			
		||||
			sender = "-->"
 | 
			
		||||
			text = fmt.Sprintf("%s joined the room.", displayname)
 | 
			
		||||
		case "leave":
 | 
			
		||||
			sender = "<--"
 | 
			
		||||
			if evt.Sender != *evt.StateKey {
 | 
			
		||||
				reason, _ := evt.Content["reason"].(string)
 | 
			
		||||
				text = fmt.Sprintf("%s kicked %s: %s", evt.Sender, displayname, reason)
 | 
			
		||||
			} else {
 | 
			
		||||
				text = fmt.Sprintf("%s left the room.", displayname)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	} else if displayname != prevDisplayname {
 | 
			
		||||
		sender = "---"
 | 
			
		||||
		text = fmt.Sprintf("%s changed their display name to %s.", prevDisplayname, displayname)
 | 
			
		||||
	}
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MainView) ProcessMembershipEvent(room *widget.RoomView, evt *gomatrix.Event) (message *types.Message) {
 | 
			
		||||
	sender, text := view.getMembershipEventContent(evt)
 | 
			
		||||
	if len(text) == 0 {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	message = room.NewMessage(evt.ID, sender, "m.room.member", text, unixToTime(evt.Timestamp))
 | 
			
		||||
	message.TextColor = tcell.ColorGreen
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func unixToTime(unix int64) time.Time {
 | 
			
		||||
	timestamp := time.Now()
 | 
			
		||||
	if unix != 0 {
 | 
			
		||||
		timestamp = time.Unix(unix/1000, unix%1000*1000)
 | 
			
		||||
	}
 | 
			
		||||
	return timestamp
 | 
			
		||||
func (view *MainView) ParseEvent(roomView ifc.RoomView, evt *gomatrix.Event) ifc.Message {
 | 
			
		||||
	return parser.ParseEvent(view.gmx, roomView.MxRoom(), evt)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ import (
 | 
			
		||||
	"strings"
 | 
			
		||||
	"unicode/utf8"
 | 
			
		||||
 | 
			
		||||
	"github.com/gdamore/tcell"
 | 
			
		||||
	"maunium.net/go/tcell"
 | 
			
		||||
	"github.com/mattn/go-runewidth"
 | 
			
		||||
	"github.com/zyedidia/clipboard"
 | 
			
		||||
	"maunium.net/go/tview"
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@
 | 
			
		||||
package widget
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/gdamore/tcell"
 | 
			
		||||
	"maunium.net/go/tcell"
 | 
			
		||||
	"maunium.net/go/tview"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@ import (
 | 
			
		||||
	"hash/fnv"
 | 
			
		||||
	"sort"
 | 
			
		||||
 | 
			
		||||
	"github.com/gdamore/tcell"
 | 
			
		||||
	"maunium.net/go/tcell"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var colorNames []string
 | 
			
		||||
 
 | 
			
		||||
@@ -1,354 +0,0 @@
 | 
			
		||||
// gomuks - A terminal Matrix client written in Go.
 | 
			
		||||
// Copyright (C) 2018 Tulir Asokan
 | 
			
		||||
//
 | 
			
		||||
// This program is free software: you can redistribute it and/or modify
 | 
			
		||||
// it under the terms of the GNU General Public License as published by
 | 
			
		||||
// the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
// (at your option) any later version.
 | 
			
		||||
//
 | 
			
		||||
// This program is distributed in the hope that it will be useful,
 | 
			
		||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
// GNU General Public License for more details.
 | 
			
		||||
//
 | 
			
		||||
// You should have received a copy of the GNU General Public License
 | 
			
		||||
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
package widget
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/gob"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"math"
 | 
			
		||||
	"os"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/gdamore/tcell"
 | 
			
		||||
	"maunium.net/go/gomuks/ui/debug"
 | 
			
		||||
	"maunium.net/go/gomuks/ui/types"
 | 
			
		||||
	"maunium.net/go/tview"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type MessageView struct {
 | 
			
		||||
	*tview.Box
 | 
			
		||||
 | 
			
		||||
	ScrollOffset    int
 | 
			
		||||
	MaxSenderWidth  int
 | 
			
		||||
	DateFormat      string
 | 
			
		||||
	TimestampFormat string
 | 
			
		||||
	TimestampWidth  int
 | 
			
		||||
	LoadingMessages bool
 | 
			
		||||
 | 
			
		||||
	widestSender int
 | 
			
		||||
	prevWidth    int
 | 
			
		||||
	prevHeight   int
 | 
			
		||||
	prevMsgCount int
 | 
			
		||||
 | 
			
		||||
	messageIDs map[string]*types.Message
 | 
			
		||||
	messages   []*types.Message
 | 
			
		||||
 | 
			
		||||
	textBuffer []string
 | 
			
		||||
	metaBuffer []types.MessageMeta
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewMessageView() *MessageView {
 | 
			
		||||
	return &MessageView{
 | 
			
		||||
		Box:             tview.NewBox(),
 | 
			
		||||
		MaxSenderWidth:  15,
 | 
			
		||||
		DateFormat:      "January _2, 2006",
 | 
			
		||||
		TimestampFormat: "15:04:05",
 | 
			
		||||
		TimestampWidth:  8,
 | 
			
		||||
		ScrollOffset:    0,
 | 
			
		||||
 | 
			
		||||
		messages:   make([]*types.Message, 0),
 | 
			
		||||
		messageIDs: make(map[string]*types.Message),
 | 
			
		||||
		textBuffer: make([]string, 0),
 | 
			
		||||
		metaBuffer: make([]types.MessageMeta, 0),
 | 
			
		||||
 | 
			
		||||
		widestSender: 5,
 | 
			
		||||
		prevWidth:    -1,
 | 
			
		||||
		prevHeight:   -1,
 | 
			
		||||
		prevMsgCount: -1,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MessageView) NewMessage(id, sender, msgtype, text string, timestamp time.Time) *types.Message {
 | 
			
		||||
	return types.NewMessage(id, sender, msgtype, text,
 | 
			
		||||
		timestamp.Format(view.TimestampFormat),
 | 
			
		||||
		timestamp.Format(view.DateFormat),
 | 
			
		||||
		GetHashColor(sender))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MessageView) SaveHistory(path string) error {
 | 
			
		||||
	file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0600)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer file.Close()
 | 
			
		||||
 | 
			
		||||
	enc := gob.NewEncoder(file)
 | 
			
		||||
	err = enc.Encode(view.messages)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MessageView) LoadHistory(path string) (int, error) {
 | 
			
		||||
	file, err := os.OpenFile(path, os.O_RDONLY, 0600)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if os.IsNotExist(err) {
 | 
			
		||||
			return 0, nil
 | 
			
		||||
		}
 | 
			
		||||
		return -1, err
 | 
			
		||||
	}
 | 
			
		||||
	defer file.Close()
 | 
			
		||||
 | 
			
		||||
	dec := gob.NewDecoder(file)
 | 
			
		||||
	err = dec.Decode(&view.messages)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return -1, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, message := range view.messages {
 | 
			
		||||
		view.updateWidestSender(message.Sender)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return len(view.messages), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MessageView) updateWidestSender(sender string) {
 | 
			
		||||
	if len(sender) > view.widestSender {
 | 
			
		||||
		view.widestSender = len(sender)
 | 
			
		||||
		if view.widestSender > view.MaxSenderWidth {
 | 
			
		||||
			view.widestSender = view.MaxSenderWidth
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type MessageDirection int
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	AppendMessage MessageDirection = iota
 | 
			
		||||
	PrependMessage
 | 
			
		||||
	IgnoreMessage
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (view *MessageView) UpdateMessageID(message *types.Message, newID string) {
 | 
			
		||||
	delete(view.messageIDs, message.ID)
 | 
			
		||||
	message.ID = newID
 | 
			
		||||
	view.messageIDs[message.ID] = message
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MessageView) AddMessage(message *types.Message, direction MessageDirection) {
 | 
			
		||||
	if message == nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	msg, messageExists := view.messageIDs[message.ID]
 | 
			
		||||
	if msg != nil && messageExists {
 | 
			
		||||
		message.CopyTo(msg)
 | 
			
		||||
		message = msg
 | 
			
		||||
		direction = IgnoreMessage
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	view.updateWidestSender(message.Sender)
 | 
			
		||||
 | 
			
		||||
	_, _, width, _ := view.GetInnerRect()
 | 
			
		||||
	width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap
 | 
			
		||||
	message.CalculateBuffer(width)
 | 
			
		||||
 | 
			
		||||
	if direction == AppendMessage {
 | 
			
		||||
		if view.ScrollOffset > 0 {
 | 
			
		||||
			view.ScrollOffset += message.Height()
 | 
			
		||||
		}
 | 
			
		||||
		view.messages = append(view.messages, message)
 | 
			
		||||
		view.appendBuffer(message)
 | 
			
		||||
	} else if direction == PrependMessage {
 | 
			
		||||
		view.messages = append([]*types.Message{message}, view.messages...)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	view.messageIDs[message.ID] = message
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MessageView) appendBuffer(message *types.Message) {
 | 
			
		||||
	if len(view.metaBuffer) > 0 {
 | 
			
		||||
		prevMeta := view.metaBuffer[len(view.metaBuffer)-1]
 | 
			
		||||
		if prevMeta != nil && prevMeta.GetDate() != message.Date {
 | 
			
		||||
			view.textBuffer = append(view.textBuffer, fmt.Sprintf("Date changed to %s", message.Date))
 | 
			
		||||
			view.metaBuffer = append(view.metaBuffer, &types.BasicMeta{TextColor: tcell.ColorGreen})
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	view.textBuffer = append(view.textBuffer, message.Buffer()...)
 | 
			
		||||
	for range message.Buffer() {
 | 
			
		||||
		view.metaBuffer = append(view.metaBuffer, message)
 | 
			
		||||
	}
 | 
			
		||||
	view.prevMsgCount++
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MessageView) recalculateBuffers() {
 | 
			
		||||
	_, _, width, height := view.GetInnerRect()
 | 
			
		||||
 | 
			
		||||
	width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap
 | 
			
		||||
	recalculateMessageBuffers := width != view.prevWidth
 | 
			
		||||
	if height != view.prevHeight || recalculateMessageBuffers || len(view.messages) != view.prevMsgCount {
 | 
			
		||||
		view.textBuffer = []string{}
 | 
			
		||||
		view.metaBuffer = []types.MessageMeta{}
 | 
			
		||||
		view.prevMsgCount = 0
 | 
			
		||||
		for _, message := range view.messages {
 | 
			
		||||
			if recalculateMessageBuffers {
 | 
			
		||||
				message.CalculateBuffer(width)
 | 
			
		||||
			}
 | 
			
		||||
			view.appendBuffer(message)
 | 
			
		||||
		}
 | 
			
		||||
		view.prevHeight = height
 | 
			
		||||
		view.prevWidth = width
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const PaddingAtTop = 5
 | 
			
		||||
 | 
			
		||||
func (view *MessageView) AddScrollOffset(diff int) {
 | 
			
		||||
	_, _, _, height := view.GetInnerRect()
 | 
			
		||||
 | 
			
		||||
	totalHeight := len(view.textBuffer)
 | 
			
		||||
	if diff >= 0 && view.ScrollOffset+diff >= totalHeight-height+PaddingAtTop {
 | 
			
		||||
		view.ScrollOffset = totalHeight - height + PaddingAtTop
 | 
			
		||||
	} else {
 | 
			
		||||
		view.ScrollOffset += diff
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if view.ScrollOffset > totalHeight-height+PaddingAtTop {
 | 
			
		||||
		view.ScrollOffset = totalHeight - height + PaddingAtTop
 | 
			
		||||
	}
 | 
			
		||||
	if view.ScrollOffset < 0 {
 | 
			
		||||
		view.ScrollOffset = 0
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MessageView) Height() int {
 | 
			
		||||
	_, _, _, height := view.GetInnerRect()
 | 
			
		||||
	return height
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MessageView) TotalHeight() int {
 | 
			
		||||
	return len(view.textBuffer)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MessageView) IsAtTop() bool {
 | 
			
		||||
	_, _, _, height := view.GetInnerRect()
 | 
			
		||||
	totalHeight := len(view.textBuffer)
 | 
			
		||||
	return view.ScrollOffset >= totalHeight-height+PaddingAtTop
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	TimestampSenderGap = 1
 | 
			
		||||
	SenderSeparatorGap = 1
 | 
			
		||||
	SenderMessageGap   = 3
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func getScrollbarStyle(scrollbarHere, isTop, isBottom bool) (char rune, style tcell.Style) {
 | 
			
		||||
	char = '│'
 | 
			
		||||
	style = tcell.StyleDefault
 | 
			
		||||
	if scrollbarHere {
 | 
			
		||||
		style = style.Foreground(tcell.ColorGreen)
 | 
			
		||||
	}
 | 
			
		||||
	if isTop {
 | 
			
		||||
		if scrollbarHere {
 | 
			
		||||
			char = '╥'
 | 
			
		||||
		} else {
 | 
			
		||||
			char = '┬'
 | 
			
		||||
		}
 | 
			
		||||
	} else if isBottom {
 | 
			
		||||
		if scrollbarHere {
 | 
			
		||||
			char = '╨'
 | 
			
		||||
		} else {
 | 
			
		||||
			char = '┴'
 | 
			
		||||
		}
 | 
			
		||||
	} else if scrollbarHere {
 | 
			
		||||
		char = '║'
 | 
			
		||||
	}
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (view *MessageView) Draw(screen tcell.Screen) {
 | 
			
		||||
	view.Box.Draw(screen)
 | 
			
		||||
 | 
			
		||||
	x, y, _, height := view.GetInnerRect()
 | 
			
		||||
	view.recalculateBuffers()
 | 
			
		||||
 | 
			
		||||
	if len(view.textBuffer) == 0 {
 | 
			
		||||
		writeLineSimple(screen, "It's quite empty in here.", x, y+height)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	usernameX := x + view.TimestampWidth + TimestampSenderGap
 | 
			
		||||
	messageX := usernameX + view.widestSender + SenderMessageGap
 | 
			
		||||
	separatorX := usernameX + view.widestSender + SenderSeparatorGap
 | 
			
		||||
 | 
			
		||||
	indexOffset := len(view.textBuffer) - view.ScrollOffset - height
 | 
			
		||||
	if indexOffset <= -PaddingAtTop {
 | 
			
		||||
		message := "Scroll up to load more messages."
 | 
			
		||||
		if view.LoadingMessages {
 | 
			
		||||
			message = "Loading more messages..."
 | 
			
		||||
		}
 | 
			
		||||
		writeLineSimpleColor(screen, message, messageX, y, tcell.ColorGreen)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(view.textBuffer) != len(view.metaBuffer) {
 | 
			
		||||
		debug.ExtPrintf("Unexpected text/meta buffer length mismatch: %d != %d.", len(view.textBuffer), len(view.metaBuffer))
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var scrollBarHeight, scrollBarPos int
 | 
			
		||||
	// Black magic (aka math) used to figure out where the scroll bar should be put.
 | 
			
		||||
	{
 | 
			
		||||
		viewportHeight := float64(height)
 | 
			
		||||
		contentHeight := float64(len(view.textBuffer))
 | 
			
		||||
 | 
			
		||||
		scrollBarHeight = int(math.Ceil(viewportHeight / (contentHeight / viewportHeight)))
 | 
			
		||||
 | 
			
		||||
		scrollBarPos = height - int(math.Round(float64(view.ScrollOffset)/contentHeight*viewportHeight))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var prevMeta types.MessageMeta
 | 
			
		||||
	firstLine := true
 | 
			
		||||
	skippedLines := 0
 | 
			
		||||
 | 
			
		||||
	for line := 0; line < height; line++ {
 | 
			
		||||
		index := indexOffset + line
 | 
			
		||||
		if index < 0 {
 | 
			
		||||
			skippedLines++
 | 
			
		||||
			continue
 | 
			
		||||
		} else if index >= len(view.textBuffer) {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		showScrollbar := line-skippedLines >= scrollBarPos-scrollBarHeight && line-skippedLines < scrollBarPos
 | 
			
		||||
		isTop := firstLine && view.ScrollOffset+height >= len(view.textBuffer)
 | 
			
		||||
		isBottom := line == height-1 && view.ScrollOffset == 0
 | 
			
		||||
 | 
			
		||||
		borderChar, borderStyle := getScrollbarStyle(showScrollbar, isTop, isBottom)
 | 
			
		||||
 | 
			
		||||
		firstLine = false
 | 
			
		||||
 | 
			
		||||
		screen.SetContent(separatorX, y+line, borderChar, nil, borderStyle)
 | 
			
		||||
 | 
			
		||||
		text, meta := view.textBuffer[index], view.metaBuffer[index]
 | 
			
		||||
		if meta != prevMeta {
 | 
			
		||||
			if len(meta.GetTimestamp()) > 0 {
 | 
			
		||||
				writeLineSimpleColor(screen, meta.GetTimestamp(), x, y+line, meta.GetTimestampColor())
 | 
			
		||||
			}
 | 
			
		||||
			if prevMeta == nil || meta.GetSender() != prevMeta.GetSender() {
 | 
			
		||||
				writeLineColor(
 | 
			
		||||
					screen, tview.AlignRight, meta.GetSender(),
 | 
			
		||||
					usernameX, y+line, view.widestSender,
 | 
			
		||||
					meta.GetSenderColor())
 | 
			
		||||
			}
 | 
			
		||||
			prevMeta = meta
 | 
			
		||||
		}
 | 
			
		||||
		writeLineSimpleColor(screen, text, messageX, y+line, meta.GetTextColor())
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -17,24 +17,24 @@
 | 
			
		||||
package widget
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/gdamore/tcell"
 | 
			
		||||
	"maunium.net/go/tcell"
 | 
			
		||||
	"github.com/mattn/go-runewidth"
 | 
			
		||||
	"maunium.net/go/tview"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func writeLineSimple(screen tcell.Screen, line string, x, y int) {
 | 
			
		||||
	writeLine(screen, tview.AlignLeft, line, x, y, 1<<30, tcell.StyleDefault)
 | 
			
		||||
func WriteLineSimple(screen tcell.Screen, line string, x, y int) {
 | 
			
		||||
	WriteLine(screen, tview.AlignLeft, line, x, y, 1<<30, tcell.StyleDefault)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func writeLineSimpleColor(screen tcell.Screen, line string, x, y int, color tcell.Color) {
 | 
			
		||||
	writeLine(screen, tview.AlignLeft, line, x, y, 1<<30, tcell.StyleDefault.Foreground(color))
 | 
			
		||||
func WriteLineSimpleColor(screen tcell.Screen, line string, x, y int, color tcell.Color) {
 | 
			
		||||
	WriteLine(screen, tview.AlignLeft, line, x, y, 1<<30, tcell.StyleDefault.Foreground(color))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func writeLineColor(screen tcell.Screen, align int, line string, x, y, maxWidth int, color tcell.Color) {
 | 
			
		||||
	writeLine(screen, align, line, x, y, maxWidth, tcell.StyleDefault.Foreground(color))
 | 
			
		||||
func WriteLineColor(screen tcell.Screen, align int, line string, x, y, maxWidth int, color tcell.Color) {
 | 
			
		||||
	WriteLine(screen, align, line, x, y, maxWidth, tcell.StyleDefault.Foreground(color))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func writeLine(screen tcell.Screen, align int, line string, x, y, maxWidth int, style tcell.Style) {
 | 
			
		||||
func WriteLine(screen tcell.Screen, align int, line string, x, y, maxWidth int, style tcell.Style) {
 | 
			
		||||
	offsetX := 0
 | 
			
		||||
	if align == tview.AlignRight {
 | 
			
		||||
		offsetX = maxWidth - runewidth.StringWidth(line)
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user