Add device list and legacy verification commands
This commit is contained in:
		| @@ -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 { | ||||
|   | ||||
							
								
								
									
										193
									
								
								ui/commands.go
									
									
									
									
									
								
							
							
						
						
									
										193
									
								
								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() | ||||
| @@ -451,6 +633,17 @@ Things: rooms, users, baremessages, images, typingnotif | ||||
| /react <reaction>    - React to 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). | ||||
| /create [room name]   - Create a room. | ||||
|   | ||||
							
								
								
									
										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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user