Compare commits

...

27 Commits

Author SHA1 Message Date
Tobias Gläßer b2ade39223 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. 2019-06-24 09:12:20 +03:00
Tobias Gläßer 935983725c Added vim feature for editing URL in new tab 2019-06-24 09:12:20 +03:00
Thomas Buckley-Houston b780a79f2e Gofmt: some minor capitalisation 2019-06-24 09:12:20 +03:00
Thomas Buckley-Houston 714cad8615 Travis: upload logs to text host
This is because Travis' logs had 2 problems.
  1. it doesn't capture the entire log output
  2. it doesn't show logs when there's a timeout
2019-06-24 09:12:20 +03:00
Tobias Gläßer 7a622b230b Fixed bug where keyEvents variable was initialized wrongly. This led to key combinations only working after a certain number of key strokes. 2019-06-24 09:12:20 +03:00
Thomas Buckley-Houston ed79db0510 Vim mode: Small updates from PR review 2019-06-24 09:12:11 +03:00
Thomas Buckley-Houston 8161ea34e6 Vim mode: convert unexported symbols to lowercase 2019-06-24 09:12:11 +03:00
Thomas Buckley-Houston 59d2c31acf Fixes tests for Vim mode
Vim mode still needs a lot more tests
2019-06-24 09:12:11 +03:00
Thomas Buckley-Houston eae72e94a6 Adds some Vim-specific integration tests 2019-06-24 09:12:11 +03:00
Thomas Buckley-Houston e03923394c Refactors Vim code from tty.go into its own file 2019-06-24 09:12:11 +03:00
Tobias Gläßer 49eebee0c9 Fixed typo 2019-06-24 09:09:58 +03:00
Tobias Gläßer d3fff67c61 Prettified js files 2019-06-24 09:09:58 +03:00
Tobias Gläßer bd5c30640d Instead of adding 1 to Y coord, add 1 to height. This fixes an issues
with activating input boxes using link hinting.
2019-06-24 09:09:58 +03:00
Tobias Gläßer 0b7d1dc7ef Added vim like navigation. This is still in an early stage. 2019-06-24 09:09:58 +03:00
Tobias Gläßer 15c7b45b6f Added features needed for vim like navigation to the webextension. 2019-06-24 09:09:58 +03:00
Tobias Gläßer 7b7e6bc308 Refactored code using switchToTab and added new features.
Added moveTabLeft and moveTabRight functions, which take
a tab ID and try to move the tab as far right or left
in the tabOrder as possible.
Added previouslyVisitedTab function that switches to the
previously selected tab.
2019-06-24 09:09:58 +03:00
Tobias Gläßer 86acac617b Added duplicate_tab, restore_tab commands. 2019-06-24 09:09:58 +03:00
Tobias Gläßer 631483bbd9 Added initial configuration for vim like keybindings.
The keybindings are hardcoded for now, but this is going to change.
2019-06-24 09:09:58 +03:00
Tobias Gläßer c794f10287 Added sessions permission 2019-06-24 09:09:58 +03:00
Tobias Gläßer fac1af7f2a Fixed typo in comment 2019-06-24 09:09:58 +03:00
Tobias Gläßer 8fc15f3301 ignore debug log in interfacer/ directory 2019-06-24 09:09:58 +03:00
Tobias Gläßer ebc8de95b9 ignore manifest.json backup file 2019-06-24 09:09:58 +03:00
Tobias Gläßer dc9533969f Set default DISPLAY environment variable for xclipboard functionality 2019-06-24 09:09:58 +03:00
Tobias Gläßer ca30b7722b added command for links hints 2019-06-24 09:09:58 +03:00
Tobias Gläßer baf808f35d gofmt code 2019-06-24 09:09:58 +03:00
Tobias Gläßer 3b246ff796 Created overlayVimMode function for displaying vim navigation state 2019-06-24 09:09:58 +03:00
Tobias Gläßer ee1291b41a Added new vimium.js with code taken from vimium 2019-06-24 09:09:58 +03:00
19 changed files with 2486 additions and 80 deletions

2
.gitignore vendored
View File

@ -7,8 +7,10 @@ interfacer/vendor
interfacer/dist
interfacer/interfacer
interfacer/browsh
interfacer/debug
webextension.go
webext/node_modules
webext/dist/*
webext/manifest.json~
dist
*.xpi

View File

@ -40,8 +40,8 @@ script:
- cd $REPO_ROOT/interfacer && go test test/tty/*.go -v -ginkgo.slowSpecThreshold=30 -ginkgo.flakeAttempts=3
- cd $REPO_ROOT/interfacer && go test test/http-server/*.go -v -ginkgo.slowSpecThreshold=30 -ginkgo.flakeAttempts=3
after_failure:
- cat $REPO_ROOT/interfacer/test/tty/debug.log
- cat $REPO_ROOT/interfacer/test/http-server/debug.log
- cat $REPO_ROOT/interfacer/test/tty/debug.log | curl -F 'f:1=<-' ix.io
- cat $REPO_ROOT/interfacer/test/http-server/debug.log | curl -F 'f:1=<-' ix.io
after_success:
- $REPO_ROOT/contrib/release_if_new_version.sh

View File

@ -155,6 +155,7 @@ func TTYStart(injectedScreen tcell.Screen) {
Log("Starting Browsh CLI client")
go readStdin()
startWebSocketServer()
setupLinkHints()
}
func toInt(char string) int {
@ -181,6 +182,12 @@ func ttyEntry() {
// from tcell.
os.Setenv("TERM", "xterm-truecolor")
}
// This is for getting the clipboard (github.com/atotto/clipboard) to work
// with the applications xsel and xclip on systems with an X display server.
if os.Getenv("DISPLAY") == "" {
os.Setenv("DISPLAY", ":0")
}
realScreen, err := tcell.NewScreen()
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)

View File

@ -88,6 +88,8 @@ func handleWebextensionCommand(message []byte) {
}
case "/screenshot":
saveScreenshot(parts[1])
case "/link_hints":
parseJSONLinkHints(strings.Join(parts[1:], ","))
default:
Log("WEBEXT: " + string(message))
}

View File

@ -73,6 +73,66 @@ func getFirefoxProfilePath() string {
func setDefaults() {
// Temporary experimental configurable keybindings
viper.SetDefault("tty.keys.next-tab", []string{"\u001c", "28", "2"})
// Vim commands
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

@ -160,9 +160,9 @@ func (f *frame) updateInputBoxes(incoming incomingFrameText) {
inputBox := f.inputBoxes[incomingInputBox.ID]
inputBox.X = incomingInputBox.X
// TODO: Why do we have to add the 1 to the y coord??
inputBox.Y = (incomingInputBox.Y + 1) / 2
inputBox.Y = (incomingInputBox.Y + 0) / 2
inputBox.Width = incomingInputBox.Width
inputBox.Height = incomingInputBox.Height / 2
inputBox.Height = (incomingInputBox.Height / 2) + 1
inputBox.FgColour = incomingInputBox.FgColour
inputBox.TagName = incomingInputBox.TagName
inputBox.Type = incomingInputBox.Type
@ -312,7 +312,7 @@ func (f *frame) maybeFocusInputBox(x, y int) {
left := inputBox.X
right := inputBox.X + inputBox.Width
if x >= left && x < right && y >= top && y < bottom {
urlBarFocus(false)
URLBarFocus(false)
inputBox.isActive = true
activeInputBox = inputBox
}

View File

@ -12,7 +12,7 @@ var activeInputBox *inputBox
// A box into which you can enter text. Generally will be forwarded to a standard
// HTML input box in the real browser.
//
// Note that tcell alreay has some ready-made code in its 'views' concept for
// Note that tcell already has some ready-made code in its 'views' concept for
// dealing with input areas. However, at the time of writing it wasn't well documented,
// so it was unclear how easy it would be to integrate the requirements of Browsh's
// input boxes - namely overlaying them onto the existing graphics and having them
@ -181,7 +181,7 @@ func (i *inputBox) handleEnterKey(modifier tcell.ModMask) {
} else {
sendMessageToWebExtension("/url_bar," + string(i.text))
}
urlBarFocus(false)
URLBarFocus(false)
}
if i.isMultiLine() && modifier != tcell.ModAlt {
i.cursorInsertRune([]rune("\n")[0])

View File

@ -18,6 +18,9 @@ var tabsOrder []int
// the tab being deleted, so we need to keep track of all deleted IDs
var tabsDeleted []int
// ID of the tab that was active before the current one
var previouslyVisitedTabID int
// A single tab synced from the browser
type tab struct {
ID int `json:"id"`
@ -61,6 +64,10 @@ func newTab(id int) {
}
}
func restoreTab() {
sendMessageToWebExtension("/restore_tab")
}
func removeTab(id int) {
if len(Tabs) == 1 {
quitBrowsh()
@ -84,23 +91,63 @@ func removeTabIDfromTabsOrder(id int) {
}
}
func moveTabLeft(id int) {
// If the tab ID is already completely to the left in the tab order
// there's nothing to do
if tabsOrder[0] == id {
return
}
for i, tabID := range tabsOrder {
if tabID == id {
tabsOrder[i-1], tabsOrder[i] = tabsOrder[i], tabsOrder[i-1]
break
}
}
}
func moveTabRight(id int) {
// If the tab ID is already completely to the right in the tab order
// there's nothing to do
if tabsOrder[len(tabsOrder)-1] == id {
return
}
for i, tabID := range tabsOrder {
if tabID == id {
tabsOrder[i+1], tabsOrder[i] = tabsOrder[i], tabsOrder[i+1]
break
}
}
}
func duplicateTab(id int) {
sendMessageToWebExtension(fmt.Sprintf("/duplicate_tab,%d", id))
}
// Creating a new tab in the browser without a URI means it won't register with the
// web extension, which means that, come the moment when we actually have a URI for the new
// tab then we can't talk to it to tell it navigate. So we need to only create a real new
// tab when we actually have a URL.
func createNewEmptyTab() {
createNewEmptyTabWithURI("")
}
func createNewEmptyTabWithURI(URI string) {
if isNewEmptyTabActive() {
return
}
newTab(-1)
tab := Tabs[-1]
tab.Title = "New Tab"
tab.URI = ""
tab.URI = URI
tab.Active = true
CurrentTab = tab
CurrentTab.frame.resetCells()
renderUI()
urlBarFocus(true)
URLBarFocus(true)
// Allows for typing directly at the end of URI
urlInputBox.selectionOff()
renderCurrentTabWindow()
}
@ -116,15 +163,41 @@ func nextTab() {
} else {
i++
}
sendMessageToWebExtension(fmt.Sprintf("/switch_to_tab,%d", tabsOrder[i]))
CurrentTab = Tabs[tabsOrder[i]]
renderUI()
renderCurrentTabWindow()
switchToTab(tabsOrder[i])
break
}
}
}
func prevTab() {
for i := 0; i < len(tabsOrder); i++ {
if tabsOrder[i] == CurrentTab.ID {
if i-1 < 0 {
i = len(tabsOrder) - 1
} else {
i--
}
switchToTab(tabsOrder[i])
break
}
}
}
func previouslyVisitedTab() {
if previouslyVisitedTabID == 0 {
return
}
switchToTab(previouslyVisitedTabID)
}
func switchToTab(num int) {
sendMessageToWebExtension(fmt.Sprintf("/switch_to_tab,%d", num))
previouslyVisitedTabID = CurrentTab.ID
CurrentTab = Tabs[num]
renderUI()
renderCurrentTabWindow()
}
func isTabPreviouslyDeleted(id int) bool {
for i := 0; i < len(tabsDeleted); i++ {
if tabsDeleted[i] == id {

View File

@ -5,14 +5,20 @@ import (
"fmt"
"os"
"strconv"
"time"
"github.com/gdamore/tcell"
"github.com/go-errors/errors"
"github.com/spf13/viper"
)
type Coordinate struct {
X, Y int
}
var (
screen tcell.Screen
screen tcell.Screen
// The height of the tabs and URL bar
uiHeight = 2
// IsMonochromeMode decides whether to render the TTY in full colour or monochrome
IsMonochromeMode = false
@ -51,7 +57,7 @@ func readStdin() {
}
}
func handleUserKeyPress(ev *tcell.EventKey) {
func handleShortcuts(ev *tcell.EventKey) {
if CurrentTab == nil {
if ev.Key() == tcell.KeyCtrlQ {
quitBrowsh()
@ -82,16 +88,24 @@ 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)
}
if activeInputBox != nil {
handleInputBoxInput(ev)
} else {
handleVimControl(ev)
handleScrolling(ev) // TODO: shouldn't you be able to still use mouse scrolling?
}
}
// Matches a human-readable key defintion with a Tcell event
func isKey(userKey string, ev *tcell.EventKey) bool {
key := viper.GetStringSlice(userKey)
runeMatch := []rune(key[0])[0] == ev.Rune()
@ -139,34 +153,66 @@ func isMultiLineEnter(ev *tcell.EventKey) bool {
return activeInputBox.isMultiLine() && ev.Key() == 13 && ev.Modifiers() != 4
}
func handleScrolling(ev *tcell.EventKey) {
func generateLeftClickYHack(x, y int, yHack bool) {
newMouseEvent := tcell.NewEventMouse(x, y+uiHeight, tcell.Button1, 0)
handleMouseEventYHack(newMouseEvent, yHack)
time.Sleep(time.Millisecond * 100)
newMouseEvent = tcell.NewEventMouse(x, y+uiHeight, 0, 0)
handleMouseEventYHack(newMouseEvent, yHack)
}
func generateLeftClick(x, y int) {
generateLeftClickYHack(x, y, false)
}
// TODO: This isn't working for opening new tabs.
func generateMiddleClick(x, y int) {
newMouseEvent := tcell.NewEventMouse(x, y+uiHeight, tcell.Button2, 0)
handleMouseEvent(newMouseEvent)
time.Sleep(time.Millisecond * 100)
newMouseEvent = tcell.NewEventMouse(x, y+uiHeight, 0, 0)
handleMouseEvent(newMouseEvent)
}
func doScroll(relX int, relY int) {
doScrollAbsolute(CurrentTab.frame.xScroll+relX, CurrentTab.frame.yScroll+relY)
}
func doScrollAbsolute(absX int, absY int) {
yScrollOriginal := CurrentTab.frame.yScroll
_, height := screen.Size()
height -= uiHeight
if ev.Key() == tcell.KeyUp {
CurrentTab.frame.yScroll -= 2
}
if ev.Key() == tcell.KeyDown {
CurrentTab.frame.yScroll += 2
}
if ev.Key() == tcell.KeyPgUp {
CurrentTab.frame.yScroll -= height
}
if ev.Key() == tcell.KeyPgDn {
CurrentTab.frame.yScroll += height
}
CurrentTab.frame.yScroll = absY
CurrentTab.frame.xScroll = absX
CurrentTab.frame.limitScroll(height)
sendMessageToWebExtension(
fmt.Sprintf(
"/tab_command,/scroll_status,%d,%d",
CurrentTab.frame.xScroll,
CurrentTab.frame.yScroll*2))
fmt.Sprintf("/tab_command,/scroll_status,%d,%d",
CurrentTab.frame.xScroll, CurrentTab.frame.yScroll*2))
if CurrentTab.frame.yScroll != yScrollOriginal {
renderCurrentTabWindow()
}
}
func handleMouseEvent(ev *tcell.EventMouse) {
func handleScrolling(ev *tcell.EventKey) {
_, height := screen.Size()
height -= uiHeight
if ev.Key() == tcell.KeyUp {
doScroll(0, -2)
}
if ev.Key() == tcell.KeyDown {
doScroll(0, 2)
}
if ev.Key() == tcell.KeyPgUp {
doScroll(0, -height)
}
if ev.Key() == tcell.KeyPgDn {
doScroll(0, height)
}
}
func handleMouseEventYHack(ev *tcell.EventMouse, yHack bool) {
if CurrentTab == nil {
return
}
@ -186,27 +232,24 @@ func handleMouseEvent(ev *tcell.EventMouse) {
"mouse_y": int(yInFrame),
"modifiers": int(ev.Modifiers()),
}
if yHack {
eventMap["y_hack"] = true
}
marshalled, _ := json.Marshal(eventMap)
sendMessageToWebExtension("/stdin," + string(marshalled))
}
func handleMouseEvent(ev *tcell.EventMouse) {
handleMouseEventYHack(ev, false)
}
func handleMouseScroll(scrollType tcell.ButtonMask) {
yScrollOriginal := CurrentTab.frame.yScroll
_, height := screen.Size()
height -= uiHeight
if scrollType == tcell.WheelUp {
CurrentTab.frame.yScroll -= 1
doScroll(0, -1)
} else if scrollType == tcell.WheelDown {
CurrentTab.frame.yScroll += 1
}
CurrentTab.frame.limitScroll(height)
sendMessageToWebExtension(
fmt.Sprintf(
"/tab_command,/scroll_status,%d,%d",
CurrentTab.frame.xScroll,
CurrentTab.frame.yScroll*2))
if CurrentTab.frame.yScroll != yScrollOriginal {
renderCurrentTabWindow()
doScroll(0, 1)
}
}
@ -255,6 +298,7 @@ func renderCurrentTabWindow() {
activeInputBox.renderCursor()
}
overlayPageStatusMessage()
overlayVimMode()
overlayCallToSupport()
screen.Show()
}
@ -275,6 +319,8 @@ func getCell(x, y int) cell {
return currentCell
}
// These are the dark and light grey squares that appear in the background to indicate that
// nothing has been rendered there yet.
func getHatchedCellColours(x int) (tcell.Color, tcell.Color) {
var bgColour, fgColour tcell.Color
if x%2 == 0 {

View File

@ -87,13 +87,15 @@ func renderURLBar() {
func urlBarFocusToggle() {
if urlInputBox.isActive {
urlBarFocus(false)
URLBarFocus(false)
} else {
urlBarFocus(true)
URLBarFocus(true)
}
}
func urlBarFocus(on bool) {
// Set the focus of the URL bar. Also used in tests to ensure the URL bar is in fact focussed as
// toggling doesn't guarantee that you will gain focus.
func URLBarFocus(on bool) {
if !on {
activeInputBox = nil
urlInputBox.isActive = false
@ -108,6 +110,46 @@ func urlBarFocus(on bool) {
}
}
func overlayVimMode() {
_, height := screen.Size()
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:
writeString(0, height-1, "vis", tcell.StyleDefault)
case caretMode:
writeString(0, height-1, "car", tcell.StyleDefault)
writeString(caretPos.X, caretPos.Y, "#", tcell.StyleDefault)
case findMode:
writeString(0, height-1, "/"+findText, tcell.StyleDefault)
case markModeMake:
writeString(0, height-1, "mark", tcell.StyleDefault)
case markModeGoto:
writeString(0, height-1, "goto", tcell.StyleDefault)
}
switch currentVimMode {
case linkMode, linkModeNewTab, linkModeMultipleNewTab, linkModeCopy:
if !linkModeWithHints {
findAndHighlightTextOnScreen(linkText)
}
if linkHintWriteStringCalls != nil {
(*linkHintWriteStringCalls)()
}
}
}
func overlayPageStatusMessage() {
_, height := screen.Size()
writeString(0, height-1, CurrentTab.StatusMessage, tcell.StyleDefault)

View File

@ -0,0 +1,672 @@
package browsh
import (
"encoding/json"
"reflect"
"strings"
"time"
"unicode"
"github.com/atotto/clipboard"
"github.com/gdamore/tcell"
)
// TODO: A little description as to the respective responsibilties of this code versus the
// vimium.js code.
type vimMode int
const (
normalMode vimMode = iota + 1
insertMode
insertModeHard
findMode
linkMode
linkModeNewTab
linkModeMultipleNewTab
linkModeCopy
waitMode
visualMode
caretMode
markModeMake
markModeGoto
)
// TODO: What's a mark?
type mark struct {
tabID int
URI string
xScroll int
yScroll int
}
type hintRect struct {
Bottom int `json:"bottom"`
Top int `json:"top"`
Left int `json:"left"`
Right int `json:"right"`
Width int `json:"width"`
Height int `json:"height"`
Href string `json:"href"`
}
var (
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)
// Position coordinate for caret mode
caretPos Coordinate
// For link modes
linkText string
linkHintRects []hintRect
linkHintKeys = "asdfwerxcv"
linkHints []string
linkHintsToRects = make(map[string]*hintRect)
linkModeWithHints = true
linkHintWriteStringCalls *func()
)
func setupLinkHints() {
lowerAlpha := "abcdefghijklmnopqrstuvwxyz"
missingAlpha := lowerAlpha
// Use linkHintKeys first to generate link hints
for i := 0; i < len(linkHintKeys); i++ {
for j := 0; j < len(linkHintKeys); j++ {
linkHints = append(linkHints, string(linkHintKeys[i])+string(linkHintKeys[j]))
}
missingAlpha = strings.Replace(missingAlpha, string(linkHintKeys[i]), "", -1)
}
// `missingAlpha` contains all keys that aren't in `linkHintKeys`
// we use this to generate the last link hint key combinations,
// so this will only be used when we run out of `linkHintKeys` based
// link hint key combinations.
for i := 0; i < len(missingAlpha); i++ {
for j := 0; j < len(lowerAlpha); j++ {
linkHints = append(linkHints, string(missingAlpha[i])+string(lowerAlpha[j]))
}
}
}
// Moves the caret in CaretMode.
// `isCaretAtBoundary` is a function that tests for the reaching of the boundaries of the given axis.
// The axis of motion is decided by giving a reference to `caretPos.X` or `caretPos.Y` as `valRef`.
// The step size and direction is given by the value of step.
func moveVimCaret(isCaretAtBoundary func() bool, valRef *int, step int) {
var prevCell, nextCell, nextNextCell cell
var r rune
hasNextNextCell := false
for isCaretAtBoundary() {
prevCell = getCell(caretPos.X, caretPos.Y-uiHeight)
*valRef += step
nextCell = getCell(caretPos.X, caretPos.Y-uiHeight)
if isCaretAtBoundary() {
*valRef += step
nextNextCell = getCell(caretPos.X, caretPos.Y-uiHeight)
*valRef -= step
hasNextNextCell = true
} else {
hasNextNextCell = false
}
r = nextCell.character[0]
// Check if the next cell is different in any way
if !reflect.DeepEqual(prevCell, nextCell) {
if hasNextNextCell {
// This condition should apply to the spaces between words and the like
// Checking with unicode.isSpace() didn't give correct results for some reason
// TODO: find out what that reason is and improve this
if !unicode.IsLetter(r) && unicode.IsLetter(nextNextCell.character[0]) {
continue
}
// If the upcoming cell is deeply equal we can continue to go forward
if reflect.DeepEqual(nextCell, nextNextCell) {
continue
}
}
// This cell is different and other conditions for continuing don't apply
// therefore we stop going forward.
break
}
}
}
// TODO: This fails if the tab with mark.tabID doesn't exist anymore it should recreate said tab, then go to the mark's URL and position
func gotoMark(mark *mark) {
if CurrentTab.ID != mark.tabID {
ensureTabExists(mark.tabID)
switchToTab(mark.tabID)
}
if CurrentTab.URI != mark.URI {
sendMessageToWebExtension("/tab_command,/url," + mark.URI)
//sleep?
}
doScrollAbsolute(mark.xScroll, mark.yScroll)
}
// Make a mark at the current position in the current tab
func makeMark() *mark {
return &mark{CurrentTab.ID, CurrentTab.URI, CurrentTab.frame.xScroll, CurrentTab.frame.yScroll}
}
func goIntoWaitMode() {
changeVimMode(waitMode)
waitModeStartTime = time.Now()
}
func updateLinkHintDisplay() {
linkHintsToRects = make(map[string]*hintRect)
lh := len(linkHintRects)
var ht string
// List of closures
var fc []*func()
for i, r := range linkHintRects {
// When the number of link hints is small enough
// using just one key for individual link hints suffices.
// Otherwise use the prepared link hint key combinations.
if lh <= len(linkHintKeys) {
ht = string(linkHintKeys[i])
} else {
ht = linkHints[i]
}
// Add the key combination ht to the linkHintsToRects map.
// When the user presses it, we can easily lookup the
// link hint properties associated with it.
linkHintsToRects[ht] = &linkHintRects[i]
// When the first key got hit,
// shorten the link hints accordingly
offsetLeft := 0
if strings.HasPrefix(ht, linkText) {
ht = ht[len(linkText):len(ht)]
offsetLeft = len(linkText)
}
// Make copies of parameter values
rLeftCopy, rTopCopy, htCopy := r.Left, r.Top, ht
// Link hints are in upper case in new tab mode
if currentVimMode == linkModeNewTab {
htCopy = strings.ToUpper(htCopy)
}
// Create closure
f := func() {
writeString(rLeftCopy+offsetLeft, rTopCopy+uiHeight, htCopy, tcell.StyleDefault)
}
fc = append(fc, &f)
}
// Create closure that calls the other closures
ff := func() {
for _, f := range fc {
(*f)()
}
}
linkHintWriteStringCalls = &ff
}
func eraseLinkHints() {
linkText = ""
linkHintWriteStringCalls = nil
linkHintsToRects = make(map[string]*hintRect)
linkHintRects = nil
}
func resetLinkHints() {
linkText = ""
updateLinkHintDisplay()
}
func isNormalModeKey(ev *tcell.EventKey) bool {
if ev != nil && ev.Key() == tcell.KeyESC {
return true
}
return false
}
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:]
}
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*time.Duration(waitModeMaxMilliseconds) {
return
}
changeVimMode(normalMode)
fallthrough
case normalMode:
command = mapVimKeyEvents(ev, "normal")
case insertMode:
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:
command = handleVimMode(ev, "visual")
case caretMode:
command = handleVimMode(ev, "caret")
case markModeMake:
if unicode.IsLower(ev.Rune()) {
if localMarkMap[CurrentTab.ID] == nil {
localMarkMap[CurrentTab.ID] = make(map[rune]*mark)
}
localMarkMap[CurrentTab.ID][ev.Rune()] = makeMark()
} else if unicode.IsUpper(ev.Rune()) {
globalMarkMap[ev.Rune()] = makeMark()
}
command = "normalMode"
case markModeGoto:
if mark, ok := globalMarkMap[ev.Rune()]; ok {
gotoMark(mark)
} else if m, ok := localMarkMap[CurrentTab.ID]; unicode.IsLower(ev.Rune()) && ok {
if mark, ok := m[ev.Rune()]; ok {
gotoMark(mark)
}
}
command = "normalMode"
case findMode:
if isNormalModeKey(ev) {
command = "normalMode"
findText = ""
} else {
if ev.Key() == tcell.KeyEnter {
changeVimMode(normalMode)
command = "findText"
break
}
if ev.Key() == tcell.KeyBackspace || ev.Key() == tcell.KeyBackspace2 {
if len(findText) > 0 {
findText = findText[:len(findText)-1]
}
} else {
findText += string(ev.Rune())
}
}
case linkMode, linkModeNewTab, linkModeMultipleNewTab, linkModeCopy:
if isNormalModeKey(ev) {
command = "normalMode"
eraseLinkHints()
} else {
linkText += string(ev.Rune())
updateLinkHintDisplay()
if linkModeWithHints {
if r, ok := linkHintsToRects[linkText]; ok {
if r != nil {
switch currentVimMode {
case linkMode:
if (*r).Height == 2 {
generateLeftClickYHack((*r).Left, (*r).Top, true)
} else {
generateLeftClick((*r).Left, (*r).Top)
}
case linkModeNewTab:
sendMessageToWebExtension("/new_tab," + r.Href)
case linkModeMultipleNewTab:
resetLinkHints()
return
case linkModeCopy:
clipboard.WriteAll(r.Href)
}
goIntoWaitMode()
eraseLinkHints()
}
}
} else {
coords := findAndHighlightTextOnScreen(linkText)
if len(coords) == 1 {
goIntoWaitMode()
if currentVimMode == linkModeNewTab {
generateMiddleClick(coords[0].X, coords[0].Y)
} else {
generateLeftClick(coords[0].X, coords[0].Y)
}
linkText = ""
return
} else if len(coords) == 0 {
changeVimMode(normalMode)
linkText = ""
return
}
}
}
}
executeVimCommand(command)
}
func executeVimCommand(command string) {
if len(command) == 0 {
return
}
currentCommand := command
command = ""
switch currentCommand {
case "urlUp":
sendMessageToWebExtension("/tab_command,/url_up")
case "urlRoot":
sendMessageToWebExtension("/tab_command,/url_root")
case "scrollToTop":
doScroll(0, -CurrentTab.frame.domRowCount())
case "scrollToBottom":
doScroll(0, CurrentTab.frame.domRowCount())
case "scrollUp":
doScroll(0, -1)
case "scrollDown":
doScroll(0, 1)
case "scrollLeft":
doScroll(-1, 0)
case "scrollRight":
doScroll(1, 0)
case "editURL":
urlBarFocusToggle()
case "editURLInNewTab":
createNewEmptyTabWithURI(CurrentTab.URI)
case "firstTab":
switchToTab(tabsOrder[0])
case "lastTab":
switchToTab(tabsOrder[len(tabsOrder)-1])
case "scrollHalfPageDown":
_, height := screen.Size()
doScroll(0, (height-uiHeight)/2)
case "scrollHalfPageUp":
_, height := screen.Size()
doScroll(0, -((height - uiHeight) / 2))
case "historyBack":
sendMessageToWebExtension("/tab_command,/history_back")
case "historyForward":
sendMessageToWebExtension("/tab_command,/history_forward")
case "reload":
sendMessageToWebExtension("/tab_command,/reload")
case "prevTab":
prevTab()
case "nextTab":
nextTab()
case "previouslyVisitedTab":
previouslyVisitedTab()
case "newTab":
createNewEmptyTab()
case "removeTab":
removeTab(CurrentTab.ID)
case "restoreTab":
restoreTab()
case "duplicateTab":
duplicateTab(CurrentTab.ID)
case "moveTabLeft":
moveTabLeft(CurrentTab.ID)
case "moveTabRight":
moveTabRight(CurrentTab.ID)
case "copyURL":
clipboard.WriteAll(CurrentTab.URI)
case "openClipboardURL":
URI, _ := clipboard.ReadAll()
sendMessageToWebExtension("/tab_command,/url," + URI)
case "openClipboardURLInNewTab":
URI, _ := clipboard.ReadAll()
sendMessageToWebExtension("/new_tab," + URI)
case "focusFirstTextInput":
sendMessageToWebExtension("/tab_command,/focus_first_text_input")
case "followLinkLabeledNext":
sendMessageToWebExtension("/tab_command,/follow_link_labeled_next")
case "followLinkLabeledPrevious":
sendMessageToWebExtension("/tab_command,/follow_link_labeled_previous")
case "viewHelp":
sendMessageToWebExtension("/new_tab,https://www.brow.sh/docs/keybindings/")
case "openLinkInCurrentTab":
changeVimMode(linkMode)
sendMessageToWebExtension("/tab_command,/get_clickable_hints")
eraseLinkHints()
case "openLinkInNewTab":
changeVimMode(linkModeNewTab)
sendMessageToWebExtension("/tab_command,/get_link_hints")
eraseLinkHints()
case "openMultipleLinksInNewTab":
changeVimMode(linkModeMultipleNewTab)
sendMessageToWebExtension("/tab_command,/get_link_hints")
eraseLinkHints()
case "copyLinkURL":
changeVimMode(linkModeCopy)
sendMessageToWebExtension("/tab_command,/get_link_hints")
eraseLinkHints()
case "findText":
fallthrough
case "findNext":
sendMessageToWebExtension("/tab_command,/find_next," + findText)
case "findPrevious":
sendMessageToWebExtension("/tab_command,/find_previous," + findText)
case "makeMark":
changeVimMode(markModeMake)
case "gotoMark":
changeVimMode(markModeGoto)
case "insertMode":
changeVimMode(insertMode)
case "insertModeHard":
changeVimMode(insertModeHard)
case "findMode":
changeVimMode(findMode)
case "normalMode":
changeVimMode(normalMode)
// Visual mode
case "visualMode":
changeVimMode(visualMode)
case "swapVisualModeCursorPosition":
// Stub
case "copyVisualModeSelection":
// Caret mode
case "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 {
var offsets = make([]Coordinate, 0)
var splitString []string
var r rune
var s string
width, height := screen.Size()
screenText := ""
index := 0
for y := 0; y < height-uiHeight; y++ {
screenText = ""
for x := 0; x < width; x++ {
r = getCell(x, y).character[0]
s = string(r)
if len(s) == 0 || len(s) > 1 {
screenText += " "
} else {
screenText += string(getCell(x, y).character[0])
}
}
index = 0
splitString = strings.Split(strings.ToLower(screenText), strings.ToLower(text))
for _, s := range splitString {
if index+len(s) >= width {
break
}
offsets = append(offsets, Coordinate{index + len(s), y})
index += len(s) + len(text)
}
}
return offsets
}
func findAndHighlightTextOnScreen(text string) []Coordinate {
var x, y int
var styling = tcell.StyleDefault
offsets := searchVisibleScreenForText(text)
for _, offset := range offsets {
y = offset.Y
x = offset.X
for z := 0; z < len(text); z++ {
screen.SetContent(x+z, y+uiHeight, rune(text[z]), nil, styling)
}
}
screen.Show()
return offsets
}
// Parse incoming link hints
func parseJSONLinkHints(jsonString string) {
jsonBytes := []byte(jsonString)
if err := json.Unmarshal(jsonBytes, &linkHintRects); err != nil {
Shutdown(err)
}
// Optimize link hint positions
for i := 0; i < len(linkHintRects); i++ {
r := &linkHintRects[i]
// For links that are more than one line high
// we want to position the link hint in the vertical middle
if r.Height > 2 {
if r.Height%2 == 0 {
r.Top += r.Height / 2
} else {
r.Top += r.Height/2 - 1
}
}
// For links that are more one character long we try to move
// the link hint two characters to the right, if possible.
if r.Width > 1 {
o := r.Left
r.Left += r.Width/2 - 1
if r.Left > o+2 {
r.Left = o + 2
}
}
}
Log("Received parseJSONLinkHint")
// This is where the display of actual link hints is prepared
updateLinkHintDisplay()
}

View File

@ -15,6 +15,7 @@ import (
gomega "github.com/onsi/gomega"
"browsh/interfacer/src/browsh"
"github.com/spf13/viper"
)
@ -144,10 +145,13 @@ func sleepUntilPageLoad(maxTime time.Duration) {
// GotoURL sends the browsh browser to the specified URL
func GotoURL(url string) {
SpecialKey(tcell.KeyCtrlL)
browsh.URLBarFocus(true)
Keyboard(url)
SpecialKey(tcell.KeyEnter)
WaitForPageLoad()
// Hack to force text to be rerendered. Because there's a bug where text sometimes doesn't get
// rendered.
mouseClick(3, 3)
// TODO: Looking for the URL isn't optimal because it could be the same URL
// as the previous test.
gomega.Expect(url).To(BeInFrameAt(0, 1))
@ -213,7 +217,7 @@ func GetBgColour(x, y int) [3]int32 {
}
func ensureOnlyOneTab() {
if len(browsh.Tabs) > 1 {
for len(browsh.Tabs) > 1 {
SpecialKey(tcell.KeyCtrlW)
}
}

View File

@ -1,6 +1,7 @@
package test
import (
"browsh/interfacer/src/browsh"
"testing"
"github.com/gdamore/tcell"
@ -8,12 +9,12 @@ import (
. "github.com/onsi/gomega"
)
func TestIntegration(t *testing.T) {
func TestMain(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Integration tests")
}
var _ = Describe("Showing a basic webpage", func() {
var _ = Describe("Core functionality", func() {
BeforeEach(func() {
GotoURL(testSiteURL + "/smorgasbord/")
})
@ -106,10 +107,10 @@ var _ = Describe("Showing a basic webpage", func() {
It("should enter multiple lines of text", func() {
Keyboard(`So here is a lot of text that will hopefully split across lines`)
Expect("So here is a lot of").To(BeInFrameAt(1, 3))
Expect("text that will").To(BeInFrameAt(1, 4))
Expect("hopefully split across").To(BeInFrameAt(1, 5))
Expect("lines").To(BeInFrameAt(1, 6))
Expect("So here is a lot of").To(BeInFrameAt(1, 2))
Expect("text that will").To(BeInFrameAt(1, 3))
Expect("hopefully split across").To(BeInFrameAt(1, 4))
Expect("lines").To(BeInFrameAt(1, 5))
})
It("should scroll multiple lines of text", func() {
@ -120,37 +121,33 @@ var _ = Describe("Showing a basic webpage", func() {
for i := 1; i <= 6; i++ {
SpecialKey(tcell.KeyUp)
}
Expect("lines").To(BeInFrameAt(1, 6))
Expect("lines").To(BeInFrameAt(1, 5))
})
})
})
Describe("Tabs", func() {
BeforeEach(func() {
SpecialKey(tcell.KeyCtrlT)
})
AfterEach(func() {
ensureOnlyOneTab()
})
It("should create a new tab", func() {
Expect("New Tab").To(BeInFrameAt(21, 0))
SpecialKey(tcell.KeyCtrlT)
Expect(len(browsh.Tabs)).To(Equal(2))
})
It("should be able to goto a new URL", func() {
Keyboard(testSiteURL + "/smorgasbord/another.html")
SpecialKey(tcell.KeyEnter)
Expect("Another").To(BeInFrameAt(21, 0))
SpecialKey(tcell.KeyCtrlT)
GotoURL(testSiteURL + "/smorgasbord/another.html")
Expect("Another▄webpage").To(BeInFrameAt(1, 3))
})
It("should cycle to the next tab", func() {
Expect(" ").To(BeInFrameAt(0, 1))
SpecialKey(tcell.KeyCtrlL)
GotoURL(testSiteURL + "/smorgasbord/")
SpecialKey(tcell.KeyCtrlT)
GotoURL(testSiteURL + "/smorgasbord/another.html")
triggerUserKeyFor("tty.keys.next-tab")
URL := testSiteURL + "/smorgasbord/ "
Expect(URL).To(BeInFrameAt(0, 1))
Expect("Smörgåsbord").To(BeInFrameAt(0, 0))
})
})
})
@ -191,7 +188,7 @@ var _ = Describe("Showing a basic webpage", func() {
})
Describe("Text positioning", func() {
It("should position the left/right-aligned coloumns", func() {
It("should position the left/right-aligned columns", func() {
Expect("Smörgåsbord▄(Swedish:").To(BeInFrameAt(12, 10))
Expect("The▄Swedish▄word").To(BeInFrameAt(42, 10))
})

View File

@ -0,0 +1,52 @@
package test
import (
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func TestVim(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Integration tests")
}
var _ = Describe("Vim tests", func() {
BeforeEach(func() {
GotoURL(testSiteURL + "/smorgasbord/")
})
It("should navigate to a new page by using a link hint", func() {
Expect("Another▄page").To(BeInFrameAt(12, 18))
Keyboard("f")
Keyboard("a")
Expect("Another").To(BeInFrameAt(0, 0))
})
It("should scroll the page by one line", func() {
Expect("[ˈsmœrɡɔsˌbuːɖ])▄is▄a").To(BeInFrameAt(12, 11))
Keyboard("j")
Expect("type▄of▄Scandinavian▄").To(BeInFrameAt(12, 11))
})
Describe("Tabs", func() {
BeforeEach(func() {
ensureOnlyOneTab()
})
It("should create a new tab", func() {
Keyboard("t")
Expect("New Tab").To(BeInFrameAt(21, 0))
})
It("should cycle to the next tab", func() {
GotoURL(testSiteURL + "/smorgasbord/")
Keyboard("t")
GotoURL(testSiteURL + "/smorgasbord/another.html")
Keyboard("J")
URL := testSiteURL + "/smorgasbord/ "
Expect(URL).To(BeInFrameAt(0, 1))
})
})
})

View File

@ -32,6 +32,7 @@
"<all_urls>",
"webRequest",
"webRequestBlocking",
"tabs"
"tabs",
"sessions"
]
}

View File

@ -17,6 +17,9 @@ export default MixinBase =>
case "/frame_pixels":
this.sendToTerminal(`/frame_pixels,${message.slice(14)}`);
break;
case "/link_hints":
this.sendToTerminal(`/link_hints,${message.slice(12)}`);
break;
case "/tab_info":
incoming = JSON.parse(utils.rebuildArgsToSingleArg(parts));
this._updateTabInfo(incoming);

View File

@ -29,9 +29,15 @@ export default MixinBase =>
case "/switch_to_tab":
this.switchToTab(parts.slice(1).join(","));
break;
case "/duplicate_tab":
this.duplicateTab(parts.slice(1).join(","));
break;
case "/remove_tab":
this.removeTab(parts.slice(1).join(","));
break;
case "/restore_tab":
this.restoreTab();
break;
case "/raw_text_request":
this._rawTextRequest(parts[1], parts[2], parts.slice(3).join(","));
break;
@ -175,6 +181,24 @@ export default MixinBase =>
this.tabs[id] = null;
}
duplicateTab(id) {
browser.tabs.duplicate(parseInt(id));
}
restoreTab() {
var sessionsInfo = browser.sessions.getRecentlyClosed({ maxResults: 1 });
sessionsInfo.then(this._restoreTab);
}
_restoreTab(sessionsInfo) {
var mySessionInfo = sessionsInfo[0];
if (mySessionInfo.tab) {
browser.sessions.restore(mySessionInfo.tab.sessionId);
} else {
browser.sessions.restore(mySessionInfo.window.sessionId);
}
}
// We use the `browser` object here rather than going into the actual content script
// because the content script may have crashed, even never loaded.
screenshotActiveTab() {

View File

@ -1,4 +1,10 @@
import utils from "utils";
import { Rect } from "vimium";
import { DomUtils } from "vimium";
import { LocalHints } from "vimium";
import { VimiumNormal } from "vimium";
import { MiscVimium } from "vimium";
MiscVimium();
export default MixinBase =>
class extends MixinBase {
@ -35,19 +41,149 @@ export default MixinBase =>
break;
case "/url":
url = utils.rebuildArgsToSingleArg(parts);
document.location.href = url;
window.location.href = url;
break;
case "/url_up":
this.urlUp();
break;
case "/url_root":
window.location.href = window.location.origin;
break;
case "/history_back":
history.go(-1);
break;
case "/history_forward":
history.go(1);
break;
case "/reload":
window.location.reload();
break;
case "/window_stop":
window.stop();
break;
case "/find_next":
this.findNext(parts[1]);
break;
case "/find_previous":
window.find(parts[1], false, true, false, false, true, true);
break;
case "/get_link_hints":
this.getLinkHints(false);
break;
case "/get_clickable_hints":
this.getLinkHints(true);
break;
case "/focus_first_text_input":
this.focusFirstTextInput();
break;
case "/follow_link_labeled_next":
this._followLinkLabeledNext();
break;
case "/follow_link_labeled_previous":
this._followLinkLabeledPrevious();
break;
default:
this.log("Unknown command sent to tab", message);
}
}
focusFirstTextInput() {
VimiumNormal.focusInput(1);
}
//adapted vimium code
followLinkLabeledNext() {
var nextPatterns = "next,more,newer,>,,→,»,≫,>>,weiter" || "";
var nextStrings = nextPatterns.split(",").filter(function(s) {
return s.trim().length;
});
return (
VimiumNormal.findAndFollowRel("next") ||
VimiumNormal.findAndFollowLink(nextStrings)
);
}
_followLinkLabeledNext() {
this.followLinkLabeledNext();
}
//adapted vimium code
followLinkLabeledPrevious() {
var previousPatterns =
"prev,previous,back,older,<,,←,«,≪,<<,zurück" || "";
var previousStrings = previousPatterns.split(",").filter(function(s) {
return s.trim().length;
});
return (
VimiumNormal.findAndFollowRel("prev") ||
VimiumNormal.findAndFollowLink(previousStrings)
);
}
_followLinkLabeledPrevious() {
this.followLinkLabeledPrevious();
}
// Eg; This goes from www.domain.com/topic/suptopic/ to www.domain.com/topic/
urlUp() {
// this is taken from vimium's code
var url = window.location.href;
if (url[url.length - 1] === "/") {
url = url.substring(0, url.length - 1);
}
var urlsplit = url.split("/");
// make sure we haven't hit the base domain yet
if (urlsplit.length > 3) {
urlsplit = urlsplit.slice(0, Math.max(3, urlsplit.length - 1));
window.location.href = urlsplit.join("/");
}
}
getLinkHints(clickable) {
var hints = LocalHints.getLocalHints(!clickable);
var rect, bottom, top, left, right, width, height, results, result, href;
results = [];
for (let idx in hints) {
if (!hints[idx].hasOwnProperty("rect")) {
continue;
}
href = hints[idx]["href"];
rect = hints[idx]["rect"];
bottom = Math.round(
((rect["bottom"] - window.scrollY) *
this.dimensions.scale_factor.height) /
2
);
top = Math.round(
((rect["top"] - window.scrollY) *
this.dimensions.scale_factor.height) /
2
);
left = Math.round(rect["left"] * this.dimensions.scale_factor.width);
right = Math.round(rect["right"] * this.dimensions.scale_factor.width);
result = Rect.create(left, top, right, bottom);
result.href = href;
results.push(result);
}
this.sendMessage(`/link_hints,${JSON.stringify(results)}`);
}
findNext(text) {
window.find(text, false, false, false, false, true, true);
//var s = window.getSelection();
//var oRange = s.getRangeAt(0); //get the text range
//var oRect = oRange.getBoundingClientRect();
//window.scrollTo(400, 20000);
this.dimensions.y_scroll = Math.round(
window.scrollY * this.dimensions.scale_factor.height
);
this.dimensions.x_scroll = Math.round(
window.scrollX * this.dimensions.scale_factor.width
);
this.dimensions.update();
this._mightSendBigFrames();
}
_launch() {
const mode = this.config.http_server_mode_type;
if (mode.includes("raw_text_")) {
@ -119,9 +255,25 @@ export default MixinBase =>
_handleMouse(input) {
switch (input.button) {
case 1:
this._mouseAction("mousemove", input.mouse_x, input.mouse_y);
var y_hack = false;
if (input.hasOwnProperty("y_hack")) {
y_hack = true;
}
this._mouseAction(
"mousemove",
input.mouse_x,
input.mouse_y,
0,
y_hack
);
if (!this._mousedown) {
this._mouseAction("mousedown", input.mouse_x, input.mouse_y);
this._mouseAction(
"mousedown",
input.mouse_x,
input.mouse_y,
0,
y_hack
);
setTimeout(() => {
this.sendSmallTextFrame();
}, 500);
@ -129,10 +281,26 @@ export default MixinBase =>
this._mousedown = true;
break;
case 0:
this._mouseAction("mousemove", input.mouse_x, input.mouse_y);
var y_hack = false;
if (input.hasOwnProperty("y_hack")) {
y_hack = true;
}
this._mouseAction(
"mousemove",
input.mouse_x,
input.mouse_y,
0,
y_hack
);
if (this._mousedown) {
this._mouseAction("click", input.mouse_x, input.mouse_y);
this._mouseAction("mouseup", input.mouse_x, input.mouse_y);
this._mouseAction("click", input.mouse_x, input.mouse_y, 0, y_hack);
this._mouseAction(
"mouseup",
input.mouse_x,
input.mouse_y,
0,
y_hack
);
}
this._mousedown = false;
break;
@ -186,8 +354,12 @@ export default MixinBase =>
}
}
_mouseAction(type, x, y) {
const [dom_x, dom_y] = this._getDOMCoordsFromMouseCoords(x, y);
_mouseAction(type, x, y, button, y_hack = false) {
let [dom_x, dom_y] = this._getDOMCoordsFromMouseCoords(x, y);
if (y_hack) {
const [dom_x2, dom_y2] = this._getDOMCoordsFromMouseCoords(x, y + 1);
dom_y = (dom_y + dom_y2) / 2;
}
const element = document.elementFromPoint(
dom_x - window.scrollX,
dom_y - window.scrollY
@ -208,7 +380,7 @@ export default MixinBase =>
false,
false,
false,
0,
button,
null
);
element.dispatchEvent(clickEvent);

1249
webext/src/vimium.js Normal file

File diff suppressed because it is too large Load Diff