Add device list and legacy verification commands
This commit is contained in:
parent
ecdd4f08cb
commit
77a1514c90
@ -162,6 +162,8 @@ func (c *Container) PasswordLogin(user, password string) error {
|
||||
},
|
||||
Password: password,
|
||||
InitialDeviceDisplayName: "gomuks",
|
||||
|
||||
StoreCredentials: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@ -171,8 +173,6 @@ func (c *Container) PasswordLogin(user, password string) error {
|
||||
}
|
||||
|
||||
func (c *Container) finishLogin(resp *mautrix.RespLogin) {
|
||||
c.client.SetCredentials(resp.UserID, resp.AccessToken)
|
||||
c.client.DeviceID = resp.DeviceID
|
||||
c.config.UserID = resp.UserID
|
||||
c.config.DeviceID = resp.DeviceID
|
||||
c.config.AccessToken = resp.AccessToken
|
||||
@ -218,6 +218,8 @@ func (c *Container) SingleSignOn() error {
|
||||
Type: "m.login.token",
|
||||
Token: loginToken,
|
||||
InitialDeviceDisplayName: "gomuks",
|
||||
|
||||
StoreCredentials: true,
|
||||
})
|
||||
if err != nil {
|
||||
respondHTML(w, http.StatusForbidden, err.Error())
|
||||
|
@ -73,6 +73,19 @@ func (cache *RoomCache) IsEncrypted(roomID id.RoomID) bool {
|
||||
return room != nil && room.Encrypted
|
||||
}
|
||||
|
||||
func (cache *RoomCache) GetEncryptionEvent(roomID id.RoomID) *event.EncryptionEventContent {
|
||||
room := cache.Get(roomID)
|
||||
evt := room.GetStateEvent(event.StateEncryption, "")
|
||||
if evt == nil {
|
||||
return nil
|
||||
}
|
||||
content, ok := evt.Content.Parsed.(*event.EncryptionEventContent)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
func (cache *RoomCache) FindSharedRooms(userID id.UserID) (shared []id.RoomID) {
|
||||
// FIXME this disables unloading so TouchNode wouldn't try to double-lock
|
||||
cache.DisableUnloading()
|
||||
|
@ -20,6 +20,8 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mattn/go-runewidth"
|
||||
|
||||
"maunium.net/go/gomuks/config"
|
||||
"maunium.net/go/gomuks/debug"
|
||||
"maunium.net/go/gomuks/interface"
|
||||
@ -45,6 +47,8 @@ type Command struct {
|
||||
OrigText string
|
||||
}
|
||||
|
||||
type CommandAutocomplete Command
|
||||
|
||||
func (cmd *Command) Reply(message string, args ...interface{}) {
|
||||
cmd.Room.AddServiceMessage(fmt.Sprintf(message, args...))
|
||||
cmd.UI.Render()
|
||||
@ -60,12 +64,15 @@ func (alias *Alias) Process(cmd *Command) *Command {
|
||||
}
|
||||
|
||||
type CommandHandler func(cmd *Command)
|
||||
type CommandAutocompleter func(cmd *CommandAutocomplete) (completions []string, newText string)
|
||||
|
||||
type CommandProcessor struct {
|
||||
gomuksPointerContainer
|
||||
|
||||
aliases map[string]*Alias
|
||||
commands map[string]CommandHandler
|
||||
|
||||
autocompleters map[string]CommandAutocompleter
|
||||
}
|
||||
|
||||
func NewCommandProcessor(parent *MainView) *CommandProcessor {
|
||||
@ -97,6 +104,12 @@ func NewCommandProcessor(parent *MainView) *CommandProcessor {
|
||||
"dl": {"download"},
|
||||
"o": {"open"},
|
||||
},
|
||||
autocompleters: map[string]CommandAutocompleter{
|
||||
"devices": autocompleteDevice,
|
||||
"device": autocompleteDevice,
|
||||
"verify": autocompleteDevice,
|
||||
"unverify": autocompleteDevice,
|
||||
},
|
||||
commands: map[string]CommandHandler{
|
||||
"unknown-command": cmdUnknownCommand,
|
||||
|
||||
@ -140,6 +153,11 @@ func NewCommandProcessor(parent *MainView) *CommandProcessor {
|
||||
"trace": cmdTrace,
|
||||
|
||||
"fingerprint": cmdFingerprint,
|
||||
"devices": cmdDevices,
|
||||
"verify": cmdVerify,
|
||||
"device": cmdDevice,
|
||||
"unverify": cmdUnverify,
|
||||
"blacklist": cmdBlacklist,
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -169,6 +187,47 @@ func (ch *CommandProcessor) ParseCommand(roomView *RoomView, text string) *Comma
|
||||
}
|
||||
}
|
||||
|
||||
func (ch *CommandProcessor) Autocomplete(roomView *RoomView, text string, cursorOffset int) ([]string, string, bool) {
|
||||
var completions []string
|
||||
if cmd := (*CommandAutocomplete)(ch.ParseCommand(roomView, text)); cmd == nil {
|
||||
return completions, text, false
|
||||
} else if handler, ok := ch.autocompleters[cmd.Command]; !ok {
|
||||
return completions, text, false
|
||||
} else if cursorOffset != runewidth.StringWidth(text) {
|
||||
return completions, text, false
|
||||
} else {
|
||||
completions, newText := handler(cmd)
|
||||
if newText == "" {
|
||||
newText = text
|
||||
}
|
||||
return completions, newText, true
|
||||
}
|
||||
}
|
||||
|
||||
func (ch *CommandProcessor) AutocompleteCommand(word string) (completions []string) {
|
||||
if word[0] != '/' {
|
||||
return
|
||||
}
|
||||
word = word[1:]
|
||||
for alias := range ch.aliases {
|
||||
if alias == word {
|
||||
return []string{"/" + alias}
|
||||
}
|
||||
if strings.HasPrefix(alias, word) {
|
||||
completions = append(completions, "/"+alias)
|
||||
}
|
||||
}
|
||||
for command := range ch.commands {
|
||||
if command == word {
|
||||
return []string{"/" + command}
|
||||
}
|
||||
if strings.HasPrefix(command, word) {
|
||||
completions = append(completions, "/"+command)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (ch *CommandProcessor) HandleCommand(cmd *Command) {
|
||||
defer debug.Recover()
|
||||
if cmd == nil {
|
||||
|
195
ui/commands.go
195
ui/commands.go
@ -37,6 +37,7 @@ import (
|
||||
"github.com/russross/blackfriday/v2"
|
||||
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/crypto"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/format"
|
||||
"maunium.net/go/mautrix/id"
|
||||
@ -365,6 +366,187 @@ func cmdFingerprint(cmd *Command) {
|
||||
}
|
||||
}
|
||||
|
||||
// region TODO these four functions currently use the crypto internals directly. switch to interfaces before releasing
|
||||
|
||||
func autocompleteDeviceUserID(cmd *CommandAutocomplete) (completions []string, newText string) {
|
||||
userCompletions := cmd.Room.AutocompleteUser(cmd.Args[0])
|
||||
if len(userCompletions) == 1 {
|
||||
newText = fmt.Sprintf("/%s %s ", cmd.OrigCommand, userCompletions[0].id)
|
||||
} else {
|
||||
completions = make([]string, len(userCompletions))
|
||||
for i, completion := range userCompletions {
|
||||
completions[i] = completion.id
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func autocompleteDeviceDeviceID(cmd *CommandAutocomplete) (completions []string, newText string) {
|
||||
mach := cmd.Matrix.Crypto().(*crypto.OlmMachine)
|
||||
devices, err := mach.CryptoStore.GetDevices(id.UserID(cmd.Args[0]))
|
||||
if len(devices) == 0 || err != nil {
|
||||
return
|
||||
}
|
||||
var completedDeviceID id.DeviceID
|
||||
if len(cmd.Args) > 1 {
|
||||
existingID := strings.ToUpper(cmd.Args[1])
|
||||
for _, device := range devices {
|
||||
deviceIDStr := string(device.DeviceID)
|
||||
if deviceIDStr == existingID {
|
||||
// We don't want to do any autocompletion if there's already a full device ID there.
|
||||
return []string{}, ""
|
||||
} else if strings.HasPrefix(strings.ToUpper(device.Name), existingID) || strings.HasPrefix(deviceIDStr, existingID) {
|
||||
completedDeviceID = device.DeviceID
|
||||
completions = append(completions, fmt.Sprintf("%s (%s)", device.DeviceID, device.Name))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
completions = make([]string, len(devices))
|
||||
i := 0
|
||||
for _, device := range devices {
|
||||
completedDeviceID = device.DeviceID
|
||||
completions[i] = fmt.Sprintf("%s (%s)", device.DeviceID, device.Name)
|
||||
i++
|
||||
}
|
||||
}
|
||||
if len(completions) == 1 {
|
||||
newText = fmt.Sprintf("/%s %s %s ", cmd.OrigCommand, cmd.Args[0], completedDeviceID)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func autocompleteDevice(cmd *CommandAutocomplete) ([]string, string) {
|
||||
if len(cmd.Args) == 0 {
|
||||
return []string{}, ""
|
||||
} else if len(cmd.Args) == 1 && !unicode.IsSpace(rune(cmd.RawArgs[len(cmd.RawArgs)-1])) {
|
||||
return autocompleteDeviceUserID(cmd)
|
||||
} else if cmd.Command != "devices" {
|
||||
return autocompleteDeviceDeviceID(cmd)
|
||||
}
|
||||
return []string{}, ""
|
||||
}
|
||||
|
||||
func getDevice(cmd *Command) *crypto.DeviceIdentity {
|
||||
if len(cmd.Args) < 2 {
|
||||
cmd.Reply("Usage: /%s <user id> <device id> [fingerprint]", cmd.Command)
|
||||
return nil
|
||||
}
|
||||
mach := cmd.Matrix.Crypto().(*crypto.OlmMachine)
|
||||
device, err := mach.GetOrFetchDevice(id.UserID(cmd.Args[0]), id.DeviceID(cmd.Args[1]))
|
||||
if err != nil {
|
||||
cmd.Reply("Failed to get device: %v", err)
|
||||
return nil
|
||||
}
|
||||
return device
|
||||
}
|
||||
|
||||
func putDevice(cmd *Command, device *crypto.DeviceIdentity, action string) {
|
||||
mach := cmd.Matrix.Crypto().(*crypto.OlmMachine)
|
||||
err := mach.CryptoStore.PutDevice(device.UserID, device)
|
||||
if err != nil {
|
||||
cmd.Reply("Failed to save device: %v", err)
|
||||
} else {
|
||||
cmd.Reply("Successfully %s %s/%s (%s)", action, device.UserID, device.DeviceID, device.Name)
|
||||
}
|
||||
mach.OnDevicesChanged(device.UserID)
|
||||
}
|
||||
|
||||
func cmdDevices(cmd *Command) {
|
||||
if len(cmd.Args) == 0 {
|
||||
cmd.Reply("Usage: /devices <user id>")
|
||||
return
|
||||
}
|
||||
userID := id.UserID(cmd.Args[0])
|
||||
mach := cmd.Matrix.Crypto().(*crypto.OlmMachine)
|
||||
devices, err := mach.CryptoStore.GetDevices(userID)
|
||||
if err != nil {
|
||||
cmd.Reply("Failed to get device list: %v", err)
|
||||
}
|
||||
if len(devices) == 0 {
|
||||
cmd.Reply("Fetching device list from server...")
|
||||
devices = mach.LoadDevices(userID)
|
||||
}
|
||||
if len(devices) == 0 {
|
||||
cmd.Reply("No devices found for %s", userID)
|
||||
return
|
||||
}
|
||||
var buf strings.Builder
|
||||
for _, device := range devices {
|
||||
_, _ = fmt.Fprintf(&buf, "%s (%s) - %s - %s\n", device.DeviceID, device.Name, device.Trust.String(), device.Fingerprint())
|
||||
}
|
||||
resp := buf.String()
|
||||
cmd.Reply(resp[:len(resp)-1])
|
||||
}
|
||||
|
||||
func cmdDevice(cmd *Command) {
|
||||
device := getDevice(cmd)
|
||||
if device == nil {
|
||||
return
|
||||
}
|
||||
deviceType := "Device"
|
||||
if device.Deleted {
|
||||
deviceType = "Deleted device"
|
||||
}
|
||||
cmd.Reply("%s %s of %s\nFingerprint: %s\nIdentity key: %s\nDevice name: %s\nTrust state: %s",
|
||||
deviceType, device.DeviceID, device.UserID,
|
||||
device.Fingerprint(), device.IdentityKey,
|
||||
device.Name, device.Trust.String())
|
||||
}
|
||||
|
||||
func cmdVerify(cmd *Command) {
|
||||
device := getDevice(cmd)
|
||||
if device == nil {
|
||||
return
|
||||
}
|
||||
if len(cmd.Args) == 2 {
|
||||
cmd.Reply("Interactive verification UI is not yet implemented")
|
||||
} else {
|
||||
fingerprint := strings.Join(cmd.Args[2:], "")
|
||||
if string(device.SigningKey) != fingerprint {
|
||||
cmd.Reply("Mismatching fingerprint")
|
||||
return
|
||||
}
|
||||
action := "verified"
|
||||
if device.Trust == crypto.TrustStateBlacklisted {
|
||||
action = "unblacklisted and verified"
|
||||
}
|
||||
device.Trust = crypto.TrustStateVerified
|
||||
putDevice(cmd, device, action)
|
||||
}
|
||||
}
|
||||
|
||||
func cmdUnverify(cmd *Command) {
|
||||
device := getDevice(cmd)
|
||||
if device == nil {
|
||||
return
|
||||
}
|
||||
if device.Trust == crypto.TrustStateUnset {
|
||||
cmd.Reply("That device is already not verified")
|
||||
return
|
||||
}
|
||||
action := "unverified"
|
||||
if device.Trust == crypto.TrustStateBlacklisted {
|
||||
action = "unblacklisted"
|
||||
}
|
||||
device.Trust = crypto.TrustStateUnset
|
||||
putDevice(cmd, device, action)
|
||||
}
|
||||
|
||||
func cmdBlacklist(cmd *Command) {
|
||||
device := getDevice(cmd)
|
||||
if device == nil {
|
||||
return
|
||||
}
|
||||
action := "blacklisted"
|
||||
if device.Trust == crypto.TrustStateVerified {
|
||||
action = "unverified and blacklisted"
|
||||
}
|
||||
device.Trust = crypto.TrustStateBlacklisted
|
||||
putDevice(cmd, device, action)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
func cmdHeapProfile(cmd *Command) {
|
||||
if len(cmd.Args) == 0 || cmd.Args[0] != "nogc" {
|
||||
runtime.GC()
|
||||
@ -449,7 +631,18 @@ Things: rooms, users, baremessages, images, typingnotif
|
||||
/rainbowme <message> - Send rainbow text in an emote.
|
||||
/reply [text] - Reply to the selected message.
|
||||
/react <reaction> - React to the selected message.
|
||||
/redact [reason] - Redact the selected message.
|
||||
/redact [reason] - Redact the selected message.
|
||||
|
||||
# Encryption
|
||||
/fingerprint - View the fingerprint of your device.
|
||||
|
||||
/devices <user id> - View the device list of a user.
|
||||
/device <user id> <device id> - Show info about a specific device.
|
||||
/unverify <user id> <device id> - Un-verify a device.
|
||||
/blacklist <user id> <device id> - Blacklist a device.
|
||||
/verify <user id> <device id> [fingerprint]
|
||||
- Verify a device. If the fingerprint is not provided,
|
||||
interactive emoji verification will be started.
|
||||
|
||||
# Rooms
|
||||
/pm <user id> <...> - Create a private chat with the given user(s).
|
||||
|
190
ui/room-view.go
190
ui/room-view.go
@ -22,15 +22,18 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/kyokomi/emoji"
|
||||
"github.com/mattn/go-runewidth"
|
||||
"github.com/zyedidia/clipboard"
|
||||
|
||||
"maunium.net/go/mautrix/crypto/attachment"
|
||||
"maunium.net/go/mauview"
|
||||
"maunium.net/go/tcell"
|
||||
|
||||
"maunium.net/go/gomuks/lib/util"
|
||||
"maunium.net/go/mautrix/crypto/attachment"
|
||||
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
@ -39,7 +42,6 @@ import (
|
||||
"maunium.net/go/gomuks/debug"
|
||||
"maunium.net/go/gomuks/interface"
|
||||
"maunium.net/go/gomuks/lib/open"
|
||||
"maunium.net/go/gomuks/lib/util"
|
||||
"maunium.net/go/gomuks/matrix/muksevt"
|
||||
"maunium.net/go/gomuks/matrix/rooms"
|
||||
"maunium.net/go/gomuks/ui/messages"
|
||||
@ -420,65 +422,6 @@ func (view *RoomView) SetTyping(users []id.UserID) {
|
||||
}
|
||||
}
|
||||
|
||||
type completion struct {
|
||||
displayName string
|
||||
id string
|
||||
}
|
||||
|
||||
func (view *RoomView) autocompleteUser(existingText string) (completions []completion) {
|
||||
textWithoutPrefix := strings.TrimPrefix(existingText, "@")
|
||||
for userID, user := range view.Room.GetMembers() {
|
||||
if user.Displayname == textWithoutPrefix || string(userID) == existingText {
|
||||
// Exact match, return that.
|
||||
return []completion{{user.Displayname, string(userID)}}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(user.Displayname, textWithoutPrefix) || strings.HasPrefix(string(userID), existingText) {
|
||||
completions = append(completions, completion{user.Displayname, string(userID)})
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (view *RoomView) autocompleteRoom(existingText string) (completions []completion) {
|
||||
for _, room := range view.parent.rooms {
|
||||
alias := string(room.Room.GetCanonicalAlias())
|
||||
if alias == existingText {
|
||||
// Exact match, return that.
|
||||
return []completion{{alias, string(room.Room.ID)}}
|
||||
}
|
||||
if strings.HasPrefix(alias, existingText) {
|
||||
completions = append(completions, completion{alias, string(room.Room.ID)})
|
||||
continue
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (view *RoomView) autocompleteEmoji(word string) (completions []string) {
|
||||
if len(word) == 0 || word[0] != ':' {
|
||||
return
|
||||
}
|
||||
var valueCompletion1 string
|
||||
var manyValues bool
|
||||
for name, value := range emoji.CodeMap() {
|
||||
if name == word {
|
||||
return []string{value}
|
||||
} else if strings.HasPrefix(name, word) {
|
||||
completions = append(completions, name)
|
||||
if valueCompletion1 == "" {
|
||||
valueCompletion1 = value
|
||||
} else if valueCompletion1 != value {
|
||||
manyValues = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !manyValues && len(completions) > 0 {
|
||||
return []string{emoji.CodeMap()[completions[0]]}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (view *RoomView) SetEditing(evt *muksevt.Event) {
|
||||
if evt == nil {
|
||||
view.editing = nil
|
||||
@ -584,23 +527,90 @@ func (view *RoomView) SelectPrevious() {
|
||||
}
|
||||
}
|
||||
|
||||
func (view *RoomView) InputTabComplete(text string, cursorOffset int) {
|
||||
debug.Print("Tab completing", cursorOffset, text)
|
||||
str := runewidth.Truncate(text, cursorOffset, "")
|
||||
word := findWordToTabComplete(str)
|
||||
startIndex := len(str) - len(word)
|
||||
type completion struct {
|
||||
displayName string
|
||||
id string
|
||||
}
|
||||
|
||||
var strCompletions []string
|
||||
var strCompletion string
|
||||
func (view *RoomView) AutocompleteUser(existingText string) (completions []completion) {
|
||||
textWithoutPrefix := strings.TrimPrefix(existingText, "@")
|
||||
for userID, user := range view.Room.GetMembers() {
|
||||
if user.Displayname == textWithoutPrefix || string(userID) == existingText {
|
||||
// Exact match, return that.
|
||||
return []completion{{user.Displayname, string(userID)}}
|
||||
}
|
||||
|
||||
completions := view.autocompleteUser(word)
|
||||
completions = append(completions, view.autocompleteRoom(word)...)
|
||||
if strings.HasPrefix(user.Displayname, textWithoutPrefix) || strings.HasPrefix(string(userID), existingText) {
|
||||
completions = append(completions, completion{user.Displayname, string(userID)})
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (view *RoomView) AutocompleteRoom(existingText string) (completions []completion) {
|
||||
for _, room := range view.parent.rooms {
|
||||
alias := string(room.Room.GetCanonicalAlias())
|
||||
if alias == existingText {
|
||||
// Exact match, return that.
|
||||
return []completion{{alias, string(room.Room.ID)}}
|
||||
}
|
||||
if strings.HasPrefix(alias, existingText) {
|
||||
completions = append(completions, completion{alias, string(room.Room.ID)})
|
||||
continue
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (view *RoomView) AutocompleteEmoji(word string) (completions []string) {
|
||||
if word[0] != ':' {
|
||||
return
|
||||
}
|
||||
var valueCompletion1 string
|
||||
var manyValues bool
|
||||
for name, value := range emoji.CodeMap() {
|
||||
if name == word {
|
||||
return []string{value}
|
||||
} else if strings.HasPrefix(name, word) {
|
||||
completions = append(completions, name)
|
||||
if valueCompletion1 == "" {
|
||||
valueCompletion1 = value
|
||||
} else if valueCompletion1 != value {
|
||||
manyValues = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !manyValues && len(completions) > 0 {
|
||||
return []string{emoji.CodeMap()[completions[0]]}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func findWordToTabComplete(text string) string {
|
||||
output := ""
|
||||
runes := []rune(text)
|
||||
for i := len(runes) - 1; i >= 0; i-- {
|
||||
if unicode.IsSpace(runes[i]) {
|
||||
break
|
||||
}
|
||||
output = string(runes[i]) + output
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func (view *RoomView) defaultAutocomplete(word string, startIndex int) (strCompletions []string, strCompletion string) {
|
||||
if len(word) == 0 {
|
||||
return []string{}, ""
|
||||
}
|
||||
|
||||
completions := view.AutocompleteUser(word)
|
||||
completions = append(completions, view.AutocompleteRoom(word)...)
|
||||
|
||||
if len(completions) == 1 {
|
||||
completion := completions[0]
|
||||
strCompletion = fmt.Sprintf("[%s](https://matrix.to/#/%s)", completion.displayName, completion.id)
|
||||
if startIndex == 0 {
|
||||
strCompletion = strCompletion + ": "
|
||||
if startIndex == 0 && completion.id[0] == '@' {
|
||||
strCompletion = strCompletion + ":"
|
||||
}
|
||||
} else if len(completions) > 1 {
|
||||
for _, completion := range completions {
|
||||
@ -608,18 +618,44 @@ func (view *RoomView) InputTabComplete(text string, cursorOffset int) {
|
||||
}
|
||||
}
|
||||
|
||||
strCompletions = append(strCompletions, view.autocompleteEmoji(word)...)
|
||||
strCompletions = append(strCompletions, view.parent.cmdProcessor.AutocompleteCommand(word)...)
|
||||
strCompletions = append(strCompletions, view.AutocompleteEmoji(word)...)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (view *RoomView) InputTabComplete(text string, cursorOffset int) {
|
||||
if len(text) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
debug.Print("Tab completing", cursorOffset, text)
|
||||
|
||||
str := runewidth.Truncate(text, cursorOffset, "")
|
||||
word := findWordToTabComplete(str)
|
||||
startIndex := len(str) - len(word)
|
||||
|
||||
var strCompletion string
|
||||
|
||||
strCompletions, newText, ok := view.parent.cmdProcessor.Autocomplete(view, text, cursorOffset)
|
||||
if !ok {
|
||||
strCompletions, strCompletion = view.defaultAutocomplete(word, startIndex)
|
||||
}
|
||||
|
||||
if len(strCompletions) > 0 {
|
||||
strCompletion = util.LongestCommonPrefix(strCompletions)
|
||||
sort.Sort(sort.StringSlice(strCompletions))
|
||||
}
|
||||
|
||||
if len(strCompletion) > 0 {
|
||||
text = str[0:startIndex] + strCompletion + text[len(str):]
|
||||
if len(strCompletion) > 0 && len(strCompletions) < 2 {
|
||||
strCompletion += " "
|
||||
strCompletions = []string{}
|
||||
}
|
||||
|
||||
view.input.SetTextAndMoveCursor(text)
|
||||
if len(strCompletion) > 0 && newText == text {
|
||||
newText = str[0:startIndex] + strCompletion + text[len(str):]
|
||||
}
|
||||
|
||||
view.input.SetTextAndMoveCursor(newText)
|
||||
view.SetCompletions(strCompletions)
|
||||
}
|
||||
|
||||
|
@ -22,15 +22,15 @@ import (
|
||||
"os"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
sync "github.com/sasha-s/go-deadlock"
|
||||
|
||||
"maunium.net/go/gomuks/ui/messages"
|
||||
"maunium.net/go/mautrix/id"
|
||||
"maunium.net/go/mauview"
|
||||
"maunium.net/go/tcell"
|
||||
|
||||
"maunium.net/go/gomuks/ui/messages"
|
||||
"maunium.net/go/mautrix/id"
|
||||
|
||||
"maunium.net/go/gomuks/config"
|
||||
"maunium.net/go/gomuks/debug"
|
||||
"maunium.net/go/gomuks/interface"
|
||||
@ -139,18 +139,6 @@ func (view *MainView) InputChanged(roomView *RoomView, text string) {
|
||||
}
|
||||
}
|
||||
|
||||
func findWordToTabComplete(text string) string {
|
||||
output := ""
|
||||
runes := []rune(text)
|
||||
for i := len(runes) - 1; i >= 0; i-- {
|
||||
if unicode.IsSpace(runes[i]) {
|
||||
break
|
||||
}
|
||||
output = string(runes[i]) + output
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func (view *MainView) ShowBare(roomView *RoomView) {
|
||||
if roomView == nil {
|
||||
return
|
||||
|
Loading…
Reference in New Issue
Block a user