Compare commits

...

89 Commits

Author SHA1 Message Date
Thomas Buckley-Houston a0752462d4
Merge branch 'master' into vim-mode-2022 2023-02-02 18:49:23 -05:00
Thomas Buckley-Houston 889263ac07
Linting 2022-07-25 23:16:46 -04:00
Thomas Buckley-Houston ed2a7e086b
Merge branch 'master' into vim-mode-2022 2022-07-25 23:13:17 -04:00
Thomas Buckley-Houston 5dbb731817
Vim Mode 🥳
After nearly 4 years I'm finally getting round to shipping all the
tremendous hard work of @tobimensch, @j-rewerts and @ed2k. I can't
apologise enough that it's taken so long.
2022-07-25 23:07:40 -04:00
Your Name 21081ad670 merge vim_test into tty_test 2019-06-24 18:52:58 -07:00
ed2k 5dc678ed75
Merge branch 'vim-mode-experimental' into vim-mode-experimental 2019-06-24 20:39:03 -05:00
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
Your Name 2206efba0a get another version of prettier 2019-06-23 22:04:07 -07:00
Your Name a1bbf9bc81 Merge remote-tracking branch 'ed2k/vim-mode-experimental' into localtest 2019-06-23 22:03:22 -07:00
Your Name bfdc1d1e2e get another version of prettier 2019-06-23 22:01:38 -07:00
Your Name fdf57cd4e3 changed by prettier 2019-06-23 21:44:49 -07:00
Your Name b9b627046c merge vim mode code 2019-06-23 21:30:46 -07:00
Your Name fae952bae1 Merge branch 'vim-mode-experimental' into localtest 2019-06-23 18:28:00 -07:00
Your Name 0c57d3cecc better logging and improve tab test stability 2019-06-23 18:19:53 -07:00
Your Name 7a39926fa6 add test delay for tab 2019-06-23 11:03:34 -07:00
Your Name d9251ec25c timestamp, wait for body 2019-06-23 11:03:34 -07:00
Your Name 9797f400c0 clarify dev path 2019-06-23 11:03:34 -07:00
Your Name a937e46000 test 2019-06-18 20:19:34 -07:00
Your Name 8363581fd3 Merge remote-tracking branch 'origin/master' into vim-mode-experimental 2019-06-18 18:46:38 -07:00
Thomas Buckley-Houston f290601e11 Fix for Viper's lowercasing of config keys :/
See spf13/viper#635
2019-06-18 21:19:21 +03:00
Your Name 36ac818ceb move vim test into tty test seems helpful 2019-06-16 17:39:30 -07:00
Your Name 76e7eb1ac6 use test script from master 2019-06-15 21:13:17 -07:00
Your Name ffa586c612 Merge remote-tracking branch 'origin/vim-mode-experimental' 2019-06-15 12:24:25 -07:00
Thomas Buckley-Houston e88b42a914
Create FUNDING.yml 2019-06-13 10:46:18 +03:00
Tobias Gläßer 0c0b9073e4 Merge branch 'vim-mode-experimental' of https://github.com/browsh-org/browsh into vim-mode-experimental 2018-11-23 15:37:47 +01:00
Tobias Gläßer 3449ec12d2 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. 2018-11-23 10:08:11 +01:00
Tobias Gläßer e10510f634 Added vim feature for editing URL in new tab 2018-11-15 23:52:14 +01:00
Tobias Gläßer d034497851 Merge branch 'vim-mode-experimental' of https://github.com/browsh-org/browsh into vim-mode-experimental 2018-11-15 16:45:08 +01:00
Thomas Buckley-Houston 2bf920bbd6 Gofmt: some minor capitalisation 2018-11-15 10:03:22 -05:00
Thomas Buckley-Houston fbb1cfc060 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
2018-11-15 10:03:22 -05:00
Tobias Gläßer 68823278a4 Allow for using Escape to leave input boxes 2018-11-15 09:55:46 -05:00
Thomas Buckley-Houston 748bf9d53f Gofmt: some minor capitalisation 2018-11-14 23:29:23 +09:00
Thomas Buckley-Houston 9c668e83c9 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
2018-11-14 23:15:23 +09:00
Tobias Gläßer 565e6f4c3c Fixed bug where keyEvents variable was initialized wrongly. This led to key combinations only working after a certain number of key strokes. 2018-11-13 07:42:52 -05:00
Thomas Buckley-Houston 15f541c2dd Travis CI: Bash timeout for integration tests 2018-11-13 06:45:14 -05:00
Thomas Buckley-Houston af487aefe1 Vim mode: Small updates from PR review 2018-11-13 06:45:14 -05:00
Thomas Buckley-Houston 26e9c6185f Vim mode: convert unexported symbols to lowercase 2018-11-13 06:45:14 -05:00
Thomas Buckley-Houston 3beeb76668 Fixes tests for Vim mode
Vim mode still needs a lot more tests
2018-11-13 06:45:14 -05:00
Thomas Buckley-Houston e3568cd949 Adds some Vim-specific integration tests 2018-11-13 06:45:14 -05:00
Thomas Buckley-Houston 6f998bea69 Refactors Vim code from tty.go into its own file 2018-11-13 06:45:14 -05:00
Thomas Buckley-Houston 0bda8f16f5 Adds Golang clipboard dep 2018-11-13 06:45:14 -05:00
Thomas Buckley-Houston bf44f91a27 Update JS and Go deps. Bump Browsh to v1.5.0 2018-11-13 06:45:14 -05:00
Thomas Buckley-Houston d17cb59be4 Update FF Marionette commands
In Firefox 63 an old syntax for Marionette commands was deprecated.
Updating mostly just meant prepending `WebDriver` to existing commands.

This should fix most problems in #232
2018-11-13 06:45:14 -05:00
Thomas Buckley-Houston 11f746bc08 Newer NVM formats package.lock differently 2018-11-13 06:45:14 -05:00
Thomas Buckley-Houston 034d9c4a0a Update Go `dep` to v0.5.0 2018-11-13 06:45:14 -05:00
Tobias Gläßer ae1df350a3 Fixed typo 2018-10-29 11:38:48 -04:00
Tobias Gläßer 61cd7e1818 Prettified js files 2018-10-29 10:35:22 -04:00
Tobias Gläßer d037732844 Merge branch 'master' of https://github.com/browsh-org/browsh into vim-mode-experimental 2018-10-29 10:22:32 -04:00
Tobias Gläßer 721b2c8e15 Instead of adding 1 to Y coord, add 1 to height. This fixes an issues
with activating input boxes using link hinting.
2018-10-29 10:20:44 -04:00
Tobias Gläßer 4099f51f83 Added vim like navigation. This is still in an early stage. 2018-10-29 10:18:39 -04:00
Tobias Gläßer 052aecdc9b Added features needed for vim like navigation to the webextension. 2018-10-29 10:05:09 -04:00
Tobias Gläßer b78d896b9b 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.
2018-10-29 09:20:52 -04:00
Tobias Gläßer f730983189 Added duplicate_tab, restore_tab commands. 2018-10-29 09:14:40 -04:00
Tobias Gläßer c40c724564 Added initial configuration for vim like keybindings.
The keybindings are hardcoded for now, but this is going to change.
2018-10-29 09:11:24 -04:00
Tobias Gläßer 9329c9f830 Added sessions permission 2018-10-29 06:40:18 -04:00
Tobias Gläßer 0d2dfee777 Fixed typo in comment 2018-10-24 13:48:54 -04:00
Tobias Gläßer a04bdac73a ignore debug log in interfacer/ directory 2018-10-18 05:30:56 -04:00
Tobias Gläßer 3c8afeda5e ignore manifest.json backup file 2018-10-07 15:10:11 -04:00
Tobias Gläßer 8b35a6889a Set default DISPLAY environment variable for xclipboard functionality 2018-10-07 15:00:23 -04:00
Tobias Gläßer a78d98bdd6 added command for links hints 2018-10-07 14:59:09 -04:00
Tobias Gläßer 6bd4c9699b gofmt code 2018-10-07 14:57:43 -04:00
Tobias Gläßer ced13791d3 Created overlayVimMode function for displaying vim navigation state 2018-10-07 14:52:00 -04:00
Tobias Gläßer bdd0df1296 Added new vimium.js with code taken from vimium 2018-10-07 14:45:01 -04:00
26 changed files with 2592 additions and 90 deletions

2
.gitignore vendored
View File

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

View File

@ -4,6 +4,7 @@ go 1.18
require (
github.com/NYTimes/gziphandler v1.1.1
github.com/atotto/clipboard v0.1.4
github.com/gdamore/tcell v1.4.0
github.com/go-errors/errors v1.4.2
github.com/gorilla/websocket v1.5.0

View File

@ -40,6 +40,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=

View File

@ -11,6 +11,7 @@ import (
"runtime"
"strconv"
"strings"
"time"
// TCell seems to be one of the best projects in any language for handling terminal
// standards across the major OSs.
@ -23,7 +24,7 @@ import (
var (
logo = `
//// ////
//// ////
/ / / /
// //
// // ,,,,,,,,
@ -73,7 +74,7 @@ func Log(msg string) {
}
defer f.Close()
msg = msg + "\n"
msg = time.Now().Format("01-02T15:04:05.999 ") + msg + "\n"
if _, wErr := f.WriteString(msg); wErr != nil {
Shutdown(wErr)
}
@ -159,6 +160,7 @@ func TTYStart(injectedScreen tcell.Screen) {
Log("Starting Browsh CLI client")
go readStdin()
startWebSocketServer()
setupLinkHints()
}
func toInt(char string) int {
@ -185,6 +187,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

@ -62,7 +62,7 @@ func webSocketReader(ws *websocket.Conn) {
triggerSocketWriterClose()
return
}
Shutdown(err)
Shutdown(errors.New(err.Error()))
}
}
}
@ -88,8 +88,10 @@ func handleWebextensionCommand(message []byte) {
}
case "/screenshot":
saveScreenshot(parts[1])
case "/link_hints":
parseJSONLinkHints(strings.Join(parts[1:], ","))
default:
Log("WEBEXT: " + string(message))
Log("IGNORE " + string(message))
}
}
@ -128,7 +130,6 @@ func webSocketWriter(ws *websocket.Conn) {
defer ws.Close()
for {
message = <-stdinChannel
Log(fmt.Sprintf("TTY sending: %s", message))
if err := ws.WriteMessage(websocket.TextMessage, []byte(message)); err != nil {
if err == websocket.ErrCloseSent {
Log("Socket writer detected that the browser closed the websocket")

View File

@ -74,6 +74,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

@ -87,7 +87,7 @@ func startHeadlessFirefox() {
}
in := bufio.NewScanner(stdout)
for in.Scan() {
Log("FF-CONSOLE: " + in.Text())
Log("start headless FF-CONSOLE: " + in.Text())
}
}
@ -185,7 +185,7 @@ func startWERFirefox() {
strings.Contains(in.Text(), "dbus") {
continue
}
Log("FF-CONSOLE: " + in.Text())
Log("start WER FF-CONSOLE: " + in.Text())
}
Log("WER Firefox unexpectedly closed")
}

View File

@ -89,6 +89,14 @@ func (f *frame) buildFrameText(incoming incomingFrameText) {
if !f.isIncomingFrameTextValid(incoming) {
return
}
var s = "/frame_text "
for _, c := range incoming.Text {
if c != "" {
s = s + c
}
}
Log(s)
f.updateInputBoxes(incoming)
f.populateFrameText(incoming)
}
@ -160,9 +168,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 +320,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])
@ -237,6 +237,9 @@ func handleInputBoxInput(ev *tcell.EventKey) {
case tcell.KeyEnter:
activeInputBox.removeSelectedText()
activeInputBox.handleEnterKey(ev.Modifiers())
case tcell.KeyEscape:
activeInputBox.isActive = false
activeInputBox = nil
case tcell.KeyRune:
activeInputBox.removeSelectedText()
activeInputBox.cursorInsertRune(ev.Rune())

View File

@ -28,7 +28,9 @@ func (m *multiLine) convert() []rune {
}
if m.isInsideWord() {
// TODO: This sometimes causes a panic :/
m.currentWordish += m.currentCharacter
if m.currentCharacter != "" {
m.currentWordish += m.currentCharacter
}
} else {
m.addWhitespace()
}

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()
@ -86,16 +92,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()
@ -143,34 +157,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
}
@ -190,27 +236,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)
}
}
@ -259,6 +302,7 @@ func renderCurrentTabWindow() {
activeInputBox.renderCursor()
}
overlayPageStatusMessage()
overlayVimMode()
overlayCallToSupport()
screen.Show()
}
@ -279,6 +323,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,690 @@
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)
var ht string
// List of closures
var fc []*func()
hintStrings := buildHintStrings(len(linkHintRects))
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.
ht = hintStrings[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
}
// Builds the provided number of hint links.
// Based on https://github.com/philc/vimium/blob/881a6fdc3644f55fc02ad56454203f654cc76618/content_scripts/link_hints.coffee#L449
func buildHintStrings(numHints int) []string {
if numHints == 0 {
return make([]string, 0)
}
hints := make([]string, 1)
hints[0] = ""
offset := 0
for len(hints)-offset <= numHints {
hint := hints[offset]
offset = offset + 1
for _, char := range linkHintKeys {
hints = append(hints, string(char)+hint)
}
}
return hints[1 : numHints+1]
}
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

@ -0,0 +1,32 @@
<html>
<head>
<meta charset="utf-8">
<title>Links</title>
</head>
<body>
<p>Links</p>
<a href="smorgasbord/another.html">Link 1</a>
<a href="another2.html">Link 2</a>
<a href="another3.html">Link 3</a>
<a href="another4.html">Link 4</a>
<a href="another5.html">Link 5</a>
<a href="another6.html">Link 6</a>
<a href="another7.html">Link 7</a>
<a href="another8.html">Link 8</a>
<a href="another9.html">Link 9</a>
<a href="another10.html">Link 10</a>
<a href="another11.html">Link 11</a>
<a href="another12.html">Link 12</a>
<a href="another13.html">Link 13</a>
<a href="another14.html">Link 14</a>
<a href="another15.html">Link 15</a>
<a href="another16.html">Link 16</a>
<a href="another17.html">Link 17</a>
<a href="another18.html">Link 18</a>
<a href="another19.html">Link 19</a>
<a href="smorgasbord/another.html">Link 20</a>
</body>
</html>

View File

@ -112,6 +112,7 @@ func waitForNextFrame() {
func WaitForText(text string, x, y int) {
var found string
start := time.Now()
browsh.Log("expect " + text)
for time.Since(start) < perTestTimeout {
found = GetText(x, y, runeCount(text))
if found == text {
@ -119,7 +120,7 @@ func WaitForText(text string, x, y int) {
}
time.Sleep(100 * time.Millisecond)
}
panic("Waiting for '" + text + "' to appear but it didn't")
browsh.Log("Waiting for '" + text + "' to appear but it didn't")
}
// WaitForPageLoad waits for the page to load
@ -132,6 +133,7 @@ func sleepUntilPageLoad(maxTime time.Duration) {
time.Sleep(1000 * time.Millisecond)
for time.Since(start) < maxTime {
if browsh.CurrentTab != nil {
browsh.Log("pageload " + browsh.CurrentTab.PageState)
if browsh.CurrentTab.PageState == "parsing_complete" {
time.Sleep(200 * time.Millisecond)
return
@ -139,11 +141,12 @@ func sleepUntilPageLoad(maxTime time.Duration) {
}
time.Sleep(50 * time.Millisecond)
}
panic("Page didn't load within timeout")
browsh.Log("Page didn't load within timeout")
}
// GotoURL sends the browsh browser to the specified URL
func GotoURL(url string) {
browsh.Log("gotourl " + url)
SpecialKey(tcell.KeyCtrlL)
Keyboard(url)
SpecialKey(tcell.KeyEnter)
@ -159,6 +162,16 @@ func GotoURL(url string) {
time.Sleep(500 * time.Millisecond)
}
func MouseClick() {
// TODO: hack to work around bug where text sometimes doesn't render on page load.
// Clicking with the mouse triggers a reparse by the web extension
time.Sleep(100 * time.Millisecond)
mouseClick(3, 6)
time.Sleep(500 * time.Millisecond)
mouseClick(3, 6)
time.Sleep(500 * time.Millisecond)
}
func mouseClick(x, y int) {
simScreen.InjectMouse(x, y, 1, tcell.ModNone)
simScreen.InjectMouse(x, y, 0, tcell.ModNone)
@ -228,7 +241,6 @@ func initBrowsh() {
browsh.IsTesting = true
simScreen = tcell.NewSimulationScreen("UTF-8")
browsh.Initialise()
}
func stopFirefox() {
@ -243,18 +255,19 @@ func runeCount(text string) int {
}
var _ = ginkgo.BeforeEach(func() {
browsh.Log("\n---------")
browsh.Log(ginkgo.CurrentGinkgoTestDescription().FullTestText)
browsh.Log("---------")
browsh.Log("Attempting to restart WER Firefox...")
stopFirefox()
browsh.ResetTabs()
browsh.StartFirefox()
sleepUntilPageLoad(startupWait)
browsh.IsMonochromeMode = false
browsh.Log("\n---------")
browsh.Log(ginkgo.CurrentGinkgoTestDescription().FullTestText)
browsh.Log("---------")
})
var _ = ginkgo.BeforeSuite(func() {
browsh.Log("BeforeSuite---------")
os.Truncate(framesLogFile, 0)
initTerm()
initBrowsh()
@ -269,4 +282,5 @@ var _ = ginkgo.BeforeSuite(func() {
var _ = ginkgo.AfterSuite(func() {
stopFirefox()
browsh.Log("AfterSuite--------------")
})

View File

@ -1,7 +1,9 @@
package test
import (
"browsh/interfacer/src/browsh"
"testing"
"time"
"github.com/gdamore/tcell"
. "github.com/onsi/ginkgo"
@ -26,6 +28,31 @@ var _ = Describe("Showing a basic webpage", func() {
})
Describe("Interaction", func() {
It("should navigate to a new page by using a link hint", func() {
Expect("Another▄page").To(BeInFrameAt(12, 18))
Keyboard("f")
time.Sleep(500 * time.Millisecond)
Keyboard("a")
time.Sleep(500 * time.Millisecond)
Expect("Another").To(BeInFrameAt(0, 0))
SpecialKey(tcell.KeyCtrlL)
Keyboard(testSiteURL + "/links.html")
SpecialKey(tcell.KeyEnter)
Expect("Links").To(BeInFrameAt(0, 0))
Keyboard("f")
time.Sleep(500 * time.Millisecond)
Keyboard("a")
time.Sleep(500 * time.Millisecond)
Expect("Another").To(BeInFrameAt(0, 0))
// TODO: test double keys
})
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))
})
It("should navigate to a new page by using the URL bar", func() {
SpecialKey(tcell.KeyCtrlL)
Keyboard(testSiteURL + "/smorgasbord/another.html")
@ -107,10 +134,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() {
@ -122,23 +149,19 @@ 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() {
SpecialKey(tcell.KeyCtrlT)
Expect("New Tab").To(BeInFrameAt(21, 0))
// HACK to prevent URL bar being focussed at the start of the next test.
// TODO: Find a more consistent and abstracted way to ensure that the URL
// bar is not focussed at the beginning of new tests.
@ -146,19 +169,40 @@ var _ = Describe("Showing a basic webpage", func() {
})
It("should be able to goto a new URL", func() {
SpecialKey(tcell.KeyCtrlT)
Keyboard(testSiteURL + "/smorgasbord/another.html")
SpecialKey(tcell.KeyEnter)
Expect("Another").To(BeInFrameAt(21, 0))
})
It("should cycle to the next tab", func() {
SpecialKey(tcell.KeyCtrlT)
Expect(" ").To(BeInFrameAt(0, 1))
SpecialKey(tcell.KeyCtrlL)
GotoURL(testSiteURL + "/smorgasbord/another.html")
// SpecialKey(tcell.KeyCtrlL) stops working after ctrl-t
Keyboard(testSiteURL + "/smorgasbord/another.html")
SpecialKey(tcell.KeyEnter)
Expect("Another").To(BeInFrameAt(21, 0))
triggerUserKeyFor("tty.keys.next-tab")
URL := testSiteURL + "/smorgasbord/ "
Expect(URL).To(BeInFrameAt(0, 1))
})
It("should create a new tab", func() {
Keyboard("t")
Expect("New Tab").To(BeInFrameAt(21, 0))
// need this to make tcell to work for the next round
SpecialKey(tcell.KeyCtrlL)
})
It("should cycle to the next tab", func() {
Keyboard("t")
Keyboard(testSiteURL + "/smorgasbord/another.html")
SpecialKey(tcell.KeyEnter)
Expect("Another").To(BeInFrameAt(21, 0))
Keyboard("J")
URL := testSiteURL + "/smorgasbord/ "
Expect(URL).To(BeInFrameAt(0, 1))
})
})
})
})
@ -198,7 +242,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

@ -11,9 +11,11 @@ function versioned_xpi_file() {
# You'll want to use this with `go run ./cmd/browsh --debug --firefox.use-existing`
function build_webextension_watch() {
pushd "$PROJECT_ROOT"/webext/dist || _panic
"$NODE_BIN"/web-ext run \
--firefox contrib/firefoxheadless.sh \
--firefox ../contrib/firefoxheadless.sh \
--verbose
popd || _panic
}
function build_webextension_production() {

View File

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

View File

@ -66,7 +66,6 @@ export default class extends utils.mixins(CommonMixin, TTYCommandsMixin) {
_listenForTerminalMessages() {
this.log("Starting to listen to TTY");
this.terminal.addEventListener("message", (event) => {
this.log("Message from terminal: " + event.data);
this.handleTerminalMessage(event.data);
});
}

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,12 +354,19 @@ 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
);
if (!element) {
return;
}
element.focus();
var clickEvent = document.createEvent("MouseEvents");
clickEvent.initMouseEvent(
@ -208,7 +383,7 @@ export default (MixinBase) =>
false,
false,
false,
0,
button,
null
);
element.dispatchEvent(clickEvent);

View File

@ -129,6 +129,7 @@ export default class extends utils.mixins(CommonMixin, CommandsMixin) {
_setupInteractiveMode() {
this._setupDebouncedFunctions();
this._startMutationObserver();
// TODO: wait until body exists
this.sendAllBigFrames();
// TODO:
// Disabling CSS transitions is not easy, many pages won't even render
@ -211,15 +212,32 @@ export default class extends utils.mixins(CommonMixin, CommandsMixin) {
let target = document.querySelector("body");
let observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (!target) {
const nodes = Array.from(mutation.addedNodes);
for (let node of nodes) {
if (node.matches && node.matches("body")) {
target = node;
observer.observe(target, {
subtree: true,
characterData: true,
childList: true,
});
break;
}
}
}
this.log("!!MUTATION!!", mutation);
this._debouncedSmallTextFrame();
});
});
observer.observe(target, {
subtree: true,
characterData: true,
childList: true,
});
if (target) {
observer.observe(target, {
subtree: true,
characterData: true,
childList: true,
});
}
}
_listenForBackgroundMessages() {

View File

@ -78,6 +78,9 @@ export default class extends utils.mixins(CommonMixin, SerialiseMixin) {
// Search through every node in the DOM looking for displayable text.
__getTextNodes() {
if (!document.body) {
return;
}
this._text_nodes = [];
const walker = document.createTreeWalker(
document.body,

1249
webext/src/vimium.js Normal file

File diff suppressed because it is too large Load Diff