260426.4 · 5d0b84b1b465a0abd6a27e893d37d5f68f8e1836
feat(wasm): Introduce ghostty-wasm as backing WASM terminal (fixes #700) (fixes #670) (fixes #698)
This gets us all the goodness (or most of it) of ghostty, and fixes a few outstanding problems. This works like xterm.js, just better.
reading declarations
Changes to wscreen.go. All files
@@ -19,625 +19,206 @@ package tcell import ( "errors"- "fmt"+ "io" "sync" "syscall/js"- "unicode/utf8" "github.com/gdamore/tcell/v3/tty" ) -// NewTerminfoScreen gets a screen. Most options are ignored for this platform,-// but shared CellBuffer options such as content sanitization are honored.-func NewTerminfoScreen(opts ...TerminfoScreenOption) (Screen, error) {- t := &wScreen{}- t.fallback = make(map[rune]string)- for _, opt := range opts {- opt.apply(t)+// initialize installs the browser-backed TTY used by tScreen on js/wasm.+func (t *tScreen) initialize() error {+ if t.tty == nil {+ t.tty = newBrowserTty()+ }+ if t.term == "" {+ t.term = "ghostty-truecolor" }-- return &baseScreen{screenImpl: t}, nil-}--func NewTerminfoScreenFromTty(_ tty.Tty, _ ...TerminfoScreenOption) (Screen, error) {- // TODO: When we want to support testing webasm, we'll have to change this- // to use a MockTerm. That will be appropriate when we switch to xterm.js.- // The options are ignored for this platform.- return nil, errors.New("not implemented")-}--type TerminfoScreenOption interface{ apply(*wScreen) }-type OptColors int-type OptTerm string-type OptAltScreen bool-type OptSanitizeContent bool-type OptAdvancedKeys bool--func (OptColors) apply(*wScreen) {}-func (OptTerm) apply(*wScreen) {}-func (OptAltScreen) apply(*wScreen) {}-func (OptAdvancedKeys) apply(*wScreen) {}-func (o OptSanitizeContent) apply(w *wScreen) {- w.cells.sanitizeContent = bool(o)-}--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()+func getCharset() string {+ return "UTF-8" } -// paletteColor gives a more natural palette color actually matching-// typical XTerm. We might in the future want to permit styling these-// via CSS.+type browserTty struct {+ mu sync.Mutex+ cond *sync.Cond+ started bool+ drained bool+ closed bool+ input []byte+ resizeQ chan<- bool -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()+ writeFunc js.Value+ sizeFunc js.Value+ closeFuncs []js.Func } -func (t *wScreen) drawCell(x, y int) int {- str, style, width := t.cells.Get(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- }-- t.cells.SetDirty(x, y, false)- js.Global().Call("drawCell", x, y, str, fg, bg, int(style.attrs), int(us), int(uc))-- return width+func newBrowserTty() *browserTty {+ t := &browserTty{}+ t.cond = sync.NewCond(&t.mu)+ return t } -func (t *wScreen) ShowCursor(x, y int) {- t.Lock()- js.Global().Call("showCursor", x, y)- t.Unlock()-}+func (t *browserTty) Start() error {+ t.mu.Lock()+ defer t.mu.Unlock() -func (t *wScreen) SetCursor(cs CursorStyle, cc Color) {- if !cc.Valid() {- cc = ColorLightGray+ if t.started {+ return nil }- 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()+ global := js.Global()+ t.writeFunc = global.Get("tcellWrite")+ t.sizeFunc = global.Get("tcellWindowSize")+ if t.writeFunc.Type() != js.TypeFunction || t.sizeFunc.Type() != js.TypeFunction {+ return errors.New("tcell wasm terminal host is not installed") } - for y := 0; y < t.h; y++ {- for x := 0; x < t.w; x++ {- width := t.drawCell(x, y)- x += width - 1+ onData := js.FuncOf(func(this js.Value, args []js.Value) any {+ if len(args) == 0 {+ return nil }- }-- 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()-}+ if args[0].InstanceOf(global.Get("Uint8Array")) {+ data := make([]byte, args[0].Get("byteLength").Int())+ js.CopyBytesToGo(data, args[0])+ t.enqueue(data)+ } else {+ t.enqueue([]byte(args[0].String()))+ }+ return nil+ })+ onResize := js.FuncOf(func(this js.Value, args []js.Value) any {+ t.mu.Lock()+ resizeQ := t.resizeQ+ t.mu.Unlock()+ if resizeQ != nil {+ select {+ case resizeQ <- true:+ default:+ }+ }+ return nil+ })+ t.closeFuncs = []js.Func{onData, onResize}+ global.Set("tcellRead", onData)+ global.Set("tcellResize", onResize) -func (t *wScreen) EnablePaste() {- t.Lock()- t.pasteEnabled = true- t.enablePasting(true)- t.Unlock()+ t.started = true+ t.drained = false+ t.closed = false+ return nil } -func (t *wScreen) DisablePaste() {- t.Lock()- t.pasteEnabled = false- t.enablePasting(false)- t.Unlock()-}+func (t *browserTty) Stop() error {+ t.mu.Lock()+ t.started = false+ t.drained = false+ funcs := t.closeFuncs+ t.closeFuncs = nil+ t.cond.Broadcast()+ t.mu.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))+ js.Global().Set("tcellRead", js.Undefined())+ js.Global().Set("tcellResize", js.Undefined())+ for _, fn := range funcs {+ fn.Release() }+ return nil } -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 (s *wScreen) GetClipboard() {-}--func (s *wScreen) SetClipboard(_ []byte) {-}--func (s *wScreen) HasClipboard() bool {- return false-}--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 *browserTty) Drain() error {+ t.mu.Lock()+ t.input = nil+ t.drained = true+ t.cond.Broadcast()+ t.mu.Unlock()+ return nil } -func (t *wScreen) postEvent(ev Event) {- select {- case t.evch <- ev:- case <-t.quit:- }+func (t *browserTty) NotifyResize(resizeQ chan<- bool) {+ t.mu.Lock()+ t.resizeQ = resizeQ+ t.mu.Unlock() } -func (t *wScreen) onMouseEvent(this js.Value, args []js.Value) any {- 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+func (t *browserTty) WindowSize() (tty.WindowSize, error) {+ var ws tty.WindowSize+ t.mu.Lock()+ sizeFunc := t.sizeFunc+ t.mu.Unlock()+ if sizeFunc.Type() != js.TypeFunction {+ ws.Width = 80+ ws.Height = 24+ return ws, nil }-- if args[4].Bool() { // mod alt- mod |= ModAlt+ size := sizeFunc.Invoke()+ ws.Width = size.Get("cols").Int()+ ws.Height = size.Get("rows").Int()+ ws.PixelWidth = size.Get("pixelWidth").Int()+ ws.PixelHeight = size.Get("pixelHeight").Int()+ if ws.Width == 0 {+ ws.Width = 80 }-- if args[5].Bool() { // mod ctrl- mod |= ModCtrl+ if ws.Height == 0 {+ ws.Height = 24 }-- t.postEvent(NewEventMouse(args[0].Int(), args[1].Int(), button, mod))- return nil+ return ws, nil } -func (t *wScreen) onKeyEvent(this js.Value, args []js.Value) any {- 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- }+func (t *browserTty) Read(b []byte) (int, error) {+ t.mu.Lock()+ defer t.mu.Unlock() - if args[2].Bool() { // mod alt- mod |= ModAlt+ for len(t.input) == 0 && t.started && !t.drained && !t.closed {+ t.cond.Wait() }-- if args[3].Bool() { // mod ctrl- mod |= ModCtrl+ if t.closed {+ return 0, io.EOF }-- if args[4].Bool() { // mod meta- mod |= ModMeta- }-- // next try function keys- if k, ok := WebKeyNames[key]; ok {- t.postEvent(NewEventKey(k, "", mod))- return nil+ if (!t.started || t.drained) && len(t.input) == 0 {+ return 0, io.EOF }-- // finally try normal, printable chars- r, _ := utf8.DecodeRuneInString(key)- t.postEvent(NewEventKey(KeyRune, string(r), mod))- return nil-}--func (t *wScreen) onPaste(this js.Value, args []js.Value) any {- t.postEvent(NewEventPaste(args[0].Bool()))- return nil-}--func (t *wScreen) onFocus(this js.Value, args []js.Value) any {- 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) any {- 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()+ n := copy(b, t.input)+ t.input = t.input[n:]+ return n, nil } -func (t *wScreen) SetSize(w, h int) {- if w == t.w && h == t.h {- return+func (t *browserTty) Write(b []byte) (int, error) {+ t.mu.Lock()+ writeFunc := t.writeFunc+ started := t.started+ t.mu.Unlock()+ if !started || writeFunc.Type() != js.TypeFunction {+ return 0, io.ErrClosedPipe } - 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))+ data := js.Global().Get("Uint8Array").New(len(b))+ js.CopyBytesToJS(data, b)+ writeFunc.Invoke(data)+ return len(b), nil } -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()+func (t *browserTty) Close() error {+ t.mu.Lock()+ if t.closed {+ t.mu.Unlock() return nil }- t.running = false- t.clearScreen()- t.enableMouse(0)- t.enablePasting(false)- js.Global().Set("onKeyEvent", js.FuncOf(t.unset)) // stop key presses- 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)-}+ t.closed = true+ t.started = false+ t.drained = false+ t.cond.Broadcast()+ t.mu.Unlock() -func (*wScreen) ShowNotification(title string, body string) {}--// 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,-}--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",+ return t.Stop() } -func (*wScreen) KeyboardProtocol() KeyProtocol {- return XTermKeyboard // not really but we report keys unambiguously+func (t *browserTty) enqueue(data []byte) {+ t.mu.Lock()+ if t.started && !t.closed {+ t.input = append(t.input, data...)+ t.cond.Broadcast()+ }+ t.mu.Unlock() }--func (*wScreen) Terminal() (string, string) { return "Tcell-WebASM", "" }