tcell/wscreen.go

671 lines
15 KiB
Go

// Copyright 2024 The TCell Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use file except in compliance with the License.
// You may obtain a copy of the license at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build js && wasm
// +build js,wasm
package tcell
import (
"errors"
"fmt"
"strings"
"sync"
"syscall/js"
"unicode/utf8"
"github.com/gdamore/tcell/v2/terminfo"
)
func NewTerminfoScreen() (Screen, error) {
t := &wScreen{}
t.fallback = make(map[rune]string)
return &baseScreen{screenImpl: t}, nil
}
type wScreen struct {
w, h int
style Style
cells CellBuffer
running bool
clear bool
flagsPresent bool
pasteEnabled bool
mouseFlags MouseFlags
cursorStyle CursorStyle
quit chan struct{}
evch chan Event
fallback map[rune]string
finiOnce sync.Once
sync.Mutex
}
func (t *wScreen) Init() error {
t.w, t.h = 80, 24 // default for html as of now
t.evch = make(chan Event, 10)
t.quit = make(chan struct{})
t.Lock()
t.running = true
t.style = StyleDefault
t.cells.Resize(t.w, t.h)
t.Unlock()
js.Global().Set("onKeyEvent", js.FuncOf(t.onKeyEvent))
js.Global().Set("onMouseClick", js.FuncOf(t.unset))
js.Global().Set("onMouseMove", js.FuncOf(t.unset))
js.Global().Set("onFocus", js.FuncOf(t.unset))
return nil
}
func (t *wScreen) Fini() {
t.finiOnce.Do(func() {
close(t.quit)
})
}
func (t *wScreen) SetStyle(style Style) {
t.Lock()
t.style = style
t.Unlock()
}
// paletteColor gives a more natural palette color actually matching
// typical XTerm. We might in the future want to permit styling these
// via CSS.
var palette = map[Color]int32{
ColorBlack: 0x000000,
ColorMaroon: 0xcd0000,
ColorGreen: 0x00cd00,
ColorOlive: 0xcdcd00,
ColorNavy: 0x0000ee,
ColorPurple: 0xcd00cd,
ColorTeal: 0x00cdcd,
ColorSilver: 0xe5e5e5,
ColorGray: 0x7f7f7f,
ColorRed: 0xff0000,
ColorLime: 0x00ff00,
ColorYellow: 0xffff00,
ColorBlue: 0x5c5cff,
ColorFuchsia: 0xff00ff,
ColorAqua: 0x00ffff,
ColorWhite: 0xffffff,
}
func paletteColor(c Color) int32 {
if c.IsRGB() {
return int32(c & 0xffffff)
}
if c >= ColorBlack && c <= ColorWhite {
return palette[c]
}
return c.Hex()
}
func (t *wScreen) drawCell(x, y int) int {
mainc, combc, style, width := t.cells.GetContent(x, y)
if !t.cells.Dirty(x, y) {
return width
}
if style == StyleDefault {
style = t.style
}
fg, bg := paletteColor(style.fg), paletteColor(style.bg)
if fg == -1 {
fg = 0xe5e5e5
}
if bg == -1 {
bg = 0x000000
}
us, uc := style.ulStyle, paletteColor(style.ulColor)
if uc == -1 {
uc = 0x000000
}
s := ""
if len(combc) > 0 {
b := make([]rune, 0, 1 + len(combc))
b = append(b, mainc)
b = append(b, combc...)
s = string(b)
} else {
s = string(mainc)
}
t.cells.SetDirty(x, y, false)
js.Global().Call("drawCell", x, y, s, fg, bg, int(style.attrs), int(us), int(uc))
return width
}
func (t *wScreen) ShowCursor(x, y int) {
t.Lock()
js.Global().Call("showCursor", x, y)
t.Unlock()
}
func (t *wScreen) SetCursor(cs CursorStyle, cc Color) {
if !cc.Valid() {
cc = ColorLightGray
}
t.Lock()
js.Global().Call("setCursorStyle", curStyleClasses[cs], fmt.Sprintf("#%06x", cc.Hex()))
t.Unlock()
}
func (t *wScreen) HideCursor() {
t.ShowCursor(-1, -1)
}
func (t *wScreen) Show() {
t.Lock()
t.resize()
t.draw()
t.Unlock()
}
func (t *wScreen) clearScreen() {
js.Global().Call("clearScreen", t.style.fg.Hex(), t.style.bg.Hex())
t.clear = false
}
func (t *wScreen) draw() {
if t.clear {
t.clearScreen()
}
for y := 0; y < t.h; y++ {
for x := 0; x < t.w; x++ {
width := t.drawCell(x, y)
x += width - 1
}
}
js.Global().Call("show")
}
func (t *wScreen) EnableMouse(flags ...MouseFlags) {
var f MouseFlags
flagsPresent := false
for _, flag := range flags {
f |= flag
flagsPresent = true
}
if !flagsPresent {
f = MouseMotionEvents | MouseDragEvents | MouseButtonEvents
}
t.Lock()
t.mouseFlags = f
t.enableMouse(f)
t.Unlock()
}
func (t *wScreen) enableMouse(f MouseFlags) {
if f&MouseButtonEvents != 0 {
js.Global().Set("onMouseClick", js.FuncOf(t.onMouseEvent))
} else {
js.Global().Set("onMouseClick", js.FuncOf(t.unset))
}
if f&MouseDragEvents != 0 || f&MouseMotionEvents != 0 {
js.Global().Set("onMouseMove", js.FuncOf(t.onMouseEvent))
} else {
js.Global().Set("onMouseMove", js.FuncOf(t.unset))
}
}
func (t *wScreen) DisableMouse() {
t.Lock()
t.mouseFlags = 0
t.enableMouse(0)
t.Unlock()
}
func (t *wScreen) EnablePaste() {
t.Lock()
t.pasteEnabled = true
t.enablePasting(true)
t.Unlock()
}
func (t *wScreen) DisablePaste() {
t.Lock()
t.pasteEnabled = false
t.enablePasting(false)
t.Unlock()
}
func (t *wScreen) enablePasting(on bool) {
if on {
js.Global().Set("onPaste", js.FuncOf(t.onPaste))
} else {
js.Global().Set("onPaste", js.FuncOf(t.unset))
}
}
func (t *wScreen) EnableFocus() {
t.Lock()
js.Global().Set("onFocus", js.FuncOf(t.onFocus))
t.Unlock()
}
func (t *wScreen) DisableFocus() {
t.Lock()
js.Global().Set("onFocus", js.FuncOf(t.unset))
t.Unlock()
}
func (t *wScreen) Size() (int, int) {
t.Lock()
w, h := t.w, t.h
t.Unlock()
return w, h
}
// resize does nothing, as asking the web window to resize
// without a specified width or height will cause no change.
func (t *wScreen) resize() {}
func (t *wScreen) Colors() int {
return 16777216 // 256 ^ 3
}
func (t *wScreen) clip(x, y int) (int, int) {
w, h := t.cells.Size()
if x < 0 {
x = 0
}
if y < 0 {
y = 0
}
if x > w-1 {
x = w - 1
}
if y > h-1 {
y = h - 1
}
return x, y
}
func (t *wScreen) postEvent(ev Event) {
select {
case t.evch <- ev:
case <-t.quit:
}
}
func (t *wScreen) onMouseEvent(this js.Value, args []js.Value) interface{} {
mod := ModNone
button := ButtonNone
switch args[2].Int() {
case 0:
if t.mouseFlags&MouseMotionEvents == 0 {
// don't want this event! is a mouse motion event, but user has asked not.
return nil
}
button = ButtonNone
case 1:
button = Button1
case 2:
button = Button3 // Note we prefer to treat right as button 2
case 3:
button = Button2 // And the middle button as button 3
}
if args[3].Bool() { // mod shift
mod |= ModShift
}
if args[4].Bool() { // mod alt
mod |= ModAlt
}
if args[5].Bool() { // mod ctrl
mod |= ModCtrl
}
t.postEvent(NewEventMouse(args[0].Int(), args[1].Int(), button, mod))
return nil
}
func (t *wScreen) onKeyEvent(this js.Value, args []js.Value) interface{} {
key := args[0].String()
// don't accept any modifier keys as their own
if key == "Control" || key == "Alt" || key == "Meta" || key == "Shift" {
return nil
}
mod := ModNone
if args[1].Bool() { // mod shift
mod |= ModShift
}
if args[2].Bool() { // mod alt
mod |= ModAlt
}
if args[3].Bool() { // mod ctrl
mod |= ModCtrl
}
if args[4].Bool() { // mod meta
mod |= ModMeta
}
// check for special case of Ctrl + key
if mod == ModCtrl {
if k, ok := WebKeyNames["Ctrl-"+strings.ToLower(key)]; ok {
t.postEvent(NewEventKey(k, 0, mod))
return nil
}
}
// next try function keys
if k, ok := WebKeyNames[key]; ok {
t.postEvent(NewEventKey(k, 0, mod))
return nil
}
// finally try normal, printable chars
r, _ := utf8.DecodeRuneInString(key)
t.postEvent(NewEventKey(KeyRune, r, mod))
return nil
}
func (t *wScreen) onPaste(this js.Value, args []js.Value) interface{} {
t.postEvent(NewEventPaste(args[0].Bool()))
return nil
}
func (t *wScreen) onFocus(this js.Value, args []js.Value) interface{} {
t.postEvent(NewEventFocus(args[0].Bool()))
return nil
}
// unset is a dummy function for js when we want nothing to
// happen when javascript calls a function (for example, when
// mouse input is disabled, when onMouseEvent() is called from
// js, it redirects here and does nothing).
func (t *wScreen) unset(this js.Value, args []js.Value) interface{} {
return nil
}
func (t *wScreen) Sync() {
t.Lock()
t.resize()
t.clear = true
t.cells.Invalidate()
t.draw()
t.Unlock()
}
func (t *wScreen) CharacterSet() string {
return "UTF-8"
}
func (t *wScreen) RegisterRuneFallback(orig rune, fallback string) {
t.Lock()
t.fallback[orig] = fallback
t.Unlock()
}
func (t *wScreen) UnregisterRuneFallback(orig rune) {
t.Lock()
delete(t.fallback, orig)
t.Unlock()
}
func (t *wScreen) CanDisplay(r rune, checkFallbacks bool) bool {
if utf8.ValidRune(r) {
return true
}
if !checkFallbacks {
return false
}
if _, ok := t.fallback[r]; ok {
return true
}
return false
}
func (t *wScreen) HasMouse() bool {
return true
}
func (t *wScreen) HasKey(k Key) bool {
return true
}
func (t *wScreen) SetSize(w, h int) {
if w == t.w && h == t.h {
return
}
t.cells.Invalidate()
t.cells.Resize(w, h)
js.Global().Call("resize", w, h)
t.w, t.h = w, h
t.postEvent(NewEventResize(w, h))
}
func (t *wScreen) Resize(int, int, int, int) {}
// Suspend simply pauses all input and output, and clears the screen.
// There isn't a "default terminal" to go back to.
func (t *wScreen) Suspend() error {
t.Lock()
if !t.running {
t.Unlock()
return nil
}
t.running = false
t.clearScreen()
t.enableMouse(0)
t.enablePasting(false)
js.Global().Set("onKeyEvent", js.FuncOf(t.unset)) // stop keypresses
return nil
}
func (t *wScreen) Resume() error {
t.Lock()
if t.running {
return errors.New("already engaged")
}
t.running = true
t.enableMouse(t.mouseFlags)
t.enablePasting(t.pasteEnabled)
js.Global().Set("onKeyEvent", js.FuncOf(t.onKeyEvent))
t.Unlock()
return nil
}
func (t *wScreen) Beep() error {
js.Global().Call("beep")
return nil
}
func (t *wScreen) Tty() (Tty, bool) {
return nil, false
}
func (t *wScreen) GetCells() *CellBuffer {
return &t.cells
}
func (t *wScreen) EventQ() chan Event {
return t.evch
}
func (t *wScreen) StopQ() <-chan struct{} {
return t.quit
}
func (t *wScreen) SetTitle(title string) {
js.Global().Call("setTitle", title)
}
// WebKeyNames maps string names reported from HTML
// (KeyboardEvent.key) to tcell accepted keys.
var WebKeyNames = map[string]Key{
"Enter": KeyEnter,
"Backspace": KeyBackspace,
"Tab": KeyTab,
"Backtab": KeyBacktab,
"Escape": KeyEsc,
"Backspace2": KeyBackspace2,
"Delete": KeyDelete,
"Insert": KeyInsert,
"ArrowUp": KeyUp,
"ArrowDown": KeyDown,
"ArrowLeft": KeyLeft,
"ArrowRight": KeyRight,
"Home": KeyHome,
"End": KeyEnd,
"UpLeft": KeyUpLeft, // not supported by HTML
"UpRight": KeyUpRight, // not supported by HTML
"DownLeft": KeyDownLeft, // not supported by HTML
"DownRight": KeyDownRight, // not supported by HTML
"Center": KeyCenter,
"PgDn": KeyPgDn,
"PgUp": KeyPgUp,
"Clear": KeyClear,
"Exit": KeyExit,
"Cancel": KeyCancel,
"Pause": KeyPause,
"Print": KeyPrint,
"F1": KeyF1,
"F2": KeyF2,
"F3": KeyF3,
"F4": KeyF4,
"F5": KeyF5,
"F6": KeyF6,
"F7": KeyF7,
"F8": KeyF8,
"F9": KeyF9,
"F10": KeyF10,
"F11": KeyF11,
"F12": KeyF12,
"F13": KeyF13,
"F14": KeyF14,
"F15": KeyF15,
"F16": KeyF16,
"F17": KeyF17,
"F18": KeyF18,
"F19": KeyF19,
"F20": KeyF20,
"F21": KeyF21,
"F22": KeyF22,
"F23": KeyF23,
"F24": KeyF24,
"F25": KeyF25,
"F26": KeyF26,
"F27": KeyF27,
"F28": KeyF28,
"F29": KeyF29,
"F30": KeyF30,
"F31": KeyF31,
"F32": KeyF32,
"F33": KeyF33,
"F34": KeyF34,
"F35": KeyF35,
"F36": KeyF36,
"F37": KeyF37,
"F38": KeyF38,
"F39": KeyF39,
"F40": KeyF40,
"F41": KeyF41,
"F42": KeyF42,
"F43": KeyF43,
"F44": KeyF44,
"F45": KeyF45,
"F46": KeyF46,
"F47": KeyF47,
"F48": KeyF48,
"F49": KeyF49,
"F50": KeyF50,
"F51": KeyF51,
"F52": KeyF52,
"F53": KeyF53,
"F54": KeyF54,
"F55": KeyF55,
"F56": KeyF56,
"F57": KeyF57,
"F58": KeyF58,
"F59": KeyF59,
"F60": KeyF60,
"F61": KeyF61,
"F62": KeyF62,
"F63": KeyF63,
"F64": KeyF64,
"Ctrl-a": KeyCtrlA, // not reported by HTML- need to do special check
"Ctrl-b": KeyCtrlB, // not reported by HTML- need to do special check
"Ctrl-c": KeyCtrlC, // not reported by HTML- need to do special check
"Ctrl-d": KeyCtrlD, // not reported by HTML- need to do special check
"Ctrl-e": KeyCtrlE, // not reported by HTML- need to do special check
"Ctrl-f": KeyCtrlF, // not reported by HTML- need to do special check
"Ctrl-g": KeyCtrlG, // not reported by HTML- need to do special check
"Ctrl-j": KeyCtrlJ, // not reported by HTML- need to do special check
"Ctrl-k": KeyCtrlK, // not reported by HTML- need to do special check
"Ctrl-l": KeyCtrlL, // not reported by HTML- need to do special check
"Ctrl-n": KeyCtrlN, // not reported by HTML- need to do special check
"Ctrl-o": KeyCtrlO, // not reported by HTML- need to do special check
"Ctrl-p": KeyCtrlP, // not reported by HTML- need to do special check
"Ctrl-q": KeyCtrlQ, // not reported by HTML- need to do special check
"Ctrl-r": KeyCtrlR, // not reported by HTML- need to do special check
"Ctrl-s": KeyCtrlS, // not reported by HTML- need to do special check
"Ctrl-t": KeyCtrlT, // not reported by HTML- need to do special check
"Ctrl-u": KeyCtrlU, // not reported by HTML- need to do special check
"Ctrl-v": KeyCtrlV, // not reported by HTML- need to do special check
"Ctrl-w": KeyCtrlW, // not reported by HTML- need to do special check
"Ctrl-x": KeyCtrlX, // not reported by HTML- need to do special check
"Ctrl-y": KeyCtrlY, // not reported by HTML- need to do special check
"Ctrl-z": KeyCtrlZ, // not reported by HTML- need to do special check
"Ctrl- ": KeyCtrlSpace, // not reported by HTML- need to do special check
"Ctrl-_": KeyCtrlUnderscore, // not reported by HTML- need to do special check
"Ctrl-]": KeyCtrlRightSq, // not reported by HTML- need to do special check
"Ctrl-\\": KeyCtrlBackslash, // not reported by HTML- need to do special check
"Ctrl-^": KeyCtrlCarat, // not reported by HTML- need to do special check
}
var curStyleClasses = map[CursorStyle]string{
CursorStyleDefault: "cursor-blinking-block",
CursorStyleBlinkingBlock: "cursor-blinking-block",
CursorStyleSteadyBlock: "cursor-steady-block",
CursorStyleBlinkingUnderline: "cursor-blinking-underline",
CursorStyleSteadyUnderline: "cursor-steady-underline",
CursorStyleBlinkingBar: "cursor-blinking-bar",
CursorStyleSteadyBar: "cursor-steady-bar",
}
func LookupTerminfo(name string) (ti *terminfo.Terminfo, e error) {
return nil, errors.New("LookupTermInfo not supported")
}