From b2ade392237b844f661cd0aa19883961b8a8da16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Gl=C3=A4=C3=9Fer?= Date: Fri, 23 Nov 2018 10:08:11 +0100 Subject: [PATCH] Fixed bug in key event handling between vim modes, where the same key event could get interpreted repeatedly. Also some rewrites/improvements of the code. Key mappings can now contain control characters and meta keys with a vim-like notation. There's a hard insert mode, which disables all of browsh's shortcuts and requires 4 hits on ESC to leave. There's a new multiple link opening feature analogous to vimium, that's still incomplete. --- interfacer/src/browsh/config.go | 102 +++++++----- interfacer/src/browsh/tty.go | 8 +- interfacer/src/browsh/ui.go | 10 +- interfacer/src/browsh/vim_mode.go | 258 +++++++++++++++++++----------- 4 files changed, 239 insertions(+), 139 deletions(-) diff --git a/interfacer/src/browsh/config.go b/interfacer/src/browsh/config.go index 0ffd915..9e72755 100644 --- a/interfacer/src/browsh/config.go +++ b/interfacer/src/browsh/config.go @@ -75,50 +75,64 @@ func setDefaults() { viper.SetDefault("tty.keys.next-tab", []string{"\u001c", "28", "2"}) // Vim commands - vimCommandsBindings["gg"] = "scrollToTop" - vimCommandsBindings["G"] = "scrollToBottom" - vimCommandsBindings["j"] = "scrollDown" - vimCommandsBindings["k"] = "scrollUp" - vimCommandsBindings["h"] = "scrollLeft" - vimCommandsBindings["l"] = "scrollRight" - vimCommandsBindings["d"] = "scrollHalfPageDown" - vimCommandsBindings["u"] = "scrollHalfPageUp" - vimCommandsBindings["e"] = "editURL" - vimCommandsBindings["ge"] = "editURL" - vimCommandsBindings["gE"] = "editURLInNewTab" - vimCommandsBindings["H"] = "historyBack" - vimCommandsBindings["L"] = "historyForward" - vimCommandsBindings["J"] = "prevTab" - vimCommandsBindings["K"] = "nextTab" - vimCommandsBindings["r"] = "reload" - vimCommandsBindings["xx"] = "removeTab" - vimCommandsBindings["X"] = "restoreTab" - vimCommandsBindings["t"] = "newTab" - vimCommandsBindings["/"] = "findMode" - vimCommandsBindings["n"] = "findNext" - vimCommandsBindings["N"] = "findPrevious" - vimCommandsBindings["g0"] = "firstTab" - vimCommandsBindings["g$"] = "lastTab" - vimCommandsBindings["gu"] = "urlUp" - vimCommandsBindings["gU"] = "urlRoot" - vimCommandsBindings["<<"] = "moveTabLeft" - vimCommandsBindings[">>"] = "moveTabRight" - vimCommandsBindings["^"] = "previouslyVisitedTab" - vimCommandsBindings["m"] = "makeMark" - vimCommandsBindings["'"] = "gotoMark" - vimCommandsBindings["i"] = "insertMode" - vimCommandsBindings["yy"] = "copyURL" - vimCommandsBindings["p"] = "openClipboardURL" - vimCommandsBindings["P"] = "openClipboardURLInNewTab" - vimCommandsBindings["gi"] = "focusFirstTextInput" - vimCommandsBindings["f"] = "openLinkInCurrentTab" - vimCommandsBindings["F"] = "openLinkInNewTab" - vimCommandsBindings["yf"] = "copyLinkURL" - vimCommandsBindings["[["] = "followLinkLabeledPrevious" - vimCommandsBindings["]]"] = "followLinkLabeledNext" - vimCommandsBindings["yt"] = "duplicateTab" - vimCommandsBindings["v"] = "visualMode" - vimCommandsBindings["?"] = "viewHelp" + vimKeyMap["normal gg"] = "scrollToTop" + vimKeyMap["normal G"] = "scrollToBottom" + vimKeyMap["normal j"] = "scrollDown" + vimKeyMap["normal k"] = "scrollUp" + vimKeyMap["normal h"] = "scrollLeft" + vimKeyMap["normal l"] = "scrollRight" + vimKeyMap["normal d"] = "scrollHalfPageDown" + vimKeyMap["normal "] = "scrollHalfPageDown" + vimKeyMap["normal u"] = "scrollHalfPageUp" + vimKeyMap["normal "] = "scrollHalfPageUp" + vimKeyMap["normal e"] = "editURL" + vimKeyMap["normal ge"] = "editURL" + vimKeyMap["normal gE"] = "editURLInNewTab" + vimKeyMap["normal H"] = "historyBack" + vimKeyMap["normal L"] = "historyForward" + vimKeyMap["normal J"] = "prevTab" + vimKeyMap["normal K"] = "nextTab" + vimKeyMap["normal r"] = "reload" + vimKeyMap["normal xx"] = "removeTab" + vimKeyMap["normal X"] = "restoreTab" + vimKeyMap["normal t"] = "newTab" + vimKeyMap["normal T"] = "searchForTab" + vimKeyMap["normal /"] = "findMode" + vimKeyMap["normal n"] = "findNext" + vimKeyMap["normal N"] = "findPrevious" + vimKeyMap["normal g0"] = "firstTab" + vimKeyMap["normal g$"] = "lastTab" + vimKeyMap["normal gu"] = "urlUp" + vimKeyMap["normal gU"] = "urlRoot" + vimKeyMap["normal <<"] = "moveTabLeft" + vimKeyMap["normal >>"] = "moveTabRight" + vimKeyMap["normal ^"] = "previouslyVisitedTab" + vimKeyMap["normal m"] = "makeMark" + vimKeyMap["normal '"] = "gotoMark" + vimKeyMap["normal i"] = "insertMode" + vimKeyMap["normal I"] = "insertModeHard" + vimKeyMap["normal yy"] = "copyURL" + vimKeyMap["normal p"] = "openClipboardURL" + vimKeyMap["normal P"] = "openClipboardURLInNewTab" + vimKeyMap["normal gi"] = "focusFirstTextInput" + vimKeyMap["normal f"] = "openLinkInCurrentTab" + vimKeyMap["normal F"] = "openLinkInNewTab" + vimKeyMap["normal "] = "openMultipleLinksInNewTab" + vimKeyMap["normal yf"] = "copyLinkURL" + vimKeyMap["normal [["] = "followLinkLabeledPrevious" + vimKeyMap["normal ]]"] = "followLinkLabeledNext" + vimKeyMap["normal yt"] = "duplicateTab" + vimKeyMap["normal v"] = "visualMode" + vimKeyMap["normal ?"] = "viewHelp" + vimKeyMap["caret v"] = "visualMode" + vimKeyMap["caret h"] = "moveCaretLeft" + vimKeyMap["caret l"] = "moveCaretRight" + vimKeyMap["caret j"] = "moveCaretDown" + vimKeyMap["caret k"] = "moveCaretUp" + vimKeyMap["caret "] = "clickAtCaretPosition" + vimKeyMap["visual c"] = "caretMode" + vimKeyMap["visual o"] = "swapVisualModeCursorPosition" + vimKeyMap["visual y"] = "copyVisualModeSelection" } func loadConfig() { diff --git a/interfacer/src/browsh/tty.go b/interfacer/src/browsh/tty.go index f7663b5..577cfb1 100644 --- a/interfacer/src/browsh/tty.go +++ b/interfacer/src/browsh/tty.go @@ -57,7 +57,7 @@ func readStdin() { } } -func handleUserKeyPress(ev *tcell.EventKey) { +func handleShortcuts(ev *tcell.EventKey) { if CurrentTab == nil { if ev.Key() == tcell.KeyCtrlQ { quitBrowsh() @@ -88,6 +88,12 @@ func handleUserKeyPress(ev *tcell.EventKey) { if isKey("tty.keys.next-tab", ev) { nextTab() } +} + +func handleUserKeyPress(ev *tcell.EventKey) { + if currentVimMode != insertModeHard { + handleShortcuts(ev) + } if !urlInputBox.isActive { forwardKeyPress(ev) } diff --git a/interfacer/src/browsh/ui.go b/interfacer/src/browsh/ui.go index ab7f531..62ac44b 100644 --- a/interfacer/src/browsh/ui.go +++ b/interfacer/src/browsh/ui.go @@ -115,10 +115,14 @@ func overlayVimMode() { switch currentVimMode { case insertMode: writeString(0, height-1, "ins", tcell.StyleDefault) + case insertModeHard: + writeString(0, height-1, "INS", tcell.StyleDefault) case linkMode: writeString(0, height-1, "lnk", tcell.StyleDefault) case linkModeNewTab: writeString(0, height-1, "LNK", tcell.StyleDefault) + case linkModeMultipleNewTab: + writeString(0, height-1, "*LNK", tcell.StyleDefault) case linkModeCopy: writeString(0, height-1, "cp", tcell.StyleDefault) case visualMode: @@ -128,14 +132,14 @@ func overlayVimMode() { writeString(caretPos.X, caretPos.Y, "#", tcell.StyleDefault) case findMode: writeString(0, height-1, "/"+findText, tcell.StyleDefault) - case makeMarkMode: + case markModeMake: writeString(0, height-1, "mark", tcell.StyleDefault) - case gotoMarkMode: + case markModeGoto: writeString(0, height-1, "goto", tcell.StyleDefault) } switch currentVimMode { - case linkMode, linkModeNewTab, linkModeCopy: + case linkMode, linkModeNewTab, linkModeMultipleNewTab, linkModeCopy: if !linkModeWithHints { findAndHighlightTextOnScreen(linkText) } diff --git a/interfacer/src/browsh/vim_mode.go b/interfacer/src/browsh/vim_mode.go index 6a11219..cee6ee1 100644 --- a/interfacer/src/browsh/vim_mode.go +++ b/interfacer/src/browsh/vim_mode.go @@ -19,15 +19,17 @@ type vimMode int const ( normalMode vimMode = iota + 1 insertMode + insertModeHard findMode linkMode linkModeNewTab + linkModeMultipleNewTab linkModeCopy waitMode visualMode caretMode - makeMarkMode - gotoMarkMode + markModeMake + markModeGoto ) // TODO: What's a mark? @@ -49,11 +51,13 @@ type hintRect struct { } var ( - currentVimMode = normalMode - vimCommandsBindings = make(map[string]string) - keyEvents = make([]*tcell.EventKey, 0, 11) - waitModeStartTime time.Time - findText string + currentVimMode = normalMode + vimKeyMap = make(map[string]string) + keyEvents = make([]*tcell.EventKey, 0, 11) + waitModeStartTime time.Time + waitModeMaxMilliseconds = 1000 + findText string + latestKeyCombination string // Marks globalMarkMap = make(map[rune]*mark) localMarkMap = make(map[int]map[rune]*mark) @@ -156,7 +160,7 @@ func makeMark() *mark { } func goIntoWaitMode() { - currentVimMode = waitMode + changeVimMode(waitMode) waitModeStartTime = time.Now() } @@ -219,96 +223,111 @@ func eraseLinkHints() { linkHintRects = nil } +func resetLinkHints() { + linkText = "" + updateLinkHintDisplay() +} + func isNormalModeKey(ev *tcell.EventKey) bool { - if ev.Key() == tcell.KeyESC { + if ev != nil && ev.Key() == tcell.KeyESC { return true } return false } -func handleVimControl(ev *tcell.EventKey) { - var lastRune rune - command := "" - - if len(keyEvents) > 0 && keyEvents[0] != nil { - lastRune = keyEvents[len(keyEvents)-1].Rune() +func keyEventToString(ev *tcell.EventKey) string { + if ev == nil { + return "" } + r := string(ev.Rune()) + if ev.Modifiers()&tcell.ModAlt != 0 && ev.Modifiers()&tcell.ModCtrl != 0 { + return "" + } else if ev.Modifiers()&tcell.ModAlt != 0 { + return "" + } else if ev.Modifiers()&tcell.ModCtrl != 0 { + return "" + } + + switch ev.Key() { + case tcell.KeyEnter: + return "" + } + + return r +} + + +func getNLastKeyEvent(n int) *tcell.EventKey { + if n < 0 || keyEvents == nil { + return nil + } + if len(keyEvents) > n { + return keyEvents[len(keyEvents)-n-1] + } + return nil +} + +func mapVimKeyEvents(ev *tcell.EventKey, mapMode string) string { + var lastEvent *tcell.EventKey + command := "" + keyEvents = append(keyEvents, ev) if len(keyEvents) > 10 { keyEvents = keyEvents[1:] } - keyCombination := string(lastRune) + string(ev.Rune()) + lastEvent = getNLastKeyEvent(1) + latestKeyCombination = keyEventToString(lastEvent) + keyEventToString(ev) + + command = vimKeyMap[mapMode+" "+latestKeyCombination] + if len(command) == 0 { + latestKeyCombination = keyEventToString(ev) + command = vimKeyMap[mapMode+" "+latestKeyCombination] + } + if len(command) <= 0 { + latestKeyCombination = "" + } else { + // Since len(command) must be greather than 0 here, + // a key mapping did match, therefore we reset keyEvents + keyEvents = nil + } + return command +} + +func handleVimMode(ev *tcell.EventKey, mode string) string { + if isNormalModeKey(ev) { + return "normalMode" + } else { + return mapVimKeyEvents(ev, mode) + } +} + +func handleVimControl(ev *tcell.EventKey) { + var command string switch currentVimMode { case waitMode: - if time.Since(waitModeStartTime) < time.Millisecond*1000 { + if time.Since(waitModeStartTime) < time.Millisecond*time.Duration(waitModeMaxMilliseconds) { return } - currentVimMode = normalMode + changeVimMode(normalMode) fallthrough case normalMode: - command = vimCommandsBindings[keyCombination] - if len(command) == 0 { - keyCombination := string(ev.Rune()) - command = vimCommandsBindings[keyCombination] - } + command = mapVimKeyEvents(ev, "normal") case insertMode: - if isNormalModeKey(ev) { + command = handleVimMode(ev, "insert") + case insertModeHard: + if isNormalModeKey(ev) && isNormalModeKey(getNLastKeyEvent(0)) && isNormalModeKey(getNLastKeyEvent(1)) && isNormalModeKey(getNLastKeyEvent(2)) { command = "normalMode" + } else { + command = mapVimKeyEvents(ev, "insertHard") } case visualMode: - if isNormalModeKey(ev) { - command = "normalMode" - } else { - if ev.Rune() == 'c' { - command = "caretMode" - } - if ev.Rune() == 'o' { - //swap cursor position begin->end or end->begin - } - if ev.Rune() == 'y' { - //clipboard - } - } + command = handleVimMode(ev, "visual") case caretMode: - if isNormalModeKey(ev) { - command = "normalMode" - } else { - switch ev.Key() { - case tcell.KeyEnter: - generateLeftClick(caretPos.X, caretPos.Y-uiHeight) - } - switch ev.Rune() { - case 'v': - command = "visualMode" - case 'h': - moveVimCaret(func() bool { return caretPos.X > 0 }, &caretPos.X, -1) - case 'l': - width, _ := screen.Size() - moveVimCaret(func() bool { return caretPos.X < width }, &caretPos.X, 1) - case 'k': - _, height := screen.Size() - moveVimCaret(func() bool { return caretPos.Y >= uiHeight }, &caretPos.Y, -1) - if caretPos.Y < uiHeight { - command = "scrollHalfPageUp" - if CurrentTab.frame.yScroll == 0 { - caretPos.Y = uiHeight - } else { - caretPos.Y += (height - uiHeight) / 2 - } - } - case 'j': - _, height := screen.Size() - moveVimCaret(func() bool { return caretPos.Y <= height-uiHeight }, &caretPos.Y, 1) - if caretPos.Y > height-uiHeight { - command = "scrollHalfPageDown" - caretPos.Y -= (height - uiHeight) / 2 - } - } - } - case makeMarkMode: + command = handleVimMode(ev, "caret") + case markModeMake: if unicode.IsLower(ev.Rune()) { if localMarkMap[CurrentTab.ID] == nil { localMarkMap[CurrentTab.ID] = make(map[rune]*mark) @@ -319,7 +338,7 @@ func handleVimControl(ev *tcell.EventKey) { } command = "normalMode" - case gotoMarkMode: + case markModeGoto: if mark, ok := globalMarkMap[ev.Rune()]; ok { gotoMark(mark) } else if m, ok := localMarkMap[CurrentTab.ID]; unicode.IsLower(ev.Rune()) && ok { @@ -335,7 +354,7 @@ func handleVimControl(ev *tcell.EventKey) { findText = "" } else { if ev.Key() == tcell.KeyEnter { - currentVimMode = normalMode + changeVimMode(normalMode) command = "findText" break } @@ -347,7 +366,7 @@ func handleVimControl(ev *tcell.EventKey) { findText += string(ev.Rune()) } } - case linkMode, linkModeNewTab, linkModeCopy: + case linkMode, linkModeNewTab, linkModeMultipleNewTab, linkModeCopy: if isNormalModeKey(ev) { command = "normalMode" eraseLinkHints() @@ -366,6 +385,9 @@ func handleVimControl(ev *tcell.EventKey) { } case linkModeNewTab: sendMessageToWebExtension("/new_tab," + r.Href) + case linkModeMultipleNewTab: + resetLinkHints() + return case linkModeCopy: clipboard.WriteAll(r.Href) } @@ -386,7 +408,7 @@ func handleVimControl(ev *tcell.EventKey) { linkText = "" return } else if len(coords) == 0 { - currentVimMode = normalMode + changeVimMode(normalMode) linkText = "" return } @@ -394,13 +416,17 @@ func handleVimControl(ev *tcell.EventKey) { } } - if len(command) > 0 { - executeVimCommand(command) - } + executeVimCommand(command) } func executeVimCommand(command string) { - switch command { + if len(command) == 0 { + return + } + + currentCommand := command + command = "" + switch currentCommand { case "urlUp": sendMessageToWebExtension("/tab_command,/url_up") case "urlRoot": @@ -472,15 +498,19 @@ func executeVimCommand(command string) { case "viewHelp": sendMessageToWebExtension("/new_tab,https://www.brow.sh/docs/keybindings/") case "openLinkInCurrentTab": - currentVimMode = linkMode + changeVimMode(linkMode) sendMessageToWebExtension("/tab_command,/get_clickable_hints") eraseLinkHints() case "openLinkInNewTab": - currentVimMode = linkModeNewTab + changeVimMode(linkModeNewTab) + sendMessageToWebExtension("/tab_command,/get_link_hints") + eraseLinkHints() + case "openMultipleLinksInNewTab": + changeVimMode(linkModeMultipleNewTab) sendMessageToWebExtension("/tab_command,/get_link_hints") eraseLinkHints() case "copyLinkURL": - currentVimMode = linkModeCopy + changeVimMode(linkModeCopy) sendMessageToWebExtension("/tab_command,/get_link_hints") eraseLinkHints() case "findText": @@ -490,22 +520,68 @@ func executeVimCommand(command string) { case "findPrevious": sendMessageToWebExtension("/tab_command,/find_previous," + findText) case "makeMark": - currentVimMode = makeMarkMode + changeVimMode(markModeMake) case "gotoMark": - currentVimMode = gotoMarkMode + changeVimMode(markModeGoto) case "insertMode": - currentVimMode = insertMode + changeVimMode(insertMode) + case "insertModeHard": + changeVimMode(insertModeHard) case "findMode": - currentVimMode = findMode + changeVimMode(findMode) case "normalMode": - currentVimMode = normalMode + changeVimMode(normalMode) + // Visual mode case "visualMode": - currentVimMode = visualMode + changeVimMode(visualMode) + case "swapVisualModeCursorPosition": + // Stub + case "copyVisualModeSelection": + // Caret mode case "caretMode": - currentVimMode = caretMode + changeVimMode(caretMode) width, height := screen.Size() caretPos.X, caretPos.Y = width/2, height/2 + case "clickAtCaretPosition": + generateLeftClick(caretPos.X, caretPos.Y-uiHeight) + case "moveCaretLeft": + moveVimCaret(func() bool { return caretPos.X > 0 }, &caretPos.X, -1) + case "moveCaretRight": + width, _ := screen.Size() + moveVimCaret(func() bool { return caretPos.X < width }, &caretPos.X, 1) + case "moveCaretUp": + _, height := screen.Size() + moveVimCaret(func() bool { return caretPos.Y >= uiHeight }, &caretPos.Y, -1) + if caretPos.Y < uiHeight { + command = "scrollHalfPageUp" + if CurrentTab.frame.yScroll == 0 { + caretPos.Y = uiHeight + } else { + caretPos.Y += (height - uiHeight) / 2 + } + } + case "moveCaretDown": + _, height := screen.Size() + moveVimCaret(func() bool { return caretPos.Y <= height-uiHeight }, &caretPos.Y, 1) + if caretPos.Y > height-uiHeight { + command = "scrollHalfPageDown" + caretPos.Y -= (height - uiHeight) / 2 + } } + + // A command can spawn another + executeVimCommand(command) +} + +func changeVimMode(mode vimMode) { + if currentVimMode == mode { + // No change + return + } + + currentVimMode = mode + // Reset keyEvents + keyEvents = nil } func searchVisibleScreenForText(text string) []Coordinate {