Fix wide characters in input field and prepare for tab completion

This commit is contained in:
Tulir Asokan 2018-03-17 14:00:02 +02:00
parent 4196931e1b
commit dacc7fd6a3
3 changed files with 75 additions and 34 deletions

View File

@ -37,7 +37,7 @@ type AdvancedInputField struct {
// Cursor position // Cursor position
cursorOffset int cursorOffset int
viewOffset int viewOffset int
// The text that was entered. // The text that was entered.
text string text string
@ -75,9 +75,11 @@ type AdvancedInputField struct {
changed func(text string) changed func(text string)
// An optional function which is called when the user indicated that they // An optional function which is called when the user indicated that they
// are done entering text. The key which was pressed is provided (tab, // are done entering text. The key which was pressed is provided (enter or escape).
// shift-tab, enter, or escape).
done func(tcell.Key) done func(tcell.Key)
// An optional function which is called when the user presses tab.
tabComplete func(text string, cursorOffset int)
} }
// NewAdvancedInputField returns a new input field. // NewAdvancedInputField returns a new input field.
@ -198,13 +200,16 @@ func (field *AdvancedInputField) SetChangedFunc(handler func(text string)) *Adva
// //
// - KeyEnter: Done entering text. // - KeyEnter: Done entering text.
// - KeyEscape: Abort text input. // - KeyEscape: Abort text input.
// - KeyTab: Move to the next field.
// - KeyBacktab: Move to the previous field.
func (field *AdvancedInputField) SetDoneFunc(handler func(key tcell.Key)) *AdvancedInputField { func (field *AdvancedInputField) SetDoneFunc(handler func(key tcell.Key)) *AdvancedInputField {
field.done = handler field.done = handler
return field return field
} }
func (field *AdvancedInputField) SetTabCompleteFunc(handler func(text string, cursorOffset int)) *AdvancedInputField {
field.tabComplete = handler
return field
}
// SetFinishedFunc calls SetDoneFunc(). // SetFinishedFunc calls SetDoneFunc().
func (field *AdvancedInputField) SetFinishedFunc(handler func(key tcell.Key)) tview.FormItem { func (field *AdvancedInputField) SetFinishedFunc(handler func(key tcell.Key)) tview.FormItem {
return field.SetDoneFunc(handler) return field.SetDoneFunc(handler)
@ -255,9 +260,9 @@ func (field *AdvancedInputField) Draw(screen tcell.Screen) {
// Recalculate view offset // Recalculate view offset
if field.cursorOffset < field.viewOffset { if field.cursorOffset < field.viewOffset {
field.viewOffset = field.cursorOffset field.viewOffset = field.cursorOffset
} else if field.cursorOffset > field.viewOffset + fieldWidth { } else if field.cursorOffset > field.viewOffset+fieldWidth {
field.viewOffset = field.cursorOffset - fieldWidth field.viewOffset = field.cursorOffset - fieldWidth
} else if textWidth - field.viewOffset < fieldWidth { } else if textWidth-field.viewOffset < fieldWidth {
field.viewOffset = textWidth - fieldWidth field.viewOffset = textWidth - fieldWidth
} }
// Make sure view offset didn't become negative // Make sure view offset didn't become negative
@ -268,7 +273,7 @@ func (field *AdvancedInputField) Draw(screen tcell.Screen) {
// Draw entered text. // Draw entered text.
runes := []rune(text) runes := []rune(text)
relPos := 0 relPos := 0
for pos := field.viewOffset; pos <= fieldWidth + field.viewOffset && pos < len(runes); pos++ { for pos := field.viewOffset; pos <= fieldWidth+field.viewOffset && pos < len(runes); pos++ {
ch := runes[pos] ch := runes[pos]
w := runewidth.RuneWidth(ch) w := runewidth.RuneWidth(ch)
_, _, style, _ := screen.GetContent(x+relPos, y) _, _, style, _ := screen.GetContent(x+relPos, y)
@ -327,10 +332,14 @@ func (field *AdvancedInputField) setCursor(screen tcell.Screen) {
} }
var ( var (
lastWord = regexp.MustCompile(`\S+\s*$`) lastWord = regexp.MustCompile(`\S+\s*$`)
firstWord = regexp.MustCompile(`^\s*\S+`) firstWord = regexp.MustCompile(`^\s*\S+`)
) )
func SubstringBefore(s string, w int) string {
return runewidth.Truncate(s, w, "")
}
// InputHandler returns the handler for this primitive. // InputHandler returns the handler for this primitive.
func (field *AdvancedInputField) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { func (field *AdvancedInputField) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
return field.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { return field.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
@ -353,57 +362,76 @@ func (field *AdvancedInputField) InputHandler() func(event *tcell.EventKey, setF
// Process key event. // Process key event.
switch key := event.Key(); key { switch key := event.Key(); key {
case tcell.KeyRune: // Regular character. case tcell.KeyRune: // Regular character.
runes := []rune(field.text) leftPart := SubstringBefore(field.text, field.cursorOffset)
newText := string(runes[0:field.cursorOffset]) + string(event.Rune()) + string(runes[field.cursorOffset:]) newText := leftPart + string(event.Rune()) + field.text[len(leftPart):]
if field.accept != nil { if field.accept != nil {
if !field.accept(newText, event.Rune()) { if !field.accept(newText, event.Rune()) {
break break
} }
} }
field.text = newText field.text = newText
field.cursorOffset++ field.cursorOffset += runewidth.RuneWidth(event.Rune())
case tcell.KeyCtrlV: case tcell.KeyCtrlV:
clip, _ := clipboard.ReadAll("clipboard") clip, _ := clipboard.ReadAll("clipboard")
runes := []rune(field.text) leftPart := SubstringBefore(field.text, field.cursorOffset)
field.text = string(runes[0:field.cursorOffset]) + clip + string(runes[field.cursorOffset:]) field.text = leftPart + clip + field.text[len(leftPart):]
field.cursorOffset += runewidth.StringWidth(clip) field.cursorOffset += runewidth.StringWidth(clip)
case tcell.KeyLeft: // Move cursor left. case tcell.KeyLeft: // Move cursor left.
before := SubstringBefore(field.text, field.cursorOffset)
if event.Modifiers() == tcell.ModCtrl { if event.Modifiers() == tcell.ModCtrl {
runes := []rune(field.text) found := lastWord.FindString(before)
found := lastWord.FindString(string(runes[0:field.cursorOffset]))
field.cursorOffset -= runewidth.StringWidth(found) field.cursorOffset -= runewidth.StringWidth(found)
} else { } else if len(before) > 0 {
field.cursorOffset-- beforeRunes := []rune(before)
char := beforeRunes[len(beforeRunes)-1]
field.cursorOffset -= runewidth.RuneWidth(char)
} }
case tcell.KeyRight: // Move cursor right. case tcell.KeyRight: // Move cursor right.
before := SubstringBefore(field.text, field.cursorOffset)
after := field.text[len(before):]
if event.Modifiers() == tcell.ModCtrl { if event.Modifiers() == tcell.ModCtrl {
runes := []rune(field.text) found := firstWord.FindString(after)
found := firstWord.FindString(string(runes[field.cursorOffset:]))
field.cursorOffset += runewidth.StringWidth(found) field.cursorOffset += runewidth.StringWidth(found)
} else { } else if len(after) > 0 {
field.cursorOffset++ char := []rune(after)[0]
field.cursorOffset += runewidth.RuneWidth(char)
} }
case tcell.KeyDelete: // Delete next character. case tcell.KeyDelete: // Delete next character.
if field.cursorOffset >= runewidth.StringWidth(field.text) { if field.cursorOffset >= runewidth.StringWidth(field.text) {
break break
} }
runes := []rune(field.text) leftPart := SubstringBefore(field.text, field.cursorOffset)
field.text = string(runes[0:field.cursorOffset]) + string(runes[field.cursorOffset + 1:]) rightPart := field.text[len(leftPart):]
rightPartRunes := []rune(rightPart)
rightPartRunes = rightPartRunes[1:]
rightPart = string(rightPartRunes)
field.text = leftPart + rightPart
case tcell.KeyBackspace, tcell.KeyBackspace2: // Delete last character. case tcell.KeyBackspace, tcell.KeyBackspace2: // Delete last character.
if field.cursorOffset == 0 { if field.cursorOffset == 0 {
break break
} }
runes := []rune(field.text)
if key == tcell.KeyBackspace { // Ctrl+backspace if key == tcell.KeyBackspace { // Ctrl+backspace
orig := string(runes[0:field.cursorOffset]) leftPart := SubstringBefore(field.text, field.cursorOffset)
replacement := lastWord.ReplaceAllString(orig, "") rightPart := field.text[len(leftPart):]
field.text = replacement + string(runes[field.cursorOffset:]) replacement := lastWord.ReplaceAllString(leftPart, "")
field.cursorOffset -= runewidth.StringWidth(orig) - runewidth.StringWidth(replacement) field.text = replacement + rightPart
field.cursorOffset -= runewidth.StringWidth(leftPart) - runewidth.StringWidth(replacement)
} else { // Just backspace } else { // Just backspace
field.text = string(runes[0:field.cursorOffset - 1]) + string(runes[field.cursorOffset:]) leftPart := SubstringBefore(field.text, field.cursorOffset)
field.cursorOffset-- rightPart := field.text[len(leftPart):]
leftPartRunes := []rune(leftPart)
leftPartRunes = leftPartRunes[0 : len(leftPartRunes)-1]
leftPart = string(leftPartRunes)
removedChar := field.text[len(leftPart) : len(field.text)-len(rightPart)]
field.text = leftPart + rightPart
field.cursorOffset -= runewidth.StringWidth(removedChar)
} }
case tcell.KeyEnter, tcell.KeyTab, tcell.KeyBacktab, tcell.KeyEscape: // We're done. case tcell.KeyTab: // Tab-completion
if field.tabComplete != nil {
field.tabComplete(field.text, field.cursorOffset)
}
case tcell.KeyEnter, tcell.KeyEscape: // We're done.
if field.done != nil { if field.done != nil {
field.done(key) field.done(key)
} }

View File

@ -45,12 +45,15 @@ type gomuks struct {
config *Config config *Config
} }
var gdebug DebugPrinter
func NewGomuks(debug bool) *gomuks { func NewGomuks(debug bool) *gomuks {
configDir := filepath.Join(os.Getenv("HOME"), ".config/gomuks") configDir := filepath.Join(os.Getenv("HOME"), ".config/gomuks")
gmx := &gomuks{ gmx := &gomuks{
app: tview.NewApplication(), app: tview.NewApplication(),
} }
gmx.debug = NewDebugPane(gmx) gmx.debug = NewDebugPane(gmx)
gdebug = gmx.debug
gmx.config = NewConfig(gmx, configDir) gmx.config = NewConfig(gmx, configDir)
gmx.ui = NewGomuksUI(gmx) gmx.ui = NewGomuksUI(gmx)
gmx.matrix = NewMatrixContainer(gmx) gmx.matrix = NewMatrixContainer(gmx)

View File

@ -65,11 +65,14 @@ func (ui *GomuksUI) NewMainView() tview.Primitive {
mainView.roomList. mainView.roomList.
ShowSecondaryText(false). ShowSecondaryText(false).
SetSelectedBackgroundColor(tcell.ColorDarkGreen).
SetSelectedTextColor(tcell.ColorWhite).
SetBorderPadding(0, 0, 1, 0) SetBorderPadding(0, 0, 1, 0)
mainView.input. mainView.input.
SetDoneFunc(mainView.InputDone). SetDoneFunc(mainView.InputDone).
SetChangedFunc(mainView.InputChanged). SetChangedFunc(mainView.InputChanged).
SetTabCompleteFunc(mainView.InputTabComplete).
SetFieldBackgroundColor(tcell.ColorDefault). SetFieldBackgroundColor(tcell.ColorDefault).
SetPlaceholder("Send a message..."). SetPlaceholder("Send a message...").
SetPlaceholderExtColor(tcell.ColorGray). SetPlaceholderExtColor(tcell.ColorGray).
@ -93,6 +96,13 @@ func (view *MainView) InputChanged(text string) {
} }
} }
func (view *MainView) InputTabComplete(text string, cursorOffset int) {
roomView, _ := view.rooms[view.CurrentRoomID()]
if roomView != nil {
// text[0:cursorOffset]
}
}
func (view *MainView) InputDone(key tcell.Key) { func (view *MainView) InputDone(key tcell.Key) {
if key == tcell.KeyEnter { if key == tcell.KeyEnter {
room, text := view.CurrentRoomID(), view.input.GetText() room, text := view.CurrentRoomID(), view.input.GetText()
@ -128,7 +138,7 @@ func (view *MainView) HandleCommand(room, command string, args []string) {
view.matrix.client.LeaveRoom(room) view.matrix.client.LeaveRoom(room)
case "/join": case "/join":
if len(args) == 0 { if len(args) == 0 {
view.AddMessage(room, "Usage: /join <room>") view.AddMessage(room, "Usage: /join <room>")
break break
} }
view.debug.Print(view.matrix.JoinRoom(args[0])) view.debug.Print(view.matrix.JoinRoom(args[0]))
@ -207,7 +217,7 @@ func (view *MainView) AddRoom(room string) {
return return
} }
view.roomIDs = append(view.roomIDs, room) view.roomIDs = append(view.roomIDs, room)
view.addRoom(len(view.roomIDs) - 1, room) view.addRoom(len(view.roomIDs)-1, room)
} }
func (view *MainView) RemoveRoom(room string) { func (view *MainView) RemoveRoom(room string) {