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.

This commit is contained in:
Tobias Gläßer 2018-11-23 10:08:11 +01:00 committed by Thomas Buckley-Houston
parent 935983725c
commit b2ade39223
4 changed files with 239 additions and 139 deletions

View File

@ -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 <C-d>"] = "scrollHalfPageDown"
vimKeyMap["normal u"] = "scrollHalfPageUp"
vimKeyMap["normal <C-u>"] = "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 <M-f>"] = "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 <Enter>"] = "clickAtCaretPosition"
vimKeyMap["visual c"] = "caretMode"
vimKeyMap["visual o"] = "swapVisualModeCursorPosition"
vimKeyMap["visual y"] = "copyVisualModeSelection"
}
func loadConfig() {

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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 "<C-M-" + r + ">"
} else if ev.Modifiers()&tcell.ModAlt != 0 {
return "<M-" + r + ">"
} else if ev.Modifiers()&tcell.ModCtrl != 0 {
return "<C-" + strings.ToLower(ev.Name()[5:]) + ">"
}
switch ev.Key() {
case tcell.KeyEnter:
return "<Enter>"
}
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 {