gemini-browser

A text-based gemini browser
git clone git://git.laack.co/gemini-browser.git
Log | Files | Refs | README

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 }