diff --git a/Gopkg.lock b/Gopkg.lock index 25c518f..5e46ec5 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -2,79 +2,104 @@ [[projects]] + digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec" name = "github.com/davecgh/go-spew" packages = ["spew"] + pruneopts = "UT" revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73" version = "v1.1.1" [[projects]] + digest = "1:2694597a20d303502ad1b7f1cc688977c7dc34da732ccf4d27e98b552cceed5b" name = "github.com/disintegration/imaging" packages = ["."] + pruneopts = "UT" revision = "0bd5694c78c9c3d9a3cd06a706a8f3c59296a9ac" version = "v1.5.0" [[projects]] branch = "master" + digest = "1:40d0056c1b1f503c366ba441df92a82b5a2654d6f3747b1689a611eb5c9ce0a2" name = "github.com/gdamore/encoding" packages = ["."] + pruneopts = "UT" revision = "b23993cbb6353f0e6aa98d0ee318a34728f628b9" [[projects]] + digest = "1:f4bd9b6cfcbaafa94259861747781022d8f06ff3c88aafe49a815ac02609bc96" name = "github.com/kyokomi/emoji" packages = ["."] + pruneopts = "UT" revision = "2e9a9507333f3ee28f3fab88c2c3aba34455d734" version = "v1.5.1" [[projects]] + digest = "1:c65a16ac77d0b1aefc7009cabb6ac5ad05def02025f5be85f450c03f52cc6f86" name = "github.com/lucasb-eyer/go-colorful" packages = ["."] + pruneopts = "UT" revision = "345fbb3dbcdb252d9985ee899a84963c0fa24c82" version = "v1.0" [[projects]] + digest = "1:cdb899c199f907ac9fb50495ec71212c95cb5b0e0a8ee0800da0238036091033" name = "github.com/mattn/go-runewidth" packages = ["."] + pruneopts = "UT" revision = "ce7b0b5c7b45a81508558cd1dba6bb1e4ddb51bb" version = "v0.0.3" [[projects]] branch = "master" + digest = "1:0e1e5f960c58fdc677212fcc70e55042a0084d367623e51afbdb568963832f5d" name = "github.com/nu7hatch/gouuid" packages = ["."] + pruneopts = "UT" revision = "179d4d0c4d8d407a32af483c2354df1d2c91e6c3" [[projects]] + digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe" name = "github.com/pmezard/go-difflib" packages = ["difflib"] + pruneopts = "UT" revision = "792786c7400a136282c1664665ae0a8db921c6c2" version = "v1.0.0" [[projects]] + branch = "master" + digest = "1:3fd3d634f6815f19ac4b2c5e16d28ec9aa4584d0bba25d1ee6c424d813cca22a" name = "github.com/renstrom/fuzzysearch" packages = ["fuzzy"] + pruneopts = "UT" revision = "b18e754edff4833912ef4dce9eaca885bd3f0de1" - version = "v1.0.1" [[projects]] branch = "master" + digest = "1:def689e73e9252f6f7fe66834a76751a41b767e03daab299e607e7226c58a855" name = "github.com/shurcooL/sanitized_anchor_name" packages = ["."] + pruneopts = "UT" revision = "86672fcb3f950f35f2e675df2240550f2a50762f" [[projects]] + digest = "1:18752d0b95816a1b777505a97f71c7467a8445b8ffb55631a7bf779f6ba4fa83" name = "github.com/stretchr/testify" packages = ["assert"] + pruneopts = "UT" revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686" version = "v1.2.2" [[projects]] branch = "master" + digest = "1:22738a5261588086d75acb3c792f7c931d30fd32e351117e9e046331135f5297" name = "github.com/zyedidia/clipboard" packages = ["."] + pruneopts = "UT" revision = "bd31d747117d04b4e25b61f73e1ea4faeea3c56a" [[projects]] branch = "master" + digest = "1:441333ba1f7f3074035297647244e3d1ce3665e4e7c5054b3a8b138463f8ff58" name = "golang.org/x/image" packages = [ "bmp", @@ -83,79 +108,108 @@ "tiff/lzw", "vp8", "vp8l", - "webp" + "webp", ] - revision = "c73c2afc3b812cdd6385de5a50616511c4a3d458" + pruneopts = "UT" + revision = "249dc8530c0efc2766606a940c1c50b434b2f1cd" [[projects]] branch = "master" + digest = "1:1a1ecfa7b54ca3f7a0115ab5c578d7d6a5d8b605839c549e80260468c42f8be7" name = "golang.org/x/net" packages = [ "html", - "html/atom" + "html/atom", ] - revision = "8a410e7b638dca158bf9e766925842f6651ff828" + pruneopts = "UT" + revision = "88d92db4c548972d942ac2a3531a8a9a34c82ca6" [[projects]] + digest = "1:37672ad5821719e2df8509c2edd4ba5ae192463237c73c3a2d24ef8b2bc9e36f" name = "golang.org/x/text" packages = [ "encoding", "encoding/internal/identifier", "internal/gen", "transform", - "unicode/cldr" + "unicode/cldr", ] + pruneopts = "UT" revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" version = "v0.3.0" [[projects]] + digest = "1:2ee0f15eb0fb04f918db7c2dcf39745f40d69f798ef171610a730e8a56aaa4fd" name = "gopkg.in/russross/blackfriday.v2" packages = ["."] - revision = "cadec560ec52d93835bf2f15bd794700d3a2473b" - version = "v2.0.0" + pruneopts = "UT" + revision = "d3b5b032dc8e8927d31a5071b56e14c89f045135" + version = "v2.0.1" [[projects]] branch = "v1" + digest = "1:b71ccd8615521db4ea2f9381f55780a1312654f5d8431d2e58eca51c74110de2" name = "gopkg.in/toast.v1" packages = ["."] + pruneopts = "UT" revision = "0a84660828b24d25b35525c9a1f1f51267f8da91" [[projects]] + digest = "1:342378ac4dcb378a5448dd723f0784ae519383532f5e70ade24132c4c8693202" name = "gopkg.in/yaml.v2" packages = ["."] + pruneopts = "UT" revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183" version = "v2.2.1" [[projects]] branch = "master" - name = "maunium.net/go/gomatrix" + digest = "1:d23e0bbfa4e45fe847e61fdfd442b24adf9bd3d0b050a846f9ef0f3af4f43c86" + name = "maunium.net/go/mautrix" packages = ["."] - revision = "d651abc3ecb4bbd85a6b17b710632e73ddbbc6aa" - -[[projects]] - name = "maunium.net/go/maulogger" - packages = ["."] - revision = "64f0aa33b6c51313e15575257db71dec44fe7988" - version = "v1.0" + pruneopts = "UT" + revision = "e8080dcf484d1db9021d2019fee132ffc9e37e3c" [[projects]] branch = "master" + digest = "1:28d6d6123ce29c9b0b5d393af0085f1454a9d93d70b4c9893efe6cff025702b9" name = "maunium.net/go/tcell" packages = [ ".", - "terminfo" + "terminfo", ] - revision = "f32e44a866ab0475655e11ede91337d7b562956d" + pruneopts = "UT" + revision = "a62189e4543d731b94ae0d3b659d1853975ea657" [[projects]] branch = "master" + digest = "1:b4c2e264221b6afa98aec4e0a097f3775cb014eebac8a6f49d33c53b237b77af" name = "maunium.net/go/tview" packages = ["."] - revision = "8b261597bbdb95dcaef03854aaa0cc192f56b1ff" + pruneopts = "UT" + revision = "dae31f32cda75339f5ec2a64411ef3ec1862ef5e" [solve-meta] analyzer-name = "dep" 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-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 5927463..2fa3148 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -63,7 +63,7 @@ [[constraint]] branch = "master" - name = "maunium.net/go/gomatrix" + name = "maunium.net/go/mautrix" [[constraint]] branch = "master" diff --git a/README.md b/README.md index 79509d3..e39c33b 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ ![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. diff --git a/config/config.go b/config/config.go index e4fb743..8f45d76 100644 --- a/config/config.go +++ b/config/config.go @@ -25,7 +25,7 @@ import ( "strings" "gopkg.in/yaml.v2" - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" "maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/matrix/pushrules" "maunium.net/go/gomuks/matrix/rooms" @@ -289,12 +289,12 @@ func (config *Config) PutRoom(room *rooms.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.Room = room 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 } diff --git a/interface/matrix.go b/interface/matrix.go index 0538df9..01f224e 100644 --- a/interface/matrix.go +++ b/interface/matrix.go @@ -17,12 +17,12 @@ package ifc import ( - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" "maunium.net/go/gomuks/matrix/rooms" ) type MatrixContainer interface { - Client() *gomatrix.Client + Client() *mautrix.Client InitClient() error Initialized() bool @@ -33,14 +33,14 @@ type MatrixContainer interface { Logout() SendPreferencesToMatrix() - SendMessage(roomID string, msgtype gomatrix.MessageType, message string) (string, error) - SendMarkdownMessage(roomID string, msgtype gomatrix.MessageType, message string) (string, error) + SendMessage(roomID string, msgtype mautrix.MessageType, message string) (string, error) + SendMarkdownMessage(roomID string, msgtype mautrix.MessageType, message string) (string, error) SendTyping(roomID string, typing bool) MarkRead(roomID, eventID string) JoinRoom(roomID, server string) (*rooms.Room, 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 Download(mxcURL string) ([]byte, string, string, error) diff --git a/interface/ui.go b/interface/ui.go index 929dc65..39473f5 100644 --- a/interface/ui.go +++ b/interface/ui.go @@ -19,7 +19,7 @@ package ifc import ( "time" - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" "maunium.net/go/gomuks/matrix/pushrules" "maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/tcell" @@ -50,7 +50,7 @@ type MainView interface { UpdateTags(room *rooms.Room) 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) InitialSyncDone() @@ -73,8 +73,8 @@ type RoomView interface { SetTyping(users []string) UpdateUserList() - NewMessage(id, sender string, msgtype gomatrix.MessageType, text string, timestamp time.Time) Message - NewTempMessage(msgtype gomatrix.MessageType, text string) Message + NewMessage(id, sender string, msgtype mautrix.MessageType, text string, timestamp time.Time) Message + NewTempMessage(msgtype mautrix.MessageType, text string) Message AddMessage(message Message, direction MessageDirection) AddServiceMessage(message string) } @@ -111,8 +111,8 @@ type Message interface { SetID(id string) ID() string - SetType(msgtype gomatrix.MessageType) - Type() gomatrix.MessageType + SetType(msgtype mautrix.MessageType) + Type() mautrix.MessageType NotificationContent() string diff --git a/matrix/doc.go b/matrix/doc.go index 2cd54e2..f789895 100644 --- a/matrix/doc.go +++ b/matrix/doc.go @@ -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 diff --git a/matrix/matrix.go b/matrix/matrix.go index 060becd..cf9e463 100644 --- a/matrix/matrix.go +++ b/matrix/matrix.go @@ -34,20 +34,20 @@ import ( "encoding/json" "gopkg.in/russross/blackfriday.v2" - "maunium.net/go/gomatrix" "maunium.net/go/gomuks/config" "maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/lib/bfhtml" "maunium.net/go/gomuks/matrix/pushrules" "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. type Container struct { - client *gomatrix.Client + client *mautrix.Client syncer *GomuksSyncer gmx ifc.Gomuks ui ifc.GomuksUI @@ -69,12 +69,18 @@ func NewContainer(gmx ifc.Gomuks) *Container { return c } -// Client returns the underlying gomatrix Client. -func (c *Container) Client() *gomatrix.Client { +// Client returns the underlying mautrix Client. +func (c *Container) Client() *mautrix.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 { if len(c.config.HS) == 0 { return fmt.Errorf("no homeserver in config") @@ -92,10 +98,11 @@ func (c *Container) InitClient() 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 { return err } + c.client.Logger = mxLogger{} allowInsecure := len(os.Getenv("GOMUKS_ALLOW_INSECURE_CONNECTIONS")) > 0 if allowInsecure { @@ -112,14 +119,14 @@ func (c *Container) InitClient() error { 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 { return c.client != nil } // Login sends a password login request with the given username and password. 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", User: user, Password: password, @@ -175,7 +182,7 @@ func (c *Container) PushRules() *pushrules.PushRuleset { 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. func (c *Container) OnLogin() { @@ -185,15 +192,16 @@ func (c *Container) OnLogin() { debug.Print("Initializing syncer") c.syncer = NewGomuksSyncer(c.config) - c.syncer.OnEventType(gomatrix.EventMessage, c.HandleMessage) - c.syncer.OnEventType(gomatrix.StateMember, c.HandleMembership) - c.syncer.OnEventType(gomatrix.EphemeralEventReceipt, c.HandleReadReceipt) - c.syncer.OnEventType(gomatrix.EphemeralEventTyping, c.HandleTyping) - c.syncer.OnEventType(gomatrix.AccountDataDirectChats, c.HandleDirectChatInfo) - c.syncer.OnEventType(gomatrix.AccountDataPushRules, c.HandlePushRules) - c.syncer.OnEventType(gomatrix.AccountDataRoomTags, c.HandleTag) + c.syncer.OnEventType(mautrix.EventMessage, c.HandleMessage) + c.syncer.OnEventType(mautrix.StateMember, c.HandleMembership) + c.syncer.OnEventType(mautrix.EphemeralEventReceipt, c.HandleReadReceipt) + c.syncer.OnEventType(mautrix.EphemeralEventTyping, c.HandleTyping) + c.syncer.OnEventType(mautrix.AccountDataDirectChats, c.HandleDirectChatInfo) + c.syncer.OnEventType(mautrix.AccountDataPushRules, c.HandlePushRules) + c.syncer.OnEventType(mautrix.AccountDataRoomTags, c.HandleTag) c.syncer.OnEventType(AccountDataGomuksPreferences, c.HandlePreferences) c.syncer.InitDoneCallback = func() { + debug.Print("Initial sync done") c.config.AuthCache.InitialSyncDone = true c.config.SaveAuthCache() c.ui.MainView().InitialSyncDone() @@ -227,7 +235,7 @@ func (c *Container) Start() { return default: 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") c.Logout() } 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 rt, _ := json.Marshal(&evt.Content) 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. -func (c *Container) HandleMessage(source EventSource, evt *gomatrix.Event) { +func (c *Container) HandleMessage(source EventSource, evt *mautrix.Event) { if source&EventSourceLeave != 0 { 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. -func (c *Container) HandleMembership(source EventSource, evt *gomatrix.Event) { +func (c *Container) HandleMembership(source EventSource, evt *mautrix.Event) { isLeave := source&EventSourceLeave != 0 isTimeline := source&EventSourceTimeline != 0 isNonTimelineLeave := isLeave && !isTimeline @@ -302,9 +310,9 @@ func (c *Container) HandleMembership(source EventSource, evt *gomatrix.Event) { c.HandleMessage(source, evt) } -func (c *Container) processOwnMembershipChange(evt *gomatrix.Event) { +func (c *Container) processOwnMembershipChange(evt *mautrix.Event) { membership := evt.Content.Membership - prevMembership := gomatrix.MembershipLeave + prevMembership := mautrix.MembershipLeave if evt.Unsigned.PrevContent != nil { 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 for eventID, rawContent := range evt.Content.Raw { content, ok := rawContent.(map[string]interface{}) @@ -353,7 +361,7 @@ func (c *Container) parseReadReceipt(evt *gomatrix.Event) (largestTimestampEvent return } -func (c *Container) HandleReadReceipt(source EventSource, evt *gomatrix.Event) { +func (c *Container) HandleReadReceipt(source EventSource, evt *mautrix.Event) { if source&EventSourceLeave != 0 { return } @@ -368,7 +376,7 @@ func (c *Container) HandleReadReceipt(source EventSource, evt *gomatrix.Event) { 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) for _, rawRoomIDList := range evt.Content.Raw { roomIDList, ok := rawRoomIDList.([]interface{}) @@ -391,7 +399,7 @@ func (c *Container) parseDirectChatInfo(evt *gomatrix.Event) map[*rooms.Room]boo return directChats } -func (c *Container) HandleDirectChatInfo(source EventSource, evt *gomatrix.Event) { +func (c *Container) HandleDirectChatInfo(source EventSource, evt *mautrix.Event) { directChats := c.parseDirectChatInfo(evt) for _, room := range c.config.Rooms { 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. -func (c *Container) HandlePushRules(source EventSource, evt *gomatrix.Event) { +func (c *Container) HandlePushRules(source EventSource, evt *mautrix.Event) { debug.Print("Received updated push rules") var err error 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. -func (c *Container) HandleTag(source EventSource, evt *gomatrix.Event) { +func (c *Container) HandleTag(source EventSource, evt *mautrix.Event) { room := c.config.GetRoom(evt.RoomID) 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 { order := "0.5" if len(info.Order) > 0 { - order = info.Order + order = info.Order.String() } newTags[index] = rooms.RoomTag{ 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. -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) } @@ -448,11 +456,11 @@ func (c *Container) MarkRead(roomID, eventID string) { } // 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() c.SendTyping(roomID, false) - resp, err := c.client.SendMessageEvent(roomID, gomatrix.EventMessage, - gomatrix.Content{MsgType: msgtype, Body: text}) + resp, err := c.client.SendMessageEvent(roomID, mautrix.EventMessage, + mautrix.Content{MsgType: msgtype, Body: text}) if err != nil { 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. // 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() html := c.renderMarkdown(text) @@ -504,11 +512,11 @@ func (c *Container) SendMarkdownMessage(roomID string, msgtype gomatrix.MessageT text = roomRegex.ReplaceAllString(text, "$1") c.SendTyping(roomID, false) - resp, err := c.client.SendMessageEvent(roomID, gomatrix.EventMessage, - gomatrix.Content{ + resp, err := c.client.SendMessageEvent(roomID, mautrix.EventMessage, + mautrix.Content{ MsgType: msgtype, Body: text, - Format: gomatrix.FormatHTML, + Format: mautrix.FormatHTML, FormattedBody: html, }) if err != nil { @@ -560,7 +568,7 @@ func (c *Container) LeaveRoom(roomID string) error { } // 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) if err != nil { return nil, "", err diff --git a/matrix/matrix_test.go b/matrix/matrix_test.go index b6511ec..0d8bc3c 100644 --- a/matrix/matrix_test.go +++ b/matrix/matrix_test.go @@ -21,7 +21,7 @@ import ( "fmt" "github.com/stretchr/testify/assert" "io/ioutil" - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" "maunium.net/go/gomuks/config" "net/http" "os" @@ -80,7 +80,7 @@ func TestContainer_SendMarkdownMessage_WithMarkdown(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) { 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) @@ -91,7 +91,7 @@ func TestContainer_SendTyping(t *testing.T) { return nil, err } - call := gomatrix.ReqTyping{} + call := mautrix.ReqTyping{} err = json.Unmarshal(rawBody, &call) if err != nil { return nil, err @@ -187,8 +187,8 @@ func TestContainer_GetHistory(t *testing.T) { assert.Equal(t, "456", prevBatch) } -func mockClient(fn func(*http.Request) (*http.Response, error)) *gomatrix.Client { - client, _ := gomatrix.NewClient("https://example.com", "@user:example.com", "foobar") +func mockClient(fn func(*http.Request) (*http.Response, error)) *mautrix.Client { + client, _ := mautrix.NewClient("https://example.com", "@user:example.com", "foobar") client.Client = &http.Client{Transport: MockRoundTripper{RT: fn}} return client } diff --git a/matrix/pushrules/condition.go b/matrix/pushrules/condition.go index 22d59aa..bacab56 100644 --- a/matrix/pushrules/condition.go +++ b/matrix/pushrules/condition.go @@ -21,14 +21,14 @@ import ( "strconv" "strings" - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" "maunium.net/go/gomuks/lib/glob" ) // Room is an interface with the functions that are needed for processing room-specific push conditions type Room interface { - GetMember(mxid string) *gomatrix.Member - GetMembers() map[string]*gomatrix.Member + GetMember(mxid string) *mautrix.Member + GetMembers() map[string]*mautrix.Member GetSessionOwner() string } @@ -59,7 +59,7 @@ type PushCondition struct { var MemberCountFilterRegex = regexp.MustCompile("^(==|[<>]=?)?([0-9]+)$") // 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 { case KindEventMatch: 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, '.') key := cond.Key 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() if ownerID == event.Sender { return false @@ -115,7 +115,7 @@ func (cond *PushCondition) matchDisplayName(room Room, event *gomatrix.Event) bo 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) if len(group) != 3 { return false diff --git a/matrix/pushrules/condition_test.go b/matrix/pushrules/condition_test.go index 7fd06ee..750f2c7 100644 --- a/matrix/pushrules/condition_test.go +++ b/matrix/pushrules/condition_test.go @@ -21,7 +21,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" "maunium.net/go/gomuks/matrix/pushrules" "maunium.net/go/gomuks/matrix/rooms" ) @@ -30,7 +30,7 @@ var ( blankTestRoom *rooms.Room displaynameTestRoom pushrules.Room - countConditionTestEvent *gomatrix.Event + countConditionTestEvent *mautrix.Event displaynamePushCondition *pushrules.PushCondition ) @@ -38,7 +38,7 @@ var ( func init() { blankTestRoom = rooms.NewRoom("!fakeroom:maunium.net", "@tulir:maunium.net") - countConditionTestEvent = &gomatrix.Event{ + countConditionTestEvent = &mautrix.Event{ Sender: "@tulir:maunium.net", Type: "m.room.message", Timestamp: 1523791120, @@ -56,8 +56,8 @@ func init() { } } -func newFakeEvent(evtType string, content map[string]interface{}) *gomatrix.Event { - return &gomatrix.Event{ +func newFakeEvent(evtType string, content map[string]interface{}) *mautrix.Event { + return &mautrix.Event{ Sender: "@tulir:maunium.net", Type: evtType, Timestamp: 1523791120, diff --git a/matrix/pushrules/pushrules.go b/matrix/pushrules/pushrules.go index b383c66..643f2f2 100644 --- a/matrix/pushrules/pushrules.go +++ b/matrix/pushrules/pushrules.go @@ -4,16 +4,16 @@ import ( "encoding/json" "net/url" - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" ) // 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") } // 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)) // client.BuildURL returns the URL without a trailing slash, but the pushrules endpoint requires the slash. 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. -func EventToPushRules(event *gomatrix.Event) (*PushRuleset, error) { +func EventToPushRules(event *mautrix.Event) (*PushRuleset, error) { content := &contentWithRuleset{} err := json.Unmarshal(event.Content.VeryRaw, content) if err != nil { diff --git a/matrix/pushrules/pushrules_test.go b/matrix/pushrules/pushrules_test.go index 09698ac..73fa787 100644 --- a/matrix/pushrules/pushrules_test.go +++ b/matrix/pushrules/pushrules_test.go @@ -21,7 +21,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" "maunium.net/go/gomuks/matrix/pushrules" ) @@ -33,7 +33,7 @@ func init() { } func TestEventToPushRules(t *testing.T) { - event := &gomatrix.Event{ + event := &mautrix.Event{ Type: "m.push_rules", Timestamp: 1523380910, Content: mapExamplePushRules, diff --git a/matrix/pushrules/rule.go b/matrix/pushrules/rule.go index 71f71e5..62318ac 100644 --- a/matrix/pushrules/rule.go +++ b/matrix/pushrules/rule.go @@ -18,7 +18,7 @@ package pushrules import ( "encoding/gob" - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" "maunium.net/go/gomuks/lib/glob" ) @@ -28,7 +28,7 @@ func init() { } type PushRuleCollection interface { - GetActions(room Room, event *gomatrix.Event) PushActionArray + GetActions(room Room, event *mautrix.Event) PushActionArray } type PushRuleArray []*PushRule @@ -40,7 +40,7 @@ func (rules PushRuleArray) SetType(typ PushRuleType) PushRuleArray { 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 { if !rule.Match(room, event) { continue @@ -67,7 +67,7 @@ func (rules PushRuleArray) SetTypeAndMap(typ PushRuleType) PushRuleMap { 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 found bool switch ruleMap.Type { @@ -122,7 +122,7 @@ type PushRule struct { 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 { 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 { if !cond.Match(room, event) { return false @@ -149,7 +149,7 @@ func (rule *PushRule) matchConditions(room Room, event *gomatrix.Event) bool { 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) if err != nil { return false diff --git a/matrix/pushrules/ruleset.go b/matrix/pushrules/ruleset.go index 940025f..366702e 100644 --- a/matrix/pushrules/ruleset.go +++ b/matrix/pushrules/ruleset.go @@ -19,7 +19,7 @@ package pushrules import ( "encoding/json" - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" ) type PushRuleset struct { @@ -80,7 +80,7 @@ var DefaultPushActions = make(PushActionArray, 0) // GetActions matches the given event against all of the push rule // collections in this push ruleset in the order of priority as // 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 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. diff --git a/matrix/rooms/room.go b/matrix/rooms/room.go index bd233f8..72251c4 100644 --- a/matrix/rooms/room.go +++ b/matrix/rooms/room.go @@ -23,7 +23,7 @@ import ( "time" "encoding/gob" - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" "maunium.net/go/gomuks/debug" "os" ) @@ -58,7 +58,7 @@ type UnreadMessage struct { // Room represents a single Matrix room. type Room struct { - *gomatrix.Room + *mautrix.Room // Whether or not the user has left the room. HasLeft bool @@ -82,10 +82,10 @@ type Room struct { LastReceivedMessage time.Time // 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 same time as memberCache. - firstMemberCache *gomatrix.Member + firstMemberCache *mautrix.Member // The name of the room. Calculated from the state event name, // canonical_alias or alias or the member cache. 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 // 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] if !exists { - room.State[event.Type] = make(map[string]*gomatrix.Event) + room.State[event.Type] = make(map[string]*mautrix.Event) } switch event.Type { - case gomatrix.StateRoomName: + case mautrix.StateRoomName: room.nameCache = "" - case gomatrix.StateCanonicalAlias: + case mautrix.StateCanonicalAlias: if room.nameCacheSource >= CanonicalAliasRoomName { room.nameCache = "" } room.canonicalAliasCache = "" - case gomatrix.StateAliases: + case mautrix.StateAliases: if room.nameCacheSource >= AliasRoomName { room.nameCache = "" } room.aliasesCache = nil - case gomatrix.StateMember: + case mautrix.StateMember: room.memberCache = nil room.firstMemberCache = nil if room.nameCacheSource >= MemberRoomName { room.nameCache = "" } - case gomatrix.StateTopic: + case mautrix.StateTopic: room.topicCache = "" } @@ -248,7 +248,7 @@ func (room *Room) UpdateState(event *gomatrix.Event) { if event.StateKey != nil { 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) } @@ -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. -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] event, _ := stateEventMap[stateKey] return event } // 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] return stateEventMap } @@ -275,7 +275,7 @@ func (room *Room) GetStateEvents(eventType gomatrix.EventType) map[string]*gomat // GetTopic returns the topic of the room. func (room *Room) GetTopic() string { if len(room.topicCache) == 0 { - topicEvt := room.GetStateEvent(gomatrix.StateTopic, "") + topicEvt := room.GetStateEvent(mautrix.StateTopic, "") if topicEvt != nil { room.topicCache = topicEvt.Content.Topic } @@ -285,7 +285,7 @@ func (room *Room) GetTopic() string { func (room *Room) GetCanonicalAlias() string { if len(room.canonicalAliasCache) == 0 { - canonicalAliasEvt := room.GetStateEvent(gomatrix.StateCanonicalAlias, "") + canonicalAliasEvt := room.GetStateEvent(mautrix.StateCanonicalAlias, "") if canonicalAliasEvt != nil { room.canonicalAliasCache = canonicalAliasEvt.Content.Alias } else { @@ -301,7 +301,7 @@ func (room *Room) GetCanonicalAlias() string { // GetAliases returns the list of aliases that point to this room. func (room *Room) GetAliases() []string { if room.aliasesCache == nil { - aliasEvents := room.GetStateEvents(gomatrix.StateAliases) + aliasEvents := room.GetStateEvents(mautrix.StateAliases) room.aliasesCache = []string{} for _, event := range aliasEvents { 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. func (room *Room) updateNameFromNameEvent() { - nameEvt := room.GetStateEvent(gomatrix.StateRoomName, "") + nameEvt := room.GetStateEvent(mautrix.StateRoomName, "") if nameEvt != nil { 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. -func (room *Room) createMemberCache() map[string]*gomatrix.Member { - cache := make(map[string]*gomatrix.Member) - events := room.GetStateEvents(gomatrix.StateMember) +func (room *Room) createMemberCache() map[string]*mautrix.Member { + cache := make(map[string]*mautrix.Member) + events := room.GetStateEvents(mautrix.StateMember) room.firstMemberCache = nil if events != nil { for userID, event := range events { @@ -407,7 +407,7 @@ func (room *Room) createMemberCache() map[string]*gomatrix.Member { // // The members are returned from the cache. // 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 { room.createMemberCache() } @@ -416,7 +416,7 @@ func (room *Room) GetMembers() map[string]*gomatrix.Member { // GetMember returns the member with the given MXID. // 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 { room.createMemberCache() } @@ -432,7 +432,7 @@ func (room *Room) GetSessionOwner() string { // NewRoom creates a new Room with the given ID func NewRoom(roomID, owner string) *Room { return &Room{ - Room: gomatrix.NewRoom(roomID), + Room: mautrix.NewRoom(roomID), fetchHistoryLock: &sync.Mutex{}, SessionUserID: owner, } diff --git a/matrix/rooms/room_test.go b/matrix/rooms/room_test.go index 1fabdcc..db7f586 100644 --- a/matrix/rooms/room_test.go +++ b/matrix/rooms/room_test.go @@ -21,7 +21,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" "maunium.net/go/gomuks/matrix/rooms" ) @@ -39,7 +39,7 @@ func TestNewRoom_DefaultValues(t *testing.T) { func TestRoom_GetCanonicalAlias(t *testing.T) { room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net") - room.UpdateState(&gomatrix.Event{ + room.UpdateState(&mautrix.Event{ Type: "m.room.canonical_alias", Content: map[string]interface{}{ "alias": "#foo:maunium.net", @@ -50,7 +50,7 @@ func TestRoom_GetCanonicalAlias(t *testing.T) { func TestRoom_GetTopic(t *testing.T) { room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net") - room.UpdateState(&gomatrix.Event{ + room.UpdateState(&mautrix.Event{ Type: "m.room.topic", Content: map[string]interface{}{ "topic": "test topic", @@ -87,7 +87,7 @@ func TestRoom_GetAliases(t *testing.T) { } func addName(room *rooms.Room) { - room.UpdateState(&gomatrix.Event{ + room.UpdateState(&mautrix.Event{ Type: "m.room.name", Content: map[string]interface{}{ "name": "Test room", @@ -96,7 +96,7 @@ func addName(room *rooms.Room) { } func addCanonicalAlias(room *rooms.Room) { - room.UpdateState(&gomatrix.Event{ + room.UpdateState(&mautrix.Event{ Type: "m.room.canonical_alias", Content: map[string]interface{}{ "alias": "#foo:maunium.net", @@ -106,7 +106,7 @@ func addCanonicalAlias(room *rooms.Room) { func addAliases(room *rooms.Room) { server1 := "maunium.net" - room.UpdateState(&gomatrix.Event{ + room.UpdateState(&mautrix.Event{ Type: "m.room.aliases", StateKey: &server1, Content: map[string]interface{}{ @@ -115,7 +115,7 @@ func addAliases(room *rooms.Room) { }) server2 := "matrix.org" - room.UpdateState(&gomatrix.Event{ + room.UpdateState(&mautrix.Event{ Type: "m.room.aliases", StateKey: &server2, Content: map[string]interface{}{ @@ -126,7 +126,7 @@ func addAliases(room *rooms.Room) { func addMembers(room *rooms.Room, count int) { user1 := "@tulir:maunium.net" - room.UpdateState(&gomatrix.Event{ + room.UpdateState(&mautrix.Event{ Type: "m.room.member", StateKey: &user1, Content: map[string]interface{}{ @@ -146,7 +146,7 @@ func addMembers(room *rooms.Room, count int) { if i%5 == 0 { content["membership"] = "invite" } - room.UpdateState(&gomatrix.Event{ + room.UpdateState(&mautrix.Event{ Type: "m.room.member", StateKey: &userN, Content: content, diff --git a/matrix/sync.go b/matrix/sync.go index 6d5def7..06c7f47 100644 --- a/matrix/sync.go +++ b/matrix/sync.go @@ -14,15 +14,16 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -// 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 import ( "encoding/json" + "maunium.net/go/gomuks/debug" "time" - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" "maunium.net/go/gomuks/matrix/rooms" ) @@ -44,14 +45,14 @@ const ( 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 // 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. type GomuksSyncer struct { Session SyncerSession - listeners map[gomatrix.EventType][]EventHandler // event type to listeners array + listeners map[mautrix.EventType][]EventHandler // event type to listeners array FirstSyncDone bool InitDoneCallback func() } @@ -60,13 +61,14 @@ type GomuksSyncer struct { func NewGomuksSyncer(session SyncerSession) *GomuksSyncer { return &GomuksSyncer{ Session: session, - listeners: make(map[gomatrix.EventType][]EventHandler), + listeners: make(map[mautrix.EventType][]EventHandler), FirstSyncDone: false, } } // 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.AccountData.Events, EventSourceAccountData, false) @@ -106,7 +108,7 @@ func (s *GomuksSyncer) ProcessResponse(res *gomatrix.RespSync, since string) (er 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 { if !checkStateKey || event.StateKey != nil { 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 { event.RoomID = room.ID } - if event.Type.Class == gomatrix.StateEventType { + if event.Type.Class == mautrix.StateEventType { room.UpdateState(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. // 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] if !exists { 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) } -func (s *GomuksSyncer) notifyListeners(source EventSource, event *gomatrix.Event) { +func (s *GomuksSyncer) notifyListeners(source EventSource, event *mautrix.Event) { listeners, exists := s.listeners[event.Type] if !exists { 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. -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 } // GetFilterJSON returns a filter with a timeline limit of 50. func (s *GomuksSyncer) GetFilterJSON(userID string) json.RawMessage { - filter := &gomatrix.Filter{ - Room: gomatrix.RoomFilter{ + filter := &mautrix.Filter{ + Room: mautrix.RoomFilter{ IncludeLeave: false, - State: gomatrix.FilterPart{ + State: mautrix.FilterPart{ Types: []string{ "m.room.member", "m.room.name", @@ -163,21 +166,21 @@ func (s *GomuksSyncer) GetFilterJSON(userID string) json.RawMessage { "m.room.aliases", }, }, - Timeline: gomatrix.FilterPart{ + Timeline: mautrix.FilterPart{ Types: []string{"m.room.message", "m.room.member"}, Limit: 50, }, - Ephemeral: gomatrix.FilterPart{ + Ephemeral: mautrix.FilterPart{ Types: []string{"m.typing", "m.receipt"}, }, - AccountData: gomatrix.FilterPart{ + AccountData: mautrix.FilterPart{ Types: []string{"m.tag"}, }, }, - AccountData: gomatrix.FilterPart{ + AccountData: mautrix.FilterPart{ Types: []string{"m.push_rules", "m.direct", "net.maunium.gomuks.preferences"}, }, - Presence: gomatrix.FilterPart{ + Presence: mautrix.FilterPart{ Types: []string{}, }, } diff --git a/matrix/sync_test.go b/matrix/sync_test.go index 49bbcb0..79cb11b 100644 --- a/matrix/sync_test.go +++ b/matrix/sync_test.go @@ -20,7 +20,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" "maunium.net/go/gomuks/matrix" "maunium.net/go/gomuks/matrix/rooms" ) @@ -42,13 +42,13 @@ func TestGomuksSyncer_ProcessResponse(t *testing.T) { userID: "@tulir:maunium.net", rooms: map[string]*rooms.Room{ "!foo:maunium.net": { - Room: gomatrix.NewRoom("!foo:maunium.net"), + Room: mautrix.NewRoom("!foo:maunium.net"), }, "!bar:maunium.net": { - Room: gomatrix.NewRoom("!bar:maunium.net"), + Room: mautrix.NewRoom("!bar: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.GetFilterJSON("@tulir:maunium.net") - joinEvt := &gomatrix.Event{ + joinEvt := &mautrix.Event{ ID: "!join:maunium.net", Type: "m.room.member", Sender: "@tulir:maunium.net", @@ -67,7 +67,7 @@ func TestGomuksSyncer_ProcessResponse(t *testing.T) { "membership": "join", }, } - messageEvt := &gomatrix.Event{ + messageEvt := &mautrix.Event{ ID: "!msg:maunium.net", Type: "m.room.message", Content: map[string]interface{}{ @@ -75,11 +75,11 @@ func TestGomuksSyncer_ProcessResponse(t *testing.T) { "msgtype": "m.text", }, } - unhandledEvt := &gomatrix.Event{ + unhandledEvt := &mautrix.Event{ ID: "!unhandled:maunium.net", Type: "m.room.unhandled_event", } - inviteEvt := &gomatrix.Event{ + inviteEvt := &mautrix.Event{ ID: "!invite:matrix.org", Type: "m.room.member", Sender: "@you:matrix.org", @@ -88,7 +88,7 @@ func TestGomuksSyncer_ProcessResponse(t *testing.T) { "membership": "invite", }, } - leaveEvt := &gomatrix.Event{ + leaveEvt := &mautrix.Event{ ID: "!leave:matrix.org", Type: "m.room.member", Sender: "@you:matrix.org", @@ -100,27 +100,27 @@ func TestGomuksSyncer_ProcessResponse(t *testing.T) { resp := newRespSync() resp.Rooms.Join["!foo:maunium.net"] = join{ - State: events{Events: []*gomatrix.Event{joinEvt}}, - Timeline: timeline{Events: []*gomatrix.Event{messageEvt, unhandledEvt}}, + State: events{Events: []*mautrix.Event{joinEvt}}, + Timeline: timeline{Events: []*mautrix.Event{messageEvt, unhandledEvt}}, } resp.Rooms.Invite["!bar:maunium.net"] = struct { State struct { - Events []*gomatrix.Event `json:"events"` + Events []*mautrix.Event `json:"events"` } `json:"invite_state"` }{ - State: events{Events: []*gomatrix.Event{inviteEvt}}, + State: events{Events: []*mautrix.Event{inviteEvt}}, } resp.Rooms.Leave["!test:maunium.net"] = struct { State struct { - Events []*gomatrix.Event `json:"events"` + Events []*mautrix.Event `json:"events"` } `json:"state"` Timeline struct { - Events []*gomatrix.Event `json:"events"` + Events []*mautrix.Event `json:"events"` Limited bool `json:"limited"` PrevBatch string `json:"prev_batch"` } `json:"timeline"` }{ - State: events{Events: []*gomatrix.Event{leaveEvt}}, + State: events{Events: []*mautrix.Event{leaveEvt}}, } syncer.ProcessResponse(resp, "since") @@ -145,28 +145,28 @@ func (mss *mockSyncerSession) GetUserID() string { } type events struct { - Events []*gomatrix.Event `json:"events"` + Events []*mautrix.Event `json:"events"` } type timeline struct { - Events []*gomatrix.Event `json:"events"` + Events []*mautrix.Event `json:"events"` Limited bool `json:"limited"` PrevBatch string `json:"prev_batch"` } type join struct { State struct { - Events []*gomatrix.Event `json:"events"` + Events []*mautrix.Event `json:"events"` } `json:"state"` Timeline struct { - Events []*gomatrix.Event `json:"events"` + Events []*mautrix.Event `json:"events"` Limited bool `json:"limited"` PrevBatch string `json:"prev_batch"` } `json:"timeline"` Ephemeral struct { - Events []*gomatrix.Event `json:"events"` + Events []*mautrix.Event `json:"events"` } `json:"ephemeral"` AccountData struct { - Events []*gomatrix.Event `json:"events"` + Events []*mautrix.Event `json:"events"` } `json:"account_data"` } @@ -175,42 +175,42 @@ func ptr(text string) *string { } 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) } -func newRespSync() *gomatrix.RespSync { - resp := &gomatrix.RespSync{NextBatch: "123"} +func newRespSync() *mautrix.RespSync { + resp := &mautrix.RespSync{NextBatch: "123"} resp.Rooms.Join = make(map[string]struct { State struct { - Events []*gomatrix.Event `json:"events"` + Events []*mautrix.Event `json:"events"` } `json:"state"` Timeline struct { - Events []*gomatrix.Event `json:"events"` + Events []*mautrix.Event `json:"events"` Limited bool `json:"limited"` PrevBatch string `json:"prev_batch"` } `json:"timeline"` Ephemeral struct { - Events []*gomatrix.Event `json:"events"` + Events []*mautrix.Event `json:"events"` } `json:"ephemeral"` AccountData struct { - Events []*gomatrix.Event `json:"events"` + Events []*mautrix.Event `json:"events"` } `json:"account_data"` }) resp.Rooms.Invite = make(map[string]struct { State struct { - Events []*gomatrix.Event `json:"events"` + Events []*mautrix.Event `json:"events"` } `json:"invite_state"` }) resp.Rooms.Leave = make(map[string]struct { State struct { - Events []*gomatrix.Event `json:"events"` + Events []*mautrix.Event `json:"events"` } `json:"state"` Timeline struct { - Events []*gomatrix.Event `json:"events"` + Events []*mautrix.Event `json:"events"` Limited bool `json:"limited"` PrevBatch string `json:"prev_batch"` } `json:"timeline"` diff --git a/ui/commands.go b/ui/commands.go index b80fa07..0feda29 100644 --- a/ui/commands.go +++ b/ui/commands.go @@ -19,7 +19,7 @@ package ui import ( "encoding/json" "fmt" - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" "strings" "unicode" @@ -133,7 +133,7 @@ func cmdSendEvent(cmd *Command) { return } roomID := cmd.Args[0] - eventType := gomatrix.NewEventType(cmd.Args[1]) + eventType := mautrix.NewEventType(cmd.Args[1]) rawContent := strings.Join(cmd.Args[2:], "") debug.Print(roomID, eventType, rawContent) @@ -162,7 +162,7 @@ func cmdSetState(cmd *Command) { } roomID := cmd.Args[0] - eventType := gomatrix.NewEventType(cmd.Args[1]) + eventType := mautrix.NewEventType(cmd.Args[1]) stateKey := cmd.Args[2] if stateKey == "-" { stateKey = "" diff --git a/ui/messages/base.go b/ui/messages/base.go index c9da389..441aca1 100644 --- a/ui/messages/base.go +++ b/ui/messages/base.go @@ -18,7 +18,7 @@ package messages import ( "encoding/gob" - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" "time" "maunium.net/go/gomuks/config" @@ -34,7 +34,7 @@ func init() { type BaseMessage struct { MsgID string - MsgType gomatrix.MessageType + MsgType mautrix.MessageType MsgSenderID string MsgSender string MsgSenderColor tcell.Color @@ -48,7 +48,7 @@ type BaseMessage struct { 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{ MsgSenderID: sender, MsgSender: displayname, @@ -195,11 +195,11 @@ func (msg *BaseMessage) SetID(id string) { msg.MsgID = id } -func (msg *BaseMessage) Type() gomatrix.MessageType { +func (msg *BaseMessage) Type() mautrix.MessageType { return msg.MsgType } -func (msg *BaseMessage) SetType(msgtype gomatrix.MessageType) { +func (msg *BaseMessage) SetType(msgtype mautrix.MessageType) { msg.MsgType = msgtype } diff --git a/ui/messages/expandedtextmessage.go b/ui/messages/expandedtextmessage.go index 2e77a24..fbb373d 100644 --- a/ui/messages/expandedtextmessage.go +++ b/ui/messages/expandedtextmessage.go @@ -18,7 +18,7 @@ package messages import ( "encoding/gob" - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" "time" "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. -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{ BaseMessage: newBaseMessage(id, sender, displayname, msgtype, timestamp), MsgText: text, diff --git a/ui/messages/imagemessage.go b/ui/messages/imagemessage.go index a17c842..8ccff67 100644 --- a/ui/messages/imagemessage.go +++ b/ui/messages/imagemessage.go @@ -20,7 +20,7 @@ import ( "bytes" "encoding/gob" "fmt" - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" "time" "image/color" @@ -48,7 +48,7 @@ type ImageMessage struct { } // 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{ newBaseMessage(id, sender, displayname, msgtype, timestamp), body, diff --git a/ui/messages/parser/htmlparser.go b/ui/messages/parser/htmlparser.go index bcde14d..b2deeba 100644 --- a/ui/messages/parser/htmlparser.go +++ b/ui/messages/parser/htmlparser.go @@ -24,10 +24,10 @@ import ( "github.com/lucasb-eyer/go-colorful" "golang.org/x/net/html" - "maunium.net/go/gomatrix" "maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/ui/messages/tstring" "maunium.net/go/gomuks/ui/widget" + "maunium.net/go/mautrix" "maunium.net/go/tcell" "strconv" ) @@ -69,6 +69,9 @@ func (parser *htmlParser) getAttribute(node *html.Node, attribute string) string } func digits(num int) int { + if num <= 0 { + return 0 + } 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. -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 = strings.Replace(htmlData, "\t", " ", -1) parser := htmlParser{room} str := parser.Parse(htmlData) - if evt.Content.MsgType == gomatrix.MsgEmote { + if evt.Content.MsgType == mautrix.MsgEmote { str = tstring.Join([]tstring.TString{ tstring.NewTString("* "), tstring.NewColorTString(senderDisplayname, widget.GetHashColor(evt.Sender)), diff --git a/ui/messages/parser/parser.go b/ui/messages/parser/parser.go index 1d7ced3..ef6578f 100644 --- a/ui/messages/parser/parser.go +++ b/ui/messages/parser/parser.go @@ -18,24 +18,28 @@ package parser import ( "fmt" + "html" "strings" "time" - "maunium.net/go/gomatrix" "maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/ui/messages" "maunium.net/go/gomuks/ui/messages/tstring" "maunium.net/go/gomuks/ui/widget" + "maunium.net/go/mautrix" "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 { - case gomatrix.EventMessage: + case mautrix.EventSticker: + evt.Content.MsgType = mautrix.MsgImage + fallthrough + case mautrix.EventMessage: return ParseMessage(matrix, room, evt) - case gomatrix.StateMember: + case mautrix.StateMember: return ParseMembershipEvent(room, evt) } return nil @@ -49,16 +53,27 @@ func unixToTime(unix int64) time.Time { 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 member := room.GetMember(evt.Sender) if member != nil { 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 %[1]s
%[2]s

%[3]s", + replyToEvt.Sender, replyToEvt.Content.FormattedBody, evt.Content.FormattedBody) + } ts := unixToTime(evt.Timestamp) switch evt.Content.MsgType { case "m.text", "m.notice", "m.emote": - if evt.Content.Format == gomatrix.FormatHTML { + if evt.Content.Format == mautrix.FormatHTML { text := ParseHTMLMessage(room, evt, displayname) 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 } -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 { case "invite": sender = "---" @@ -88,7 +103,7 @@ func getMembershipChangeMessage(evt *gomatrix.Event, membership, prevMembership case "leave": sender = "<--" 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.Colorize(len(senderDisplayname)+len(" unbanned "), len(displayname), widget.GetHashColor(*evt.StateKey)) } else { @@ -111,7 +126,7 @@ func getMembershipChangeMessage(evt *gomatrix.Event, membership, prevMembership 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) senderDisplayname := evt.Sender if member != nil { @@ -124,7 +139,7 @@ func getMembershipEventContent(room *rooms.Room, evt *gomatrix.Event) (sender st displayname = *evt.StateKey } - prevMembership := gomatrix.MembershipLeave + prevMembership := mautrix.MembershipLeave prevDisplayname := *evt.StateKey if evt.Unsigned.PrevContent != nil { prevMembership = evt.Unsigned.PrevContent.Membership @@ -146,7 +161,7 @@ func getMembershipEventContent(room *rooms.Room, evt *gomatrix.Event) (sender st 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) if len(text) == 0 { return nil diff --git a/ui/messages/textmessage.go b/ui/messages/textmessage.go index d5ce324..8622c32 100644 --- a/ui/messages/textmessage.go +++ b/ui/messages/textmessage.go @@ -19,7 +19,7 @@ package messages import ( "encoding/gob" "fmt" - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" "time" "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. -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{ BaseMessage: newBaseMessage(id, sender, displayname, msgtype, timestamp), MsgText: text, @@ -58,7 +58,7 @@ func (msg *TextMessage) getCache() tstring.TString { return msg.cache } -func (msg *TextMessage) SetType(msgtype gomatrix.MessageType) { +func (msg *TextMessage) SetType(msgtype mautrix.MessageType) { msg.BaseMessage.SetType(msgtype) msg.cache = nil } diff --git a/ui/room-view.go b/ui/room-view.go index 4b2cd48..6eda559 100644 --- a/ui/room-view.go +++ b/ui/room-view.go @@ -18,7 +18,7 @@ package ui import ( "fmt" - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" "path/filepath" "sort" "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) displayname := sender if member != nil { @@ -357,11 +357,11 @@ func (view *RoomView) newUIMessage(id, sender string, msgtype gomatrix.MessageTy 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) } -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() id := strconv.FormatInt(now.UnixNano(), 10) sender := "" diff --git a/ui/view-login.go b/ui/view-login.go index a900870..3d42506 100644 --- a/ui/view-login.go +++ b/ui/view-login.go @@ -17,11 +17,11 @@ package ui import ( - "maunium.net/go/gomatrix" "maunium.net/go/gomuks/config" "maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/ui/widget" + "maunium.net/go/mautrix" "maunium.net/go/tview" ) @@ -89,8 +89,8 @@ func (view *LoginView) Login() { debug.Print("Init error:", err) err = view.matrix.Login(mxid, password) if err != nil { - if httpErr, ok := err.(gomatrix.HTTPError); ok { - if respErr, ok := httpErr.WrappedError.(gomatrix.RespError); ok { + if httpErr, ok := err.(mautrix.HTTPError); ok { + if respErr, ok := httpErr.WrappedError.(mautrix.RespError); ok { view.Error(respErr.Err) } else { view.Error(httpErr.Message) diff --git a/ui/view-main.go b/ui/view-main.go index c5abb19..5faa804 100644 --- a/ui/view-main.go +++ b/ui/view-main.go @@ -26,7 +26,7 @@ import ( "bufio" "os" - "maunium.net/go/gomatrix" + "maunium.net/go/mautrix" "maunium.net/go/gomuks/config" "maunium.net/go/gomuks/debug" "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) if err != nil { tempMessage.SetState(ifc.MessageStateFailed) - if httpErr, ok := err.(gomatrix.HTTPError); ok { - if respErr, ok := httpErr.WrappedError.(gomatrix.RespError); ok { + if httpErr, ok := err.(mautrix.HTTPError); ok { + if respErr, ok := httpErr.WrappedError.(mautrix.RespError); ok { // Show shorter version if available err = respErr } @@ -510,6 +510,6 @@ func (view *MainView) LoadHistory(room string) { 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) } diff --git a/ui/widget/border.go b/ui/widget/border.go index b3eb65d..834eedb 100644 --- a/ui/widget/border.go +++ b/ui/widget/border.go @@ -40,11 +40,11 @@ func (border *Border) Draw(screen tcell.Screen) { x, y, width, height := border.GetRect() if width == 1 { 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 { for borderX := x; borderX < x+width; borderX++ { - screen.SetContent(borderX, y, tview.GraphicsHoriBar, nil, background) + screen.SetContent(borderX, y, tview.Borders.Horizontal, nil, background) } } } diff --git a/vendor/golang.org/x/image/bmp/reader.go b/vendor/golang.org/x/image/bmp/reader.go index a0f2715..c10a022 100644 --- a/vendor/golang.org/x/image/bmp/reader.go +++ b/vendor/golang.org/x/image/bmp/reader.go @@ -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 // immediately followed by a BITMAPINFOHEADER. const ( - fileHeaderLen = 14 - infoHeaderLen = 40 + fileHeaderLen = 14 + infoHeaderLen = 40 + v4InfoHeaderLen = 108 + v5InfoHeaderLen = 124 ) 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 } if string(b[:2]) != "BM" { return image.Config{}, 0, false, errors.New("bmp: invalid format") } 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 } + 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]))) height := int(int32(readUint32(b[22:26]))) if height < 0 { @@ -159,14 +165,22 @@ func decodeConfig(r io.Reader) (config image.Config, bitsPerPixel int, topDown b if width < 0 || height < 0 { 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]) + // 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 { return image.Config{}, 0, false, ErrUnsupported } switch bpp { case 8: - if offset != fileHeaderLen+infoHeaderLen+256*4 { + if offset != fileHeaderLen+infoLen+256*4 { return image.Config{}, 0, false, ErrUnsupported } _, 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 case 24: - if offset != fileHeaderLen+infoHeaderLen { + if offset != fileHeaderLen+infoLen { return image.Config{}, 0, false, ErrUnsupported } return image.Config{ColorModel: color.RGBAModel, Width: width, Height: height}, 24, topDown, nil case 32: - if offset != fileHeaderLen+infoHeaderLen { + if offset != fileHeaderLen+infoLen { return image.Config{}, 0, false, ErrUnsupported } return image.Config{ColorModel: color.RGBAModel, Width: width, Height: height}, 32, topDown, nil diff --git a/vendor/golang.org/x/image/tiff/reader.go b/vendor/golang.org/x/image/tiff/reader.go index 8a941c1..ce2ef71 100644 --- a/vendor/golang.org/x/image/tiff/reader.go +++ b/vendor/golang.org/x/image/tiff/reader.go @@ -110,7 +110,7 @@ func (d *decoder) ifdUint(p []byte) (u []uint, err error) { 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 // entry and an error, if any. func (d *decoder) parseIFD(p []byte) (int, error) { diff --git a/vendor/golang.org/x/image/vp8/decode.go b/vendor/golang.org/x/image/vp8/decode.go index 1bb5028..2aa9fee 100644 --- a/vendor/golang.org/x/image/vp8/decode.go +++ b/vendor/golang.org/x/image/vp8/decode.go @@ -82,7 +82,7 @@ type mb struct { pred [4]uint8 // 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 - // that that region has non-zero coefficients. + // that region has non-zero coefficients. nzMask uint8 // nzY16 is a 0/1 value that is 1 if the macroblock used Y16 prediction and // had non-zero coefficients. @@ -274,7 +274,7 @@ func (d *Decoder) parseOtherPartitions() error { var partLens [maxNOP]int 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 // lengths are stored as 24-bit uints, i.e. up to 16 MiB per partition. n := 3 * (d.nOP - 1) diff --git a/vendor/golang.org/x/image/webp/decode.go b/vendor/golang.org/x/image/webp/decode.go index 111f358..f77a4eb 100644 --- a/vendor/golang.org/x/image/webp/decode.go +++ b/vendor/golang.org/x/image/webp/decode.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build go1.6 - package webp import ( diff --git a/vendor/golang.org/x/image/webp/doc.go b/vendor/golang.org/x/image/webp/doc.go new file mode 100644 index 0000000..e321c85 --- /dev/null +++ b/vendor/golang.org/x/image/webp/doc.go @@ -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" diff --git a/vendor/golang.org/x/image/webp/webp.go b/vendor/golang.org/x/image/webp/webp.go deleted file mode 100644 index 850cdc8..0000000 --- a/vendor/golang.org/x/image/webp/webp.go +++ /dev/null @@ -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). diff --git a/vendor/golang.org/x/net/html/const.go b/vendor/golang.org/x/net/html/const.go index 5eb7c5a..a3a918f 100644 --- a/vendor/golang.org/x/net/html/const.go +++ b/vendor/golang.org/x/net/html/const.go @@ -97,8 +97,16 @@ func isSpecialElement(element *Node) bool { switch element.Namespace { case "", "html": return isSpecialElementMap[element.Data] + case "math": + switch element.Data { + case "mi", "mo", "mn", "ms", "mtext", "annotation-xml": + return true + } case "svg": - return element.Data == "foreignObject" + switch element.Data { + case "foreignObject", "desc", "title": + return true + } } return false } diff --git a/vendor/golang.org/x/net/html/parse.go b/vendor/golang.org/x/net/html/parse.go index 4b1fa42..64a5793 100644 --- a/vendor/golang.org/x/net/html/parse.go +++ b/vendor/golang.org/x/net/html/parse.go @@ -470,6 +470,10 @@ func (p *parser) resetInsertionMode() { case a.Table: p.im = inTableIM case a.Template: + // TODO: remove this divergence from the HTML5 spec. + if n.Namespace != "" { + continue + } p.im = p.templateStack.top() case a.Head: // TODO: remove this divergence from the HTML5 spec. @@ -984,6 +988,14 @@ func inBodyIM(p *parser) bool { p.acknowledgeSelfClosingTag() p.popUntil(buttonScope, a.P) 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 != "" { p.form.Attr = []Attribute{{Key: "action", Val: action}} } @@ -1252,12 +1264,6 @@ func (p *parser) inBodyEndTagFormatting(tagAtom a.Atom) { switch commonAncestor.DataAtom { case a.Table, a.Tbody, a.Tfoot, a.Thead, a.Tr: p.fosterParent(lastNode) - case a.Template: - // TODO: remove namespace checking - if commonAncestor.Namespace == "html" { - commonAncestor = commonAncestor.LastChild - } - fallthrough default: commonAncestor.AppendChild(lastNode) } @@ -2209,6 +2215,15 @@ func (p *parser) parse() error { } // 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 listed in r's data, and nodes' parents can +// differ from the nesting implied by a naive processing of start and end +// s. Conversely, explicit 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. func Parse(r io.Reader) (*Node, error) { 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 // found. If the fragment is the InnerHTML for an existing element, pass that // element in context. +// +// It has the same intricacies as Parse. func ParseFragment(r io.Reader, context *Node) ([]*Node, error) { contextTag := "" if context != nil { diff --git a/vendor/gopkg.in/russross/blackfriday.v2/.travis.yml b/vendor/gopkg.in/russross/blackfriday.v2/.travis.yml index a4eb257..b0b525a 100644 --- a/vendor/gopkg.in/russross/blackfriday.v2/.travis.yml +++ b/vendor/gopkg.in/russross/blackfriday.v2/.travis.yml @@ -1,18 +1,17 @@ -# Travis CI (http://travis-ci.org/) is a continuous integration service for -# open source projects. This file configures it to run unit tests for -# blackfriday. - +sudo: false language: go - go: - - 1.5 - - 1.6 - - 1.7 - + - "1.10.x" + - "1.11.x" + - tip +matrix: + fast_finish: true + allow_failures: + - go: tip install: - - go get -d -t -v ./... - - go build -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). script: - - go test -v ./... - - go test -run=^$ -bench=BenchmarkReference -benchmem + - go get -t -v ./... + - diff -u <(echo -n) <(gofmt -d -s .) + - go tool vet . + - go test -v ./... diff --git a/vendor/gopkg.in/russross/blackfriday.v2/README.md b/vendor/gopkg.in/russross/blackfriday.v2/README.md index 2e0db35..d5a8649 100644 --- a/vendor/gopkg.in/russross/blackfriday.v2/README.md +++ b/vendor/gopkg.in/russross/blackfriday.v2/README.md @@ -34,9 +34,15 @@ Versions -------- Currently maintained and recommended version of Blackfriday is `v2`. It's being -developed on its own branch: https://github.com/russross/blackfriday/v2. You -should install and import it via [gopkg.in][6] at -`gopkg.in/russross/blackfriday.v2`. +developed on its own branch: https://github.com/russross/blackfriday/tree/v2 and the +documentation is available at +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: @@ -198,7 +204,7 @@ implements the following extensions: Cat : Fluffy animal everyone likes - + Internet : Vector of transmission for pictures of cats @@ -209,7 +215,7 @@ implements the following extensions: end of the document. A footnote looks like this: This is a footnote.[^1] - + [^1]: the footnote text. * **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, but for markdown. -* [LaTeX output](https://bitbucket.org/ambrevar/blackfriday-latex): +* [LaTeX output](https://github.com/Ambrevar/Blackfriday-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 ---- diff --git a/vendor/gopkg.in/russross/blackfriday.v2/block.go b/vendor/gopkg.in/russross/blackfriday.v2/block.go index d7da33f..b860747 100644 --- a/vendor/gopkg.in/russross/blackfriday.v2/block.go +++ b/vendor/gopkg.in/russross/blackfriday.v2/block.go @@ -17,6 +17,7 @@ import ( "bytes" "html" "regexp" + "strings" "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, // 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. -func isFenceLine(data []byte, syntax *string, oldmarker string) (end int, marker string) { +// If info is not nil, it gets set to the syntax specified in the fence line. +func isFenceLine(data []byte, info *string, oldmarker string) (end int, marker string) { i, size := 0, 0 // 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 - // into one, always get the syntax, and discard it if the caller doesn't care. - if syntax != nil { - syn := 0 + // into one, always get the info string, and discard it if the caller doesn't care. + if info != nil { + infoLength := 0 i = skipChar(data, i, ' ') if i >= len(data) { @@ -617,14 +618,14 @@ func isFenceLine(data []byte, syntax *string, oldmarker string) (end int, marker return 0, "" } - syntaxStart := i + infoStart := i if data[i] == '{' { i++ - syntaxStart++ + infoStart++ for i < len(data) && data[i] != '}' && data[i] != '\n' { - syn++ + infoLength++ 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 // of the {} block - for syn > 0 && isspace(data[syntaxStart]) { - syntaxStart++ - syn-- + for infoLength > 0 && isspace(data[infoStart]) { + infoStart++ + infoLength-- } - for syn > 0 && isspace(data[syntaxStart+syn-1]) { - syn-- + for infoLength > 0 && isspace(data[infoStart+infoLength-1]) { + infoLength-- } - i++ + i = skipChar(data, i, ' ') } else { - for i < len(data) && !isspace(data[i]) { - syn++ + for i < len(data) && !isverticalspace(data[i]) { + infoLength++ i++ } } - *syntax = string(data[syntaxStart : syntaxStart+syn]) + *info = strings.TrimSpace(string(data[infoStart : infoStart+infoLength])) } - i = skipChar(data, i, ' ') - if i >= len(data) || data[i] != '\n' { - if i == len(data) { - return i, marker - } + if i == len(data) { + return i, marker + } + if i > len(data) || data[i] != '\n' { return 0, "" } 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. // If doRender is true, a final newline is mandatory to recognize the fenced code block. func (p *Markdown) fencedCodeBlock(data []byte, doRender bool) int { - var syntax string - beg, marker := isFenceLine(data, &syntax, "") + var info string + beg, marker := isFenceLine(data, &info, "") if beg == 0 || beg >= len(data) { return 0 } var work bytes.Buffer - work.Write([]byte(syntax)) + work.Write([]byte(info)) work.WriteByte('\n') for { @@ -1148,6 +1148,18 @@ func (p *Markdown) list(data []byte, flags ListType) int { 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 // into lists and sublists. func endsWithBlankLine(block *Node) bool { @@ -1246,6 +1258,7 @@ func (p *Markdown) listItem(data []byte, flags *ListType) int { // process the following lines containsBlankLine := false sublist := 0 + codeBlockMarker := "" gatherlines: for line < len(data) { @@ -1279,6 +1292,27 @@ gatherlines: 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 switch { // is this a nested list item? @@ -1286,14 +1320,21 @@ gatherlines: p.oliPrefix(chunk) > 0 || p.dliPrefix(chunk) > 0: - if containsBlankLine { - *flags |= ListItemContainsBlock + // to be a nested list, it must be indented more + // 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 not, it is the next item in the same list - if indent <= itemIndent { - break gatherlines + if containsBlankLine { + *flags |= ListItemContainsBlock } // is this the first item in the nested list? diff --git a/vendor/gopkg.in/russross/blackfriday.v2/go.mod b/vendor/gopkg.in/russross/blackfriday.v2/go.mod new file mode 100644 index 0000000..620b74e --- /dev/null +++ b/vendor/gopkg.in/russross/blackfriday.v2/go.mod @@ -0,0 +1 @@ +module github.com/russross/blackfriday/v2 diff --git a/vendor/gopkg.in/russross/blackfriday.v2/html.go b/vendor/gopkg.in/russross/blackfriday.v2/html.go index 25fb185..284c871 100644 --- a/vendor/gopkg.in/russross/blackfriday.v2/html.go +++ b/vendor/gopkg.in/russross/blackfriday.v2/html.go @@ -35,6 +35,7 @@ const ( Safelink // Only link to trusted protocols NofollowLinks // Only link with rel="nofollow" NoreferrerLinks // Only link with rel="noreferrer" + NoopenerLinks // Only link with rel="noopener" HrefTargetBlank // Add a blank target CompletePage // Generate a complete HTML page UseXHTML // Generate XHTML output instead of HTML @@ -87,6 +88,10 @@ type HTMLRendererParameters struct { HeadingIDPrefix string // If set, add this text to the back of each Heading ID, to ensure uniqueness. HeadingIDSuffix string + // Increase heading levels: if the offset is 1,

becomes

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) 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 { val = append(val, "noreferrer") } + if flags&NoopenerLinks != 0 { + val = append(val, "noopener") + } if flags&HrefTargetBlank != 0 { 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 { urlFrag := prefix + string(slugify(node.Destination)) - anchor := fmt.Sprintf(`%d`, urlFrag, node.NoteID) + anchor := fmt.Sprintf(`%d`, urlFrag, node.NoteID) return []byte(fmt.Sprintf(`%s`, urlFrag, anchor)) } @@ -460,9 +468,10 @@ var ( ) func headingTagsFromLevel(level int) ([]byte, []byte) { - switch level { - case 1: + if level <= 1 { return h1Tag, h1CloseTag + } + switch level { case 2: return h2Tag, h2CloseTag case 3: @@ -471,9 +480,8 @@ func headingTagsFromLevel(level int) ([]byte, []byte) { return h4Tag, h4CloseTag case 5: return h5Tag, h5CloseTag - default: - return h6Tag, h6CloseTag } + return h6Tag, h6CloseTag } 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.cr(w) case Heading: - openTag, closeTag := headingTagsFromLevel(node.Level) + headingLevel := r.HTMLRendererParameters.HeadingLevelOffset + node.Level + openTag, closeTag := headingTagsFromLevel(headingLevel) if entering { if node.IsTitleblock { attrs = append(attrs, `class="title"`) diff --git a/vendor/gopkg.in/russross/blackfriday.v2/inline.go b/vendor/gopkg.in/russross/blackfriday.v2/inline.go index 3d63310..4ed2907 100644 --- a/vendor/gopkg.in/russross/blackfriday.v2/inline.go +++ b/vendor/gopkg.in/russross/blackfriday.v2/inline.go @@ -23,8 +23,22 @@ var ( urlRe = `((https?|ftp):\/\/|\/)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)]+` anchorRe = regexp.MustCompile(`^(]+")?\s?>` + urlRe + `<\/a>)`) - // TODO: improve this regexp to catch all possible entities: - htmlEntityRe = regexp.MustCompile(`&[a-z]{2,5};`) + // https://www.w3.org/TR/html5/syntax.html#character-references + // 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 < + // at one end and ∳ 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 ¾ and ▒, 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 diff --git a/vendor/gopkg.in/russross/blackfriday.v2/markdown.go b/vendor/gopkg.in/russross/blackfriday.v2/markdown.go index ff61cb0..58d2e45 100644 --- a/vendor/gopkg.in/russross/blackfriday.v2/markdown.go +++ b/vendor/gopkg.in/russross/blackfriday.v2/markdown.go @@ -93,46 +93,46 @@ const ( // 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. var blockTags = map[string]struct{}{ - "blockquote": struct{}{}, - "del": struct{}{}, - "div": struct{}{}, - "dl": struct{}{}, - "fieldset": struct{}{}, - "form": struct{}{}, - "h1": struct{}{}, - "h2": struct{}{}, - "h3": struct{}{}, - "h4": struct{}{}, - "h5": struct{}{}, - "h6": struct{}{}, - "iframe": struct{}{}, - "ins": struct{}{}, - "math": struct{}{}, - "noscript": struct{}{}, - "ol": struct{}{}, - "pre": struct{}{}, - "p": struct{}{}, - "script": struct{}{}, - "style": struct{}{}, - "table": struct{}{}, - "ul": struct{}{}, + "blockquote": {}, + "del": {}, + "div": {}, + "dl": {}, + "fieldset": {}, + "form": {}, + "h1": {}, + "h2": {}, + "h3": {}, + "h4": {}, + "h5": {}, + "h6": {}, + "iframe": {}, + "ins": {}, + "math": {}, + "noscript": {}, + "ol": {}, + "pre": {}, + "p": {}, + "script": {}, + "style": {}, + "table": {}, + "ul": {}, // HTML5 - "address": struct{}{}, - "article": struct{}{}, - "aside": struct{}{}, - "canvas": struct{}{}, - "figcaption": struct{}{}, - "figure": struct{}{}, - "footer": struct{}{}, - "header": struct{}{}, - "hgroup": struct{}{}, - "main": struct{}{}, - "nav": struct{}{}, - "output": struct{}{}, - "progress": struct{}{}, - "section": struct{}{}, - "video": struct{}{}, + "address": {}, + "article": {}, + "aside": {}, + "canvas": {}, + "figcaption": {}, + "figure": {}, + "footer": {}, + "header": {}, + "hgroup": {}, + "main": {}, + "nav": {}, + "output": {}, + "progress": {}, + "section": {}, + "video": {}, } // 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. // // 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 // footnotes. @@ -813,7 +813,17 @@ func ispunct(c byte) bool { // Test if a character is a whitespace character. 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. diff --git a/vendor/maunium.net/go/gomatrix/.gitignore b/vendor/maunium.net/go/gomatrix/.gitignore deleted file mode 100644 index daf913b..0000000 --- a/vendor/maunium.net/go/gomatrix/.gitignore +++ /dev/null @@ -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 diff --git a/vendor/maunium.net/go/gomatrix/.travis.yml b/vendor/maunium.net/go/gomatrix/.travis.yml deleted file mode 100644 index fadc326..0000000 --- a/vendor/maunium.net/go/gomatrix/.travis.yml +++ /dev/null @@ -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 diff --git a/vendor/maunium.net/go/gomatrix/README.md b/vendor/maunium.net/go/gomatrix/README.md deleted file mode 100644 index ea9109a..0000000 --- a/vendor/maunium.net/go/gomatrix/README.md +++ /dev/null @@ -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.** diff --git a/vendor/maunium.net/go/maulogger/LICENSE b/vendor/maunium.net/go/maulogger/LICENSE deleted file mode 100644 index c9739fb..0000000 --- a/vendor/maunium.net/go/maulogger/LICENSE +++ /dev/null @@ -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. diff --git a/vendor/maunium.net/go/maulogger/README.md b/vendor/maunium.net/go/maulogger/README.md deleted file mode 100644 index 68fe253..0000000 --- a/vendor/maunium.net/go/maulogger/README.md +++ /dev/null @@ -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` diff --git a/vendor/maunium.net/go/maulogger/logger.go b/vendor/maunium.net/go/maulogger/logger.go deleted file mode 100644 index e887237..0000000 --- a/vendor/maunium.net/go/maulogger/logger.go +++ /dev/null @@ -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() - } -} diff --git a/vendor/maunium.net/go/mautrix/.gitignore b/vendor/maunium.net/go/mautrix/.gitignore new file mode 100644 index 0000000..66f8fb5 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/.gitignore @@ -0,0 +1,2 @@ +.idea/ +.vscode/ diff --git a/vendor/maunium.net/go/gomatrix/LICENSE b/vendor/maunium.net/go/mautrix/LICENSE similarity index 100% rename from vendor/maunium.net/go/gomatrix/LICENSE rename to vendor/maunium.net/go/mautrix/LICENSE diff --git a/vendor/maunium.net/go/mautrix/README.md b/vendor/maunium.net/go/mautrix/README.md new file mode 100644 index 0000000..ca135a6 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/README.md @@ -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. diff --git a/vendor/maunium.net/go/gomatrix/client.go b/vendor/maunium.net/go/mautrix/client.go similarity index 98% rename from vendor/maunium.net/go/gomatrix/client.go rename to vendor/maunium.net/go/mautrix/client.go index 0806138..d908b62 100644 --- a/vendor/maunium.net/go/gomatrix/client.go +++ b/vendor/maunium.net/go/mautrix/client.go @@ -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 -package gomatrix +// Specification can be found at http://matrix.org/docs/spec/client_server/r0.4.0.html +package mautrix import ( "bytes" @@ -10,7 +10,6 @@ import ( "fmt" "io" "io/ioutil" - "maunium.net/go/maulogger" "net/http" "net/url" "path" @@ -20,6 +19,10 @@ import ( "time" ) +type Logger interface { + Debugfln(message string, args ...interface{}) +} + // Client represents a Matrix client. type Client struct { 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. Syncer Syncer // The thing which can process /sync responses 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, // no user_id parameter will be sent. @@ -132,7 +135,6 @@ func (cli *Client) Sync() error { filterID = resFilter.FilterID cli.Store.SaveFilterID(cli.UserID, filterID) } - for { resSync, err := cli.SyncRequest(30000, nextBatch, filterID, false, "") 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. // -// res, err := cli.RegisterDummy(&gomatrix.ReqRegister{ +// res, err := cli.RegisterDummy(&mautrix.ReqRegister{ // Username: "alice", // 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 -// resp, err := cli.CreateRoom(&gomatrix.ReqCreateRoom{ +// resp, err := cli.CreateRoom(&mautrix.ReqCreateRoom{ // Preset: "public_chat", // }) // fmt.Println("Room:", resp.RoomID) diff --git a/vendor/maunium.net/go/gomatrix/events.go b/vendor/maunium.net/go/mautrix/events.go similarity index 89% rename from vendor/maunium.net/go/gomatrix/events.go rename to vendor/maunium.net/go/mautrix/events.go index 30166cd..c974bf9 100644 --- a/vendor/maunium.net/go/gomatrix/events.go +++ b/vendor/maunium.net/go/mautrix/events.go @@ -1,4 +1,5 @@ -package gomatrix +// Copyright 2018 Tulir Asokan +package mautrix import ( "encoding/json" @@ -101,7 +102,7 @@ var ( // Ephemeral events var ( EphemeralEventReceipt = EventType{"m.receipt", EphemeralEventType} - EphemeralEventTyping = EventType{"m.receipt", EphemeralEventType} + EphemeralEventTyping = EventType{"m.typing", EphemeralEventType} ) // Account data events @@ -165,6 +166,17 @@ type Unsigned struct { PrevSender string `json:"prev_sender,omitempty"` ReplacesState string `json:"replaces_state,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 { @@ -182,7 +194,8 @@ type Content struct { // Membership key for easy access in m.room.member events 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 Member @@ -197,14 +210,30 @@ type Content struct { type serializableContent Content +var DisableFancyEventParsing = false + func (content *Content) UnmarshalJSON(data []byte) error { 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 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) { err = json.Unmarshal(content.VeryRaw, &pl) return @@ -228,7 +257,7 @@ func (content *Content) GetInfo() *FileInfo { } type Tags map[string]struct { - Order string `json:"order"` + Order json.Number `json:"order"` } type RoomName struct { @@ -411,3 +440,9 @@ type InReplyTo struct { // Not required, just for future-proofing RoomID string `json:"room_id,omitempty"` } + +type MatchedCommand struct { + Target string `json:"target"` + Matched string `json:"matched"` + Arguments map[string]string `json:"arguments"` +} diff --git a/vendor/maunium.net/go/gomatrix/filter.go b/vendor/maunium.net/go/mautrix/filter.go similarity index 99% rename from vendor/maunium.net/go/gomatrix/filter.go rename to vendor/maunium.net/go/mautrix/filter.go index 2a0c37f..41cab2d 100644 --- a/vendor/maunium.net/go/gomatrix/filter.go +++ b/vendor/maunium.net/go/mautrix/filter.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package gomatrix +package mautrix import "errors" diff --git a/vendor/maunium.net/go/gomatrix/reply.go b/vendor/maunium.net/go/mautrix/reply.go similarity index 98% rename from vendor/maunium.net/go/gomatrix/reply.go rename to vendor/maunium.net/go/mautrix/reply.go index 6985421..5e3af92 100644 --- a/vendor/maunium.net/go/gomatrix/reply.go +++ b/vendor/maunium.net/go/mautrix/reply.go @@ -1,4 +1,5 @@ -package gomatrix +// Copyright 2018 Tulir Asokan +package mautrix import ( "fmt" diff --git a/vendor/maunium.net/go/gomatrix/requests.go b/vendor/maunium.net/go/mautrix/requests.go similarity index 97% rename from vendor/maunium.net/go/gomatrix/requests.go rename to vendor/maunium.net/go/mautrix/requests.go index d8e10a6..b90e6fb 100644 --- a/vendor/maunium.net/go/gomatrix/requests.go +++ b/vendor/maunium.net/go/mautrix/requests.go @@ -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 type ReqRegister struct { @@ -31,7 +31,7 @@ type ReqCreateRoom struct { Invite []string `json:"invite,omitempty"` Invite3PID []ReqInvite3PID `json:"invite_3pid,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"` IsDirect bool `json:"is_direct,omitempty"` } @@ -79,4 +79,4 @@ type ReqTyping struct { type ReqPresence struct { Presence string `json:"presence"` -} \ No newline at end of file +} diff --git a/vendor/maunium.net/go/gomatrix/responses.go b/vendor/maunium.net/go/mautrix/responses.go similarity index 95% rename from vendor/maunium.net/go/gomatrix/responses.go rename to vendor/maunium.net/go/mautrix/responses.go index 9524d62..2adf90a 100644 --- a/vendor/maunium.net/go/gomatrix/responses.go +++ b/vendor/maunium.net/go/mautrix/responses.go @@ -1,4 +1,4 @@ -package gomatrix +package mautrix // 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 @@ -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 type RespMessages struct { - Start string `json:"start"` + Start string `json:"start"` 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 @@ -146,8 +146,8 @@ type RespSync struct { } `json:"state"` Timeline struct { Events []*Event `json:"events"` - Limited bool `json:"limited"` - PrevBatch string `json:"prev_batch"` + Limited bool `json:"limited"` + PrevBatch string `json:"prev_batch"` } `json:"timeline"` } `json:"leave"` Join map[string]struct { @@ -156,14 +156,14 @@ type RespSync struct { } `json:"state"` Timeline struct { Events []*Event `json:"events"` - Limited bool `json:"limited"` - PrevBatch string `json:"prev_batch"` + Limited bool `json:"limited"` + PrevBatch string `json:"prev_batch"` } `json:"timeline"` Ephemeral struct { - Events []*Event `json:"events"` + Events []*Event `json:"events"` } `json:"ephemeral"` AccountData struct { - Events []*Event `json:"events"` + Events []*Event `json:"events"` } `json:"account_data"` } `json:"join"` Invite map[string]struct { diff --git a/vendor/maunium.net/go/gomatrix/room.go b/vendor/maunium.net/go/mautrix/room.go similarity index 98% rename from vendor/maunium.net/go/gomatrix/room.go rename to vendor/maunium.net/go/mautrix/room.go index 80a91d8..086e259 100644 --- a/vendor/maunium.net/go/gomatrix/room.go +++ b/vendor/maunium.net/go/mautrix/room.go @@ -1,4 +1,4 @@ -package gomatrix +package mautrix // Room represents a single Matrix room. type Room struct { diff --git a/vendor/maunium.net/go/gomatrix/store.go b/vendor/maunium.net/go/mautrix/store.go similarity index 99% rename from vendor/maunium.net/go/gomatrix/store.go rename to vendor/maunium.net/go/mautrix/store.go index 6dc687e..774398e 100644 --- a/vendor/maunium.net/go/gomatrix/store.go +++ b/vendor/maunium.net/go/mautrix/store.go @@ -1,4 +1,4 @@ -package gomatrix +package mautrix // Storer is an interface which must be satisfied to store client data. // diff --git a/vendor/maunium.net/go/gomatrix/sync.go b/vendor/maunium.net/go/mautrix/sync.go similarity index 99% rename from vendor/maunium.net/go/gomatrix/sync.go rename to vendor/maunium.net/go/mautrix/sync.go index 09170d7..9589edc 100644 --- a/vendor/maunium.net/go/gomatrix/sync.go +++ b/vendor/maunium.net/go/mautrix/sync.go @@ -1,4 +1,4 @@ -package gomatrix +package mautrix import ( "encoding/json" diff --git a/vendor/maunium.net/go/gomatrix/userids.go b/vendor/maunium.net/go/mautrix/userids.go similarity index 98% rename from vendor/maunium.net/go/gomatrix/userids.go rename to vendor/maunium.net/go/mautrix/userids.go index 23e7807..ce6e02d 100644 --- a/vendor/maunium.net/go/gomatrix/userids.go +++ b/vendor/maunium.net/go/mautrix/userids.go @@ -1,4 +1,4 @@ -package gomatrix +package mautrix import ( "bytes" @@ -125,6 +125,6 @@ func ExtractUserLocalpart(userID string) (string, error) { } return strings.TrimPrefix( strings.SplitN(userID, ":", 2)[0], // @foo:bar:8448 => [ "@foo", "bar:8448" ] - "@", // remove "@" prefix + "@", // remove "@" prefix ), nil } diff --git a/vendor/maunium.net/go/tcell/README.adoc b/vendor/maunium.net/go/tcell/README.adoc new file mode 100644 index 0000000..3b60057 --- /dev/null +++ b/vendor/maunium.net/go/tcell/README.adoc @@ -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. +|=== diff --git a/vendor/maunium.net/go/tcell/cell.go b/vendor/maunium.net/go/tcell/cell.go index 496f10f..957b62f 100644 --- a/vendor/maunium.net/go/tcell/cell.go +++ b/vendor/maunium.net/go/tcell/cell.go @@ -52,6 +52,10 @@ func (cb *CellBuffer) SetContent(x int, y int, i := 0 for i < len(c.currComb) { r := c.currComb[i] + if r == '\u200d' { + i += 2 + continue + } if runewidth.RuneWidth(r) != 0 { // not a combining character, yank it 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 // 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) { for i := range cb.cells { c := &cb.cells[i] c.currMain = r c.currComb = nil c.currStyle = style + c.width = 1 } } diff --git a/vendor/maunium.net/go/tcell/console_win.go b/vendor/maunium.net/go/tcell/console_win.go index 5957a17..a7507c2 100644 --- a/vendor/maunium.net/go/tcell/console_win.go +++ b/vendor/maunium.net/go/tcell/console_win.go @@ -28,7 +28,6 @@ type cScreen struct { in syscall.Handle out syscall.Handle cancelflag syscall.Handle - title syscall.Handle scandone chan struct{} evch chan Event quit chan struct{} diff --git a/vendor/maunium.net/go/tcell/tcell.png b/vendor/maunium.net/go/tcell/tcell.png deleted file mode 100644 index 24333c4..0000000 Binary files a/vendor/maunium.net/go/tcell/tcell.png and /dev/null differ diff --git a/vendor/maunium.net/go/tcell/tcell.svg b/vendor/maunium.net/go/tcell/tcell.svg deleted file mode 100644 index d8695d5..0000000 --- a/vendor/maunium.net/go/tcell/tcell.svg +++ /dev/null @@ -1,93 +0,0 @@ - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - tcell - - diff --git a/vendor/maunium.net/go/tcell/terminfo/term_termite.go b/vendor/maunium.net/go/tcell/terminfo/term_termite.go new file mode 100644 index 0000000..8e7f683 --- /dev/null +++ b/vendor/maunium.net/go/tcell/terminfo/term_termite.go @@ -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", + }) +} diff --git a/vendor/maunium.net/go/tcell/tscreen.go b/vendor/maunium.net/go/tcell/tscreen.go index d8e62b2..dd49814 100644 --- a/vendor/maunium.net/go/tcell/tscreen.go +++ b/vendor/maunium.net/go/tcell/tscreen.go @@ -444,8 +444,8 @@ func (t *tScreen) ResetTitle() { func (t *tScreen) Fini() { t.Lock() defer t.Unlock() - - ti := t.ti + + ti := t.ti t.cells.Resize(0, 0) t.TPuts(ti.ShowCursor) t.TPuts(ti.AttrOff) @@ -467,7 +467,7 @@ func (t *tScreen) Fini() { default: close(t.quit) } - + t.termioFini() } diff --git a/vendor/maunium.net/go/tview/LICENSE.txt b/vendor/maunium.net/go/tview/LICENSE.txt index 8aa2645..9d69430 100644 --- a/vendor/maunium.net/go/tview/LICENSE.txt +++ b/vendor/maunium.net/go/tview/LICENSE.txt @@ -1,6 +1,6 @@ MIT License -Copyright (c) [year] [fullname] +Copyright (c) 2018 Oliver Kuederle Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/vendor/maunium.net/go/tview/README.md b/vendor/maunium.net/go/tview/README.md index 3e5734e..7ce06a2 100644 --- a/vendor/maunium.net/go/tview/README.md +++ b/vendor/maunium.net/go/tview/README.md @@ -12,6 +12,7 @@ Among these components are: - __Input forms__ (include __input/password fields__, __drop-down selections__, __checkboxes__, and __buttons__) - Navigable multi-color __text views__ - Sophisticated navigable __table views__ +- Flexible __tree views__ - Selectable __lists__ - __Grid__, __Flexbox__ and __page layouts__ - 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.) +- 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) - 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) - Added background colors and text attributes to color tags. - v0.12 (2018-03-13) diff --git a/vendor/maunium.net/go/tview/ansii.go b/vendor/maunium.net/go/tview/ansi.go similarity index 82% rename from vendor/maunium.net/go/tview/ansii.go rename to vendor/maunium.net/go/tview/ansi.go index 0ce3d4a..4d14c28 100644 --- a/vendor/maunium.net/go/tview/ansii.go +++ b/vendor/maunium.net/go/tview/ansi.go @@ -8,44 +8,44 @@ import ( "strings" ) -// The states of the ANSII escape code parser. +// The states of the ANSI escape code parser. const ( - ansiiText = iota - ansiiEscape - ansiiSubstring - ansiiControlSequence + ansiText = iota + ansiEscape + ansiSubstring + 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. -type ansii struct { +type ansi struct { io.Writer // Reusable buffers. buffer *bytes.Buffer // The entire output text of one Write(). 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 } -// 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 // and are simply removed. The translated text is written to the provided // writer. -func ANSIIWriter(writer io.Writer) io.Writer { - return &ansii{ +func ANSIWriter(writer io.Writer) io.Writer { + return &ansi{ Writer: writer, buffer: new(bytes.Buffer), csiParameter: 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. -func (a *ansii) Write(text []byte) (int, error) { +func (a *ansi) Write(text []byte) (int, error) { defer func() { a.buffer.Reset() }() @@ -54,23 +54,23 @@ func (a *ansii) Write(text []byte) (int, error) { switch a.state { // We just entered an escape sequence. - case ansiiEscape: + case ansiEscape: switch r { case '[': // Control Sequence Introducer. a.csiParameter.Reset() a.csiIntermediate.Reset() - a.state = ansiiControlSequence + a.state = ansiControlSequence case 'c': // Reset. fmt.Fprint(a.buffer, "[-:-:-]") - a.state = ansiiText + a.state = ansiText case 'P', ']', 'X', '^', '_': // Substrings and commands. - a.state = ansiiSubstring + a.state = ansiSubstring default: // Ignore. - a.state = ansiiText + a.state = ansiText } // CSI Sequences. - case ansiiControlSequence: + case ansiControlSequence: switch { case r >= 0x30 && r <= 0x3f: // Parameter bytes. if _, err := a.csiParameter.WriteRune(r); err != nil { @@ -166,16 +166,16 @@ func (a *ansii) Write(text []byte) (int, error) { red := (colorNumber - 16) / 36 green := ((colorNumber - 16) / 6) % 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 { 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. red, _ := strconv.Atoi(fields[index+2]) green, _ := strconv.Atoi(fields[index+3]) 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 { @@ -194,22 +194,22 @@ func (a *ansii) Write(text []byte) (int, error) { fmt.Fprintf(a.buffer, "[%s:%s%s]", foreground, background, attributes) } } - a.state = ansiiText + a.state = ansiText default: // Undefined byte. - a.state = ansiiText // Abort CSI. + a.state = ansiText // Abort CSI. } // We just entered a substring/command sequence. - case ansiiSubstring: + case ansiSubstring: if r == 27 { // Most likely the end of the substring. - a.state = ansiiEscape + a.state = ansiEscape } // Ignore all other characters. - // "ansiiText" and all others. + // "ansiText" and all others. default: if r == 27 { // This is the start of an escape sequence. - a.state = ansiiEscape + a.state = ansiEscape } else { // Just a regular rune. Send to buffer. if _, err := a.buffer.WriteRune(r); err != nil { @@ -227,11 +227,11 @@ func (a *ansii) Write(text []byte) (int, error) { 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. -func TranslateANSII(text string) string { +func TranslateANSI(text string) string { var buffer bytes.Buffer - writer := ANSIIWriter(&buffer) + writer := ANSIWriter(&buffer) writer.Write([]byte(text)) return buffer.String() } diff --git a/vendor/maunium.net/go/tview/application.go b/vendor/maunium.net/go/tview/application.go index 7e6da3a..ae4d7f8 100644 --- a/vendor/maunium.net/go/tview/application.go +++ b/vendor/maunium.net/go/tview/application.go @@ -1,24 +1,36 @@ package tview import ( - "fmt" - "os" "sync" "maunium.net/go/tcell" ) +// The size of the event/update/redraw channels. +const queueSize = 100 + // Application represents the top node of an application. // // 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 // 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 { sync.RWMutex // The application's 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. focus Primitive @@ -48,13 +60,23 @@ type Application struct { // was drawn. afterDraw func(screen tcell.Screen) - // If this value is true, the application has entered suspended mode. - suspended bool + // Used to send screen events from separate goroutine to main event loop + 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. 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 @@ -97,140 +119,222 @@ func (a *Application) GetScreen() tcell.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 // when Stop() was called. func (a *Application) Run() error { var err error a.Lock() - // Make a screen. - a.screen, err = tcell.NewScreen() - if err != nil { - a.Unlock() - return err + // Make a screen if there is none yet. + if a.screen == nil { + a.screen, err = tcell.NewScreen() + if err != nil { + a.Unlock() + return err + } } if err = a.screen.Init(); err != nil { a.Unlock() return err } 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. a.Unlock() - a.Draw() + a.draw() - // Start event loop. - for { - a.Lock() - screen := a.screen - if a.suspended { - a.suspended = false // Clear previous suspended flag. - } - a.Unlock() - if screen == nil { - break - } - - // Wait for next event. - 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 { + // Separate loop to wait for screen events. + var wg sync.WaitGroup + wg.Add(1) + a.suspendToken <- struct{}{} // We need this to get started. + go func() { + defer wg.Done() + for range a.suspendToken { + for { + a.RLock() + screen := a.screen + a.RUnlock() + if screen == nil { + // We have no screen. We might need to stop. 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 { - if handler := p.PasteHandler(); handler != nil { - handler(event) - a.Draw() + switch event := event.(type) { + case *tcell.EventKey: + a.RLock() + 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() - screen := a.screen - a.Unlock() - screen.Clear() - a.Draw() + + // If we have updates, now is the time to execute them. + case updater := <-a.updates: + updater() } } + a.running = false + close(a.suspendToken) + wg.Wait() + return nil } // Stop stops the application, causing Run() to return. func (a *Application) Stop() { - a.RLock() - defer a.RUnlock() - if a.screen == nil { + a.Lock() + defer a.Unlock() + screen := a.screen + if screen == nil { return } - a.screen.Fini() 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 @@ -243,29 +347,24 @@ func (a *Application) Stop() { func (a *Application) Suspend(f func()) bool { a.Lock() - if a.suspended || a.screen == nil { - // Application is already suspended. + screen := a.screen + if screen == nil { + // Screen has not yet been initialized. a.Unlock() return false } - // Enter suspended mode. - a.suspended = true + // Enter suspended mode. Make a new screen here already so our event loop can + // continue. + a.screen = nil + a.running = false + screen.Fini() 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. f() - // Make a new screen and redraw. + // Initialize our new screen and draw the contents. a.Lock() var err error a.screen, err = tcell.NewScreen() @@ -278,23 +377,36 @@ func (a *Application) Suspend(f func()) bool { panic(err) } a.screen.EnableMouse() + a.running = true 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. return true } -// Draw refreshes the screen. It calls the Draw() function of the application's -// root primitive and then syncs the screen buffer. +// Draw refreshes the screen (during the next update cycle). It calls the Draw() +// function of the application's root primitive and then syncs the screen +// buffer. 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 root := a.root fullscreen := a.rootFullscreen before := a.beforeDraw after := a.afterDraw - a.RUnlock() // Maybe we're not ready yet or not anymore. if screen == nil || root == nil { @@ -427,3 +539,35 @@ func (a *Application) GetFocus() Primitive { defer a.RUnlock() 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 +} diff --git a/vendor/maunium.net/go/tview/borders.go b/vendor/maunium.net/go/tview/borders.go new file mode 100644 index 0000000..946c878 --- /dev/null +++ b/vendor/maunium.net/go/tview/borders.go @@ -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, +} diff --git a/vendor/maunium.net/go/tview/box.go b/vendor/maunium.net/go/tview/box.go index ff3cc1e..85bf1b2 100644 --- a/vendor/maunium.net/go/tview/box.go +++ b/vendor/maunium.net/go/tview/box.go @@ -32,6 +32,9 @@ type Box struct { // The color of the border. borderColor tcell.Color + // The style attributes of the border. + borderAttributes tcell.AttrMask + // The title. Only visible if there is a border, too. title string @@ -48,10 +51,6 @@ type Box struct { // Whether or not this box has focus. 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 // event to be forwarded to the primitive's default input handler (nil if // nothing should be forwarded). @@ -78,7 +77,6 @@ func NewBox() *Box { borderColor: Styles.BorderColor, titleColor: Styles.TitleColor, titleAlign: AlignCenter, - clampToScreen: true, } b.focus = b return b @@ -121,6 +119,7 @@ func (b *Box) SetRect(x, y, width, height int) { b.y = y b.width = width b.height = height + b.innerX = -1 // Mark inner rect as uninitialized. } // 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 } +// 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. func (b *Box) SetTitle(title string) *Box { b.title = title @@ -319,30 +327,30 @@ func (b *Box) Draw(screen tcell.Screen) { // Draw border. 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 if b.focus.HasFocus() { - vertical = GraphicsDbVertBar - horizontal = GraphicsDbHorBar - topLeft = GraphicsDbTopLeftCorner - topRight = GraphicsDbTopRightCorner - bottomLeft = GraphicsDbBottomLeftCorner - bottomRight = GraphicsDbBottomRightCorner + horizontal = Borders.HorizontalFocus + vertical = Borders.VerticalFocus + topLeft = Borders.TopLeftFocus + topRight = Borders.TopRightFocus + bottomLeft = Borders.BottomLeftFocus + bottomRight = Borders.BottomRightFocus } else { - vertical = GraphicsHoriBar - horizontal = GraphicsVertBar - topLeft = GraphicsTopLeftCorner - topRight = GraphicsTopRightCorner - bottomLeft = GraphicsBottomLeftCorner - bottomRight = GraphicsBottomRightCorner + horizontal = Borders.Horizontal + vertical = Borders.Vertical + topLeft = Borders.TopLeft + topRight = Borders.TopRight + bottomLeft = Borders.BottomLeft + bottomRight = Borders.BottomRight } 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+b.height-1, vertical, nil, border) + screen.SetContent(x, b.y, horizontal, 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++ { - screen.SetContent(b.x, y, horizontal, nil, border) - screen.SetContent(b.x+b.width-1, y, horizontal, nil, border) + screen.SetContent(b.x, y, vertical, 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.width-1, b.y, topRight, nil, border) @@ -351,11 +359,11 @@ func (b *Box) Draw(screen tcell.Screen) { // Draw title. if b.title != "" && b.width >= 4 { - _, 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 { + printed, _ := Print(screen, b.title, b.x+1, b.y, b.width-2, b.titleAlign, b.titleColor) + if len(b.title)-printed > 0 && printed > 0 { _, _, style, _ := screen.GetContent(b.x+b.width-2, b.y) 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. - if b.clampToScreen { - width, height := screen.Size() - if b.innerX < 0 { - b.innerWidth += b.innerX - b.innerX = 0 - } - if b.innerX+b.innerWidth >= width { - b.innerWidth = width - b.innerX - } - if b.innerY+b.innerHeight >= height { - b.innerHeight = height - b.innerY - } - if b.innerY < 0 { - b.innerHeight += b.innerY - b.innerY = 0 - } + width, height := screen.Size() + if b.innerX < 0 { + b.innerWidth += b.innerX + b.innerX = 0 + } + if b.innerX+b.innerWidth >= width { + b.innerWidth = width - b.innerX + } + if b.innerY+b.innerHeight >= height { + b.innerHeight = height - b.innerY + } + if b.innerY < 0 { + b.innerHeight += b.innerY + b.innerY = 0 } } diff --git a/vendor/maunium.net/go/tview/checkbox.go b/vendor/maunium.net/go/tview/checkbox.go index ae58720..48d4592 100644 --- a/vendor/maunium.net/go/tview/checkbox.go +++ b/vendor/maunium.net/go/tview/checkbox.go @@ -124,9 +124,9 @@ func (c *Checkbox) SetChangedFunc(handler func(checked bool)) *Checkbox { return c } -// SetDoneFunc sets a handler which is called when the user is done entering -// text. The callback function is provided with the key that was pressed, which -// is one of the following: +// SetDoneFunc sets a handler which is called when the user is done using the +// checkbox. The callback function is provided with the key that was pressed, +// which is one of the following: // // - KeyEscape: Abort text input. // - KeyTab: Move to the next field. diff --git a/vendor/maunium.net/go/tview/doc.go b/vendor/maunium.net/go/tview/doc.go index ccaaaf1..ddc410f 100644 --- a/vendor/maunium.net/go/tview/doc.go +++ b/vendor/maunium.net/go/tview/doc.go @@ -7,10 +7,12 @@ 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. - - Table: Scrollable display of tabular data. Table cells, rows, or columns may - also be highlighted. + - Table: A scrollable display of tabular data. Table cells, rows, or columns + 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. - InputField: One-line input fields to enter text. - DropDown: Drop-down selection fields. @@ -83,7 +85,7 @@ tag is as follows: [::] -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 that are not specified will be left unchanged. A field with just a dash ("-") means "reset to default". @@ -135,6 +137,27 @@ Unicode Support 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 All widgets listed above contain the Box type. All of Box's functions are diff --git a/vendor/maunium.net/go/tview/dropdown.go b/vendor/maunium.net/go/tview/dropdown.go index 4e4d888..02c93bd 100644 --- a/vendor/maunium.net/go/tview/dropdown.go +++ b/vendor/maunium.net/go/tview/dropdown.go @@ -354,6 +354,7 @@ func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr // Hand control over to the list. d.open = true + optionBefore := d.currentOption d.list.SetSelectedFunc(func(index int, mainText, secondaryText string, shortcut rune) { // An option was selected. Close the list again. 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]) } evalPrefix() + } else if event.Key() == tcell.KeyEscape { + d.open = false + d.currentOption = optionBefore + setFocus(d) } else { d.prefix = "" } diff --git a/vendor/maunium.net/go/tview/flex.go b/vendor/maunium.net/go/tview/flex.go index ad42d3a..bc1cfe1 100644 --- a/vendor/maunium.net/go/tview/flex.go +++ b/vendor/maunium.net/go/tview/flex.go @@ -28,7 +28,7 @@ type Flex struct { *Box // The items to be positioned. - items []flexItem + items []*flexItem // FlexRow or FlexColumn. 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 // space but nothing will be drawn. 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 } @@ -94,6 +94,19 @@ func (f *Flex) RemoveItem(p Primitive) *Flex { 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. func (f *Flex) Draw(screen tcell.Screen) { f.Box.Draw(screen) diff --git a/vendor/maunium.net/go/tview/form.go b/vendor/maunium.net/go/tview/form.go index fe0e980..e960a52 100644 --- a/vendor/maunium.net/go/tview/form.go +++ b/vendor/maunium.net/go/tview/form.go @@ -26,7 +26,7 @@ type FormItem interface { // required. 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 // 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). @@ -218,6 +218,37 @@ func (f *Form) AddButton(label string, selected func()) *Form { 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 // specified. func (f *Form) Clear(includeButtons bool) *Form { @@ -251,6 +282,14 @@ func (f *Form) GetFormItem(index int) FormItem { 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 // no such element is found, nil is returned. Buttons are not searched and will // therefore not be returned. @@ -263,6 +302,18 @@ func (f *Form) GetFormItemByLabel(label string) FormItem { 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 // key. func (f *Form) SetCancelFunc(callback func()) *Form { diff --git a/vendor/maunium.net/go/tview/grid.go b/vendor/maunium.net/go/tview/grid.go index 77797ba..d8f6c97 100644 --- a/vendor/maunium.net/go/tview/grid.go +++ b/vendor/maunium.net/go/tview/grid.go @@ -583,11 +583,11 @@ func (g *Grid) Draw(screen tcell.Screen) { } by := item.y - 1 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 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. @@ -596,28 +596,28 @@ func (g *Grid) Draw(screen tcell.Screen) { } bx := item.x - 1 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 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. 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. 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. 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. 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) } } } diff --git a/vendor/maunium.net/go/tview/inputfield.go b/vendor/maunium.net/go/tview/inputfield.go index 73224f2..ccc66e3 100644 --- a/vendor/maunium.net/go/tview/inputfield.go +++ b/vendor/maunium.net/go/tview/inputfield.go @@ -11,10 +11,23 @@ import ( ) // 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 -// input). +// The following keys can be used for navigation and editing: +// +// - 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. type InputField struct { @@ -53,6 +66,12 @@ type InputField struct { // disables masking. 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. accept func(text string, ch rune) bool @@ -83,6 +102,7 @@ func NewInputField() *InputField { // SetText sets the current text of the input field. func (i *InputField) SetText(text string) *InputField { i.text = text + i.cursorPos = len(text) if i.changed != nil { 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 // 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). func (i *InputField) SetAcceptanceFunc(handler func(textToCheck string, lastChar rune) bool) *InputField { i.accept = handler @@ -244,56 +264,69 @@ func (i *InputField) Draw(screen tcell.Screen) { screen.SetContent(x+index, y, ' ', nil, fieldStyle) } - // Draw placeholder text. + // Text. + var cursorScreenPos int text := i.text 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 { // Draw entered text. if i.maskCharacter > 0 { text = strings.Repeat(string(i.maskCharacter), utf8.RuneCountInString(i.text)) - } else { - text = Escape(text) } - fieldWidth-- // We need one cell for the cursor. - if fieldWidth < runewidth.StringWidth(text) { - Print(screen, text, x, y, fieldWidth, AlignRight, i.fieldTextColor) + stringWidth := runewidth.StringWidth(text) + if fieldWidth >= stringWidth { + // 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 { - 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. 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. func (i *InputField) InputHandler() 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. switch key := event.Key(); key { case tcell.KeyRune: // Regular character. - newText := i.text + string(event.Rune()) - if i.accept != nil { - if !i.accept(newText, event.Rune()) { + if event.Modifiers()&tcell.ModAlt > 0 { + // We accept some Alt- key combinations. + 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 } } - i.text = newText case tcell.KeyCtrlU: // Delete all. 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. - lastWord := regexp.MustCompile(`\s*\S+\s*$`) - i.text = lastWord.ReplaceAllString(i.text, "") - case tcell.KeyBackspace, tcell.KeyBackspace2: // Delete last character. - if len(i.text) == 0 { - break + lastWord := regexp.MustCompile(`\S+\s*$`) + newText := lastWord.ReplaceAllString(i.text[:i.cursorPos], "") + i.text[i.cursorPos:] + i.cursorPos -= len(i.text) - len(newText) + i.text = newText + 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) - i.text = string(runes[:len(runes)-1]) + case tcell.KeyDelete: // Delete character after the cursor. + 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. if i.done != nil { i.done(key) diff --git a/vendor/maunium.net/go/tview/list.go b/vendor/maunium.net/go/tview/list.go index bc5be85..e8dc5dd 100644 --- a/vendor/maunium.net/go/tview/list.go +++ b/vendor/maunium.net/go/tview/list.go @@ -85,6 +85,19 @@ func (l *List) GetCurrentItem() int { 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. func (l *List) SetMainTextColor(color tcell.Color) *List { 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 // 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 return l } diff --git a/vendor/maunium.net/go/tview/modal.go b/vendor/maunium.net/go/tview/modal.go index f53a265..f5e92f1 100644 --- a/vendor/maunium.net/go/tview/modal.go +++ b/vendor/maunium.net/go/tview/modal.go @@ -40,6 +40,11 @@ func NewModal() *Modal { SetButtonBackgroundColor(Styles.PrimitiveBackgroundColor). SetButtonTextColor(Styles.PrimaryTextColor) 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.SetBorder(true). SetBackgroundColor(Styles.ContrastBackgroundColor). @@ -81,6 +86,16 @@ func (m *Modal) AddButtons(labels []string) *Modal { 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) } return m diff --git a/vendor/maunium.net/go/tview/semigraphics.go b/vendor/maunium.net/go/tview/semigraphics.go new file mode 100644 index 0000000..2455c87 --- /dev/null +++ b/vendor/maunium.net/go/tview/semigraphics.go @@ -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) +} diff --git a/vendor/maunium.net/go/tview/table.go b/vendor/maunium.net/go/tview/table.go index 2491ec7..6c636e7 100644 --- a/vendor/maunium.net/go/tview/table.go +++ b/vendor/maunium.net/go/tview/table.go @@ -231,6 +231,10 @@ type Table struct { // The number of visible rows the last time the table was drawn. 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 // selected cell. If entire rows selected, the column value is undefined. // Likewise for entire columns. @@ -276,9 +280,21 @@ func (t *Table) SetBordersColor(color tcell.Color) *Table { 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 // 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 // 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 -// 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. // // 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 -// 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. func (t *Table) GetCell(row, column int) *TableCell { 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] } +// 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. func (t *Table) GetRowCount() int { return len(t.cells) @@ -644,7 +685,6 @@ ColumnLoop: } expWidth := toDistribute * expansion / expansionTotal widths[index] += expWidth - tableWidth += expWidth toDistribute -= expWidth expansionTotal -= expansion } @@ -668,24 +708,24 @@ ColumnLoop: // Draw borders. rowY *= 2 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 rowY == 0 { - ch = GraphicsTopLeftCorner + ch = Borders.TopLeft } else { - ch = GraphicsLeftT + ch = Borders.LeftT } } else if rowY == 0 { - ch = GraphicsTopT + ch = Borders.TopT } drawBorder(columnX, rowY, ch) rowY++ if rowY >= height { break // No space for the text anymore. } - drawBorder(columnX, rowY, GraphicsVertBar) + drawBorder(columnX, rowY, Borders.Vertical) } else if columnIndex > 0 { // Draw 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)) if StringWidth(cell.Text)-printed > 0 && printed > 0 { _, _, 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. if rowY := 2 * len(rows); t.borders && rowY < height { 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 { - ch = GraphicsBottomLeftCorner + ch = Borders.BottomLeft } drawBorder(columnX, rowY, ch) } @@ -730,26 +770,31 @@ ColumnLoop: for rowY := range rows { rowY *= 2 if rowY+1 < height { - drawBorder(columnX, rowY+1, GraphicsVertBar) + drawBorder(columnX, rowY+1, Borders.Vertical) } - ch := GraphicsRightT + ch := Borders.RightT if rowY == 0 { - ch = GraphicsTopRightCorner + ch = Borders.TopRight } drawBorder(columnX, rowY, ch) } 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. - 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 bx := 0; bx < w && fromX+bx < x+width; bx++ { m, c, style, _ := screen.GetContent(fromX+bx, fromY+by) - if selected { - fg, _, _ := style.Decompose() + fg, bg, a := style.Decompose() + if invert { if fg == textColor || fg == t.bordersColor { fg = backgroundColor } @@ -758,10 +803,16 @@ ColumnLoop: } style = style.Background(textColor).Foreground(fg) } else { - if backgroundColor == tcell.ColorDefault { - continue + if backgroundColor != tcell.ColorDefault { + 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) } @@ -770,11 +821,12 @@ ColumnLoop: // Color the cell backgrounds. To avoid undesirable artefacts, we combine // 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 text tcell.Color selected bool - }) + } + cellsByBackgroundColor := make(map[tcell.Color][]*cellInfo) var backgroundColors []tcell.Color for rowY, row := range rows { columnX := 0 @@ -794,11 +846,7 @@ ColumnLoop: columnSelected := t.columnsSelectable && !t.rowsSelectable && column == t.selectedColumn cellSelected := !cell.NotSelectable && (columnSelected || rowSelected || t.rowsSelectable && t.columnsSelectable && column == t.selectedColumn && row == t.selectedRow) entries, ok := cellsByBackgroundColor[cell.BackgroundColor] - cellsByBackgroundColor[cell.BackgroundColor] = append(entries, &struct { - x, y, w, h int - text tcell.Color - selected bool - }{ + cellsByBackgroundColor[cell.BackgroundColor] = append(entries, &cellInfo{ x: bx, y: by, w: bw, @@ -822,13 +870,18 @@ ColumnLoop: _, _, lj := c.Hcl() return li < lj }) + selFg, selBg, selAttr := t.selectedStyle.Decompose() for _, bgColor := range backgroundColors { entries := cellsByBackgroundColor[bgColor] for _, cell := range entries { 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 { - 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) } } } diff --git a/vendor/maunium.net/go/tview/textview.go b/vendor/maunium.net/go/tview/textview.go index 44aeb1e..63d9796 100644 --- a/vendor/maunium.net/go/tview/textview.go +++ b/vendor/maunium.net/go/tview/textview.go @@ -31,7 +31,7 @@ type textViewIndex struct { // 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 // but if a handler is installed via SetChangedFunc(), you can cause it to be -// redrawn. +// redrawn. (See SetChangedFunc() for more details.) // // Navigation // @@ -103,6 +103,10 @@ type TextView struct { // during re-indexing. Set to -1 if there is no current highlight. 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. highlights map[string]struct{} @@ -170,6 +174,7 @@ func NewTextView() *TextView { align: AlignLeft, wrap: true, textColor: Styles.PrimaryTextColor, + regions: 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 -// text view has changed. This is typically used to cause the application to -// redraw the screen. +// text view has changed. This is useful when text is written to this io.Writer +// 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 { t.changed = handler return t @@ -270,6 +287,16 @@ func (t *TextView) SetDoneFunc(handler func(key tcell.Key)) *TextView { 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 // is scrollable. func (t *TextView) ScrollToBeginning() *TextView { @@ -294,6 +321,12 @@ func (t *TextView) ScrollToEnd() *TextView { 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. func (t *TextView) Clear() *TextView { t.buffer = nil @@ -420,13 +453,33 @@ func (t *TextView) GetRegionText(regionID string) string { 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 // replaced with TabSize space characters. A "\n" or "\r\n" will be interpreted // as a new line. func (t *TextView) Write(p []byte) (n int, err error) { // Notify at the end. - if t.changed != nil { - defer t.changed() + t.Lock() + changed := t.changed + t.Unlock() + if changed != nil { + defer changed() // Deadlocks may occur if we lock here. } t.Lock() @@ -492,7 +545,7 @@ func (t *TextView) reindexBuffer(width int) { return // Nothing has changed. We can still use the current index. } 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 width < 1 { @@ -511,8 +564,9 @@ func (t *TextView) reindexBuffer(width int) { colorTags [][]string escapeIndices [][]int ) + strippedStr := str if t.dynamicColors { - colorTagIndices, colorTags, escapeIndices, str, _ = decomposeString(str) + colorTagIndices, colorTags, escapeIndices, strippedStr, _ = decomposeString(str) } // Find all regions in this line. Then remove them. @@ -523,14 +577,12 @@ func (t *TextView) reindexBuffer(width int) { if t.regions { regionIndices = regionPattern.FindAllStringIndex(str, -1) regions = regionPattern.FindAllStringSubmatch(str, -1) - str = regionPattern.ReplaceAllString(str, "") - if !t.dynamicColors { - // We haven't detected escape tags yet. Do it now. - escapeIndices = escapePattern.FindAllStringIndex(str, -1) - str = escapePattern.ReplaceAllString(str, "[$1$2]") - } + strippedStr = regionPattern.ReplaceAllString(strippedStr, "") } + // We don't need the original string anymore for now. + str = strippedStr + // Split the line if required. var splitLines []string if t.wrap && len(str) > 0 { @@ -574,15 +626,53 @@ func (t *TextView) reindexBuffer(width int) { // Shift original position with tags. lineLength := len(splitLine) + remainingLength := lineLength + tagEnd := originalPos + totalTagLength := 0 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. - originalPos += colorTagIndices[colorPos][1] - colorTagIndices[colorPos][0] foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colorTags[colorPos]) colorPos++ - } else if regionPos < len(regionIndices) && regionIndices[regionPos][0] <= originalPos+lineLength { + case 1: // Process region tags. - originalPos += regionIndices[regionPos][1] - regionIndices[regionPos][0] regionID = regions[regionPos][1] _, highlighted = t.highlights[regionID] @@ -591,23 +681,21 @@ func (t *TextView) reindexBuffer(width int) { line := len(t.index) if t.fromHighlight < 0 { t.fromHighlight, t.toHighlight = line, line + t.posHighlight = runewidth.StringWidth(splitLine[:strippedTagStart]) } else if line > t.toHighlight { t.toHighlight = line } } regionPos++ - } else if escapePos < len(escapeIndices) && escapeIndices[escapePos][0] <= originalPos+lineLength { + case 2: // Process escape tags. - originalPos++ escapePos++ - } else { - break } } // Advance to next line. - originalPos += lineLength + originalPos += lineLength + totalTagLength // Append this line. line.NextPos = originalPos @@ -649,7 +737,7 @@ func (t *TextView) Draw(screen tcell.Screen) { t.pageSize = height // If the width has changed, we need to reindex. - if width != t.lastWidth { + if width != t.lastWidth && t.wrap { t.index = nil } t.lastWidth = width @@ -672,6 +760,16 @@ func (t *TextView) Draw(screen tcell.Screen) { // No, let's move to the start of the highlights. 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 @@ -737,8 +835,9 @@ func (t *TextView) Draw(screen tcell.Screen) { colorTags [][]string escapeIndices [][]int ) + strippedText := text if t.dynamicColors { - colorTagIndices, colorTags, escapeIndices, _, _ = decomposeString(text) + colorTagIndices, colorTags, escapeIndices, strippedText, _ = decomposeString(text) } // Get regions. @@ -749,8 +848,10 @@ func (t *TextView) Draw(screen tcell.Screen) { if t.regions { regionIndices = regionPattern.FindAllStringIndex(text, -1) regions = regionPattern.FindAllStringSubmatch(text, -1) + strippedText = regionPattern.ReplaceAllString(strippedText, "") if !t.dynamicColors { 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. - var currentTag, currentRegion, currentEscapeTag, skipped, runeSeqWidth int - runeSequence := make([]rune, 0, 10) - flush := func() { - if len(runeSequence) == 0 { - return + var colorPos, regionPos, escapePos, tagOffset, skipped int + iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { + // Process tags. + for { + 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. @@ -803,87 +922,30 @@ func (t *TextView) Draw(screen tcell.Screen) { 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. if !t.wrap && skipped < skip { - skipped += chWidth - continue + skipped += screenWidth + return false } // Stop at the right border. - if posX+runeSeqWidth+chWidth > width { - break + if posX+screenWidth > width { + return true } - // Flush the rune sequence. - flush() + // Draw the character. + 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. - runeSequence = append(runeSequence, ch) - runeSeqWidth += chWidth - } - if posX+runeSeqWidth <= width { - flush() - } + // Advance. + posX += screenWidth + return false + }) } // If this view is not scrollable, we'll purge the buffer of lines that have diff --git a/vendor/maunium.net/go/tview/treeview.go b/vendor/maunium.net/go/tview/treeview.go new file mode 100644 index 0000000..1b0af21 --- /dev/null +++ b/vendor/maunium.net/go/tview/treeview.go @@ -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() + }) +} diff --git a/vendor/maunium.net/go/tview/util.go b/vendor/maunium.net/go/tview/util.go index 41e52dd..e408b18 100644 --- a/vendor/maunium.net/go/tview/util.go +++ b/vendor/maunium.net/go/tview/util.go @@ -1,11 +1,9 @@ package tview import ( - "fmt" "math" "regexp" "strconv" - "strings" "unicode" "maunium.net/go/tcell" @@ -19,97 +17,13 @@ const ( 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. var ( 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_,;: \-\.]*)"\]`) escapePattern = 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+`) ) @@ -204,13 +118,12 @@ func overlayStyle(background tcell.Color, defaultStyle tcell.Style, fgColor, bgC defFg, defBg, defAttr := defaultStyle.Decompose() style := defaultStyle.Background(background) - if fgColor == "-" { - style = style.Foreground(defFg) - } else if fgColor != "" { + style = style.Foreground(defFg) + if fgColor != "" { style = style.Foreground(tcell.GetColor(fgColor)) } - if bgColor == "-" { + if bgColor == "-" || bgColor == "" && defBg != tcell.ColorDefault { style = style.Background(defBg) } else if 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. // See the package description for details. // -// Returns the number of actual runes printed (not including color tags) and the -// actual width used for the printed runes. +// Returns the number of actual bytes of the text printed (including color tags) +// 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) { 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. - colorIndices, colors, escapeIndices, strippedText, _ := decomposeString(text) + colorIndices, colors, escapeIndices, strippedText, strippedWidth := decomposeString(text) - // We deal with runes, not with bytes. - runes := []rune(strippedText) - - // This helper function takes positions for a substring of "runes" and returns - // a new string corresponding to this substring, making sure printing that - // substring will observe color tags. - substring := func(from, to int) string { + // We want to reduce all alignments to AlignLeft. + if align == AlignRight { + if strippedWidth <= maxWidth { + // There's enough space for the entire text. + return printWithStyle(screen, text, x+maxWidth-strippedWidth, y, maxWidth, AlignLeft, style) + } + // Trim characters off the beginning. var ( - colorPos, escapePos, runePos, startPos int + bytes, width, colorPos, escapePos, tagOffset int foregroundColor, backgroundColor, attributes string ) - if from >= len(runes) { - return "" - } - for pos := range text { - // Handle color tags. - if colorPos < len(colorIndices) && pos >= colorIndices[colorPos][0] && pos < colorIndices[colorPos][1] { - if pos == colorIndices[colorPos][1]-1 { - if runePos <= from { - foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos]) - } - colorPos++ + _, originalBackground, _ := style.Decompose() + iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { + // Update color/escape tag offset and style. + if colorPos < len(colorIndices) && textPos+tagOffset >= colorIndices[colorPos][0] && textPos+tagOffset < colorIndices[colorPos][1] { + 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) && textPos+tagOffset >= escapeIndices[escapePos][0] && textPos+tagOffset < escapeIndices[escapePos][1] { + 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 } - - // Handle escape tags. - if escapePos < len(escapeIndices) && pos >= escapeIndices[escapePos][0] && pos < escapeIndices[escapePos][1] { - 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) + return false + }) + return bytes, width } else if align == AlignCenter { - width := runewidth.StringWidth(strippedText) - if width == maxWidth { + if strippedWidth == maxWidth { // Use the exact space. return printWithStyle(screen, text, x, y, maxWidth, AlignLeft, style) - } else if width < maxWidth { + } else if strippedWidth < maxWidth { // 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) } else { // Chop off runes until we have a perfect fit. var choppedLeft, choppedRight, leftIndex, rightIndex int - rightIndex = len(runes) - 1 - for rightIndex > leftIndex && width-choppedLeft-choppedRight > maxWidth { + rightIndex = len(strippedText) + for rightIndex-1 > leftIndex && strippedWidth-choppedLeft-choppedRight > maxWidth { if choppedLeft < choppedRight { - leftWidth := runewidth.RuneWidth(runes[leftIndex]) - choppedLeft += leftWidth - leftIndex++ - for leftIndex < len(runes) && leftIndex < rightIndex && runewidth.RuneWidth(runes[leftIndex]) == 0 { - leftIndex++ - } + // Iterate on the left by one character. + iterateString(strippedText[leftIndex:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { + choppedLeft += screenWidth + leftIndex += textWidth + return true + }) } else { - rightWidth := runewidth.RuneWidth(runes[rightIndex]) - choppedRight += rightWidth - rightIndex-- + // Iterate on the right by one character. + iterateStringReverse(strippedText[leftIndex:rightIndex], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { + 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. - drawn := 0 - drawnWidth := 0 var ( - colorPos, escapePos int - foregroundColor, backgroundColor, attributes string + drawn, drawnWidth, colorPos, escapePos, tagOffset int + foregroundColor, backgroundColor, attributes string ) - runeSequence := make([]rune, 0, 10) - runeSeqWidth := 0 - flush := func() { - if len(runeSequence) == 0 { - return // Nothing to flush. + iterateString(strippedText, func(main rune, comb []rune, textPos, length, screenPos, screenWidth int) bool { + // Only continue if there is still space. + if drawnWidth+screenWidth > maxWidth { + return true + } + + // 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. @@ -418,70 +352,23 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int, _, _, finalStyle, _ := screen.GetContent(finalX, y) _, background, _ := finalStyle.Decompose() finalStyle = overlayStyle(background, style, foregroundColor, backgroundColor, attributes) - 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++ { - // 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 + for offset := screenWidth - 1; offset >= 0; offset-- { + // To avoid undesired effects, we populate all cells. + if offset == 0 { + screen.SetContent(finalX+offset, y, main, comb, finalStyle) + } else { + screen.SetContent(finalX+offset, y, ' ', nil, finalStyle) } } - // Check if we have enough space for this rune. - chWidth := runewidth.RuneWidth(ch) - if drawnWidth+chWidth > maxWidth { - break // No. We're done then. - } + // Advance. + drawn += length + drawnWidth += screenWidth - // Put this rune in the queue. - 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 false + }) - } - if drawnWidth+runeSeqWidth <= maxWidth { - flush() - } - - return drawn, drawnWidth + return drawn + tagOffset + len(escapeIndices), drawnWidth } // 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) // 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 - // positions are in stripped index space. - addLine := func(from, to int) { - // Shift indices back to original index space. - var colorTagIndex, escapeIndex int - for colorTagIndex < len(colorTagIndices) && to >= colorTagIndices[colorTagIndex][0] || - escapeIndex < len(escapeIndices) && to >= escapeIndices[escapeIndex][0] { - past := 0 - if colorTagIndex < len(colorTagIndices) { - tagWidth := colorTagIndices[colorTagIndex][1] - colorTagIndices[colorTagIndex][0] - if colorTagIndices[colorTagIndex][0] < from { - from += tagWidth - to += tagWidth - 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. + // Process stripped text one character at a time. + var ( + colorPos, escapePos, breakpointPos, tagOffset int + lastBreakpoint, lastContinuation, currentLineStart int + lineWidth, continuationWidth int + newlineBreakpoint bool + ) + unescape := func(substr string, startIndex int) string { + // A helper function to unescape escaped tags. + for index := escapePos; index >= 0; index-- { + if index < len(escapeIndices) && startIndex > escapeIndices[index][0] && startIndex < escapeIndices[index][1]-1 { + pos := escapeIndices[index][1] - 2 - startIndex + return substr[:pos] + substr[pos+1:] } } - lines = append(lines, text[from:to]) + return substr } - - // Determine final breakpoints. - var start, lastEnd, newStart, breakPoint int - for { - // What's our candidate string? - var candidate string - if breakPoint < len(breakPoints) { - candidate = text[start:breakPoints[breakPoint][1]] - } else { - candidate = text[start:] + iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { + // Handle colour tags. + if colorPos < len(colorTagIndices) && textPos+tagOffset >= colorTagIndices[colorPos][0] && textPos+tagOffset < colorTagIndices[colorPos][1] { + tagOffset += colorTagIndices[colorPos][1] - colorTagIndices[colorPos][0] + colorPos++ } - candidate = strings.TrimRightFunc(candidate, unicode.IsSpace) - if runewidth.StringWidth(candidate) >= width { - // We're past the available width. - if lastEnd > start { - // Use the previous candidate. - 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++ - } + // Handle escape tags. + if escapePos < len(escapeIndices) && textPos+tagOffset == escapeIndices[escapePos][1]-2 { + tagOffset++ + escapePos++ } + + // 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 } -// 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 // 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: @@ -643,3 +485,121 @@ func PrintJoinedBorder(screen tcell.Screen, x, y int, ch rune, color tcell.Color func Escape(text string) string { 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 +}