@ 261105.1

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

wscreen.go

@@ -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", "" }