Tabs! As we all know and love
Adding, cycling, deleting. The width of the tab handle is currently fixed to 20. And if there are more tabs than can fit in the tab bar then the extra ones just dissapear off to the right, but they can still be cycled to with CTRL-Tab. The marks the end of feature development in preperation for a version 1 release.
This commit is contained in:
parent
5c7ff71c79
commit
938d51b692
|
@ -142,21 +142,21 @@ func Shell(command string) string {
|
|||
return stripWhitespace(string(out))
|
||||
}
|
||||
|
||||
// Start ... Start Browsh
|
||||
// Start starts Browsh
|
||||
func Start(injectedScreen tcell.Screen) {
|
||||
var isTesting = fmt.Sprintf("%T", injectedScreen) == "*tcell.simscreen"
|
||||
screen = injectedScreen
|
||||
initialise(isTesting)
|
||||
if !*isUseExistingFirefox {
|
||||
if isTesting {
|
||||
writeString(0, 0, "Starting Browsh in test mode...")
|
||||
writeString(0, 0, "Starting Browsh in test mode...", tcell.StyleDefault)
|
||||
go startWERFirefox()
|
||||
} else {
|
||||
writeString(0, 0, "Starting Browsh, the modern terminal web browser...")
|
||||
writeString(0, 0, "Starting Browsh, the modern terminal web browser...", tcell.StyleDefault)
|
||||
setupFirefox()
|
||||
}
|
||||
} else {
|
||||
writeString(0, 0, "Waiting for a Firefox instance to connect...")
|
||||
writeString(0, 0, "Waiting for a Firefox instance to connect...", tcell.StyleDefault)
|
||||
}
|
||||
Log("Starting Browsh CLI client")
|
||||
go readStdin()
|
||||
|
|
|
@ -93,7 +93,7 @@ func startWERFirefox() {
|
|||
"--firefox=" + rootDir + "/webext/contrib/firefoxheadless.sh",
|
||||
"--verbose",
|
||||
"--no-reload",
|
||||
"--url=http://localhost:" + TestServerPort + "/smorgasbord",
|
||||
"--url=https://www.google.com",
|
||||
}
|
||||
firefoxProcess := exec.Command(rootDir+"/webext/node_modules/.bin/web-ext", args...)
|
||||
firefoxProcess.Dir = rootDir + "/webext/dist/"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package browsh
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"encoding/json"
|
||||
"unicode"
|
||||
|
||||
|
@ -76,14 +77,16 @@ func parseJSONFrameText(jsonString string) {
|
|||
if err := json.Unmarshal(jsonBytes, &incoming); err != nil {
|
||||
Shutdown(err)
|
||||
}
|
||||
ensureTabExists(incoming.Meta.TabID)
|
||||
if (!isTabPresent(incoming.Meta.TabID)) {
|
||||
Log(fmt.Sprintf("Not building frame for non-existent tab ID: %d", incoming.Meta.TabID))
|
||||
return
|
||||
}
|
||||
tabs[incoming.Meta.TabID].frame.buildFrameText(incoming)
|
||||
}
|
||||
|
||||
func (f *frame) buildFrameText(incoming incomingFrameText) {
|
||||
f.setup(incoming.Meta)
|
||||
if (!f.isIncomingFrameTextValid(incoming)) { return }
|
||||
CurrentTab = tabs[incoming.Meta.TabID]
|
||||
f.updateInputBoxes(incoming)
|
||||
f.populateFrameText(incoming)
|
||||
}
|
||||
|
@ -94,7 +97,10 @@ func parseJSONFramePixels(jsonString string) {
|
|||
if err := json.Unmarshal(jsonBytes, &incoming); err != nil {
|
||||
Shutdown(err)
|
||||
}
|
||||
ensureTabExists(incoming.Meta.TabID)
|
||||
if (!isTabPresent(incoming.Meta.TabID)) {
|
||||
Log(fmt.Sprintf("Not building frame for non-existent tab ID: %d", incoming.Meta.TabID))
|
||||
return
|
||||
}
|
||||
if (len(tabs[incoming.Meta.TabID].frame.text) == 0) { return }
|
||||
tabs[incoming.Meta.TabID].frame.buildFramePixels(incoming)
|
||||
}
|
||||
|
@ -102,7 +108,6 @@ func parseJSONFramePixels(jsonString string) {
|
|||
func (f *frame) buildFramePixels(incoming incomingFramePixels) {
|
||||
f.setup(incoming.Meta)
|
||||
if (!f.isIncomingFramePixelsValid(incoming)) { return }
|
||||
CurrentTab = tabs[incoming.Meta.TabID]
|
||||
f.populateFramePixels(incoming)
|
||||
}
|
||||
|
||||
|
|
|
@ -168,7 +168,11 @@ func handleInputBoxInput(ev *tcell.EventKey) {
|
|||
activeInputBox.cursorBackspace()
|
||||
case tcell.KeyEnter:
|
||||
if urlInputBox.isActive {
|
||||
sendMessageToWebExtension("/url_bar," + activeInputBox.text)
|
||||
if isNewEmptyTabActive() {
|
||||
sendMessageToWebExtension("/new_tab," + activeInputBox.text)
|
||||
} else {
|
||||
sendMessageToWebExtension("/url_bar," + activeInputBox.text)
|
||||
}
|
||||
urlBarFocus(false)
|
||||
}
|
||||
case tcell.KeyRune:
|
||||
|
|
|
@ -1,16 +1,24 @@
|
|||
package browsh
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// Map of all tab data
|
||||
var tabs = make(map[int]*tab)
|
||||
// Slice of the order in which tabs appear in the tab bar
|
||||
var tabsOrder []int
|
||||
// There can be a race condition between the webext sending a tab state update and the
|
||||
// the tab being deleted, so we need to keep track of all deleted IDs
|
||||
var tabsDeleted []int
|
||||
// CurrentTab is the currently active tab in the TTY browser
|
||||
var CurrentTab *tab
|
||||
|
||||
// A single tab synced from the browser
|
||||
type tab struct {
|
||||
ID int `json:"id"`
|
||||
Active bool `json:"active"`
|
||||
Title string `json:"title"`
|
||||
URI string `json:"uri"`
|
||||
PageState string `json:"page_state"`
|
||||
|
@ -20,24 +28,111 @@ type tab struct {
|
|||
|
||||
func ensureTabExists(id int) {
|
||||
if _, ok := tabs[id]; !ok {
|
||||
tabs[id] = &tab{
|
||||
ID: id,
|
||||
frame: frame{
|
||||
xScroll: 0,
|
||||
yScroll: 0,
|
||||
},
|
||||
newTab(id)
|
||||
if isNewEmptyTabActive() {
|
||||
removeTab(-1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isTabPresent(id int) bool {
|
||||
_, ok := tabs[id]
|
||||
return ok
|
||||
}
|
||||
|
||||
func newTab(id int) {
|
||||
tabsOrder = append(tabsOrder, id)
|
||||
tabs[id] = &tab{
|
||||
ID: id,
|
||||
frame: frame{
|
||||
xScroll: 0,
|
||||
yScroll: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func removeTab(id int) {
|
||||
if (len(tabs) == 1) { quitBrowsh() }
|
||||
tabsDeleted = append(tabsDeleted, id)
|
||||
sendMessageToWebExtension(fmt.Sprintf("/remove_tab,%d", id))
|
||||
nextTab()
|
||||
removeTabIDfromTabsOrder(id)
|
||||
delete(tabs, id)
|
||||
renderUI()
|
||||
renderCurrentTabWindow()
|
||||
}
|
||||
|
||||
// A bit complicated! Just want to remove an integer from a slice whilst retaining
|
||||
// order :/
|
||||
func removeTabIDfromTabsOrder(id int) {
|
||||
for i := 0; i < len(tabsOrder); i++ {
|
||||
if tabsOrder[i] == id {
|
||||
tabsOrder = append(tabsOrder[:i], tabsOrder[i+1:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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() {
|
||||
if isNewEmptyTabActive() { return }
|
||||
newTab(-1)
|
||||
tab := tabs[-1]
|
||||
tab.Title = "New Tab"
|
||||
tab.URI = ""
|
||||
tab.Active = true
|
||||
CurrentTab = tab
|
||||
CurrentTab.frame.resetCells()
|
||||
renderUI()
|
||||
urlBarFocus(true)
|
||||
renderCurrentTabWindow()
|
||||
}
|
||||
|
||||
func isNewEmptyTabActive() bool {
|
||||
return isTabPresent(-1)
|
||||
}
|
||||
|
||||
func nextTab() {
|
||||
for i := 0; i < len(tabsOrder); i++ {
|
||||
if tabsOrder[i] == CurrentTab.ID {
|
||||
if (i + 1 == len(tabsOrder)) {
|
||||
i = 0;
|
||||
} else {
|
||||
i++
|
||||
}
|
||||
sendMessageToWebExtension(fmt.Sprintf("/switch_to_tab,%d", tabsOrder[i]))
|
||||
CurrentTab = tabs[tabsOrder[i]]
|
||||
renderUI()
|
||||
renderCurrentTabWindow()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isTabPreviouslyDeleted(id int) bool {
|
||||
for i := 0; i < len(tabsDeleted); i++ {
|
||||
if tabsDeleted[i] == id {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func parseJSONTabState(jsonString string) {
|
||||
var incoming tab
|
||||
jsonBytes := []byte(jsonString)
|
||||
if err := json.Unmarshal(jsonBytes, &incoming); err != nil {
|
||||
Shutdown(err)
|
||||
}
|
||||
if (isTabPreviouslyDeleted(incoming.ID)) {
|
||||
return
|
||||
}
|
||||
ensureTabExists(incoming.ID)
|
||||
CurrentTab = tabs[incoming.ID]
|
||||
if (incoming.Active && !isNewEmptyTabActive()) {
|
||||
CurrentTab = tabs[incoming.ID]
|
||||
}
|
||||
tabs[incoming.ID].handleStateChange(&incoming)
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@ package browsh
|
|||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"unicode/utf8"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
|
@ -54,10 +53,17 @@ func handleUserKeyPress(ev *tcell.EventKey) {
|
|||
quitBrowsh()
|
||||
case tcell.KeyCtrlL:
|
||||
urlBarFocusToggle()
|
||||
case tcell.KeyCtrlT:
|
||||
createNewEmptyTab()
|
||||
case tcell.KeyCtrlW:
|
||||
removeTab(CurrentTab.ID)
|
||||
}
|
||||
if (ev.Rune() == 'm' && ev.Modifiers() == 4) {
|
||||
toggleMonochromeMode()
|
||||
}
|
||||
if (ev.Key() == 9 && ev.Modifiers() == 0) {
|
||||
nextTab()
|
||||
}
|
||||
forwardKeyPress(ev)
|
||||
if activeInputBox != nil {
|
||||
handleInputBoxInput(ev)
|
||||
|
@ -135,7 +141,7 @@ func handleMouseEvent(ev *tcell.EventMouse) {
|
|||
func handleTTYResize() {
|
||||
width, _ := screen.Size()
|
||||
// TODO: How does this work with wide UTF8 chars?
|
||||
urlInputBox.Width = width - utf8.RuneCountInString(urlBarControls)
|
||||
urlInputBox.Width = width
|
||||
screen.Sync()
|
||||
sendTtySize()
|
||||
}
|
||||
|
|
|
@ -7,9 +7,8 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
urlBarControls = " ← | x | "
|
||||
urlInputBox = inputBox{
|
||||
X: utf8.RuneCountInString(urlBarControls),
|
||||
X: 0,
|
||||
Y: 1,
|
||||
Height: 1,
|
||||
text: "",
|
||||
|
@ -27,12 +26,9 @@ func renderUI() {
|
|||
// Write a simple text string to the screen.
|
||||
// Not for use in the browser frames themselves. If you want anything to appear in
|
||||
// the browser that must be done through the webextension.
|
||||
func writeString(x, y int, str string) {
|
||||
var defaultColours = tcell.StyleDefault
|
||||
rgb := tcell.NewHexColor(int32(0xffffff))
|
||||
defaultColours.Foreground(rgb)
|
||||
func writeString(x, y int, str string, style tcell.Style) {
|
||||
for _, c := range str {
|
||||
screen.SetContent(x, y, c, nil, defaultColours)
|
||||
screen.SetContent(x, y, c, nil, style)
|
||||
x++
|
||||
}
|
||||
screen.Show()
|
||||
|
@ -41,34 +37,40 @@ func writeString(x, y int, str string) {
|
|||
func fillLineToEnd(x, y int) {
|
||||
width, _ := screen.Size()
|
||||
for i := x; i < width - 1; i++ {
|
||||
writeString(i, y, " ")
|
||||
writeString(i, y, " ", tcell.StyleDefault)
|
||||
}
|
||||
}
|
||||
|
||||
func renderTabs() {
|
||||
var tab *tab
|
||||
var style tcell.Style
|
||||
count := 0
|
||||
xPosition := 0
|
||||
tabTitleLength := 15
|
||||
for _, tab := range tabs {
|
||||
if (tab.frame.text == nil) { continue } // TODO: this shouldn't be needed
|
||||
tabTitleLength := 20
|
||||
for _, tabID := range tabsOrder {
|
||||
tab = tabs[tabID]
|
||||
tabTitle := []rune(tab.Title)
|
||||
tabTitleContent := string(tabTitle[0:tabTitleLength]) + " |x "
|
||||
writeString(xPosition, 0, tabTitleContent)
|
||||
tabTitleContent := string(tabTitle[0:tabTitleLength])
|
||||
style = tcell.StyleDefault
|
||||
if (CurrentTab.ID == tabID) { style = tcell.StyleDefault.Reverse(true) }
|
||||
writeString(xPosition, 0, tabTitleContent, style)
|
||||
style = tcell.StyleDefault.Reverse(false)
|
||||
count++
|
||||
xPosition = (count * tabTitleLength) + 4
|
||||
xPosition = count * (tabTitleLength + 1)
|
||||
writeString(xPosition - 1, 0, "|", style)
|
||||
}
|
||||
fillLineToEnd(xPosition, 0)
|
||||
}
|
||||
|
||||
func renderURLBar() {
|
||||
content := urlBarControls
|
||||
var content string
|
||||
if urlInputBox.isActive {
|
||||
writeString(0, 1, content)
|
||||
writeString(0, 1, content, tcell.StyleDefault)
|
||||
content += urlInputBox.text + " "
|
||||
urlInputBox.renderURLBox()
|
||||
} else {
|
||||
content += CurrentTab.URI
|
||||
writeString(0, 1, content)
|
||||
writeString(0, 1, content, tcell.StyleDefault)
|
||||
}
|
||||
fillLineToEnd(utf8.RuneCountInString(content), 1)
|
||||
}
|
||||
|
@ -95,6 +97,6 @@ func urlBarFocus(on bool) {
|
|||
|
||||
func overlayPageStatusMessage() {
|
||||
_, height := screen.Size()
|
||||
writeString(0, height - 1, CurrentTab.StatusMessage)
|
||||
writeString(0, height - 1, CurrentTab.StatusMessage, tcell.StyleDefault)
|
||||
}
|
||||
|
||||
|
|
|
@ -19,12 +19,10 @@ var _ = Describe("Showing a basic webpage", func() {
|
|||
})
|
||||
|
||||
Describe("Browser UI", func() {
|
||||
It("should have the page title, buttons and the current URL", func() {
|
||||
It("should have the page title and current URL", func() {
|
||||
Expect("Smörgåsbord").To(BeInFrameAt(0, 0))
|
||||
Expect(" ← |").To(BeInFrameAt(0, 1))
|
||||
Expect(" x |").To(BeInFrameAt(4, 1))
|
||||
URL := testSiteURL + "/smorgasbord/"
|
||||
Expect(URL).To(BeInFrameAt(9, 1))
|
||||
Expect(URL).To(BeInFrameAt(0, 1))
|
||||
})
|
||||
|
||||
Describe("Interaction", func() {
|
||||
|
@ -76,6 +74,34 @@ var _ = Describe("Showing a basic webpage", func() {
|
|||
Expect("!eM▄esreveR").To(BeInFrameAt(12, 21))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Tabs", func() {
|
||||
BeforeEach(func() {
|
||||
SpecialKey(tcell.KeyCtrlT)
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
SpecialKey(tcell.KeyCtrlW)
|
||||
})
|
||||
|
||||
It("should create a new tab", func() {
|
||||
Expect("New Tab").To(BeInFrameAt(21, 0))
|
||||
})
|
||||
|
||||
It("should be able to goto a new URL", func() {
|
||||
Keyboard(testSiteURL + "/smorgasbord/another.html")
|
||||
SpecialKey(tcell.KeyEnter)
|
||||
Expect("Another").To(BeInFrameAt(21, 0))
|
||||
})
|
||||
|
||||
It("should cycle to the next tab", func() {
|
||||
Expect(" ").To(BeInFrameAt(0, 1))
|
||||
SpecialKey(tcell.KeyCtrlL)
|
||||
simScreen.InjectKey(9, 0, tcell.ModNone)
|
||||
URL := testSiteURL + "/smorgasbord/"
|
||||
Expect(URL).To(BeInFrameAt(0, 1))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
package test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
"strconv"
|
||||
"net/http"
|
||||
"unicode/utf8"
|
||||
|
||||
|
@ -16,9 +14,8 @@ import (
|
|||
)
|
||||
|
||||
var simScreen tcell.SimulationScreen
|
||||
var startupWait = 10
|
||||
var startupWait = 10 * time.Second
|
||||
var perTestTimeout = 2000 * time.Millisecond
|
||||
var browserFingerprint = " ← | x | "
|
||||
var rootDir = browsh.Shell("git rev-parse --show-toplevel")
|
||||
var testSiteURL = "http://localhost:" + browsh.TestServerPort
|
||||
var ti *terminfo.Terminfo
|
||||
|
@ -88,11 +85,18 @@ func WaitForText(text string, x, y int) {
|
|||
|
||||
// WaitForPageLoad waits for the page to load
|
||||
func WaitForPageLoad() {
|
||||
sleepUntilPageLoad(perTestTimeout)
|
||||
}
|
||||
|
||||
func sleepUntilPageLoad(maxTime time.Duration) {
|
||||
start := time.Now()
|
||||
for time.Since(start) < perTestTimeout {
|
||||
if browsh.CurrentTab.PageState == "parsing_complete" {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
return
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
for time.Since(start) < maxTime {
|
||||
if browsh.CurrentTab != nil {
|
||||
if browsh.CurrentTab.PageState == "parsing_complete" {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
return
|
||||
}
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
|
@ -108,7 +112,7 @@ func GotoURL(url string) {
|
|||
WaitForPageLoad()
|
||||
// TODO: Looking for the URL isn't optimal because it could be the same URL
|
||||
// as the previous test.
|
||||
gomega.Expect(url).To(BeInFrameAt(9, 1))
|
||||
gomega.Expect(url).To(BeInFrameAt(0, 1))
|
||||
}
|
||||
|
||||
// BackspaceRemoveURL holds down the backspace key to delete the existing URL
|
||||
|
@ -168,7 +172,7 @@ func GetBgColour(x, y int) [3]int32 {
|
|||
}
|
||||
|
||||
func startHTTPServer() {
|
||||
// Use `NewServerMux()` so as not to conflict with browsh's websocket server
|
||||
// Using `NewServeMux()` so as not to conflict with browsh's websocket server
|
||||
serverMux := http.NewServeMux()
|
||||
serverMux.Handle("/", http.FileServer(http.Dir(rootDir + "/interfacer/test/sites")))
|
||||
http.ListenAndServe(":" + browsh.TestServerPort, serverMux)
|
||||
|
@ -179,24 +183,6 @@ func startBrowsh() {
|
|||
browsh.Start(simScreen)
|
||||
}
|
||||
|
||||
func waitForBrowsh() {
|
||||
var count = 0
|
||||
for {
|
||||
if count > startupWait {
|
||||
var message = "Couldn't find browsh " +
|
||||
"startup signature within " +
|
||||
strconv.Itoa(startupWait) +
|
||||
" seconds"
|
||||
panic(message)
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
if (strings.Contains(GetFrame(), browserFingerprint)) {
|
||||
break
|
||||
}
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
func runeCount(text string) int {
|
||||
return utf8.RuneCountInString(text)
|
||||
}
|
||||
|
@ -212,7 +198,7 @@ var _ = ginkgo.BeforeSuite(func() {
|
|||
initTerm()
|
||||
go startHTTPServer()
|
||||
go startBrowsh()
|
||||
waitForBrowsh()
|
||||
sleepUntilPageLoad(startupWait)
|
||||
})
|
||||
|
||||
var _ = ginkgo.AfterSuite(func() {
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import _ from 'lodash';
|
||||
import stripAnsi from 'strip-ansi';
|
||||
|
||||
// Here we keep the public functions used to mediate communications between
|
||||
// the background process, tabs and the terminal.
|
||||
export default (MixinBase) => class extends MixinBase {
|
||||
sendToCurrentTab(message) {
|
||||
this.currentTab().channel.postMessage(message);
|
||||
if (this.currentTab().channel === undefined) {
|
||||
this.log(`Attempting to send "${message}" to tab without a channel`);
|
||||
} else {
|
||||
this.currentTab().channel.postMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
sendToTerminal(message) {
|
||||
|
@ -14,14 +17,6 @@ export default (MixinBase) => class extends MixinBase {
|
|||
}
|
||||
}
|
||||
|
||||
sendState() {
|
||||
let state = _.mapValues(this.state, (v) => { return v.toString() });
|
||||
state.id = this.currentTab().id;
|
||||
state.title = this.currentTab().title;
|
||||
state.uri = this.currentTab().url;
|
||||
this.sendToTerminal(`/tab_state,${JSON.stringify(state)}`);
|
||||
}
|
||||
|
||||
log(...messages) {
|
||||
if (messages.length === 1) {
|
||||
messages = messages[0].toString();
|
||||
|
|
64
webext/src/background/dimensions.js
Normal file
64
webext/src/background/dimensions.js
Normal file
|
@ -0,0 +1,64 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
import utils from 'utils';
|
||||
import CommonMixin from 'background/common_mixin';
|
||||
|
||||
export default class extends utils.mixins(CommonMixin) {
|
||||
constructor() {
|
||||
super();
|
||||
this.tty = {};
|
||||
this.char = {};
|
||||
}
|
||||
|
||||
setCharValues(incoming) {
|
||||
if (this.char.width != incoming.width ||
|
||||
this.char.height != incoming.height) {
|
||||
this.char = _.clone(incoming);
|
||||
this.resizeBrowserWindow();
|
||||
}
|
||||
}
|
||||
|
||||
resizeBrowserWindow() {
|
||||
if (!this.tty.width || !this.char.width || !this.tty.height || !this.char.height) {
|
||||
this.log(
|
||||
'Not resizing browser window without all of the TTY and character dimensions'
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Does this include scrollbars???
|
||||
const window_width = parseInt(Math.round(this.tty.width * this.char.width));
|
||||
// Leave room for tabs and URL bar
|
||||
const tty_dom_height = this.tty.height - 2;
|
||||
// I don't know why we have to add 4 more lines to the window height?? But without
|
||||
// it text doesn't fill the bottom of the TTY.
|
||||
const window_height = parseInt(Math.round(
|
||||
(tty_dom_height + 4) * this.char.height
|
||||
));
|
||||
const current_window = browser.windows.getCurrent();
|
||||
current_window.then(
|
||||
active_window => {
|
||||
this._sendWindowResizeRequest(active_window, window_width, window_height);
|
||||
},
|
||||
error => {
|
||||
this.log('Error getting current browser window', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
_sendWindowResizeRequest(active_window, width, height) {
|
||||
const tag = 'Resizing browser window';
|
||||
this.log(tag, active_window, width, height);
|
||||
const updating = browser.windows.update(
|
||||
active_window.id,
|
||||
{
|
||||
width: width,
|
||||
height: height,
|
||||
focused: false
|
||||
}
|
||||
);
|
||||
updating.then(
|
||||
info => this.log(tag, info),
|
||||
error => this.log(tag, error)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -2,64 +2,55 @@ import _ from 'lodash';
|
|||
import utils from 'utils';
|
||||
import CommonMixin from 'background/common_mixin';
|
||||
import TTYCommandsMixin from 'background/tty_commands_mixin';
|
||||
import TabCommandsMixin from 'background/tab_commands_mixin';
|
||||
import Tab from 'background/tab';
|
||||
import Dimensions from 'background/dimensions';
|
||||
|
||||
// Boots the background process. Mainly involves connecting to the websocket server
|
||||
// launched by the Browsh CLI client and setting up listeners for new tabs that
|
||||
// have our webextension content script inside them.
|
||||
export default class extends utils.mixins(CommonMixin, TTYCommandsMixin, TabCommandsMixin) {
|
||||
export default class extends utils.mixins(CommonMixin, TTYCommandsMixin) {
|
||||
constructor() {
|
||||
super();
|
||||
// All state that needs to be synced with the TTY
|
||||
this.state = {}
|
||||
// Dimensions from the tab
|
||||
this.dimensions = {
|
||||
dom: {},
|
||||
char: {},
|
||||
frame: {}
|
||||
}
|
||||
// Keep track of connections to active tabs
|
||||
this.dimensions = new Dimensions();
|
||||
// All of the tabs open in the real browser
|
||||
this.tabs = {};
|
||||
// The ID of the tab currently opened in the browser
|
||||
// The ID of the tab currently opened tab
|
||||
this.active_tab_id = null;
|
||||
// Keep track of automatic reloads to problematic tabs
|
||||
this._tab_reloads = [];
|
||||
// When the real GUI browser first launches it's sized to the same size as the desktop
|
||||
this._is_initial_window_pending = true;
|
||||
this._is_initial_window_size_pending = true;
|
||||
// Used so that reconnections to the terminal don't also attempt to reconnect to the
|
||||
// browser.
|
||||
this._is_connected_to_browser = false;
|
||||
// browser DOM.
|
||||
this._is_connected_to_browser_dom = false;
|
||||
// The time in milliseconds between requesting a new TTY-size pixel frame
|
||||
this._small_pixel_frame_rate = 250;
|
||||
// The manager is the hub between tabs and the terminal. First we connect to the
|
||||
// terminal, as that is the process that would have initially booted the browser and
|
||||
// this very code that now runs.
|
||||
this._connectToTerminal();
|
||||
}
|
||||
|
||||
getTab(tab_id) {
|
||||
return this.tabs[tab_id.toString()];
|
||||
}
|
||||
|
||||
reloadTab(id) {
|
||||
const reloading = browser.tabs.reload(id);
|
||||
reloading.then(() => {}, (error) => this.log(error));
|
||||
}
|
||||
|
||||
_connectToTerminal() {
|
||||
// This is the websocket server run by the CLI client
|
||||
this.terminal = new WebSocket('ws://localhost:3334');
|
||||
this.terminal.addEventListener('open', (_event) => {
|
||||
this.log("Webextension connected to the terminal's websocket server");
|
||||
this.dimensions.terminal = this.terminal;
|
||||
this._listenForTerminalMessages();
|
||||
this._connectToBrowser();
|
||||
this._connectToBrowserDOM();
|
||||
this._startFrameRequestLoop();
|
||||
});
|
||||
this.terminal.addEventListener('close', (_event) => {
|
||||
this._reconnectToTerminal();
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Why is this so slow to reconnect?
|
||||
// If we've disconnected from the terminal, but we're still running, then that likely
|
||||
// means the terminal crashed, so we wait to see if the user restarts the terminal.
|
||||
_reconnectToTerminal() {
|
||||
try {
|
||||
this._connectToTerminal();
|
||||
} catch (_e) {
|
||||
_.throttle(() => this._reconnectToTerminal(), 100);
|
||||
_.debounce(() => this._reconnectToTerminal(), 50);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -74,18 +65,25 @@ export default class extends utils.mixins(CommonMixin, TTYCommandsMixin, TabComm
|
|||
});
|
||||
}
|
||||
|
||||
_connectToBrowser() {
|
||||
if (this._is_connected_to_browser) {
|
||||
this.sendState();
|
||||
this.sendToCurrentTab('/rebuild_text');
|
||||
return;
|
||||
_connectToBrowserDOM() {
|
||||
if (!this._is_connected_to_browser_dom) {
|
||||
this._initialDOMConnection()
|
||||
} else {
|
||||
this._reconnectToDOM();
|
||||
}
|
||||
}
|
||||
|
||||
_initialDOMConnection() {
|
||||
this._listenForNewTab();
|
||||
this._listenForTabUpdates();
|
||||
this._listenForTabChannelOpen();
|
||||
this._listenForFocussedTab();
|
||||
this._startFrameRequestLoop();
|
||||
this._is_connected_to_browser = true;
|
||||
}
|
||||
|
||||
_reconnectToDOM() {
|
||||
this.log("Attempting to resend browser state to terminal...");
|
||||
this.currentTab().sendStateToTerminal();
|
||||
this.sendToCurrentTab('/rebuild_text');
|
||||
}
|
||||
|
||||
// For when a tab's content script, triggered by `onDOMContentLoaded`,
|
||||
|
@ -104,25 +102,55 @@ export default class extends utils.mixins(CommonMixin, TTYCommandsMixin, TabComm
|
|||
// manually poll :/
|
||||
_listenForTabUpdates() {
|
||||
setInterval(() => {
|
||||
this._pollAllTabs((tab) => {
|
||||
this._ensureTabConnects(tab);
|
||||
this._pollAllTabs((native_tab_object) => {
|
||||
let tab = this._applyUpdates(native_tab_object);
|
||||
tab.ensureConnectionToBackground();
|
||||
});
|
||||
}, 1000);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
_handleTabUpdate(_tab_id, changes, tab) {
|
||||
this.log(`Tab ${tab.id} detected chages: ${JSON.stringify(changes)}`);
|
||||
this._ensureTabConnects(changes.status, tab)
|
||||
_maybeNewTab(tabish_object) {
|
||||
const tab_id = parseInt(tabish_object.id);
|
||||
if (this.tabs[tab_id] === undefined) {
|
||||
let new_tab = new Tab(tabish_object);
|
||||
this.tabs[tab_id] = new_tab;
|
||||
}
|
||||
return this.tabs[tab_id];
|
||||
}
|
||||
|
||||
_handleTabUpdate(_tab_id, changes, native_tab_object) {
|
||||
this.log(`Tab ${native_tab_object.id} detected chages: ${JSON.stringify(changes)}`);
|
||||
let tab = this.tabs[native_tab_object.id];
|
||||
tab.native_last_change = changes
|
||||
tab.ensureConnectionToBackground();
|
||||
}
|
||||
|
||||
// Note that although this callback signifies that the tab now exists, it is not fully
|
||||
// booted and functional until it has opened a communication channel. It can't do that
|
||||
// until it knows its internally represented ID.
|
||||
_newTabHandler(_request, sender, sendResponse) {
|
||||
this.log(`Tab ${sender.tab.id} (${sender.tab.title}) registered with background process`);
|
||||
if (this._checkForMozillaCliqzTab(sender.tab)) return;
|
||||
// Send the tab back to itself, such that it can be enlightened unto its own nature
|
||||
sendResponse(sender.tab);
|
||||
if (sender.tab.active) {
|
||||
this.active_tab_id = sender.tab.id;
|
||||
this._acknowledgeNewTab(sender.tab);
|
||||
}
|
||||
|
||||
_acknowledgeNewTab(native_tab_object) {
|
||||
let tab = this._applyUpdates(native_tab_object);
|
||||
tab.postDOMLoadInit(this.terminal, this.dimensions);
|
||||
}
|
||||
|
||||
_applyUpdates(tabish_object) {
|
||||
let tab = this._maybeNewTab({id: tabish_object.id});
|
||||
['id', 'title', 'url', 'active'].map(key => {
|
||||
if (tabish_object.hasOwnProperty(key)){
|
||||
tab[key] = tabish_object[key]
|
||||
}
|
||||
});
|
||||
if (tabish_object.active) {
|
||||
this.active_tab_id = tab.id;
|
||||
}
|
||||
return tab;
|
||||
}
|
||||
|
||||
// This is the main communication channel for all back and forth messages to tabs
|
||||
|
@ -131,14 +159,10 @@ export default class extends utils.mixins(CommonMixin, TTYCommandsMixin, TabComm
|
|||
}
|
||||
|
||||
_tabChannelOpenHandler(channel) {
|
||||
// TODO: Can we not just assume that channel.name is the same as this.active_tab_id?
|
||||
this.log(`Tab ${channel.name} connected for communication with background process`);
|
||||
this.tabs[channel.name] = {
|
||||
id: parseInt(channel.name),
|
||||
channel: channel
|
||||
};
|
||||
channel.postMessage(`/tty_size,${this.tty_width},${this.tty_height}`);
|
||||
channel.onMessage.addListener(this.handleTabMessage.bind(this));
|
||||
let tab = this.tabs[parseInt(channel.name)];
|
||||
tab.postConnectionInit(channel);
|
||||
this._is_connected_to_browser_dom = true;
|
||||
}
|
||||
|
||||
_listenForFocussedTab() {
|
||||
|
@ -146,47 +170,10 @@ export default class extends utils.mixins(CommonMixin, TTYCommandsMixin, TabComm
|
|||
}
|
||||
|
||||
_focussedTabHandler(tab) {
|
||||
this.log(`Tab ${tab.id} received new focus`);
|
||||
this.active_tab_id = tab.id
|
||||
}
|
||||
|
||||
_isTabConnected(tab_id) {
|
||||
return typeof this.tabs[tab_id.toString()] !== 'undefined';
|
||||
}
|
||||
|
||||
// For various reasons a tab's content script doesn't always load. Currently
|
||||
// the known reasons are;
|
||||
// 1. Pages without content, such as direct links to images.
|
||||
// 2. Native pages such as `about:config`.
|
||||
// 3. Unknown buggy behaviour such as on Travis :/
|
||||
// So here we attempt some workarounds.
|
||||
_ensureTabConnects(tab) {
|
||||
if (!this._isTabReloadOkay(tab.id)) {
|
||||
return;
|
||||
}
|
||||
if (tab.status === 'complete' && !this._isTabConnected(tab.id)) {
|
||||
this.log(
|
||||
`Automatically reloading tab ${tab.id} that has loaded but not connected ` +
|
||||
'to the webextension'
|
||||
);
|
||||
this.reloadTab(tab.id);
|
||||
this._trackTabReloads(tab.id);
|
||||
}
|
||||
}
|
||||
|
||||
_isTabReloadOkay(tab_id) {
|
||||
const count = this._tab_reloads[tab_id.toString()];
|
||||
if(typeof count === 'undefined') return true;
|
||||
return count <= 3;
|
||||
}
|
||||
|
||||
_trackTabReloads(tab_id) {
|
||||
if(typeof this._tab_reloads[tab_id.toString()] === 'undefined') {
|
||||
this._tab_reloads[tab_id.toString()] = 1;
|
||||
} else {
|
||||
this._tab_reloads[tab_id.toString()] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
_getTabsOnSuccess(windowInfoArray, callback) {
|
||||
for (let windowInfo of windowInfoArray) {
|
||||
windowInfo.tabs.map((tab) => {
|
||||
|
@ -210,52 +197,48 @@ export default class extends utils.mixins(CommonMixin, TTYCommandsMixin, TabComm
|
|||
);
|
||||
}
|
||||
|
||||
// On the very first startup of Firefox on a new profile it loads a tab disclaiming
|
||||
// its data collection to a third-party. Sometimes this tab loads first, sometimes
|
||||
// it loads second. Especially for testing we always need to load the tab we requested
|
||||
// first. So let's just close that tab.
|
||||
// TODO: Only do this for a testing ENV?
|
||||
_checkForMozillaCliqzTab(tab) {
|
||||
if (
|
||||
tab.title.includes('Firefox by default shares data to:') ||
|
||||
tab.title.includes('Firefox Privacy Notice')
|
||||
) {
|
||||
this.log("Removing Firefox startup page")
|
||||
const removing = browser.tabs.remove(tab.id);
|
||||
removing.then(() => {}, (error) => this.log(error));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// The browser window can only be resized once we have both the character dimensions from
|
||||
// the browser tab _and the TTY dimensions from the terminal. There's probably a more
|
||||
// efficient way of triggering this initial window resize, than just waiting for the data
|
||||
// on every frame tick.
|
||||
_initialWindowResize() {
|
||||
if (!this._is_initial_window_pending) return;
|
||||
if(this.char_width && this.char_height && this.tty_width && this.tty_height) {
|
||||
this.resizeBrowserWindow();
|
||||
this._is_initial_window_pending = false;
|
||||
}
|
||||
if (!this._is_initial_window_size_pending) return;
|
||||
this.dimensions.resizeBrowserWindow();
|
||||
this._is_initial_window_size_pending = false;
|
||||
}
|
||||
|
||||
// Instead of having each tab manage its own frame rate, just keep this single, centralised
|
||||
// heartbeat in the background process that switches automatically to the current active
|
||||
// tab.
|
||||
//
|
||||
// Note that by "frame rate" here we justs mean the rate at which a TTY-sized frame of
|
||||
// graphics pixles are sent. Larger frames are sent in response to scroll events and
|
||||
// TTY-sized text frames are sent in response to DOM mutation events.
|
||||
_startFrameRequestLoop() {
|
||||
this.log('BACKGROUND: Frame loop starting')
|
||||
setInterval(() => {
|
||||
if (!this.tty_width || !this.tty_height) {
|
||||
this.log("Not sending frame to TTY without TTY size")
|
||||
return;
|
||||
if (this._is_initial_window_size_pending) this._initialWindowResize();
|
||||
if (this._isAbleToRequestFrame()) {
|
||||
this.sendToCurrentTab('/request_frame');
|
||||
}
|
||||
if (this._is_initial_window_pending) this._initialWindowResize();
|
||||
if (!this.tabs.hasOwnProperty(this.active_tab_id)) {
|
||||
this.log("No active tab, so not requesting a frame");
|
||||
return;
|
||||
}
|
||||
this.sendToCurrentTab('/request_frame');
|
||||
}, 250);
|
||||
}, this._small_pixel_frame_rate);
|
||||
}
|
||||
|
||||
_isAbleToRequestFrame() {
|
||||
if (!this.dimensions.tty.width || !this.dimensions.tty.height) {
|
||||
this.log("Not sending frame to TTY without TTY size")
|
||||
return false;
|
||||
}
|
||||
if (!this.tabs.hasOwnProperty(this.active_tab_id)) {
|
||||
this.log("No active tab, so not requesting a frame");
|
||||
return false;
|
||||
}
|
||||
if (this.currentTab().channel === undefined) {
|
||||
this.log(
|
||||
`Active tab ${this.active_tab_id} does not have a channel, so not requesting a frame`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
131
webext/src/background/tab.js
Normal file
131
webext/src/background/tab.js
Normal file
|
@ -0,0 +1,131 @@
|
|||
import utils from 'utils';
|
||||
|
||||
import CommonMixin from 'background/common_mixin';
|
||||
import TabCommandsMixin from 'background/tab_commands_mixin';
|
||||
|
||||
export default class extends utils.mixins(CommonMixin, TabCommandsMixin) {
|
||||
constructor() {
|
||||
super();
|
||||
// Keep track of automatic reloads to problematic tabs
|
||||
this._tab_reloads = 0;
|
||||
// The maximum amount of times to try to recover a tab that won't connect
|
||||
this._max_number_of_tab_recovery_reloads = 3;
|
||||
}
|
||||
|
||||
postDOMLoadInit(terminal, dimensions) {
|
||||
this.terminal = terminal;
|
||||
this.dimensions = dimensions;
|
||||
this._closeUnwantedStartupTabs();
|
||||
}
|
||||
|
||||
postConnectionInit(channel) {
|
||||
this.channel = channel;
|
||||
this._sendTTYDimensions();
|
||||
this._listenForMessages();
|
||||
}
|
||||
|
||||
isConnected() {
|
||||
return this.channel !== undefined;
|
||||
}
|
||||
|
||||
reload() {
|
||||
const reloading = browser.tabs.reload(this.id);
|
||||
reloading.then(() => {}, error => this.log(error));
|
||||
}
|
||||
|
||||
remove() {
|
||||
const removing = browser.tabs.remove(this.id);
|
||||
removing.then(() => {}, (error) => this.log(error));
|
||||
}
|
||||
|
||||
updateStatus(status, message = '') {
|
||||
let status_message;
|
||||
switch (status) {
|
||||
case 'page_init':
|
||||
status_message = `Loading ${this.url}`;
|
||||
break;
|
||||
case 'parsing_complete':
|
||||
status_message = '';
|
||||
break;
|
||||
case 'window_unload':
|
||||
status_message = 'Loading...';
|
||||
break;
|
||||
default:
|
||||
if (message != '') status_message = message;
|
||||
}
|
||||
this.page_state = status;
|
||||
this.status_message = status_message;
|
||||
this.sendStateToTerminal();
|
||||
}
|
||||
|
||||
getStateObject() {
|
||||
return {
|
||||
id: this.id,
|
||||
active: this.active,
|
||||
removed: this.removed,
|
||||
title: this.title,
|
||||
uri: this.url,
|
||||
page_state: this.page_state,
|
||||
status_message: this.status_message
|
||||
};
|
||||
}
|
||||
|
||||
sendStateToTerminal() {
|
||||
this.sendToTerminal(`/tab_state,${JSON.stringify(this.getStateObject())}`);
|
||||
}
|
||||
|
||||
// For various reasons a tab's content script doesn't always load. Currently
|
||||
// the known reasons are;
|
||||
// 1. Pages without content, such as direct links to images.
|
||||
// 2. Native pages such as `about:config`.
|
||||
// 3. Unknown buggy behaviour such as on Travis :/
|
||||
// So here we attempt some workarounds.
|
||||
ensureConnectionToBackground() {
|
||||
let native_status;
|
||||
if (!this._isItOKToRetryReload()) {
|
||||
return;
|
||||
}
|
||||
if (this.native_last_change) {
|
||||
native_status = this.native_last_change.status;
|
||||
}
|
||||
if (native_status === 'complete' && !this._isConnected()) {
|
||||
this.log(
|
||||
`Automatically reloading tab ${this.id} that has loaded but not connected ` +
|
||||
'to the webextension'
|
||||
);
|
||||
this.reload();
|
||||
this._reload_count++;
|
||||
}
|
||||
}
|
||||
|
||||
_listenForMessages() {
|
||||
this.channel.onMessage.addListener(this.handleTabMessage.bind(this));
|
||||
}
|
||||
|
||||
_sendTTYDimensions() {
|
||||
this.channel.postMessage(
|
||||
`/tty_size,${this.dimensions.tty.width},${this.dimensions.tty.height}`
|
||||
);
|
||||
}
|
||||
|
||||
_isItOKToRetryReload() {
|
||||
return this._reload_count <= this._max_number_of_tab_recovery_reloads;
|
||||
}
|
||||
|
||||
// On the very first startup of Firefox on a new profile it loads a tab disclaiming
|
||||
// its data collection to a third-party. Sometimes this tab loads first, sometimes
|
||||
// it loads second. Especially for testing we always need to load the tab we requested
|
||||
// first. So let's just close that tab.
|
||||
// TODO: Only do this for a testing ENV?
|
||||
_closeUnwantedStartupTabs() {
|
||||
if (
|
||||
this.title.includes('Firefox by default shares data to:') ||
|
||||
this.title.includes('Firefox Privacy Notice')
|
||||
) {
|
||||
this.log("Removing Firefox startup page")
|
||||
this.remove();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
import utils from 'utils';
|
||||
|
||||
// Handle commands from tabs, like sending a frame or information about
|
||||
// the current character dimensions .
|
||||
// the current character dimensions.
|
||||
export default (MixinBase) => class extends MixinBase {
|
||||
// TODO: There needs to be some consistency in this message sending protocol.
|
||||
// Eg; always requiring JSON.
|
||||
handleTabMessage(message) {
|
||||
let incoming;
|
||||
const parts = message.split(',');
|
||||
|
@ -16,18 +18,14 @@ export default (MixinBase) => class extends MixinBase {
|
|||
break;
|
||||
case '/tab_info':
|
||||
incoming = JSON.parse(utils.rebuildArgsToSingleArg(parts));
|
||||
this.currentTab().title = incoming.title
|
||||
this.currentTab().url = incoming.url
|
||||
this.sendState();
|
||||
this._updateTabInfo(incoming);
|
||||
break;
|
||||
case '/dimensions':
|
||||
incoming = JSON.parse(message.slice(12));
|
||||
this._mightResizeWindow(incoming);
|
||||
this.dimensions = incoming;
|
||||
this.dimensions.setCharValues(incoming.char);
|
||||
break;
|
||||
case '/status':
|
||||
this.updateStatus(parts[1]);
|
||||
this.sendState();
|
||||
this.updateStatus(parts[1], parts[2]);
|
||||
break;
|
||||
case '/log':
|
||||
this.log(message.slice(5));
|
||||
|
@ -37,31 +35,9 @@ export default (MixinBase) => class extends MixinBase {
|
|||
}
|
||||
}
|
||||
|
||||
_mightResizeWindow(incoming) {
|
||||
if (this.dimensions.char.width != incoming.char.width ||
|
||||
this.dimensions.char.height != incoming.char.height) {
|
||||
this.dimensions = incoming;
|
||||
this.resizeBrowserWindow();
|
||||
}
|
||||
}
|
||||
|
||||
updateStatus(status, message = '') {
|
||||
let status_message;
|
||||
switch (status) {
|
||||
case 'page_init':
|
||||
status_message = `Loading ${this.currentTab().url}`;
|
||||
break;
|
||||
case 'parsing_complete':
|
||||
status_message = '';
|
||||
break;
|
||||
case 'window_unload':
|
||||
status_message = 'Loading...';
|
||||
break;
|
||||
default:
|
||||
if (message != '') status_message = message;
|
||||
}
|
||||
this.state['page_state'] = status;
|
||||
this.state['status_message'] = status_message;
|
||||
this.sendState();
|
||||
_updateTabInfo(incoming) {
|
||||
this.title = incoming.title;
|
||||
this.url = incoming.url;
|
||||
this.sendStateToTerminal();
|
||||
}
|
||||
};
|
||||
|
|
|
@ -2,10 +2,6 @@ import utils from 'utils';
|
|||
|
||||
// Handle commands coming in from the terminal, like; STDIN keystrokes, TTY resize, etc
|
||||
export default (MixinBase) => class extends MixinBase {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
handleTerminalMessage(message) {
|
||||
const parts = message.split(',');
|
||||
const command = parts[0];
|
||||
|
@ -14,29 +10,46 @@ export default (MixinBase) => class extends MixinBase {
|
|||
this.sendToCurrentTab(message.slice(13));
|
||||
break;
|
||||
case '/tty_size':
|
||||
this.tty_width = parseInt(parts[1]);
|
||||
this.tty_height = parseInt(parts[2]);
|
||||
this.dimensions.tty.width = parseInt(parts[1]);
|
||||
this.dimensions.tty.height = parseInt(parts[2]);
|
||||
if (this.currentTab()) {
|
||||
this.sendToCurrentTab(`/tty_size,${this.tty_width},${this.tty_height}`)
|
||||
this.sendToCurrentTab(
|
||||
`/tty_size,${this.dimensions.tty.width},${this.dimensions.tty.height}`
|
||||
)
|
||||
}
|
||||
this.resizeBrowserWindow();
|
||||
this.dimensions.resizeBrowserWindow();
|
||||
break;
|
||||
case '/stdin':
|
||||
this._handleUICommand(parts);
|
||||
this.sendToCurrentTab(message);
|
||||
break;
|
||||
case '/url_bar':
|
||||
// TODO: move to CLI client
|
||||
this._handleURLBarInput(parts.slice(1).join(','));
|
||||
break;
|
||||
case '/new_tab':
|
||||
this.createNewTab(parts.slice(1).join(','));
|
||||
break;
|
||||
case '/switch_to_tab':
|
||||
this.switchToTab(parts.slice(1).join(','));
|
||||
break;
|
||||
case '/remove_tab':
|
||||
this.removeTab(parts.slice(1).join(','));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_handleUICommand(parts) {
|
||||
const input = JSON.parse(utils.rebuildArgsToSingleArg(parts));
|
||||
// CTRL mappings
|
||||
if (input.mod === 2) {
|
||||
switch(input.char) {
|
||||
default:
|
||||
}
|
||||
}
|
||||
// ALT mappings
|
||||
if (input.mod === 4) {
|
||||
switch(input.char) {
|
||||
case 'P':
|
||||
case 'p':
|
||||
this.screenshotActiveTab();
|
||||
break;
|
||||
}
|
||||
|
@ -49,6 +62,7 @@ export default (MixinBase) => class extends MixinBase {
|
|||
this.sendToCurrentTab(`/url,${final_url}`);
|
||||
}
|
||||
|
||||
// TODO: move to CLI client
|
||||
_getURLfromUserInput(input) {
|
||||
let url;
|
||||
const search_engine = 'https://www.google.com/search?q=';
|
||||
|
@ -68,50 +82,44 @@ export default (MixinBase) => class extends MixinBase {
|
|||
return url;
|
||||
}
|
||||
|
||||
resizeBrowserWindow() {
|
||||
if (!this.tty_width || !this.dimensions.char.width || !this.tty_height || !this.dimensions.char.height) {
|
||||
this.log(
|
||||
'Not resizing browser window without all of the TTY and character dimensions'
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Does this include scrollbars???
|
||||
const window_width = parseInt(Math.round(this.tty_width * this.dimensions.char.width));
|
||||
// Leave room for tabs and URL bar
|
||||
const tty_dom_height = this.tty_height - 2;
|
||||
// I don't know why we have to add 4 more lines to the window height?? But without
|
||||
// it text doesn't fill the bottom of the TTY.
|
||||
const window_height = parseInt(Math.round(
|
||||
(tty_dom_height + 4) * this.dimensions.char.height
|
||||
));
|
||||
const current_window = browser.windows.getCurrent();
|
||||
current_window.then(
|
||||
(active_window) => {
|
||||
this._sendWindowResizeRequest(active_window, window_width, window_height);
|
||||
createNewTab(url) {
|
||||
const final_url = this._getURLfromUserInput(url);
|
||||
let creating = browser.tabs.create({
|
||||
url: final_url
|
||||
});
|
||||
creating.then(
|
||||
tab => {
|
||||
this.log(`New tab created: ${tab}`);
|
||||
},
|
||||
(error) => {
|
||||
this.log('Error getting current browser window', error);
|
||||
error => {
|
||||
this.log(`Error creating new tab: ${error}`);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
_sendWindowResizeRequest(active_window, width, height) {
|
||||
const tag = 'Resizing browser window';
|
||||
this.log(tag, active_window, width, height);
|
||||
const updating = browser.windows.update(
|
||||
active_window.id,
|
||||
{
|
||||
width: width,
|
||||
height: height,
|
||||
focused: false
|
||||
switchToTab(id) {
|
||||
let updating = browser.tabs.update(parseInt(id), {
|
||||
active: true
|
||||
});
|
||||
updating.then(
|
||||
tab => {
|
||||
this.log(`Switched to tab: ${tab.id}`);
|
||||
},
|
||||
error => {
|
||||
this.log(`Error switching to tab: ${error}`);
|
||||
}
|
||||
);
|
||||
updating.then(
|
||||
(info) => {
|
||||
this.log(tag, info);
|
||||
}
|
||||
|
||||
removeTab(id) {
|
||||
this.tabs[id] = null;
|
||||
let removing = browser.tabs.remove(parseInt(id));
|
||||
removing.then(
|
||||
() => {
|
||||
this.log(`Removed tab: ${id}`);
|
||||
},
|
||||
(error) => {
|
||||
this.log(tag, error);
|
||||
error => {
|
||||
this.log(`Error removing tab: ${error}`);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -120,10 +128,10 @@ export default (MixinBase) => class extends MixinBase {
|
|||
// because the content script may have crashed, even never loaded.
|
||||
screenshotActiveTab() {
|
||||
const capturing = browser.tabs.captureVisibleTab({ format: 'jpeg' });
|
||||
capturing.then(this.saveScreenshot.bind(this), error => this.log(error));
|
||||
capturing.then(this._saveScreenshot.bind(this), error => this.log(error));
|
||||
}
|
||||
|
||||
saveScreenshot(imageUri) {
|
||||
_saveScreenshot(imageUri) {
|
||||
const data = imageUri.replace(/^data:image\/\w+;base64,/, "");
|
||||
this.sendToTerminal('/screenshot,' + data);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue