summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTobias Gläßer <tobimensch@users.noreply.github.com>2018-11-23 10:08:11 +0100
committerThomas Buckley-Houston <tom@tombh.co.uk>2019-06-24 09:12:20 +0300
commitb2ade392237b844f661cd0aa19883961b8a8da16 (patch)
tree0080f35a8de06556416b44da238c7514efa09248
parent935983725c09df938962f64e683dc6766f527a97 (diff)
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.vim-mode-experimentalpr/236
-rw-r--r--interfacer/src/browsh/config.go102
-rw-r--r--interfacer/src/browsh/tty.go8
-rw-r--r--interfacer/src/browsh/ui.go10
-rw-r--r--interfacer/src/browsh/vim_mode.go256
4 files changed, 238 insertions, 138 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 <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() {
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 := ""
+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
+}
+
- if len(keyEvents) > 0 && keyEvents[0] != nil {
- lastRune = keyEvents[len(keyEvents)-1].Rune()
+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 = "normalMode"
- }
- case visualMode:
- if isNormalModeKey(ev) {
+ command = handleVimMode(ev, "insert")
+ case insertModeHard:
+ if isNormalModeKey(ev) && isNormalModeKey(getNLastKeyEvent(0)) && isNormalModeKey(getNLastKeyEvent(1)) && isNormalModeKey(getNLastKeyEvent(2)) {
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 = mapVimKeyEvents(ev, "insertHard")
}
+ case visualMode:
+ 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 {