Move UI building to CLI
* CLI is now prepared for supporting multiple tabs. * Refactored global vars into relevant files * Now using real types in JSON sent to CLI * Still doesn't fix integration tests
This commit is contained in:
parent
ba5ce3c58b
commit
b605965e77
|
@ -6,7 +6,6 @@ import (
|
|||
"fmt"
|
||||
"strconv"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
@ -19,7 +18,6 @@ import (
|
|||
"github.com/gdamore/tcell"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/shibukawa/configdir"
|
||||
)
|
||||
|
||||
|
@ -33,51 +31,6 @@ var (
|
|||
isDebug = flag.Bool("debug", false, "Log to ./debug.log")
|
||||
startupURL = flag.String("startup-url", "https://google.com", "URL to launch at startup")
|
||||
timeLimit = flag.Int("time-limit", 0, "Kill Browsh after the specified number of seconds")
|
||||
upgrader = websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
}
|
||||
stdinChannel = make(chan string)
|
||||
marionette net.Conn
|
||||
ffCommandCount = 0
|
||||
isConnectedToWebExtension = false
|
||||
screen tcell.Screen
|
||||
uiHeight = 2
|
||||
frame = Frame{}
|
||||
State map[string]string
|
||||
defaultFFPrefs = map[string]string{
|
||||
"browser.startup.homepage": "'https://www.google.com'",
|
||||
"startup.homepage_welcome_url": "'https://www.google.com'",
|
||||
"startup.homepage_welcome_url.additional": "''",
|
||||
"devtools.errorconsole.enabled": "true",
|
||||
"devtools.chrome.enabled": "true",
|
||||
|
||||
// Send Browser Console (different from Devtools console) output to
|
||||
// STDOUT.
|
||||
"browser.dom.window.dump.enabled": "true",
|
||||
|
||||
// From:
|
||||
// http://hg.mozilla.org/mozilla-central/file/1dd81c324ac7/build/automation.py.in//l388
|
||||
// Make url-classifier updates so rare that they won"t affect tests.
|
||||
"urlclassifier.updateinterval": "172800",
|
||||
// Point the url-classifier to a nonexistent local URL for fast failures.
|
||||
"browser.safebrowsing.provider.0.gethashURL": "'http://localhost/safebrowsing-dummy/gethash'",
|
||||
"browser.safebrowsing.provider.0.keyURL": "'http://localhost/safebrowsing-dummy/newkey'",
|
||||
"browser.safebrowsing.provider.0.updateURL": "'http://localhost/safebrowsing-dummy/update'",
|
||||
|
||||
// Disable self repair/SHIELD
|
||||
"browser.selfsupport.url": "'https://localhost/selfrepair'",
|
||||
// Disable Reader Mode UI tour
|
||||
"browser.reader.detectedFirstArticle": "true",
|
||||
|
||||
// Set the policy firstURL to an empty string to prevent
|
||||
// the privacy info page to be opened on every "web-ext run".
|
||||
// (See #1114 for rationale)
|
||||
"datareporting.policy.firstRunURL": "''",
|
||||
}
|
||||
// TestServerPort ... Port for the test server
|
||||
TestServerPort = "4444"
|
||||
)
|
||||
|
||||
func setupLogging() {
|
||||
|
@ -94,7 +47,8 @@ func setupLogging() {
|
|||
}
|
||||
}
|
||||
|
||||
// Log ... general purpose logger
|
||||
// Log for general purpose logging
|
||||
// TODO: accept generic types
|
||||
func Log(msg string) {
|
||||
if !*isDebug {
|
||||
return
|
||||
|
@ -120,10 +74,12 @@ func initialise(isTesting bool) {
|
|||
setupLogging()
|
||||
}
|
||||
|
||||
// Shutdown ... Cleanly Shutdown browsh
|
||||
// Shutdown tries its best to cleanly shutdown browsh and the associated browser
|
||||
func Shutdown(err error) {
|
||||
exitCode := 0
|
||||
screen.Fini()
|
||||
if screen != nil {
|
||||
screen.Fini()
|
||||
}
|
||||
if err.Error() != "normal" {
|
||||
exitCode = 1
|
||||
println(err.Error())
|
||||
|
|
|
@ -3,12 +3,23 @@ package browsh
|
|||
import (
|
||||
"strings"
|
||||
"fmt"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
var (
|
||||
// TestServerPort is the port for the test web socket server
|
||||
TestServerPort = "4444"
|
||||
upgrader = websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
}
|
||||
stdinChannel = make(chan string)
|
||||
isConnectedToWebExtension = false
|
||||
)
|
||||
|
||||
func sendMessageToWebExtension(message string) {
|
||||
if (!isConnectedToWebExtension) {
|
||||
Log("Webextension not connected. Message not sent: " + message)
|
||||
|
@ -39,20 +50,14 @@ func handleWebextensionCommand(message []byte) {
|
|||
command := parts[0]
|
||||
switch command {
|
||||
case "/frame_text":
|
||||
frame.parseJSONFrameText(strings.Join(parts[1:], ","))
|
||||
renderUI()
|
||||
renderFrame()
|
||||
parseJSONFrameText(strings.Join(parts[1:], ","))
|
||||
renderCurrentTabWindow()
|
||||
case "/frame_pixels":
|
||||
frame.parseJSONFramePixels(strings.Join(parts[1:], ","))
|
||||
parseJSONFramePixels(strings.Join(parts[1:], ","))
|
||||
renderCurrentTabWindow()
|
||||
case "/tab_state":
|
||||
parseJSONTabState(strings.Join(parts[1:], ","))
|
||||
renderUI()
|
||||
renderFrame()
|
||||
case "/state":
|
||||
oldState := map[string]string{}
|
||||
for k,v := range State{
|
||||
oldState[k] = v
|
||||
}
|
||||
parseJSONState(strings.Join(parts[1:], ","))
|
||||
handleStateChange(oldState)
|
||||
case "/screenshot":
|
||||
saveScreenshot(parts[1])
|
||||
default:
|
||||
|
@ -60,26 +65,6 @@ func handleWebextensionCommand(message []byte) {
|
|||
}
|
||||
}
|
||||
|
||||
func parseJSONState(jsonString string) {
|
||||
jsonBytes := []byte(jsonString)
|
||||
if err := json.Unmarshal(jsonBytes, &State); err != nil {
|
||||
Shutdown(err)
|
||||
}
|
||||
}
|
||||
|
||||
func handleStateChange(oldState map[string]string) {
|
||||
if (State["page_state"] != oldState["page_state"]) {
|
||||
Log("State change: page_state=" + State["page_state"])
|
||||
if (State["page_state"] == "page_init") {
|
||||
yScroll = 0
|
||||
}
|
||||
}
|
||||
if (State["frame_width"] != "" && State["frame_height"] != "") {
|
||||
frame.width = toInt(State["frame_width"])
|
||||
frame.height = toInt(State["frame_height"])
|
||||
}
|
||||
}
|
||||
|
||||
// When the socket reader attempts to read from a closed websocket it quickly and
|
||||
// simply closes its associated Go routine. However the socket writer won't
|
||||
// automatically notice until it actually needs to send something. So we force that
|
||||
|
@ -113,11 +98,8 @@ func webSocketServer(w http.ResponseWriter, r *http.Request) {
|
|||
if err != nil {
|
||||
Shutdown(err)
|
||||
}
|
||||
|
||||
isConnectedToWebExtension = true
|
||||
|
||||
go webSocketWriter(ws)
|
||||
go webSocketReader(ws)
|
||||
|
||||
sendTtySize()
|
||||
}
|
||||
|
|
|
@ -14,6 +14,41 @@ import (
|
|||
"github.com/go-errors/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
marionette net.Conn
|
||||
ffCommandCount = 0
|
||||
defaultFFPrefs = map[string]string{
|
||||
"browser.startup.homepage": "'https://www.google.com'",
|
||||
"startup.homepage_welcome_url": "'https://www.google.com'",
|
||||
"startup.homepage_welcome_url.additional": "''",
|
||||
"devtools.errorconsole.enabled": "true",
|
||||
"devtools.chrome.enabled": "true",
|
||||
|
||||
// Send Browser Console (different from Devtools console) output to
|
||||
// STDOUT.
|
||||
"browser.dom.window.dump.enabled": "true",
|
||||
|
||||
// From:
|
||||
// http://hg.mozilla.org/mozilla-central/file/1dd81c324ac7/build/automation.py.in//l388
|
||||
// Make url-classifier updates so rare that they won"t affect tests.
|
||||
"urlclassifier.updateinterval": "172800",
|
||||
// Point the url-classifier to a nonexistent local URL for fast failures.
|
||||
"browser.safebrowsing.provider.0.gethashURL": "'http://localhost/safebrowsing-dummy/gethash'",
|
||||
"browser.safebrowsing.provider.0.keyURL": "'http://localhost/safebrowsing-dummy/newkey'",
|
||||
"browser.safebrowsing.provider.0.updateURL": "'http://localhost/safebrowsing-dummy/update'",
|
||||
|
||||
// Disable self repair/SHIELD
|
||||
"browser.selfsupport.url": "'https://localhost/selfrepair'",
|
||||
// Disable Reader Mode UI tour
|
||||
"browser.reader.detectedFirstArticle": "true",
|
||||
|
||||
// Set the policy firstURL to an empty string to prevent
|
||||
// the privacy info page to be opened on every "web-ext run".
|
||||
// (See #1114 for rationale)
|
||||
"datareporting.policy.firstRunURL": "''",
|
||||
}
|
||||
)
|
||||
|
||||
func startHeadlessFirefox() {
|
||||
Log("Starting Firefox in headless mode")
|
||||
firefoxPath := Shell("which " + *firefoxBinary)
|
||||
|
|
|
@ -8,11 +8,13 @@ import (
|
|||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
// Frame is a single frame for the entire DOM. The TTY is merely a window onto a
|
||||
// A frame is a single snapshot of the DOM. The TTY is merely a window onto a
|
||||
// region of this frame.
|
||||
type Frame struct {
|
||||
type frame struct {
|
||||
width int
|
||||
height int
|
||||
xScroll int
|
||||
yScroll int
|
||||
pixels [][2]tcell.Color
|
||||
text [][]rune
|
||||
textColours []tcell.Color
|
||||
|
@ -25,40 +27,99 @@ type cell struct {
|
|||
bgColour tcell.Color
|
||||
}
|
||||
|
||||
// Text frames received from the webextension are 1 dimensional arrays of strings.
|
||||
// They are made up of a repeating pattern of 4 items:
|
||||
// ["RED", "GREEN", "BLUE", "CHARACTER" ...]
|
||||
func (f *Frame) parseJSONFrameText(jsonString string) {
|
||||
if (len(f.pixels) == 0) { f.preFillPixelsWithBlack() }
|
||||
var index, textIndex int
|
||||
var frame []string
|
||||
type incomingFrameText struct {
|
||||
TabID int `json:"id"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Text []string `json:"text"`
|
||||
Colours []int32 `json:"colours"`
|
||||
}
|
||||
|
||||
// TODO: Can these be sent as binary blobs?
|
||||
type incomingFramePixels struct {
|
||||
TabID int `json:"id"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Colours []int32 `json:"colours"`
|
||||
}
|
||||
|
||||
func (f *frame) rowCount() int {
|
||||
return f.height / 2
|
||||
}
|
||||
|
||||
func parseJSONFrameText(jsonString string) {
|
||||
var incoming incomingFrameText
|
||||
jsonBytes := []byte(jsonString)
|
||||
if err := json.Unmarshal(jsonBytes, &frame); err != nil {
|
||||
if err := json.Unmarshal(jsonBytes, &incoming); err != nil {
|
||||
Shutdown(err)
|
||||
}
|
||||
if (len(frame) < f.width * (f.height / 2 ) * 4) {
|
||||
ensureTabExists(incoming.TabID)
|
||||
tabs[incoming.TabID].frame.buildFrameText(incoming)
|
||||
}
|
||||
|
||||
func (f *frame) buildFrameText(incoming incomingFrameText) {
|
||||
f.setup(incoming.Width, incoming.Height)
|
||||
if (len(f.pixels) == 0) { f.preFillPixels() }
|
||||
if (!f.isIncomingFrameTextValid(incoming)) { return }
|
||||
CurrentTab = tabs[incoming.TabID]
|
||||
f.populateFrameText(incoming)
|
||||
}
|
||||
|
||||
func parseJSONFramePixels(jsonString string) {
|
||||
var incoming incomingFramePixels
|
||||
jsonBytes := []byte(jsonString)
|
||||
if err := json.Unmarshal(jsonBytes, &incoming); err != nil {
|
||||
Shutdown(err)
|
||||
}
|
||||
ensureTabExists(incoming.TabID)
|
||||
if (len(tabs[incoming.TabID].frame.text) == 0) { return }
|
||||
tabs[incoming.TabID].frame.buildFramePixels(incoming)
|
||||
}
|
||||
|
||||
func (f *frame) buildFramePixels(incoming incomingFramePixels) {
|
||||
f.setup(incoming.Width, incoming.Height)
|
||||
if (!f.isIncomingFramePixelsValid(incoming)) { return }
|
||||
f.populateFramePixels(incoming)
|
||||
}
|
||||
|
||||
func (f *frame) setup(width, height int) {
|
||||
f.width = width
|
||||
f.height = height
|
||||
f.resetCells()
|
||||
}
|
||||
|
||||
func (f *frame) resetCells() {
|
||||
f.cells = make([]cell, (f.rowCount()) * f.width)
|
||||
}
|
||||
|
||||
func (f *frame) isIncomingFrameTextValid(incoming incomingFrameText) bool {
|
||||
if (len(incoming.Text) < f.width * (f.rowCount())) {
|
||||
Log(
|
||||
fmt.Sprintf(
|
||||
"Not parsing small text frame. Data length: %d, current dimensions: %dx(%d/2)*4=%d",
|
||||
len(frame),
|
||||
"Not parsing small text frame. Data length: %d, current dimensions: %dx(%d/2)=%d",
|
||||
len(incoming.Text),
|
||||
f.width,
|
||||
f.height,
|
||||
f.width * (f.height / 2 ) * 4))
|
||||
return
|
||||
f.width * (f.rowCount())))
|
||||
return false
|
||||
}
|
||||
f.cells = make([]cell, (f.height / 2) * f.width)
|
||||
f.text = make([][]rune, (f.height / 2) * f.width)
|
||||
f.textColours = make([]tcell.Color, (f.height / 2) * f.width)
|
||||
for y := 0; y < f.height / 2; y++ {
|
||||
return true
|
||||
}
|
||||
|
||||
func (f *frame) populateFrameText(incoming incomingFrameText) {
|
||||
var index, colourIndex int
|
||||
f.text = make([][]rune, (f.rowCount()) * f.width)
|
||||
f.textColours = make([]tcell.Color, (f.rowCount()) * f.width)
|
||||
for y := 0; y < f.rowCount(); y++ {
|
||||
for x := 0; x < f.width; x++ {
|
||||
index = ((f.width * y) + x)
|
||||
textIndex = index * 4
|
||||
colourIndex = index * 3
|
||||
f.textColours[index] = tcell.NewRGBColor(
|
||||
toInt32(frame[textIndex + 0]),
|
||||
toInt32(frame[textIndex + 1]),
|
||||
toInt32(frame[textIndex + 2]),
|
||||
incoming.Colours[colourIndex + 0],
|
||||
incoming.Colours[colourIndex + 1],
|
||||
incoming.Colours[colourIndex + 2],
|
||||
)
|
||||
f.text[index] = []rune(frame[textIndex + 3])
|
||||
f.text[index] = []rune(incoming.Text[index])
|
||||
f.buildCell(x, y);
|
||||
}
|
||||
}
|
||||
|
@ -66,40 +127,21 @@ func (f *Frame) parseJSONFrameText(jsonString string) {
|
|||
|
||||
// This covers the rare situation where a text frame has been sent before any pixel
|
||||
// data has been populated.
|
||||
func (f *Frame) preFillPixelsWithBlack() {
|
||||
func (f *frame) preFillPixels() {
|
||||
f.pixels = make([][2]tcell.Color, f.height * f.width)
|
||||
for i := range f.pixels {
|
||||
f.pixels[i] = [2]tcell.Color{
|
||||
tcell.NewRGBColor(0, 0, 0),
|
||||
tcell.NewRGBColor(0, 0, 0),
|
||||
tcell.NewRGBColor(255, 255, 255),
|
||||
tcell.NewRGBColor(255, 255, 255),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pixel frames received from the webextension are 1 dimensional arrays of strings.
|
||||
// They are made up of a repeating pattern of 6 items:
|
||||
// ["FG RED", "FG GREEN", "FG BLUE", "BG RED", "BG GREEN", "BG BLUE" ...]
|
||||
// TODO: Can these be sent as binary blobs?
|
||||
func (f *Frame) parseJSONFramePixels(jsonString string) {
|
||||
if (len(f.text) == 0) { return }
|
||||
func (f *frame) populateFramePixels(incoming incomingFramePixels) {
|
||||
var index, indexFg, indexBg, pixelIndexFg, pixelIndexBg int
|
||||
var frame []string
|
||||
jsonBytes := []byte(jsonString)
|
||||
if err := json.Unmarshal(jsonBytes, &frame); err != nil {
|
||||
Shutdown(err)
|
||||
}
|
||||
if (len(frame) != f.width * f.height * 3) {
|
||||
Log(
|
||||
fmt.Sprintf(
|
||||
"Not parsing pixels frame. Data length: %d, current dimensions: %dx%d*3=%d",
|
||||
len(frame),
|
||||
f.width,
|
||||
f.height,
|
||||
f.width * f.height * 3))
|
||||
return
|
||||
}
|
||||
f.cells = make([]cell, (f.height / 2) * f.width)
|
||||
f.resetCells()
|
||||
f.pixels = make([][2]tcell.Color, f.height * f.width)
|
||||
data := incoming.Colours
|
||||
for y := 0; y < f.height; y += 2 {
|
||||
for x := 0; x < f.width; x++ {
|
||||
index = (f.width * (y / 2)) + x
|
||||
|
@ -109,14 +151,14 @@ func (f *Frame) parseJSONFramePixels(jsonString string) {
|
|||
pixelIndexFg = indexFg * 3
|
||||
pixels := [2]tcell.Color{
|
||||
tcell.NewRGBColor(
|
||||
toInt32(frame[pixelIndexBg + 0]),
|
||||
toInt32(frame[pixelIndexBg + 1]),
|
||||
toInt32(frame[pixelIndexBg + 2]),
|
||||
data[pixelIndexBg + 0],
|
||||
data[pixelIndexBg + 1],
|
||||
data[pixelIndexBg + 2],
|
||||
),
|
||||
tcell.NewRGBColor(
|
||||
toInt32(frame[pixelIndexFg + 0]),
|
||||
toInt32(frame[pixelIndexFg + 1]),
|
||||
toInt32(frame[pixelIndexFg + 2]),
|
||||
data[pixelIndexFg + 0],
|
||||
data[pixelIndexFg + 1],
|
||||
data[pixelIndexFg + 2],
|
||||
),
|
||||
}
|
||||
f.pixels[index] = pixels
|
||||
|
@ -125,14 +167,27 @@ func (f *Frame) parseJSONFramePixels(jsonString string) {
|
|||
}
|
||||
}
|
||||
|
||||
func (f *frame) isIncomingFramePixelsValid(incoming incomingFramePixels) bool {
|
||||
if (len(incoming.Colours) != f.width * f.height * 3) {
|
||||
Log(
|
||||
fmt.Sprintf(
|
||||
"Not parsing pixels frame. Data length: %d, current dimensions: %dx%d*3=%d",
|
||||
len(incoming.Colours),
|
||||
f.width,
|
||||
f.height,
|
||||
f.width * f.height * 3))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// This is where we implement the UTF8 half-block trick.
|
||||
// This a half-block: "▄", notice how it takes up precisely half a text cell. This
|
||||
// means that we can get 2 pixel colours from it, the top pixel comes from setting
|
||||
// the background colour and the bottom pixel comes from setting the foreground
|
||||
// colour, namely the colour of the text.
|
||||
func (f *Frame) buildCell(x int, y int) {
|
||||
func (f *frame) buildCell(x int, y int) {
|
||||
index := ((f.width * y) + x)
|
||||
if (index >= len(f.pixels)) { return } // TODO: There must be a better way
|
||||
character, fgColour := f.getCharacterAt(index)
|
||||
pixelFg, bgColour := f.getPixelColoursAt(index)
|
||||
if (isCharacterTransparent(character)) {
|
||||
|
@ -142,13 +197,13 @@ func (f *Frame) buildCell(x int, y int) {
|
|||
f.addCell(index, fgColour, bgColour, character)
|
||||
}
|
||||
|
||||
func (f *Frame) getCharacterAt(index int) ([]rune, tcell.Color) {
|
||||
func (f *frame) getCharacterAt(index int) ([]rune, tcell.Color) {
|
||||
character := f.text[index]
|
||||
colour := f.textColours[index]
|
||||
return character, colour
|
||||
}
|
||||
|
||||
func (f *Frame) getPixelColoursAt(index int) (tcell.Color, tcell.Color) {
|
||||
func (f *frame) getPixelColoursAt(index int) (tcell.Color, tcell.Color) {
|
||||
bgColour := f.pixels[index][0]
|
||||
fgColour := f.pixels[index][1]
|
||||
return fgColour, bgColour
|
||||
|
@ -158,7 +213,7 @@ func isCharacterTransparent(character []rune) bool {
|
|||
return string(character) == "" || unicode.IsSpace(character[0]);
|
||||
}
|
||||
|
||||
func (f *Frame) addCell(index int, fgColour, bgColour tcell.Color, character []rune) {
|
||||
func (f *frame) addCell(index int, fgColour, bgColour tcell.Color, character []rune) {
|
||||
newCell := cell{
|
||||
fgColour: fgColour,
|
||||
bgColour: bgColour,
|
||||
|
@ -166,3 +221,9 @@ func (f *Frame) addCell(index int, fgColour, bgColour tcell.Color, character []r
|
|||
}
|
||||
f.cells[index] = newCell
|
||||
}
|
||||
|
||||
func (f *frame) limitScroll(height int) {
|
||||
maxYScroll := f.rowCount() - height
|
||||
if (f.yScroll > maxYScroll) { f.yScroll = maxYScroll }
|
||||
if (f.yScroll < 0) { f.yScroll = 0 }
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ package browsh
|
|||
|
||||
import (
|
||||
"testing"
|
||||
"encoding/json"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
@ -13,29 +12,37 @@ func TestFrameBuilder(t *testing.T) {
|
|||
RunSpecs(t, "Frame builder tests")
|
||||
}
|
||||
|
||||
var testFrame Frame
|
||||
var testFrame *frame
|
||||
|
||||
var frameJSONText, _ = json.Marshal([]string{
|
||||
"77", "77", "77", "A",
|
||||
"101", "101", "101", "b",
|
||||
"102", "102", "102", "c",
|
||||
"103", "103", "103", "",
|
||||
})
|
||||
var frameText = string(frameJSONText)
|
||||
|
||||
var frameJSONPixels, _ = json.Marshal([]string{
|
||||
"254", "254", "254", "111", "111", "111",
|
||||
"1", "1", "1", "2", "2", "2",
|
||||
"3", "3", "3", "4", "4", "4",
|
||||
"123", "123", "123", "200", "200", "200",
|
||||
})
|
||||
var framePixels = string(frameJSONPixels)
|
||||
var frameJSONText = `{
|
||||
"id": 1,
|
||||
"width": 2,
|
||||
"height": 4,
|
||||
"text": ["A", "b", "c", ""],
|
||||
"colours": [
|
||||
77, 77, 77,
|
||||
101, 101, 101,
|
||||
102, 102, 102,
|
||||
103, 103, 103
|
||||
]
|
||||
}`
|
||||
|
||||
var frameJSONPixels = `{
|
||||
"id": 1,
|
||||
"width": 2,
|
||||
"height": 4,
|
||||
"colours": [
|
||||
254, 254, 254, 111, 111, 111,
|
||||
1, 1, 1, 2, 2, 2,
|
||||
3, 3, 3, 4, 4, 4,
|
||||
123, 123, 123, 200, 200, 200
|
||||
]
|
||||
}`
|
||||
|
||||
var _ = Describe("Frame struct", func() {
|
||||
BeforeEach(func() {
|
||||
testFrame = Frame{width: 2, height: 4}
|
||||
testFrame.parseJSONFrameText(frameText)
|
||||
parseJSONFrameText(frameJSONText)
|
||||
testFrame = &tabs[1].frame
|
||||
})
|
||||
|
||||
It("should parse JSON frame text", func() {
|
||||
|
@ -47,7 +54,7 @@ var _ = Describe("Frame struct", func() {
|
|||
|
||||
It("should parse JSON pixels (for text-less cells)", func() {
|
||||
var r, g, b int32
|
||||
testFrame.parseJSONFramePixels(framePixels)
|
||||
parseJSONFramePixels(frameJSONPixels)
|
||||
r, g, b = testFrame.cells[3].fgColour.RGB()
|
||||
Expect([3]int32{r, g, b}).To(Equal([3]int32{200, 200, 200}))
|
||||
r, g, b = testFrame.cells[3].bgColour.RGB()
|
||||
|
@ -56,7 +63,7 @@ var _ = Describe("Frame struct", func() {
|
|||
|
||||
It("should parse JSON pixels (using text for foreground)", func() {
|
||||
var r, g, b int32
|
||||
testFrame.parseJSONFramePixels(framePixels)
|
||||
parseJSONFramePixels(frameJSONPixels)
|
||||
r, g, b = testFrame.cells[0].fgColour.RGB()
|
||||
Expect([3]int32{r, g, b}).To(Equal([3]int32{77, 77, 77}))
|
||||
r, g, b = testFrame.cells[0].bgColour.RGB()
|
||||
|
|
53
interfacer/src/browsh/tab.go
Normal file
53
interfacer/src/browsh/tab.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
package browsh
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
var tabs = make(map[int]*tab)
|
||||
// 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"`
|
||||
Title string `json:"title"`
|
||||
URI string `json:"uri"`
|
||||
PageState string `json:"page_state"`
|
||||
StatusMessage string `json:"status_message"`
|
||||
frame frame
|
||||
}
|
||||
|
||||
func ensureTabExists(id int) {
|
||||
if _, ok := tabs[id]; !ok {
|
||||
tabs[id] = &tab{ID: id}
|
||||
}
|
||||
}
|
||||
|
||||
func parseJSONTabState(jsonString string) {
|
||||
var incoming tab
|
||||
jsonBytes := []byte(jsonString)
|
||||
if err := json.Unmarshal(jsonBytes, &incoming); err != nil {
|
||||
Shutdown(err)
|
||||
}
|
||||
ensureTabExists(incoming.ID)
|
||||
tabs[incoming.ID].handleStateChange(incoming)
|
||||
}
|
||||
|
||||
|
||||
func (t *tab) handleStateChange(incoming tab) {
|
||||
if (t.PageState != incoming.PageState) {
|
||||
// TODO: Take the browser's scroll events as lead
|
||||
if (incoming.PageState == "page_init") {
|
||||
t.frame.yScroll = 0
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: What's the idiomatic Golang way to do this?
|
||||
t.Title = incoming.Title
|
||||
t.URI = incoming.URI
|
||||
t.PageState = incoming.PageState
|
||||
t.StatusMessage = incoming.StatusMessage
|
||||
|
||||
renderUI()
|
||||
}
|
|
@ -12,6 +12,8 @@ import (
|
|||
var (
|
||||
xScroll = 0
|
||||
yScroll = 0
|
||||
screen tcell.Screen
|
||||
uiHeight = 2
|
||||
)
|
||||
|
||||
func setupTcell() {
|
||||
|
@ -52,8 +54,8 @@ func readStdin() {
|
|||
button := ev.Buttons()
|
||||
eventMap := map[string]interface{}{
|
||||
"button": int(button),
|
||||
"mouse_x": int(x + xScroll),
|
||||
"mouse_y": int(y - uiHeight + yScroll),
|
||||
"mouse_x": int(x + CurrentTab.frame.xScroll),
|
||||
"mouse_y": int(y - uiHeight + CurrentTab.frame.yScroll),
|
||||
"modifiers": int(ev.Modifiers()),
|
||||
}
|
||||
marshalled, _ := json.Marshal(eventMap)
|
||||
|
@ -73,33 +75,27 @@ func handleUserKeyPress(ev *tcell.EventKey) {
|
|||
}
|
||||
|
||||
func handleScrolling(ev *tcell.EventKey) {
|
||||
yScrollOriginal := yScroll
|
||||
yScrollOriginal := CurrentTab.frame.yScroll
|
||||
_, height := screen.Size()
|
||||
height -= uiHeight
|
||||
if ev.Key() == tcell.KeyUp {
|
||||
yScroll -= 2
|
||||
CurrentTab.frame.yScroll -= 2
|
||||
}
|
||||
if ev.Key() == tcell.KeyDown {
|
||||
yScroll += 2
|
||||
CurrentTab.frame.yScroll += 2
|
||||
}
|
||||
if ev.Key() == tcell.KeyPgUp {
|
||||
yScroll -= height
|
||||
CurrentTab.frame.yScroll -= height
|
||||
}
|
||||
if ev.Key() == tcell.KeyPgDn {
|
||||
yScroll += height
|
||||
CurrentTab.frame.yScroll += height
|
||||
}
|
||||
limitScroll(height)
|
||||
if (yScroll != yScrollOriginal) {
|
||||
renderFrame()
|
||||
CurrentTab.frame.limitScroll(height)
|
||||
if (CurrentTab.frame.yScroll != yScrollOriginal) {
|
||||
renderCurrentTabWindow()
|
||||
}
|
||||
}
|
||||
|
||||
func limitScroll(height int) {
|
||||
maxYScroll := (frame.height / 2) - height
|
||||
if (yScroll > maxYScroll) { yScroll = maxYScroll }
|
||||
if (yScroll < 0) { yScroll = 0 }
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
@ -111,42 +107,30 @@ func writeString(x, y int, str string) {
|
|||
screen.SetContent(x, y, c, nil, defaultColours)
|
||||
x++
|
||||
}
|
||||
screen.Sync()
|
||||
screen.Show()
|
||||
}
|
||||
|
||||
func renderAll() {
|
||||
renderUI()
|
||||
renderFrame()
|
||||
}
|
||||
|
||||
// Render the tabs and URL bar
|
||||
// TODO: Temporary function, UI rendering should all be moved into this CLI app
|
||||
func renderUI() {
|
||||
return
|
||||
var styling = tcell.StyleDefault
|
||||
var runeChars []rune
|
||||
width, _ := screen.Size()
|
||||
index := 0
|
||||
for y := 0; y < uiHeight ; y++ {
|
||||
for x := 0; x < width; x++ {
|
||||
styling = styling.Foreground(frame.cells[index].fgColour)
|
||||
styling = styling.Background(frame.cells[index].bgColour)
|
||||
runeChars = frame.cells[index].character
|
||||
index++
|
||||
screen.SetCell(x, y, styling, runeChars[0])
|
||||
}
|
||||
}
|
||||
screen.Show()
|
||||
renderCurrentTabWindow()
|
||||
}
|
||||
|
||||
// Tcell uses a buffer to collect screen updates on, it only actually sends
|
||||
// ANSI rendering commands to the terminal when we tell it to. And even then it
|
||||
// will try to minimise rendering commands by only rendering parts of the terminal
|
||||
// that have changed.
|
||||
func renderFrame() {
|
||||
if (len(frame.pixels) == 0 || len(frame.text) == 0) { return }
|
||||
func renderCurrentTabWindow() {
|
||||
if (len(CurrentTab.frame.pixels) == 0 || len(CurrentTab.frame.text) == 0) {
|
||||
Log("Not rendering frame without complimentary pixels and text:")
|
||||
Log(
|
||||
fmt.Sprintf(
|
||||
"pixels: %d, text: %d",
|
||||
len(CurrentTab.frame.pixels), len(CurrentTab.frame.text)))
|
||||
return
|
||||
}
|
||||
var styling = tcell.StyleDefault
|
||||
var runeChars []rune
|
||||
frame := &CurrentTab.frame
|
||||
width, height := screen.Size()
|
||||
if (frame.width == 0 || frame.height == 0) {
|
||||
Log("Not rendering frame with a zero dimension")
|
||||
|
@ -155,38 +139,24 @@ func renderFrame() {
|
|||
index := 0
|
||||
for y := 0; y < height - uiHeight; y++ {
|
||||
for x := 0; x < width; x++ {
|
||||
index = ((y + yScroll) * frame.width) + ((x + xScroll))
|
||||
if (!checkCell(index, x + xScroll, y + yScroll)) { return }
|
||||
index = ((y + frame.yScroll) * frame.width) + ((x + frame.xScroll))
|
||||
if (!checkCell(index, x + frame.xScroll, y + frame.yScroll)) { return }
|
||||
styling = styling.Foreground(frame.cells[index].fgColour)
|
||||
styling = styling.Background(frame.cells[index].bgColour)
|
||||
runeChars = frame.cells[index].character
|
||||
if (len(runeChars) == 0) { continue } // TODO: shouldn't need this
|
||||
// TODO: do this is in isCharacterTransparent()
|
||||
if (len(runeChars) == 0) { continue }
|
||||
screen.SetCell(x, y + uiHeight, styling, runeChars[0])
|
||||
}
|
||||
}
|
||||
overlayPageStatusMessage(height)
|
||||
screen.Show()
|
||||
}
|
||||
|
||||
func overlayPageStatusMessage(height int) {
|
||||
message := State["page_status_message"]
|
||||
x := 0
|
||||
fg := tcell.NewHexColor(int32(0xffffff))
|
||||
bg := tcell.NewHexColor(int32(0x000000))
|
||||
style := tcell.StyleDefault
|
||||
style.Foreground(fg)
|
||||
style.Foreground(bg)
|
||||
for _, c := range message {
|
||||
screen.SetCell(x, height - 1, style, c)
|
||||
x++
|
||||
}
|
||||
}
|
||||
|
||||
func checkCell(index, x, y int) bool {
|
||||
if (index >= len(frame.cells)) {
|
||||
if (index >= len(CurrentTab.frame.cells)) {
|
||||
message := fmt.Sprintf(
|
||||
"Blank frame data (size: %d) at %dx%d, index: %d",
|
||||
len(frame.cells), x, y, index)
|
||||
len(CurrentTab.frame.cells), x, y, index)
|
||||
Log(message)
|
||||
return false;
|
||||
}
|
||||
|
|
46
interfacer/src/browsh/ui.go
Normal file
46
interfacer/src/browsh/ui.go
Normal file
|
@ -0,0 +1,46 @@
|
|||
package browsh
|
||||
|
||||
import (
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Render tabs, URL bar, status messages, etc
|
||||
func renderUI() {
|
||||
renderTabs()
|
||||
renderURLBar()
|
||||
overlayPageStatusMessage()
|
||||
}
|
||||
|
||||
func fillLineToEnd(x, y int) {
|
||||
width, _ := screen.Size()
|
||||
for i := x; i < width - 1; i++ {
|
||||
writeString(i, y, " ")
|
||||
}
|
||||
}
|
||||
|
||||
func renderTabs() {
|
||||
count := 0
|
||||
xPosition := 0
|
||||
tabTitleLength := 15
|
||||
for _, tab := range tabs {
|
||||
if (tab.frame.text == nil) { continue } // TODO: this shouldn't be needed
|
||||
tabTitle := []rune(tab.Title)
|
||||
tabTitleContent := string(tabTitle[0:tabTitleLength]) + " |x "
|
||||
writeString(xPosition, 0, tabTitleContent)
|
||||
count++
|
||||
xPosition = (count * tabTitleLength) + 4
|
||||
}
|
||||
fillLineToEnd(xPosition, 0)
|
||||
}
|
||||
|
||||
func renderURLBar() {
|
||||
content := " ← | x | " + CurrentTab.URI
|
||||
writeString(0, 1, content)
|
||||
fillLineToEnd(utf8.RuneCountInString(content) - 1, 0)
|
||||
}
|
||||
|
||||
func overlayPageStatusMessage() {
|
||||
_, height := screen.Size()
|
||||
writeString(0, height - 1, CurrentTab.StatusMessage)
|
||||
}
|
||||
|
|
@ -89,7 +89,7 @@ func WaitForText(text string, x, y int) {
|
|||
func WaitForPageLoad() {
|
||||
start := time.Now()
|
||||
for time.Since(start) < perTestTimeout {
|
||||
if browsh.State["page_state"] == "parsing_complete" {
|
||||
if browsh.CurrentTab.PageState == "parsing_complete" {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -13,8 +13,11 @@ export default (MixinBase) => class extends MixinBase {
|
|||
}
|
||||
|
||||
sendState() {
|
||||
const state = _.mapValues(this.state, (v) => { return v.toString() });
|
||||
this.sendToTerminal(`/state,${JSON.stringify(state)}`);
|
||||
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) {
|
||||
|
|
|
@ -110,6 +110,7 @@ export default class extends utils.mixins(CommonMixin, TTYCommandsMixin, TabComm
|
|||
// 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.onMessage.addListener(this.handleTabMessage.bind(this));
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import charWidthInTTY from 'string-width';
|
||||
|
||||
import utils from 'utils';
|
||||
|
||||
// Handle commands from tabs, like sending a frame or information about
|
||||
|
@ -17,21 +15,21 @@ export default (MixinBase) => class extends MixinBase {
|
|||
this.sendToTerminal(`/frame_pixels,${message.slice(14)}`);
|
||||
break;
|
||||
case '/tab_info':
|
||||
this.currentTab().info = JSON.parse(utils.rebuildArgsToSingleArg(parts));
|
||||
incoming = JSON.parse(utils.rebuildArgsToSingleArg(parts));
|
||||
this.currentTab().title = incoming.title
|
||||
this.currentTab().url = incoming.url
|
||||
this.sendState();
|
||||
break;
|
||||
case '/dimensions':
|
||||
incoming = JSON.parse(message.slice(12));
|
||||
this._mightResizeWindow(incoming);
|
||||
this.dimensions = incoming;
|
||||
this._sendFrameSize();
|
||||
break;
|
||||
case '/status':
|
||||
if (this._current_frame) {
|
||||
this.updateStatus(parts[1]);
|
||||
this.sendState();
|
||||
}
|
||||
this.updateStatus(parts[1]);
|
||||
this.sendState();
|
||||
break;
|
||||
case `/log`:
|
||||
case '/log':
|
||||
this.log(message.slice(5));
|
||||
break;
|
||||
default:
|
||||
|
@ -47,18 +45,11 @@ export default (MixinBase) => class extends MixinBase {
|
|||
}
|
||||
}
|
||||
|
||||
_sendFrameSize() {
|
||||
this.state['frame_width'] = this.dimensions.frame.width;
|
||||
this.state['frame_height'] = this.dimensions.frame.height;
|
||||
this.sendState();
|
||||
}
|
||||
|
||||
updateStatus(status, message = '') {
|
||||
if (typeof this._current_frame === 'undefined') return;
|
||||
let status_message;
|
||||
switch (status) {
|
||||
case 'page_init':
|
||||
status_message = `Loading ${this.currentTab().info.url}`;
|
||||
status_message = `Loading ${this.currentTab().url}`;
|
||||
break;
|
||||
case 'parsing_complete':
|
||||
status_message = '';
|
||||
|
@ -70,52 +61,7 @@ export default (MixinBase) => class extends MixinBase {
|
|||
if (message != '') status_message = message;
|
||||
}
|
||||
this.state['page_state'] = status;
|
||||
this.state['page_status_message'] = status_message;
|
||||
this.state['status_message'] = status_message;
|
||||
this.sendState();
|
||||
}
|
||||
|
||||
_applyUI() {
|
||||
const tabs = this._buildTTYRow(this._buildTabs());
|
||||
const urlBar = this._buildURLBar();
|
||||
this._current_frame = tabs.concat(urlBar).concat(this._current_frame);
|
||||
}
|
||||
|
||||
_buildTabs() {
|
||||
return this.currentTab().info.title.trim();
|
||||
}
|
||||
|
||||
_buildTTYRow(text) {
|
||||
let char, char_width;
|
||||
let row = [];
|
||||
for (let index = 0; index < this.tty_width; index++) {
|
||||
if (index < text.length) {
|
||||
char = text[index];
|
||||
} else {
|
||||
char = " "
|
||||
}
|
||||
char_width = charWidthInTTY(char);
|
||||
if (char_width === 0) {
|
||||
char = " ";
|
||||
}
|
||||
if (char_width > 1) {
|
||||
index += char_width - 1;
|
||||
}
|
||||
row = row.concat(utils.ttyPlainCell(char));
|
||||
for (var padding = 0; padding < char_width - 1; padding++) {
|
||||
row = row.concat(utils.ttyPlainCell(" "));
|
||||
}
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
_buildURLBar() {
|
||||
let content;
|
||||
if (this.isURLBarFocused) {
|
||||
content = this.urlBarUserContent;
|
||||
} else {
|
||||
content = this.currentTab().info.url;
|
||||
}
|
||||
content = ' ← | x | ' + content;
|
||||
return this._buildTTYRow(content);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -5,8 +5,6 @@ import utils from 'utils';
|
|||
export default (MixinBase) => class extends MixinBase {
|
||||
constructor() {
|
||||
super();
|
||||
this.isURLBarFocused = false;
|
||||
this.urlBarUserContent = "";
|
||||
}
|
||||
|
||||
handleTerminalMessage(message) {
|
||||
|
@ -28,24 +26,14 @@ export default (MixinBase) => class extends MixinBase {
|
|||
this.sendToCurrentTab('/request_frame')
|
||||
}, 250);
|
||||
break;
|
||||
case '/status':
|
||||
this.updateStatus('', parts.slice(1).join(','));
|
||||
case '/url_bar':
|
||||
this._handleURLBarInput(parts.slice(1).join(','));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_handleUICommand(parts) {
|
||||
const input = JSON.parse(utils.rebuildArgsToSingleArg(parts));
|
||||
if (this.isURLBarFocused) {
|
||||
this._handleURLBarInput(input);
|
||||
return true;
|
||||
}
|
||||
switch(input.key) {
|
||||
case 12: // CTRL+L
|
||||
this.isURLBarFocused = true;
|
||||
this.urlBarUserContent = "";
|
||||
return true;
|
||||
}
|
||||
if (input.mod === 4) {
|
||||
switch(input.char) {
|
||||
case 'P':
|
||||
|
@ -62,9 +50,6 @@ export default (MixinBase) => class extends MixinBase {
|
|||
case x > 3 && x < 6:
|
||||
this.sendToCurrentTab('/window_stop');
|
||||
break;
|
||||
default:
|
||||
this.urlBarUserContent = "";
|
||||
this.isURLBarFocused = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
@ -72,30 +57,13 @@ export default (MixinBase) => class extends MixinBase {
|
|||
}
|
||||
|
||||
_handleURLBarInput(input) {
|
||||
let char = input.char;
|
||||
switch (input.key) {
|
||||
case 12: // CTRL+L
|
||||
this.isURLBarFocused = false;
|
||||
return;
|
||||
case 13: // enter
|
||||
this.sendToCurrentTab(`/url,${this._getURLfromUserInput()}`);
|
||||
this.isURLBarFocused = false;
|
||||
return;
|
||||
case 32: // spacebar
|
||||
char = " ";
|
||||
break;
|
||||
case 127: // backspace
|
||||
this.urlBarUserContent = this.urlBarUserContent.slice(0, -1);
|
||||
return;
|
||||
}
|
||||
if (typeof char === 'undefined') return;
|
||||
this.urlBarUserContent += char;
|
||||
const final_url = this._getURLfromUserInput(input);
|
||||
this.sendToCurrentTab(`/url,${final_url}`);
|
||||
}
|
||||
|
||||
_getURLfromUserInput() {
|
||||
_getURLfromUserInput(input) {
|
||||
let url;
|
||||
const search_engine = 'https://www.google.com/search?q=';
|
||||
let input = this.urlBarUserContent;
|
||||
// Basically just check to see if there is text either side of a dot
|
||||
const is_straddled_dot = RegExp(/^[^\s]+\.[^\s]+/);
|
||||
// More comprehensive URL pattern
|
||||
|
|
|
@ -18,7 +18,9 @@ export default class extends utils.mixins(CommonMixin) {
|
|||
sendFrame() {
|
||||
this.getScaledScreenshot();
|
||||
this._serialiseFrame();
|
||||
if (this.frame.length > 0) {
|
||||
this.frame.width = this.dimensions.frame.width;
|
||||
this.frame.height = this.dimensions.frame.height;
|
||||
if (this.frame.colours.length > 0) {
|
||||
this.sendMessage(`/frame_pixels,${JSON.stringify(this.frame)}`);
|
||||
} else {
|
||||
this.log("Not sending empty pixels frame");
|
||||
|
@ -164,12 +166,16 @@ export default class extends utils.mixins(CommonMixin) {
|
|||
}
|
||||
|
||||
_serialiseFrame() {
|
||||
this.frame = [];
|
||||
this.frame = {
|
||||
id: parseInt(this.channel.name),
|
||||
colours: []
|
||||
};
|
||||
const height = this.dimensions.frame.height;
|
||||
const width = this.dimensions.frame.width;
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
this.getScaledPixelAt(x, y).map((c) => this.frame.push(c.toString()));
|
||||
// TODO: Explore sending as binary data
|
||||
this.getScaledPixelAt(x, y).map((c) => this.frame.colours.push(c));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ export default class extends utils.mixins(CommonMixin) {
|
|||
this.dimensions = dimensions;
|
||||
this.graphics_builder = graphics_builder;
|
||||
this.tty_grid = new TTYGrid(dimensions, graphics_builder);
|
||||
this.frame = [];
|
||||
this._parse_started_elements = [];
|
||||
// A `range` is the DOM's representation of elements and nodes as they are rendered in
|
||||
// the DOM. Think of the 'range' that is created when you select/highlight text for
|
||||
|
@ -25,7 +24,9 @@ export default class extends utils.mixins(CommonMixin) {
|
|||
sendFrame() {
|
||||
this.buildFormattedText();
|
||||
this._serialiseFrame();
|
||||
if (this.frame.length > 0) {
|
||||
this.frame.width = this.dimensions.frame.width;
|
||||
this.frame.height = this.dimensions.frame.height;
|
||||
if (this.frame.text.length > 0) {
|
||||
this.sendMessage(`/frame_text,${JSON.stringify(this.frame)}`);
|
||||
} else {
|
||||
this.log("Not sending empty text frame");
|
||||
|
@ -297,7 +298,11 @@ export default class extends utils.mixins(CommonMixin) {
|
|||
|
||||
__serialiseFrame() {
|
||||
let cell, index;
|
||||
this.frame = [];
|
||||
this.frame = {
|
||||
id: parseInt(this.channel.name),
|
||||
text: [],
|
||||
colours: []
|
||||
};
|
||||
const height = this.dimensions.frame.height / 2;
|
||||
const width = this.dimensions.frame.width;
|
||||
for (let y = 0; y < height; y++) {
|
||||
|
@ -305,13 +310,13 @@ export default class extends utils.mixins(CommonMixin) {
|
|||
index = (y * width) + x;
|
||||
cell = this.tty_grid.cells[index];
|
||||
if (cell === undefined) {
|
||||
this.frame.push("0")
|
||||
this.frame.push("0")
|
||||
this.frame.push("0")
|
||||
this.frame.push("")
|
||||
this.frame.colours.push(0)
|
||||
this.frame.colours.push(0)
|
||||
this.frame.colours.push(0)
|
||||
this.frame.text.push("")
|
||||
} else {
|
||||
cell.fg_colour.map((c) => this.frame.push(c.toString()));
|
||||
this.frame.push(cell.rune);
|
||||
cell.fg_colour.map((c) => this.frame.colours.push(c));
|
||||
this.frame.text.push(cell.rune);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ let graphics_builder;
|
|||
|
||||
function setup() {
|
||||
let dimensions = new Dimensions();
|
||||
graphics_builder = new GraphicsBuilder(undefined, dimensions);
|
||||
graphics_builder = new GraphicsBuilder({name: "1"}, dimensions);
|
||||
graphics_builder.getScaledScreenshot();
|
||||
}
|
||||
|
||||
|
@ -22,10 +22,11 @@ describe('Graphics Builder', () => {
|
|||
|
||||
it('should serialise a scaled frame', () => {
|
||||
graphics_builder._serialiseFrame();
|
||||
expect(graphics_builder.frame.length).to.equal(36);
|
||||
expect(graphics_builder.frame[0]).to.equal('111');
|
||||
expect(graphics_builder.frame[3]).to.equal('0');
|
||||
expect(graphics_builder.frame[32]).to.equal('111');
|
||||
expect(graphics_builder.frame[35]).to.equal('0');
|
||||
const colours = graphics_builder.frame.colours
|
||||
expect(colours.length).to.equal(36);
|
||||
expect(colours[0]).to.equal(111);
|
||||
expect(colours[3]).to.equal(0);
|
||||
expect(colours[32]).to.equal(111);
|
||||
expect(colours[35]).to.equal(0);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,11 +13,12 @@ import {
|
|||
} from 'fixtures/canvas_pixels';
|
||||
|
||||
let graphics_builder, text_builder;
|
||||
let channel = {name: 1};
|
||||
|
||||
function setup() {
|
||||
let dimensions = new Dimensions();
|
||||
graphics_builder = new GraphicsBuilder(undefined, dimensions);
|
||||
text_builder = new TextBuilder(undefined, dimensions, graphics_builder);
|
||||
graphics_builder = new GraphicsBuilder(channel, dimensions);
|
||||
text_builder = new TextBuilder(channel, dimensions, graphics_builder);
|
||||
graphics_builder.getScreenshotWithText();
|
||||
graphics_builder.getScreenshotWithoutText();
|
||||
graphics_builder.getScaledScreenshot();
|
||||
|
@ -68,12 +69,15 @@ describe('Text Builder', () => {
|
|||
|
||||
it('should serialise a frame', () => {
|
||||
text_builder._serialiseFrame();
|
||||
expect(text_builder.frame).to.deep.equal([
|
||||
'255', '255', '255', 't',
|
||||
'255', '255', '255', 'e',
|
||||
'255', '255', '255', 's',
|
||||
'255', '255', '255', 'n',
|
||||
'0', '0', '0', '', '0', '0', '0', ''
|
||||
expect(text_builder.frame.colours).to.deep.equal([
|
||||
255, 255, 255,
|
||||
255, 255, 255,
|
||||
255, 255, 255,
|
||||
255, 255, 255,
|
||||
0, 0, 0, 0, 0, 0
|
||||
]);
|
||||
expect(text_builder.frame.text).to.deep.equal([
|
||||
"t", "e", "s", "n", "", ""
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue