// 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") }