wscreen.go (15571B)
1 // Copyright 2024 The TCell Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use file except in compliance with the License. 5 // You may obtain a copy of the license at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 //go:build js && wasm 16 // +build js,wasm 17 18 package tcell 19 20 import ( 21 "errors" 22 "fmt" 23 "strings" 24 "sync" 25 "syscall/js" 26 "unicode/utf8" 27 28 "github.com/gdamore/tcell/v2/terminfo" 29 ) 30 31 func NewTerminfoScreen() (Screen, error) { 32 t := &wScreen{} 33 t.fallback = make(map[rune]string) 34 35 return &baseScreen{screenImpl: t}, nil 36 } 37 38 type wScreen struct { 39 w, h int 40 style Style 41 cells CellBuffer 42 43 running bool 44 clear bool 45 flagsPresent bool 46 pasteEnabled bool 47 mouseFlags MouseFlags 48 49 cursorStyle CursorStyle 50 51 quit chan struct{} 52 evch chan Event 53 fallback map[rune]string 54 finiOnce sync.Once 55 56 sync.Mutex 57 } 58 59 func (t *wScreen) Init() error { 60 t.w, t.h = 80, 24 // default for html as of now 61 t.evch = make(chan Event, 10) 62 t.quit = make(chan struct{}) 63 64 t.Lock() 65 t.running = true 66 t.style = StyleDefault 67 t.cells.Resize(t.w, t.h) 68 t.Unlock() 69 70 js.Global().Set("onKeyEvent", js.FuncOf(t.onKeyEvent)) 71 js.Global().Set("onMouseClick", js.FuncOf(t.unset)) 72 js.Global().Set("onMouseMove", js.FuncOf(t.unset)) 73 js.Global().Set("onFocus", js.FuncOf(t.unset)) 74 75 return nil 76 } 77 78 func (t *wScreen) Fini() { 79 t.finiOnce.Do(func() { 80 close(t.quit) 81 }) 82 } 83 84 func (t *wScreen) SetStyle(style Style) { 85 t.Lock() 86 t.style = style 87 t.Unlock() 88 } 89 90 // paletteColor gives a more natural palette color actually matching 91 // typical XTerm. We might in the future want to permit styling these 92 // via CSS. 93 94 var palette = map[Color]int32{ 95 ColorBlack: 0x000000, 96 ColorMaroon: 0xcd0000, 97 ColorGreen: 0x00cd00, 98 ColorOlive: 0xcdcd00, 99 ColorNavy: 0x0000ee, 100 ColorPurple: 0xcd00cd, 101 ColorTeal: 0x00cdcd, 102 ColorSilver: 0xe5e5e5, 103 ColorGray: 0x7f7f7f, 104 ColorRed: 0xff0000, 105 ColorLime: 0x00ff00, 106 ColorYellow: 0xffff00, 107 ColorBlue: 0x5c5cff, 108 ColorFuchsia: 0xff00ff, 109 ColorAqua: 0x00ffff, 110 ColorWhite: 0xffffff, 111 } 112 113 func paletteColor(c Color) int32 { 114 if c.IsRGB() { 115 return int32(c & 0xffffff) 116 } 117 if c >= ColorBlack && c <= ColorWhite { 118 return palette[c] 119 } 120 return c.Hex() 121 } 122 123 func (t *wScreen) drawCell(x, y int) int { 124 mainc, combc, style, width := t.cells.GetContent(x, y) 125 126 if !t.cells.Dirty(x, y) { 127 return width 128 } 129 130 if style == StyleDefault { 131 style = t.style 132 } 133 134 fg, bg := paletteColor(style.fg), paletteColor(style.bg) 135 if fg == -1 { 136 fg = 0xe5e5e5 137 } 138 if bg == -1 { 139 bg = 0x000000 140 } 141 us, uc := style.ulStyle, paletteColor(style.ulColor) 142 if uc == -1 { 143 uc = 0x000000 144 } 145 146 s := "" 147 if len(combc) > 0 { 148 b := make([]rune, 0, 1 + len(combc)) 149 b = append(b, mainc) 150 b = append(b, combc...) 151 s = string(b) 152 } else { 153 s = string(mainc) 154 } 155 156 t.cells.SetDirty(x, y, false) 157 js.Global().Call("drawCell", x, y, s, fg, bg, int(style.attrs), int(us), int(uc)) 158 159 return width 160 } 161 162 func (t *wScreen) ShowCursor(x, y int) { 163 t.Lock() 164 js.Global().Call("showCursor", x, y) 165 t.Unlock() 166 } 167 168 func (t *wScreen) SetCursor(cs CursorStyle, cc Color) { 169 if !cc.Valid() { 170 cc = ColorLightGray 171 } 172 t.Lock() 173 js.Global().Call("setCursorStyle", curStyleClasses[cs], fmt.Sprintf("#%06x", cc.Hex())) 174 t.Unlock() 175 } 176 177 func (t *wScreen) HideCursor() { 178 t.ShowCursor(-1, -1) 179 } 180 181 func (t *wScreen) Show() { 182 t.Lock() 183 t.resize() 184 t.draw() 185 t.Unlock() 186 } 187 188 func (t *wScreen) clearScreen() { 189 js.Global().Call("clearScreen", t.style.fg.Hex(), t.style.bg.Hex()) 190 t.clear = false 191 } 192 193 func (t *wScreen) draw() { 194 if t.clear { 195 t.clearScreen() 196 } 197 198 for y := 0; y < t.h; y++ { 199 for x := 0; x < t.w; x++ { 200 width := t.drawCell(x, y) 201 x += width - 1 202 } 203 } 204 205 js.Global().Call("show") 206 } 207 208 func (t *wScreen) EnableMouse(flags ...MouseFlags) { 209 var f MouseFlags 210 flagsPresent := false 211 for _, flag := range flags { 212 f |= flag 213 flagsPresent = true 214 } 215 if !flagsPresent { 216 f = MouseMotionEvents | MouseDragEvents | MouseButtonEvents 217 } 218 219 t.Lock() 220 t.mouseFlags = f 221 t.enableMouse(f) 222 t.Unlock() 223 } 224 225 func (t *wScreen) enableMouse(f MouseFlags) { 226 if f&MouseButtonEvents != 0 { 227 js.Global().Set("onMouseClick", js.FuncOf(t.onMouseEvent)) 228 } else { 229 js.Global().Set("onMouseClick", js.FuncOf(t.unset)) 230 } 231 232 if f&MouseDragEvents != 0 || f&MouseMotionEvents != 0 { 233 js.Global().Set("onMouseMove", js.FuncOf(t.onMouseEvent)) 234 } else { 235 js.Global().Set("onMouseMove", js.FuncOf(t.unset)) 236 } 237 } 238 239 func (t *wScreen) DisableMouse() { 240 t.Lock() 241 t.mouseFlags = 0 242 t.enableMouse(0) 243 t.Unlock() 244 } 245 246 func (t *wScreen) EnablePaste() { 247 t.Lock() 248 t.pasteEnabled = true 249 t.enablePasting(true) 250 t.Unlock() 251 } 252 253 func (t *wScreen) DisablePaste() { 254 t.Lock() 255 t.pasteEnabled = false 256 t.enablePasting(false) 257 t.Unlock() 258 } 259 260 func (t *wScreen) enablePasting(on bool) { 261 if on { 262 js.Global().Set("onPaste", js.FuncOf(t.onPaste)) 263 } else { 264 js.Global().Set("onPaste", js.FuncOf(t.unset)) 265 } 266 } 267 268 func (t *wScreen) EnableFocus() { 269 t.Lock() 270 js.Global().Set("onFocus", js.FuncOf(t.onFocus)) 271 t.Unlock() 272 } 273 274 func (t *wScreen) DisableFocus() { 275 t.Lock() 276 js.Global().Set("onFocus", js.FuncOf(t.unset)) 277 t.Unlock() 278 } 279 280 func (t *wScreen) Size() (int, int) { 281 t.Lock() 282 w, h := t.w, t.h 283 t.Unlock() 284 return w, h 285 } 286 287 // resize does nothing, as asking the web window to resize 288 // without a specified width or height will cause no change. 289 func (t *wScreen) resize() {} 290 291 func (t *wScreen) Colors() int { 292 return 16777216 // 256 ^ 3 293 } 294 295 func (t *wScreen) clip(x, y int) (int, int) { 296 w, h := t.cells.Size() 297 if x < 0 { 298 x = 0 299 } 300 if y < 0 { 301 y = 0 302 } 303 if x > w-1 { 304 x = w - 1 305 } 306 if y > h-1 { 307 y = h - 1 308 } 309 return x, y 310 } 311 312 func (t *wScreen) postEvent(ev Event) { 313 select { 314 case t.evch <- ev: 315 case <-t.quit: 316 } 317 } 318 319 func (t *wScreen) onMouseEvent(this js.Value, args []js.Value) interface{} { 320 mod := ModNone 321 button := ButtonNone 322 323 switch args[2].Int() { 324 case 0: 325 if t.mouseFlags&MouseMotionEvents == 0 { 326 // don't want this event! is a mouse motion event, but user has asked not. 327 return nil 328 } 329 button = ButtonNone 330 case 1: 331 button = Button1 332 case 2: 333 button = Button3 // Note we prefer to treat right as button 2 334 case 3: 335 button = Button2 // And the middle button as button 3 336 } 337 338 if args[3].Bool() { // mod shift 339 mod |= ModShift 340 } 341 342 if args[4].Bool() { // mod alt 343 mod |= ModAlt 344 } 345 346 if args[5].Bool() { // mod ctrl 347 mod |= ModCtrl 348 } 349 350 t.postEvent(NewEventMouse(args[0].Int(), args[1].Int(), button, mod)) 351 return nil 352 } 353 354 func (t *wScreen) onKeyEvent(this js.Value, args []js.Value) interface{} { 355 key := args[0].String() 356 357 // don't accept any modifier keys as their own 358 if key == "Control" || key == "Alt" || key == "Meta" || key == "Shift" { 359 return nil 360 } 361 362 mod := ModNone 363 if args[1].Bool() { // mod shift 364 mod |= ModShift 365 } 366 367 if args[2].Bool() { // mod alt 368 mod |= ModAlt 369 } 370 371 if args[3].Bool() { // mod ctrl 372 mod |= ModCtrl 373 } 374 375 if args[4].Bool() { // mod meta 376 mod |= ModMeta 377 } 378 379 // check for special case of Ctrl + key 380 if mod == ModCtrl { 381 if k, ok := WebKeyNames["Ctrl-"+strings.ToLower(key)]; ok { 382 t.postEvent(NewEventKey(k, 0, mod)) 383 return nil 384 } 385 } 386 387 // next try function keys 388 if k, ok := WebKeyNames[key]; ok { 389 t.postEvent(NewEventKey(k, 0, mod)) 390 return nil 391 } 392 393 // finally try normal, printable chars 394 r, _ := utf8.DecodeRuneInString(key) 395 t.postEvent(NewEventKey(KeyRune, r, mod)) 396 return nil 397 } 398 399 func (t *wScreen) onPaste(this js.Value, args []js.Value) interface{} { 400 t.postEvent(NewEventPaste(args[0].Bool())) 401 return nil 402 } 403 404 func (t *wScreen) onFocus(this js.Value, args []js.Value) interface{} { 405 t.postEvent(NewEventFocus(args[0].Bool())) 406 return nil 407 } 408 409 // unset is a dummy function for js when we want nothing to 410 // happen when javascript calls a function (for example, when 411 // mouse input is disabled, when onMouseEvent() is called from 412 // js, it redirects here and does nothing). 413 func (t *wScreen) unset(this js.Value, args []js.Value) interface{} { 414 return nil 415 } 416 417 func (t *wScreen) Sync() { 418 t.Lock() 419 t.resize() 420 t.clear = true 421 t.cells.Invalidate() 422 t.draw() 423 t.Unlock() 424 } 425 426 func (t *wScreen) CharacterSet() string { 427 return "UTF-8" 428 } 429 430 func (t *wScreen) RegisterRuneFallback(orig rune, fallback string) { 431 t.Lock() 432 t.fallback[orig] = fallback 433 t.Unlock() 434 } 435 436 func (t *wScreen) UnregisterRuneFallback(orig rune) { 437 t.Lock() 438 delete(t.fallback, orig) 439 t.Unlock() 440 } 441 442 func (t *wScreen) CanDisplay(r rune, checkFallbacks bool) bool { 443 if utf8.ValidRune(r) { 444 return true 445 } 446 if !checkFallbacks { 447 return false 448 } 449 if _, ok := t.fallback[r]; ok { 450 return true 451 } 452 return false 453 } 454 455 func (t *wScreen) HasMouse() bool { 456 return true 457 } 458 459 func (t *wScreen) HasKey(k Key) bool { 460 return true 461 } 462 463 func (t *wScreen) SetSize(w, h int) { 464 if w == t.w && h == t.h { 465 return 466 } 467 468 t.cells.Invalidate() 469 t.cells.Resize(w, h) 470 js.Global().Call("resize", w, h) 471 t.w, t.h = w, h 472 t.postEvent(NewEventResize(w, h)) 473 } 474 475 func (t *wScreen) Resize(int, int, int, int) {} 476 477 // Suspend simply pauses all input and output, and clears the screen. 478 // There isn't a "default terminal" to go back to. 479 func (t *wScreen) Suspend() error { 480 t.Lock() 481 if !t.running { 482 t.Unlock() 483 return nil 484 } 485 t.running = false 486 t.clearScreen() 487 t.enableMouse(0) 488 t.enablePasting(false) 489 js.Global().Set("onKeyEvent", js.FuncOf(t.unset)) // stop keypresses 490 return nil 491 } 492 493 func (t *wScreen) Resume() error { 494 t.Lock() 495 496 if t.running { 497 return errors.New("already engaged") 498 } 499 t.running = true 500 501 t.enableMouse(t.mouseFlags) 502 t.enablePasting(t.pasteEnabled) 503 504 js.Global().Set("onKeyEvent", js.FuncOf(t.onKeyEvent)) 505 506 t.Unlock() 507 return nil 508 } 509 510 func (t *wScreen) Beep() error { 511 js.Global().Call("beep") 512 return nil 513 } 514 515 func (t *wScreen) Tty() (Tty, bool) { 516 return nil, false 517 } 518 519 func (t *wScreen) GetCells() *CellBuffer { 520 return &t.cells 521 } 522 523 func (t *wScreen) EventQ() chan Event { 524 return t.evch 525 } 526 527 func (t *wScreen) StopQ() <-chan struct{} { 528 return t.quit 529 } 530 531 func (t *wScreen) SetTitle(title string) { 532 js.Global().Call("setTitle", title) 533 } 534 535 // WebKeyNames maps string names reported from HTML 536 // (KeyboardEvent.key) to tcell accepted keys. 537 var WebKeyNames = map[string]Key{ 538 "Enter": KeyEnter, 539 "Backspace": KeyBackspace, 540 "Tab": KeyTab, 541 "Backtab": KeyBacktab, 542 "Escape": KeyEsc, 543 "Backspace2": KeyBackspace2, 544 "Delete": KeyDelete, 545 "Insert": KeyInsert, 546 "ArrowUp": KeyUp, 547 "ArrowDown": KeyDown, 548 "ArrowLeft": KeyLeft, 549 "ArrowRight": KeyRight, 550 "Home": KeyHome, 551 "End": KeyEnd, 552 "UpLeft": KeyUpLeft, // not supported by HTML 553 "UpRight": KeyUpRight, // not supported by HTML 554 "DownLeft": KeyDownLeft, // not supported by HTML 555 "DownRight": KeyDownRight, // not supported by HTML 556 "Center": KeyCenter, 557 "PgDn": KeyPgDn, 558 "PgUp": KeyPgUp, 559 "Clear": KeyClear, 560 "Exit": KeyExit, 561 "Cancel": KeyCancel, 562 "Pause": KeyPause, 563 "Print": KeyPrint, 564 "F1": KeyF1, 565 "F2": KeyF2, 566 "F3": KeyF3, 567 "F4": KeyF4, 568 "F5": KeyF5, 569 "F6": KeyF6, 570 "F7": KeyF7, 571 "F8": KeyF8, 572 "F9": KeyF9, 573 "F10": KeyF10, 574 "F11": KeyF11, 575 "F12": KeyF12, 576 "F13": KeyF13, 577 "F14": KeyF14, 578 "F15": KeyF15, 579 "F16": KeyF16, 580 "F17": KeyF17, 581 "F18": KeyF18, 582 "F19": KeyF19, 583 "F20": KeyF20, 584 "F21": KeyF21, 585 "F22": KeyF22, 586 "F23": KeyF23, 587 "F24": KeyF24, 588 "F25": KeyF25, 589 "F26": KeyF26, 590 "F27": KeyF27, 591 "F28": KeyF28, 592 "F29": KeyF29, 593 "F30": KeyF30, 594 "F31": KeyF31, 595 "F32": KeyF32, 596 "F33": KeyF33, 597 "F34": KeyF34, 598 "F35": KeyF35, 599 "F36": KeyF36, 600 "F37": KeyF37, 601 "F38": KeyF38, 602 "F39": KeyF39, 603 "F40": KeyF40, 604 "F41": KeyF41, 605 "F42": KeyF42, 606 "F43": KeyF43, 607 "F44": KeyF44, 608 "F45": KeyF45, 609 "F46": KeyF46, 610 "F47": KeyF47, 611 "F48": KeyF48, 612 "F49": KeyF49, 613 "F50": KeyF50, 614 "F51": KeyF51, 615 "F52": KeyF52, 616 "F53": KeyF53, 617 "F54": KeyF54, 618 "F55": KeyF55, 619 "F56": KeyF56, 620 "F57": KeyF57, 621 "F58": KeyF58, 622 "F59": KeyF59, 623 "F60": KeyF60, 624 "F61": KeyF61, 625 "F62": KeyF62, 626 "F63": KeyF63, 627 "F64": KeyF64, 628 "Ctrl-a": KeyCtrlA, // not reported by HTML- need to do special check 629 "Ctrl-b": KeyCtrlB, // not reported by HTML- need to do special check 630 "Ctrl-c": KeyCtrlC, // not reported by HTML- need to do special check 631 "Ctrl-d": KeyCtrlD, // not reported by HTML- need to do special check 632 "Ctrl-e": KeyCtrlE, // not reported by HTML- need to do special check 633 "Ctrl-f": KeyCtrlF, // not reported by HTML- need to do special check 634 "Ctrl-g": KeyCtrlG, // not reported by HTML- need to do special check 635 "Ctrl-j": KeyCtrlJ, // not reported by HTML- need to do special check 636 "Ctrl-k": KeyCtrlK, // not reported by HTML- need to do special check 637 "Ctrl-l": KeyCtrlL, // not reported by HTML- need to do special check 638 "Ctrl-n": KeyCtrlN, // not reported by HTML- need to do special check 639 "Ctrl-o": KeyCtrlO, // not reported by HTML- need to do special check 640 "Ctrl-p": KeyCtrlP, // not reported by HTML- need to do special check 641 "Ctrl-q": KeyCtrlQ, // not reported by HTML- need to do special check 642 "Ctrl-r": KeyCtrlR, // not reported by HTML- need to do special check 643 "Ctrl-s": KeyCtrlS, // not reported by HTML- need to do special check 644 "Ctrl-t": KeyCtrlT, // not reported by HTML- need to do special check 645 "Ctrl-u": KeyCtrlU, // not reported by HTML- need to do special check 646 "Ctrl-v": KeyCtrlV, // not reported by HTML- need to do special check 647 "Ctrl-w": KeyCtrlW, // not reported by HTML- need to do special check 648 "Ctrl-x": KeyCtrlX, // not reported by HTML- need to do special check 649 "Ctrl-y": KeyCtrlY, // not reported by HTML- need to do special check 650 "Ctrl-z": KeyCtrlZ, // not reported by HTML- need to do special check 651 "Ctrl- ": KeyCtrlSpace, // not reported by HTML- need to do special check 652 "Ctrl-_": KeyCtrlUnderscore, // not reported by HTML- need to do special check 653 "Ctrl-]": KeyCtrlRightSq, // not reported by HTML- need to do special check 654 "Ctrl-\\": KeyCtrlBackslash, // not reported by HTML- need to do special check 655 "Ctrl-^": KeyCtrlCarat, // not reported by HTML- need to do special check 656 } 657 658 var curStyleClasses = map[CursorStyle]string{ 659 CursorStyleDefault: "cursor-blinking-block", 660 CursorStyleBlinkingBlock: "cursor-blinking-block", 661 CursorStyleSteadyBlock: "cursor-steady-block", 662 CursorStyleBlinkingUnderline: "cursor-blinking-underline", 663 CursorStyleSteadyUnderline: "cursor-steady-underline", 664 CursorStyleBlinkingBar: "cursor-blinking-bar", 665 CursorStyleSteadyBar: "cursor-steady-bar", 666 } 667 668 func LookupTerminfo(name string) (ti *terminfo.Terminfo, e error) { 669 return nil, errors.New("LookupTermInfo not supported") 670 }