browsh/interfacer/src/browsh/tty.go

295 lines
7.2 KiB
Go

package browsh
import (
"encoding/json"
"fmt"
"os"
"strconv"
"github.com/gdamore/tcell"
"github.com/go-errors/errors"
"github.com/spf13/viper"
)
var (
screen tcell.Screen
uiHeight = 2
// IsMonochromeMode decides whether to render the TTY in full colour or monochrome
IsMonochromeMode = false
errNormalExit = errors.New("normal")
)
func setupTcell() {
var err error
if err = screen.Init(); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
IsMonochromeMode = viper.GetBool("monochrome")
screen.EnableMouse()
screen.Clear()
}
func sendTtySize() {
width, height := screen.Size()
urlInputBox.Width = width
sendMessageToWebExtension(fmt.Sprintf("/tty_size,%d,%d", width, height))
}
// This is basically a proxy that listens to STDIN and forwards all relevant input
// from the user to the webextension. So keyboard, mouse, terminal resizes, etc.
func readStdin() {
for {
ev := screen.PollEvent()
switch ev := ev.(type) {
case *tcell.EventKey:
handleUserKeyPress(ev)
case *tcell.EventResize:
handleTTYResize()
case *tcell.EventMouse:
handleMouseEvent(ev)
}
}
}
func handleUserKeyPress(ev *tcell.EventKey) {
if CurrentTab == nil {
if ev.Key() == tcell.KeyCtrlQ {
quitBrowsh()
}
return
}
switch ev.Key() {
case tcell.KeyCtrlQ:
quitBrowsh()
case tcell.KeyCtrlL:
urlBarFocusToggle()
case tcell.KeyCtrlT:
createNewEmptyTab()
case tcell.KeyCtrlU:
if !isNewEmptyTabActive() {
sendMessageToWebExtension("/new_tab,view-source:" + CurrentTab.URI)
}
case tcell.KeyCtrlW:
removeTab(CurrentTab.ID)
case tcell.KeyBackspace, tcell.KeyBackspace2:
if activeInputBox == nil {
sendMessageToWebExtension("/tab_command,/history_back")
}
}
if ev.Rune() == 'm' && ev.Modifiers() == 4 {
toggleMonochromeMode()
}
if ev.Key() == 279 && ev.Modifiers() == 0 {
// F1 key
openHelpTab()
}
if isKey("tty.keys.next-tab", ev) {
nextTab()
}
if !urlInputBox.isActive {
forwardKeyPress(ev)
}
if activeInputBox != nil {
handleInputBoxInput(ev)
} else {
handleScrolling(ev) // TODO: shouldn't you be able to still use mouse scrolling?
}
}
func isKey(userKey string, ev *tcell.EventKey) bool {
key := viper.GetStringSlice(userKey)
runeMatch := []rune(key[0])[0] == ev.Rune()
intKey, _ := strconv.Atoi(key[1])
keyCodeMatch := intKey == int(ev.Key())
modifierKey, _ := strconv.Atoi(key[2])
modifierMatch := modifierKey == int(ev.Modifiers())
return runeMatch && keyCodeMatch && modifierMatch
}
func quitBrowsh() {
if !viper.GetBool("firefox.use-existing") {
quitFirefox()
}
Shutdown(errNormalExit)
}
func toggleMonochromeMode() {
IsMonochromeMode = !IsMonochromeMode
}
func openHelpTab() {
sendMessageToWebExtension("/new_tab,https://www.brow.sh/docs/introduction/")
}
func forwardKeyPress(ev *tcell.EventKey) {
if isMultiLineEnter(ev) {
return
}
eventMap := map[string]interface{}{
"key": int(ev.Key()),
"char": string(ev.Rune()),
"mod": int(ev.Modifiers()),
}
marshalled, _ := json.Marshal(eventMap)
sendMessageToWebExtension("/stdin," + string(marshalled))
}
// Allow user to use ENTER key without triggering submission on multiline input
// boxes.
func isMultiLineEnter(ev *tcell.EventKey) bool {
if activeInputBox == nil {
return false
}
return activeInputBox.isMultiLine() && ev.Key() == 13 && ev.Modifiers() != 4
}
func handleScrolling(ev *tcell.EventKey) {
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.limitScroll(height)
sendMessageToWebExtension(
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) {
if CurrentTab == nil {
return
}
x, y := ev.Position()
xInFrame := x + CurrentTab.frame.xScroll
yInFrame := y - uiHeight + CurrentTab.frame.yScroll
button := ev.Buttons()
if button == tcell.WheelUp || button == tcell.WheelDown {
handleMouseScroll(button)
}
if button == 1 {
CurrentTab.frame.maybeFocusInputBox(xInFrame, yInFrame)
}
eventMap := map[string]interface{}{
"button": int(button),
"mouse_x": int(xInFrame),
"mouse_y": int(yInFrame),
"modifiers": int(ev.Modifiers()),
}
marshalled, _ := json.Marshal(eventMap)
sendMessageToWebExtension("/stdin," + string(marshalled))
}
func handleMouseScroll(scrollType tcell.ButtonMask) {
yScrollOriginal := CurrentTab.frame.yScroll
_, height := screen.Size()
height -= uiHeight
if scrollType == tcell.WheelUp {
CurrentTab.frame.yScroll -= 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()
}
}
func handleTTYResize() {
width, _ := screen.Size()
urlInputBox.Width = width
screen.Sync()
sendTtySize()
}
// 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 renderCurrentTabWindow() {
var currentCell cell
styling := tcell.StyleDefault
var runeChars []rune
width, height := screen.Size()
if CurrentTab == nil || CurrentTab.frame.cells == nil {
return
}
CurrentTab.frame.overlayInputBoxContent()
for y := 0; y < height-uiHeight; y++ {
for x := 0; x < width; x++ {
currentCell = getCell(x, y)
runeChars = currentCell.character
// TODO: do this is in isCharacterTransparent()
if len(runeChars) == 0 {
continue
}
if IsMonochromeMode {
styling = styling.Foreground(tcell.ColorWhite)
styling = styling.Background(tcell.ColorBlack)
if runeChars[0] == '▄' {
runeChars[0] = ' '
}
} else {
styling = styling.Foreground(currentCell.fgColour)
styling = styling.Background(currentCell.bgColour)
}
screen.SetCell(x, y+uiHeight, styling, runeChars[0])
}
}
if activeInputBox != nil {
activeInputBox.renderCursor()
}
overlayPageStatusMessage()
overlayCallToSupport()
screen.Show()
}
func getCell(x, y int) cell {
var currentCell cell
var ok bool
frame := &CurrentTab.frame
index := ((y + frame.yScroll) * frame.totalWidth) + (x + frame.xScroll)
if currentCell, ok = frame.cells.load(index); !ok {
fgColour, bgColour := getHatchedCellColours(x)
currentCell = cell{
fgColour: fgColour,
bgColour: bgColour,
character: []rune("▄"),
}
}
return currentCell
}
func getHatchedCellColours(x int) (tcell.Color, tcell.Color) {
var bgColour, fgColour tcell.Color
if x%2 == 0 {
bgColour = tcell.NewHexColor(0xa9a9a9)
fgColour = tcell.NewHexColor(0x797979)
} else {
bgColour = tcell.NewHexColor(0x797979)
fgColour = tcell.NewHexColor(0xa9a9a9)
}
return fgColour, bgColour
}