Fix things

This commit is contained in:
Tulir Asokan 2018-11-14 00:00:35 +02:00
parent cfb2cc057c
commit ba387764ca
91 changed files with 3316 additions and 1557 deletions

94
Gopkg.lock generated
View File

@ -2,79 +2,104 @@
[[projects]] [[projects]]
digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec"
name = "github.com/davecgh/go-spew" name = "github.com/davecgh/go-spew"
packages = ["spew"] packages = ["spew"]
pruneopts = "UT"
revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73" revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73"
version = "v1.1.1" version = "v1.1.1"
[[projects]] [[projects]]
digest = "1:2694597a20d303502ad1b7f1cc688977c7dc34da732ccf4d27e98b552cceed5b"
name = "github.com/disintegration/imaging" name = "github.com/disintegration/imaging"
packages = ["."] packages = ["."]
pruneopts = "UT"
revision = "0bd5694c78c9c3d9a3cd06a706a8f3c59296a9ac" revision = "0bd5694c78c9c3d9a3cd06a706a8f3c59296a9ac"
version = "v1.5.0" version = "v1.5.0"
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:40d0056c1b1f503c366ba441df92a82b5a2654d6f3747b1689a611eb5c9ce0a2"
name = "github.com/gdamore/encoding" name = "github.com/gdamore/encoding"
packages = ["."] packages = ["."]
pruneopts = "UT"
revision = "b23993cbb6353f0e6aa98d0ee318a34728f628b9" revision = "b23993cbb6353f0e6aa98d0ee318a34728f628b9"
[[projects]] [[projects]]
digest = "1:f4bd9b6cfcbaafa94259861747781022d8f06ff3c88aafe49a815ac02609bc96"
name = "github.com/kyokomi/emoji" name = "github.com/kyokomi/emoji"
packages = ["."] packages = ["."]
pruneopts = "UT"
revision = "2e9a9507333f3ee28f3fab88c2c3aba34455d734" revision = "2e9a9507333f3ee28f3fab88c2c3aba34455d734"
version = "v1.5.1" version = "v1.5.1"
[[projects]] [[projects]]
digest = "1:c65a16ac77d0b1aefc7009cabb6ac5ad05def02025f5be85f450c03f52cc6f86"
name = "github.com/lucasb-eyer/go-colorful" name = "github.com/lucasb-eyer/go-colorful"
packages = ["."] packages = ["."]
pruneopts = "UT"
revision = "345fbb3dbcdb252d9985ee899a84963c0fa24c82" revision = "345fbb3dbcdb252d9985ee899a84963c0fa24c82"
version = "v1.0" version = "v1.0"
[[projects]] [[projects]]
digest = "1:cdb899c199f907ac9fb50495ec71212c95cb5b0e0a8ee0800da0238036091033"
name = "github.com/mattn/go-runewidth" name = "github.com/mattn/go-runewidth"
packages = ["."] packages = ["."]
pruneopts = "UT"
revision = "ce7b0b5c7b45a81508558cd1dba6bb1e4ddb51bb" revision = "ce7b0b5c7b45a81508558cd1dba6bb1e4ddb51bb"
version = "v0.0.3" version = "v0.0.3"
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:0e1e5f960c58fdc677212fcc70e55042a0084d367623e51afbdb568963832f5d"
name = "github.com/nu7hatch/gouuid" name = "github.com/nu7hatch/gouuid"
packages = ["."] packages = ["."]
pruneopts = "UT"
revision = "179d4d0c4d8d407a32af483c2354df1d2c91e6c3" revision = "179d4d0c4d8d407a32af483c2354df1d2c91e6c3"
[[projects]] [[projects]]
digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe"
name = "github.com/pmezard/go-difflib" name = "github.com/pmezard/go-difflib"
packages = ["difflib"] packages = ["difflib"]
pruneopts = "UT"
revision = "792786c7400a136282c1664665ae0a8db921c6c2" revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0" version = "v1.0.0"
[[projects]] [[projects]]
branch = "master"
digest = "1:3fd3d634f6815f19ac4b2c5e16d28ec9aa4584d0bba25d1ee6c424d813cca22a"
name = "github.com/renstrom/fuzzysearch" name = "github.com/renstrom/fuzzysearch"
packages = ["fuzzy"] packages = ["fuzzy"]
pruneopts = "UT"
revision = "b18e754edff4833912ef4dce9eaca885bd3f0de1" revision = "b18e754edff4833912ef4dce9eaca885bd3f0de1"
version = "v1.0.1"
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:def689e73e9252f6f7fe66834a76751a41b767e03daab299e607e7226c58a855"
name = "github.com/shurcooL/sanitized_anchor_name" name = "github.com/shurcooL/sanitized_anchor_name"
packages = ["."] packages = ["."]
pruneopts = "UT"
revision = "86672fcb3f950f35f2e675df2240550f2a50762f" revision = "86672fcb3f950f35f2e675df2240550f2a50762f"
[[projects]] [[projects]]
digest = "1:18752d0b95816a1b777505a97f71c7467a8445b8ffb55631a7bf779f6ba4fa83"
name = "github.com/stretchr/testify" name = "github.com/stretchr/testify"
packages = ["assert"] packages = ["assert"]
pruneopts = "UT"
revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686" revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686"
version = "v1.2.2" version = "v1.2.2"
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:22738a5261588086d75acb3c792f7c931d30fd32e351117e9e046331135f5297"
name = "github.com/zyedidia/clipboard" name = "github.com/zyedidia/clipboard"
packages = ["."] packages = ["."]
pruneopts = "UT"
revision = "bd31d747117d04b4e25b61f73e1ea4faeea3c56a" revision = "bd31d747117d04b4e25b61f73e1ea4faeea3c56a"
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:441333ba1f7f3074035297647244e3d1ce3665e4e7c5054b3a8b138463f8ff58"
name = "golang.org/x/image" name = "golang.org/x/image"
packages = [ packages = [
"bmp", "bmp",
@ -83,79 +108,108 @@
"tiff/lzw", "tiff/lzw",
"vp8", "vp8",
"vp8l", "vp8l",
"webp" "webp",
] ]
revision = "c73c2afc3b812cdd6385de5a50616511c4a3d458" pruneopts = "UT"
revision = "249dc8530c0efc2766606a940c1c50b434b2f1cd"
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:1a1ecfa7b54ca3f7a0115ab5c578d7d6a5d8b605839c549e80260468c42f8be7"
name = "golang.org/x/net" name = "golang.org/x/net"
packages = [ packages = [
"html", "html",
"html/atom" "html/atom",
] ]
revision = "8a410e7b638dca158bf9e766925842f6651ff828" pruneopts = "UT"
revision = "88d92db4c548972d942ac2a3531a8a9a34c82ca6"
[[projects]] [[projects]]
digest = "1:37672ad5821719e2df8509c2edd4ba5ae192463237c73c3a2d24ef8b2bc9e36f"
name = "golang.org/x/text" name = "golang.org/x/text"
packages = [ packages = [
"encoding", "encoding",
"encoding/internal/identifier", "encoding/internal/identifier",
"internal/gen", "internal/gen",
"transform", "transform",
"unicode/cldr" "unicode/cldr",
] ]
pruneopts = "UT"
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
version = "v0.3.0" version = "v0.3.0"
[[projects]] [[projects]]
digest = "1:2ee0f15eb0fb04f918db7c2dcf39745f40d69f798ef171610a730e8a56aaa4fd"
name = "gopkg.in/russross/blackfriday.v2" name = "gopkg.in/russross/blackfriday.v2"
packages = ["."] packages = ["."]
revision = "cadec560ec52d93835bf2f15bd794700d3a2473b" pruneopts = "UT"
version = "v2.0.0" revision = "d3b5b032dc8e8927d31a5071b56e14c89f045135"
version = "v2.0.1"
[[projects]] [[projects]]
branch = "v1" branch = "v1"
digest = "1:b71ccd8615521db4ea2f9381f55780a1312654f5d8431d2e58eca51c74110de2"
name = "gopkg.in/toast.v1" name = "gopkg.in/toast.v1"
packages = ["."] packages = ["."]
pruneopts = "UT"
revision = "0a84660828b24d25b35525c9a1f1f51267f8da91" revision = "0a84660828b24d25b35525c9a1f1f51267f8da91"
[[projects]] [[projects]]
digest = "1:342378ac4dcb378a5448dd723f0784ae519383532f5e70ade24132c4c8693202"
name = "gopkg.in/yaml.v2" name = "gopkg.in/yaml.v2"
packages = ["."] packages = ["."]
pruneopts = "UT"
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183" revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
version = "v2.2.1" version = "v2.2.1"
[[projects]] [[projects]]
branch = "master" branch = "master"
name = "maunium.net/go/gomatrix" digest = "1:d23e0bbfa4e45fe847e61fdfd442b24adf9bd3d0b050a846f9ef0f3af4f43c86"
name = "maunium.net/go/mautrix"
packages = ["."] packages = ["."]
revision = "d651abc3ecb4bbd85a6b17b710632e73ddbbc6aa" pruneopts = "UT"
revision = "e8080dcf484d1db9021d2019fee132ffc9e37e3c"
[[projects]]
name = "maunium.net/go/maulogger"
packages = ["."]
revision = "64f0aa33b6c51313e15575257db71dec44fe7988"
version = "v1.0"
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:28d6d6123ce29c9b0b5d393af0085f1454a9d93d70b4c9893efe6cff025702b9"
name = "maunium.net/go/tcell" name = "maunium.net/go/tcell"
packages = [ packages = [
".", ".",
"terminfo" "terminfo",
] ]
revision = "f32e44a866ab0475655e11ede91337d7b562956d" pruneopts = "UT"
revision = "a62189e4543d731b94ae0d3b659d1853975ea657"
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:b4c2e264221b6afa98aec4e0a097f3775cb014eebac8a6f49d33c53b237b77af"
name = "maunium.net/go/tview" name = "maunium.net/go/tview"
packages = ["."] packages = ["."]
revision = "8b261597bbdb95dcaef03854aaa0cc192f56b1ff" pruneopts = "UT"
revision = "dae31f32cda75339f5ec2a64411ef3ec1862ef5e"
[solve-meta] [solve-meta]
analyzer-name = "dep" analyzer-name = "dep"
analyzer-version = 1 analyzer-version = 1
inputs-digest = "6c0a263ebffa1c073f4b67a895832017afeab906b0d88b31da38ae22bc75e1aa" input-imports = [
"github.com/disintegration/imaging",
"github.com/kyokomi/emoji",
"github.com/lucasb-eyer/go-colorful",
"github.com/mattn/go-runewidth",
"github.com/renstrom/fuzzysearch/fuzzy",
"github.com/stretchr/testify/assert",
"github.com/zyedidia/clipboard",
"golang.org/x/image/bmp",
"golang.org/x/image/tiff",
"golang.org/x/image/webp",
"golang.org/x/net/html",
"gopkg.in/russross/blackfriday.v2",
"gopkg.in/toast.v1",
"gopkg.in/yaml.v2",
"maunium.net/go/mautrix",
"maunium.net/go/tcell",
"maunium.net/go/tview",
]
solver-name = "gps-cdcl" solver-name = "gps-cdcl"
solver-version = 1 solver-version = 1

View File

@ -63,7 +63,7 @@
[[constraint]] [[constraint]]
branch = "master" branch = "master"
name = "maunium.net/go/gomatrix" name = "maunium.net/go/mautrix"
[[constraint]] [[constraint]]
branch = "master" branch = "master"

View File

@ -8,7 +8,7 @@
![Chat Preview](chat-preview.png) ![Chat Preview](chat-preview.png)
A terminal Matrix client written in Go using [gomatrix](https://github.com/matrix-org/gomatrix) and [tview](https://github.com/rivo/tview). A terminal Matrix client written in Go using [mautrix](https://github.com/matrix-org/mautrix) and [tview](https://github.com/rivo/tview).
Basic usage is possible, but expect bugs and missing features. Basic usage is possible, but expect bugs and missing features.

View File

@ -25,7 +25,7 @@ import (
"strings" "strings"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
"maunium.net/go/gomatrix" "maunium.net/go/mautrix"
"maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/debug"
"maunium.net/go/gomuks/matrix/pushrules" "maunium.net/go/gomuks/matrix/pushrules"
"maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/matrix/rooms"
@ -289,12 +289,12 @@ func (config *Config) PutRoom(room *rooms.Room) {
room.Save(config.getRoomCachePath(room)) room.Save(config.getRoomCachePath(room))
} }
func (config *Config) SaveRoom(room *gomatrix.Room) { func (config *Config) SaveRoom(room *mautrix.Room) {
gmxRoom := config.GetRoom(room.ID) gmxRoom := config.GetRoom(room.ID)
gmxRoom.Room = room gmxRoom.Room = room
gmxRoom.Save(config.getRoomCachePath(gmxRoom)) gmxRoom.Save(config.getRoomCachePath(gmxRoom))
} }
func (config *Config) LoadRoom(roomID string) *gomatrix.Room { func (config *Config) LoadRoom(roomID string) *mautrix.Room {
return config.GetRoom(roomID).Room return config.GetRoom(roomID).Room
} }

View File

@ -17,12 +17,12 @@
package ifc package ifc
import ( import (
"maunium.net/go/gomatrix" "maunium.net/go/mautrix"
"maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/matrix/rooms"
) )
type MatrixContainer interface { type MatrixContainer interface {
Client() *gomatrix.Client Client() *mautrix.Client
InitClient() error InitClient() error
Initialized() bool Initialized() bool
@ -33,14 +33,14 @@ type MatrixContainer interface {
Logout() Logout()
SendPreferencesToMatrix() SendPreferencesToMatrix()
SendMessage(roomID string, msgtype gomatrix.MessageType, message string) (string, error) SendMessage(roomID string, msgtype mautrix.MessageType, message string) (string, error)
SendMarkdownMessage(roomID string, msgtype gomatrix.MessageType, message string) (string, error) SendMarkdownMessage(roomID string, msgtype mautrix.MessageType, message string) (string, error)
SendTyping(roomID string, typing bool) SendTyping(roomID string, typing bool)
MarkRead(roomID, eventID string) MarkRead(roomID, eventID string)
JoinRoom(roomID, server string) (*rooms.Room, error) JoinRoom(roomID, server string) (*rooms.Room, error)
LeaveRoom(roomID string) error LeaveRoom(roomID string) error
GetHistory(roomID, prevBatch string, limit int) ([]*gomatrix.Event, string, error) GetHistory(roomID, prevBatch string, limit int) ([]*mautrix.Event, string, error)
GetRoom(roomID string) *rooms.Room GetRoom(roomID string) *rooms.Room
Download(mxcURL string) ([]byte, string, string, error) Download(mxcURL string) ([]byte, string, string, error)

View File

@ -19,7 +19,7 @@ package ifc
import ( import (
"time" "time"
"maunium.net/go/gomatrix" "maunium.net/go/mautrix"
"maunium.net/go/gomuks/matrix/pushrules" "maunium.net/go/gomuks/matrix/pushrules"
"maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/matrix/rooms"
"maunium.net/go/tcell" "maunium.net/go/tcell"
@ -50,7 +50,7 @@ type MainView interface {
UpdateTags(room *rooms.Room) UpdateTags(room *rooms.Room)
SetTyping(roomID string, users []string) SetTyping(roomID string, users []string)
ParseEvent(roomView RoomView, evt *gomatrix.Event) Message ParseEvent(roomView RoomView, evt *mautrix.Event) Message
NotifyMessage(room *rooms.Room, message Message, should pushrules.PushActionArrayShould) NotifyMessage(room *rooms.Room, message Message, should pushrules.PushActionArrayShould)
InitialSyncDone() InitialSyncDone()
@ -73,8 +73,8 @@ type RoomView interface {
SetTyping(users []string) SetTyping(users []string)
UpdateUserList() UpdateUserList()
NewMessage(id, sender string, msgtype gomatrix.MessageType, text string, timestamp time.Time) Message NewMessage(id, sender string, msgtype mautrix.MessageType, text string, timestamp time.Time) Message
NewTempMessage(msgtype gomatrix.MessageType, text string) Message NewTempMessage(msgtype mautrix.MessageType, text string) Message
AddMessage(message Message, direction MessageDirection) AddMessage(message Message, direction MessageDirection)
AddServiceMessage(message string) AddServiceMessage(message string)
} }
@ -111,8 +111,8 @@ type Message interface {
SetID(id string) SetID(id string)
ID() string ID() string
SetType(msgtype gomatrix.MessageType) SetType(msgtype mautrix.MessageType)
Type() gomatrix.MessageType Type() mautrix.MessageType
NotificationContent() string NotificationContent() string

View File

@ -1,2 +1,2 @@
// Package matrix contains wrappers for gomatrix for use by the UI of gomuks. // Package matrix contains wrappers for mautrix for use by the UI of gomuks.
package matrix package matrix

View File

@ -34,20 +34,20 @@ import (
"encoding/json" "encoding/json"
"gopkg.in/russross/blackfriday.v2" "gopkg.in/russross/blackfriday.v2"
"maunium.net/go/gomatrix"
"maunium.net/go/gomuks/config" "maunium.net/go/gomuks/config"
"maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/debug"
"maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/interface"
"maunium.net/go/gomuks/lib/bfhtml" "maunium.net/go/gomuks/lib/bfhtml"
"maunium.net/go/gomuks/matrix/pushrules" "maunium.net/go/gomuks/matrix/pushrules"
"maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/matrix/rooms"
"maunium.net/go/mautrix"
) )
// Container is a wrapper for a gomatrix Client and some other stuff. // Container is a wrapper for a mautrix Client and some other stuff.
// //
// It is used for all Matrix calls from the UI and Matrix event handlers. // It is used for all Matrix calls from the UI and Matrix event handlers.
type Container struct { type Container struct {
client *gomatrix.Client client *mautrix.Client
syncer *GomuksSyncer syncer *GomuksSyncer
gmx ifc.Gomuks gmx ifc.Gomuks
ui ifc.GomuksUI ui ifc.GomuksUI
@ -69,12 +69,18 @@ func NewContainer(gmx ifc.Gomuks) *Container {
return c return c
} }
// Client returns the underlying gomatrix Client. // Client returns the underlying mautrix Client.
func (c *Container) Client() *gomatrix.Client { func (c *Container) Client() *mautrix.Client {
return c.client return c.client
} }
// InitClient initializes the gomatrix client and connects to the homeserver specified in the config. type mxLogger struct{}
func (log mxLogger) Debugfln(message string, args ...interface{}) {
debug.Printf("[Matrix] "+message, args...)
}
// InitClient initializes the mautrix client and connects to the homeserver specified in the config.
func (c *Container) InitClient() error { func (c *Container) InitClient() error {
if len(c.config.HS) == 0 { if len(c.config.HS) == 0 {
return fmt.Errorf("no homeserver in config") return fmt.Errorf("no homeserver in config")
@ -92,10 +98,11 @@ func (c *Container) InitClient() error {
} }
var err error var err error
c.client, err = gomatrix.NewClient(c.config.HS, mxid, accessToken) c.client, err = mautrix.NewClient(c.config.HS, mxid, accessToken)
if err != nil { if err != nil {
return err return err
} }
c.client.Logger = mxLogger{}
allowInsecure := len(os.Getenv("GOMUKS_ALLOW_INSECURE_CONNECTIONS")) > 0 allowInsecure := len(os.Getenv("GOMUKS_ALLOW_INSECURE_CONNECTIONS")) > 0
if allowInsecure { if allowInsecure {
@ -112,14 +119,14 @@ func (c *Container) InitClient() error {
return nil return nil
} }
// Initialized returns whether or not the gomatrix client is initialized (see InitClient()) // Initialized returns whether or not the mautrix client is initialized (see InitClient())
func (c *Container) Initialized() bool { func (c *Container) Initialized() bool {
return c.client != nil return c.client != nil
} }
// Login sends a password login request with the given username and password. // Login sends a password login request with the given username and password.
func (c *Container) Login(user, password string) error { func (c *Container) Login(user, password string) error {
resp, err := c.client.Login(&gomatrix.ReqLogin{ resp, err := c.client.Login(&mautrix.ReqLogin{
Type: "m.login.password", Type: "m.login.password",
User: user, User: user,
Password: password, Password: password,
@ -175,7 +182,7 @@ func (c *Container) PushRules() *pushrules.PushRuleset {
return c.config.PushRules return c.config.PushRules
} }
var AccountDataGomuksPreferences = gomatrix.NewEventType("net.maunium.gomuks.preferences") var AccountDataGomuksPreferences = mautrix.NewEventType("net.maunium.gomuks.preferences")
// OnLogin initializes the syncer and updates the room list. // OnLogin initializes the syncer and updates the room list.
func (c *Container) OnLogin() { func (c *Container) OnLogin() {
@ -185,15 +192,16 @@ func (c *Container) OnLogin() {
debug.Print("Initializing syncer") debug.Print("Initializing syncer")
c.syncer = NewGomuksSyncer(c.config) c.syncer = NewGomuksSyncer(c.config)
c.syncer.OnEventType(gomatrix.EventMessage, c.HandleMessage) c.syncer.OnEventType(mautrix.EventMessage, c.HandleMessage)
c.syncer.OnEventType(gomatrix.StateMember, c.HandleMembership) c.syncer.OnEventType(mautrix.StateMember, c.HandleMembership)
c.syncer.OnEventType(gomatrix.EphemeralEventReceipt, c.HandleReadReceipt) c.syncer.OnEventType(mautrix.EphemeralEventReceipt, c.HandleReadReceipt)
c.syncer.OnEventType(gomatrix.EphemeralEventTyping, c.HandleTyping) c.syncer.OnEventType(mautrix.EphemeralEventTyping, c.HandleTyping)
c.syncer.OnEventType(gomatrix.AccountDataDirectChats, c.HandleDirectChatInfo) c.syncer.OnEventType(mautrix.AccountDataDirectChats, c.HandleDirectChatInfo)
c.syncer.OnEventType(gomatrix.AccountDataPushRules, c.HandlePushRules) c.syncer.OnEventType(mautrix.AccountDataPushRules, c.HandlePushRules)
c.syncer.OnEventType(gomatrix.AccountDataRoomTags, c.HandleTag) c.syncer.OnEventType(mautrix.AccountDataRoomTags, c.HandleTag)
c.syncer.OnEventType(AccountDataGomuksPreferences, c.HandlePreferences) c.syncer.OnEventType(AccountDataGomuksPreferences, c.HandlePreferences)
c.syncer.InitDoneCallback = func() { c.syncer.InitDoneCallback = func() {
debug.Print("Initial sync done")
c.config.AuthCache.InitialSyncDone = true c.config.AuthCache.InitialSyncDone = true
c.config.SaveAuthCache() c.config.SaveAuthCache()
c.ui.MainView().InitialSyncDone() c.ui.MainView().InitialSyncDone()
@ -227,7 +235,7 @@ func (c *Container) Start() {
return return
default: default:
if err := c.client.Sync(); err != nil { if err := c.client.Sync(); err != nil {
if httpErr, ok := err.(gomatrix.HTTPError); ok && httpErr.Code == http.StatusUnauthorized { if httpErr, ok := err.(mautrix.HTTPError); ok && httpErr.Code == http.StatusUnauthorized {
debug.Print("Sync() errored with ", err, " -> logging out") debug.Print("Sync() errored with ", err, " -> logging out")
c.Logout() c.Logout()
} else { } else {
@ -240,7 +248,7 @@ func (c *Container) Start() {
} }
} }
func (c *Container) HandlePreferences(source EventSource, evt *gomatrix.Event) { func (c *Container) HandlePreferences(source EventSource, evt *mautrix.Event) {
orig := c.config.Preferences orig := c.config.Preferences
rt, _ := json.Marshal(&evt.Content) rt, _ := json.Marshal(&evt.Content)
json.Unmarshal(rt, &c.config.Preferences) json.Unmarshal(rt, &c.config.Preferences)
@ -259,7 +267,7 @@ func (c *Container) SendPreferencesToMatrix() {
} }
// HandleMessage is the event handler for the m.room.message timeline event. // HandleMessage is the event handler for the m.room.message timeline event.
func (c *Container) HandleMessage(source EventSource, evt *gomatrix.Event) { func (c *Container) HandleMessage(source EventSource, evt *mautrix.Event) {
if source&EventSourceLeave != 0 { if source&EventSourceLeave != 0 {
return return
} }
@ -286,7 +294,7 @@ func (c *Container) HandleMessage(source EventSource, evt *gomatrix.Event) {
} }
// HandleMembership is the event handler for the m.room.member state event. // HandleMembership is the event handler for the m.room.member state event.
func (c *Container) HandleMembership(source EventSource, evt *gomatrix.Event) { func (c *Container) HandleMembership(source EventSource, evt *mautrix.Event) {
isLeave := source&EventSourceLeave != 0 isLeave := source&EventSourceLeave != 0
isTimeline := source&EventSourceTimeline != 0 isTimeline := source&EventSourceTimeline != 0
isNonTimelineLeave := isLeave && !isTimeline isNonTimelineLeave := isLeave && !isTimeline
@ -302,9 +310,9 @@ func (c *Container) HandleMembership(source EventSource, evt *gomatrix.Event) {
c.HandleMessage(source, evt) c.HandleMessage(source, evt)
} }
func (c *Container) processOwnMembershipChange(evt *gomatrix.Event) { func (c *Container) processOwnMembershipChange(evt *mautrix.Event) {
membership := evt.Content.Membership membership := evt.Content.Membership
prevMembership := gomatrix.MembershipLeave prevMembership := mautrix.MembershipLeave
if evt.Unsigned.PrevContent != nil { if evt.Unsigned.PrevContent != nil {
prevMembership = evt.Unsigned.PrevContent.Membership prevMembership = evt.Unsigned.PrevContent.Membership
} }
@ -326,7 +334,7 @@ func (c *Container) processOwnMembershipChange(evt *gomatrix.Event) {
} }
} }
func (c *Container) parseReadReceipt(evt *gomatrix.Event) (largestTimestampEvent string) { func (c *Container) parseReadReceipt(evt *mautrix.Event) (largestTimestampEvent string) {
var largestTimestamp int64 var largestTimestamp int64
for eventID, rawContent := range evt.Content.Raw { for eventID, rawContent := range evt.Content.Raw {
content, ok := rawContent.(map[string]interface{}) content, ok := rawContent.(map[string]interface{})
@ -353,7 +361,7 @@ func (c *Container) parseReadReceipt(evt *gomatrix.Event) (largestTimestampEvent
return return
} }
func (c *Container) HandleReadReceipt(source EventSource, evt *gomatrix.Event) { func (c *Container) HandleReadReceipt(source EventSource, evt *mautrix.Event) {
if source&EventSourceLeave != 0 { if source&EventSourceLeave != 0 {
return return
} }
@ -368,7 +376,7 @@ func (c *Container) HandleReadReceipt(source EventSource, evt *gomatrix.Event) {
c.ui.Render() c.ui.Render()
} }
func (c *Container) parseDirectChatInfo(evt *gomatrix.Event) map[*rooms.Room]bool { func (c *Container) parseDirectChatInfo(evt *mautrix.Event) map[*rooms.Room]bool {
directChats := make(map[*rooms.Room]bool) directChats := make(map[*rooms.Room]bool)
for _, rawRoomIDList := range evt.Content.Raw { for _, rawRoomIDList := range evt.Content.Raw {
roomIDList, ok := rawRoomIDList.([]interface{}) roomIDList, ok := rawRoomIDList.([]interface{})
@ -391,7 +399,7 @@ func (c *Container) parseDirectChatInfo(evt *gomatrix.Event) map[*rooms.Room]boo
return directChats return directChats
} }
func (c *Container) HandleDirectChatInfo(source EventSource, evt *gomatrix.Event) { func (c *Container) HandleDirectChatInfo(source EventSource, evt *mautrix.Event) {
directChats := c.parseDirectChatInfo(evt) directChats := c.parseDirectChatInfo(evt)
for _, room := range c.config.Rooms { for _, room := range c.config.Rooms {
shouldBeDirect := directChats[room] shouldBeDirect := directChats[room]
@ -403,7 +411,7 @@ func (c *Container) HandleDirectChatInfo(source EventSource, evt *gomatrix.Event
} }
// HandlePushRules is the event handler for the m.push_rules account data event. // HandlePushRules is the event handler for the m.push_rules account data event.
func (c *Container) HandlePushRules(source EventSource, evt *gomatrix.Event) { func (c *Container) HandlePushRules(source EventSource, evt *mautrix.Event) {
debug.Print("Received updated push rules") debug.Print("Received updated push rules")
var err error var err error
c.config.PushRules, err = pushrules.EventToPushRules(evt) c.config.PushRules, err = pushrules.EventToPushRules(evt)
@ -415,7 +423,7 @@ func (c *Container) HandlePushRules(source EventSource, evt *gomatrix.Event) {
} }
// HandleTag is the event handler for the m.tag account data event. // HandleTag is the event handler for the m.tag account data event.
func (c *Container) HandleTag(source EventSource, evt *gomatrix.Event) { func (c *Container) HandleTag(source EventSource, evt *mautrix.Event) {
room := c.config.GetRoom(evt.RoomID) room := c.config.GetRoom(evt.RoomID)
newTags := make([]rooms.RoomTag, len(evt.Content.RoomTags)) newTags := make([]rooms.RoomTag, len(evt.Content.RoomTags))
@ -423,7 +431,7 @@ func (c *Container) HandleTag(source EventSource, evt *gomatrix.Event) {
for tag, info := range evt.Content.RoomTags { for tag, info := range evt.Content.RoomTags {
order := "0.5" order := "0.5"
if len(info.Order) > 0 { if len(info.Order) > 0 {
order = info.Order order = info.Order.String()
} }
newTags[index] = rooms.RoomTag{ newTags[index] = rooms.RoomTag{
Tag: tag, Tag: tag,
@ -438,7 +446,7 @@ func (c *Container) HandleTag(source EventSource, evt *gomatrix.Event) {
} }
// HandleTyping is the event handler for the m.typing event. // HandleTyping is the event handler for the m.typing event.
func (c *Container) HandleTyping(source EventSource, evt *gomatrix.Event) { func (c *Container) HandleTyping(source EventSource, evt *mautrix.Event) {
c.ui.MainView().SetTyping(evt.RoomID, evt.Content.TypingUserIDs) c.ui.MainView().SetTyping(evt.RoomID, evt.Content.TypingUserIDs)
} }
@ -448,11 +456,11 @@ func (c *Container) MarkRead(roomID, eventID string) {
} }
// SendMessage sends a message with the given text to the given room. // SendMessage sends a message with the given text to the given room.
func (c *Container) SendMessage(roomID string, msgtype gomatrix.MessageType, text string) (string, error) { func (c *Container) SendMessage(roomID string, msgtype mautrix.MessageType, text string) (string, error) {
defer debug.Recover() defer debug.Recover()
c.SendTyping(roomID, false) c.SendTyping(roomID, false)
resp, err := c.client.SendMessageEvent(roomID, gomatrix.EventMessage, resp, err := c.client.SendMessageEvent(roomID, mautrix.EventMessage,
gomatrix.Content{MsgType: msgtype, Body: text}) mautrix.Content{MsgType: msgtype, Body: text})
if err != nil { if err != nil {
return "", err return "", err
} }
@ -491,7 +499,7 @@ var roomRegex = regexp.MustCompile("\\[.+?]\\(https://matrix.to/#/(#.+?:[^/]+?)\
// //
// If the given text contains markdown formatting symbols, it will be rendered into HTML before sending. // If the given text contains markdown formatting symbols, it will be rendered into HTML before sending.
// Otherwise, it will be sent as plain text. // Otherwise, it will be sent as plain text.
func (c *Container) SendMarkdownMessage(roomID string, msgtype gomatrix.MessageType, text string) (string, error) { func (c *Container) SendMarkdownMessage(roomID string, msgtype mautrix.MessageType, text string) (string, error) {
defer debug.Recover() defer debug.Recover()
html := c.renderMarkdown(text) html := c.renderMarkdown(text)
@ -504,11 +512,11 @@ func (c *Container) SendMarkdownMessage(roomID string, msgtype gomatrix.MessageT
text = roomRegex.ReplaceAllString(text, "$1") text = roomRegex.ReplaceAllString(text, "$1")
c.SendTyping(roomID, false) c.SendTyping(roomID, false)
resp, err := c.client.SendMessageEvent(roomID, gomatrix.EventMessage, resp, err := c.client.SendMessageEvent(roomID, mautrix.EventMessage,
gomatrix.Content{ mautrix.Content{
MsgType: msgtype, MsgType: msgtype,
Body: text, Body: text,
Format: gomatrix.FormatHTML, Format: mautrix.FormatHTML,
FormattedBody: html, FormattedBody: html,
}) })
if err != nil { if err != nil {
@ -560,7 +568,7 @@ func (c *Container) LeaveRoom(roomID string) error {
} }
// GetHistory fetches room history. // GetHistory fetches room history.
func (c *Container) GetHistory(roomID, prevBatch string, limit int) ([]*gomatrix.Event, string, error) { func (c *Container) GetHistory(roomID, prevBatch string, limit int) ([]*mautrix.Event, string, error) {
resp, err := c.client.Messages(roomID, prevBatch, "", 'b', limit) resp, err := c.client.Messages(roomID, prevBatch, "", 'b', limit)
if err != nil { if err != nil {
return nil, "", err return nil, "", err

View File

@ -21,7 +21,7 @@ import (
"fmt" "fmt"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"io/ioutil" "io/ioutil"
"maunium.net/go/gomatrix" "maunium.net/go/mautrix"
"maunium.net/go/gomuks/config" "maunium.net/go/gomuks/config"
"net/http" "net/http"
"os" "os"
@ -80,7 +80,7 @@ func TestContainer_SendMarkdownMessage_WithMarkdown(t *testing.T) {
} }
func TestContainer_SendTyping(t *testing.T) { func TestContainer_SendTyping(t *testing.T) {
var calls []gomatrix.ReqTyping var calls []mautrix.ReqTyping
c := Container{client: mockClient(func(req *http.Request) (*http.Response, error) { c := Container{client: mockClient(func(req *http.Request) (*http.Response, error) {
if req.Method != http.MethodPut || req.URL.Path != "/_matrix/client/r0/rooms/!foo:example.com/typing/@user:example.com" { if req.Method != http.MethodPut || req.URL.Path != "/_matrix/client/r0/rooms/!foo:example.com/typing/@user:example.com" {
return nil, fmt.Errorf("unexpected query: %s %s", req.Method, req.URL.Path) return nil, fmt.Errorf("unexpected query: %s %s", req.Method, req.URL.Path)
@ -91,7 +91,7 @@ func TestContainer_SendTyping(t *testing.T) {
return nil, err return nil, err
} }
call := gomatrix.ReqTyping{} call := mautrix.ReqTyping{}
err = json.Unmarshal(rawBody, &call) err = json.Unmarshal(rawBody, &call)
if err != nil { if err != nil {
return nil, err return nil, err
@ -187,8 +187,8 @@ func TestContainer_GetHistory(t *testing.T) {
assert.Equal(t, "456", prevBatch) assert.Equal(t, "456", prevBatch)
} }
func mockClient(fn func(*http.Request) (*http.Response, error)) *gomatrix.Client { func mockClient(fn func(*http.Request) (*http.Response, error)) *mautrix.Client {
client, _ := gomatrix.NewClient("https://example.com", "@user:example.com", "foobar") client, _ := mautrix.NewClient("https://example.com", "@user:example.com", "foobar")
client.Client = &http.Client{Transport: MockRoundTripper{RT: fn}} client.Client = &http.Client{Transport: MockRoundTripper{RT: fn}}
return client return client
} }

View File

@ -21,14 +21,14 @@ import (
"strconv" "strconv"
"strings" "strings"
"maunium.net/go/gomatrix" "maunium.net/go/mautrix"
"maunium.net/go/gomuks/lib/glob" "maunium.net/go/gomuks/lib/glob"
) )
// Room is an interface with the functions that are needed for processing room-specific push conditions // Room is an interface with the functions that are needed for processing room-specific push conditions
type Room interface { type Room interface {
GetMember(mxid string) *gomatrix.Member GetMember(mxid string) *mautrix.Member
GetMembers() map[string]*gomatrix.Member GetMembers() map[string]*mautrix.Member
GetSessionOwner() string GetSessionOwner() string
} }
@ -59,7 +59,7 @@ type PushCondition struct {
var MemberCountFilterRegex = regexp.MustCompile("^(==|[<>]=?)?([0-9]+)$") var MemberCountFilterRegex = regexp.MustCompile("^(==|[<>]=?)?([0-9]+)$")
// Match checks if this condition is fulfilled for the given event in the given room. // Match checks if this condition is fulfilled for the given event in the given room.
func (cond *PushCondition) Match(room Room, event *gomatrix.Event) bool { func (cond *PushCondition) Match(room Room, event *mautrix.Event) bool {
switch cond.Kind { switch cond.Kind {
case KindEventMatch: case KindEventMatch:
return cond.matchValue(room, event) return cond.matchValue(room, event)
@ -72,7 +72,7 @@ func (cond *PushCondition) Match(room Room, event *gomatrix.Event) bool {
} }
} }
func (cond *PushCondition) matchValue(room Room, event *gomatrix.Event) bool { func (cond *PushCondition) matchValue(room Room, event *mautrix.Event) bool {
index := strings.IndexRune(cond.Key, '.') index := strings.IndexRune(cond.Key, '.')
key := cond.Key key := cond.Key
subkey := "" subkey := ""
@ -106,7 +106,7 @@ func (cond *PushCondition) matchValue(room Room, event *gomatrix.Event) bool {
} }
} }
func (cond *PushCondition) matchDisplayName(room Room, event *gomatrix.Event) bool { func (cond *PushCondition) matchDisplayName(room Room, event *mautrix.Event) bool {
ownerID := room.GetSessionOwner() ownerID := room.GetSessionOwner()
if ownerID == event.Sender { if ownerID == event.Sender {
return false return false
@ -115,7 +115,7 @@ func (cond *PushCondition) matchDisplayName(room Room, event *gomatrix.Event) bo
return strings.Contains(event.Content.Body, member.Displayname) return strings.Contains(event.Content.Body, member.Displayname)
} }
func (cond *PushCondition) matchMemberCount(room Room, event *gomatrix.Event) bool { func (cond *PushCondition) matchMemberCount(room Room, event *mautrix.Event) bool {
group := MemberCountFilterRegex.FindStringSubmatch(cond.MemberCountCondition) group := MemberCountFilterRegex.FindStringSubmatch(cond.MemberCountCondition)
if len(group) != 3 { if len(group) != 3 {
return false return false

View File

@ -21,7 +21,7 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"maunium.net/go/gomatrix" "maunium.net/go/mautrix"
"maunium.net/go/gomuks/matrix/pushrules" "maunium.net/go/gomuks/matrix/pushrules"
"maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/matrix/rooms"
) )
@ -30,7 +30,7 @@ var (
blankTestRoom *rooms.Room blankTestRoom *rooms.Room
displaynameTestRoom pushrules.Room displaynameTestRoom pushrules.Room
countConditionTestEvent *gomatrix.Event countConditionTestEvent *mautrix.Event
displaynamePushCondition *pushrules.PushCondition displaynamePushCondition *pushrules.PushCondition
) )
@ -38,7 +38,7 @@ var (
func init() { func init() {
blankTestRoom = rooms.NewRoom("!fakeroom:maunium.net", "@tulir:maunium.net") blankTestRoom = rooms.NewRoom("!fakeroom:maunium.net", "@tulir:maunium.net")
countConditionTestEvent = &gomatrix.Event{ countConditionTestEvent = &mautrix.Event{
Sender: "@tulir:maunium.net", Sender: "@tulir:maunium.net",
Type: "m.room.message", Type: "m.room.message",
Timestamp: 1523791120, Timestamp: 1523791120,
@ -56,8 +56,8 @@ func init() {
} }
} }
func newFakeEvent(evtType string, content map[string]interface{}) *gomatrix.Event { func newFakeEvent(evtType string, content map[string]interface{}) *mautrix.Event {
return &gomatrix.Event{ return &mautrix.Event{
Sender: "@tulir:maunium.net", Sender: "@tulir:maunium.net",
Type: evtType, Type: evtType,
Timestamp: 1523791120, Timestamp: 1523791120,

View File

@ -4,16 +4,16 @@ import (
"encoding/json" "encoding/json"
"net/url" "net/url"
"maunium.net/go/gomatrix" "maunium.net/go/mautrix"
) )
// GetPushRules returns the push notification rules for the global scope. // GetPushRules returns the push notification rules for the global scope.
func GetPushRules(client *gomatrix.Client) (*PushRuleset, error) { func GetPushRules(client *mautrix.Client) (*PushRuleset, error) {
return GetScopedPushRules(client, "global") return GetScopedPushRules(client, "global")
} }
// GetScopedPushRules returns the push notification rules for the given scope. // GetScopedPushRules returns the push notification rules for the given scope.
func GetScopedPushRules(client *gomatrix.Client, scope string) (resp *PushRuleset, err error) { func GetScopedPushRules(client *mautrix.Client, scope string) (resp *PushRuleset, err error) {
u, _ := url.Parse(client.BuildURL("pushrules", scope)) u, _ := url.Parse(client.BuildURL("pushrules", scope))
// client.BuildURL returns the URL without a trailing slash, but the pushrules endpoint requires the slash. // client.BuildURL returns the URL without a trailing slash, but the pushrules endpoint requires the slash.
u.Path += "/" u.Path += "/"
@ -26,7 +26,7 @@ type contentWithRuleset struct {
} }
// EventToPushRules converts a m.push_rules event to a PushRuleset by passing the data through JSON. // EventToPushRules converts a m.push_rules event to a PushRuleset by passing the data through JSON.
func EventToPushRules(event *gomatrix.Event) (*PushRuleset, error) { func EventToPushRules(event *mautrix.Event) (*PushRuleset, error) {
content := &contentWithRuleset{} content := &contentWithRuleset{}
err := json.Unmarshal(event.Content.VeryRaw, content) err := json.Unmarshal(event.Content.VeryRaw, content)
if err != nil { if err != nil {

View File

@ -21,7 +21,7 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"maunium.net/go/gomatrix" "maunium.net/go/mautrix"
"maunium.net/go/gomuks/matrix/pushrules" "maunium.net/go/gomuks/matrix/pushrules"
) )
@ -33,7 +33,7 @@ func init() {
} }
func TestEventToPushRules(t *testing.T) { func TestEventToPushRules(t *testing.T) {
event := &gomatrix.Event{ event := &mautrix.Event{
Type: "m.push_rules", Type: "m.push_rules",
Timestamp: 1523380910, Timestamp: 1523380910,
Content: mapExamplePushRules, Content: mapExamplePushRules,

View File

@ -18,7 +18,7 @@ package pushrules
import ( import (
"encoding/gob" "encoding/gob"
"maunium.net/go/gomatrix" "maunium.net/go/mautrix"
"maunium.net/go/gomuks/lib/glob" "maunium.net/go/gomuks/lib/glob"
) )
@ -28,7 +28,7 @@ func init() {
} }
type PushRuleCollection interface { type PushRuleCollection interface {
GetActions(room Room, event *gomatrix.Event) PushActionArray GetActions(room Room, event *mautrix.Event) PushActionArray
} }
type PushRuleArray []*PushRule type PushRuleArray []*PushRule
@ -40,7 +40,7 @@ func (rules PushRuleArray) SetType(typ PushRuleType) PushRuleArray {
return rules return rules
} }
func (rules PushRuleArray) GetActions(room Room, event *gomatrix.Event) PushActionArray { func (rules PushRuleArray) GetActions(room Room, event *mautrix.Event) PushActionArray {
for _, rule := range rules { for _, rule := range rules {
if !rule.Match(room, event) { if !rule.Match(room, event) {
continue continue
@ -67,7 +67,7 @@ func (rules PushRuleArray) SetTypeAndMap(typ PushRuleType) PushRuleMap {
return data return data
} }
func (ruleMap PushRuleMap) GetActions(room Room, event *gomatrix.Event) PushActionArray { func (ruleMap PushRuleMap) GetActions(room Room, event *mautrix.Event) PushActionArray {
var rule *PushRule var rule *PushRule
var found bool var found bool
switch ruleMap.Type { switch ruleMap.Type {
@ -122,7 +122,7 @@ type PushRule struct {
Pattern string `json:"pattern,omitempty"` Pattern string `json:"pattern,omitempty"`
} }
func (rule *PushRule) Match(room Room, event *gomatrix.Event) bool { func (rule *PushRule) Match(room Room, event *mautrix.Event) bool {
if !rule.Enabled { if !rule.Enabled {
return false return false
} }
@ -140,7 +140,7 @@ func (rule *PushRule) Match(room Room, event *gomatrix.Event) bool {
} }
} }
func (rule *PushRule) matchConditions(room Room, event *gomatrix.Event) bool { func (rule *PushRule) matchConditions(room Room, event *mautrix.Event) bool {
for _, cond := range rule.Conditions { for _, cond := range rule.Conditions {
if !cond.Match(room, event) { if !cond.Match(room, event) {
return false return false
@ -149,7 +149,7 @@ func (rule *PushRule) matchConditions(room Room, event *gomatrix.Event) bool {
return true return true
} }
func (rule *PushRule) matchPattern(room Room, event *gomatrix.Event) bool { func (rule *PushRule) matchPattern(room Room, event *mautrix.Event) bool {
pattern, err := glob.Compile(rule.Pattern) pattern, err := glob.Compile(rule.Pattern)
if err != nil { if err != nil {
return false return false

View File

@ -19,7 +19,7 @@ package pushrules
import ( import (
"encoding/json" "encoding/json"
"maunium.net/go/gomatrix" "maunium.net/go/mautrix"
) )
type PushRuleset struct { type PushRuleset struct {
@ -80,7 +80,7 @@ var DefaultPushActions = make(PushActionArray, 0)
// GetActions matches the given event against all of the push rule // GetActions matches the given event against all of the push rule
// collections in this push ruleset in the order of priority as // collections in this push ruleset in the order of priority as
// specified in spec section 11.12.1.4. // specified in spec section 11.12.1.4.
func (rs *PushRuleset) GetActions(room Room, event *gomatrix.Event) (match PushActionArray) { func (rs *PushRuleset) GetActions(room Room, event *mautrix.Event) (match PushActionArray) {
// Add push rule collections to array in priority order // Add push rule collections to array in priority order
arrays := []PushRuleCollection{rs.Override, rs.Content, rs.Room, rs.Sender, rs.Underride} arrays := []PushRuleCollection{rs.Override, rs.Content, rs.Room, rs.Sender, rs.Underride}
// Loop until one of the push rule collections matches the room/event combo. // Loop until one of the push rule collections matches the room/event combo.

View File

@ -23,7 +23,7 @@ import (
"time" "time"
"encoding/gob" "encoding/gob"
"maunium.net/go/gomatrix" "maunium.net/go/mautrix"
"maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/debug"
"os" "os"
) )
@ -58,7 +58,7 @@ type UnreadMessage struct {
// Room represents a single Matrix room. // Room represents a single Matrix room.
type Room struct { type Room struct {
*gomatrix.Room *mautrix.Room
// Whether or not the user has left the room. // Whether or not the user has left the room.
HasLeft bool HasLeft bool
@ -82,10 +82,10 @@ type Room struct {
LastReceivedMessage time.Time LastReceivedMessage time.Time
// MXID -> Member cache calculated from membership events. // MXID -> Member cache calculated from membership events.
memberCache map[string]*gomatrix.Member memberCache map[string]*mautrix.Member
// The first non-SessionUserID member in the room. Calculated at // The first non-SessionUserID member in the room. Calculated at
// the same time as memberCache. // the same time as memberCache.
firstMemberCache *gomatrix.Member firstMemberCache *mautrix.Member
// The name of the room. Calculated from the state event name, // The name of the room. Calculated from the state event name,
// canonical_alias or alias or the member cache. // canonical_alias or alias or the member cache.
nameCache string nameCache string
@ -216,31 +216,31 @@ func (room *Room) Tags() []RoomTag {
// UpdateState updates the room's current state with the given Event. This will clobber events based // UpdateState updates the room's current state with the given Event. This will clobber events based
// on the type/state_key combination. // on the type/state_key combination.
func (room *Room) UpdateState(event *gomatrix.Event) { func (room *Room) UpdateState(event *mautrix.Event) {
_, exists := room.State[event.Type] _, exists := room.State[event.Type]
if !exists { if !exists {
room.State[event.Type] = make(map[string]*gomatrix.Event) room.State[event.Type] = make(map[string]*mautrix.Event)
} }
switch event.Type { switch event.Type {
case gomatrix.StateRoomName: case mautrix.StateRoomName:
room.nameCache = "" room.nameCache = ""
case gomatrix.StateCanonicalAlias: case mautrix.StateCanonicalAlias:
if room.nameCacheSource >= CanonicalAliasRoomName { if room.nameCacheSource >= CanonicalAliasRoomName {
room.nameCache = "" room.nameCache = ""
} }
room.canonicalAliasCache = "" room.canonicalAliasCache = ""
case gomatrix.StateAliases: case mautrix.StateAliases:
if room.nameCacheSource >= AliasRoomName { if room.nameCacheSource >= AliasRoomName {
room.nameCache = "" room.nameCache = ""
} }
room.aliasesCache = nil room.aliasesCache = nil
case gomatrix.StateMember: case mautrix.StateMember:
room.memberCache = nil room.memberCache = nil
room.firstMemberCache = nil room.firstMemberCache = nil
if room.nameCacheSource >= MemberRoomName { if room.nameCacheSource >= MemberRoomName {
room.nameCache = "" room.nameCache = ""
} }
case gomatrix.StateTopic: case mautrix.StateTopic:
room.topicCache = "" room.topicCache = ""
} }
@ -248,7 +248,7 @@ func (room *Room) UpdateState(event *gomatrix.Event) {
if event.StateKey != nil { if event.StateKey != nil {
stateKey = *event.StateKey stateKey = *event.StateKey
} }
if event.Type != gomatrix.StateMember { if event.Type != mautrix.StateMember {
debug.Printf("Updating state %s#%s for %s", event.Type, stateKey, room.ID) debug.Printf("Updating state %s#%s for %s", event.Type, stateKey, room.ID)
} }
@ -260,14 +260,14 @@ func (room *Room) UpdateState(event *gomatrix.Event) {
} }
// GetStateEvent returns the state event for the given type/state_key combo, or nil. // GetStateEvent returns the state event for the given type/state_key combo, or nil.
func (room *Room) GetStateEvent(eventType gomatrix.EventType, stateKey string) *gomatrix.Event { func (room *Room) GetStateEvent(eventType mautrix.EventType, stateKey string) *mautrix.Event {
stateEventMap, _ := room.State[eventType] stateEventMap, _ := room.State[eventType]
event, _ := stateEventMap[stateKey] event, _ := stateEventMap[stateKey]
return event return event
} }
// GetStateEvents returns the state events for the given type. // GetStateEvents returns the state events for the given type.
func (room *Room) GetStateEvents(eventType gomatrix.EventType) map[string]*gomatrix.Event { func (room *Room) GetStateEvents(eventType mautrix.EventType) map[string]*mautrix.Event {
stateEventMap, _ := room.State[eventType] stateEventMap, _ := room.State[eventType]
return stateEventMap return stateEventMap
} }
@ -275,7 +275,7 @@ func (room *Room) GetStateEvents(eventType gomatrix.EventType) map[string]*gomat
// GetTopic returns the topic of the room. // GetTopic returns the topic of the room.
func (room *Room) GetTopic() string { func (room *Room) GetTopic() string {
if len(room.topicCache) == 0 { if len(room.topicCache) == 0 {
topicEvt := room.GetStateEvent(gomatrix.StateTopic, "") topicEvt := room.GetStateEvent(mautrix.StateTopic, "")
if topicEvt != nil { if topicEvt != nil {
room.topicCache = topicEvt.Content.Topic room.topicCache = topicEvt.Content.Topic
} }
@ -285,7 +285,7 @@ func (room *Room) GetTopic() string {
func (room *Room) GetCanonicalAlias() string { func (room *Room) GetCanonicalAlias() string {
if len(room.canonicalAliasCache) == 0 { if len(room.canonicalAliasCache) == 0 {
canonicalAliasEvt := room.GetStateEvent(gomatrix.StateCanonicalAlias, "") canonicalAliasEvt := room.GetStateEvent(mautrix.StateCanonicalAlias, "")
if canonicalAliasEvt != nil { if canonicalAliasEvt != nil {
room.canonicalAliasCache = canonicalAliasEvt.Content.Alias room.canonicalAliasCache = canonicalAliasEvt.Content.Alias
} else { } else {
@ -301,7 +301,7 @@ func (room *Room) GetCanonicalAlias() string {
// GetAliases returns the list of aliases that point to this room. // GetAliases returns the list of aliases that point to this room.
func (room *Room) GetAliases() []string { func (room *Room) GetAliases() []string {
if room.aliasesCache == nil { if room.aliasesCache == nil {
aliasEvents := room.GetStateEvents(gomatrix.StateAliases) aliasEvents := room.GetStateEvents(mautrix.StateAliases)
room.aliasesCache = []string{} room.aliasesCache = []string{}
for _, event := range aliasEvents { for _, event := range aliasEvents {
room.aliasesCache = append(room.aliasesCache, event.Content.Aliases...) room.aliasesCache = append(room.aliasesCache, event.Content.Aliases...)
@ -312,7 +312,7 @@ func (room *Room) GetAliases() []string {
// updateNameFromNameEvent updates the room display name to be the name set in the name event. // updateNameFromNameEvent updates the room display name to be the name set in the name event.
func (room *Room) updateNameFromNameEvent() { func (room *Room) updateNameFromNameEvent() {
nameEvt := room.GetStateEvent(gomatrix.StateRoomName, "") nameEvt := room.GetStateEvent(mautrix.StateRoomName, "")
if nameEvt != nil { if nameEvt != nil {
room.nameCache = nameEvt.Content.Name room.nameCache = nameEvt.Content.Name
} }
@ -384,9 +384,9 @@ func (room *Room) GetTitle() string {
} }
// createMemberCache caches all member events into a easily processable MXID -> *Member map. // createMemberCache caches all member events into a easily processable MXID -> *Member map.
func (room *Room) createMemberCache() map[string]*gomatrix.Member { func (room *Room) createMemberCache() map[string]*mautrix.Member {
cache := make(map[string]*gomatrix.Member) cache := make(map[string]*mautrix.Member)
events := room.GetStateEvents(gomatrix.StateMember) events := room.GetStateEvents(mautrix.StateMember)
room.firstMemberCache = nil room.firstMemberCache = nil
if events != nil { if events != nil {
for userID, event := range events { for userID, event := range events {
@ -407,7 +407,7 @@ func (room *Room) createMemberCache() map[string]*gomatrix.Member {
// //
// The members are returned from the cache. // The members are returned from the cache.
// If the cache is empty, it is updated first. // If the cache is empty, it is updated first.
func (room *Room) GetMembers() map[string]*gomatrix.Member { func (room *Room) GetMembers() map[string]*mautrix.Member {
if len(room.memberCache) == 0 || room.firstMemberCache == nil { if len(room.memberCache) == 0 || room.firstMemberCache == nil {
room.createMemberCache() room.createMemberCache()
} }
@ -416,7 +416,7 @@ func (room *Room) GetMembers() map[string]*gomatrix.Member {
// GetMember returns the member with the given MXID. // GetMember returns the member with the given MXID.
// If the member doesn't exist, nil is returned. // If the member doesn't exist, nil is returned.
func (room *Room) GetMember(userID string) *gomatrix.Member { func (room *Room) GetMember(userID string) *mautrix.Member {
if len(room.memberCache) == 0 { if len(room.memberCache) == 0 {
room.createMemberCache() room.createMemberCache()
} }
@ -432,7 +432,7 @@ func (room *Room) GetSessionOwner() string {
// NewRoom creates a new Room with the given ID // NewRoom creates a new Room with the given ID
func NewRoom(roomID, owner string) *Room { func NewRoom(roomID, owner string) *Room {
return &Room{ return &Room{
Room: gomatrix.NewRoom(roomID), Room: mautrix.NewRoom(roomID),
fetchHistoryLock: &sync.Mutex{}, fetchHistoryLock: &sync.Mutex{},
SessionUserID: owner, SessionUserID: owner,
} }

View File

@ -21,7 +21,7 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"maunium.net/go/gomatrix" "maunium.net/go/mautrix"
"maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/matrix/rooms"
) )
@ -39,7 +39,7 @@ func TestNewRoom_DefaultValues(t *testing.T) {
func TestRoom_GetCanonicalAlias(t *testing.T) { func TestRoom_GetCanonicalAlias(t *testing.T) {
room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net") room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net")
room.UpdateState(&gomatrix.Event{ room.UpdateState(&mautrix.Event{
Type: "m.room.canonical_alias", Type: "m.room.canonical_alias",
Content: map[string]interface{}{ Content: map[string]interface{}{
"alias": "#foo:maunium.net", "alias": "#foo:maunium.net",
@ -50,7 +50,7 @@ func TestRoom_GetCanonicalAlias(t *testing.T) {
func TestRoom_GetTopic(t *testing.T) { func TestRoom_GetTopic(t *testing.T) {
room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net") room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net")
room.UpdateState(&gomatrix.Event{ room.UpdateState(&mautrix.Event{
Type: "m.room.topic", Type: "m.room.topic",
Content: map[string]interface{}{ Content: map[string]interface{}{
"topic": "test topic", "topic": "test topic",
@ -87,7 +87,7 @@ func TestRoom_GetAliases(t *testing.T) {
} }
func addName(room *rooms.Room) { func addName(room *rooms.Room) {
room.UpdateState(&gomatrix.Event{ room.UpdateState(&mautrix.Event{
Type: "m.room.name", Type: "m.room.name",
Content: map[string]interface{}{ Content: map[string]interface{}{
"name": "Test room", "name": "Test room",
@ -96,7 +96,7 @@ func addName(room *rooms.Room) {
} }
func addCanonicalAlias(room *rooms.Room) { func addCanonicalAlias(room *rooms.Room) {
room.UpdateState(&gomatrix.Event{ room.UpdateState(&mautrix.Event{
Type: "m.room.canonical_alias", Type: "m.room.canonical_alias",
Content: map[string]interface{}{ Content: map[string]interface{}{
"alias": "#foo:maunium.net", "alias": "#foo:maunium.net",
@ -106,7 +106,7 @@ func addCanonicalAlias(room *rooms.Room) {
func addAliases(room *rooms.Room) { func addAliases(room *rooms.Room) {
server1 := "maunium.net" server1 := "maunium.net"
room.UpdateState(&gomatrix.Event{ room.UpdateState(&mautrix.Event{
Type: "m.room.aliases", Type: "m.room.aliases",
StateKey: &server1, StateKey: &server1,
Content: map[string]interface{}{ Content: map[string]interface{}{
@ -115,7 +115,7 @@ func addAliases(room *rooms.Room) {
}) })
server2 := "matrix.org" server2 := "matrix.org"
room.UpdateState(&gomatrix.Event{ room.UpdateState(&mautrix.Event{
Type: "m.room.aliases", Type: "m.room.aliases",
StateKey: &server2, StateKey: &server2,
Content: map[string]interface{}{ Content: map[string]interface{}{
@ -126,7 +126,7 @@ func addAliases(room *rooms.Room) {
func addMembers(room *rooms.Room, count int) { func addMembers(room *rooms.Room, count int) {
user1 := "@tulir:maunium.net" user1 := "@tulir:maunium.net"
room.UpdateState(&gomatrix.Event{ room.UpdateState(&mautrix.Event{
Type: "m.room.member", Type: "m.room.member",
StateKey: &user1, StateKey: &user1,
Content: map[string]interface{}{ Content: map[string]interface{}{
@ -146,7 +146,7 @@ func addMembers(room *rooms.Room, count int) {
if i%5 == 0 { if i%5 == 0 {
content["membership"] = "invite" content["membership"] = "invite"
} }
room.UpdateState(&gomatrix.Event{ room.UpdateState(&mautrix.Event{
Type: "m.room.member", Type: "m.room.member",
StateKey: &userN, StateKey: &userN,
Content: content, Content: content,

View File

@ -14,15 +14,16 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
// Based on https://github.com/matrix-org/gomatrix/blob/master/sync.go // Based on https://github.com/matrix-org/mautrix/blob/master/sync.go
package matrix package matrix
import ( import (
"encoding/json" "encoding/json"
"maunium.net/go/gomuks/debug"
"time" "time"
"maunium.net/go/gomatrix" "maunium.net/go/mautrix"
"maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/matrix/rooms"
) )
@ -44,14 +45,14 @@ const (
EventSourceEphemeral EventSourceEphemeral
) )
type EventHandler func(source EventSource, event *gomatrix.Event) type EventHandler func(source EventSource, event *mautrix.Event)
// GomuksSyncer is the default syncing implementation. You can either write your own syncer, or selectively // GomuksSyncer is the default syncing implementation. You can either write your own syncer, or selectively
// replace parts of this default syncer (e.g. the ProcessResponse method). The default syncer uses the observer // replace parts of this default syncer (e.g. the ProcessResponse method). The default syncer uses the observer
// pattern to notify callers about incoming events. See GomuksSyncer.OnEventType for more information. // pattern to notify callers about incoming events. See GomuksSyncer.OnEventType for more information.
type GomuksSyncer struct { type GomuksSyncer struct {
Session SyncerSession Session SyncerSession
listeners map[gomatrix.EventType][]EventHandler // event type to listeners array listeners map[mautrix.EventType][]EventHandler // event type to listeners array
FirstSyncDone bool FirstSyncDone bool
InitDoneCallback func() InitDoneCallback func()
} }
@ -60,13 +61,14 @@ type GomuksSyncer struct {
func NewGomuksSyncer(session SyncerSession) *GomuksSyncer { func NewGomuksSyncer(session SyncerSession) *GomuksSyncer {
return &GomuksSyncer{ return &GomuksSyncer{
Session: session, Session: session,
listeners: make(map[gomatrix.EventType][]EventHandler), listeners: make(map[mautrix.EventType][]EventHandler),
FirstSyncDone: false, FirstSyncDone: false,
} }
} }
// ProcessResponse processes a Matrix sync response. // ProcessResponse processes a Matrix sync response.
func (s *GomuksSyncer) ProcessResponse(res *gomatrix.RespSync, since string) (err error) { func (s *GomuksSyncer) ProcessResponse(res *mautrix.RespSync, since string) (err error) {
debug.Print("Received sync response")
s.processSyncEvents(nil, res.Presence.Events, EventSourcePresence, false) s.processSyncEvents(nil, res.Presence.Events, EventSourcePresence, false)
s.processSyncEvents(nil, res.AccountData.Events, EventSourceAccountData, false) s.processSyncEvents(nil, res.AccountData.Events, EventSourceAccountData, false)
@ -106,7 +108,7 @@ func (s *GomuksSyncer) ProcessResponse(res *gomatrix.RespSync, since string) (er
return return
} }
func (s *GomuksSyncer) processSyncEvents(room *rooms.Room, events []*gomatrix.Event, source EventSource, checkStateKey bool) { func (s *GomuksSyncer) processSyncEvents(room *rooms.Room, events []*mautrix.Event, source EventSource, checkStateKey bool) {
for _, event := range events { for _, event := range events {
if !checkStateKey || event.StateKey != nil { if !checkStateKey || event.StateKey != nil {
s.processSyncEvent(room, event, source) s.processSyncEvent(room, event, source)
@ -114,11 +116,11 @@ func (s *GomuksSyncer) processSyncEvents(room *rooms.Room, events []*gomatrix.Ev
} }
} }
func (s *GomuksSyncer) processSyncEvent(room *rooms.Room, event *gomatrix.Event, source EventSource) { func (s *GomuksSyncer) processSyncEvent(room *rooms.Room, event *mautrix.Event, source EventSource) {
if room != nil { if room != nil {
event.RoomID = room.ID event.RoomID = room.ID
} }
if event.Type.Class == gomatrix.StateEventType { if event.Type.Class == mautrix.StateEventType {
room.UpdateState(event) room.UpdateState(event)
} }
s.notifyListeners(source, event) s.notifyListeners(source, event)
@ -126,7 +128,7 @@ func (s *GomuksSyncer) processSyncEvent(room *rooms.Room, event *gomatrix.Event,
// OnEventType allows callers to be notified when there are new events for the given event type. // OnEventType allows callers to be notified when there are new events for the given event type.
// There are no duplicate checks. // There are no duplicate checks.
func (s *GomuksSyncer) OnEventType(eventType gomatrix.EventType, callback EventHandler) { func (s *GomuksSyncer) OnEventType(eventType mautrix.EventType, callback EventHandler) {
_, exists := s.listeners[eventType] _, exists := s.listeners[eventType]
if !exists { if !exists {
s.listeners[eventType] = []EventHandler{} s.listeners[eventType] = []EventHandler{}
@ -134,7 +136,7 @@ func (s *GomuksSyncer) OnEventType(eventType gomatrix.EventType, callback EventH
s.listeners[eventType] = append(s.listeners[eventType], callback) s.listeners[eventType] = append(s.listeners[eventType], callback)
} }
func (s *GomuksSyncer) notifyListeners(source EventSource, event *gomatrix.Event) { func (s *GomuksSyncer) notifyListeners(source EventSource, event *mautrix.Event) {
listeners, exists := s.listeners[event.Type] listeners, exists := s.listeners[event.Type]
if !exists { if !exists {
return return
@ -145,16 +147,17 @@ func (s *GomuksSyncer) notifyListeners(source EventSource, event *gomatrix.Event
} }
// OnFailedSync always returns a 10 second wait period between failed /syncs, never a fatal error. // OnFailedSync always returns a 10 second wait period between failed /syncs, never a fatal error.
func (s *GomuksSyncer) OnFailedSync(res *gomatrix.RespSync, err error) (time.Duration, error) { func (s *GomuksSyncer) OnFailedSync(res *mautrix.RespSync, err error) (time.Duration, error) {
debug.Printf("Sync failed: %v", err)
return 10 * time.Second, nil return 10 * time.Second, nil
} }
// GetFilterJSON returns a filter with a timeline limit of 50. // GetFilterJSON returns a filter with a timeline limit of 50.
func (s *GomuksSyncer) GetFilterJSON(userID string) json.RawMessage { func (s *GomuksSyncer) GetFilterJSON(userID string) json.RawMessage {
filter := &gomatrix.Filter{ filter := &mautrix.Filter{
Room: gomatrix.RoomFilter{ Room: mautrix.RoomFilter{
IncludeLeave: false, IncludeLeave: false,
State: gomatrix.FilterPart{ State: mautrix.FilterPart{
Types: []string{ Types: []string{
"m.room.member", "m.room.member",
"m.room.name", "m.room.name",
@ -163,21 +166,21 @@ func (s *GomuksSyncer) GetFilterJSON(userID string) json.RawMessage {
"m.room.aliases", "m.room.aliases",
}, },
}, },
Timeline: gomatrix.FilterPart{ Timeline: mautrix.FilterPart{
Types: []string{"m.room.message", "m.room.member"}, Types: []string{"m.room.message", "m.room.member"},
Limit: 50, Limit: 50,
}, },
Ephemeral: gomatrix.FilterPart{ Ephemeral: mautrix.FilterPart{
Types: []string{"m.typing", "m.receipt"}, Types: []string{"m.typing", "m.receipt"},
}, },
AccountData: gomatrix.FilterPart{ AccountData: mautrix.FilterPart{
Types: []string{"m.tag"}, Types: []string{"m.tag"},
}, },
}, },
AccountData: gomatrix.FilterPart{ AccountData: mautrix.FilterPart{
Types: []string{"m.push_rules", "m.direct", "net.maunium.gomuks.preferences"}, Types: []string{"m.push_rules", "m.direct", "net.maunium.gomuks.preferences"},
}, },
Presence: gomatrix.FilterPart{ Presence: mautrix.FilterPart{
Types: []string{}, Types: []string{},
}, },
} }

View File

@ -20,7 +20,7 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"maunium.net/go/gomatrix" "maunium.net/go/mautrix"
"maunium.net/go/gomuks/matrix" "maunium.net/go/gomuks/matrix"
"maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/matrix/rooms"
) )
@ -42,13 +42,13 @@ func TestGomuksSyncer_ProcessResponse(t *testing.T) {
userID: "@tulir:maunium.net", userID: "@tulir:maunium.net",
rooms: map[string]*rooms.Room{ rooms: map[string]*rooms.Room{
"!foo:maunium.net": { "!foo:maunium.net": {
Room: gomatrix.NewRoom("!foo:maunium.net"), Room: mautrix.NewRoom("!foo:maunium.net"),
}, },
"!bar:maunium.net": { "!bar:maunium.net": {
Room: gomatrix.NewRoom("!bar:maunium.net"), Room: mautrix.NewRoom("!bar:maunium.net"),
}, },
"!test:maunium.net": { "!test:maunium.net": {
Room: gomatrix.NewRoom("!test:maunium.net"), Room: mautrix.NewRoom("!test:maunium.net"),
}, },
}, },
} }
@ -58,7 +58,7 @@ func TestGomuksSyncer_ProcessResponse(t *testing.T) {
syncer.OnEventType("m.room.message", ml.receive) syncer.OnEventType("m.room.message", ml.receive)
syncer.GetFilterJSON("@tulir:maunium.net") syncer.GetFilterJSON("@tulir:maunium.net")
joinEvt := &gomatrix.Event{ joinEvt := &mautrix.Event{
ID: "!join:maunium.net", ID: "!join:maunium.net",
Type: "m.room.member", Type: "m.room.member",
Sender: "@tulir:maunium.net", Sender: "@tulir:maunium.net",
@ -67,7 +67,7 @@ func TestGomuksSyncer_ProcessResponse(t *testing.T) {
"membership": "join", "membership": "join",
}, },
} }
messageEvt := &gomatrix.Event{ messageEvt := &mautrix.Event{
ID: "!msg:maunium.net", ID: "!msg:maunium.net",
Type: "m.room.message", Type: "m.room.message",
Content: map[string]interface{}{ Content: map[string]interface{}{
@ -75,11 +75,11 @@ func TestGomuksSyncer_ProcessResponse(t *testing.T) {
"msgtype": "m.text", "msgtype": "m.text",
}, },
} }
unhandledEvt := &gomatrix.Event{ unhandledEvt := &mautrix.Event{
ID: "!unhandled:maunium.net", ID: "!unhandled:maunium.net",
Type: "m.room.unhandled_event", Type: "m.room.unhandled_event",
} }
inviteEvt := &gomatrix.Event{ inviteEvt := &mautrix.Event{
ID: "!invite:matrix.org", ID: "!invite:matrix.org",
Type: "m.room.member", Type: "m.room.member",
Sender: "@you:matrix.org", Sender: "@you:matrix.org",
@ -88,7 +88,7 @@ func TestGomuksSyncer_ProcessResponse(t *testing.T) {
"membership": "invite", "membership": "invite",
}, },
} }
leaveEvt := &gomatrix.Event{ leaveEvt := &mautrix.Event{
ID: "!leave:matrix.org", ID: "!leave:matrix.org",
Type: "m.room.member", Type: "m.room.member",
Sender: "@you:matrix.org", Sender: "@you:matrix.org",
@ -100,27 +100,27 @@ func TestGomuksSyncer_ProcessResponse(t *testing.T) {
resp := newRespSync() resp := newRespSync()
resp.Rooms.Join["!foo:maunium.net"] = join{ resp.Rooms.Join["!foo:maunium.net"] = join{
State: events{Events: []*gomatrix.Event{joinEvt}}, State: events{Events: []*mautrix.Event{joinEvt}},
Timeline: timeline{Events: []*gomatrix.Event{messageEvt, unhandledEvt}}, Timeline: timeline{Events: []*mautrix.Event{messageEvt, unhandledEvt}},
} }
resp.Rooms.Invite["!bar:maunium.net"] = struct { resp.Rooms.Invite["!bar:maunium.net"] = struct {
State struct { State struct {
Events []*gomatrix.Event `json:"events"` Events []*mautrix.Event `json:"events"`
} `json:"invite_state"` } `json:"invite_state"`
}{ }{
State: events{Events: []*gomatrix.Event{inviteEvt}}, State: events{Events: []*mautrix.Event{inviteEvt}},
} }
resp.Rooms.Leave["!test:maunium.net"] = struct { resp.Rooms.Leave["!test:maunium.net"] = struct {
State struct { State struct {
Events []*gomatrix.Event `json:"events"` Events []*mautrix.Event `json:"events"`
} `json:"state"` } `json:"state"`
Timeline struct { Timeline struct {
Events []*gomatrix.Event `json:"events"` Events []*mautrix.Event `json:"events"`
Limited bool `json:"limited"` Limited bool `json:"limited"`
PrevBatch string `json:"prev_batch"` PrevBatch string `json:"prev_batch"`
} `json:"timeline"` } `json:"timeline"`
}{ }{
State: events{Events: []*gomatrix.Event{leaveEvt}}, State: events{Events: []*mautrix.Event{leaveEvt}},
} }
syncer.ProcessResponse(resp, "since") syncer.ProcessResponse(resp, "since")
@ -145,28 +145,28 @@ func (mss *mockSyncerSession) GetUserID() string {
} }
type events struct { type events struct {
Events []*gomatrix.Event `json:"events"` Events []*mautrix.Event `json:"events"`
} }
type timeline struct { type timeline struct {
Events []*gomatrix.Event `json:"events"` Events []*mautrix.Event `json:"events"`
Limited bool `json:"limited"` Limited bool `json:"limited"`
PrevBatch string `json:"prev_batch"` PrevBatch string `json:"prev_batch"`
} }
type join struct { type join struct {
State struct { State struct {
Events []*gomatrix.Event `json:"events"` Events []*mautrix.Event `json:"events"`
} `json:"state"` } `json:"state"`
Timeline struct { Timeline struct {
Events []*gomatrix.Event `json:"events"` Events []*mautrix.Event `json:"events"`
Limited bool `json:"limited"` Limited bool `json:"limited"`
PrevBatch string `json:"prev_batch"` PrevBatch string `json:"prev_batch"`
} `json:"timeline"` } `json:"timeline"`
Ephemeral struct { Ephemeral struct {
Events []*gomatrix.Event `json:"events"` Events []*mautrix.Event `json:"events"`
} `json:"ephemeral"` } `json:"ephemeral"`
AccountData struct { AccountData struct {
Events []*gomatrix.Event `json:"events"` Events []*mautrix.Event `json:"events"`
} `json:"account_data"` } `json:"account_data"`
} }
@ -175,42 +175,42 @@ func ptr(text string) *string {
} }
type mockListener struct { type mockListener struct {
received []*gomatrix.Event received []*mautrix.Event
} }
func (ml *mockListener) receive(source matrix.EventSource, evt *gomatrix.Event) { func (ml *mockListener) receive(source matrix.EventSource, evt *mautrix.Event) {
ml.received = append(ml.received, evt) ml.received = append(ml.received, evt)
} }
func newRespSync() *gomatrix.RespSync { func newRespSync() *mautrix.RespSync {
resp := &gomatrix.RespSync{NextBatch: "123"} resp := &mautrix.RespSync{NextBatch: "123"}
resp.Rooms.Join = make(map[string]struct { resp.Rooms.Join = make(map[string]struct {
State struct { State struct {
Events []*gomatrix.Event `json:"events"` Events []*mautrix.Event `json:"events"`
} `json:"state"` } `json:"state"`
Timeline struct { Timeline struct {
Events []*gomatrix.Event `json:"events"` Events []*mautrix.Event `json:"events"`
Limited bool `json:"limited"` Limited bool `json:"limited"`
PrevBatch string `json:"prev_batch"` PrevBatch string `json:"prev_batch"`
} `json:"timeline"` } `json:"timeline"`
Ephemeral struct { Ephemeral struct {
Events []*gomatrix.Event `json:"events"` Events []*mautrix.Event `json:"events"`
} `json:"ephemeral"` } `json:"ephemeral"`
AccountData struct { AccountData struct {
Events []*gomatrix.Event `json:"events"` Events []*mautrix.Event `json:"events"`
} `json:"account_data"` } `json:"account_data"`
}) })
resp.Rooms.Invite = make(map[string]struct { resp.Rooms.Invite = make(map[string]struct {
State struct { State struct {
Events []*gomatrix.Event `json:"events"` Events []*mautrix.Event `json:"events"`
} `json:"invite_state"` } `json:"invite_state"`
}) })
resp.Rooms.Leave = make(map[string]struct { resp.Rooms.Leave = make(map[string]struct {
State struct { State struct {
Events []*gomatrix.Event `json:"events"` Events []*mautrix.Event `json:"events"`
} `json:"state"` } `json:"state"`
Timeline struct { Timeline struct {
Events []*gomatrix.Event `json:"events"` Events []*mautrix.Event `json:"events"`
Limited bool `json:"limited"` Limited bool `json:"limited"`
PrevBatch string `json:"prev_batch"` PrevBatch string `json:"prev_batch"`
} `json:"timeline"` } `json:"timeline"`

View File

@ -19,7 +19,7 @@ package ui
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"maunium.net/go/gomatrix" "maunium.net/go/mautrix"
"strings" "strings"
"unicode" "unicode"
@ -133,7 +133,7 @@ func cmdSendEvent(cmd *Command) {
return return
} }
roomID := cmd.Args[0] roomID := cmd.Args[0]
eventType := gomatrix.NewEventType(cmd.Args[1]) eventType := mautrix.NewEventType(cmd.Args[1])
rawContent := strings.Join(cmd.Args[2:], "") rawContent := strings.Join(cmd.Args[2:], "")
debug.Print(roomID, eventType, rawContent) debug.Print(roomID, eventType, rawContent)
@ -162,7 +162,7 @@ func cmdSetState(cmd *Command) {
} }
roomID := cmd.Args[0] roomID := cmd.Args[0]
eventType := gomatrix.NewEventType(cmd.Args[1]) eventType := mautrix.NewEventType(cmd.Args[1])
stateKey := cmd.Args[2] stateKey := cmd.Args[2]
if stateKey == "-" { if stateKey == "-" {
stateKey = "" stateKey = ""

View File

@ -18,7 +18,7 @@ package messages
import ( import (
"encoding/gob" "encoding/gob"
"maunium.net/go/gomatrix" "maunium.net/go/mautrix"
"time" "time"
"maunium.net/go/gomuks/config" "maunium.net/go/gomuks/config"
@ -34,7 +34,7 @@ func init() {
type BaseMessage struct { type BaseMessage struct {
MsgID string MsgID string
MsgType gomatrix.MessageType MsgType mautrix.MessageType
MsgSenderID string MsgSenderID string
MsgSender string MsgSender string
MsgSenderColor tcell.Color MsgSenderColor tcell.Color
@ -48,7 +48,7 @@ type BaseMessage struct {
prevPrefs config.UserPreferences prevPrefs config.UserPreferences
} }
func newBaseMessage(id, sender, displayname string, msgtype gomatrix.MessageType, timestamp time.Time) BaseMessage { func newBaseMessage(id, sender, displayname string, msgtype mautrix.MessageType, timestamp time.Time) BaseMessage {
return BaseMessage{ return BaseMessage{
MsgSenderID: sender, MsgSenderID: sender,
MsgSender: displayname, MsgSender: displayname,
@ -195,11 +195,11 @@ func (msg *BaseMessage) SetID(id string) {
msg.MsgID = id msg.MsgID = id
} }
func (msg *BaseMessage) Type() gomatrix.MessageType { func (msg *BaseMessage) Type() mautrix.MessageType {
return msg.MsgType return msg.MsgType
} }
func (msg *BaseMessage) SetType(msgtype gomatrix.MessageType) { func (msg *BaseMessage) SetType(msgtype mautrix.MessageType) {
msg.MsgType = msgtype msg.MsgType = msgtype
} }

View File

@ -18,7 +18,7 @@ package messages
import ( import (
"encoding/gob" "encoding/gob"
"maunium.net/go/gomatrix" "maunium.net/go/mautrix"
"time" "time"
"maunium.net/go/gomuks/config" "maunium.net/go/gomuks/config"
@ -35,7 +35,7 @@ type ExpandedTextMessage struct {
} }
// NewExpandedTextMessage creates a new ExpandedTextMessage object with the provided values and the default state. // NewExpandedTextMessage creates a new ExpandedTextMessage object with the provided values and the default state.
func NewExpandedTextMessage(id, sender, displayname string, msgtype gomatrix.MessageType, text tstring.TString, timestamp time.Time) UIMessage { func NewExpandedTextMessage(id, sender, displayname string, msgtype mautrix.MessageType, text tstring.TString, timestamp time.Time) UIMessage {
return &ExpandedTextMessage{ return &ExpandedTextMessage{
BaseMessage: newBaseMessage(id, sender, displayname, msgtype, timestamp), BaseMessage: newBaseMessage(id, sender, displayname, msgtype, timestamp),
MsgText: text, MsgText: text,

View File

@ -20,7 +20,7 @@ import (
"bytes" "bytes"
"encoding/gob" "encoding/gob"
"fmt" "fmt"
"maunium.net/go/gomatrix" "maunium.net/go/mautrix"
"time" "time"
"image/color" "image/color"
@ -48,7 +48,7 @@ type ImageMessage struct {
} }
// NewImageMessage creates a new ImageMessage object with the provided values and the default state. // NewImageMessage creates a new ImageMessage object with the provided values and the default state.
func NewImageMessage(matrix ifc.MatrixContainer, id, sender, displayname string, msgtype gomatrix.MessageType, body, homeserver, fileID string, data []byte, timestamp time.Time) UIMessage { func NewImageMessage(matrix ifc.MatrixContainer, id, sender, displayname string, msgtype mautrix.MessageType, body, homeserver, fileID string, data []byte, timestamp time.Time) UIMessage {
return &ImageMessage{ return &ImageMessage{
newBaseMessage(id, sender, displayname, msgtype, timestamp), newBaseMessage(id, sender, displayname, msgtype, timestamp),
body, body,

View File

@ -24,10 +24,10 @@ import (
"github.com/lucasb-eyer/go-colorful" "github.com/lucasb-eyer/go-colorful"
"golang.org/x/net/html" "golang.org/x/net/html"
"maunium.net/go/gomatrix"
"maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/matrix/rooms"
"maunium.net/go/gomuks/ui/messages/tstring" "maunium.net/go/gomuks/ui/messages/tstring"
"maunium.net/go/gomuks/ui/widget" "maunium.net/go/gomuks/ui/widget"
"maunium.net/go/mautrix"
"maunium.net/go/tcell" "maunium.net/go/tcell"
"strconv" "strconv"
) )
@ -69,6 +69,9 @@ func (parser *htmlParser) getAttribute(node *html.Node, attribute string) string
} }
func digits(num int) int { func digits(num int) int {
if num <= 0 {
return 0
}
return int(math.Floor(math.Log10(float64(num))) + 1) return int(math.Floor(math.Log10(float64(num))) + 1)
} }
@ -270,14 +273,14 @@ func (parser *htmlParser) Parse(htmlData string) tstring.TString {
} }
// ParseHTMLMessage parses a HTML-formatted Matrix event into a UIMessage. // ParseHTMLMessage parses a HTML-formatted Matrix event into a UIMessage.
func ParseHTMLMessage(room *rooms.Room, evt *gomatrix.Event, senderDisplayname string) tstring.TString { func ParseHTMLMessage(room *rooms.Room, evt *mautrix.Event, senderDisplayname string) tstring.TString {
htmlData := evt.Content.FormattedBody htmlData := evt.Content.FormattedBody
htmlData = strings.Replace(htmlData, "\t", " ", -1) htmlData = strings.Replace(htmlData, "\t", " ", -1)
parser := htmlParser{room} parser := htmlParser{room}
str := parser.Parse(htmlData) str := parser.Parse(htmlData)
if evt.Content.MsgType == gomatrix.MsgEmote { if evt.Content.MsgType == mautrix.MsgEmote {
str = tstring.Join([]tstring.TString{ str = tstring.Join([]tstring.TString{
tstring.NewTString("* "), tstring.NewTString("* "),
tstring.NewColorTString(senderDisplayname, widget.GetHashColor(evt.Sender)), tstring.NewColorTString(senderDisplayname, widget.GetHashColor(evt.Sender)),

View File

@ -18,24 +18,28 @@ package parser
import ( import (
"fmt" "fmt"
"html"
"strings" "strings"
"time" "time"
"maunium.net/go/gomatrix"
"maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/debug"
"maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/interface"
"maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/matrix/rooms"
"maunium.net/go/gomuks/ui/messages" "maunium.net/go/gomuks/ui/messages"
"maunium.net/go/gomuks/ui/messages/tstring" "maunium.net/go/gomuks/ui/messages/tstring"
"maunium.net/go/gomuks/ui/widget" "maunium.net/go/gomuks/ui/widget"
"maunium.net/go/mautrix"
"maunium.net/go/tcell" "maunium.net/go/tcell"
) )
func ParseEvent(matrix ifc.MatrixContainer, room *rooms.Room, evt *gomatrix.Event) messages.UIMessage { func ParseEvent(matrix ifc.MatrixContainer, room *rooms.Room, evt *mautrix.Event) messages.UIMessage {
switch evt.Type { switch evt.Type {
case gomatrix.EventMessage: case mautrix.EventSticker:
evt.Content.MsgType = mautrix.MsgImage
fallthrough
case mautrix.EventMessage:
return ParseMessage(matrix, room, evt) return ParseMessage(matrix, room, evt)
case gomatrix.StateMember: case mautrix.StateMember:
return ParseMembershipEvent(room, evt) return ParseMembershipEvent(room, evt)
} }
return nil return nil
@ -49,16 +53,27 @@ func unixToTime(unix int64) time.Time {
return timestamp return timestamp
} }
func ParseMessage(matrix ifc.MatrixContainer, room *rooms.Room, evt *gomatrix.Event) messages.UIMessage { func ParseMessage(matrix ifc.MatrixContainer, room *rooms.Room, evt *mautrix.Event) messages.UIMessage {
displayname := evt.Sender displayname := evt.Sender
member := room.GetMember(evt.Sender) member := room.GetMember(evt.Sender)
if member != nil { if member != nil {
displayname = member.Displayname displayname = member.Displayname
} }
if len(evt.Content.GetReplyTo()) > 0 {
evt.Content.RemoveReplyFallback()
replyToEvt, _ := matrix.Client().GetEvent(room.ID, evt.Content.GetReplyTo())
replyToEvt.Content.RemoveReplyFallback()
if len(replyToEvt.Content.FormattedBody) == 0 {
replyToEvt.Content.FormattedBody = html.EscapeString(replyToEvt.Content.Body)
}
evt.Content.FormattedBody = fmt.Sprintf(
"In reply to <a href='https://matrix.to/#/%[1]s'>%[1]s</a><blockquote>%[2]s</blockquote><br/>%[3]s",
replyToEvt.Sender, replyToEvt.Content.FormattedBody, evt.Content.FormattedBody)
}
ts := unixToTime(evt.Timestamp) ts := unixToTime(evt.Timestamp)
switch evt.Content.MsgType { switch evt.Content.MsgType {
case "m.text", "m.notice", "m.emote": case "m.text", "m.notice", "m.emote":
if evt.Content.Format == gomatrix.FormatHTML { if evt.Content.Format == mautrix.FormatHTML {
text := ParseHTMLMessage(room, evt, displayname) text := ParseHTMLMessage(room, evt, displayname)
return messages.NewExpandedTextMessage(evt.ID, evt.Sender, displayname, evt.Content.MsgType, text, ts) return messages.NewExpandedTextMessage(evt.ID, evt.Sender, displayname, evt.Content.MsgType, text, ts)
} }
@ -74,7 +89,7 @@ func ParseMessage(matrix ifc.MatrixContainer, room *rooms.Room, evt *gomatrix.Ev
return nil return nil
} }
func getMembershipChangeMessage(evt *gomatrix.Event, membership, prevMembership gomatrix.Membership, senderDisplayname, displayname, prevDisplayname string) (sender string, text tstring.TString) { func getMembershipChangeMessage(evt *mautrix.Event, membership, prevMembership mautrix.Membership, senderDisplayname, displayname, prevDisplayname string) (sender string, text tstring.TString) {
switch membership { switch membership {
case "invite": case "invite":
sender = "---" sender = "---"
@ -88,7 +103,7 @@ func getMembershipChangeMessage(evt *gomatrix.Event, membership, prevMembership
case "leave": case "leave":
sender = "<--" sender = "<--"
if evt.Sender != *evt.StateKey { if evt.Sender != *evt.StateKey {
if prevMembership == gomatrix.MembershipBan { if prevMembership == mautrix.MembershipBan {
text = tstring.NewColorTString(fmt.Sprintf("%s unbanned %s", senderDisplayname, displayname), tcell.ColorGreen) text = tstring.NewColorTString(fmt.Sprintf("%s unbanned %s", senderDisplayname, displayname), tcell.ColorGreen)
text.Colorize(len(senderDisplayname)+len(" unbanned "), len(displayname), widget.GetHashColor(*evt.StateKey)) text.Colorize(len(senderDisplayname)+len(" unbanned "), len(displayname), widget.GetHashColor(*evt.StateKey))
} else { } else {
@ -111,7 +126,7 @@ func getMembershipChangeMessage(evt *gomatrix.Event, membership, prevMembership
return return
} }
func getMembershipEventContent(room *rooms.Room, evt *gomatrix.Event) (sender string, text tstring.TString) { func getMembershipEventContent(room *rooms.Room, evt *mautrix.Event) (sender string, text tstring.TString) {
member := room.GetMember(evt.Sender) member := room.GetMember(evt.Sender)
senderDisplayname := evt.Sender senderDisplayname := evt.Sender
if member != nil { if member != nil {
@ -124,7 +139,7 @@ func getMembershipEventContent(room *rooms.Room, evt *gomatrix.Event) (sender st
displayname = *evt.StateKey displayname = *evt.StateKey
} }
prevMembership := gomatrix.MembershipLeave prevMembership := mautrix.MembershipLeave
prevDisplayname := *evt.StateKey prevDisplayname := *evt.StateKey
if evt.Unsigned.PrevContent != nil { if evt.Unsigned.PrevContent != nil {
prevMembership = evt.Unsigned.PrevContent.Membership prevMembership = evt.Unsigned.PrevContent.Membership
@ -146,7 +161,7 @@ func getMembershipEventContent(room *rooms.Room, evt *gomatrix.Event) (sender st
return return
} }
func ParseMembershipEvent(room *rooms.Room, evt *gomatrix.Event) messages.UIMessage { func ParseMembershipEvent(room *rooms.Room, evt *mautrix.Event) messages.UIMessage {
displayname, text := getMembershipEventContent(room, evt) displayname, text := getMembershipEventContent(room, evt)
if len(text) == 0 { if len(text) == 0 {
return nil return nil

View File

@ -19,7 +19,7 @@ package messages
import ( import (
"encoding/gob" "encoding/gob"
"fmt" "fmt"
"maunium.net/go/gomatrix" "maunium.net/go/mautrix"
"time" "time"
"maunium.net/go/gomuks/config" "maunium.net/go/gomuks/config"
@ -38,7 +38,7 @@ type TextMessage struct {
} }
// NewTextMessage creates a new UITextMessage object with the provided values and the default state. // NewTextMessage creates a new UITextMessage object with the provided values and the default state.
func NewTextMessage(id, sender, displayname string, msgtype gomatrix.MessageType, text string, timestamp time.Time) UIMessage { func NewTextMessage(id, sender, displayname string, msgtype mautrix.MessageType, text string, timestamp time.Time) UIMessage {
return &TextMessage{ return &TextMessage{
BaseMessage: newBaseMessage(id, sender, displayname, msgtype, timestamp), BaseMessage: newBaseMessage(id, sender, displayname, msgtype, timestamp),
MsgText: text, MsgText: text,
@ -58,7 +58,7 @@ func (msg *TextMessage) getCache() tstring.TString {
return msg.cache return msg.cache
} }
func (msg *TextMessage) SetType(msgtype gomatrix.MessageType) { func (msg *TextMessage) SetType(msgtype mautrix.MessageType) {
msg.BaseMessage.SetType(msgtype) msg.BaseMessage.SetType(msgtype)
msg.cache = nil msg.cache = nil
} }

View File

@ -18,7 +18,7 @@ package ui
import ( import (
"fmt" "fmt"
"maunium.net/go/gomatrix" "maunium.net/go/mautrix"
"path/filepath" "path/filepath"
"sort" "sort"
"strconv" "strconv"
@ -347,7 +347,7 @@ func (view *RoomView) UpdateUserList() {
} }
} }
func (view *RoomView) newUIMessage(id, sender string, msgtype gomatrix.MessageType, text string, timestamp time.Time) messages.UIMessage { func (view *RoomView) newUIMessage(id, sender string, msgtype mautrix.MessageType, text string, timestamp time.Time) messages.UIMessage {
member := view.Room.GetMember(sender) member := view.Room.GetMember(sender)
displayname := sender displayname := sender
if member != nil { if member != nil {
@ -357,11 +357,11 @@ func (view *RoomView) newUIMessage(id, sender string, msgtype gomatrix.MessageTy
return msg return msg
} }
func (view *RoomView) NewMessage(id, sender string, msgtype gomatrix.MessageType, text string, timestamp time.Time) ifc.Message { func (view *RoomView) NewMessage(id, sender string, msgtype mautrix.MessageType, text string, timestamp time.Time) ifc.Message {
return view.newUIMessage(id, sender, msgtype, text, timestamp) return view.newUIMessage(id, sender, msgtype, text, timestamp)
} }
func (view *RoomView) NewTempMessage(msgtype gomatrix.MessageType, text string) ifc.Message { func (view *RoomView) NewTempMessage(msgtype mautrix.MessageType, text string) ifc.Message {
now := time.Now() now := time.Now()
id := strconv.FormatInt(now.UnixNano(), 10) id := strconv.FormatInt(now.UnixNano(), 10)
sender := "" sender := ""

View File

@ -17,11 +17,11 @@
package ui package ui
import ( import (
"maunium.net/go/gomatrix"
"maunium.net/go/gomuks/config" "maunium.net/go/gomuks/config"
"maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/debug"
"maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/interface"
"maunium.net/go/gomuks/ui/widget" "maunium.net/go/gomuks/ui/widget"
"maunium.net/go/mautrix"
"maunium.net/go/tview" "maunium.net/go/tview"
) )
@ -89,8 +89,8 @@ func (view *LoginView) Login() {
debug.Print("Init error:", err) debug.Print("Init error:", err)
err = view.matrix.Login(mxid, password) err = view.matrix.Login(mxid, password)
if err != nil { if err != nil {
if httpErr, ok := err.(gomatrix.HTTPError); ok { if httpErr, ok := err.(mautrix.HTTPError); ok {
if respErr, ok := httpErr.WrappedError.(gomatrix.RespError); ok { if respErr, ok := httpErr.WrappedError.(mautrix.RespError); ok {
view.Error(respErr.Err) view.Error(respErr.Err)
} else { } else {
view.Error(httpErr.Message) view.Error(httpErr.Message)

View File

@ -26,7 +26,7 @@ import (
"bufio" "bufio"
"os" "os"
"maunium.net/go/gomatrix" "maunium.net/go/mautrix"
"maunium.net/go/gomuks/config" "maunium.net/go/gomuks/config"
"maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/debug"
"maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/interface"
@ -152,8 +152,8 @@ func (view *MainView) sendTempMessage(roomView *RoomView, tempMessage ifc.Messag
eventID, err := view.matrix.SendMarkdownMessage(roomView.Room.ID, tempMessage.Type(), text) eventID, err := view.matrix.SendMarkdownMessage(roomView.Room.ID, tempMessage.Type(), text)
if err != nil { if err != nil {
tempMessage.SetState(ifc.MessageStateFailed) tempMessage.SetState(ifc.MessageStateFailed)
if httpErr, ok := err.(gomatrix.HTTPError); ok { if httpErr, ok := err.(mautrix.HTTPError); ok {
if respErr, ok := httpErr.WrappedError.(gomatrix.RespError); ok { if respErr, ok := httpErr.WrappedError.(mautrix.RespError); ok {
// Show shorter version if available // Show shorter version if available
err = respErr err = respErr
} }
@ -510,6 +510,6 @@ func (view *MainView) LoadHistory(room string) {
view.parent.Render() view.parent.Render()
} }
func (view *MainView) ParseEvent(roomView ifc.RoomView, evt *gomatrix.Event) ifc.Message { func (view *MainView) ParseEvent(roomView ifc.RoomView, evt *mautrix.Event) ifc.Message {
return parser.ParseEvent(view.matrix, roomView.MxRoom(), evt) return parser.ParseEvent(view.matrix, roomView.MxRoom(), evt)
} }

View File

@ -40,11 +40,11 @@ func (border *Border) Draw(screen tcell.Screen) {
x, y, width, height := border.GetRect() x, y, width, height := border.GetRect()
if width == 1 { if width == 1 {
for borderY := y; borderY < y+height; borderY++ { for borderY := y; borderY < y+height; borderY++ {
screen.SetContent(x, borderY, tview.GraphicsVertBar, nil, background) screen.SetContent(x, borderY, tview.Borders.Vertical, nil, background)
} }
} else if height == 1 { } else if height == 1 {
for borderX := x; borderX < x+width; borderX++ { for borderX := x; borderX < x+width; borderX++ {
screen.SetContent(borderX, y, tview.GraphicsHoriBar, nil, background) screen.SetContent(borderX, y, tview.Borders.Horizontal, nil, background)
} }
} }
} }

View File

@ -137,20 +137,26 @@ func decodeConfig(r io.Reader) (config image.Config, bitsPerPixel int, topDown b
// We only support those BMP images that are a BITMAPFILEHEADER // We only support those BMP images that are a BITMAPFILEHEADER
// immediately followed by a BITMAPINFOHEADER. // immediately followed by a BITMAPINFOHEADER.
const ( const (
fileHeaderLen = 14 fileHeaderLen = 14
infoHeaderLen = 40 infoHeaderLen = 40
v4InfoHeaderLen = 108
v5InfoHeaderLen = 124
) )
var b [1024]byte var b [1024]byte
if _, err := io.ReadFull(r, b[:fileHeaderLen+infoHeaderLen]); err != nil { if _, err := io.ReadFull(r, b[:fileHeaderLen+4]); err != nil {
return image.Config{}, 0, false, err return image.Config{}, 0, false, err
} }
if string(b[:2]) != "BM" { if string(b[:2]) != "BM" {
return image.Config{}, 0, false, errors.New("bmp: invalid format") return image.Config{}, 0, false, errors.New("bmp: invalid format")
} }
offset := readUint32(b[10:14]) offset := readUint32(b[10:14])
if readUint32(b[14:18]) != infoHeaderLen { infoLen := readUint32(b[14:18])
if infoLen != infoHeaderLen && infoLen != v4InfoHeaderLen && infoLen != v5InfoHeaderLen {
return image.Config{}, 0, false, ErrUnsupported return image.Config{}, 0, false, ErrUnsupported
} }
if _, err := io.ReadFull(r, b[fileHeaderLen+4:fileHeaderLen+infoLen]); err != nil {
return image.Config{}, 0, false, err
}
width := int(int32(readUint32(b[18:22]))) width := int(int32(readUint32(b[18:22])))
height := int(int32(readUint32(b[22:26]))) height := int(int32(readUint32(b[22:26])))
if height < 0 { if height < 0 {
@ -159,14 +165,22 @@ func decodeConfig(r io.Reader) (config image.Config, bitsPerPixel int, topDown b
if width < 0 || height < 0 { if width < 0 || height < 0 {
return image.Config{}, 0, false, ErrUnsupported return image.Config{}, 0, false, ErrUnsupported
} }
// We only support 1 plane, 8 or 24 bits per pixel and no compression. // We only support 1 plane and 8, 24 or 32 bits per pixel and no
// compression.
planes, bpp, compression := readUint16(b[26:28]), readUint16(b[28:30]), readUint32(b[30:34]) planes, bpp, compression := readUint16(b[26:28]), readUint16(b[28:30]), readUint32(b[30:34])
// if compression is set to BITFIELDS, but the bitmask is set to the default bitmask
// that would be used if compression was set to 0, we can continue as if compression was 0
if compression == 3 && infoLen > infoHeaderLen &&
readUint32(b[54:58]) == 0xff0000 && readUint32(b[58:62]) == 0xff00 &&
readUint32(b[62:66]) == 0xff && readUint32(b[66:70]) == 0xff000000 {
compression = 0
}
if planes != 1 || compression != 0 { if planes != 1 || compression != 0 {
return image.Config{}, 0, false, ErrUnsupported return image.Config{}, 0, false, ErrUnsupported
} }
switch bpp { switch bpp {
case 8: case 8:
if offset != fileHeaderLen+infoHeaderLen+256*4 { if offset != fileHeaderLen+infoLen+256*4 {
return image.Config{}, 0, false, ErrUnsupported return image.Config{}, 0, false, ErrUnsupported
} }
_, err = io.ReadFull(r, b[:256*4]) _, err = io.ReadFull(r, b[:256*4])
@ -181,12 +195,12 @@ func decodeConfig(r io.Reader) (config image.Config, bitsPerPixel int, topDown b
} }
return image.Config{ColorModel: pcm, Width: width, Height: height}, 8, topDown, nil return image.Config{ColorModel: pcm, Width: width, Height: height}, 8, topDown, nil
case 24: case 24:
if offset != fileHeaderLen+infoHeaderLen { if offset != fileHeaderLen+infoLen {
return image.Config{}, 0, false, ErrUnsupported return image.Config{}, 0, false, ErrUnsupported
} }
return image.Config{ColorModel: color.RGBAModel, Width: width, Height: height}, 24, topDown, nil return image.Config{ColorModel: color.RGBAModel, Width: width, Height: height}, 24, topDown, nil
case 32: case 32:
if offset != fileHeaderLen+infoHeaderLen { if offset != fileHeaderLen+infoLen {
return image.Config{}, 0, false, ErrUnsupported return image.Config{}, 0, false, ErrUnsupported
} }
return image.Config{ColorModel: color.RGBAModel, Width: width, Height: height}, 32, topDown, nil return image.Config{ColorModel: color.RGBAModel, Width: width, Height: height}, 32, topDown, nil

View File

@ -110,7 +110,7 @@ func (d *decoder) ifdUint(p []byte) (u []uint, err error) {
return u, nil return u, nil
} }
// parseIFD decides whether the the IFD entry in p is "interesting" and // parseIFD decides whether the IFD entry in p is "interesting" and
// stows away the data in the decoder. It returns the tag number of the // stows away the data in the decoder. It returns the tag number of the
// entry and an error, if any. // entry and an error, if any.
func (d *decoder) parseIFD(p []byte) (int, error) { func (d *decoder) parseIFD(p []byte) (int, error) {

View File

@ -82,7 +82,7 @@ type mb struct {
pred [4]uint8 pred [4]uint8
// nzMask is a mask of 8 bits: 4 for the bottom or right 4x4 luma regions, // nzMask is a mask of 8 bits: 4 for the bottom or right 4x4 luma regions,
// and 2 + 2 for the bottom or right 4x4 chroma regions. A 1 bit indicates // and 2 + 2 for the bottom or right 4x4 chroma regions. A 1 bit indicates
// that that region has non-zero coefficients. // that region has non-zero coefficients.
nzMask uint8 nzMask uint8
// nzY16 is a 0/1 value that is 1 if the macroblock used Y16 prediction and // nzY16 is a 0/1 value that is 1 if the macroblock used Y16 prediction and
// had non-zero coefficients. // had non-zero coefficients.
@ -274,7 +274,7 @@ func (d *Decoder) parseOtherPartitions() error {
var partLens [maxNOP]int var partLens [maxNOP]int
d.nOP = 1 << d.fp.readUint(uniformProb, 2) d.nOP = 1 << d.fp.readUint(uniformProb, 2)
// The final partition length is implied by the the remaining chunk data // The final partition length is implied by the remaining chunk data
// (d.r.n) and the other d.nOP-1 partition lengths. Those d.nOP-1 partition // (d.r.n) and the other d.nOP-1 partition lengths. Those d.nOP-1 partition
// lengths are stored as 24-bit uints, i.e. up to 16 MiB per partition. // lengths are stored as 24-bit uints, i.e. up to 16 MiB per partition.
n := 3 * (d.nOP - 1) n := 3 * (d.nOP - 1)

View File

@ -2,8 +2,6 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// +build go1.6
package webp package webp
import ( import (

9
vendor/golang.org/x/image/webp/doc.go generated vendored Normal file
View File

@ -0,0 +1,9 @@
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package webp implements a decoder for WEBP images.
//
// WEBP is defined at:
// https://developers.google.com/speed/webp/docs/riff_container
package webp // import "golang.org/x/image/webp"

View File

@ -1,30 +0,0 @@
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package webp implements a decoder for WEBP images.
//
// WEBP is defined at:
// https://developers.google.com/speed/webp/docs/riff_container
//
// It requires Go 1.6 or later.
package webp // import "golang.org/x/image/webp"
// This blank Go file, other than the package clause, exists so that this
// package can be built for Go 1.5 and earlier. (The other files in this
// package are all marked "+build go1.6" for the NYCbCrA types introduced in Go
// 1.6). There is no functionality in a blank package, but some image
// manipulation programs might still underscore import this package for the
// side effect of registering the WEBP format with the standard library's
// image.RegisterFormat and image.Decode functions. For example, that program
// might contain:
//
// // Underscore imports to register some formats for image.Decode.
// import _ "image/gif"
// import _ "image/jpeg"
// import _ "image/png"
// import _ "golang.org/x/image/webp"
//
// Such a program will still compile for Go 1.5 (due to this placeholder Go
// file). It will simply not be able to recognize and decode WEBP (but still
// handle GIF, JPEG and PNG).

View File

@ -97,8 +97,16 @@ func isSpecialElement(element *Node) bool {
switch element.Namespace { switch element.Namespace {
case "", "html": case "", "html":
return isSpecialElementMap[element.Data] return isSpecialElementMap[element.Data]
case "math":
switch element.Data {
case "mi", "mo", "mn", "ms", "mtext", "annotation-xml":
return true
}
case "svg": case "svg":
return element.Data == "foreignObject" switch element.Data {
case "foreignObject", "desc", "title":
return true
}
} }
return false return false
} }

View File

@ -470,6 +470,10 @@ func (p *parser) resetInsertionMode() {
case a.Table: case a.Table:
p.im = inTableIM p.im = inTableIM
case a.Template: case a.Template:
// TODO: remove this divergence from the HTML5 spec.
if n.Namespace != "" {
continue
}
p.im = p.templateStack.top() p.im = p.templateStack.top()
case a.Head: case a.Head:
// TODO: remove this divergence from the HTML5 spec. // TODO: remove this divergence from the HTML5 spec.
@ -984,6 +988,14 @@ func inBodyIM(p *parser) bool {
p.acknowledgeSelfClosingTag() p.acknowledgeSelfClosingTag()
p.popUntil(buttonScope, a.P) p.popUntil(buttonScope, a.P)
p.parseImpliedToken(StartTagToken, a.Form, a.Form.String()) p.parseImpliedToken(StartTagToken, a.Form, a.Form.String())
if p.form == nil {
// NOTE: The 'isindex' element has been removed,
// and the 'template' element has not been designed to be
// collaborative with the index element.
//
// Ignore the token.
return true
}
if action != "" { if action != "" {
p.form.Attr = []Attribute{{Key: "action", Val: action}} p.form.Attr = []Attribute{{Key: "action", Val: action}}
} }
@ -1252,12 +1264,6 @@ func (p *parser) inBodyEndTagFormatting(tagAtom a.Atom) {
switch commonAncestor.DataAtom { switch commonAncestor.DataAtom {
case a.Table, a.Tbody, a.Tfoot, a.Thead, a.Tr: case a.Table, a.Tbody, a.Tfoot, a.Thead, a.Tr:
p.fosterParent(lastNode) p.fosterParent(lastNode)
case a.Template:
// TODO: remove namespace checking
if commonAncestor.Namespace == "html" {
commonAncestor = commonAncestor.LastChild
}
fallthrough
default: default:
commonAncestor.AppendChild(lastNode) commonAncestor.AppendChild(lastNode)
} }
@ -2209,6 +2215,15 @@ func (p *parser) parse() error {
} }
// Parse returns the parse tree for the HTML from the given Reader. // Parse returns the parse tree for the HTML from the given Reader.
//
// It implements the HTML5 parsing algorithm
// (https://html.spec.whatwg.org/multipage/syntax.html#tree-construction),
// which is very complicated. The resultant tree can contain implicitly created
// nodes that have no explicit <tag> listed in r's data, and nodes' parents can
// differ from the nesting implied by a naive processing of start and end
// <tag>s. Conversely, explicit <tag>s in r's data can be silently dropped,
// with no corresponding node in the resulting tree.
//
// The input is assumed to be UTF-8 encoded. // The input is assumed to be UTF-8 encoded.
func Parse(r io.Reader) (*Node, error) { func Parse(r io.Reader) (*Node, error) {
p := &parser{ p := &parser{
@ -2230,6 +2245,8 @@ func Parse(r io.Reader) (*Node, error) {
// ParseFragment parses a fragment of HTML and returns the nodes that were // ParseFragment parses a fragment of HTML and returns the nodes that were
// found. If the fragment is the InnerHTML for an existing element, pass that // found. If the fragment is the InnerHTML for an existing element, pass that
// element in context. // element in context.
//
// It has the same intricacies as Parse.
func ParseFragment(r io.Reader, context *Node) ([]*Node, error) { func ParseFragment(r io.Reader, context *Node) ([]*Node, error) {
contextTag := "" contextTag := ""
if context != nil { if context != nil {

View File

@ -1,18 +1,17 @@
# Travis CI (http://travis-ci.org/) is a continuous integration service for sudo: false
# open source projects. This file configures it to run unit tests for
# blackfriday.
language: go language: go
go: go:
- 1.5 - "1.10.x"
- 1.6 - "1.11.x"
- 1.7 - tip
matrix:
fast_finish: true
allow_failures:
- go: tip
install: install:
- go get -d -t -v ./... - # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (we want it to happen inside script step).
- go build -v ./...
script: script:
- go test -v ./... - go get -t -v ./...
- go test -run=^$ -bench=BenchmarkReference -benchmem - diff -u <(echo -n) <(gofmt -d -s .)
- go tool vet .
- go test -v ./...

View File

@ -34,9 +34,15 @@ Versions
-------- --------
Currently maintained and recommended version of Blackfriday is `v2`. It's being Currently maintained and recommended version of Blackfriday is `v2`. It's being
developed on its own branch: https://github.com/russross/blackfriday/v2. You developed on its own branch: https://github.com/russross/blackfriday/tree/v2 and the
should install and import it via [gopkg.in][6] at documentation is available at
`gopkg.in/russross/blackfriday.v2`. https://godoc.org/gopkg.in/russross/blackfriday.v2.
It is `go get`-able via via [gopkg.in][6] at `gopkg.in/russross/blackfriday.v2`,
but we highly recommend using package management tool like [dep][7] or
[Glide][8] and make use of semantic versioning. With package management you
should import `github.com/russross/blackfriday` and specify that you're using
version 2.0.0.
Version 2 offers a number of improvements over v1: Version 2 offers a number of improvements over v1:
@ -198,7 +204,7 @@ implements the following extensions:
Cat Cat
: Fluffy animal everyone likes : Fluffy animal everyone likes
Internet Internet
: Vector of transmission for pictures of cats : Vector of transmission for pictures of cats
@ -209,7 +215,7 @@ implements the following extensions:
end of the document. A footnote looks like this: end of the document. A footnote looks like this:
This is a footnote.[^1] This is a footnote.[^1]
[^1]: the footnote text. [^1]: the footnote text.
* **Autolinking**. Blackfriday can find URLs that have not been * **Autolinking**. Blackfriday can find URLs that have not been
@ -255,9 +261,11 @@ are a few of note:
* [markdownfmt](https://github.com/shurcooL/markdownfmt): like gofmt, * [markdownfmt](https://github.com/shurcooL/markdownfmt): like gofmt,
but for markdown. but for markdown.
* [LaTeX output](https://bitbucket.org/ambrevar/blackfriday-latex): * [LaTeX output](https://github.com/Ambrevar/Blackfriday-LaTeX):
renders output as LaTeX. renders output as LaTeX.
* [Blackfriday-Confluence](https://github.com/kentaro-m/blackfriday-confluence): provides a [Confluence Wiki Markup](https://confluence.atlassian.com/doc/confluence-wiki-markup-251003035.html) renderer.
Todo Todo
---- ----

View File

@ -17,6 +17,7 @@ import (
"bytes" "bytes"
"html" "html"
"regexp" "regexp"
"strings"
"github.com/shurcooL/sanitized_anchor_name" "github.com/shurcooL/sanitized_anchor_name"
) )
@ -568,8 +569,8 @@ func (*Markdown) isHRule(data []byte) bool {
// isFenceLine checks if there's a fence line (e.g., ``` or ``` go) at the beginning of data, // isFenceLine checks if there's a fence line (e.g., ``` or ``` go) at the beginning of data,
// and returns the end index if so, or 0 otherwise. It also returns the marker found. // and returns the end index if so, or 0 otherwise. It also returns the marker found.
// If syntax is not nil, it gets set to the syntax specified in the fence line. // If info is not nil, it gets set to the syntax specified in the fence line.
func isFenceLine(data []byte, syntax *string, oldmarker string) (end int, marker string) { func isFenceLine(data []byte, info *string, oldmarker string) (end int, marker string) {
i, size := 0, 0 i, size := 0, 0
// skip up to three spaces // skip up to three spaces
@ -605,9 +606,9 @@ func isFenceLine(data []byte, syntax *string, oldmarker string) (end int, marker
} }
// TODO(shurcooL): It's probably a good idea to simplify the 2 code paths here // TODO(shurcooL): It's probably a good idea to simplify the 2 code paths here
// into one, always get the syntax, and discard it if the caller doesn't care. // into one, always get the info string, and discard it if the caller doesn't care.
if syntax != nil { if info != nil {
syn := 0 infoLength := 0
i = skipChar(data, i, ' ') i = skipChar(data, i, ' ')
if i >= len(data) { if i >= len(data) {
@ -617,14 +618,14 @@ func isFenceLine(data []byte, syntax *string, oldmarker string) (end int, marker
return 0, "" return 0, ""
} }
syntaxStart := i infoStart := i
if data[i] == '{' { if data[i] == '{' {
i++ i++
syntaxStart++ infoStart++
for i < len(data) && data[i] != '}' && data[i] != '\n' { for i < len(data) && data[i] != '}' && data[i] != '\n' {
syn++ infoLength++
i++ i++
} }
@ -634,31 +635,30 @@ func isFenceLine(data []byte, syntax *string, oldmarker string) (end int, marker
// strip all whitespace at the beginning and the end // strip all whitespace at the beginning and the end
// of the {} block // of the {} block
for syn > 0 && isspace(data[syntaxStart]) { for infoLength > 0 && isspace(data[infoStart]) {
syntaxStart++ infoStart++
syn-- infoLength--
} }
for syn > 0 && isspace(data[syntaxStart+syn-1]) { for infoLength > 0 && isspace(data[infoStart+infoLength-1]) {
syn-- infoLength--
} }
i++ i++
i = skipChar(data, i, ' ')
} else { } else {
for i < len(data) && !isspace(data[i]) { for i < len(data) && !isverticalspace(data[i]) {
syn++ infoLength++
i++ i++
} }
} }
*syntax = string(data[syntaxStart : syntaxStart+syn]) *info = strings.TrimSpace(string(data[infoStart : infoStart+infoLength]))
} }
i = skipChar(data, i, ' ') if i == len(data) {
if i >= len(data) || data[i] != '\n' { return i, marker
if i == len(data) { }
return i, marker if i > len(data) || data[i] != '\n' {
}
return 0, "" return 0, ""
} }
return i + 1, marker // Take newline into account. return i + 1, marker // Take newline into account.
@ -668,14 +668,14 @@ func isFenceLine(data []byte, syntax *string, oldmarker string) (end int, marker
// or 0 otherwise. It writes to out if doRender is true, otherwise it has no side effects. // or 0 otherwise. It writes to out if doRender is true, otherwise it has no side effects.
// If doRender is true, a final newline is mandatory to recognize the fenced code block. // If doRender is true, a final newline is mandatory to recognize the fenced code block.
func (p *Markdown) fencedCodeBlock(data []byte, doRender bool) int { func (p *Markdown) fencedCodeBlock(data []byte, doRender bool) int {
var syntax string var info string
beg, marker := isFenceLine(data, &syntax, "") beg, marker := isFenceLine(data, &info, "")
if beg == 0 || beg >= len(data) { if beg == 0 || beg >= len(data) {
return 0 return 0
} }
var work bytes.Buffer var work bytes.Buffer
work.Write([]byte(syntax)) work.Write([]byte(info))
work.WriteByte('\n') work.WriteByte('\n')
for { for {
@ -1148,6 +1148,18 @@ func (p *Markdown) list(data []byte, flags ListType) int {
return i return i
} }
// Returns true if the list item is not the same type as its parent list
func (p *Markdown) listTypeChanged(data []byte, flags *ListType) bool {
if p.dliPrefix(data) > 0 && *flags&ListTypeDefinition == 0 {
return true
} else if p.oliPrefix(data) > 0 && *flags&ListTypeOrdered == 0 {
return true
} else if p.uliPrefix(data) > 0 && (*flags&ListTypeOrdered != 0 || *flags&ListTypeDefinition != 0) {
return true
}
return false
}
// Returns true if block ends with a blank line, descending if needed // Returns true if block ends with a blank line, descending if needed
// into lists and sublists. // into lists and sublists.
func endsWithBlankLine(block *Node) bool { func endsWithBlankLine(block *Node) bool {
@ -1246,6 +1258,7 @@ func (p *Markdown) listItem(data []byte, flags *ListType) int {
// process the following lines // process the following lines
containsBlankLine := false containsBlankLine := false
sublist := 0 sublist := 0
codeBlockMarker := ""
gatherlines: gatherlines:
for line < len(data) { for line < len(data) {
@ -1279,6 +1292,27 @@ gatherlines:
chunk := data[line+indentIndex : i] chunk := data[line+indentIndex : i]
if p.extensions&FencedCode != 0 {
// determine if in or out of codeblock
// if in codeblock, ignore normal list processing
_, marker := isFenceLine(chunk, nil, codeBlockMarker)
if marker != "" {
if codeBlockMarker == "" {
// start of codeblock
codeBlockMarker = marker
} else {
// end of codeblock.
codeBlockMarker = ""
}
}
// we are in a codeblock, write line, and continue
if codeBlockMarker != "" || marker != "" {
raw.Write(data[line+indentIndex : i])
line = i
continue gatherlines
}
}
// evaluate how this line fits in // evaluate how this line fits in
switch { switch {
// is this a nested list item? // is this a nested list item?
@ -1286,14 +1320,21 @@ gatherlines:
p.oliPrefix(chunk) > 0 || p.oliPrefix(chunk) > 0 ||
p.dliPrefix(chunk) > 0: p.dliPrefix(chunk) > 0:
if containsBlankLine { // to be a nested list, it must be indented more
*flags |= ListItemContainsBlock // if not, it is either a different kind of list
// or the next item in the same list
if indent <= itemIndent {
if p.listTypeChanged(chunk, flags) {
*flags |= ListItemEndOfList
} else if containsBlankLine {
*flags |= ListItemContainsBlock
}
break gatherlines
} }
// to be a nested list, it must be indented more if containsBlankLine {
// if not, it is the next item in the same list *flags |= ListItemContainsBlock
if indent <= itemIndent {
break gatherlines
} }
// is this the first item in the nested list? // is this the first item in the nested list?

1
vendor/gopkg.in/russross/blackfriday.v2/go.mod generated vendored Normal file
View File

@ -0,0 +1 @@
module github.com/russross/blackfriday/v2

View File

@ -35,6 +35,7 @@ const (
Safelink // Only link to trusted protocols Safelink // Only link to trusted protocols
NofollowLinks // Only link with rel="nofollow" NofollowLinks // Only link with rel="nofollow"
NoreferrerLinks // Only link with rel="noreferrer" NoreferrerLinks // Only link with rel="noreferrer"
NoopenerLinks // Only link with rel="noopener"
HrefTargetBlank // Add a blank target HrefTargetBlank // Add a blank target
CompletePage // Generate a complete HTML page CompletePage // Generate a complete HTML page
UseXHTML // Generate XHTML output instead of HTML UseXHTML // Generate XHTML output instead of HTML
@ -87,6 +88,10 @@ type HTMLRendererParameters struct {
HeadingIDPrefix string HeadingIDPrefix string
// If set, add this text to the back of each Heading ID, to ensure uniqueness. // If set, add this text to the back of each Heading ID, to ensure uniqueness.
HeadingIDSuffix string HeadingIDSuffix string
// Increase heading levels: if the offset is 1, <h1> becomes <h2> etc.
// Negative offset is also valid.
// Resulting levels are clipped between 1 and 6.
HeadingLevelOffset int
Title string // Document title (used if CompletePage is set) Title string // Document title (used if CompletePage is set)
CSS string // Optional CSS file URL (used if CompletePage is set) CSS string // Optional CSS file URL (used if CompletePage is set)
@ -282,6 +287,9 @@ func appendLinkAttrs(attrs []string, flags HTMLFlags, link []byte) []string {
if flags&NoreferrerLinks != 0 { if flags&NoreferrerLinks != 0 {
val = append(val, "noreferrer") val = append(val, "noreferrer")
} }
if flags&NoopenerLinks != 0 {
val = append(val, "noopener")
}
if flags&HrefTargetBlank != 0 { if flags&HrefTargetBlank != 0 {
attrs = append(attrs, "target=\"_blank\"") attrs = append(attrs, "target=\"_blank\"")
} }
@ -331,7 +339,7 @@ func (r *HTMLRenderer) tag(w io.Writer, name []byte, attrs []string) {
func footnoteRef(prefix string, node *Node) []byte { func footnoteRef(prefix string, node *Node) []byte {
urlFrag := prefix + string(slugify(node.Destination)) urlFrag := prefix + string(slugify(node.Destination))
anchor := fmt.Sprintf(`<a rel="footnote" href="#fn:%s">%d</a>`, urlFrag, node.NoteID) anchor := fmt.Sprintf(`<a href="#fn:%s">%d</a>`, urlFrag, node.NoteID)
return []byte(fmt.Sprintf(`<sup class="footnote-ref" id="fnref:%s">%s</sup>`, urlFrag, anchor)) return []byte(fmt.Sprintf(`<sup class="footnote-ref" id="fnref:%s">%s</sup>`, urlFrag, anchor))
} }
@ -460,9 +468,10 @@ var (
) )
func headingTagsFromLevel(level int) ([]byte, []byte) { func headingTagsFromLevel(level int) ([]byte, []byte) {
switch level { if level <= 1 {
case 1:
return h1Tag, h1CloseTag return h1Tag, h1CloseTag
}
switch level {
case 2: case 2:
return h2Tag, h2CloseTag return h2Tag, h2CloseTag
case 3: case 3:
@ -471,9 +480,8 @@ func headingTagsFromLevel(level int) ([]byte, []byte) {
return h4Tag, h4CloseTag return h4Tag, h4CloseTag
case 5: case 5:
return h5Tag, h5CloseTag return h5Tag, h5CloseTag
default:
return h6Tag, h6CloseTag
} }
return h6Tag, h6CloseTag
} }
func (r *HTMLRenderer) outHRTag(w io.Writer) { func (r *HTMLRenderer) outHRTag(w io.Writer) {
@ -651,7 +659,8 @@ func (r *HTMLRenderer) RenderNode(w io.Writer, node *Node, entering bool) WalkSt
r.out(w, node.Literal) r.out(w, node.Literal)
r.cr(w) r.cr(w)
case Heading: case Heading:
openTag, closeTag := headingTagsFromLevel(node.Level) headingLevel := r.HTMLRendererParameters.HeadingLevelOffset + node.Level
openTag, closeTag := headingTagsFromLevel(headingLevel)
if entering { if entering {
if node.IsTitleblock { if node.IsTitleblock {
attrs = append(attrs, `class="title"`) attrs = append(attrs, `class="title"`)

View File

@ -23,8 +23,22 @@ var (
urlRe = `((https?|ftp):\/\/|\/)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)]+` urlRe = `((https?|ftp):\/\/|\/)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)]+`
anchorRe = regexp.MustCompile(`^(<a\shref="` + urlRe + `"(\stitle="[^"<>]+")?\s?>` + urlRe + `<\/a>)`) anchorRe = regexp.MustCompile(`^(<a\shref="` + urlRe + `"(\stitle="[^"<>]+")?\s?>` + urlRe + `<\/a>)`)
// TODO: improve this regexp to catch all possible entities: // https://www.w3.org/TR/html5/syntax.html#character-references
htmlEntityRe = regexp.MustCompile(`&[a-z]{2,5};`) // highest unicode code point in 17 planes (2^20): 1,114,112d =
// 7 dec digits or 6 hex digits
// named entity references can be 2-31 characters with stuff like &lt;
// at one end and &CounterClockwiseContourIntegral; at the other. There
// are also sometimes numbers at the end, although this isn't inherent
// in the specification; there are never numbers anywhere else in
// current character references, though; see &frac34; and &blk12;, etc.
// https://www.w3.org/TR/html5/syntax.html#named-character-references
//
// entity := "&" (named group | number ref) ";"
// named group := [a-zA-Z]{2,31}[0-9]{0,2}
// number ref := "#" (dec ref | hex ref)
// dec ref := [0-9]{1,7}
// hex ref := ("x" | "X") [0-9a-fA-F]{1,6}
htmlEntityRe = regexp.MustCompile(`&([a-zA-Z]{2,31}[0-9]{0,2}|#([0-9]{1,7}|[xX][0-9a-fA-F]{1,6}));`)
) )
// Functions to parse text within a block // Functions to parse text within a block

View File

@ -93,46 +93,46 @@ const (
// blockTags is a set of tags that are recognized as HTML block tags. // blockTags is a set of tags that are recognized as HTML block tags.
// Any of these can be included in markdown text without special escaping. // Any of these can be included in markdown text without special escaping.
var blockTags = map[string]struct{}{ var blockTags = map[string]struct{}{
"blockquote": struct{}{}, "blockquote": {},
"del": struct{}{}, "del": {},
"div": struct{}{}, "div": {},
"dl": struct{}{}, "dl": {},
"fieldset": struct{}{}, "fieldset": {},
"form": struct{}{}, "form": {},
"h1": struct{}{}, "h1": {},
"h2": struct{}{}, "h2": {},
"h3": struct{}{}, "h3": {},
"h4": struct{}{}, "h4": {},
"h5": struct{}{}, "h5": {},
"h6": struct{}{}, "h6": {},
"iframe": struct{}{}, "iframe": {},
"ins": struct{}{}, "ins": {},
"math": struct{}{}, "math": {},
"noscript": struct{}{}, "noscript": {},
"ol": struct{}{}, "ol": {},
"pre": struct{}{}, "pre": {},
"p": struct{}{}, "p": {},
"script": struct{}{}, "script": {},
"style": struct{}{}, "style": {},
"table": struct{}{}, "table": {},
"ul": struct{}{}, "ul": {},
// HTML5 // HTML5
"address": struct{}{}, "address": {},
"article": struct{}{}, "article": {},
"aside": struct{}{}, "aside": {},
"canvas": struct{}{}, "canvas": {},
"figcaption": struct{}{}, "figcaption": {},
"figure": struct{}{}, "figure": {},
"footer": struct{}{}, "footer": {},
"header": struct{}{}, "header": {},
"hgroup": struct{}{}, "hgroup": {},
"main": struct{}{}, "main": {},
"nav": struct{}{}, "nav": {},
"output": struct{}{}, "output": {},
"progress": struct{}{}, "progress": {},
"section": struct{}{}, "section": {},
"video": struct{}{}, "video": {},
} }
// Renderer is the rendering interface. This is mostly of interest if you are // Renderer is the rendering interface. This is mostly of interest if you are
@ -480,11 +480,11 @@ func (p *Markdown) parseRefsToAST() {
// [^note]: This is the explanation. // [^note]: This is the explanation.
// //
// Footnotes should be placed at the end of the document in an ordered list. // Footnotes should be placed at the end of the document in an ordered list.
// Inline footnotes such as: // Finally, there are inline footnotes such as:
// //
// Inline footnotes^[Not supported.] also exist. // Inline footnotes^[Also supported.] provide a quick inline explanation,
// but are rendered at the bottom of the document.
// //
// are not yet supported.
// reference holds all information necessary for a reference-style links or // reference holds all information necessary for a reference-style links or
// footnotes. // footnotes.
@ -813,7 +813,17 @@ func ispunct(c byte) bool {
// Test if a character is a whitespace character. // Test if a character is a whitespace character.
func isspace(c byte) bool { func isspace(c byte) bool {
return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v' return ishorizontalspace(c) || isverticalspace(c)
}
// Test if a character is a horizontal whitespace character.
func ishorizontalspace(c byte) bool {
return c == ' ' || c == '\t'
}
// Test if a character is a vertical character.
func isverticalspace(c byte) bool {
return c == '\n' || c == '\r' || c == '\f' || c == '\v'
} }
// Test if a character is letter. // Test if a character is letter.

View File

@ -1,24 +0,0 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof

View File

@ -1,9 +0,0 @@
language: go
go:
- 1.8
install:
- go get github.com/golang/lint/golint
- go get github.com/fzipp/gocyclo
- go get github.com/client9/misspell/...
- go get github.com/gordonklaus/ineffassign
script: ./hooks/pre-commit

View File

@ -1,6 +0,0 @@
# gomatrix
[![GoDoc](https://godoc.org/github.com/matrix-org/gomatrix?status.svg)](https://godoc.org/github.com/matrix-org/gomatrix)
A Golang Matrix client.
**THIS IS UNDER ACTIVE DEVELOPMENT: BREAKING CHANGES ARE FREQUENT.**

View File

@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2016 Tulir Asokan
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,6 +0,0 @@
# maulogger
A logger in Go.
Docs: [godoc.org/maunium.net/go/maulogger](https://godoc.org/maunium.net/go/maulogger)
Go get: `go get maunium.net/go/maulogger`

View File

@ -1,219 +0,0 @@
package maulog
import (
"bufio"
"fmt"
"os"
"time"
)
// Level is the severity level of a log entry.
type Level struct {
Name string
Severity, Color int
}
// LogWriter writes to the log with an optional prefix
type LogWriter struct {
Level Level
Prefix string
}
func (lw LogWriter) Write(p []byte) (n int, err error) {
log(lw.Level, fmt.Sprint(lw.Prefix, string(p)))
return len(p), nil
}
// GetColor gets the ANSI escape color code for the log level.
func (lvl Level) GetColor() []byte {
if lvl.Color < 0 {
return []byte("")
}
return []byte(fmt.Sprintf("\x1b[%dm", lvl.Color))
}
// GetReset gets the ANSI escape reset code.
func (lvl Level) GetReset() []byte {
if lvl.Color < 0 {
return []byte("")
}
return []byte("\x1b[0m")
}
var (
// Debug is the level for debug messages.
Debug = Level{Name: "DEBUG", Color: 36, Severity: 0}
// Info is the level for basic log messages.
Info = Level{Name: "INFO", Color: -1, Severity: 10}
// Warn is the level saying that something went wrong, but the program will continue operating mostly normally.
Warn = Level{Name: "WARN", Color: 33, Severity: 50}
// Error is the level saying that something went wrong and the program may not operate as expected, but will still continue.
Error = Level{Name: "ERROR", Color: 31, Severity: 100}
// Fatal is the level saying that something went wrong and the program will not operate normally.
Fatal = Level{Name: "FATAL", Color: 35, Severity: 9001}
)
// PrintLevel tells the first severity level at which messages should be printed to stdout
var PrintLevel = 10
// PrintDebug means PrintLevel = 0, kept for backwards compatibility
var PrintDebug = false
// FileTimeformat is the time format used in log file names.
var FileTimeformat = "2006-01-02"
// FileformatArgs is an undocumented integer.
var FileformatArgs = 3
// Fileformat is the format used for log file names.
var Fileformat = func(now string, i int) string { return fmt.Sprintf("%[1]s-%02[2]d.log", now, i) }
// Timeformat is the time format used in logging.
var Timeformat = "15:04:05 02.01.2006"
var writer *bufio.Writer
var lines int
// InitWithWriter initializes MauLogger with the given writer.
func InitWithWriter(w *bufio.Writer) {
writer = w
}
// Init initializes MauLogger.
func Init() {
// Find the next file name.
now := time.Now().Format(FileTimeformat)
i := 1
for ; ; i++ {
if _, err := os.Stat(Fileformat(now, i)); os.IsNotExist(err) {
break
}
if i == 99 {
i = 1
break
}
}
// Open the file
file, err := os.OpenFile(Fileformat(now, i), os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0700)
if err != nil {
panic(err)
}
if file == nil {
panic(os.ErrInvalid)
}
// Create a writer
writer = bufio.NewWriter(file)
}
// Debugf formats and logs a debug message.
func Debugf(message string, args ...interface{}) {
logln(Debug, fmt.Sprintf(message, args...))
}
// Printf formats and logs a string in the Info log level.
func Printf(message string, args ...interface{}) {
Infof(message, args...)
}
// Infof formats and logs a string in the Info log level.
func Infof(message string, args ...interface{}) {
logln(Info, fmt.Sprintf(message, args...))
}
// Warnf formats and logs a string in the Warn log level.
func Warnf(message string, args ...interface{}) {
logln(Warn, fmt.Sprintf(message, args...))
}
// Errorf formats and logs a string in the Error log level.
func Errorf(message string, args ...interface{}) {
logln(Error, fmt.Sprintf(message, args...))
}
// Fatalf formats and logs a string in the Fatal log level.
func Fatalf(message string, args ...interface{}) {
logln(Fatal, fmt.Sprintf(message, args...))
}
// Logf formats and logs a message in the given log level.
func Logf(level Level, message string, args ...interface{}) {
logln(level, fmt.Sprintf(message, args...))
}
// Debugln logs a debug message.
func Debugln(args ...interface{}) {
log(Debug, fmt.Sprintln(args...))
}
// Println logs a string in the Info log level.
func Println(args ...interface{}) {
Infoln(args...)
}
// Infoln logs a string in the Info log level.
func Infoln(args ...interface{}) {
log(Info, fmt.Sprintln(args...))
}
// Warnln logs a string in the Warn log level.
func Warnln(args ...interface{}) {
log(Warn, fmt.Sprintln(args...))
}
// Errorln logs a string in the Error log level.
func Errorln(args ...interface{}) {
log(Error, fmt.Sprintln(args...))
}
// Fatalln logs a string in the Fatal log level.
func Fatalln(args ...interface{}) {
log(Fatal, fmt.Sprintln(args...))
}
// Logln logs a message in the given log level.
func Logln(level Level, args ...interface{}) {
log(level, fmt.Sprintln(args...))
}
func logln(level Level, message string) {
log(level, fmt.Sprintln(message))
}
func log(level Level, message string) {
// Prefix the message with the timestamp and log level.
msg := []byte(fmt.Sprintf("[%[1]s] [%[2]s] %[3]s", time.Now().Format(Timeformat), level.Name, message))
if writer != nil {
// Write it to the log file.
_, err := writer.Write(msg)
if err != nil {
panic(err)
}
lines++
// Flush the file if needed
if lines == 5 {
lines = 0
writer.Flush()
}
}
// Print to stdout using correct color
if level.Severity >= PrintLevel || PrintDebug {
if level.Severity >= Error.Severity {
os.Stderr.Write(level.GetColor())
os.Stderr.Write(msg)
os.Stderr.Write(level.GetReset())
} else {
os.Stdout.Write(level.GetColor())
os.Stdout.Write(msg)
os.Stdout.Write(level.GetReset())
}
}
}
// Shutdown cleans up the logger.
func Shutdown() {
if writer != nil {
writer.Flush()
}
}

2
vendor/maunium.net/go/mautrix/.gitignore generated vendored Normal file
View File

@ -0,0 +1,2 @@
.idea/
.vscode/

4
vendor/maunium.net/go/mautrix/README.md generated vendored Normal file
View File

@ -0,0 +1,4 @@
# mautrix-go
[![GoDoc](https://godoc.org/maunium.net/go/mautrix?status.svg)](https://godoc.org/maunium.net/go/mautrix)
A Golang Matrix framework.

View File

@ -1,7 +1,7 @@
// Package gomatrix implements the Matrix Client-Server API. // Package mautrix implements the Matrix Client-Server API.
// //
// Specification can be found at http://matrix.org/docs/spec/client_server/r0.2.0.html // Specification can be found at http://matrix.org/docs/spec/client_server/r0.4.0.html
package gomatrix package mautrix
import ( import (
"bytes" "bytes"
@ -10,7 +10,6 @@ import (
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"maunium.net/go/maulogger"
"net/http" "net/http"
"net/url" "net/url"
"path" "path"
@ -20,6 +19,10 @@ import (
"time" "time"
) )
type Logger interface {
Debugfln(message string, args ...interface{})
}
// Client represents a Matrix client. // Client represents a Matrix client.
type Client struct { type Client struct {
HomeserverURL *url.URL // The base homeserver URL HomeserverURL *url.URL // The base homeserver URL
@ -29,7 +32,7 @@ type Client struct {
Client *http.Client // The underlying HTTP client which will be used to make HTTP requests. Client *http.Client // The underlying HTTP client which will be used to make HTTP requests.
Syncer Syncer // The thing which can process /sync responses Syncer Syncer // The thing which can process /sync responses
Store Storer // The thing which can store rooms/tokens/ids Store Storer // The thing which can store rooms/tokens/ids
Logger maulogger.Logger Logger Logger
// The ?user_id= query parameter for application services. This must be set *prior* to calling a method. If this is empty, // The ?user_id= query parameter for application services. This must be set *prior* to calling a method. If this is empty,
// no user_id parameter will be sent. // no user_id parameter will be sent.
@ -132,7 +135,6 @@ func (cli *Client) Sync() error {
filterID = resFilter.FilterID filterID = resFilter.FilterID
cli.Store.SaveFilterID(cli.UserID, filterID) cli.Store.SaveFilterID(cli.UserID, filterID)
} }
for { for {
resSync, err := cli.SyncRequest(30000, nextBatch, filterID, false, "") resSync, err := cli.SyncRequest(30000, nextBatch, filterID, false, "")
if err != nil { if err != nil {
@ -337,7 +339,7 @@ func (cli *Client) RegisterGuest(req *ReqRegister) (*RespRegister, *RespUserInte
// //
// This does not set credentials on the client instance. See SetCredentials() instead. // This does not set credentials on the client instance. See SetCredentials() instead.
// //
// res, err := cli.RegisterDummy(&gomatrix.ReqRegister{ // res, err := cli.RegisterDummy(&mautrix.ReqRegister{
// Username: "alice", // Username: "alice",
// Password: "wonderland", // Password: "wonderland",
// }) // })
@ -544,7 +546,7 @@ func (cli *Client) RedactEvent(roomID, eventID string, req *ReqRedact) (resp *Re
} }
// CreateRoom creates a new Matrix room. See https://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-createroom // CreateRoom creates a new Matrix room. See https://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-createroom
// resp, err := cli.CreateRoom(&gomatrix.ReqCreateRoom{ // resp, err := cli.CreateRoom(&mautrix.ReqCreateRoom{
// Preset: "public_chat", // Preset: "public_chat",
// }) // })
// fmt.Println("Room:", resp.RoomID) // fmt.Println("Room:", resp.RoomID)

View File

@ -1,4 +1,5 @@
package gomatrix // Copyright 2018 Tulir Asokan
package mautrix
import ( import (
"encoding/json" "encoding/json"
@ -101,7 +102,7 @@ var (
// Ephemeral events // Ephemeral events
var ( var (
EphemeralEventReceipt = EventType{"m.receipt", EphemeralEventType} EphemeralEventReceipt = EventType{"m.receipt", EphemeralEventType}
EphemeralEventTyping = EventType{"m.receipt", EphemeralEventType} EphemeralEventTyping = EventType{"m.typing", EphemeralEventType}
) )
// Account data events // Account data events
@ -165,6 +166,17 @@ type Unsigned struct {
PrevSender string `json:"prev_sender,omitempty"` PrevSender string `json:"prev_sender,omitempty"`
ReplacesState string `json:"replaces_state,omitempty"` ReplacesState string `json:"replaces_state,omitempty"`
Age int64 `json:"age,omitempty"` Age int64 `json:"age,omitempty"`
PassiveCommand map[string]*MatchedPassiveCommand `json:"m.passive_command,omitempty"`
}
type MatchedPassiveCommand struct {
// Matched string `json:"matched"`
// Value string `json:"value"`
Captured [][]string `json:"captured"`
BackCompatCommand string `json:"command"`
BackCompatArguments map[string]string `json:"arguments"`
} }
type Content struct { type Content struct {
@ -182,7 +194,8 @@ type Content struct {
// Membership key for easy access in m.room.member events // Membership key for easy access in m.room.member events
Membership Membership `json:"membership,omitempty"` Membership Membership `json:"membership,omitempty"`
RelatesTo *RelatesTo `json:"m.relates_to,omitempty"` RelatesTo *RelatesTo `json:"m.relates_to,omitempty"`
Command *MatchedCommand `json:"m.command,omitempty"`
PowerLevels PowerLevels
Member Member
@ -197,14 +210,30 @@ type Content struct {
type serializableContent Content type serializableContent Content
var DisableFancyEventParsing = false
func (content *Content) UnmarshalJSON(data []byte) error { func (content *Content) UnmarshalJSON(data []byte) error {
content.VeryRaw = data content.VeryRaw = data
if err := json.Unmarshal(data, &content.Raw); err != nil { if err := json.Unmarshal(data, &content.Raw); err != nil || DisableFancyEventParsing {
return err return err
} }
return json.Unmarshal(data, (*serializableContent)(content)) return json.Unmarshal(data, (*serializableContent)(content))
} }
func (content *Content) GetCommand() *MatchedCommand {
if content.Command == nil {
content.Command = &MatchedCommand{}
}
return content.Command
}
func (content *Content) GetRelatesTo() *RelatesTo {
if content.RelatesTo == nil {
content.RelatesTo = &RelatesTo{}
}
return content.RelatesTo
}
func (content *Content) UnmarshalPowerLevels() (pl PowerLevels, err error) { func (content *Content) UnmarshalPowerLevels() (pl PowerLevels, err error) {
err = json.Unmarshal(content.VeryRaw, &pl) err = json.Unmarshal(content.VeryRaw, &pl)
return return
@ -228,7 +257,7 @@ func (content *Content) GetInfo() *FileInfo {
} }
type Tags map[string]struct { type Tags map[string]struct {
Order string `json:"order"` Order json.Number `json:"order"`
} }
type RoomName struct { type RoomName struct {
@ -411,3 +440,9 @@ type InReplyTo struct {
// Not required, just for future-proofing // Not required, just for future-proofing
RoomID string `json:"room_id,omitempty"` RoomID string `json:"room_id,omitempty"`
} }
type MatchedCommand struct {
Target string `json:"target"`
Matched string `json:"matched"`
Arguments map[string]string `json:"arguments"`
}

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package gomatrix package mautrix
import "errors" import "errors"

View File

@ -1,4 +1,5 @@
package gomatrix // Copyright 2018 Tulir Asokan
package mautrix
import ( import (
"fmt" "fmt"

View File

@ -1,4 +1,4 @@
package gomatrix package mautrix
// ReqRegister is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-register // ReqRegister is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-register
type ReqRegister struct { type ReqRegister struct {
@ -31,7 +31,7 @@ type ReqCreateRoom struct {
Invite []string `json:"invite,omitempty"` Invite []string `json:"invite,omitempty"`
Invite3PID []ReqInvite3PID `json:"invite_3pid,omitempty"` Invite3PID []ReqInvite3PID `json:"invite_3pid,omitempty"`
CreationContent map[string]interface{} `json:"creation_content,omitempty"` CreationContent map[string]interface{} `json:"creation_content,omitempty"`
InitialState []*Event `json:"initial_state,omitempty"` InitialState []*Event `json:"initial_state,omitempty"`
Preset string `json:"preset,omitempty"` Preset string `json:"preset,omitempty"`
IsDirect bool `json:"is_direct,omitempty"` IsDirect bool `json:"is_direct,omitempty"`
} }
@ -79,4 +79,4 @@ type ReqTyping struct {
type ReqPresence struct { type ReqPresence struct {
Presence string `json:"presence"` Presence string `json:"presence"`
} }

View File

@ -1,4 +1,4 @@
package gomatrix package mautrix
// RespError is the standard JSON error response from Homeservers. It also implements the Golang "error" interface. // RespError is the standard JSON error response from Homeservers. It also implements the Golang "error" interface.
// See http://matrix.org/docs/spec/client_server/r0.2.0.html#api-standards // See http://matrix.org/docs/spec/client_server/r0.2.0.html#api-standards
@ -63,9 +63,9 @@ type RespJoinedMembers struct {
// RespMessages is the JSON response for https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-rooms-roomid-messages // RespMessages is the JSON response for https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-rooms-roomid-messages
type RespMessages struct { type RespMessages struct {
Start string `json:"start"` Start string `json:"start"`
Chunk []*Event `json:"chunk"` Chunk []*Event `json:"chunk"`
End string `json:"end"` End string `json:"end"`
} }
// RespSendEvent is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-send-eventtype-txnid // RespSendEvent is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-send-eventtype-txnid
@ -146,8 +146,8 @@ type RespSync struct {
} `json:"state"` } `json:"state"`
Timeline struct { Timeline struct {
Events []*Event `json:"events"` Events []*Event `json:"events"`
Limited bool `json:"limited"` Limited bool `json:"limited"`
PrevBatch string `json:"prev_batch"` PrevBatch string `json:"prev_batch"`
} `json:"timeline"` } `json:"timeline"`
} `json:"leave"` } `json:"leave"`
Join map[string]struct { Join map[string]struct {
@ -156,14 +156,14 @@ type RespSync struct {
} `json:"state"` } `json:"state"`
Timeline struct { Timeline struct {
Events []*Event `json:"events"` Events []*Event `json:"events"`
Limited bool `json:"limited"` Limited bool `json:"limited"`
PrevBatch string `json:"prev_batch"` PrevBatch string `json:"prev_batch"`
} `json:"timeline"` } `json:"timeline"`
Ephemeral struct { Ephemeral struct {
Events []*Event `json:"events"` Events []*Event `json:"events"`
} `json:"ephemeral"` } `json:"ephemeral"`
AccountData struct { AccountData struct {
Events []*Event `json:"events"` Events []*Event `json:"events"`
} `json:"account_data"` } `json:"account_data"`
} `json:"join"` } `json:"join"`
Invite map[string]struct { Invite map[string]struct {

View File

@ -1,4 +1,4 @@
package gomatrix package mautrix
// Room represents a single Matrix room. // Room represents a single Matrix room.
type Room struct { type Room struct {

View File

@ -1,4 +1,4 @@
package gomatrix package mautrix
// Storer is an interface which must be satisfied to store client data. // Storer is an interface which must be satisfied to store client data.
// //

View File

@ -1,4 +1,4 @@
package gomatrix package mautrix
import ( import (
"encoding/json" "encoding/json"

View File

@ -1,4 +1,4 @@
package gomatrix package mautrix
import ( import (
"bytes" "bytes"
@ -125,6 +125,6 @@ func ExtractUserLocalpart(userID string) (string, error) {
} }
return strings.TrimPrefix( return strings.TrimPrefix(
strings.SplitN(userID, ":", 2)[0], // @foo:bar:8448 => [ "@foo", "bar:8448" ] strings.SplitN(userID, ":", 2)[0], // @foo:bar:8448 => [ "@foo", "bar:8448" ]
"@", // remove "@" prefix "@", // remove "@" prefix
), nil ), nil
} }

270
vendor/maunium.net/go/tcell/README.adoc generated vendored Normal file
View File

@ -0,0 +1,270 @@
= tcell
image:https://img.shields.io/travis/gdamore/tcell.svg?label=linux[Linux Status,link="https://travis-ci.org/gdamore/tcell"]
image:https://img.shields.io/appveyor/ci/gdamore/tcell.svg?label=windows[Windows Status,link="https://ci.appveyor.com/project/gdamore/tcell"]
image:https://img.shields.io/badge/license-APACHE2-blue.svg[Apache License,link="https://github.com/gdamore/tcell/blob/master/LICENSE"]
image:https://img.shields.io/badge/gitter-join-brightgreen.svg[Gitter,link="https://gitter.im/gdamore/tcell"]
image:https://img.shields.io/badge/godoc-reference-blue.svg[GoDoc,link="https://godoc.org/github.com/gdamore/tcell"]
image:http://goreportcard.com/badge/gdamore/tcell[Go Report Card,link="http://goreportcard.com/report/gdamore/tcell"]
image:https://codecov.io/gh/gdamore/tcell/branch/master/graph/badge.svg[codecov,link="https://codecov.io/gh/gdamore/tcell"]
image:https://tidelift.com/badges/github/gdamore/tcell?style=flat[Dependencies]
[cols="2",grid="none"]
|===
|_Tcell_ is a _Go_ package that provides a cell based view for text terminals, like _xterm_.
It was inspired by _termbox_, but includes many additional improvements.
a|[.right]
image::logos/tcell.png[float="right"]
|===
## Examples
* https://github.com/gdamore/proxima5[proxima5] - space shooter (https://youtu.be/jNxKTCmY_bQ[video])
* https://github.com/gdamore/govisor[govisor] - service management UI (http://2.bp.blogspot.com/--OsvnfzSNow/Vf7aqMw3zXI/AAAAAAAAARo/uOMtOvw4Sbg/s1600/Screen%2BShot%2B2015-09-20%2Bat%2B9.08.41%2BAM.png[screenshot])
* mouse demo - included mouse test (http://2.bp.blogspot.com/-fWvW5opT0es/VhIdItdKqJI/AAAAAAAAATE/7Ojc0L1SpB0/s1600/Screen%2BShot%2B2015-10-04%2Bat%2B11.47.13%2BPM.png[screenshot])
* https://github.com/gdamore/gomatrix[gomatrix] - converted from Termbox
* https://github.com/zyedidia/micro/[micro] - lightweight text editor with syntax-highlighting and themes
* https://github.com/viktomas/godu[godu] - simple golang utility helping to discover large files/folders.
* https://github.com/rivo/tview[tview] - rich interactive widgets for terminal UIs
* https://github.com/marcusolsson/tui-go[tui-go] - UI library for terminal apps
* https://github.com/rgm3/gomandelbrot[gomandelbrot] - Mandelbrot!
* https://github.com/senorprogrammer/wtf[WTF]- Personal information dashboard for your terminal
* https://github.com/browsh-org/browsh[browsh] - A fully-modern text-based browser, rendering to TTY and browsers (https://www.youtube.com/watch?v=HZq86XfBoRo[video])
* https://github.com/sachaos/go-life[go-life] - Conway's Game of Life.
## Pure Go Terminfo Database
_Tcell_ includes a full parser and expander for terminfo capability strings,
so that it can avoid hard coding escape strings for formatting. It also favors
portability, and includes support for all POSIX systems.
The database is also flexible & extensible, and can modified by either running
a program to build the entire database, or an entry for just a single terminal.
## More Portable
_Tcell_ is portable to a wide variety of systems.
_Tcell_ is believed
to work with all of the systems officially supported by golang with
the exception of nacl (which lacks any kind of a terminal interface).
(Plan9 is not supported by _Tcell_, but it is experimental status only
in golang.) For all of these systems *except Solaris/illumos*, _Tcell_
is pure Go, with no need for CGO.
## No Async IO
_Tcell_ is able to operate without requiring `SIGIO` signals (unlike _termbox_),
or asynchronous I/O, and can instead use standard Go file
objects and Go routines.
This means it should be safe, especially for
use with programs that use exec, or otherwise need to manipulate the
tty streams.
This model is also much closer to idiomatic Go, leading
to fewer surprises.
## Rich Unicode & non-Unicode support
_Tcell_ includes enhanced support for Unicode, including wide characters and
combining characters, provided your terminal can support them.
Note that
Windows terminals generally don't support the full Unicode repertoire.
It will also convert to and from Unicode locales, so that the program
can work with UTF-8 internally, and get reasonable output in other locales.
_Tcell_ tries hard to convert to native characters on both input and output, and
on output _Tcell_ even makes use of the alternate character set to facilitate
drawing certain characters.
## More Function Keys
_Tcell_ also has richer support for a larger number of special keys that some terminals can send.
## Better Color Handling
_Tcell_ will respect your terminal's color space as specified within your terminfo
entries, so that for example attempts to emit color sequences on VT100 terminals
won't result in unintended consequences.
In Windows mode, _Tcell_ supports 16 colors, bold, dim, and reverse,
instead of just termbox's 8 colors with reverse. (Note that there is some
conflation with bold/dim and colors.)
_Tcell_ maps 16 colors down to 8, for terminals that need it.
(The upper 8 colors are just brighter versions of the lower 8.)
## Better Mouse Support
_Tcell_ supports enhanced mouse tracking mode, so your application can receive
regular mouse motion events, and wheel events, if your terminal supports it.
## _Termbox_ Compatibility
A compatibility layer for _termbox_ is provided in the `compat` directory.
To use it, try importing `github.com/gdamore/tcell/termbox`
instead. Most _termbox-go_ programs will probably work without further
modification.
## Working With Unicode
Internally Tcell uses UTF-8, just like Go.
However, Tcell understands how to
convert to and from other character sets, using the capabilities of
the `golang.org/x/text/encoding packages`.
Your application must supply
them, as the full set of the most common ones bloats the program by about 2MB.
If you're lazy, and want them all anyway, see the `encoding` sub-directory.
## Wide & Combining Characters
The `SetContent()` API takes a primary rune, and an optional list of combining runes.
If any of the runes is a wide (East Asian) rune occupying two cells,
then the library will skip output from the following cell, but care must be
taken in the application to avoid explicitly attempting to set content in the
next cell, otherwise the results are undefined. (Normally wide character
is displayed, and the other character is not; do not depend on that behavior.)
Experience has shown that the vanilla Windows 8 console application does not
support any of these characters properly, but at least some options like
_ConEmu_ do support Wide characters.
## Colors
_Tcell_ assumes the ANSI/XTerm color model, including the 256 color map that
XTerm uses when it supports 256 colors. The terminfo guidance will be
honored, with respect to the number of colors supported. Also, only
terminals which expose ANSI style `setaf` and `setab` will support color;
if you have a color terminal that only has `setf` and `setb`, please let me
know; it wouldn't be hard to add that if there is need.
## 24-bit Color
_Tcell_ _supports true color_! (That is, if your terminal can support it,
_Tcell_ can accurately display 24-bit color.)
To use 24-bit color, you need to use a terminal that supports it. Modern
xterm and similar teminal emulators can support this. As terminfo lacks any
way to describe this capability, we fabricate the capability for
terminals with names ending in `*-truecolor`. The stock distribution ships
with a database that defines `xterm-truecolor`.
To try it out, set your
`TERM` variable to `xterm-truecolor`.
When using TrueColor, programs will display the colors that the programmer
intended, overriding any "`themes`" you may have set in your terminal
emulator. (For some cases, accurate color fidelity is more important
than respecting themes. For other cases, such as typical text apps that
only use a few colors, its more desirable to respect the themes that
the user has established.)
If you find this undesirable, you can either use a `TERM` variable
that lacks the `TRUECOLOR` setting, or set `TCELL_TRUECOLOR=disable` in your
environment.
## Performance
Reasonable attempts have been made to minimize sending data to terminals,
avoiding repeated sequences or drawing the same cell on refresh updates.
## Terminfo
(Not relevent for Windows users.)
The Terminfo implementation operates with two forms of database. The first
is the built-in go database, which contains a number of real database entries
that are compiled into the program directly. This should minimize calling
out to database file searches.
The second is in the form of JSON files, that contain the same information,
which can be located either by the `$TCELLDB` environment file, `$HOME/.tcelldb`,
or is located in the Go source directory as `database.json`.
These files (both the Go and the JSON files) can be generated using the
mkinfo.go program. If you need to regnerate the entire set for some reason,
run the mkdatabase.sh file. The generation uses the infocmp(1) program on
the system to collect the necessary information.
The `mkinfo.go` program can also be used to generate specific database entries
for named terminals, in case your favorite terminal is missing. (If you
find that this is the case, please let me know and I'll try to add it!)
_Tcell_ requires that the terminal support the `cup` mode of cursor addressing.
Terminals without absolute cursor addressability are not supported.
This is unlikely to be a problem; such terminals have not been mass produced
since the early 1970s.
## Mouse Support
Mouse support is detected via the `kmous` terminfo variable, however,
enablement/disablement and decoding mouse events is done using hard coded
sequences based on the XTerm X11 model. As of this writing all popular
terminals with mouse tracking support this model. (Full terminfo support
is not possible as terminfo sequences are not defined.)
On Windows, the mouse works normally.
Mouse wheel buttons on various terminals are known to work, but the support
in terminal emulators, as well as support for various buttons and
live mouse tracking, varies widely. Modern _xterm_, macOS _Terminal_, and _iTerm_ all work well.
## Testablity
There is a `SimulationScreen`, that can be used to simulate a real screen
for automated testing. The supplied tests do this. The simulation contains
event delivery, screen resizing support, and capabilities to inject events
and examine "`physical`" screen contents.
## Platforms
### POSIX (Linux, FreeBSD, macOS, Solaris, etc.)
For mainstream systems with a suitably well defined system call interface
to tty settings, everything works using pure Go.
For the remainder (right now means only Solaris/illumos) we use POSIX function
calls to manage termios, which implies that CGO is required on those platforms.
### Windows
Windows console mode applications are supported. Unfortunately _mintty_
and other _cygwin_ style applications are not supported.
Modern console applications like ConEmu, as well as the Windows 10
console itself, support all the good features (resize, mouse tracking, etc.)
I haven't figured out how to cleanly resolve the dichotomy between cygwin
style termios and the Windows Console API; it seems that perhaps nobody else
has either. If anyone has suggestions, let me know! Really, if you're
using a Windows application, you should use the native Windows console or a
fully compatible console implementation.
### Plan9 and Native Client (Nacl)
The nacl and plan9 platforms won't work, but compilation stubs are supplied
for folks that want to include parts of this in software targetting those
platforms. The Simulation screen works, but as Tcell doesn't know how to
allocate a real screen object on those platforms, `NewScreen()` will fail.
If anyone has wisdom about how to improve support for either of these,
please let me know. PRs are especially welcome.
### Commercial Support
_Tcell_ is absolutely free, but if you want to obtain commercial, professional support, there are options.
[cols="2",align="center",frame="none", grid="none"]
|===
^.^|
image:logos/tidelift.png[100,100]
a|
https://tidelift.com/[Tidelift] subscriptions include support for _Tcell_, as well as many other open source packages.
^.^|
image:logos/staysail.png[100,100]
a|
mailto:info@staysail.tech[Staysail Systems, Inc.] offers direct support, and custom development around _Tcell_ on an hourly basis.
^.^|
image:logos/patreon.png[100,100]
a|I also welcome donations at https://www.patron.com/gedamore/[Patreon], if you just want to make a contribution.
|===

View File

@ -52,6 +52,10 @@ func (cb *CellBuffer) SetContent(x int, y int,
i := 0 i := 0
for i < len(c.currComb) { for i < len(c.currComb) {
r := c.currComb[i] r := c.currComb[i]
if r == '\u200d' {
i += 2
continue
}
if runewidth.RuneWidth(r) != 0 { if runewidth.RuneWidth(r) != 0 {
// not a combining character, yank it // not a combining character, yank it
c.currComb = append(c.currComb[:i-1], c.currComb[i+1:]...) c.currComb = append(c.currComb[:i-1], c.currComb[i+1:]...)
@ -175,12 +179,13 @@ func (cb *CellBuffer) Resize(w, h int) {
// Fill fills the entire cell buffer array with the specified character // Fill fills the entire cell buffer array with the specified character
// and style. Normally choose ' ' to clear the screen. This API doesn't // and style. Normally choose ' ' to clear the screen. This API doesn't
// support combining characters. // support combining characters, or characters with a width larger than one.
func (cb *CellBuffer) Fill(r rune, style Style) { func (cb *CellBuffer) Fill(r rune, style Style) {
for i := range cb.cells { for i := range cb.cells {
c := &cb.cells[i] c := &cb.cells[i]
c.currMain = r c.currMain = r
c.currComb = nil c.currComb = nil
c.currStyle = style c.currStyle = style
c.width = 1
} }
} }

View File

@ -28,7 +28,6 @@ type cScreen struct {
in syscall.Handle in syscall.Handle
out syscall.Handle out syscall.Handle
cancelflag syscall.Handle cancelflag syscall.Handle
title syscall.Handle
scandone chan struct{} scandone chan struct{}
evch chan Event evch chan Event
quit chan struct{} quit chan struct{}

BIN
vendor/maunium.net/go/tcell/tcell.png generated vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

View File

@ -1,93 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
id="svg8"
version="1.1"
viewBox="0 0 210 297"
height="297mm"
width="210mm">
<defs
id="defs2">
<linearGradient
id="linearGradient2680">
<stop
id="stop2676"
offset="0"
style="stop-color:#ababab;stop-opacity:1;" />
<stop
id="stop2678"
offset="1"
style="stop-color:#ababab;stop-opacity:0;" />
</linearGradient>
<marker
style="overflow:visible"
id="Arrow1Lstart"
refX="0.0"
refY="0.0"
orient="auto">
<path
transform="scale(0.8) translate(12.5,0)"
style="fill-rule:evenodd;stroke:#000000;stroke-width:1.0pt"
d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z "
id="path848" />
</marker>
<radialGradient
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1,0,0,0.28804762,0,94.764912)"
r="19.622099"
fy="133.10568"
fx="111.58373"
cy="133.10568"
cx="111.58373"
id="radialGradient2684"
xlink:href="#linearGradient2680" />
</defs>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1">
<rect
y="99.44445"
x="31.750006"
height="86.430557"
width="129.64584"
id="rect1130"
style="opacity:1;fill:#5d6c53;fill-opacity:1;stroke:#244f24;stroke-width:10;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<text
transform="scale(0.99941234,1.000588)"
id="text1128"
y="160.47581"
x="44.689861"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:46.0962944px;line-height:1.25;font-family:'Glass TTY VT220';-inkscape-font-specification:'Glass TTY VT220, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#5ef86d;fill-opacity:1;stroke:#59ff32;stroke-width:0.80994618;stroke-opacity:0.94520545;"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:46.0962944px;font-family:'Glass TTY VT220';-inkscape-font-specification:'Glass TTY VT220, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#5ef86d;fill-opacity:1;stroke:#59ff32;stroke-width:0.80994618;stroke-opacity:0.94520545;"
y="160.47581"
x="44.689861"
id="tspan1126">tcell</tspan></text>
<flowRoot
style="fill:black;fill-opacity:1;stroke:none;font-family:sans-serif;font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;letter-spacing:0px;word-spacing:0px"
id="flowRoot1132"
xml:space="preserve"><flowRegion
id="flowRegion1134"><rect
y="432.51968"
x="290"
height="160"
width="330"
id="rect1136" /></flowRegion><flowPara
id="flowPara1138"></flowPara></flowRoot> </g>
</svg>

Before

Width:  |  Height:  |  Size: 3.7 KiB

152
vendor/maunium.net/go/tcell/terminfo/term_termite.go generated vendored Normal file
View File

@ -0,0 +1,152 @@
// Generated automatically. DO NOT HAND-EDIT.
package terminfo
func init() {
// VTE-based terminal
AddTerminfo(&Terminfo{
Name: "xterm-termite",
Columns: 80,
Lines: 24,
Colors: 256,
Bell: "\a",
Clear: "\x1b[H\x1b[2J",
EnterCA: "\x1b[?1049h",
ExitCA: "\x1b[?1049l",
ShowCursor: "\x1b[?12l\x1b[?25h",
HideCursor: "\x1b[?25l",
AttrOff: "\x1b(B\x1b[m",
Underline: "\x1b[4m",
Bold: "\x1b[1m",
Dim: "\x1b[2m",
Reverse: "\x1b[7m",
EnterKeypad: "\x1b[?1h\x1b=",
ExitKeypad: "\x1b[?1l\x1b>",
SetFg: "\x1b[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m",
SetBg: "\x1b[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m",
SetFgBg: "\x1b[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;;%?%p2%{8}%<%t4%p2%d%e%p2%{16}%<%t10%p2%{8}%-%d%e48;5;%p2%d%;m",
AltChars: "++,,--..00``aaffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~",
EnterAcs: "\x1b(0",
ExitAcs: "\x1b(B",
Mouse: "\x1b[M",
MouseMode: "%?%p1%{1}%=%t%'h'%Pa%e%'l'%Pa%;\x1b[?1000%ga%c\x1b[?1002%ga%c\x1b[?1003%ga%c\x1b[?1006%ga%c",
SetCursor: "\x1b[%i%p1%d;%p2%dH",
CursorBack1: "\b",
CursorUp1: "\x1b[A",
KeyUp: "\x1bOA",
KeyDown: "\x1bOB",
KeyRight: "\x1bOC",
KeyLeft: "\x1bOD",
KeyInsert: "\x1b[2~",
KeyDelete: "\x1b[3~",
KeyBackspace: "\xff",
KeyHome: "\x1bOH",
KeyEnd: "\x1bOF",
KeyPgUp: "\x1b[5~",
KeyPgDn: "\x1b[6~",
KeyF1: "\x1bOP",
KeyF2: "\x1bOQ",
KeyF3: "\x1bOR",
KeyF4: "\x1bOS",
KeyF5: "\x1b[15~",
KeyF6: "\x1b[17~",
KeyF7: "\x1b[18~",
KeyF8: "\x1b[19~",
KeyF9: "\x1b[20~",
KeyF10: "\x1b[21~",
KeyF11: "\x1b[23~",
KeyF12: "\x1b[24~",
KeyF13: "\x1b[1;2P",
KeyF14: "\x1b[1;2Q",
KeyF15: "\x1b[1;2R",
KeyF16: "\x1b[1;2S",
KeyF17: "\x1b[15;2~",
KeyF18: "\x1b[17;2~",
KeyF19: "\x1b[18;2~",
KeyF20: "\x1b[19;2~",
KeyF21: "\x1b[20;2~",
KeyF22: "\x1b[21;2~",
KeyF23: "\x1b[23;2~",
KeyF24: "\x1b[24;2~",
KeyF25: "\x1b[1;5P",
KeyF26: "\x1b[1;5Q",
KeyF27: "\x1b[1;5R",
KeyF28: "\x1b[1;5S",
KeyF29: "\x1b[15;5~",
KeyF30: "\x1b[17;5~",
KeyF31: "\x1b[18;5~",
KeyF32: "\x1b[19;5~",
KeyF33: "\x1b[20;5~",
KeyF34: "\x1b[21;5~",
KeyF35: "\x1b[23;5~",
KeyF36: "\x1b[24;5~",
KeyF37: "\x1b[1;6P",
KeyF38: "\x1b[1;6Q",
KeyF39: "\x1b[1;6R",
KeyF40: "\x1b[1;6S",
KeyF41: "\x1b[15;6~",
KeyF42: "\x1b[17;6~",
KeyF43: "\x1b[18;6~",
KeyF44: "\x1b[19;6~",
KeyF45: "\x1b[20;6~",
KeyF46: "\x1b[21;6~",
KeyF47: "\x1b[23;6~",
KeyF48: "\x1b[24;6~",
KeyF49: "\x1b[1;3P",
KeyF50: "\x1b[1;3Q",
KeyF51: "\x1b[1;3R",
KeyF52: "\x1b[1;3S",
KeyF53: "\x1b[15;3~",
KeyF54: "\x1b[17;3~",
KeyF55: "\x1b[18;3~",
KeyF56: "\x1b[19;3~",
KeyF57: "\x1b[20;3~",
KeyF58: "\x1b[21;3~",
KeyF59: "\x1b[23;3~",
KeyF60: "\x1b[24;3~",
KeyF61: "\x1b[1;4P",
KeyF62: "\x1b[1;4Q",
KeyF63: "\x1b[1;4R",
KeyBacktab: "\x1b[Z",
KeyShfLeft: "\x1b[1;2D",
KeyShfRight: "\x1b[1;2C",
KeyShfUp: "\x1b[1;2A",
KeyShfDown: "\x1b[1;2B",
KeyCtrlLeft: "\x1b[1;5D",
KeyCtrlRight: "\x1b[1;5C",
KeyCtrlUp: "\x1b[1;5A",
KeyCtrlDown: "\x1b[1;5B",
KeyMetaLeft: "\x1b[1;9D",
KeyMetaRight: "\x1b[1;9C",
KeyMetaUp: "\x1b[1;9A",
KeyMetaDown: "\x1b[1;9B",
KeyAltLeft: "\x1b[1;3D",
KeyAltRight: "\x1b[1;3C",
KeyAltUp: "\x1b[1;3A",
KeyAltDown: "\x1b[1;3B",
KeyAltShfLeft: "\x1b[1;4D",
KeyAltShfRight: "\x1b[1;4C",
KeyAltShfUp: "\x1b[1;4A",
KeyAltShfDown: "\x1b[1;4B",
KeyMetaShfLeft: "\x1b[1;10D",
KeyMetaShfRight: "\x1b[1;10C",
KeyMetaShfUp: "\x1b[1;10A",
KeyMetaShfDown: "\x1b[1;10B",
KeyCtrlShfLeft: "\x1b[1;6D",
KeyCtrlShfRight: "\x1b[1;6C",
KeyCtrlShfUp: "\x1b[1;6A",
KeyCtrlShfDown: "\x1b[1;6B",
KeyShfHome: "\x1b[1;2H",
KeyShfEnd: "\x1b[1;2F",
KeyCtrlHome: "\x1b[1;5H",
KeyCtrlEnd: "\x1b[1;5F",
KeyAltHome: "\x1b[1;9H",
KeyAltEnd: "\x1b[1;9F",
KeyCtrlShfHome: "\x1b[1;6H",
KeyCtrlShfEnd: "\x1b[1;6F",
KeyMetaShfHome: "\x1b[1;10H",
KeyMetaShfEnd: "\x1b[1;10F",
KeyAltShfHome: "\x1b[1;4H",
KeyAltShfEnd: "\x1b[1;4F",
})
}

View File

@ -444,8 +444,8 @@ func (t *tScreen) ResetTitle() {
func (t *tScreen) Fini() { func (t *tScreen) Fini() {
t.Lock() t.Lock()
defer t.Unlock() defer t.Unlock()
ti := t.ti ti := t.ti
t.cells.Resize(0, 0) t.cells.Resize(0, 0)
t.TPuts(ti.ShowCursor) t.TPuts(ti.ShowCursor)
t.TPuts(ti.AttrOff) t.TPuts(ti.AttrOff)
@ -467,7 +467,7 @@ func (t *tScreen) Fini() {
default: default:
close(t.quit) close(t.quit)
} }
t.termioFini() t.termioFini()
} }

View File

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) [year] [fullname] Copyright (c) 2018 Oliver Kuederle
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -12,6 +12,7 @@ Among these components are:
- __Input forms__ (include __input/password fields__, __drop-down selections__, __checkboxes__, and __buttons__) - __Input forms__ (include __input/password fields__, __drop-down selections__, __checkboxes__, and __buttons__)
- Navigable multi-color __text views__ - Navigable multi-color __text views__
- Sophisticated navigable __table views__ - Sophisticated navigable __table views__
- Flexible __tree views__
- Selectable __lists__ - Selectable __lists__
- __Grid__, __Flexbox__ and __page layouts__ - __Grid__, __Flexbox__ and __page layouts__
- Modal __message windows__ - Modal __message windows__
@ -64,9 +65,17 @@ Add your issue here on GitHub. Feel free to get in touch if you have any questio
(There are no corresponding tags in the project. I only keep such a history in this README.) (There are no corresponding tags in the project. I only keep such a history in this README.)
- v0.19 (2018-10-28)
- Added `QueueUpdate()` and `QueueEvent()` to `Application` to help with modifications to primitives from goroutines.
- v0.18 (2018-10-18)
- `InputField` elements can now be navigated freely.
- v0.17 (2018-06-20)
- Added `TreeView`.
- v0.15 (2018-05-02)
- `Flex` and `Grid` don't clear their background per default, thus allowing for custom modals. See the [Wiki](https://github.com/rivo/tview/wiki/Modal) for an example.
- v0.14 (2018-04-13) - v0.14 (2018-04-13)
- Added an `Escape()` function which keep strings like color or region tags from being recognized as such. - Added an `Escape()` function which keep strings like color or region tags from being recognized as such.
- Added `ANSIIWriter()` and `TranslateANSII()` which convert ANSII escape sequences to `tview` color tags. - Added `ANSIWriter()` and `TranslateANSI()` which convert ANSI escape sequences to `tview` color tags.
- v0.13 (2018-04-01) - v0.13 (2018-04-01)
- Added background colors and text attributes to color tags. - Added background colors and text attributes to color tags.
- v0.12 (2018-03-13) - v0.12 (2018-03-13)

View File

@ -8,44 +8,44 @@ import (
"strings" "strings"
) )
// The states of the ANSII escape code parser. // The states of the ANSI escape code parser.
const ( const (
ansiiText = iota ansiText = iota
ansiiEscape ansiEscape
ansiiSubstring ansiSubstring
ansiiControlSequence ansiControlSequence
) )
// ansii is a io.Writer which translates ANSII escape codes into tview color // ansi is a io.Writer which translates ANSI escape codes into tview color
// tags. // tags.
type ansii struct { type ansi struct {
io.Writer io.Writer
// Reusable buffers. // Reusable buffers.
buffer *bytes.Buffer // The entire output text of one Write(). buffer *bytes.Buffer // The entire output text of one Write().
csiParameter, csiIntermediate *bytes.Buffer // Partial CSI strings. csiParameter, csiIntermediate *bytes.Buffer // Partial CSI strings.
// The current state of the parser. One of the ansii constants. // The current state of the parser. One of the ansi constants.
state int state int
} }
// ANSIIWriter returns an io.Writer which translates any ANSII escape codes // ANSIWriter returns an io.Writer which translates any ANSI escape codes
// written to it into tview color tags. Other escape codes don't have an effect // written to it into tview color tags. Other escape codes don't have an effect
// and are simply removed. The translated text is written to the provided // and are simply removed. The translated text is written to the provided
// writer. // writer.
func ANSIIWriter(writer io.Writer) io.Writer { func ANSIWriter(writer io.Writer) io.Writer {
return &ansii{ return &ansi{
Writer: writer, Writer: writer,
buffer: new(bytes.Buffer), buffer: new(bytes.Buffer),
csiParameter: new(bytes.Buffer), csiParameter: new(bytes.Buffer),
csiIntermediate: new(bytes.Buffer), csiIntermediate: new(bytes.Buffer),
state: ansiiText, state: ansiText,
} }
} }
// Write parses the given text as a string of runes, translates ANSII escape // Write parses the given text as a string of runes, translates ANSI escape
// codes to color tags and writes them to the output writer. // codes to color tags and writes them to the output writer.
func (a *ansii) Write(text []byte) (int, error) { func (a *ansi) Write(text []byte) (int, error) {
defer func() { defer func() {
a.buffer.Reset() a.buffer.Reset()
}() }()
@ -54,23 +54,23 @@ func (a *ansii) Write(text []byte) (int, error) {
switch a.state { switch a.state {
// We just entered an escape sequence. // We just entered an escape sequence.
case ansiiEscape: case ansiEscape:
switch r { switch r {
case '[': // Control Sequence Introducer. case '[': // Control Sequence Introducer.
a.csiParameter.Reset() a.csiParameter.Reset()
a.csiIntermediate.Reset() a.csiIntermediate.Reset()
a.state = ansiiControlSequence a.state = ansiControlSequence
case 'c': // Reset. case 'c': // Reset.
fmt.Fprint(a.buffer, "[-:-:-]") fmt.Fprint(a.buffer, "[-:-:-]")
a.state = ansiiText a.state = ansiText
case 'P', ']', 'X', '^', '_': // Substrings and commands. case 'P', ']', 'X', '^', '_': // Substrings and commands.
a.state = ansiiSubstring a.state = ansiSubstring
default: // Ignore. default: // Ignore.
a.state = ansiiText a.state = ansiText
} }
// CSI Sequences. // CSI Sequences.
case ansiiControlSequence: case ansiControlSequence:
switch { switch {
case r >= 0x30 && r <= 0x3f: // Parameter bytes. case r >= 0x30 && r <= 0x3f: // Parameter bytes.
if _, err := a.csiParameter.WriteRune(r); err != nil { if _, err := a.csiParameter.WriteRune(r); err != nil {
@ -166,16 +166,16 @@ func (a *ansii) Write(text []byte) (int, error) {
red := (colorNumber - 16) / 36 red := (colorNumber - 16) / 36
green := ((colorNumber - 16) / 6) % 6 green := ((colorNumber - 16) / 6) % 6
blue := (colorNumber - 16) % 6 blue := (colorNumber - 16) % 6
color = fmt.Sprintf("%02x%02x%02x", 255*red/5, 255*green/5, 255*blue/5) color = fmt.Sprintf("#%02x%02x%02x", 255*red/5, 255*green/5, 255*blue/5)
} else if colorNumber <= 255 { } else if colorNumber <= 255 {
grey := 255 * (colorNumber - 232) / 23 grey := 255 * (colorNumber - 232) / 23
color = fmt.Sprintf("%02x%02x%02x", grey, grey, grey) color = fmt.Sprintf("#%02x%02x%02x", grey, grey, grey)
} }
} else if fields[index+1] == "2" && len(fields) > index+4 { // 24-bit colors. } else if fields[index+1] == "2" && len(fields) > index+4 { // 24-bit colors.
red, _ := strconv.Atoi(fields[index+2]) red, _ := strconv.Atoi(fields[index+2])
green, _ := strconv.Atoi(fields[index+3]) green, _ := strconv.Atoi(fields[index+3])
blue, _ := strconv.Atoi(fields[index+4]) blue, _ := strconv.Atoi(fields[index+4])
color = fmt.Sprintf("%02x%02x%02x", red, green, blue) color = fmt.Sprintf("#%02x%02x%02x", red, green, blue)
} }
} }
if len(color) > 0 { if len(color) > 0 {
@ -194,22 +194,22 @@ func (a *ansii) Write(text []byte) (int, error) {
fmt.Fprintf(a.buffer, "[%s:%s%s]", foreground, background, attributes) fmt.Fprintf(a.buffer, "[%s:%s%s]", foreground, background, attributes)
} }
} }
a.state = ansiiText a.state = ansiText
default: // Undefined byte. default: // Undefined byte.
a.state = ansiiText // Abort CSI. a.state = ansiText // Abort CSI.
} }
// We just entered a substring/command sequence. // We just entered a substring/command sequence.
case ansiiSubstring: case ansiSubstring:
if r == 27 { // Most likely the end of the substring. if r == 27 { // Most likely the end of the substring.
a.state = ansiiEscape a.state = ansiEscape
} // Ignore all other characters. } // Ignore all other characters.
// "ansiiText" and all others. // "ansiText" and all others.
default: default:
if r == 27 { if r == 27 {
// This is the start of an escape sequence. // This is the start of an escape sequence.
a.state = ansiiEscape a.state = ansiEscape
} else { } else {
// Just a regular rune. Send to buffer. // Just a regular rune. Send to buffer.
if _, err := a.buffer.WriteRune(r); err != nil { if _, err := a.buffer.WriteRune(r); err != nil {
@ -227,11 +227,11 @@ func (a *ansii) Write(text []byte) (int, error) {
return len(text), nil return len(text), nil
} }
// TranslateANSII replaces ANSII escape sequences found in the provided string // TranslateANSI replaces ANSI escape sequences found in the provided string
// with tview's color tags and returns the resulting string. // with tview's color tags and returns the resulting string.
func TranslateANSII(text string) string { func TranslateANSI(text string) string {
var buffer bytes.Buffer var buffer bytes.Buffer
writer := ANSIIWriter(&buffer) writer := ANSIWriter(&buffer)
writer.Write([]byte(text)) writer.Write([]byte(text))
return buffer.String() return buffer.String()
} }

View File

@ -1,24 +1,36 @@
package tview package tview
import ( import (
"fmt"
"os"
"sync" "sync"
"maunium.net/go/tcell" "maunium.net/go/tcell"
) )
// The size of the event/update/redraw channels.
const queueSize = 100
// Application represents the top node of an application. // Application represents the top node of an application.
// //
// It is not strictly required to use this class as none of the other classes // It is not strictly required to use this class as none of the other classes
// depend on it. However, it provides useful tools to set up an application and // depend on it. However, it provides useful tools to set up an application and
// plays nicely with all widgets. // plays nicely with all widgets.
//
// The following command displays a primitive p on the screen until Ctrl-C is
// pressed:
//
// if err := tview.NewApplication().SetRoot(p, true).Run(); err != nil {
// panic(err)
// }
type Application struct { type Application struct {
sync.RWMutex sync.RWMutex
// The application's screen. // The application's screen.
screen tcell.Screen screen tcell.Screen
// Indicates whether the application's screen is currently active. This is
// false during suspended mode.
running bool
// The primitive which currently has the keyboard focus. // The primitive which currently has the keyboard focus.
focus Primitive focus Primitive
@ -48,13 +60,23 @@ type Application struct {
// was drawn. // was drawn.
afterDraw func(screen tcell.Screen) afterDraw func(screen tcell.Screen)
// If this value is true, the application has entered suspended mode. // Used to send screen events from separate goroutine to main event loop
suspended bool events chan tcell.Event
// Functions queued from goroutines, used to serialize updates to primitives.
updates chan func()
// A channel which signals the end of the suspended mode.
suspendToken chan struct{}
} }
// NewApplication creates and returns a new application. // NewApplication creates and returns a new application.
func NewApplication() *Application { func NewApplication() *Application {
return &Application{} return &Application{
events: make(chan tcell.Event, queueSize),
updates: make(chan func(), queueSize),
suspendToken: make(chan struct{}, 1),
}
} }
// SetInputCapture sets a function which captures all key events before they are // SetInputCapture sets a function which captures all key events before they are
@ -97,140 +119,222 @@ func (a *Application) GetScreen() tcell.Screen {
return a.screen return a.screen
} }
// SetScreen allows you to provide your own tcell.Screen object. For most
// applications, this is not needed and you should be familiar with
// tcell.Screen when using this function. Run() will call Init() and Fini() on
// the provided screen object.
//
// This function is typically called before calling Run(). Calling it while an
// application is running will switch the application to the new screen. Fini()
// will be called on the old screen and Init() on the new screen (errors
// returned by Init() will lead to a panic).
//
// Note that calling Suspend() will invoke Fini() on your screen object and it
// will not be restored when suspended mode ends. Instead, a new default screen
// object will be created.
func (a *Application) SetScreen(screen tcell.Screen) *Application {
a.Lock()
defer a.Unlock()
if a.running {
a.screen.Fini()
}
a.screen = screen
if a.running {
if err := a.screen.Init(); err != nil {
panic(err)
}
}
return a
}
// Run starts the application and thus the event loop. This function returns // Run starts the application and thus the event loop. This function returns
// when Stop() was called. // when Stop() was called.
func (a *Application) Run() error { func (a *Application) Run() error {
var err error var err error
a.Lock() a.Lock()
// Make a screen. // Make a screen if there is none yet.
a.screen, err = tcell.NewScreen() if a.screen == nil {
if err != nil { a.screen, err = tcell.NewScreen()
a.Unlock() if err != nil {
return err a.Unlock()
return err
}
} }
if err = a.screen.Init(); err != nil { if err = a.screen.Init(); err != nil {
a.Unlock() a.Unlock()
return err return err
} }
a.screen.EnableMouse() a.screen.EnableMouse()
a.running = true
// We catch panics to clean up because they mess up the terminal.
defer func() {
if p := recover(); p != nil {
if a.screen != nil {
a.screen.Fini()
}
a.running = false
panic(p)
}
}()
// Draw the screen for the first time. // Draw the screen for the first time.
a.Unlock() a.Unlock()
a.Draw() a.draw()
// Start event loop. // Separate loop to wait for screen events.
for { var wg sync.WaitGroup
a.Lock() wg.Add(1)
screen := a.screen a.suspendToken <- struct{}{} // We need this to get started.
if a.suspended { go func() {
a.suspended = false // Clear previous suspended flag. defer wg.Done()
} for range a.suspendToken {
a.Unlock() for {
if screen == nil { a.RLock()
break screen := a.screen
} a.RUnlock()
if screen == nil {
// Wait for next event. // We have no screen. We might need to stop.
event := a.screen.PollEvent()
if event == nil {
a.Lock()
if a.suspended {
// This screen was renewed due to suspended mode.
a.suspended = false
a.Unlock()
continue // Resume.
}
a.Unlock()
// The screen was finalized. Exit the loop.
break
}
switch event := event.(type) {
case *tcell.EventKey:
a.RLock()
p := a.focus
a.RUnlock()
// Intercept keys.
if a.inputCapture != nil {
event = a.inputCapture(event)
if event == nil {
break // Don't forward event.
}
}
// Pass other key events to the currently focused primitive.
if p != nil {
if handler := p.InputHandler(); handler != nil {
handler(event, func(p Primitive) {
a.SetFocus(p)
})
a.Draw()
}
}
case *tcell.EventMouse:
a.RLock()
p := a.focus
a.RUnlock()
// Intercept keys.
if a.mouseCapture != nil {
event = a.mouseCapture(event)
if event == nil {
break // Don't forward event.
}
}
// Pass other key events to the currently focused primitive.
if p != nil {
if handler := p.MouseHandler(); handler != nil {
handler(event, func(p Primitive) {
a.SetFocus(p)
})
//a.Draw()
}
}
case *tcell.EventPaste:
a.RLock()
p := a.focus
a.RUnlock()
if a.pasteCapture != nil {
event = a.pasteCapture(event)
if event == nil {
break break
} }
// Wait for next event and queue it.
event := screen.PollEvent()
if event != nil {
// Regular event. Queue.
a.QueueEvent(event)
continue
}
// A screen was finalized (event is nil).
a.RLock()
running := a.running
a.RUnlock()
if running {
// The application was stopped. End the event loop.
a.QueueEvent(nil)
return
}
// We're in suspended mode (running is false). Pause and wait for new
// token.
break
}
}
}()
// Start event loop.
EventLoop:
for {
select {
case event := <-a.events:
if event == nil {
break EventLoop
} }
if p != nil { switch event := event.(type) {
if handler := p.PasteHandler(); handler != nil { case *tcell.EventKey:
handler(event) a.RLock()
a.Draw() p := a.focus
inputCapture := a.inputCapture
a.RUnlock()
// Intercept keys.
if inputCapture != nil {
event = inputCapture(event)
if event == nil {
continue // Don't forward event.
}
} }
// Ctrl-C closes the application.
if event.Key() == tcell.KeyCtrlC {
a.Stop()
}
// Pass other key events to the currently focused primitive.
if p != nil {
if handler := p.InputHandler(); handler != nil {
handler(event, func(p Primitive) {
a.SetFocus(p)
})
a.draw()
}
}
case *tcell.EventMouse:
a.RLock()
p := a.focus
a.RUnlock()
// Intercept keys.
if a.mouseCapture != nil {
event = a.mouseCapture(event)
if event == nil {
break // Don't forward event.
}
}
// Pass other key events to the currently focused primitive.
if p != nil {
if handler := p.MouseHandler(); handler != nil {
handler(event, func(p Primitive) {
a.SetFocus(p)
})
//a.Draw()
}
}
case *tcell.EventPaste:
a.RLock()
p := a.focus
a.RUnlock()
if a.pasteCapture != nil {
event = a.pasteCapture(event)
if event == nil {
break
}
}
if p != nil {
if handler := p.PasteHandler(); handler != nil {
handler(event)
a.Draw()
}
}
case *tcell.EventResize:
a.RLock()
screen := a.screen
a.RUnlock()
screen.Clear()
a.draw()
} }
case *tcell.EventResize:
a.Lock() // If we have updates, now is the time to execute them.
screen := a.screen case updater := <-a.updates:
a.Unlock() updater()
screen.Clear()
a.Draw()
} }
} }
a.running = false
close(a.suspendToken)
wg.Wait()
return nil return nil
} }
// Stop stops the application, causing Run() to return. // Stop stops the application, causing Run() to return.
func (a *Application) Stop() { func (a *Application) Stop() {
a.RLock() a.Lock()
defer a.RUnlock() defer a.Unlock()
if a.screen == nil { screen := a.screen
if screen == nil {
return return
} }
a.screen.Fini()
a.screen = nil a.screen = nil
screen.Fini()
// a.running is still true, the main loop will clean up.
} }
// Suspend temporarily suspends the application by exiting terminal UI mode and // Suspend temporarily suspends the application by exiting terminal UI mode and
@ -243,29 +347,24 @@ func (a *Application) Stop() {
func (a *Application) Suspend(f func()) bool { func (a *Application) Suspend(f func()) bool {
a.Lock() a.Lock()
if a.suspended || a.screen == nil { screen := a.screen
// Application is already suspended. if screen == nil {
// Screen has not yet been initialized.
a.Unlock() a.Unlock()
return false return false
} }
// Enter suspended mode. // Enter suspended mode. Make a new screen here already so our event loop can
a.suspended = true // continue.
a.screen = nil
a.running = false
screen.Fini()
a.Unlock() a.Unlock()
a.Stop()
// Deal with panics during suspended mode. Exit the program.
defer func() {
if p := recover(); p != nil {
fmt.Println(p)
os.Exit(1)
}
}()
// Wait for "f" to return. // Wait for "f" to return.
f() f()
// Make a new screen and redraw. // Initialize our new screen and draw the contents.
a.Lock() a.Lock()
var err error var err error
a.screen, err = tcell.NewScreen() a.screen, err = tcell.NewScreen()
@ -278,23 +377,36 @@ func (a *Application) Suspend(f func()) bool {
panic(err) panic(err)
} }
a.screen.EnableMouse() a.screen.EnableMouse()
a.running = true
a.Unlock() a.Unlock()
a.Draw() a.draw()
a.suspendToken <- struct{}{}
// One key event will get lost, see https://github.com/gdamore/tcell/issues/194
// Continue application loop. // Continue application loop.
return true return true
} }
// Draw refreshes the screen. It calls the Draw() function of the application's // Draw refreshes the screen (during the next update cycle). It calls the Draw()
// root primitive and then syncs the screen buffer. // function of the application's root primitive and then syncs the screen
// buffer.
func (a *Application) Draw() *Application { func (a *Application) Draw() *Application {
a.RLock() a.QueueUpdate(func() {
a.draw()
})
return a
}
// draw actually does what Draw() promises to do.
func (a *Application) draw() *Application {
a.Lock()
defer a.Unlock()
screen := a.screen screen := a.screen
root := a.root root := a.root
fullscreen := a.rootFullscreen fullscreen := a.rootFullscreen
before := a.beforeDraw before := a.beforeDraw
after := a.afterDraw after := a.afterDraw
a.RUnlock()
// Maybe we're not ready yet or not anymore. // Maybe we're not ready yet or not anymore.
if screen == nil || root == nil { if screen == nil || root == nil {
@ -427,3 +539,35 @@ func (a *Application) GetFocus() Primitive {
defer a.RUnlock() defer a.RUnlock()
return a.focus return a.focus
} }
// QueueUpdate is used to synchronize access to primitives from non-main
// goroutines. The provided function will be executed as part of the event loop
// and thus will not cause race conditions with other such update functions or
// the Draw() function.
//
// Note that Draw() is not implicitly called after the execution of f as that
// may not be desirable. You can call Draw() from f if the screen should be
// refreshed after each update. Alternatively, use QueueUpdateDraw() to follow
// up with an immediate refresh of the screen.
func (a *Application) QueueUpdate(f func()) *Application {
a.updates <- f
return a
}
// QueueUpdateDraw works like QueueUpdate() except it refreshes the screen
// immediately after executing f.
func (a *Application) QueueUpdateDraw(f func()) *Application {
a.QueueUpdate(func() {
f()
a.draw()
})
return a
}
// QueueEvent sends an event to the Application event loop.
//
// It is not recommended for event to be nil.
func (a *Application) QueueEvent(event tcell.Event) *Application {
a.events <- event
return a
}

45
vendor/maunium.net/go/tview/borders.go generated vendored Normal file
View File

@ -0,0 +1,45 @@
package tview
// Borders defines various borders used when primitives are drawn.
// These may be changed to accommodate a different look and feel.
var Borders = struct {
Horizontal rune
Vertical rune
TopLeft rune
TopRight rune
BottomLeft rune
BottomRight rune
LeftT rune
RightT rune
TopT rune
BottomT rune
Cross rune
HorizontalFocus rune
VerticalFocus rune
TopLeftFocus rune
TopRightFocus rune
BottomLeftFocus rune
BottomRightFocus rune
}{
Horizontal: BoxDrawingsLightHorizontal,
Vertical: BoxDrawingsLightVertical,
TopLeft: BoxDrawingsLightDownAndRight,
TopRight: BoxDrawingsLightDownAndLeft,
BottomLeft: BoxDrawingsLightUpAndRight,
BottomRight: BoxDrawingsLightUpAndLeft,
LeftT: BoxDrawingsLightVerticalAndRight,
RightT: BoxDrawingsLightVerticalAndLeft,
TopT: BoxDrawingsLightDownAndHorizontal,
BottomT: BoxDrawingsLightUpAndHorizontal,
Cross: BoxDrawingsLightVerticalAndHorizontal,
HorizontalFocus: BoxDrawingsDoubleHorizontal,
VerticalFocus: BoxDrawingsDoubleVertical,
TopLeftFocus: BoxDrawingsDoubleDownAndRight,
TopRightFocus: BoxDrawingsDoubleDownAndLeft,
BottomLeftFocus: BoxDrawingsDoubleUpAndRight,
BottomRightFocus: BoxDrawingsDoubleUpAndLeft,
}

88
vendor/maunium.net/go/tview/box.go generated vendored
View File

@ -32,6 +32,9 @@ type Box struct {
// The color of the border. // The color of the border.
borderColor tcell.Color borderColor tcell.Color
// The style attributes of the border.
borderAttributes tcell.AttrMask
// The title. Only visible if there is a border, too. // The title. Only visible if there is a border, too.
title string title string
@ -48,10 +51,6 @@ type Box struct {
// Whether or not this box has focus. // Whether or not this box has focus.
hasFocus bool hasFocus bool
// If set to true, the inner rect of this box will be within the screen at the
// last time the box was drawn.
clampToScreen bool
// An optional capture function which receives a key event and returns the // An optional capture function which receives a key event and returns the
// event to be forwarded to the primitive's default input handler (nil if // event to be forwarded to the primitive's default input handler (nil if
// nothing should be forwarded). // nothing should be forwarded).
@ -78,7 +77,6 @@ func NewBox() *Box {
borderColor: Styles.BorderColor, borderColor: Styles.BorderColor,
titleColor: Styles.TitleColor, titleColor: Styles.TitleColor,
titleAlign: AlignCenter, titleAlign: AlignCenter,
clampToScreen: true,
} }
b.focus = b b.focus = b
return b return b
@ -121,6 +119,7 @@ func (b *Box) SetRect(x, y, width, height int) {
b.y = y b.y = y
b.width = width b.width = width
b.height = height b.height = height
b.innerX = -1 // Mark inner rect as uninitialized.
} }
// SetDrawFunc sets a callback function which is invoked after the box primitive // SetDrawFunc sets a callback function which is invoked after the box primitive
@ -273,6 +272,15 @@ func (b *Box) SetBorderColor(color tcell.Color) *Box {
return b return b
} }
// SetBorderAttributes sets the border's style attributes. You can combine
// different attributes using bitmask operations:
//
// box.SetBorderAttributes(tcell.AttrUnderline | tcell.AttrBold)
func (b *Box) SetBorderAttributes(attr tcell.AttrMask) *Box {
b.borderAttributes = attr
return b
}
// SetTitle sets the box's title. // SetTitle sets the box's title.
func (b *Box) SetTitle(title string) *Box { func (b *Box) SetTitle(title string) *Box {
b.title = title b.title = title
@ -319,30 +327,30 @@ func (b *Box) Draw(screen tcell.Screen) {
// Draw border. // Draw border.
if b.border && b.width >= 2 && b.height >= 2 { if b.border && b.width >= 2 && b.height >= 2 {
border := background.Foreground(b.borderColor) border := background.Foreground(b.borderColor) | tcell.Style(b.borderAttributes)
var vertical, horizontal, topLeft, topRight, bottomLeft, bottomRight rune var vertical, horizontal, topLeft, topRight, bottomLeft, bottomRight rune
if b.focus.HasFocus() { if b.focus.HasFocus() {
vertical = GraphicsDbVertBar horizontal = Borders.HorizontalFocus
horizontal = GraphicsDbHorBar vertical = Borders.VerticalFocus
topLeft = GraphicsDbTopLeftCorner topLeft = Borders.TopLeftFocus
topRight = GraphicsDbTopRightCorner topRight = Borders.TopRightFocus
bottomLeft = GraphicsDbBottomLeftCorner bottomLeft = Borders.BottomLeftFocus
bottomRight = GraphicsDbBottomRightCorner bottomRight = Borders.BottomRightFocus
} else { } else {
vertical = GraphicsHoriBar horizontal = Borders.Horizontal
horizontal = GraphicsVertBar vertical = Borders.Vertical
topLeft = GraphicsTopLeftCorner topLeft = Borders.TopLeft
topRight = GraphicsTopRightCorner topRight = Borders.TopRight
bottomLeft = GraphicsBottomLeftCorner bottomLeft = Borders.BottomLeft
bottomRight = GraphicsBottomRightCorner bottomRight = Borders.BottomRight
} }
for x := b.x + 1; x < b.x+b.width-1; x++ { for x := b.x + 1; x < b.x+b.width-1; x++ {
screen.SetContent(x, b.y, vertical, nil, border) screen.SetContent(x, b.y, horizontal, nil, border)
screen.SetContent(x, b.y+b.height-1, vertical, nil, border) screen.SetContent(x, b.y+b.height-1, horizontal, nil, border)
} }
for y := b.y + 1; y < b.y+b.height-1; y++ { for y := b.y + 1; y < b.y+b.height-1; y++ {
screen.SetContent(b.x, y, horizontal, nil, border) screen.SetContent(b.x, y, vertical, nil, border)
screen.SetContent(b.x+b.width-1, y, horizontal, nil, border) screen.SetContent(b.x+b.width-1, y, vertical, nil, border)
} }
screen.SetContent(b.x, b.y, topLeft, nil, border) screen.SetContent(b.x, b.y, topLeft, nil, border)
screen.SetContent(b.x+b.width-1, b.y, topRight, nil, border) screen.SetContent(b.x+b.width-1, b.y, topRight, nil, border)
@ -351,11 +359,11 @@ func (b *Box) Draw(screen tcell.Screen) {
// Draw title. // Draw title.
if b.title != "" && b.width >= 4 { if b.title != "" && b.width >= 4 {
_, printed := Print(screen, b.title, b.x+1, b.y, b.width-2, b.titleAlign, b.titleColor) printed, _ := Print(screen, b.title, b.x+1, b.y, b.width-2, b.titleAlign, b.titleColor)
if StringWidth(b.title)-printed > 0 && printed > 0 { if len(b.title)-printed > 0 && printed > 0 {
_, _, style, _ := screen.GetContent(b.x+b.width-2, b.y) _, _, style, _ := screen.GetContent(b.x+b.width-2, b.y)
fg, _, _ := style.Decompose() fg, _, _ := style.Decompose()
Print(screen, string(GraphicsEllipsis), b.x+b.width-2, b.y, 1, AlignLeft, fg) Print(screen, string(SemigraphicsHorizontalEllipsis), b.x+b.width-2, b.y, 1, AlignLeft, fg)
} }
} }
} }
@ -370,22 +378,20 @@ func (b *Box) Draw(screen tcell.Screen) {
} }
// Clamp inner rect to screen. // Clamp inner rect to screen.
if b.clampToScreen { width, height := screen.Size()
width, height := screen.Size() if b.innerX < 0 {
if b.innerX < 0 { b.innerWidth += b.innerX
b.innerWidth += b.innerX b.innerX = 0
b.innerX = 0 }
} if b.innerX+b.innerWidth >= width {
if b.innerX+b.innerWidth >= width { b.innerWidth = width - b.innerX
b.innerWidth = width - b.innerX }
} if b.innerY+b.innerHeight >= height {
if b.innerY+b.innerHeight >= height { b.innerHeight = height - b.innerY
b.innerHeight = height - b.innerY }
} if b.innerY < 0 {
if b.innerY < 0 { b.innerHeight += b.innerY
b.innerHeight += b.innerY b.innerY = 0
b.innerY = 0
}
} }
} }

View File

@ -124,9 +124,9 @@ func (c *Checkbox) SetChangedFunc(handler func(checked bool)) *Checkbox {
return c return c
} }
// SetDoneFunc sets a handler which is called when the user is done entering // SetDoneFunc sets a handler which is called when the user is done using the
// text. The callback function is provided with the key that was pressed, which // checkbox. The callback function is provided with the key that was pressed,
// is one of the following: // which is one of the following:
// //
// - KeyEscape: Abort text input. // - KeyEscape: Abort text input.
// - KeyTab: Move to the next field. // - KeyTab: Move to the next field.

31
vendor/maunium.net/go/tview/doc.go generated vendored
View File

@ -7,10 +7,12 @@ Widgets
The package implements the following widgets: The package implements the following widgets:
- TextView: Scrollable windows that display multi-colored text. Text may also - TextView: A scrollable window that display multi-colored text. Text may also
be highlighted. be highlighted.
- Table: Scrollable display of tabular data. Table cells, rows, or columns may - Table: A scrollable display of tabular data. Table cells, rows, or columns
also be highlighted. may also be highlighted.
- TreeView: A scrollable display for hierarchical data. Tree nodes can be
highlighted, collapsed, expanded, and more.
- List: A navigable text list with optional keyboard shortcuts. - List: A navigable text list with optional keyboard shortcuts.
- InputField: One-line input fields to enter text. - InputField: One-line input fields to enter text.
- DropDown: Drop-down selection fields. - DropDown: Drop-down selection fields.
@ -83,7 +85,7 @@ tag is as follows:
[<foreground>:<background>:<flags>] [<foreground>:<background>:<flags>]
Each of the three fields can be left blank and trailing fields can be ommitted. Each of the three fields can be left blank and trailing fields can be omitted.
(Empty square brackets "[]", however, are not considered color tags.) Colors (Empty square brackets "[]", however, are not considered color tags.) Colors
that are not specified will be left unchanged. A field with just a dash ("-") that are not specified will be left unchanged. A field with just a dash ("-")
means "reset to default". means "reset to default".
@ -135,6 +137,27 @@ Unicode Support
This package supports unicode characters including wide characters. This package supports unicode characters including wide characters.
Concurrency
Many functions in this package are not thread-safe. For many applications, this
may not be an issue: If your code makes changes in response to key events, it
will execute in the main goroutine and thus will not cause any race conditions.
If you access your primitives from other goroutines, however, you will need to
synchronize execution. The easiest way to do this is to call
Application.QueueUpdate() or Application.QueueUpdateDraw() (see the function
documentation for details):
go func() {
app.QueueUpdateDraw(func() {
table.SetCellSimple(0, 0, "Foo bar")
})
}()
One exception to this is the io.Writer interface implemented by TextView. You
can safely write to a TextView from any goroutine. See the TextView
documentation for details.
Type Hierarchy Type Hierarchy
All widgets listed above contain the Box type. All of Box's functions are All widgets listed above contain the Box type. All of Box's functions are

View File

@ -354,6 +354,7 @@ func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr
// Hand control over to the list. // Hand control over to the list.
d.open = true d.open = true
optionBefore := d.currentOption
d.list.SetSelectedFunc(func(index int, mainText, secondaryText string, shortcut rune) { d.list.SetSelectedFunc(func(index int, mainText, secondaryText string, shortcut rune) {
// An option was selected. Close the list again. // An option was selected. Close the list again.
d.open = false d.open = false
@ -374,6 +375,10 @@ func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr
d.prefix = string(r[:len(r)-1]) d.prefix = string(r[:len(r)-1])
} }
evalPrefix() evalPrefix()
} else if event.Key() == tcell.KeyEscape {
d.open = false
d.currentOption = optionBefore
setFocus(d)
} else { } else {
d.prefix = "" d.prefix = ""
} }

17
vendor/maunium.net/go/tview/flex.go generated vendored
View File

@ -28,7 +28,7 @@ type Flex struct {
*Box *Box
// The items to be positioned. // The items to be positioned.
items []flexItem items []*flexItem
// FlexRow or FlexColumn. // FlexRow or FlexColumn.
direction int direction int
@ -79,7 +79,7 @@ func (f *Flex) SetFullScreen(fullScreen bool) *Flex {
// You can provide a nil value for the primitive. This will still consume screen // You can provide a nil value for the primitive. This will still consume screen
// space but nothing will be drawn. // space but nothing will be drawn.
func (f *Flex) AddItem(item Primitive, fixedSize, proportion int, focus bool) *Flex { func (f *Flex) AddItem(item Primitive, fixedSize, proportion int, focus bool) *Flex {
f.items = append(f.items, flexItem{Item: item, FixedSize: fixedSize, Proportion: proportion, Focus: focus}) f.items = append(f.items, &flexItem{Item: item, FixedSize: fixedSize, Proportion: proportion, Focus: focus})
return f return f
} }
@ -94,6 +94,19 @@ func (f *Flex) RemoveItem(p Primitive) *Flex {
return f return f
} }
// ResizeItem sets a new size for the item(s) with the given primitive. If there
// are multiple Flex items with the same primitive, they will all receive the
// same size. For details regarding the size parameters, see AddItem().
func (f *Flex) ResizeItem(p Primitive, fixedSize, proportion int) *Flex {
for _, item := range f.items {
if item.Item == p {
item.FixedSize = fixedSize
item.Proportion = proportion
}
}
return f
}
// Draw draws this primitive onto the screen. // Draw draws this primitive onto the screen.
func (f *Flex) Draw(screen tcell.Screen) { func (f *Flex) Draw(screen tcell.Screen) {
f.Box.Draw(screen) f.Box.Draw(screen)

53
vendor/maunium.net/go/tview/form.go generated vendored
View File

@ -26,7 +26,7 @@ type FormItem interface {
// required. // required.
GetFieldWidth() int GetFieldWidth() int
// SetEnteredFunc sets the handler function for when the user finished // SetFinishedFunc sets the handler function for when the user finished
// entering data into the item. The handler may receive events for the // entering data into the item. The handler may receive events for the
// Enter key (we're done), the Escape key (cancel input), the Tab key (move to // Enter key (we're done), the Escape key (cancel input), the Tab key (move to
// next field), and the Backtab key (move to previous field). // next field), and the Backtab key (move to previous field).
@ -218,6 +218,37 @@ func (f *Form) AddButton(label string, selected func()) *Form {
return f return f
} }
// GetButton returns the button at the specified 0-based index. Note that
// buttons have been specially prepared for this form and modifying some of
// their attributes may have unintended side effects.
func (f *Form) GetButton(index int) *Button {
return f.buttons[index]
}
// RemoveButton removes the button at the specified position, starting with 0
// for the button that was added first.
func (f *Form) RemoveButton(index int) *Form {
f.buttons = append(f.buttons[:index], f.buttons[index+1:]...)
return f
}
// GetButtonCount returns the number of buttons in this form.
func (f *Form) GetButtonCount() int {
return len(f.buttons)
}
// GetButtonIndex returns the index of the button with the given label, starting
// with 0 for the button that was added first. If no such label was found, -1
// is returned.
func (f *Form) GetButtonIndex(label string) int {
for index, button := range f.buttons {
if button.GetLabel() == label {
return index
}
}
return -1
}
// Clear removes all input elements from the form, including the buttons if // Clear removes all input elements from the form, including the buttons if
// specified. // specified.
func (f *Form) Clear(includeButtons bool) *Form { func (f *Form) Clear(includeButtons bool) *Form {
@ -251,6 +282,14 @@ func (f *Form) GetFormItem(index int) FormItem {
return f.items[index] return f.items[index]
} }
// RemoveFormItem removes the form element at the given position, starting with
// index 0. Elements are referenced in the order they were added. Buttons are
// not included.
func (f *Form) RemoveFormItem(index int) *Form {
f.items = append(f.items[:index], f.items[index+1:]...)
return f
}
// GetFormItemByLabel returns the first form element with the given label. If // GetFormItemByLabel returns the first form element with the given label. If
// no such element is found, nil is returned. Buttons are not searched and will // no such element is found, nil is returned. Buttons are not searched and will
// therefore not be returned. // therefore not be returned.
@ -263,6 +302,18 @@ func (f *Form) GetFormItemByLabel(label string) FormItem {
return nil return nil
} }
// GetFormItemIndex returns the index of the first form element with the given
// label. If no such element is found, -1 is returned. Buttons are not searched
// and will therefore not be returned.
func (f *Form) GetFormItemIndex(label string) int {
for index, item := range f.items {
if item.GetLabel() == label {
return index
}
}
return -1
}
// SetCancelFunc sets a handler which is called when the user hits the Escape // SetCancelFunc sets a handler which is called when the user hits the Escape
// key. // key.
func (f *Form) SetCancelFunc(callback func()) *Form { func (f *Form) SetCancelFunc(callback func()) *Form {

16
vendor/maunium.net/go/tview/grid.go generated vendored
View File

@ -583,11 +583,11 @@ func (g *Grid) Draw(screen tcell.Screen) {
} }
by := item.y - 1 by := item.y - 1
if by >= 0 && by < height { if by >= 0 && by < height {
PrintJoinedBorder(screen, x+bx, y+by, GraphicsHoriBar, g.bordersColor) PrintJoinedSemigraphics(screen, x+bx, y+by, Borders.Horizontal, g.bordersColor)
} }
by = item.y + item.h by = item.y + item.h
if by >= 0 && by < height { if by >= 0 && by < height {
PrintJoinedBorder(screen, x+bx, y+by, GraphicsHoriBar, g.bordersColor) PrintJoinedSemigraphics(screen, x+bx, y+by, Borders.Horizontal, g.bordersColor)
} }
} }
for by := item.y; by < item.y+item.h; by++ { // Left/right lines. for by := item.y; by < item.y+item.h; by++ { // Left/right lines.
@ -596,28 +596,28 @@ func (g *Grid) Draw(screen tcell.Screen) {
} }
bx := item.x - 1 bx := item.x - 1
if bx >= 0 && bx < width { if bx >= 0 && bx < width {
PrintJoinedBorder(screen, x+bx, y+by, GraphicsVertBar, g.bordersColor) PrintJoinedSemigraphics(screen, x+bx, y+by, Borders.Vertical, g.bordersColor)
} }
bx = item.x + item.w bx = item.x + item.w
if bx >= 0 && bx < width { if bx >= 0 && bx < width {
PrintJoinedBorder(screen, x+bx, y+by, GraphicsVertBar, g.bordersColor) PrintJoinedSemigraphics(screen, x+bx, y+by, Borders.Vertical, g.bordersColor)
} }
} }
bx, by := item.x-1, item.y-1 // Top-left corner. bx, by := item.x-1, item.y-1 // Top-left corner.
if bx >= 0 && bx < width && by >= 0 && by < height { if bx >= 0 && bx < width && by >= 0 && by < height {
PrintJoinedBorder(screen, x+bx, y+by, GraphicsTopLeftCorner, g.bordersColor) PrintJoinedSemigraphics(screen, x+bx, y+by, Borders.TopLeft, g.bordersColor)
} }
bx, by = item.x+item.w, item.y-1 // Top-right corner. bx, by = item.x+item.w, item.y-1 // Top-right corner.
if bx >= 0 && bx < width && by >= 0 && by < height { if bx >= 0 && bx < width && by >= 0 && by < height {
PrintJoinedBorder(screen, x+bx, y+by, GraphicsTopRightCorner, g.bordersColor) PrintJoinedSemigraphics(screen, x+bx, y+by, Borders.TopRight, g.bordersColor)
} }
bx, by = item.x-1, item.y+item.h // Bottom-left corner. bx, by = item.x-1, item.y+item.h // Bottom-left corner.
if bx >= 0 && bx < width && by >= 0 && by < height { if bx >= 0 && bx < width && by >= 0 && by < height {
PrintJoinedBorder(screen, x+bx, y+by, GraphicsBottomLeftCorner, g.bordersColor) PrintJoinedSemigraphics(screen, x+bx, y+by, Borders.BottomLeft, g.bordersColor)
} }
bx, by = item.x+item.w, item.y+item.h // Bottom-right corner. bx, by = item.x+item.w, item.y+item.h // Bottom-right corner.
if bx >= 0 && bx < width && by >= 0 && by < height { if bx >= 0 && bx < width && by >= 0 && by < height {
PrintJoinedBorder(screen, x+bx, y+by, GraphicsBottomRightCorner, g.bordersColor) PrintJoinedSemigraphics(screen, x+bx, y+by, Borders.BottomRight, g.bordersColor)
} }
} }
} }

View File

@ -11,10 +11,23 @@ import (
) )
// InputField is a one-line box (three lines if there is a title) where the // InputField is a one-line box (three lines if there is a title) where the
// user can enter text. // user can enter text. Use SetAcceptanceFunc() to accept or reject input,
// SetChangedFunc() to listen for changes, and SetMaskCharacter() to hide input
// from onlookers (e.g. for password input).
// //
// Use SetMaskCharacter() to hide input from onlookers (e.g. for password // The following keys can be used for navigation and editing:
// input). //
// - Left arrow: Move left by one character.
// - Right arrow: Move right by one character.
// - Home, Ctrl-A, Alt-a: Move to the beginning of the line.
// - End, Ctrl-E, Alt-e: Move to the end of the line.
// - Alt-left, Alt-b: Move left by one word.
// - Alt-right, Alt-f: Move right by one word.
// - Backspace: Delete the character before the cursor.
// - Delete: Delete the character after the cursor.
// - Ctrl-K: Delete from the cursor to the end of the line.
// - Ctrl-W: Delete the last word before the cursor.
// - Ctrl-U: Delete the entire line.
// //
// See https://github.com/rivo/tview/wiki/InputField for an example. // See https://github.com/rivo/tview/wiki/InputField for an example.
type InputField struct { type InputField struct {
@ -53,6 +66,12 @@ type InputField struct {
// disables masking. // disables masking.
maskCharacter rune maskCharacter rune
// The cursor position as a byte index into the text string.
cursorPos int
// The number of bytes of the text string skipped ahead while drawing.
offset int
// An optional function which may reject the last character that was entered. // An optional function which may reject the last character that was entered.
accept func(text string, ch rune) bool accept func(text string, ch rune) bool
@ -83,6 +102,7 @@ func NewInputField() *InputField {
// SetText sets the current text of the input field. // SetText sets the current text of the input field.
func (i *InputField) SetText(text string) *InputField { func (i *InputField) SetText(text string) *InputField {
i.text = text i.text = text
i.cursorPos = len(text)
if i.changed != nil { if i.changed != nil {
i.changed(text) i.changed(text)
} }
@ -174,7 +194,7 @@ func (i *InputField) SetMaskCharacter(mask rune) *InputField {
// SetAcceptanceFunc sets a handler which may reject the last character that was // SetAcceptanceFunc sets a handler which may reject the last character that was
// entered (by returning false). // entered (by returning false).
// //
// This package defines a number of variables Prefixed with InputField which may // This package defines a number of variables prefixed with InputField which may
// be used for common input (e.g. numbers, maximum text length). // be used for common input (e.g. numbers, maximum text length).
func (i *InputField) SetAcceptanceFunc(handler func(textToCheck string, lastChar rune) bool) *InputField { func (i *InputField) SetAcceptanceFunc(handler func(textToCheck string, lastChar rune) bool) *InputField {
i.accept = handler i.accept = handler
@ -244,56 +264,69 @@ func (i *InputField) Draw(screen tcell.Screen) {
screen.SetContent(x+index, y, ' ', nil, fieldStyle) screen.SetContent(x+index, y, ' ', nil, fieldStyle)
} }
// Draw placeholder text. // Text.
var cursorScreenPos int
text := i.text text := i.text
if text == "" && i.placeholder != "" { if text == "" && i.placeholder != "" {
Print(screen, i.placeholder, x, y, fieldWidth, AlignLeft, i.placeholderTextColor) // Draw placeholder text.
Print(screen, Escape(i.placeholder), x, y, fieldWidth, AlignLeft, i.placeholderTextColor)
i.offset = 0
} else { } else {
// Draw entered text. // Draw entered text.
if i.maskCharacter > 0 { if i.maskCharacter > 0 {
text = strings.Repeat(string(i.maskCharacter), utf8.RuneCountInString(i.text)) text = strings.Repeat(string(i.maskCharacter), utf8.RuneCountInString(i.text))
} else {
text = Escape(text)
} }
fieldWidth-- // We need one cell for the cursor. stringWidth := runewidth.StringWidth(text)
if fieldWidth < runewidth.StringWidth(text) { if fieldWidth >= stringWidth {
Print(screen, text, x, y, fieldWidth, AlignRight, i.fieldTextColor) // We have enough space for the full text.
Print(screen, Escape(text), x, y, fieldWidth, AlignLeft, i.fieldTextColor)
i.offset = 0
iterateString(text, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
if textPos >= i.cursorPos {
return true
}
cursorScreenPos += screenWidth
return false
})
} else { } else {
Print(screen, text, x, y, fieldWidth, AlignLeft, i.fieldTextColor) // The text doesn't fit. Where is the cursor?
if i.cursorPos < 0 {
i.cursorPos = 0
} else if i.cursorPos > len(text) {
i.cursorPos = len(text)
}
// Shift the text so the cursor is inside the field.
var shiftLeft int
if i.offset > i.cursorPos {
i.offset = i.cursorPos
} else if subWidth := runewidth.StringWidth(text[i.offset:i.cursorPos]); subWidth > fieldWidth-1 {
shiftLeft = subWidth - fieldWidth + 1
}
currentOffset := i.offset
iterateString(text, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
if textPos >= currentOffset {
if shiftLeft > 0 {
i.offset = textPos + textWidth
shiftLeft -= screenWidth
} else {
if textPos+textWidth > i.cursorPos {
return true
}
cursorScreenPos += screenWidth
}
}
return false
})
Print(screen, Escape(text[i.offset:]), x, y, fieldWidth, AlignLeft, i.fieldTextColor)
} }
} }
// Set cursor. // Set cursor.
if i.focus.HasFocus() { if i.focus.HasFocus() {
i.setCursor(screen) screen.ShowCursor(x+cursorScreenPos, y)
} }
} }
// setCursor sets the cursor position.
func (i *InputField) setCursor(screen tcell.Screen) {
x := i.x
y := i.y
rightLimit := x + i.width
if i.border {
x++
y++
rightLimit -= 2
}
fieldWidth := runewidth.StringWidth(i.text)
if i.fieldWidth > 0 && fieldWidth > i.fieldWidth-1 {
fieldWidth = i.fieldWidth - 1
}
if i.labelWidth > 0 {
x += i.labelWidth + fieldWidth
} else {
x += StringWidth(i.label) + fieldWidth
}
if x >= rightLimit {
x = rightLimit - 1
}
screen.ShowCursor(x, y)
}
// InputHandler returns the handler for this primitive. // InputHandler returns the handler for this primitive.
func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) { func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return i.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) { return i.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
@ -305,27 +338,101 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p
} }
}() }()
// Movement functions.
home := func() { i.cursorPos = 0 }
end := func() { i.cursorPos = len(i.text) }
moveLeft := func() {
iterateStringReverse(i.text[:i.cursorPos], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
i.cursorPos -= textWidth
return true
})
}
moveRight := func() {
iterateString(i.text[i.cursorPos:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
i.cursorPos += textWidth
return true
})
}
moveWordLeft := func() {
i.cursorPos = len(regexp.MustCompile(`\S+\s*$`).ReplaceAllString(i.text[:i.cursorPos], ""))
}
moveWordRight := func() {
i.cursorPos = len(i.text) - len(regexp.MustCompile(`^\s*\S+\s*`).ReplaceAllString(i.text[i.cursorPos:], ""))
}
// Add character function. Returns whether or not the rune character is
// accepted.
add := func(r rune) bool {
newText := i.text[:i.cursorPos] + string(r) + i.text[i.cursorPos:]
if i.accept != nil {
return i.accept(newText, r)
}
i.text = newText
i.cursorPos += len(string(r))
return true
}
// Process key event. // Process key event.
switch key := event.Key(); key { switch key := event.Key(); key {
case tcell.KeyRune: // Regular character. case tcell.KeyRune: // Regular character.
newText := i.text + string(event.Rune()) if event.Modifiers()&tcell.ModAlt > 0 {
if i.accept != nil { // We accept some Alt- key combinations.
if !i.accept(newText, event.Rune()) { switch event.Rune() {
case 'a': // Home.
home()
case 'e': // End.
end()
case 'b': // Move word left.
moveWordLeft()
case 'f': // Move word right.
moveWordRight()
}
} else {
// Other keys are simply accepted as regular characters.
if !add(event.Rune()) {
break break
} }
} }
i.text = newText
case tcell.KeyCtrlU: // Delete all. case tcell.KeyCtrlU: // Delete all.
i.text = "" i.text = ""
i.cursorPos = 0
case tcell.KeyCtrlK: // Delete until the end of the line.
i.text = i.text[:i.cursorPos]
case tcell.KeyCtrlW: // Delete last word. case tcell.KeyCtrlW: // Delete last word.
lastWord := regexp.MustCompile(`\s*\S+\s*$`) lastWord := regexp.MustCompile(`\S+\s*$`)
i.text = lastWord.ReplaceAllString(i.text, "") newText := lastWord.ReplaceAllString(i.text[:i.cursorPos], "") + i.text[i.cursorPos:]
case tcell.KeyBackspace, tcell.KeyBackspace2: // Delete last character. i.cursorPos -= len(i.text) - len(newText)
if len(i.text) == 0 { i.text = newText
break case tcell.KeyBackspace, tcell.KeyBackspace2: // Delete character before the cursor.
iterateStringReverse(i.text[:i.cursorPos], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
i.text = i.text[:textPos] + i.text[textPos+textWidth:]
i.cursorPos -= textWidth
return true
})
if i.offset >= i.cursorPos {
i.offset = 0
} }
runes := []rune(i.text) case tcell.KeyDelete: // Delete character after the cursor.
i.text = string(runes[:len(runes)-1]) iterateString(i.text[i.cursorPos:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
i.text = i.text[:i.cursorPos] + i.text[i.cursorPos+textWidth:]
return true
})
case tcell.KeyLeft:
if event.Modifiers()&tcell.ModAlt > 0 {
moveWordLeft()
} else {
moveLeft()
}
case tcell.KeyRight:
if event.Modifiers()&tcell.ModAlt > 0 {
moveWordRight()
} else {
moveRight()
}
case tcell.KeyHome, tcell.KeyCtrlA:
home()
case tcell.KeyEnd, tcell.KeyCtrlE:
end()
case tcell.KeyEnter, tcell.KeyTab, tcell.KeyBacktab, tcell.KeyEscape: // We're done. case tcell.KeyEnter, tcell.KeyTab, tcell.KeyBacktab, tcell.KeyEscape: // We're done.
if i.done != nil { if i.done != nil {
i.done(key) i.done(key)

15
vendor/maunium.net/go/tview/list.go generated vendored
View File

@ -85,6 +85,19 @@ func (l *List) GetCurrentItem() int {
return l.currentItem return l.currentItem
} }
// RemoveItem removes the item with the given index (starting at 0) from the
// list. Does nothing if the index is out of range.
func (l *List) RemoveItem(index int) *List {
if index < 0 || index >= len(l.items) {
return l
}
l.items = append(l.items[:index], l.items[index+1:]...)
if l.currentItem >= len(l.items) {
l.currentItem = len(l.items) - 1
}
return l
}
// SetMainTextColor sets the color of the items' main text. // SetMainTextColor sets the color of the items' main text.
func (l *List) SetMainTextColor(color tcell.Color) *List { func (l *List) SetMainTextColor(color tcell.Color) *List {
l.mainTextColor = color l.mainTextColor = color
@ -127,7 +140,7 @@ func (l *List) ShowSecondaryText(show bool) *List {
// //
// This function is also called when the first item is added or when // This function is also called when the first item is added or when
// SetCurrentItem() is called. // SetCurrentItem() is called.
func (l *List) SetChangedFunc(handler func(int, string, string, rune)) *List { func (l *List) SetChangedFunc(handler func(index int, mainText string, secondaryText string, shortcut rune)) *List {
l.changed = handler l.changed = handler
return l return l
} }

15
vendor/maunium.net/go/tview/modal.go generated vendored
View File

@ -40,6 +40,11 @@ func NewModal() *Modal {
SetButtonBackgroundColor(Styles.PrimitiveBackgroundColor). SetButtonBackgroundColor(Styles.PrimitiveBackgroundColor).
SetButtonTextColor(Styles.PrimaryTextColor) SetButtonTextColor(Styles.PrimaryTextColor)
m.form.SetBackgroundColor(Styles.ContrastBackgroundColor).SetBorderPadding(0, 0, 0, 0) m.form.SetBackgroundColor(Styles.ContrastBackgroundColor).SetBorderPadding(0, 0, 0, 0)
m.form.SetCancelFunc(func() {
if m.done != nil {
m.done(-1, "")
}
})
m.frame = NewFrame(m.form).SetBorders(0, 0, 1, 0, 0, 0) m.frame = NewFrame(m.form).SetBorders(0, 0, 1, 0, 0, 0)
m.frame.SetBorder(true). m.frame.SetBorder(true).
SetBackgroundColor(Styles.ContrastBackgroundColor). SetBackgroundColor(Styles.ContrastBackgroundColor).
@ -81,6 +86,16 @@ func (m *Modal) AddButtons(labels []string) *Modal {
m.done(i, l) m.done(i, l)
} }
}) })
button := m.form.GetButton(m.form.GetButtonCount() - 1)
button.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
switch event.Key() {
case tcell.KeyDown, tcell.KeyRight:
return tcell.NewEventKey(tcell.KeyTab, 0, tcell.ModNone)
case tcell.KeyUp, tcell.KeyLeft:
return tcell.NewEventKey(tcell.KeyBacktab, 0, tcell.ModNone)
}
return event
})
}(index, label) }(index, label)
} }
return m return m

296
vendor/maunium.net/go/tview/semigraphics.go generated vendored Normal file
View File

@ -0,0 +1,296 @@
package tview
import "maunium.net/go/tcell"
// Semigraphics provides an easy way to access unicode characters for drawing.
//
// Named like the unicode characters, 'Semigraphics'-prefix used if unicode block
// isn't prefixed itself.
const (
// Block: General Punctation U+2000-U+206F (http://unicode.org/charts/PDF/U2000.pdf)
SemigraphicsHorizontalEllipsis rune = '\u2026' // …
// Block: Box Drawing U+2500-U+257F (http://unicode.org/charts/PDF/U2500.pdf)
BoxDrawingsLightHorizontal rune = '\u2500' // ─
BoxDrawingsHeavyHorizontal rune = '\u2501' // ━
BoxDrawingsLightVertical rune = '\u2502' // │
BoxDrawingsHeavyVertical rune = '\u2503' // ┃
BoxDrawingsLightTripleDashHorizontal rune = '\u2504' // ┄
BoxDrawingsHeavyTripleDashHorizontal rune = '\u2505' // ┅
BoxDrawingsLightTripleDashVertical rune = '\u2506' // ┆
BoxDrawingsHeavyTripleDashVertical rune = '\u2507' // ┇
BoxDrawingsLightQuadrupleDashHorizontal rune = '\u2508' // ┈
BoxDrawingsHeavyQuadrupleDashHorizontal rune = '\u2509' // ┉
BoxDrawingsLightQuadrupleDashVertical rune = '\u250a' // ┊
BoxDrawingsHeavyQuadrupleDashVertical rune = '\u250b' // ┋
BoxDrawingsLightDownAndRight rune = '\u250c' // ┌
BoxDrawingsDownLighAndRightHeavy rune = '\u250d' // ┍
BoxDrawingsDownHeavyAndRightLight rune = '\u250e' // ┎
BoxDrawingsHeavyDownAndRight rune = '\u250f' // ┏
BoxDrawingsLightDownAndLeft rune = '\u2510' // ┐
BoxDrawingsDownLighAndLeftHeavy rune = '\u2511' // ┑
BoxDrawingsDownHeavyAndLeftLight rune = '\u2512' // ┒
BoxDrawingsHeavyDownAndLeft rune = '\u2513' // ┓
BoxDrawingsLightUpAndRight rune = '\u2514' // └
BoxDrawingsUpLightAndRightHeavy rune = '\u2515' // ┕
BoxDrawingsUpHeavyAndRightLight rune = '\u2516' // ┖
BoxDrawingsHeavyUpAndRight rune = '\u2517' // ┗
BoxDrawingsLightUpAndLeft rune = '\u2518' // ┘
BoxDrawingsUpLightAndLeftHeavy rune = '\u2519' // ┙
BoxDrawingsUpHeavyAndLeftLight rune = '\u251a' // ┚
BoxDrawingsHeavyUpAndLeft rune = '\u251b' // ┛
BoxDrawingsLightVerticalAndRight rune = '\u251c' // ├
BoxDrawingsVerticalLightAndRightHeavy rune = '\u251d' // ┝
BoxDrawingsUpHeavyAndRightDownLight rune = '\u251e' // ┞
BoxDrawingsDownHeacyAndRightUpLight rune = '\u251f' // ┟
BoxDrawingsVerticalHeavyAndRightLight rune = '\u2520' // ┠
BoxDrawingsDownLightAnbdRightUpHeavy rune = '\u2521' // ┡
BoxDrawingsUpLightAndRightDownHeavy rune = '\u2522' // ┢
BoxDrawingsHeavyVerticalAndRight rune = '\u2523' // ┣
BoxDrawingsLightVerticalAndLeft rune = '\u2524' // ┤
BoxDrawingsVerticalLightAndLeftHeavy rune = '\u2525' // ┥
BoxDrawingsUpHeavyAndLeftDownLight rune = '\u2526' // ┦
BoxDrawingsDownHeavyAndLeftUpLight rune = '\u2527' // ┧
BoxDrawingsVerticalheavyAndLeftLight rune = '\u2528' // ┨
BoxDrawingsDownLightAndLeftUpHeavy rune = '\u2529' // ┨
BoxDrawingsUpLightAndLeftDownHeavy rune = '\u252a' // ┪
BoxDrawingsHeavyVerticalAndLeft rune = '\u252b' // ┫
BoxDrawingsLightDownAndHorizontal rune = '\u252c' // ┬
BoxDrawingsLeftHeavyAndRightDownLight rune = '\u252d' // ┭
BoxDrawingsRightHeavyAndLeftDownLight rune = '\u252e' // ┮
BoxDrawingsDownLightAndHorizontalHeavy rune = '\u252f' // ┯
BoxDrawingsDownHeavyAndHorizontalLight rune = '\u2530' // ┰
BoxDrawingsRightLightAndLeftDownHeavy rune = '\u2531' // ┱
BoxDrawingsLeftLightAndRightDownHeavy rune = '\u2532' // ┲
BoxDrawingsHeavyDownAndHorizontal rune = '\u2533' // ┳
BoxDrawingsLightUpAndHorizontal rune = '\u2534' // ┴
BoxDrawingsLeftHeavyAndRightUpLight rune = '\u2535' // ┵
BoxDrawingsRightHeavyAndLeftUpLight rune = '\u2536' // ┶
BoxDrawingsUpLightAndHorizontalHeavy rune = '\u2537' // ┷
BoxDrawingsUpHeavyAndHorizontalLight rune = '\u2538' // ┸
BoxDrawingsRightLightAndLeftUpHeavy rune = '\u2539' // ┹
BoxDrawingsLeftLightAndRightUpHeavy rune = '\u253a' // ┺
BoxDrawingsHeavyUpAndHorizontal rune = '\u253b' // ┻
BoxDrawingsLightVerticalAndHorizontal rune = '\u253c' // ┼
BoxDrawingsLeftHeavyAndRightVerticalLight rune = '\u253d' // ┽
BoxDrawingsRightHeavyAndLeftVerticalLight rune = '\u253e' // ┾
BoxDrawingsVerticalLightAndHorizontalHeavy rune = '\u253f' // ┿
BoxDrawingsUpHeavyAndDownHorizontalLight rune = '\u2540' // ╀
BoxDrawingsDownHeavyAndUpHorizontalLight rune = '\u2541' // ╁
BoxDrawingsVerticalHeavyAndHorizontalLight rune = '\u2542' // ╂
BoxDrawingsLeftUpHeavyAndRightDownLight rune = '\u2543' // ╃
BoxDrawingsRightUpHeavyAndLeftDownLight rune = '\u2544' // ╄
BoxDrawingsLeftDownHeavyAndRightUpLight rune = '\u2545' // ╅
BoxDrawingsRightDownHeavyAndLeftUpLight rune = '\u2546' // ╆
BoxDrawingsDownLightAndUpHorizontalHeavy rune = '\u2547' // ╇
BoxDrawingsUpLightAndDownHorizontalHeavy rune = '\u2548' // ╈
BoxDrawingsRightLightAndLeftVerticalHeavy rune = '\u2549' // ╉
BoxDrawingsLeftLightAndRightVerticalHeavy rune = '\u254a' // ╊
BoxDrawingsHeavyVerticalAndHorizontal rune = '\u254b' // ╋
BoxDrawingsLightDoubleDashHorizontal rune = '\u254c' // ╌
BoxDrawingsHeavyDoubleDashHorizontal rune = '\u254d' // ╍
BoxDrawingsLightDoubleDashVertical rune = '\u254e' // ╎
BoxDrawingsHeavyDoubleDashVertical rune = '\u254f' // ╏
BoxDrawingsDoubleHorizontal rune = '\u2550' // ═
BoxDrawingsDoubleVertical rune = '\u2551' // ║
BoxDrawingsDownSingleAndRightDouble rune = '\u2552' // ╒
BoxDrawingsDownDoubleAndRightSingle rune = '\u2553' // ╓
BoxDrawingsDoubleDownAndRight rune = '\u2554' // ╔
BoxDrawingsDownSingleAndLeftDouble rune = '\u2555' // ╕
BoxDrawingsDownDoubleAndLeftSingle rune = '\u2556' // ╖
BoxDrawingsDoubleDownAndLeft rune = '\u2557' // ╗
BoxDrawingsUpSingleAndRightDouble rune = '\u2558' // ╘
BoxDrawingsUpDoubleAndRightSingle rune = '\u2559' // ╙
BoxDrawingsDoubleUpAndRight rune = '\u255a' // ╚
BoxDrawingsUpSingleAndLeftDouble rune = '\u255b' // ╛
BoxDrawingsUpDobuleAndLeftSingle rune = '\u255c' // ╜
BoxDrawingsDoubleUpAndLeft rune = '\u255d' // ╝
BoxDrawingsVerticalSingleAndRightDouble rune = '\u255e' // ╞
BoxDrawingsVerticalDoubleAndRightSingle rune = '\u255f' // ╟
BoxDrawingsDoubleVerticalAndRight rune = '\u2560' // ╠
BoxDrawingsVerticalSingleAndLeftDouble rune = '\u2561' // ╡
BoxDrawingsVerticalDoubleAndLeftSingle rune = '\u2562' // ╢
BoxDrawingsDoubleVerticalAndLeft rune = '\u2563' // ╣
BoxDrawingsDownSingleAndHorizontalDouble rune = '\u2564' // ╤
BoxDrawingsDownDoubleAndHorizontalSingle rune = '\u2565' // ╥
BoxDrawingsDoubleDownAndHorizontal rune = '\u2566' // ╦
BoxDrawingsUpSingleAndHorizontalDouble rune = '\u2567' // ╧
BoxDrawingsUpDoubleAndHorizontalSingle rune = '\u2568' // ╨
BoxDrawingsDoubleUpAndHorizontal rune = '\u2569' // ╩
BoxDrawingsVerticalSingleAndHorizontalDouble rune = '\u256a' // ╪
BoxDrawingsVerticalDoubleAndHorizontalSingle rune = '\u256b' // ╫
BoxDrawingsDoubleVerticalAndHorizontal rune = '\u256c' // ╬
BoxDrawingsLightArcDownAndRight rune = '\u256d' // ╭
BoxDrawingsLightArcDownAndLeft rune = '\u256e' // ╮
BoxDrawingsLightArcUpAndLeft rune = '\u256f' // ╯
BoxDrawingsLightArcUpAndRight rune = '\u2570' // ╰
BoxDrawingsLightDiagonalUpperRightToLowerLeft rune = '\u2571' //
BoxDrawingsLightDiagonalUpperLeftToLowerRight rune = '\u2572' // ╲
BoxDrawingsLightDiagonalCross rune = '\u2573' //
BoxDrawingsLightLeft rune = '\u2574' // ╴
BoxDrawingsLightUp rune = '\u2575' // ╵
BoxDrawingsLightRight rune = '\u2576' // ╶
BoxDrawingsLightDown rune = '\u2577' // ╷
BoxDrawingsHeavyLeft rune = '\u2578' // ╸
BoxDrawingsHeavyUp rune = '\u2579' // ╹
BoxDrawingsHeavyRight rune = '\u257a' // ╺
BoxDrawingsHeavyDown rune = '\u257b' // ╻
BoxDrawingsLightLeftAndHeavyRight rune = '\u257c' // ╼
BoxDrawingsLightUpAndHeavyDown rune = '\u257d' // ╽
BoxDrawingsHeavyLeftAndLightRight rune = '\u257e' // ╾
BoxDrawingsHeavyUpAndLightDown rune = '\u257f' // ╿
)
// SemigraphicJoints is a map for joining semigraphic (or otherwise) runes.
// So far only light lines are supported but if you want to change the border
// styling you need to provide the joints, too.
// The matching will be sorted ascending by rune value, so you don't need to
// provide all rune combinations,
// e.g. (─) + (│) = (┼) will also match (│) + (─) = (┼)
var SemigraphicJoints = map[string]rune{
// (─) + (│) = (┼)
string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightVertical}): BoxDrawingsLightVerticalAndHorizontal,
// (─) + (┌) = (┬)
string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightDownAndRight}): BoxDrawingsLightDownAndHorizontal,
// (─) + (┐) = (┬)
string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightDownAndLeft}): BoxDrawingsLightDownAndHorizontal,
// (─) + (└) = (┴)
string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightUpAndRight}): BoxDrawingsLightUpAndHorizontal,
// (─) + (┘) = (┴)
string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightUpAndLeft}): BoxDrawingsLightUpAndHorizontal,
// (─) + (├) = (┼)
string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightVerticalAndRight}): BoxDrawingsLightVerticalAndHorizontal,
// (─) + (┤) = (┼)
string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndHorizontal,
// (─) + (┬) = (┬)
string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightDownAndHorizontal,
// (─) + (┴) = (┴)
string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightUpAndHorizontal,
// (─) + (┼) = (┼)
string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
// (│) + (┌) = (├)
string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightDownAndRight}): BoxDrawingsLightVerticalAndRight,
// (│) + (┐) = (┤)
string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightDownAndLeft}): BoxDrawingsLightVerticalAndLeft,
// (│) + (└) = (├)
string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightUpAndRight}): BoxDrawingsLightVerticalAndRight,
// (│) + (┘) = (┤)
string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightUpAndLeft}): BoxDrawingsLightVerticalAndLeft,
// (│) + (├) = (├)
string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightVerticalAndRight}): BoxDrawingsLightVerticalAndRight,
// (│) + (┤) = (┤)
string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndLeft,
// (│) + (┬) = (┼)
string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
// (│) + (┴) = (┼)
string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
// (│) + (┼) = (┼)
string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
// (┌) + (┐) = (┬)
string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightDownAndLeft}): BoxDrawingsLightDownAndHorizontal,
// (┌) + (└) = (├)
string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightUpAndRight}): BoxDrawingsLightVerticalAndRight,
// (┌) + (┘) = (┼)
string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightUpAndLeft}): BoxDrawingsLightVerticalAndHorizontal,
// (┌) + (├) = (├)
string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightVerticalAndRight}): BoxDrawingsLightVerticalAndRight,
// (┌) + (┤) = (┼)
string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndHorizontal,
// (┌) + (┬) = (┬)
string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightDownAndHorizontal,
// (┌) + (┴) = (┼)
string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
// (┌) + (┴) = (┼)
string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
// (┐) + (└) = (┼)
string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightUpAndRight}): BoxDrawingsLightVerticalAndHorizontal,
// (┐) + (┘) = (┤)
string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightUpAndLeft}): BoxDrawingsLightVerticalAndLeft,
// (┐) + (├) = (┼)
string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightVerticalAndRight}): BoxDrawingsLightVerticalAndHorizontal,
// (┐) + (┤) = (┤)
string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndLeft,
// (┐) + (┬) = (┬)
string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightDownAndHorizontal,
// (┐) + (┴) = (┼)
string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
// (┐) + (┼) = (┼)
string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
// (└) + (┘) = (┴)
string([]rune{BoxDrawingsLightUpAndRight, BoxDrawingsLightUpAndLeft}): BoxDrawingsLightUpAndHorizontal,
// (└) + (├) = (├)
string([]rune{BoxDrawingsLightUpAndRight, BoxDrawingsLightVerticalAndRight}): BoxDrawingsLightVerticalAndRight,
// (└) + (┤) = (┼)
string([]rune{BoxDrawingsLightUpAndRight, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndHorizontal,
// (└) + (┬) = (┼)
string([]rune{BoxDrawingsLightUpAndRight, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
// (└) + (┴) = (┴)
string([]rune{BoxDrawingsLightUpAndRight, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightUpAndHorizontal,
// (└) + (┼) = (┼)
string([]rune{BoxDrawingsLightUpAndRight, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
// (┘) + (├) = (┼)
string([]rune{BoxDrawingsLightUpAndLeft, BoxDrawingsLightVerticalAndRight}): BoxDrawingsLightVerticalAndHorizontal,
// (┘) + (┤) = (┤)
string([]rune{BoxDrawingsLightUpAndLeft, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndLeft,
// (┘) + (┬) = (┼)
string([]rune{BoxDrawingsLightUpAndLeft, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
// (┘) + (┴) = (┴)
string([]rune{BoxDrawingsLightUpAndLeft, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightUpAndHorizontal,
// (┘) + (┼) = (┼)
string([]rune{BoxDrawingsLightUpAndLeft, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
// (├) + (┤) = (┼)
string([]rune{BoxDrawingsLightVerticalAndRight, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndHorizontal,
// (├) + (┬) = (┼)
string([]rune{BoxDrawingsLightVerticalAndRight, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
// (├) + (┴) = (┼)
string([]rune{BoxDrawingsLightVerticalAndRight, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
// (├) + (┼) = (┼)
string([]rune{BoxDrawingsLightVerticalAndRight, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
// (┤) + (┬) = (┼)
string([]rune{BoxDrawingsLightVerticalAndLeft, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
// (┤) + (┴) = (┼)
string([]rune{BoxDrawingsLightVerticalAndLeft, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
// (┤) + (┼) = (┼)
string([]rune{BoxDrawingsLightVerticalAndLeft, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
// (┬) + (┴) = (┼)
string([]rune{BoxDrawingsLightDownAndHorizontal, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
// (┬) + (┼) = (┼)
string([]rune{BoxDrawingsLightDownAndHorizontal, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
// (┴) + (┼) = (┼)
string([]rune{BoxDrawingsLightUpAndHorizontal, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
}
// PrintJoinedSemigraphics prints a semigraphics rune into the screen at the given
// position with the given color, joining it with any existing semigraphics
// rune. Background colors are preserved. At this point, only regular single
// line borders are supported.
func PrintJoinedSemigraphics(screen tcell.Screen, x, y int, ch rune, color tcell.Color) {
previous, _, style, _ := screen.GetContent(x, y)
style = style.Foreground(color)
// What's the resulting rune?
var result rune
if ch == previous {
result = ch
} else {
if ch < previous {
previous, ch = ch, previous
}
result = SemigraphicJoints[string([]rune{previous, ch})]
}
if result == 0 {
result = ch
}
// We only print something if we have something.
screen.SetContent(x, y, result, nil, style)
}

119
vendor/maunium.net/go/tview/table.go generated vendored
View File

@ -231,6 +231,10 @@ type Table struct {
// The number of visible rows the last time the table was drawn. // The number of visible rows the last time the table was drawn.
visibleRows int visibleRows int
// The style of the selected rows. If this value is 0, selected rows are
// simply inverted.
selectedStyle tcell.Style
// An optional function which gets called when the user presses Enter on a // An optional function which gets called when the user presses Enter on a
// selected cell. If entire rows selected, the column value is undefined. // selected cell. If entire rows selected, the column value is undefined.
// Likewise for entire columns. // Likewise for entire columns.
@ -276,9 +280,21 @@ func (t *Table) SetBordersColor(color tcell.Color) *Table {
return t return t
} }
// SetSelectedStyle sets a specific style for selected cells. If no such style
// is set, per default, selected cells are inverted (i.e. their foreground and
// background colors are swapped).
//
// To reset a previous setting to its default, make the following call:
//
// table.SetSelectedStyle(tcell.ColorDefault, tcell.ColorDefault, 0)
func (t *Table) SetSelectedStyle(foregroundColor, backgroundColor tcell.Color, attributes tcell.AttrMask) *Table {
t.selectedStyle = tcell.StyleDefault.Foreground(foregroundColor).Background(backgroundColor) | tcell.Style(attributes)
return t
}
// SetSeparator sets the character used to fill the space between two // SetSeparator sets the character used to fill the space between two
// neighboring cells. This is a space character ' ' per default but you may // neighboring cells. This is a space character ' ' per default but you may
// want to set it to GraphicsVertBar (or any other rune) if the column // want to set it to Borders.Vertical (or any other rune) if the column
// separation should be more visible. If cell borders are activated, this is // separation should be more visible. If cell borders are activated, this is
// ignored. // ignored.
// //
@ -373,7 +389,7 @@ func (t *Table) SetDoneFunc(handler func(key tcell.Key)) *Table {
} }
// SetCell sets the content of a cell the specified position. It is ok to // SetCell sets the content of a cell the specified position. It is ok to
// directly instantiate a TableCell object. If the cell has contain, at least // directly instantiate a TableCell object. If the cell has content, at least
// the Text and Color fields should be set. // the Text and Color fields should be set.
// //
// Note that setting cells in previously unknown rows and columns will // Note that setting cells in previously unknown rows and columns will
@ -406,7 +422,7 @@ func (t *Table) SetCellSimple(row, column int, text string) *Table {
} }
// GetCell returns the contents of the cell at the specified position. A valid // GetCell returns the contents of the cell at the specified position. A valid
// TableCell object is always returns but it will be uninitialized if the cell // TableCell object is always returned but it will be uninitialized if the cell
// was not previously set. // was not previously set.
func (t *Table) GetCell(row, column int) *TableCell { func (t *Table) GetCell(row, column int) *TableCell {
if row >= len(t.cells) || column >= len(t.cells[row]) { if row >= len(t.cells) || column >= len(t.cells[row]) {
@ -415,6 +431,31 @@ func (t *Table) GetCell(row, column int) *TableCell {
return t.cells[row][column] return t.cells[row][column]
} }
// RemoveRow removes the row at the given position from the table. If there is
// no such row, this has no effect.
func (t *Table) RemoveRow(row int) *Table {
if row < 0 || row >= len(t.cells) {
return t
}
t.cells = append(t.cells[:row], t.cells[row+1:]...)
return t
}
// RemoveColumn removes the column at the given position from the table. If
// there is no such column, this has no effect.
func (t *Table) RemoveColumn(column int) *Table {
for row := range t.cells {
if column < 0 || column >= len(t.cells[row]) {
continue
}
t.cells[row] = append(t.cells[row][:column], t.cells[row][column+1:]...)
}
return t
}
// GetRowCount returns the number of rows in the table. // GetRowCount returns the number of rows in the table.
func (t *Table) GetRowCount() int { func (t *Table) GetRowCount() int {
return len(t.cells) return len(t.cells)
@ -644,7 +685,6 @@ ColumnLoop:
} }
expWidth := toDistribute * expansion / expansionTotal expWidth := toDistribute * expansion / expansionTotal
widths[index] += expWidth widths[index] += expWidth
tableWidth += expWidth
toDistribute -= expWidth toDistribute -= expWidth
expansionTotal -= expansion expansionTotal -= expansion
} }
@ -668,24 +708,24 @@ ColumnLoop:
// Draw borders. // Draw borders.
rowY *= 2 rowY *= 2
for pos := 0; pos < columnWidth && columnX+1+pos < width; pos++ { for pos := 0; pos < columnWidth && columnX+1+pos < width; pos++ {
drawBorder(columnX+pos+1, rowY, GraphicsHoriBar) drawBorder(columnX+pos+1, rowY, Borders.Horizontal)
} }
ch := GraphicsCross ch := Borders.Cross
if columnIndex == 0 { if columnIndex == 0 {
if rowY == 0 { if rowY == 0 {
ch = GraphicsTopLeftCorner ch = Borders.TopLeft
} else { } else {
ch = GraphicsLeftT ch = Borders.LeftT
} }
} else if rowY == 0 { } else if rowY == 0 {
ch = GraphicsTopT ch = Borders.TopT
} }
drawBorder(columnX, rowY, ch) drawBorder(columnX, rowY, ch)
rowY++ rowY++
if rowY >= height { if rowY >= height {
break // No space for the text anymore. break // No space for the text anymore.
} }
drawBorder(columnX, rowY, GraphicsVertBar) drawBorder(columnX, rowY, Borders.Vertical)
} else if columnIndex > 0 { } else if columnIndex > 0 {
// Draw separator. // Draw separator.
drawBorder(columnX, rowY, t.separator) drawBorder(columnX, rowY, t.separator)
@ -706,18 +746,18 @@ ColumnLoop:
_, printed := printWithStyle(screen, cell.Text, x+columnX+1, y+rowY, finalWidth, cell.Align, tcell.StyleDefault.Foreground(cell.Color)|tcell.Style(cell.Attributes)) _, printed := printWithStyle(screen, cell.Text, x+columnX+1, y+rowY, finalWidth, cell.Align, tcell.StyleDefault.Foreground(cell.Color)|tcell.Style(cell.Attributes))
if StringWidth(cell.Text)-printed > 0 && printed > 0 { if StringWidth(cell.Text)-printed > 0 && printed > 0 {
_, _, style, _ := screen.GetContent(x+columnX+1+finalWidth-1, y+rowY) _, _, style, _ := screen.GetContent(x+columnX+1+finalWidth-1, y+rowY)
printWithStyle(screen, string(GraphicsEllipsis), x+columnX+1+finalWidth-1, y+rowY, 1, AlignLeft, style) printWithStyle(screen, string(SemigraphicsHorizontalEllipsis), x+columnX+1+finalWidth-1, y+rowY, 1, AlignLeft, style)
} }
} }
// Draw bottom border. // Draw bottom border.
if rowY := 2 * len(rows); t.borders && rowY < height { if rowY := 2 * len(rows); t.borders && rowY < height {
for pos := 0; pos < columnWidth && columnX+1+pos < width; pos++ { for pos := 0; pos < columnWidth && columnX+1+pos < width; pos++ {
drawBorder(columnX+pos+1, rowY, GraphicsHoriBar) drawBorder(columnX+pos+1, rowY, Borders.Horizontal)
} }
ch := GraphicsBottomT ch := Borders.BottomT
if columnIndex == 0 { if columnIndex == 0 {
ch = GraphicsBottomLeftCorner ch = Borders.BottomLeft
} }
drawBorder(columnX, rowY, ch) drawBorder(columnX, rowY, ch)
} }
@ -730,26 +770,31 @@ ColumnLoop:
for rowY := range rows { for rowY := range rows {
rowY *= 2 rowY *= 2
if rowY+1 < height { if rowY+1 < height {
drawBorder(columnX, rowY+1, GraphicsVertBar) drawBorder(columnX, rowY+1, Borders.Vertical)
} }
ch := GraphicsRightT ch := Borders.RightT
if rowY == 0 { if rowY == 0 {
ch = GraphicsTopRightCorner ch = Borders.TopRight
} }
drawBorder(columnX, rowY, ch) drawBorder(columnX, rowY, ch)
} }
if rowY := 2 * len(rows); rowY < height { if rowY := 2 * len(rows); rowY < height {
drawBorder(columnX, rowY, GraphicsBottomRightCorner) drawBorder(columnX, rowY, Borders.BottomRight)
} }
} }
// Helper function which colors the background of a box. // Helper function which colors the background of a box.
colorBackground := func(fromX, fromY, w, h int, backgroundColor, textColor tcell.Color, selected bool) { // backgroundColor == tcell.ColorDefault => Don't color the background.
// textColor == tcell.ColorDefault => Don't change the text color.
// attr == 0 => Don't change attributes.
// invert == true => Ignore attr, set text to backgroundColor or t.backgroundColor;
// set background to textColor.
colorBackground := func(fromX, fromY, w, h int, backgroundColor, textColor tcell.Color, attr tcell.AttrMask, invert bool) {
for by := 0; by < h && fromY+by < y+height; by++ { for by := 0; by < h && fromY+by < y+height; by++ {
for bx := 0; bx < w && fromX+bx < x+width; bx++ { for bx := 0; bx < w && fromX+bx < x+width; bx++ {
m, c, style, _ := screen.GetContent(fromX+bx, fromY+by) m, c, style, _ := screen.GetContent(fromX+bx, fromY+by)
if selected { fg, bg, a := style.Decompose()
fg, _, _ := style.Decompose() if invert {
if fg == textColor || fg == t.bordersColor { if fg == textColor || fg == t.bordersColor {
fg = backgroundColor fg = backgroundColor
} }
@ -758,10 +803,16 @@ ColumnLoop:
} }
style = style.Background(textColor).Foreground(fg) style = style.Background(textColor).Foreground(fg)
} else { } else {
if backgroundColor == tcell.ColorDefault { if backgroundColor != tcell.ColorDefault {
continue bg = backgroundColor
} }
style = style.Background(backgroundColor) if textColor != tcell.ColorDefault {
fg = textColor
}
if attr != 0 {
a = attr
}
style = style.Background(bg).Foreground(fg) | tcell.Style(a)
} }
screen.SetContent(fromX+bx, fromY+by, m, c, style) screen.SetContent(fromX+bx, fromY+by, m, c, style)
} }
@ -770,11 +821,12 @@ ColumnLoop:
// Color the cell backgrounds. To avoid undesirable artefacts, we combine // Color the cell backgrounds. To avoid undesirable artefacts, we combine
// the drawing of a cell by background color, selected cells last. // the drawing of a cell by background color, selected cells last.
cellsByBackgroundColor := make(map[tcell.Color][]*struct { type cellInfo struct {
x, y, w, h int x, y, w, h int
text tcell.Color text tcell.Color
selected bool selected bool
}) }
cellsByBackgroundColor := make(map[tcell.Color][]*cellInfo)
var backgroundColors []tcell.Color var backgroundColors []tcell.Color
for rowY, row := range rows { for rowY, row := range rows {
columnX := 0 columnX := 0
@ -794,11 +846,7 @@ ColumnLoop:
columnSelected := t.columnsSelectable && !t.rowsSelectable && column == t.selectedColumn columnSelected := t.columnsSelectable && !t.rowsSelectable && column == t.selectedColumn
cellSelected := !cell.NotSelectable && (columnSelected || rowSelected || t.rowsSelectable && t.columnsSelectable && column == t.selectedColumn && row == t.selectedRow) cellSelected := !cell.NotSelectable && (columnSelected || rowSelected || t.rowsSelectable && t.columnsSelectable && column == t.selectedColumn && row == t.selectedRow)
entries, ok := cellsByBackgroundColor[cell.BackgroundColor] entries, ok := cellsByBackgroundColor[cell.BackgroundColor]
cellsByBackgroundColor[cell.BackgroundColor] = append(entries, &struct { cellsByBackgroundColor[cell.BackgroundColor] = append(entries, &cellInfo{
x, y, w, h int
text tcell.Color
selected bool
}{
x: bx, x: bx,
y: by, y: by,
w: bw, w: bw,
@ -822,13 +870,18 @@ ColumnLoop:
_, _, lj := c.Hcl() _, _, lj := c.Hcl()
return li < lj return li < lj
}) })
selFg, selBg, selAttr := t.selectedStyle.Decompose()
for _, bgColor := range backgroundColors { for _, bgColor := range backgroundColors {
entries := cellsByBackgroundColor[bgColor] entries := cellsByBackgroundColor[bgColor]
for _, cell := range entries { for _, cell := range entries {
if cell.selected { if cell.selected {
defer colorBackground(cell.x, cell.y, cell.w, cell.h, bgColor, cell.text, true) if t.selectedStyle != 0 {
defer colorBackground(cell.x, cell.y, cell.w, cell.h, selBg, selFg, selAttr, false)
} else {
defer colorBackground(cell.x, cell.y, cell.w, cell.h, bgColor, cell.text, 0, true)
}
} else { } else {
colorBackground(cell.x, cell.y, cell.w, cell.h, bgColor, cell.text, false) colorBackground(cell.x, cell.y, cell.w, cell.h, bgColor, tcell.ColorDefault, 0, false)
} }
} }
} }

View File

@ -31,7 +31,7 @@ type textViewIndex struct {
// TextView is a box which displays text. It implements the io.Writer interface // TextView is a box which displays text. It implements the io.Writer interface
// so you can stream text to it. This does not trigger a redraw automatically // so you can stream text to it. This does not trigger a redraw automatically
// but if a handler is installed via SetChangedFunc(), you can cause it to be // but if a handler is installed via SetChangedFunc(), you can cause it to be
// redrawn. // redrawn. (See SetChangedFunc() for more details.)
// //
// Navigation // Navigation
// //
@ -103,6 +103,10 @@ type TextView struct {
// during re-indexing. Set to -1 if there is no current highlight. // during re-indexing. Set to -1 if there is no current highlight.
fromHighlight, toHighlight int fromHighlight, toHighlight int
// The screen space column of the highlight in its first line. Set to -1 if
// there is no current highlight.
posHighlight int
// A set of region IDs that are currently highlighted. // A set of region IDs that are currently highlighted.
highlights map[string]struct{} highlights map[string]struct{}
@ -170,6 +174,7 @@ func NewTextView() *TextView {
align: AlignLeft, align: AlignLeft,
wrap: true, wrap: true,
textColor: Styles.PrimaryTextColor, textColor: Styles.PrimaryTextColor,
regions: false,
dynamicColors: false, dynamicColors: false,
} }
} }
@ -255,8 +260,20 @@ func (t *TextView) SetRegions(regions bool) *TextView {
} }
// SetChangedFunc sets a handler function which is called when the text of the // SetChangedFunc sets a handler function which is called when the text of the
// text view has changed. This is typically used to cause the application to // text view has changed. This is useful when text is written to this io.Writer
// redraw the screen. // in a separate goroutine. This does not automatically cause the screen to be
// refreshed so you may want to use the "changed" handler to redraw the screen.
//
// Note that to avoid race conditions or deadlocks, there are a few rules you
// should follow:
//
// - You can call Application.Draw() from this handler.
// - You can call TextView.HasFocus() from this handler.
// - During the execution of this handler, access to any other variables from
// this primitive or any other primitive should be queued using
// Application.QueueUpdate().
//
// See package description for details on dealing with concurrency.
func (t *TextView) SetChangedFunc(handler func()) *TextView { func (t *TextView) SetChangedFunc(handler func()) *TextView {
t.changed = handler t.changed = handler
return t return t
@ -270,6 +287,16 @@ func (t *TextView) SetDoneFunc(handler func(key tcell.Key)) *TextView {
return t return t
} }
// ScrollTo scrolls to the specified row and column (both starting with 0).
func (t *TextView) ScrollTo(row, column int) *TextView {
if !t.scrollable {
return t
}
t.lineOffset = row
t.columnOffset = column
return t
}
// ScrollToBeginning scrolls to the top left corner of the text if the text view // ScrollToBeginning scrolls to the top left corner of the text if the text view
// is scrollable. // is scrollable.
func (t *TextView) ScrollToBeginning() *TextView { func (t *TextView) ScrollToBeginning() *TextView {
@ -294,6 +321,12 @@ func (t *TextView) ScrollToEnd() *TextView {
return t return t
} }
// GetScrollOffset returns the number of rows and columns that are skipped at
// the top left corner when the text view has been scrolled.
func (t *TextView) GetScrollOffset() (row, column int) {
return t.lineOffset, t.columnOffset
}
// Clear removes all text from the buffer. // Clear removes all text from the buffer.
func (t *TextView) Clear() *TextView { func (t *TextView) Clear() *TextView {
t.buffer = nil t.buffer = nil
@ -420,13 +453,33 @@ func (t *TextView) GetRegionText(regionID string) string {
return escapePattern.ReplaceAllString(buffer.String(), `[$1$2]`) return escapePattern.ReplaceAllString(buffer.String(), `[$1$2]`)
} }
// Focus is called when this primitive receives focus.
func (t *TextView) Focus(delegate func(p Primitive)) {
// Implemented here with locking because this is used by layout primitives.
t.Lock()
defer t.Unlock()
t.hasFocus = true
}
// HasFocus returns whether or not this primitive has focus.
func (t *TextView) HasFocus() bool {
// Implemented here with locking because this may be used in the "changed"
// callback.
t.Lock()
defer t.Unlock()
return t.hasFocus
}
// Write lets us implement the io.Writer interface. Tab characters will be // Write lets us implement the io.Writer interface. Tab characters will be
// replaced with TabSize space characters. A "\n" or "\r\n" will be interpreted // replaced with TabSize space characters. A "\n" or "\r\n" will be interpreted
// as a new line. // as a new line.
func (t *TextView) Write(p []byte) (n int, err error) { func (t *TextView) Write(p []byte) (n int, err error) {
// Notify at the end. // Notify at the end.
if t.changed != nil { t.Lock()
defer t.changed() changed := t.changed
t.Unlock()
if changed != nil {
defer changed() // Deadlocks may occur if we lock here.
} }
t.Lock() t.Lock()
@ -492,7 +545,7 @@ func (t *TextView) reindexBuffer(width int) {
return // Nothing has changed. We can still use the current index. return // Nothing has changed. We can still use the current index.
} }
t.index = nil t.index = nil
t.fromHighlight, t.toHighlight = -1, -1 t.fromHighlight, t.toHighlight, t.posHighlight = -1, -1, -1
// If there's no space, there's no index. // If there's no space, there's no index.
if width < 1 { if width < 1 {
@ -511,8 +564,9 @@ func (t *TextView) reindexBuffer(width int) {
colorTags [][]string colorTags [][]string
escapeIndices [][]int escapeIndices [][]int
) )
strippedStr := str
if t.dynamicColors { if t.dynamicColors {
colorTagIndices, colorTags, escapeIndices, str, _ = decomposeString(str) colorTagIndices, colorTags, escapeIndices, strippedStr, _ = decomposeString(str)
} }
// Find all regions in this line. Then remove them. // Find all regions in this line. Then remove them.
@ -523,14 +577,12 @@ func (t *TextView) reindexBuffer(width int) {
if t.regions { if t.regions {
regionIndices = regionPattern.FindAllStringIndex(str, -1) regionIndices = regionPattern.FindAllStringIndex(str, -1)
regions = regionPattern.FindAllStringSubmatch(str, -1) regions = regionPattern.FindAllStringSubmatch(str, -1)
str = regionPattern.ReplaceAllString(str, "") strippedStr = regionPattern.ReplaceAllString(strippedStr, "")
if !t.dynamicColors {
// We haven't detected escape tags yet. Do it now.
escapeIndices = escapePattern.FindAllStringIndex(str, -1)
str = escapePattern.ReplaceAllString(str, "[$1$2]")
}
} }
// We don't need the original string anymore for now.
str = strippedStr
// Split the line if required. // Split the line if required.
var splitLines []string var splitLines []string
if t.wrap && len(str) > 0 { if t.wrap && len(str) > 0 {
@ -574,15 +626,53 @@ func (t *TextView) reindexBuffer(width int) {
// Shift original position with tags. // Shift original position with tags.
lineLength := len(splitLine) lineLength := len(splitLine)
remainingLength := lineLength
tagEnd := originalPos
totalTagLength := 0
for { for {
if colorPos < len(colorTagIndices) && colorTagIndices[colorPos][0] <= originalPos+lineLength { // Which tag comes next?
nextTag := make([][3]int, 0, 3)
if colorPos < len(colorTagIndices) {
nextTag = append(nextTag, [3]int{colorTagIndices[colorPos][0], colorTagIndices[colorPos][1], 0}) // 0 = color tag.
}
if regionPos < len(regionIndices) {
nextTag = append(nextTag, [3]int{regionIndices[regionPos][0], regionIndices[regionPos][1], 1}) // 1 = region tag.
}
if escapePos < len(escapeIndices) {
nextTag = append(nextTag, [3]int{escapeIndices[escapePos][0], escapeIndices[escapePos][1], 2}) // 2 = escape tag.
}
minPos := -1
tagIndex := -1
for index, pair := range nextTag {
if minPos < 0 || pair[0] < minPos {
minPos = pair[0]
tagIndex = index
}
}
// Is the next tag in range?
if tagIndex < 0 || minPos >= tagEnd+remainingLength {
break // No. We're done with this line.
}
// Advance.
strippedTagStart := nextTag[tagIndex][0] - originalPos - totalTagLength
tagEnd = nextTag[tagIndex][1]
tagLength := tagEnd - nextTag[tagIndex][0]
if nextTag[tagIndex][2] == 2 {
tagLength = 1
}
totalTagLength += tagLength
remainingLength = lineLength - (tagEnd - originalPos - totalTagLength)
// Process the tag.
switch nextTag[tagIndex][2] {
case 0:
// Process color tags. // Process color tags.
originalPos += colorTagIndices[colorPos][1] - colorTagIndices[colorPos][0]
foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colorTags[colorPos]) foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colorTags[colorPos])
colorPos++ colorPos++
} else if regionPos < len(regionIndices) && regionIndices[regionPos][0] <= originalPos+lineLength { case 1:
// Process region tags. // Process region tags.
originalPos += regionIndices[regionPos][1] - regionIndices[regionPos][0]
regionID = regions[regionPos][1] regionID = regions[regionPos][1]
_, highlighted = t.highlights[regionID] _, highlighted = t.highlights[regionID]
@ -591,23 +681,21 @@ func (t *TextView) reindexBuffer(width int) {
line := len(t.index) line := len(t.index)
if t.fromHighlight < 0 { if t.fromHighlight < 0 {
t.fromHighlight, t.toHighlight = line, line t.fromHighlight, t.toHighlight = line, line
t.posHighlight = runewidth.StringWidth(splitLine[:strippedTagStart])
} else if line > t.toHighlight { } else if line > t.toHighlight {
t.toHighlight = line t.toHighlight = line
} }
} }
regionPos++ regionPos++
} else if escapePos < len(escapeIndices) && escapeIndices[escapePos][0] <= originalPos+lineLength { case 2:
// Process escape tags. // Process escape tags.
originalPos++
escapePos++ escapePos++
} else {
break
} }
} }
// Advance to next line. // Advance to next line.
originalPos += lineLength originalPos += lineLength + totalTagLength
// Append this line. // Append this line.
line.NextPos = originalPos line.NextPos = originalPos
@ -649,7 +737,7 @@ func (t *TextView) Draw(screen tcell.Screen) {
t.pageSize = height t.pageSize = height
// If the width has changed, we need to reindex. // If the width has changed, we need to reindex.
if width != t.lastWidth { if width != t.lastWidth && t.wrap {
t.index = nil t.index = nil
} }
t.lastWidth = width t.lastWidth = width
@ -672,6 +760,16 @@ func (t *TextView) Draw(screen tcell.Screen) {
// No, let's move to the start of the highlights. // No, let's move to the start of the highlights.
t.lineOffset = t.fromHighlight t.lineOffset = t.fromHighlight
} }
// If the highlight is too far to the right, move it to the middle.
if t.posHighlight-t.columnOffset > 3*width/4 {
t.columnOffset = t.posHighlight - width/2
}
// If the highlight is off-screen on the left, move it on-screen.
if t.posHighlight-t.columnOffset < 0 {
t.columnOffset = t.posHighlight - width/4
}
} }
t.scrollToHighlights = false t.scrollToHighlights = false
@ -737,8 +835,9 @@ func (t *TextView) Draw(screen tcell.Screen) {
colorTags [][]string colorTags [][]string
escapeIndices [][]int escapeIndices [][]int
) )
strippedText := text
if t.dynamicColors { if t.dynamicColors {
colorTagIndices, colorTags, escapeIndices, _, _ = decomposeString(text) colorTagIndices, colorTags, escapeIndices, strippedText, _ = decomposeString(text)
} }
// Get regions. // Get regions.
@ -749,8 +848,10 @@ func (t *TextView) Draw(screen tcell.Screen) {
if t.regions { if t.regions {
regionIndices = regionPattern.FindAllStringIndex(text, -1) regionIndices = regionPattern.FindAllStringIndex(text, -1)
regions = regionPattern.FindAllStringSubmatch(text, -1) regions = regionPattern.FindAllStringSubmatch(text, -1)
strippedText = regionPattern.ReplaceAllString(strippedText, "")
if !t.dynamicColors { if !t.dynamicColors {
escapeIndices = escapePattern.FindAllStringIndex(text, -1) escapeIndices = escapePattern.FindAllStringIndex(text, -1)
strippedText = string(escapePattern.ReplaceAllString(strippedText, "[$1$2]"))
} }
} }
@ -769,11 +870,29 @@ func (t *TextView) Draw(screen tcell.Screen) {
} }
// Print the line. // Print the line.
var currentTag, currentRegion, currentEscapeTag, skipped, runeSeqWidth int var colorPos, regionPos, escapePos, tagOffset, skipped int
runeSequence := make([]rune, 0, 10) iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
flush := func() { // Process tags.
if len(runeSequence) == 0 { for {
return if colorPos < len(colorTags) && textPos+tagOffset >= colorTagIndices[colorPos][0] && textPos+tagOffset < colorTagIndices[colorPos][1] {
// Get the color.
foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colorTags[colorPos])
tagOffset += colorTagIndices[colorPos][1] - colorTagIndices[colorPos][0]
colorPos++
} else if regionPos < len(regionIndices) && textPos+tagOffset >= regionIndices[regionPos][0] && textPos+tagOffset < regionIndices[regionPos][1] {
// Get the region.
regionID = regions[regionPos][1]
tagOffset += regionIndices[regionPos][1] - regionIndices[regionPos][0]
regionPos++
} else {
break
}
}
// Skip the second-to-last character of an escape tag.
if escapePos < len(escapeIndices) && textPos+tagOffset == escapeIndices[escapePos][1]-2 {
tagOffset++
escapePos++
} }
// Mix the existing style with the new style. // Mix the existing style with the new style.
@ -803,87 +922,30 @@ func (t *TextView) Draw(screen tcell.Screen) {
style = style.Background(fg).Foreground(bg) style = style.Background(fg).Foreground(bg)
} }
// Draw the character.
var comb []rune
if len(runeSequence) > 1 {
// Allocate space for the combining characters only when necessary.
comb = make([]rune, len(runeSequence)-1)
copy(comb, runeSequence[1:])
}
for offset := 0; offset < runeSeqWidth; offset++ {
screen.SetContent(x+posX+offset, y+line-t.lineOffset, runeSequence[0], comb, style)
}
// Advance.
posX += runeSeqWidth
runeSequence = runeSequence[:0]
runeSeqWidth = 0
}
for pos, ch := range text {
// Get the color.
if currentTag < len(colorTags) && pos >= colorTagIndices[currentTag][0] && pos < colorTagIndices[currentTag][1] {
flush()
if pos == colorTagIndices[currentTag][1]-1 {
foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colorTags[currentTag])
currentTag++
}
continue
}
// Get the region.
if currentRegion < len(regionIndices) && pos >= regionIndices[currentRegion][0] && pos < regionIndices[currentRegion][1] {
flush()
if pos == regionIndices[currentRegion][1]-1 {
regionID = regions[currentRegion][1]
currentRegion++
}
continue
}
// Skip the second-to-last character of an escape tag.
if currentEscapeTag < len(escapeIndices) && pos >= escapeIndices[currentEscapeTag][0] && pos < escapeIndices[currentEscapeTag][1] {
flush()
if pos == escapeIndices[currentEscapeTag][1]-1 {
currentEscapeTag++
} else if pos == escapeIndices[currentEscapeTag][1]-2 {
continue
}
}
// Determine the width of this rune.
chWidth := runewidth.RuneWidth(ch)
if chWidth == 0 {
// If this is not a modifier, we treat it as a space character.
if len(runeSequence) == 0 {
ch = ' '
chWidth = 1
} else {
runeSequence = append(runeSequence, ch)
continue
}
}
// Skip to the right. // Skip to the right.
if !t.wrap && skipped < skip { if !t.wrap && skipped < skip {
skipped += chWidth skipped += screenWidth
continue return false
} }
// Stop at the right border. // Stop at the right border.
if posX+runeSeqWidth+chWidth > width { if posX+screenWidth > width {
break return true
} }
// Flush the rune sequence. // Draw the character.
flush() for offset := screenWidth - 1; offset >= 0; offset-- {
if offset == 0 {
screen.SetContent(x+posX+offset, y+line-t.lineOffset, main, comb, style)
} else {
screen.SetContent(x+posX+offset, y+line-t.lineOffset, ' ', nil, style)
}
}
// Queue this rune. // Advance.
runeSequence = append(runeSequence, ch) posX += screenWidth
runeSeqWidth += chWidth return false
} })
if posX+runeSeqWidth <= width {
flush()
}
} }
// If this view is not scrollable, we'll purge the buffer of lines that have // If this view is not scrollable, we'll purge the buffer of lines that have

684
vendor/maunium.net/go/tview/treeview.go generated vendored Normal file
View File

@ -0,0 +1,684 @@
package tview
import (
"maunium.net/go/tcell"
)
// Tree navigation events.
const (
treeNone int = iota
treeHome
treeEnd
treeUp
treeDown
treePageUp
treePageDown
)
// TreeNode represents one node in a tree view.
type TreeNode struct {
// The reference object.
reference interface{}
// This node's child nodes.
children []*TreeNode
// The item's text.
text string
// The text color.
color tcell.Color
// Whether or not this node can be selected.
selectable bool
// Whether or not this node's children should be displayed.
expanded bool
// The additional horizontal indent of this node's text.
indent int
// An optional function which is called when the user selects this node.
selected func()
// Temporary member variables.
parent *TreeNode // The parent node (nil for the root).
level int // The hierarchy level (0 for the root, 1 for its children, and so on).
graphicsX int // The x-coordinate of the left-most graphics rune.
textX int // The x-coordinate of the first rune of the text.
}
// NewTreeNode returns a new tree node.
func NewTreeNode(text string) *TreeNode {
return &TreeNode{
text: text,
color: Styles.PrimaryTextColor,
indent: 2,
expanded: true,
selectable: true,
}
}
// Walk traverses this node's subtree in depth-first, pre-order (NLR) order and
// calls the provided callback function on each traversed node (which includes
// this node) with the traversed node and its parent node (nil for this node).
// The callback returns whether traversal should continue with the traversed
// node's child nodes (true) or not recurse any deeper (false).
func (n *TreeNode) Walk(callback func(node, parent *TreeNode) bool) *TreeNode {
n.parent = nil
nodes := []*TreeNode{n}
for len(nodes) > 0 {
// Pop the top node and process it.
node := nodes[len(nodes)-1]
nodes = nodes[:len(nodes)-1]
if !callback(node, node.parent) {
// Don't add any children.
continue
}
// Add children in reverse order.
for index := len(node.children) - 1; index >= 0; index-- {
node.children[index].parent = node
nodes = append(nodes, node.children[index])
}
}
return n
}
// SetReference allows you to store a reference of any type in this node. This
// will allow you to establish a mapping between the TreeView hierarchy and your
// internal tree structure.
func (n *TreeNode) SetReference(reference interface{}) *TreeNode {
n.reference = reference
return n
}
// GetReference returns this node's reference object.
func (n *TreeNode) GetReference() interface{} {
return n.reference
}
// SetChildren sets this node's child nodes.
func (n *TreeNode) SetChildren(childNodes []*TreeNode) *TreeNode {
n.children = childNodes
return n
}
// GetChildren returns this node's children.
func (n *TreeNode) GetChildren() []*TreeNode {
return n.children
}
// ClearChildren removes all child nodes from this node.
func (n *TreeNode) ClearChildren() *TreeNode {
n.children = nil
return n
}
// AddChild adds a new child node to this node.
func (n *TreeNode) AddChild(node *TreeNode) *TreeNode {
n.children = append(n.children, node)
return n
}
// SetSelectable sets a flag indicating whether this node can be selected by
// the user.
func (n *TreeNode) SetSelectable(selectable bool) *TreeNode {
n.selectable = selectable
return n
}
// SetSelectedFunc sets a function which is called when the user selects this
// node by hitting Enter when it is selected.
func (n *TreeNode) SetSelectedFunc(handler func()) *TreeNode {
n.selected = handler
return n
}
// SetExpanded sets whether or not this node's child nodes should be displayed.
func (n *TreeNode) SetExpanded(expanded bool) *TreeNode {
n.expanded = expanded
return n
}
// Expand makes the child nodes of this node appear.
func (n *TreeNode) Expand() *TreeNode {
n.expanded = true
return n
}
// Collapse makes the child nodes of this node disappear.
func (n *TreeNode) Collapse() *TreeNode {
n.expanded = false
return n
}
// ExpandAll expands this node and all descendent nodes.
func (n *TreeNode) ExpandAll() *TreeNode {
n.Walk(func(node, parent *TreeNode) bool {
node.expanded = true
return true
})
return n
}
// CollapseAll collapses this node and all descendent nodes.
func (n *TreeNode) CollapseAll() *TreeNode {
n.Walk(func(node, parent *TreeNode) bool {
n.expanded = false
return true
})
return n
}
// IsExpanded returns whether the child nodes of this node are visible.
func (n *TreeNode) IsExpanded() bool {
return n.expanded
}
// SetText sets the node's text which is displayed.
func (n *TreeNode) SetText(text string) *TreeNode {
n.text = text
return n
}
// SetColor sets the node's text color.
func (n *TreeNode) SetColor(color tcell.Color) *TreeNode {
n.color = color
return n
}
// SetIndent sets an additional indentation for this node's text. A value of 0
// keeps the text as far left as possible with a minimum of line graphics. Any
// value greater than that moves the text to the right.
func (n *TreeNode) SetIndent(indent int) *TreeNode {
n.indent = indent
return n
}
// TreeView displays tree structures. A tree consists of nodes (TreeNode
// objects) where each node has zero or more child nodes and exactly one parent
// node (except for the root node which has no parent node).
//
// The SetRoot() function is used to specify the root of the tree. Other nodes
// are added locally to the root node or any of its descendents. See the
// TreeNode documentation for details on node attributes. (You can use
// SetReference() to store a reference to nodes of your own tree structure.)
//
// Nodes can be selected by calling SetCurrentNode(). The user can navigate the
// selection or the tree by using the following keys:
//
// - j, down arrow, right arrow: Move (the selection) down by one node.
// - k, up arrow, left arrow: Move (the selection) up by one node.
// - g, home: Move (the selection) to the top.
// - G, end: Move (the selection) to the bottom.
// - Ctrl-F, page down: Move (the selection) down by one page.
// - Ctrl-B, page up: Move (the selection) up by one page.
//
// Selected nodes can trigger the "selected" callback when the user hits Enter.
//
// The root node corresponds to level 0, its children correspond to level 1,
// their children to level 2, and so on. Per default, the first level that is
// displayed is 0, i.e. the root node. You can call SetTopLevel() to hide
// levels.
//
// If graphics are turned on (see SetGraphics()), lines indicate the tree's
// hierarchy. Alternative (or additionally), you can set different prefixes
// using SetPrefixes() for different levels, for example to display hierarchical
// bullet point lists.
//
// See https://github.com/rivo/tview/wiki/TreeView for an example.
type TreeView struct {
*Box
// The root node.
root *TreeNode
// The currently selected node or nil if no node is selected.
currentNode *TreeNode
// The movement to be performed during the call to Draw(), one of the
// constants defined above.
movement int
// The top hierarchical level shown. (0 corresponds to the root level.)
topLevel int
// Strings drawn before the nodes, based on their level.
prefixes []string
// Vertical scroll offset.
offsetY int
// If set to true, all node texts will be aligned horizontally.
align bool
// If set to true, the tree structure is drawn using lines.
graphics bool
// The color of the lines.
graphicsColor tcell.Color
// An optional function which is called when the user has navigated to a new
// tree node.
changed func(node *TreeNode)
// An optional function which is called when a tree item was selected.
selected func(node *TreeNode)
// The visible nodes, top-down, as set by process().
nodes []*TreeNode
}
// NewTreeView returns a new tree view.
func NewTreeView() *TreeView {
return &TreeView{
Box: NewBox(),
graphics: true,
graphicsColor: Styles.GraphicsColor,
}
}
// SetRoot sets the root node of the tree.
func (t *TreeView) SetRoot(root *TreeNode) *TreeView {
t.root = root
return t
}
// GetRoot returns the root node of the tree. If no such node was previously
// set, nil is returned.
func (t *TreeView) GetRoot() *TreeNode {
return t.root
}
// SetCurrentNode sets the currently selected node. Provide nil to clear all
// selections. Selected nodes must be visible and selectable, or else the
// selection will be changed to the top-most selectable and visible node.
//
// This function does NOT trigger the "changed" callback.
func (t *TreeView) SetCurrentNode(node *TreeNode) *TreeView {
t.currentNode = node
return t
}
// GetCurrentNode returns the currently selected node or nil of no node is
// currently selected.
func (t *TreeView) GetCurrentNode() *TreeNode {
return t.currentNode
}
// SetTopLevel sets the first tree level that is visible with 0 referring to the
// root, 1 to the root's child nodes, and so on. Nodes above the top level are
// not displayed.
func (t *TreeView) SetTopLevel(topLevel int) *TreeView {
t.topLevel = topLevel
return t
}
// SetPrefixes defines the strings drawn before the nodes' texts. This is a
// slice of strings where each element corresponds to a node's hierarchy level,
// i.e. 0 for the root, 1 for the root's children, and so on (levels will
// cycle).
//
// For example, to display a hierarchical list with bullet points:
//
// treeView.SetGraphics(false).
// SetPrefixes([]string{"* ", "- ", "x "})
func (t *TreeView) SetPrefixes(prefixes []string) *TreeView {
t.prefixes = prefixes
return t
}
// SetAlign controls the horizontal alignment of the node texts. If set to true,
// all texts except that of top-level nodes will be placed in the same column.
// If set to false, they will indent with the hierarchy.
func (t *TreeView) SetAlign(align bool) *TreeView {
t.align = align
return t
}
// SetGraphics sets a flag which determines whether or not line graphics are
// drawn to illustrate the tree's hierarchy.
func (t *TreeView) SetGraphics(showGraphics bool) *TreeView {
t.graphics = showGraphics
return t
}
// SetGraphicsColor sets the colors of the lines used to draw the tree structure.
func (t *TreeView) SetGraphicsColor(color tcell.Color) *TreeView {
t.graphicsColor = color
return t
}
// SetChangedFunc sets the function which is called when the user navigates to
// a new tree node.
func (t *TreeView) SetChangedFunc(handler func(node *TreeNode)) *TreeView {
t.changed = handler
return t
}
// SetSelectedFunc sets the function which is called when the user selects a
// node by pressing Enter on the current selection.
func (t *TreeView) SetSelectedFunc(handler func(node *TreeNode)) *TreeView {
t.selected = handler
return t
}
// process builds the visible tree, populates the "nodes" slice, and processes
// pending selection actions.
func (t *TreeView) process() {
_, _, _, height := t.GetInnerRect()
// Determine visible nodes and their placement.
var graphicsOffset, maxTextX int
t.nodes = nil
selectedIndex := -1
topLevelGraphicsX := -1
if t.graphics {
graphicsOffset = 1
}
t.root.Walk(func(node, parent *TreeNode) bool {
// Set node attributes.
node.parent = parent
if parent == nil {
node.level = 0
node.graphicsX = 0
node.textX = 0
} else {
node.level = parent.level + 1
node.graphicsX = parent.textX
node.textX = node.graphicsX + graphicsOffset + node.indent
}
if !t.graphics && t.align {
// Without graphics, we align nodes on the first column.
node.textX = 0
}
if node.level == t.topLevel {
// No graphics for top level nodes.
node.graphicsX = 0
node.textX = 0
}
if node.textX > maxTextX {
maxTextX = node.textX
}
if node == t.currentNode && node.selectable {
selectedIndex = len(t.nodes)
}
// Maybe we want to skip this level.
if t.topLevel == node.level && (topLevelGraphicsX < 0 || node.graphicsX < topLevelGraphicsX) {
topLevelGraphicsX = node.graphicsX
}
// Add and recurse (if desired).
if node.level >= t.topLevel {
t.nodes = append(t.nodes, node)
}
return node.expanded
})
// Post-process positions.
for _, node := range t.nodes {
// If text must align, we correct the positions.
if t.align && node.level > t.topLevel {
node.textX = maxTextX
}
// If we skipped levels, shift to the left.
if topLevelGraphicsX > 0 {
node.graphicsX -= topLevelGraphicsX
node.textX -= topLevelGraphicsX
}
}
// Process selection. (Also trigger events if necessary.)
if selectedIndex >= 0 {
// Move the selection.
newSelectedIndex := selectedIndex
MovementSwitch:
switch t.movement {
case treeUp:
for newSelectedIndex > 0 {
newSelectedIndex--
if t.nodes[newSelectedIndex].selectable {
break MovementSwitch
}
}
newSelectedIndex = selectedIndex
case treeDown:
for newSelectedIndex < len(t.nodes)-1 {
newSelectedIndex++
if t.nodes[newSelectedIndex].selectable {
break MovementSwitch
}
}
newSelectedIndex = selectedIndex
case treeHome:
for newSelectedIndex = 0; newSelectedIndex < len(t.nodes); newSelectedIndex++ {
if t.nodes[newSelectedIndex].selectable {
break MovementSwitch
}
}
newSelectedIndex = selectedIndex
case treeEnd:
for newSelectedIndex = len(t.nodes) - 1; newSelectedIndex >= 0; newSelectedIndex-- {
if t.nodes[newSelectedIndex].selectable {
break MovementSwitch
}
}
newSelectedIndex = selectedIndex
case treePageUp:
if newSelectedIndex+height < len(t.nodes) {
newSelectedIndex += height
} else {
newSelectedIndex = len(t.nodes) - 1
}
for ; newSelectedIndex < len(t.nodes); newSelectedIndex++ {
if t.nodes[newSelectedIndex].selectable {
break MovementSwitch
}
}
newSelectedIndex = selectedIndex
case treePageDown:
if newSelectedIndex >= height {
newSelectedIndex -= height
} else {
newSelectedIndex = 0
}
for ; newSelectedIndex >= 0; newSelectedIndex-- {
if t.nodes[newSelectedIndex].selectable {
break MovementSwitch
}
}
newSelectedIndex = selectedIndex
}
t.currentNode = t.nodes[newSelectedIndex]
if newSelectedIndex != selectedIndex {
t.movement = treeNone
if t.changed != nil {
t.changed(t.currentNode)
}
}
selectedIndex = newSelectedIndex
// Move selection into viewport.
if selectedIndex-t.offsetY >= height {
t.offsetY = selectedIndex - height + 1
}
if selectedIndex < t.offsetY {
t.offsetY = selectedIndex
}
} else {
// If selection is not visible or selectable, select the first candidate.
if t.currentNode != nil {
for index, node := range t.nodes {
if node.selectable {
selectedIndex = index
t.currentNode = node
break
}
}
}
if selectedIndex < 0 {
t.currentNode = nil
}
}
}
// Draw draws this primitive onto the screen.
func (t *TreeView) Draw(screen tcell.Screen) {
t.Box.Draw(screen)
if t.root == nil {
return
}
// Build the tree if necessary.
if t.nodes == nil {
t.process()
}
defer func() {
t.nodes = nil // Rebuild during next call to Draw()
}()
// Scroll the tree.
x, y, width, height := t.GetInnerRect()
switch t.movement {
case treeUp:
t.offsetY--
case treeDown:
t.offsetY++
case treeHome:
t.offsetY = 0
case treeEnd:
t.offsetY = len(t.nodes)
case treePageUp:
t.offsetY -= height
case treePageDown:
t.offsetY += height
}
t.movement = treeNone
// Fix invalid offsets.
if t.offsetY >= len(t.nodes)-height {
t.offsetY = len(t.nodes) - height
}
if t.offsetY < 0 {
t.offsetY = 0
}
// Draw the tree.
posY := y
lineStyle := tcell.StyleDefault.Background(t.backgroundColor).Foreground(t.graphicsColor)
for index, node := range t.nodes {
// Skip invisible parts.
if posY >= y+height+1 {
break
}
if index < t.offsetY {
continue
}
// Draw the graphics.
if t.graphics {
// Draw ancestor branches.
ancestor := node.parent
for ancestor != nil && ancestor.parent != nil && ancestor.parent.level >= t.topLevel {
if ancestor.graphicsX >= width {
continue
}
// Draw a branch if this ancestor is not a last child.
if ancestor.parent.children[len(ancestor.parent.children)-1] != ancestor {
if posY-1 >= y && ancestor.textX > ancestor.graphicsX {
PrintJoinedSemigraphics(screen, x+ancestor.graphicsX, posY-1, Borders.Vertical, t.graphicsColor)
}
if posY < y+height {
screen.SetContent(x+ancestor.graphicsX, posY, Borders.Vertical, nil, lineStyle)
}
}
ancestor = ancestor.parent
}
if node.textX > node.graphicsX && node.graphicsX < width {
// Connect to the node above.
if posY-1 >= y && t.nodes[index-1].graphicsX <= node.graphicsX && t.nodes[index-1].textX > node.graphicsX {
PrintJoinedSemigraphics(screen, x+node.graphicsX, posY-1, Borders.TopLeft, t.graphicsColor)
}
// Join this node.
if posY < y+height {
screen.SetContent(x+node.graphicsX, posY, Borders.BottomLeft, nil, lineStyle)
for pos := node.graphicsX + 1; pos < node.textX && pos < width; pos++ {
screen.SetContent(x+pos, posY, Borders.Horizontal, nil, lineStyle)
}
}
}
}
// Draw the prefix and the text.
if node.textX < width && posY < y+height {
// Prefix.
var prefixWidth int
if len(t.prefixes) > 0 {
_, prefixWidth = Print(screen, t.prefixes[(node.level-t.topLevel)%len(t.prefixes)], x+node.textX, posY, width-node.textX, AlignLeft, node.color)
}
// Text.
if node.textX+prefixWidth < width {
style := tcell.StyleDefault.Foreground(node.color)
if node == t.currentNode {
style = tcell.StyleDefault.Background(node.color).Foreground(t.backgroundColor)
}
printWithStyle(screen, node.text, x+node.textX+prefixWidth, posY, width-node.textX-prefixWidth, AlignLeft, style)
}
}
// Advance.
posY++
}
}
// InputHandler returns the handler for this primitive.
func (t *TreeView) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return t.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
// Because the tree is flattened into a list only at drawing time, we also
// postpone the (selection) movement to drawing time.
switch key := event.Key(); key {
case tcell.KeyTab, tcell.KeyDown, tcell.KeyRight:
t.movement = treeDown
case tcell.KeyBacktab, tcell.KeyUp, tcell.KeyLeft:
t.movement = treeUp
case tcell.KeyHome:
t.movement = treeHome
case tcell.KeyEnd:
t.movement = treeEnd
case tcell.KeyPgDn, tcell.KeyCtrlF:
t.movement = treePageDown
case tcell.KeyPgUp, tcell.KeyCtrlB:
t.movement = treePageUp
case tcell.KeyRune:
switch event.Rune() {
case 'g':
t.movement = treeHome
case 'G':
t.movement = treeEnd
case 'j':
t.movement = treeDown
case 'k':
t.movement = treeUp
}
case tcell.KeyEnter:
if t.currentNode != nil {
if t.selected != nil {
t.selected(t.currentNode)
}
if t.currentNode.selected != nil {
t.currentNode.selected()
}
}
}
t.process()
})
}

664
vendor/maunium.net/go/tview/util.go generated vendored
View File

@ -1,11 +1,9 @@
package tview package tview
import ( import (
"fmt"
"math" "math"
"regexp" "regexp"
"strconv" "strconv"
"strings"
"unicode" "unicode"
"maunium.net/go/tcell" "maunium.net/go/tcell"
@ -19,97 +17,13 @@ const (
AlignRight AlignRight
) )
// Semigraphical runes.
const (
GraphicsHoriBar = '\u2500'
GraphicsVertBar = '\u2502'
GraphicsTopLeftCorner = '\u250c'
GraphicsTopRightCorner = '\u2510'
GraphicsBottomLeftCorner = '\u2514'
GraphicsBottomRightCorner = '\u2518'
GraphicsLeftT = '\u251c'
GraphicsRightT = '\u2524'
GraphicsTopT = '\u252c'
GraphicsBottomT = '\u2534'
GraphicsCross = '\u253c'
GraphicsDbVertBar = '\u2550'
GraphicsDbHorBar = '\u2551'
GraphicsDbTopLeftCorner = '\u2554'
GraphicsDbTopRightCorner = '\u2557'
GraphicsDbBottomRightCorner = '\u255d'
GraphicsDbBottomLeftCorner = '\u255a'
GraphicsEllipsis = '\u2026'
)
// joints maps combinations of two graphical runes to the rune that results
// when joining the two in the same screen cell. The keys of this map are
// two-rune strings where the value of the first rune is lower than the value
// of the second rune. Identical runes are not contained.
var joints = map[string]rune{
"\u2500\u2502": GraphicsCross,
"\u2500\u250c": GraphicsTopT,
"\u2500\u2510": GraphicsTopT,
"\u2500\u2514": GraphicsBottomT,
"\u2500\u2518": GraphicsBottomT,
"\u2500\u251c": GraphicsCross,
"\u2500\u2524": GraphicsCross,
"\u2500\u252c": GraphicsTopT,
"\u2500\u2534": GraphicsBottomT,
"\u2500\u253c": GraphicsCross,
"\u2502\u250c": GraphicsLeftT,
"\u2502\u2510": GraphicsRightT,
"\u2502\u2514": GraphicsLeftT,
"\u2502\u2518": GraphicsRightT,
"\u2502\u251c": GraphicsLeftT,
"\u2502\u2524": GraphicsRightT,
"\u2502\u252c": GraphicsCross,
"\u2502\u2534": GraphicsCross,
"\u2502\u253c": GraphicsCross,
"\u250c\u2510": GraphicsTopT,
"\u250c\u2514": GraphicsLeftT,
"\u250c\u2518": GraphicsCross,
"\u250c\u251c": GraphicsLeftT,
"\u250c\u2524": GraphicsCross,
"\u250c\u252c": GraphicsTopT,
"\u250c\u2534": GraphicsCross,
"\u250c\u253c": GraphicsCross,
"\u2510\u2514": GraphicsCross,
"\u2510\u2518": GraphicsRightT,
"\u2510\u251c": GraphicsCross,
"\u2510\u2524": GraphicsRightT,
"\u2510\u252c": GraphicsTopT,
"\u2510\u2534": GraphicsCross,
"\u2510\u253c": GraphicsCross,
"\u2514\u2518": GraphicsBottomT,
"\u2514\u251c": GraphicsLeftT,
"\u2514\u2524": GraphicsCross,
"\u2514\u252c": GraphicsCross,
"\u2514\u2534": GraphicsBottomT,
"\u2514\u253c": GraphicsCross,
"\u2518\u251c": GraphicsCross,
"\u2518\u2524": GraphicsRightT,
"\u2518\u252c": GraphicsCross,
"\u2518\u2534": GraphicsBottomT,
"\u2518\u253c": GraphicsCross,
"\u251c\u2524": GraphicsCross,
"\u251c\u252c": GraphicsCross,
"\u251c\u2534": GraphicsCross,
"\u251c\u253c": GraphicsCross,
"\u2524\u252c": GraphicsCross,
"\u2524\u2534": GraphicsCross,
"\u2524\u253c": GraphicsCross,
"\u252c\u2534": GraphicsCross,
"\u252c\u253c": GraphicsCross,
"\u2534\u253c": GraphicsCross,
}
// Common regular expressions. // Common regular expressions.
var ( var (
colorPattern = regexp.MustCompile(`\[([a-zA-Z]+|#[0-9a-zA-Z]{6}|\-)?(:([a-zA-Z]+|#[0-9a-zA-Z]{6}|\-)?(:([lbdru]+|\-)?)?)?\]`) colorPattern = regexp.MustCompile(`\[([a-zA-Z]+|#[0-9a-zA-Z]{6}|\-)?(:([a-zA-Z]+|#[0-9a-zA-Z]{6}|\-)?(:([lbdru]+|\-)?)?)?\]`)
regionPattern = regexp.MustCompile(`\["([a-zA-Z0-9_,;: \-\.]*)"\]`) regionPattern = regexp.MustCompile(`\["([a-zA-Z0-9_,;: \-\.]*)"\]`)
escapePattern = regexp.MustCompile(`\[([a-zA-Z0-9_,;: \-\."#]+)\[(\[*)\]`) escapePattern = regexp.MustCompile(`\[([a-zA-Z0-9_,;: \-\."#]+)\[(\[*)\]`)
nonEscapePattern = regexp.MustCompile(`(\[[a-zA-Z0-9_,;: \-\."#]+\[*)\]`) nonEscapePattern = regexp.MustCompile(`(\[[a-zA-Z0-9_,;: \-\."#]+\[*)\]`)
boundaryPattern = regexp.MustCompile("([[:punct:]]\\s*|\\s+)") boundaryPattern = regexp.MustCompile(`(([[:punct:]]|\n)[ \t\f\r]*|(\s+))`)
spacePattern = regexp.MustCompile(`\s+`) spacePattern = regexp.MustCompile(`\s+`)
) )
@ -204,13 +118,12 @@ func overlayStyle(background tcell.Color, defaultStyle tcell.Style, fgColor, bgC
defFg, defBg, defAttr := defaultStyle.Decompose() defFg, defBg, defAttr := defaultStyle.Decompose()
style := defaultStyle.Background(background) style := defaultStyle.Background(background)
if fgColor == "-" { style = style.Foreground(defFg)
style = style.Foreground(defFg) if fgColor != "" {
} else if fgColor != "" {
style = style.Foreground(tcell.GetColor(fgColor)) style = style.Foreground(tcell.GetColor(fgColor))
} }
if bgColor == "-" { if bgColor == "-" || bgColor == "" && defBg != tcell.ColorDefault {
style = style.Background(defBg) style = style.Background(defBg)
} else if bgColor != "" { } else if bgColor != "" {
style = style.Background(tcell.GetColor(bgColor)) style = style.Background(tcell.GetColor(bgColor))
@ -288,8 +201,8 @@ func decomposeString(text string) (colorIndices [][]int, colors [][]string, esca
// You can change the colors and text styles mid-text by inserting a color tag. // You can change the colors and text styles mid-text by inserting a color tag.
// See the package description for details. // See the package description for details.
// //
// Returns the number of actual runes printed (not including color tags) and the // Returns the number of actual bytes of the text printed (including color tags)
// actual width used for the printed runes. // and the actual width used for the printed runes.
func Print(screen tcell.Screen, text string, x, y, maxWidth, align int, color tcell.Color) (int, int) { func Print(screen tcell.Screen, text string, x, y, maxWidth, align int, color tcell.Color) (int, int) {
return printWithStyle(screen, text, x, y, maxWidth, align, tcell.StyleDefault.Foreground(color)) return printWithStyle(screen, text, x, y, maxWidth, align, tcell.StyleDefault.Foreground(color))
} }
@ -302,115 +215,136 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int,
} }
// Decompose the text. // Decompose the text.
colorIndices, colors, escapeIndices, strippedText, _ := decomposeString(text) colorIndices, colors, escapeIndices, strippedText, strippedWidth := decomposeString(text)
// We deal with runes, not with bytes. // We want to reduce all alignments to AlignLeft.
runes := []rune(strippedText) if align == AlignRight {
if strippedWidth <= maxWidth {
// This helper function takes positions for a substring of "runes" and returns // There's enough space for the entire text.
// a new string corresponding to this substring, making sure printing that return printWithStyle(screen, text, x+maxWidth-strippedWidth, y, maxWidth, AlignLeft, style)
// substring will observe color tags. }
substring := func(from, to int) string { // Trim characters off the beginning.
var ( var (
colorPos, escapePos, runePos, startPos int bytes, width, colorPos, escapePos, tagOffset int
foregroundColor, backgroundColor, attributes string foregroundColor, backgroundColor, attributes string
) )
if from >= len(runes) { _, originalBackground, _ := style.Decompose()
return "" iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
} // Update color/escape tag offset and style.
for pos := range text { if colorPos < len(colorIndices) && textPos+tagOffset >= colorIndices[colorPos][0] && textPos+tagOffset < colorIndices[colorPos][1] {
// Handle color tags. foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos])
if colorPos < len(colorIndices) && pos >= colorIndices[colorPos][0] && pos < colorIndices[colorPos][1] { style = overlayStyle(originalBackground, style, foregroundColor, backgroundColor, attributes)
if pos == colorIndices[colorPos][1]-1 { tagOffset += colorIndices[colorPos][1] - colorIndices[colorPos][0]
if runePos <= from { colorPos++
foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos]) }
} if escapePos < len(escapeIndices) && textPos+tagOffset >= escapeIndices[escapePos][0] && textPos+tagOffset < escapeIndices[escapePos][1] {
colorPos++ tagOffset++
escapePos++
}
if strippedWidth-screenPos < maxWidth {
// We chopped off enough.
if escapePos > 0 && textPos+tagOffset-1 >= escapeIndices[escapePos-1][0] && textPos+tagOffset-1 < escapeIndices[escapePos-1][1] {
// Unescape open escape sequences.
escapeCharPos := escapeIndices[escapePos-1][1] - 2
text = text[:escapeCharPos] + text[escapeCharPos+1:]
} }
continue // Print and return.
bytes, width = printWithStyle(screen, text[textPos+tagOffset:], x, y, maxWidth, AlignLeft, style)
return true
} }
return false
// Handle escape tags. })
if escapePos < len(escapeIndices) && pos >= escapeIndices[escapePos][0] && pos < escapeIndices[escapePos][1] { return bytes, width
if pos == escapeIndices[escapePos][1]-1 {
escapePos++
} else if pos == escapeIndices[escapePos][1]-2 {
continue
}
}
// Check boundaries.
if runePos == from {
startPos = pos
} else if runePos >= to {
return fmt.Sprintf(`[%s:%s:%s]%s`, foregroundColor, backgroundColor, attributes, text[startPos:pos])
}
runePos++
}
return fmt.Sprintf(`[%s:%s:%s]%s`, foregroundColor, backgroundColor, attributes, text[startPos:])
}
// We want to reduce everything to AlignLeft.
if align == AlignRight {
width := 0
start := len(runes)
for index := start - 1; index >= 0; index-- {
w := runewidth.RuneWidth(runes[index])
if width+w > maxWidth {
break
}
width += w
start = index
}
for start < len(runes) && runewidth.RuneWidth(runes[start]) == 0 {
start++
}
return printWithStyle(screen, substring(start, len(runes)), x+maxWidth-width, y, width, AlignLeft, style)
} else if align == AlignCenter { } else if align == AlignCenter {
width := runewidth.StringWidth(strippedText) if strippedWidth == maxWidth {
if width == maxWidth {
// Use the exact space. // Use the exact space.
return printWithStyle(screen, text, x, y, maxWidth, AlignLeft, style) return printWithStyle(screen, text, x, y, maxWidth, AlignLeft, style)
} else if width < maxWidth { } else if strippedWidth < maxWidth {
// We have more space than we need. // We have more space than we need.
half := (maxWidth - width) / 2 half := (maxWidth - strippedWidth) / 2
return printWithStyle(screen, text, x+half, y, maxWidth-half, AlignLeft, style) return printWithStyle(screen, text, x+half, y, maxWidth-half, AlignLeft, style)
} else { } else {
// Chop off runes until we have a perfect fit. // Chop off runes until we have a perfect fit.
var choppedLeft, choppedRight, leftIndex, rightIndex int var choppedLeft, choppedRight, leftIndex, rightIndex int
rightIndex = len(runes) - 1 rightIndex = len(strippedText)
for rightIndex > leftIndex && width-choppedLeft-choppedRight > maxWidth { for rightIndex-1 > leftIndex && strippedWidth-choppedLeft-choppedRight > maxWidth {
if choppedLeft < choppedRight { if choppedLeft < choppedRight {
leftWidth := runewidth.RuneWidth(runes[leftIndex]) // Iterate on the left by one character.
choppedLeft += leftWidth iterateString(strippedText[leftIndex:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
leftIndex++ choppedLeft += screenWidth
for leftIndex < len(runes) && leftIndex < rightIndex && runewidth.RuneWidth(runes[leftIndex]) == 0 { leftIndex += textWidth
leftIndex++ return true
} })
} else { } else {
rightWidth := runewidth.RuneWidth(runes[rightIndex]) // Iterate on the right by one character.
choppedRight += rightWidth iterateStringReverse(strippedText[leftIndex:rightIndex], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
rightIndex-- choppedRight += screenWidth
rightIndex -= textWidth
return true
})
} }
} }
return printWithStyle(screen, substring(leftIndex, rightIndex), x, y, maxWidth, AlignLeft, style)
// Add tag offsets and determine start style.
var (
colorPos, escapePos, tagOffset int
foregroundColor, backgroundColor, attributes string
)
_, originalBackground, _ := style.Decompose()
for index := range strippedText {
// We only need the offset of the left index.
if index > leftIndex {
// We're done.
if escapePos > 0 && leftIndex+tagOffset-1 >= escapeIndices[escapePos-1][0] && leftIndex+tagOffset-1 < escapeIndices[escapePos-1][1] {
// Unescape open escape sequences.
escapeCharPos := escapeIndices[escapePos-1][1] - 2
text = text[:escapeCharPos] + text[escapeCharPos+1:]
}
break
}
// Update color/escape tag offset.
if colorPos < len(colorIndices) && index+tagOffset >= colorIndices[colorPos][0] && index+tagOffset < colorIndices[colorPos][1] {
if index <= leftIndex {
foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos])
style = overlayStyle(originalBackground, style, foregroundColor, backgroundColor, attributes)
}
tagOffset += colorIndices[colorPos][1] - colorIndices[colorPos][0]
colorPos++
}
if escapePos < len(escapeIndices) && index+tagOffset >= escapeIndices[escapePos][0] && index+tagOffset < escapeIndices[escapePos][1] {
tagOffset++
escapePos++
}
}
return printWithStyle(screen, text[leftIndex+tagOffset:], x, y, maxWidth, AlignLeft, style)
} }
} }
// Draw text. // Draw text.
drawn := 0
drawnWidth := 0
var ( var (
colorPos, escapePos int drawn, drawnWidth, colorPos, escapePos, tagOffset int
foregroundColor, backgroundColor, attributes string foregroundColor, backgroundColor, attributes string
) )
runeSequence := make([]rune, 0, 10) iterateString(strippedText, func(main rune, comb []rune, textPos, length, screenPos, screenWidth int) bool {
runeSeqWidth := 0 // Only continue if there is still space.
flush := func() { if drawnWidth+screenWidth > maxWidth {
if len(runeSequence) == 0 { return true
return // Nothing to flush. }
// Handle color tags.
if colorPos < len(colorIndices) && textPos+tagOffset >= colorIndices[colorPos][0] && textPos+tagOffset < colorIndices[colorPos][1] {
foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos])
tagOffset += colorIndices[colorPos][1] - colorIndices[colorPos][0]
colorPos++
}
// Handle scape tags.
if escapePos < len(escapeIndices) && textPos+tagOffset >= escapeIndices[escapePos][0] && textPos+tagOffset < escapeIndices[escapePos][1] {
if textPos+tagOffset == escapeIndices[escapePos][1]-2 {
tagOffset++
escapePos++
}
} }
// Print the rune sequence. // Print the rune sequence.
@ -418,70 +352,23 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int,
_, _, finalStyle, _ := screen.GetContent(finalX, y) _, _, finalStyle, _ := screen.GetContent(finalX, y)
_, background, _ := finalStyle.Decompose() _, background, _ := finalStyle.Decompose()
finalStyle = overlayStyle(background, style, foregroundColor, backgroundColor, attributes) finalStyle = overlayStyle(background, style, foregroundColor, backgroundColor, attributes)
var comb []rune for offset := screenWidth - 1; offset >= 0; offset-- {
if len(runeSequence) > 1 { // To avoid undesired effects, we populate all cells.
// Allocate space for the combining characters only when necessary. if offset == 0 {
comb = make([]rune, len(runeSequence)-1) screen.SetContent(finalX+offset, y, main, comb, finalStyle)
copy(comb, runeSequence[1:]) } else {
} screen.SetContent(finalX+offset, y, ' ', nil, finalStyle)
for offset := 0; offset < runeSeqWidth; offset++ {
// To avoid undesired effects, we place the same character in all cells.
screen.SetContent(finalX+offset, y, runeSequence[0], comb, finalStyle)
}
// Advance and reset.
drawn += len(runeSequence)
drawnWidth += runeSeqWidth
runeSequence = runeSequence[:0]
runeSeqWidth = 0
}
for pos, ch := range text {
// Handle color tags.
if colorPos < len(colorIndices) && pos >= colorIndices[colorPos][0] && pos < colorIndices[colorPos][1] {
flush()
if pos == colorIndices[colorPos][1]-1 {
foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos])
colorPos++
}
continue
}
// Handle escape tags.
if escapePos < len(escapeIndices) && pos >= escapeIndices[escapePos][0] && pos < escapeIndices[escapePos][1] {
flush()
if pos == escapeIndices[escapePos][1]-1 {
escapePos++
} else if pos == escapeIndices[escapePos][1]-2 {
continue
} }
} }
// Check if we have enough space for this rune. // Advance.
chWidth := runewidth.RuneWidth(ch) drawn += length
if drawnWidth+chWidth > maxWidth { drawnWidth += screenWidth
break // No. We're done then.
}
// Put this rune in the queue. return false
if chWidth == 0 { })
// If this is not a modifier, we treat it as a space character.
if len(runeSequence) == 0 {
ch = ' '
chWidth = 1
}
} else {
// We have a character. Flush all previous runes.
flush()
}
runeSequence = append(runeSequence, ch)
runeSeqWidth += chWidth
} return drawn + tagOffset + len(escapeIndices), drawnWidth
if drawnWidth+runeSeqWidth <= maxWidth {
flush()
}
return drawn, drawnWidth
} }
// PrintSimple prints white text to the screen at the given position. // PrintSimple prints white text to the screen at the given position.
@ -507,133 +394,88 @@ func WordWrap(text string, width int) (lines []string) {
colorTagIndices, _, escapeIndices, strippedText, _ := decomposeString(text) colorTagIndices, _, escapeIndices, strippedText, _ := decomposeString(text)
// Find candidate breakpoints. // Find candidate breakpoints.
breakPoints := boundaryPattern.FindAllStringIndex(strippedText, -1) breakpoints := boundaryPattern.FindAllStringSubmatchIndex(strippedText, -1)
// Results in one entry for each candidate. Each entry is an array a of
// indices into strippedText where a[6] < 0 for newline/punctuation matches
// and a[4] < 0 for whitespace matches.
// This helper function adds a new line to the result slice. The provided // Process stripped text one character at a time.
// positions are in stripped index space. var (
addLine := func(from, to int) { colorPos, escapePos, breakpointPos, tagOffset int
// Shift indices back to original index space. lastBreakpoint, lastContinuation, currentLineStart int
var colorTagIndex, escapeIndex int lineWidth, continuationWidth int
for colorTagIndex < len(colorTagIndices) && to >= colorTagIndices[colorTagIndex][0] || newlineBreakpoint bool
escapeIndex < len(escapeIndices) && to >= escapeIndices[escapeIndex][0] { )
past := 0 unescape := func(substr string, startIndex int) string {
if colorTagIndex < len(colorTagIndices) { // A helper function to unescape escaped tags.
tagWidth := colorTagIndices[colorTagIndex][1] - colorTagIndices[colorTagIndex][0] for index := escapePos; index >= 0; index-- {
if colorTagIndices[colorTagIndex][0] < from { if index < len(escapeIndices) && startIndex > escapeIndices[index][0] && startIndex < escapeIndices[index][1]-1 {
from += tagWidth pos := escapeIndices[index][1] - 2 - startIndex
to += tagWidth return substr[:pos] + substr[pos+1:]
colorTagIndex++
} else if colorTagIndices[colorTagIndex][0] < to {
to += tagWidth
colorTagIndex++
} else {
past++
}
} else {
past++
}
if escapeIndex < len(escapeIndices) {
tagWidth := escapeIndices[escapeIndex][1] - escapeIndices[escapeIndex][0]
if escapeIndices[escapeIndex][0] < from {
from += tagWidth
to += tagWidth
escapeIndex++
} else if escapeIndices[escapeIndex][0] < to {
to += tagWidth
escapeIndex++
} else {
past++
}
} else {
past++
}
if past == 2 {
break // All other indices are beyond the requested string.
} }
} }
lines = append(lines, text[from:to]) return substr
} }
iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
// Determine final breakpoints. // Handle colour tags.
var start, lastEnd, newStart, breakPoint int if colorPos < len(colorTagIndices) && textPos+tagOffset >= colorTagIndices[colorPos][0] && textPos+tagOffset < colorTagIndices[colorPos][1] {
for { tagOffset += colorTagIndices[colorPos][1] - colorTagIndices[colorPos][0]
// What's our candidate string? colorPos++
var candidate string
if breakPoint < len(breakPoints) {
candidate = text[start:breakPoints[breakPoint][1]]
} else {
candidate = text[start:]
} }
candidate = strings.TrimRightFunc(candidate, unicode.IsSpace)
if runewidth.StringWidth(candidate) >= width { // Handle escape tags.
// We're past the available width. if escapePos < len(escapeIndices) && textPos+tagOffset == escapeIndices[escapePos][1]-2 {
if lastEnd > start { tagOffset++
// Use the previous candidate. escapePos++
addLine(start, lastEnd)
start = newStart
} else {
// We have no previous candidate. Make a hard break.
var lineWidth int
for index, ch := range text {
if index < start {
continue
}
chWidth := runewidth.RuneWidth(ch)
if lineWidth > 0 && lineWidth+chWidth >= width {
addLine(start, index)
start = index
break
}
lineWidth += chWidth
}
}
} else {
// We haven't hit the right border yet.
if breakPoint >= len(breakPoints) {
// It's the last line. We're done.
if len(candidate) > 0 {
addLine(start, len(strippedText))
}
break
} else {
// We have a new candidate.
lastEnd = start + len(candidate)
newStart = breakPoints[breakPoint][1]
breakPoint++
}
} }
// Check if a break is warranted.
afterContinuation := lastContinuation > 0 && textPos+tagOffset >= lastContinuation
noBreakpoint := lastContinuation == 0
beyondWidth := lineWidth > 0 && lineWidth > width
if beyondWidth && noBreakpoint {
// We need a hard break without a breakpoint.
lines = append(lines, unescape(text[currentLineStart:textPos+tagOffset], currentLineStart))
currentLineStart = textPos + tagOffset
lineWidth = continuationWidth
} else if afterContinuation && (beyondWidth || newlineBreakpoint) {
// Break at last breakpoint or at newline.
lines = append(lines, unescape(text[currentLineStart:lastBreakpoint], currentLineStart))
currentLineStart = lastContinuation
lineWidth = continuationWidth
lastBreakpoint, lastContinuation, newlineBreakpoint = 0, 0, false
}
// Is this a breakpoint?
if breakpointPos < len(breakpoints) && textPos == breakpoints[breakpointPos][0] {
// Yes, it is. Set up breakpoint infos depending on its type.
lastBreakpoint = breakpoints[breakpointPos][0] + tagOffset
lastContinuation = breakpoints[breakpointPos][1] + tagOffset
newlineBreakpoint = main == '\n'
if breakpoints[breakpointPos][6] < 0 && !newlineBreakpoint {
lastBreakpoint++ // Don't skip punctuation.
}
breakpointPos++
}
// Once we hit the continuation point, we start buffering widths.
if textPos+tagOffset < lastContinuation {
continuationWidth = 0
}
lineWidth += screenWidth
continuationWidth += screenWidth
return false
})
// Flush the rest.
if currentLineStart < len(text) {
lines = append(lines, unescape(text[currentLineStart:], currentLineStart))
} }
return return
} }
// PrintJoinedBorder prints a border graphics rune into the screen at the given
// position with the given color, joining it with any existing border graphics
// rune. Background colors are preserved. At this point, only regular single
// line borders are supported.
func PrintJoinedBorder(screen tcell.Screen, x, y int, ch rune, color tcell.Color) {
previous, _, style, _ := screen.GetContent(x, y)
style = style.Foreground(color)
// What's the resulting rune?
var result rune
if ch == previous {
result = ch
} else {
if ch < previous {
previous, ch = ch, previous
}
result = joints[string(previous)+string(ch)]
}
if result == 0 {
result = ch
}
// We only print something if we have something.
screen.SetContent(x, y, result, nil, style)
}
// Escape escapes the given text such that color and/or region tags are not // Escape escapes the given text such that color and/or region tags are not
// recognized and substituted by the print functions of this package. For // recognized and substituted by the print functions of this package. For
// example, to include a tag-like string in a box title or in a TextView: // example, to include a tag-like string in a box title or in a TextView:
@ -643,3 +485,121 @@ func PrintJoinedBorder(screen tcell.Screen, x, y int, ch rune, color tcell.Color
func Escape(text string) string { func Escape(text string) string {
return nonEscapePattern.ReplaceAllString(text, "$1[]") return nonEscapePattern.ReplaceAllString(text, "$1[]")
} }
// iterateString iterates through the given string one printed character at a
// time. For each such character, the callback function is called with the
// Unicode code points of the character (the first rune and any combining runes
// which may be nil if there aren't any), the starting position (in bytes)
// within the original string, its length in bytes, the screen position of the
// character, and the screen width of it. The iteration stops if the callback
// returns true. This function returns true if the iteration was stopped before
// the last character.
func iterateString(text string, callback func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool) bool {
var (
runes []rune
lastZeroWidthJoiner bool
startIndex int
startPos int
pos int
)
// Helper function which invokes the callback.
flush := func(index int) bool {
var comb []rune
if len(runes) > 1 {
comb = runes[1:]
}
return callback(runes[0], comb, startIndex, index-startIndex, startPos, pos-startPos)
}
for index, r := range text {
if unicode.In(r, unicode.Lm, unicode.M) || r == '\u200d' {
lastZeroWidthJoiner = r == '\u200d'
} else {
// We have a rune that's not a modifier. It could be the beginning of a
// new character.
if !lastZeroWidthJoiner {
if len(runes) > 0 {
// It is. Invoke callback.
if flush(index) {
return true // We're done.
}
// Reset rune store.
runes = runes[:0]
startIndex = index
startPos = pos
}
pos += runewidth.RuneWidth(r)
} else {
lastZeroWidthJoiner = false
}
}
runes = append(runes, r)
}
// Flush any remaining runes.
if len(runes) > 0 {
flush(len(text))
}
return false
}
// iterateStringReverse iterates through the given string in reverse, starting
// from the end of the string, one printed character at a time. For each such
// character, the callback function is called with the Unicode code points of
// the character (the first rune and any combining runes which may be nil if
// there aren't any), the starting position (in bytes) within the original
// string, its length in bytes, the screen position of the character, and the
// screen width of it. The iteration stops if the callback returns true. This
// function returns true if the iteration was stopped before the last character.
func iterateStringReverse(text string, callback func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool) bool {
type runePos struct {
r rune
pos int // The byte position of the rune in the original string.
width int // The screen width of the rune.
mod bool // Modifier or zero-width-joiner.
}
// We use the following:
// len(text) >= number of runes in text.
// Put all runes into a runePos slice in reverse.
runesReverse := make([]runePos, len(text))
index := len(text) - 1
for pos, ch := range text {
runesReverse[index].r = ch
runesReverse[index].pos = pos
runesReverse[index].width = runewidth.RuneWidth(ch)
runesReverse[index].mod = unicode.In(ch, unicode.Lm, unicode.M) || ch == '\u200d'
index--
}
runesReverse = runesReverse[index+1:]
// Parse reverse runes.
var screenWidth int
buffer := make([]rune, len(text)) // We fill this up from the back so it's forward again.
bufferPos := len(text)
stringWidth := runewidth.StringWidth(text)
for index, r := range runesReverse {
// Put this rune into the buffer.
bufferPos--
buffer[bufferPos] = r.r
// Do we need to flush the buffer?
if r.pos == 0 || !r.mod && runesReverse[index+1].r != '\u200d' {
// Yes, invoke callback.
var comb []rune
if len(text)-bufferPos > 1 {
comb = buffer[bufferPos+1:]
}
if callback(r.r, comb, r.pos, len(text)-r.pos, stringWidth-screenWidth, r.width) {
return true
}
screenWidth += r.width
bufferPos = len(text)
}
}
return false
}