Compare commits
89 Commits
master
...
vim-mode-2
Author | SHA1 | Date |
---|---|---|
Thomas Buckley-Houston | a0752462d4 | |
Thomas Buckley-Houston | 889263ac07 | |
Thomas Buckley-Houston | ed2a7e086b | |
Thomas Buckley-Houston | 5dbb731817 | |
Your Name | 21081ad670 | |
ed2k | 5dc678ed75 | |
Tobias Gläßer | b2ade39223 | |
Tobias Gläßer | 935983725c | |
Thomas Buckley-Houston | b780a79f2e | |
Thomas Buckley-Houston | 714cad8615 | |
Tobias Gläßer | 7a622b230b | |
Thomas Buckley-Houston | ed79db0510 | |
Thomas Buckley-Houston | 8161ea34e6 | |
Thomas Buckley-Houston | 59d2c31acf | |
Thomas Buckley-Houston | eae72e94a6 | |
Thomas Buckley-Houston | e03923394c | |
Tobias Gläßer | 49eebee0c9 | |
Tobias Gläßer | d3fff67c61 | |
Tobias Gläßer | bd5c30640d | |
Tobias Gläßer | 0b7d1dc7ef | |
Tobias Gläßer | 15c7b45b6f | |
Tobias Gläßer | 7b7e6bc308 | |
Tobias Gläßer | 86acac617b | |
Tobias Gläßer | 631483bbd9 | |
Tobias Gläßer | c794f10287 | |
Tobias Gläßer | fac1af7f2a | |
Tobias Gläßer | 8fc15f3301 | |
Tobias Gläßer | ebc8de95b9 | |
Tobias Gläßer | dc9533969f | |
Tobias Gläßer | ca30b7722b | |
Tobias Gläßer | baf808f35d | |
Tobias Gläßer | 3b246ff796 | |
Tobias Gläßer | ee1291b41a | |
Your Name | 2206efba0a | |
Your Name | a1bbf9bc81 | |
Your Name | bfdc1d1e2e | |
Your Name | fdf57cd4e3 | |
Your Name | b9b627046c | |
Your Name | fae952bae1 | |
Your Name | 0c57d3cecc | |
Your Name | 7a39926fa6 | |
Your Name | d9251ec25c | |
Your Name | 9797f400c0 | |
Your Name | a937e46000 | |
Your Name | 8363581fd3 | |
Thomas Buckley-Houston | f290601e11 | |
Your Name | 36ac818ceb | |
Your Name | 76e7eb1ac6 | |
Your Name | ffa586c612 | |
Thomas Buckley-Houston | e88b42a914 | |
Tobias Gläßer | 0c0b9073e4 | |
Tobias Gläßer | 3449ec12d2 | |
Tobias Gläßer | e10510f634 | |
Tobias Gläßer | d034497851 | |
Thomas Buckley-Houston | 2bf920bbd6 | |
Thomas Buckley-Houston | fbb1cfc060 | |
Tobias Gläßer | 68823278a4 | |
Thomas Buckley-Houston | 748bf9d53f | |
Thomas Buckley-Houston | 9c668e83c9 | |
Tobias Gläßer | 565e6f4c3c | |
Thomas Buckley-Houston | 15f541c2dd | |
Thomas Buckley-Houston | af487aefe1 | |
Thomas Buckley-Houston | 26e9c6185f | |
Thomas Buckley-Houston | 3beeb76668 | |
Thomas Buckley-Houston | e3568cd949 | |
Thomas Buckley-Houston | 6f998bea69 | |
Thomas Buckley-Houston | 0bda8f16f5 | |
Thomas Buckley-Houston | bf44f91a27 | |
Thomas Buckley-Houston | d17cb59be4 | |
Thomas Buckley-Houston | 11f746bc08 | |
Thomas Buckley-Houston | 034d9c4a0a | |
Tobias Gläßer | ae1df350a3 | |
Tobias Gläßer | 61cd7e1818 | |
Tobias Gläßer | d037732844 | |
Tobias Gläßer | 721b2c8e15 | |
Tobias Gläßer | 4099f51f83 | |
Tobias Gläßer | 052aecdc9b | |
Tobias Gläßer | b78d896b9b | |
Tobias Gläßer | f730983189 | |
Tobias Gläßer | c40c724564 | |
Tobias Gläßer | 9329c9f830 | |
Tobias Gläßer | 0d2dfee777 | |
Tobias Gläßer | a04bdac73a | |
Tobias Gläßer | 3c8afeda5e | |
Tobias Gläßer | 8b35a6889a | |
Tobias Gläßer | a78d98bdd6 | |
Tobias Gläßer | 6bd4c9699b | |
Tobias Gläßer | ced13791d3 | |
Tobias Gläßer | bdd0df1296 |
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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=
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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>
|
|
@ -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--------------")
|
||||
})
|
||||
|
|
|
@ -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))
|
||||
})
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
"<all_urls>",
|
||||
"webRequest",
|
||||
"webRequestBlocking",
|
||||
"tabs"
|
||||
"tabs",
|
||||
"sessions"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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,
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue