diff --git a/README-wasm.md b/README-wasm.md new file mode 100644 index 0000000..26db38a --- /dev/null +++ b/README-wasm.md @@ -0,0 +1,54 @@ +# WASM for _Tcell_ + +You can build _Tcell_ project into a webpage by compiling it slightly differently. This will result in a _Tcell_ project you can embed into another html page, or use as a standalone page. + +## Building your project + +WASM needs special build flags in order to work. You can build it by executing +```sh +GOOS=js GOARCH=wasm go build -o yourfile.wasm +``` + +## Additional files + +You also need 5 other files in the same directory as the wasm. Four (`tcell.html`, `tcell.js`, `termstyle.css`, and `beep.wav`) are provided in the `webfiles` directory. The last one, `wasm_exec.js`, can be copied from GOROOT into the current directory by executing +```sh +cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" ./ +``` + +In `tcell.js`, you also need to change the constant +```js +const wasmFilePath = "yourfile.wasm" +``` +to the file you outputed to when building. + +## Displaying your project + +### Standalone + +You can see the project (with an white background around the terminal) by serving the directory. You can do this using any framework, including another golang project: + +```golang +// server.go + +package main + +import ( + "log" + "net/http" +) + +func main() { + log.Fatal(http.ListenAndServe(":8080", + http.FileServer(http.Dir("/path/to/dir/to/serve")) + )) +} +``` + +To see the webpage with this example, you can type in `localhost:8080/tcell.html` into your browser while `server.go` is running. + +### Embedding +It is recomended to use an iframe if you want to embed the app into a webpage: +```html + +``` \ No newline at end of file diff --git a/README.md b/README.md index 07d81df..347e274 100644 --- a/README.md +++ b/README.md @@ -265,18 +265,8 @@ Modern console applications like ConEmu and the Windows 10 terminal, support all the good features (resize, mouse tracking, etc.) ### WASM -WASM needs special build flags and extra files in the same directory in order to work. You can build it by executing -```sh -GOOS=js GOARCH=wasm build -o yourfile.wasm -``` -You also need 5 other files. Four (`tcell.html`, `tcell.js`, `termstyle.css`, and `beep.wav`) are provided in the `webfiles` directory. The last one, `wasm_exec.js`, can be copied from GOROOT into the current directory by executing -```sh -cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" ./ -``` -It is recomended use an iframe if you want to embed the app into a webpage: -```html - -``` + +WASM is supported, but needs additional setup detailed in [README-wasm](README-wasm.md). ### Plan9 and others diff --git a/tscreen.go b/tscreen.go index bf10b1c..448324b 100644 --- a/tscreen.go +++ b/tscreen.go @@ -12,19 +12,562 @@ // See the License for the specific language governing permissions and // limitations under the License. +//go:build !(js && wasm) +// +build !js !wasm + package tcell import ( + "bytes" + "errors" + "io" + "os" + "strconv" + "strings" + "sync" + "time" + "unicode/utf8" + + "golang.org/x/term" + "golang.org/x/text/transform" + + "github.com/gdamore/tcell/v2/terminfo" + // import the stock terminals _ "github.com/gdamore/tcell/v2/terminfo/base" ) +// NewTerminfoScreen returns a Screen that uses the stock TTY interface +// and POSIX terminal control, combined with a terminfo description taken from +// the $TERM environment variable. It returns an error if the terminal +// is not supported for any reason. +// +// For terminals that do not support dynamic resize events, the $LINES +// $COLUMNS environment variables can be set to the actual window size, +// otherwise defaults taken from the terminal database are used. +func NewTerminfoScreen() (Screen, error) { + return NewTerminfoScreenFromTty(nil) +} + +// LookupTerminfo attempts to find a definition for the named $TERM falling +// back to attempting to parse the output from infocmp. +func LookupTerminfo(name string) (ti *terminfo.Terminfo, e error) { + ti, e = terminfo.LookupTerminfo(name) + if e != nil { + ti, e = loadDynamicTerminfo(name) + if e != nil { + return nil, e + } + terminfo.AddTerminfo(ti) + } + + return +} + +// NewTerminfoScreenFromTtyTerminfo returns a Screen using a custom Tty +// implementation and custom terminfo specification. +// If the passed in tty is nil, then a reasonable default (typically /dev/tty) +// is presumed, at least on UNIX hosts. (Windows hosts will typically fail this +// call altogether.) +// If passed terminfo is nil, then TERM environment variable is queried for +// terminal specification. +func NewTerminfoScreenFromTtyTerminfo(tty Tty, ti *terminfo.Terminfo) (s Screen, e error) { + if ti == nil { + ti, e = LookupTerminfo(os.Getenv("TERM")) + if e != nil { + return + } + } + + t := &tScreen{ti: ti, tty: tty} + + t.keyexist = make(map[Key]bool) + t.keycodes = make(map[string]*tKeyCode) + if len(ti.Mouse) > 0 { + t.mouse = []byte(ti.Mouse) + } + t.prepareKeys() + t.buildAcsMap() + t.resizeQ = make(chan bool, 1) + t.fallback = make(map[rune]string) + for k, v := range RuneFallbacks { + t.fallback[k] = v + } + + return t, nil +} + +// NewTerminfoScreenFromTty returns a Screen using a custom Tty implementation. +// If the passed in tty is nil, then a reasonable default (typically /dev/tty) +// is presumed, at least on UNIX hosts. (Windows hosts will typically fail this +// call altogether.) +func NewTerminfoScreenFromTty(tty Tty) (Screen, error) { + return NewTerminfoScreenFromTtyTerminfo(tty, nil) +} + // tKeyCode represents a combination of a key code and modifiers. type tKeyCode struct { key Key mod ModMask } +// tScreen represents a screen backed by a terminfo implementation. +type tScreen struct { + ti *terminfo.Terminfo + tty Tty + h int + w int + fini bool + cells CellBuffer + buffering bool // true if we are collecting writes to buf instead of sending directly to out + buf bytes.Buffer + curstyle Style + style Style + evch chan Event + resizeQ chan bool + quit chan struct{} + keyexist map[Key]bool + keycodes map[string]*tKeyCode + keychan chan []byte + keytimer *time.Timer + keyexpire time.Time + cx int + cy int + mouse []byte + clear bool + cursorx int + cursory int + acs map[rune]string + charset string + encoder transform.Transformer + decoder transform.Transformer + fallback map[rune]string + colors map[Color]Color + palette []Color + truecolor bool + escaped bool + buttondn bool + finiOnce sync.Once + enablePaste string + disablePaste string + enterUrl string + exitUrl string + setWinSize string + cursorStyles map[CursorStyle]string + cursorStyle CursorStyle + saved *term.State + stopQ chan struct{} + running bool + wg sync.WaitGroup + mouseFlags MouseFlags + pasteEnabled bool + + sync.Mutex +} + +func (t *tScreen) Init() error { + if e := t.initialize(); e != nil { + return e + } + + t.evch = make(chan Event, 10) + t.keychan = make(chan []byte, 10) + t.keytimer = time.NewTimer(time.Millisecond * 50) + t.charset = "UTF-8" + + t.charset = getCharset() + if enc := GetEncoding(t.charset); enc != nil { + t.encoder = enc.NewEncoder() + t.decoder = enc.NewDecoder() + } else { + return ErrNoCharset + } + ti := t.ti + + // environment overrides + w := ti.Columns + h := ti.Lines + if i, _ := strconv.Atoi(os.Getenv("LINES")); i != 0 { + h = i + } + if i, _ := strconv.Atoi(os.Getenv("COLUMNS")); i != 0 { + w = i + } + if t.ti.SetFgBgRGB != "" || t.ti.SetFgRGB != "" || t.ti.SetBgRGB != "" { + t.truecolor = true + } + // A user who wants to have his themes honored can + // set this environment variable. + if os.Getenv("TCELL_TRUECOLOR") == "disable" { + t.truecolor = false + } + nColors := t.nColors() + if nColors > 256 { + nColors = 256 // clip to reasonable limits + } + t.colors = make(map[Color]Color, nColors) + t.palette = make([]Color, nColors) + for i := 0; i < nColors; i++ { + t.palette[i] = Color(i) | ColorValid + // identity map for our builtin colors + t.colors[Color(i)|ColorValid] = Color(i) | ColorValid + } + + t.quit = make(chan struct{}) + + t.Lock() + t.cx = -1 + t.cy = -1 + t.style = StyleDefault + t.cells.Resize(w, h) + t.cursorx = -1 + t.cursory = -1 + t.resize() + t.Unlock() + + if err := t.engage(); err != nil { + return err + } + + return nil +} + +func (t *tScreen) prepareKeyMod(key Key, mod ModMask, val string) { + if val != "" { + // Do not override codes that already exist + if _, exist := t.keycodes[val]; !exist { + t.keyexist[key] = true + t.keycodes[val] = &tKeyCode{key: key, mod: mod} + } + } +} + +func (t *tScreen) prepareKeyModReplace(key Key, replace Key, mod ModMask, val string) { + if val != "" { + // Do not override codes that already exist + if old, exist := t.keycodes[val]; !exist || old.key == replace { + t.keyexist[key] = true + t.keycodes[val] = &tKeyCode{key: key, mod: mod} + } + } +} + +func (t *tScreen) prepareKeyModXTerm(key Key, val string) { + + if strings.HasPrefix(val, "\x1b[") && strings.HasSuffix(val, "~") { + + // Drop the trailing ~ + val = val[:len(val)-1] + + // These suffixes are calculated assuming Xterm style modifier suffixes. + // Please see https://invisible-island.net/xterm/ctlseqs/ctlseqs.pdf for + // more information (specifically "PC-Style Function Keys"). + t.prepareKeyModReplace(key, key+12, ModShift, val+";2~") + t.prepareKeyModReplace(key, key+48, ModAlt, val+";3~") + t.prepareKeyModReplace(key, key+60, ModAlt|ModShift, val+";4~") + t.prepareKeyModReplace(key, key+24, ModCtrl, val+";5~") + t.prepareKeyModReplace(key, key+36, ModCtrl|ModShift, val+";6~") + t.prepareKeyMod(key, ModAlt|ModCtrl, val+";7~") + t.prepareKeyMod(key, ModShift|ModAlt|ModCtrl, val+";8~") + t.prepareKeyMod(key, ModMeta, val+";9~") + t.prepareKeyMod(key, ModMeta|ModShift, val+";10~") + t.prepareKeyMod(key, ModMeta|ModAlt, val+";11~") + t.prepareKeyMod(key, ModMeta|ModAlt|ModShift, val+";12~") + t.prepareKeyMod(key, ModMeta|ModCtrl, val+";13~") + t.prepareKeyMod(key, ModMeta|ModCtrl|ModShift, val+";14~") + t.prepareKeyMod(key, ModMeta|ModCtrl|ModAlt, val+";15~") + t.prepareKeyMod(key, ModMeta|ModCtrl|ModAlt|ModShift, val+";16~") + } else if strings.HasPrefix(val, "\x1bO") && len(val) == 3 { + val = val[2:] + t.prepareKeyModReplace(key, key+12, ModShift, "\x1b[1;2"+val) + t.prepareKeyModReplace(key, key+48, ModAlt, "\x1b[1;3"+val) + t.prepareKeyModReplace(key, key+24, ModCtrl, "\x1b[1;5"+val) + t.prepareKeyModReplace(key, key+36, ModCtrl|ModShift, "\x1b[1;6"+val) + t.prepareKeyModReplace(key, key+60, ModAlt|ModShift, "\x1b[1;4"+val) + t.prepareKeyMod(key, ModAlt|ModCtrl, "\x1b[1;7"+val) + t.prepareKeyMod(key, ModShift|ModAlt|ModCtrl, "\x1b[1;8"+val) + t.prepareKeyMod(key, ModMeta, "\x1b[1;9"+val) + t.prepareKeyMod(key, ModMeta|ModShift, "\x1b[1;10"+val) + t.prepareKeyMod(key, ModMeta|ModAlt, "\x1b[1;11"+val) + t.prepareKeyMod(key, ModMeta|ModAlt|ModShift, "\x1b[1;12"+val) + t.prepareKeyMod(key, ModMeta|ModCtrl, "\x1b[1;13"+val) + t.prepareKeyMod(key, ModMeta|ModCtrl|ModShift, "\x1b[1;14"+val) + t.prepareKeyMod(key, ModMeta|ModCtrl|ModAlt, "\x1b[1;15"+val) + t.prepareKeyMod(key, ModMeta|ModCtrl|ModAlt|ModShift, "\x1b[1;16"+val) + } +} + +func (t *tScreen) prepareXtermModifiers() { + if t.ti.Modifiers != terminfo.ModifiersXTerm { + return + } + t.prepareKeyModXTerm(KeyRight, t.ti.KeyRight) + t.prepareKeyModXTerm(KeyLeft, t.ti.KeyLeft) + t.prepareKeyModXTerm(KeyUp, t.ti.KeyUp) + t.prepareKeyModXTerm(KeyDown, t.ti.KeyDown) + t.prepareKeyModXTerm(KeyInsert, t.ti.KeyInsert) + t.prepareKeyModXTerm(KeyDelete, t.ti.KeyDelete) + t.prepareKeyModXTerm(KeyPgUp, t.ti.KeyPgUp) + t.prepareKeyModXTerm(KeyPgDn, t.ti.KeyPgDn) + t.prepareKeyModXTerm(KeyHome, t.ti.KeyHome) + t.prepareKeyModXTerm(KeyEnd, t.ti.KeyEnd) + t.prepareKeyModXTerm(KeyF1, t.ti.KeyF1) + t.prepareKeyModXTerm(KeyF2, t.ti.KeyF2) + t.prepareKeyModXTerm(KeyF3, t.ti.KeyF3) + t.prepareKeyModXTerm(KeyF4, t.ti.KeyF4) + t.prepareKeyModXTerm(KeyF5, t.ti.KeyF5) + t.prepareKeyModXTerm(KeyF6, t.ti.KeyF6) + t.prepareKeyModXTerm(KeyF7, t.ti.KeyF7) + t.prepareKeyModXTerm(KeyF8, t.ti.KeyF8) + t.prepareKeyModXTerm(KeyF9, t.ti.KeyF9) + t.prepareKeyModXTerm(KeyF10, t.ti.KeyF10) + t.prepareKeyModXTerm(KeyF11, t.ti.KeyF11) + t.prepareKeyModXTerm(KeyF12, t.ti.KeyF12) +} + +func (t *tScreen) prepareBracketedPaste() { + // Another workaround for lack of reporting in terminfo. + // We assume if the terminal has a mouse entry, that it + // offers bracketed paste. But we allow specific overrides + // via our terminal database. + if t.ti.EnablePaste != "" { + t.enablePaste = t.ti.EnablePaste + t.disablePaste = t.ti.DisablePaste + t.prepareKey(keyPasteStart, t.ti.PasteStart) + t.prepareKey(keyPasteEnd, t.ti.PasteEnd) + } else if t.ti.Mouse != "" { + t.enablePaste = "\x1b[?2004h" + t.disablePaste = "\x1b[?2004l" + t.prepareKey(keyPasteStart, "\x1b[200~") + t.prepareKey(keyPasteEnd, "\x1b[201~") + } +} + +func (t *tScreen) prepareExtendedOSC() { + // More stuff for limits in terminfo. This time we are applying + // the most common OSC (operating system commands). Generally + // terminals that don't understand these will ignore them. + // Again, we condition this based on mouse capabilities. + if t.ti.EnterUrl != "" { + t.enterUrl = t.ti.EnterUrl + t.exitUrl = t.ti.ExitUrl + } else if t.ti.Mouse != "" { + t.enterUrl = "\x1b]8;%p2%s;%p1%s\x1b\\" + t.exitUrl = "\x1b]8;;\x1b\\" + } + + if t.ti.SetWindowSize != "" { + t.setWinSize = t.ti.SetWindowSize + } else if t.ti.Mouse != "" { + t.setWinSize = "\x1b[8;%p1%p2%d;%dt" + } +} + +func (t *tScreen) prepareCursorStyles() { + // Another workaround for lack of reporting in terminfo. + // We assume if the terminal has a mouse entry, that it + // offers bracketed paste. But we allow specific overrides + // via our terminal database. + if t.ti.CursorDefault != "" { + t.cursorStyles = map[CursorStyle]string{ + CursorStyleDefault: t.ti.CursorDefault, + CursorStyleBlinkingBlock: t.ti.CursorBlinkingBlock, + CursorStyleSteadyBlock: t.ti.CursorSteadyBlock, + CursorStyleBlinkingUnderline: t.ti.CursorBlinkingUnderline, + CursorStyleSteadyUnderline: t.ti.CursorSteadyUnderline, + CursorStyleBlinkingBar: t.ti.CursorBlinkingBar, + CursorStyleSteadyBar: t.ti.CursorSteadyBar, + } + } else if t.ti.Mouse != "" { + t.cursorStyles = map[CursorStyle]string{ + CursorStyleDefault: "\x1b[0 q", + CursorStyleBlinkingBlock: "\x1b[1 q", + CursorStyleSteadyBlock: "\x1b[2 q", + CursorStyleBlinkingUnderline: "\x1b[3 q", + CursorStyleSteadyUnderline: "\x1b[4 q", + CursorStyleBlinkingBar: "\x1b[5 q", + CursorStyleSteadyBar: "\x1b[6 q", + } + } +} + +func (t *tScreen) prepareKey(key Key, val string) { + t.prepareKeyMod(key, ModNone, val) +} + +func (t *tScreen) prepareKeys() { + ti := t.ti + t.prepareKey(KeyBackspace, ti.KeyBackspace) + t.prepareKey(KeyF1, ti.KeyF1) + t.prepareKey(KeyF2, ti.KeyF2) + t.prepareKey(KeyF3, ti.KeyF3) + t.prepareKey(KeyF4, ti.KeyF4) + t.prepareKey(KeyF5, ti.KeyF5) + t.prepareKey(KeyF6, ti.KeyF6) + t.prepareKey(KeyF7, ti.KeyF7) + t.prepareKey(KeyF8, ti.KeyF8) + t.prepareKey(KeyF9, ti.KeyF9) + t.prepareKey(KeyF10, ti.KeyF10) + t.prepareKey(KeyF11, ti.KeyF11) + t.prepareKey(KeyF12, ti.KeyF12) + t.prepareKey(KeyF13, ti.KeyF13) + t.prepareKey(KeyF14, ti.KeyF14) + t.prepareKey(KeyF15, ti.KeyF15) + t.prepareKey(KeyF16, ti.KeyF16) + t.prepareKey(KeyF17, ti.KeyF17) + t.prepareKey(KeyF18, ti.KeyF18) + t.prepareKey(KeyF19, ti.KeyF19) + t.prepareKey(KeyF20, ti.KeyF20) + t.prepareKey(KeyF21, ti.KeyF21) + t.prepareKey(KeyF22, ti.KeyF22) + t.prepareKey(KeyF23, ti.KeyF23) + t.prepareKey(KeyF24, ti.KeyF24) + t.prepareKey(KeyF25, ti.KeyF25) + t.prepareKey(KeyF26, ti.KeyF26) + t.prepareKey(KeyF27, ti.KeyF27) + t.prepareKey(KeyF28, ti.KeyF28) + t.prepareKey(KeyF29, ti.KeyF29) + t.prepareKey(KeyF30, ti.KeyF30) + t.prepareKey(KeyF31, ti.KeyF31) + t.prepareKey(KeyF32, ti.KeyF32) + t.prepareKey(KeyF33, ti.KeyF33) + t.prepareKey(KeyF34, ti.KeyF34) + t.prepareKey(KeyF35, ti.KeyF35) + t.prepareKey(KeyF36, ti.KeyF36) + t.prepareKey(KeyF37, ti.KeyF37) + t.prepareKey(KeyF38, ti.KeyF38) + t.prepareKey(KeyF39, ti.KeyF39) + t.prepareKey(KeyF40, ti.KeyF40) + t.prepareKey(KeyF41, ti.KeyF41) + t.prepareKey(KeyF42, ti.KeyF42) + t.prepareKey(KeyF43, ti.KeyF43) + t.prepareKey(KeyF44, ti.KeyF44) + t.prepareKey(KeyF45, ti.KeyF45) + t.prepareKey(KeyF46, ti.KeyF46) + t.prepareKey(KeyF47, ti.KeyF47) + t.prepareKey(KeyF48, ti.KeyF48) + t.prepareKey(KeyF49, ti.KeyF49) + t.prepareKey(KeyF50, ti.KeyF50) + t.prepareKey(KeyF51, ti.KeyF51) + t.prepareKey(KeyF52, ti.KeyF52) + t.prepareKey(KeyF53, ti.KeyF53) + t.prepareKey(KeyF54, ti.KeyF54) + t.prepareKey(KeyF55, ti.KeyF55) + t.prepareKey(KeyF56, ti.KeyF56) + t.prepareKey(KeyF57, ti.KeyF57) + t.prepareKey(KeyF58, ti.KeyF58) + t.prepareKey(KeyF59, ti.KeyF59) + t.prepareKey(KeyF60, ti.KeyF60) + t.prepareKey(KeyF61, ti.KeyF61) + t.prepareKey(KeyF62, ti.KeyF62) + t.prepareKey(KeyF63, ti.KeyF63) + t.prepareKey(KeyF64, ti.KeyF64) + t.prepareKey(KeyInsert, ti.KeyInsert) + t.prepareKey(KeyDelete, ti.KeyDelete) + t.prepareKey(KeyHome, ti.KeyHome) + t.prepareKey(KeyEnd, ti.KeyEnd) + t.prepareKey(KeyUp, ti.KeyUp) + t.prepareKey(KeyDown, ti.KeyDown) + t.prepareKey(KeyLeft, ti.KeyLeft) + t.prepareKey(KeyRight, ti.KeyRight) + t.prepareKey(KeyPgUp, ti.KeyPgUp) + t.prepareKey(KeyPgDn, ti.KeyPgDn) + t.prepareKey(KeyHelp, ti.KeyHelp) + t.prepareKey(KeyPrint, ti.KeyPrint) + t.prepareKey(KeyCancel, ti.KeyCancel) + t.prepareKey(KeyExit, ti.KeyExit) + t.prepareKey(KeyBacktab, ti.KeyBacktab) + + t.prepareKeyMod(KeyRight, ModShift, ti.KeyShfRight) + t.prepareKeyMod(KeyLeft, ModShift, ti.KeyShfLeft) + t.prepareKeyMod(KeyUp, ModShift, ti.KeyShfUp) + t.prepareKeyMod(KeyDown, ModShift, ti.KeyShfDown) + t.prepareKeyMod(KeyHome, ModShift, ti.KeyShfHome) + t.prepareKeyMod(KeyEnd, ModShift, ti.KeyShfEnd) + t.prepareKeyMod(KeyPgUp, ModShift, ti.KeyShfPgUp) + t.prepareKeyMod(KeyPgDn, ModShift, ti.KeyShfPgDn) + + t.prepareKeyMod(KeyRight, ModCtrl, ti.KeyCtrlRight) + t.prepareKeyMod(KeyLeft, ModCtrl, ti.KeyCtrlLeft) + t.prepareKeyMod(KeyUp, ModCtrl, ti.KeyCtrlUp) + t.prepareKeyMod(KeyDown, ModCtrl, ti.KeyCtrlDown) + t.prepareKeyMod(KeyHome, ModCtrl, ti.KeyCtrlHome) + t.prepareKeyMod(KeyEnd, ModCtrl, ti.KeyCtrlEnd) + + // Sadly, xterm handling of keycodes is somewhat erratic. In + // particular, different codes are sent depending on application + // mode is in use or not, and the entries for many of these are + // simply absent from terminfo on many systems. So we insert + // a number of escape sequences if they are not already used, in + // order to have the widest correct usage. Note that prepareKey + // will not inject codes if the escape sequence is already known. + // We also only do this for terminals that have the application + // mode present. + + // Cursor mode + if ti.EnterKeypad != "" { + t.prepareKey(KeyUp, "\x1b[A") + t.prepareKey(KeyDown, "\x1b[B") + t.prepareKey(KeyRight, "\x1b[C") + t.prepareKey(KeyLeft, "\x1b[D") + t.prepareKey(KeyEnd, "\x1b[F") + t.prepareKey(KeyHome, "\x1b[H") + t.prepareKey(KeyDelete, "\x1b[3~") + t.prepareKey(KeyHome, "\x1b[1~") + t.prepareKey(KeyEnd, "\x1b[4~") + t.prepareKey(KeyPgUp, "\x1b[5~") + t.prepareKey(KeyPgDn, "\x1b[6~") + + // Application mode + t.prepareKey(KeyUp, "\x1bOA") + t.prepareKey(KeyDown, "\x1bOB") + t.prepareKey(KeyRight, "\x1bOC") + t.prepareKey(KeyLeft, "\x1bOD") + t.prepareKey(KeyHome, "\x1bOH") + } + + t.prepareKey(keyPasteStart, ti.PasteStart) + t.prepareKey(keyPasteEnd, ti.PasteEnd) + t.prepareXtermModifiers() + t.prepareBracketedPaste() + t.prepareCursorStyles() + t.prepareExtendedOSC() + +outer: + // Add key mappings for control keys. + for i := 0; i < ' '; i++ { + // Do not insert direct key codes for ambiguous keys. + // For example, ESC is used for lots of other keys, so + // when parsing this we don't want to fast path handling + // of it, but instead wait a bit before parsing it as in + // isolation. + for esc := range t.keycodes { + if []byte(esc)[0] == byte(i) { + continue outer + } + } + + t.keyexist[Key(i)] = true + + mod := ModCtrl + switch Key(i) { + case KeyBS, KeyTAB, KeyESC, KeyCR: + // directly type-able- no control sequence + mod = ModNone + } + t.keycodes[string(rune(i))] = &tKeyCode{key: Key(i), mod: mod} + } +} + +func (t *tScreen) Fini() { + t.finiOnce.Do(t.finish) +} + +func (t *tScreen) finish() { + close(t.quit) + t.finalize() +} + func (t *tScreen) SetStyle(style Style) { t.Lock() if !t.fini { @@ -68,10 +611,289 @@ func (t *tScreen) SetCell(x, y int, style Style, ch ...rune) { } } +func (t *tScreen) encodeRune(r rune, buf []byte) []byte { + + nb := make([]byte, 6) + ob := make([]byte, 6) + num := utf8.EncodeRune(ob, r) + ob = ob[:num] + dst := 0 + var err error + if enc := t.encoder; enc != nil { + enc.Reset() + dst, _, err = enc.Transform(nb, ob, true) + } + if err != nil || dst == 0 || nb[0] == '\x1a' { + // Combining characters are elided + if len(buf) == 0 { + if acs, ok := t.acs[r]; ok { + buf = append(buf, []byte(acs)...) + } else if fb, ok := t.fallback[r]; ok { + buf = append(buf, []byte(fb)...) + } else { + buf = append(buf, '?') + } + } + } else { + buf = append(buf, nb[:dst]...) + } + + return buf +} + +func (t *tScreen) sendFgBg(fg Color, bg Color, attr AttrMask) AttrMask { + ti := t.ti + if ti.Colors == 0 { + // foreground vs background, we calculate luminance + // and possibly do a reverse video + if !fg.Valid() { + return attr + } + v, ok := t.colors[fg] + if !ok { + v = FindColor(fg, []Color{ColorBlack, ColorWhite}) + t.colors[fg] = v + } + switch v { + case ColorWhite: + return attr + case ColorBlack: + return attr ^ AttrReverse + } + } + + if fg == ColorReset || bg == ColorReset { + t.TPuts(ti.ResetFgBg) + } + if t.truecolor { + if ti.SetFgBgRGB != "" && fg.IsRGB() && bg.IsRGB() { + r1, g1, b1 := fg.RGB() + r2, g2, b2 := bg.RGB() + t.TPuts(ti.TParm(ti.SetFgBgRGB, + int(r1), int(g1), int(b1), + int(r2), int(g2), int(b2))) + return attr + } + + if fg.IsRGB() && ti.SetFgRGB != "" { + r, g, b := fg.RGB() + t.TPuts(ti.TParm(ti.SetFgRGB, int(r), int(g), int(b))) + fg = ColorDefault + } + + if bg.IsRGB() && ti.SetBgRGB != "" { + r, g, b := bg.RGB() + t.TPuts(ti.TParm(ti.SetBgRGB, + int(r), int(g), int(b))) + bg = ColorDefault + } + } + + if fg.Valid() { + if v, ok := t.colors[fg]; ok { + fg = v + } else { + v = FindColor(fg, t.palette) + t.colors[fg] = v + fg = v + } + } + + if bg.Valid() { + if v, ok := t.colors[bg]; ok { + bg = v + } else { + v = FindColor(bg, t.palette) + t.colors[bg] = v + bg = v + } + } + + if fg.Valid() && bg.Valid() && ti.SetFgBg != "" { + t.TPuts(ti.TParm(ti.SetFgBg, int(fg&0xff), int(bg&0xff))) + } else { + if fg.Valid() && ti.SetFg != "" { + t.TPuts(ti.TParm(ti.SetFg, int(fg&0xff))) + } + if bg.Valid() && ti.SetBg != "" { + t.TPuts(ti.TParm(ti.SetBg, int(bg&0xff))) + } + } + return attr +} + +func (t *tScreen) drawCell(x, y int) int { + + ti := t.ti + + mainc, combc, style, width := t.cells.GetContent(x, y) + if !t.cells.Dirty(x, y) { + return width + } + + if y == t.h-1 && x == t.w-1 && t.ti.AutoMargin && ti.InsertChar != "" { + // our solution is somewhat goofy. + // we write to the second to the last cell what we want in the last cell, then we + // insert a character at that 2nd to last position to shift the last column into + // place, then we rewrite that 2nd to last cell. Old terminals suck. + t.TPuts(ti.TGoto(x-1, y)) + defer func() { + t.TPuts(ti.TGoto(x-1, y)) + t.TPuts(ti.InsertChar) + t.cy = y + t.cx = x - 1 + t.cells.SetDirty(x-1, y, true) + _ = t.drawCell(x-1, y) + t.TPuts(t.ti.TGoto(0, 0)) + t.cy = 0 + t.cx = 0 + }() + } else if t.cy != y || t.cx != x { + t.TPuts(ti.TGoto(x, y)) + t.cx = x + t.cy = y + } + + if style == StyleDefault { + style = t.style + } + if style != t.curstyle { + fg, bg, attrs := style.Decompose() + + t.TPuts(ti.AttrOff) + + attrs = t.sendFgBg(fg, bg, attrs) + if attrs&AttrBold != 0 { + t.TPuts(ti.Bold) + } + if attrs&AttrUnderline != 0 { + t.TPuts(ti.Underline) + } + if attrs&AttrReverse != 0 { + t.TPuts(ti.Reverse) + } + if attrs&AttrBlink != 0 { + t.TPuts(ti.Blink) + } + if attrs&AttrDim != 0 { + t.TPuts(ti.Dim) + } + if attrs&AttrItalic != 0 { + t.TPuts(ti.Italic) + } + if attrs&AttrStrikeThrough != 0 { + t.TPuts(ti.StrikeThrough) + } + + // URL string can be long, so don't send it unless we really need to + if t.enterUrl != "" && t.curstyle != style { + if style.url != "" { + t.TPuts(ti.TParm(t.enterUrl, style.url, style.urlId)) + } else { + t.TPuts(t.exitUrl) + } + } + + t.curstyle = style + } + + // now emit runes - taking care to not overrun width with a + // wide character, and to ensure that we emit exactly one regular + // character followed up by any residual combing characters + + if width < 1 { + width = 1 + } + + var str string + + buf := make([]byte, 0, 6) + + buf = t.encodeRune(mainc, buf) + for _, r := range combc { + buf = t.encodeRune(r, buf) + } + + str = string(buf) + if width > 1 && str == "?" { + // No FullWidth character support + str = "? " + t.cx = -1 + } + + if x > t.w-width { + // too wide to fit; emit a single space instead + width = 1 + str = " " + } + t.writeString(str) + t.cx += width + t.cells.SetDirty(x, y, false) + if width > 1 { + t.cx = -1 + } + + return width +} + +func (t *tScreen) ShowCursor(x, y int) { + t.Lock() + t.cursorx = x + t.cursory = y + t.Unlock() +} + +func (t *tScreen) SetCursorStyle(cs CursorStyle) { + t.Lock() + t.cursorStyle = cs + t.Unlock() +} + func (t *tScreen) HideCursor() { t.ShowCursor(-1, -1) } +func (t *tScreen) showCursor() { + + x, y := t.cursorx, t.cursory + w, h := t.cells.Size() + if x < 0 || y < 0 || x >= w || y >= h { + t.hideCursor() + return + } + t.TPuts(t.ti.TGoto(x, y)) + t.TPuts(t.ti.ShowCursor) + if t.cursorStyles != nil { + if esc, ok := t.cursorStyles[t.cursorStyle]; ok { + t.TPuts(esc) + } + } + t.cx = x + t.cy = y +} + +// writeString sends a string to the terminal. The string is sent as-is and +// this function does not expand inline padding indications (of the form +// $<[delay]> where [delay] is msec). In order to have these expanded, use +// TPuts. If the screen is "buffering", the string is collected in a buffer, +// with the intention that the entire buffer be sent to the terminal in one +// write operation at some point later. +func (t *tScreen) writeString(s string) { + if t.buffering { + _, _ = io.WriteString(&t.buf, s) + } else { + _, _ = io.WriteString(t.tty, s) + } +} + +func (t *tScreen) TPuts(s string) { + if t.buffering { + t.ti.TPuts(&t.buf, s) + } else { + t.ti.TPuts(t.tty, s) + } +} + func (t *tScreen) Show() { t.Lock() if !t.fini { @@ -81,6 +903,68 @@ func (t *tScreen) Show() { t.Unlock() } +func (t *tScreen) clearScreen() { + t.TPuts(t.ti.AttrOff) + t.TPuts(t.exitUrl) + fg, bg, _ := t.style.Decompose() + _ = t.sendFgBg(fg, bg, AttrNone) + t.TPuts(t.ti.Clear) + t.clear = false +} + +func (t *tScreen) hideCursor() { + // does not update cursor position + if t.ti.HideCursor != "" { + t.TPuts(t.ti.HideCursor) + } else { + // No way to hide cursor, stick it + // at bottom right of screen + t.cx, t.cy = t.cells.Size() + t.TPuts(t.ti.TGoto(t.cx, t.cy)) + } +} + +func (t *tScreen) draw() { + // clobber cursor position, because we're going to change it all + t.cx = -1 + t.cy = -1 + // make no style assumptions + t.curstyle = styleInvalid + + t.buf.Reset() + t.buffering = true + defer func() { + t.buffering = false + }() + + // hide the cursor while we move stuff around + t.hideCursor() + + if t.clear { + t.clearScreen() + } + + for y := 0; y < t.h; y++ { + for x := 0; x < t.w; x++ { + width := t.drawCell(x, y) + if width > 1 { + if x+1 < t.w { + // this is necessary so that if we ever + // go back to drawing that cell, we + // actually will *draw* it. + t.cells.SetDirty(x+1, y, true) + } + } + x += width - 1 + } + } + + // restore the cursor + t.showCursor() + + _, _ = t.buf.WriteTo(t.tty) +} + func (t *tScreen) EnableMouse(flags ...MouseFlags) { var f MouseFlags flagsPresent := false @@ -98,6 +982,29 @@ func (t *tScreen) EnableMouse(flags ...MouseFlags) { t.Unlock() } +func (t *tScreen) enableMouse(f MouseFlags) { + // Rather than using terminfo to find mouse escape sequences, we rely on the fact that + // pretty much *every* terminal that supports mouse tracking follows the + // XTerm standards (the modern ones). + if len(t.mouse) != 0 { + // start by disabling all tracking. + t.TPuts("\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l") + if f&MouseButtonEvents != 0 { + t.TPuts("\x1b[?1000h") + } + if f&MouseDragEvents != 0 { + t.TPuts("\x1b[?1002h") + } + if f&MouseMotionEvents != 0 { + t.TPuts("\x1b[?1003h") + } + if f&(MouseButtonEvents|MouseDragEvents|MouseMotionEvents) != 0 { + t.TPuts("\x1b[?1006h") + } + } + +} + func (t *tScreen) DisableMouse() { t.Lock() t.mouseFlags = 0 @@ -119,6 +1026,18 @@ func (t *tScreen) DisablePaste() { t.Unlock() } +func (t *tScreen) enablePasting(on bool) { + var s string + if on { + s = t.enablePaste + } else { + s = t.disablePaste + } + if s != "" { + t.TPuts(s) + } +} + func (t *tScreen) Size() (int, int) { t.Lock() w, h := t.w, t.h @@ -126,6 +1045,37 @@ func (t *tScreen) Size() (int, int) { return w, h } +func (t *tScreen) resize() { + if w, h, e := t.tty.WindowSize(); e == nil { + if w != t.w || h != t.h { + t.cx = -1 + t.cy = -1 + + t.cells.Resize(w, h) + t.cells.Invalidate() + t.h = h + t.w = w + ev := NewEventResize(w, h) + _ = t.PostEvent(ev) + } + } +} + +func (t *tScreen) Colors() int { + // this doesn't change, no need for lock + if t.truecolor { + return 1 << 24 + } + return t.ti.Colors +} + +// nColors returns the size of the built-in palette. +// This is distinct from Colors(), as it will generally +// always be a small number. (<= 256) +func (t *tScreen) nColors() int { + return t.ti.Colors +} + func (t *tScreen) ChannelEvents(ch chan<- Event, quit <-chan struct{}) { defer close(ch) for { @@ -159,6 +1109,70 @@ func (t *tScreen) HasPendingEvent() bool { return len(t.evch) > 0 } +// vtACSNames is a map of bytes defined by terminfo that are used in +// the terminals Alternate Character Set to represent other glyphs. +// For example, the upper left corner of the box drawing set can be +// displayed by printing "l" while in the alternate character set. +// It's not quite that simple, since the "l" is the terminfo name, +// and it may be necessary to use a different character based on +// the terminal implementation (or the terminal may lack support for +// this altogether). See buildAcsMap below for detail. +var vtACSNames = map[byte]rune{ + '+': RuneRArrow, + ',': RuneLArrow, + '-': RuneUArrow, + '.': RuneDArrow, + '0': RuneBlock, + '`': RuneDiamond, + 'a': RuneCkBoard, + 'b': '␉', // VT100, Not defined by terminfo + 'c': '␌', // VT100, Not defined by terminfo + 'd': '␋', // VT100, Not defined by terminfo + 'e': '␊', // VT100, Not defined by terminfo + 'f': RuneDegree, + 'g': RunePlMinus, + 'h': RuneBoard, + 'i': RuneLantern, + 'j': RuneLRCorner, + 'k': RuneURCorner, + 'l': RuneULCorner, + 'm': RuneLLCorner, + 'n': RunePlus, + 'o': RuneS1, + 'p': RuneS3, + 'q': RuneHLine, + 'r': RuneS7, + 's': RuneS9, + 't': RuneLTee, + 'u': RuneRTee, + 'v': RuneBTee, + 'w': RuneTTee, + 'x': RuneVLine, + 'y': RuneLEqual, + 'z': RuneGEqual, + '{': RunePi, + '|': RuneNEqual, + '}': RuneSterling, + '~': RuneBullet, +} + +// buildAcsMap builds a map of characters that we translate from Unicode to +// alternate character encodings. To do this, we use the standard VT100 ACS +// maps. This is only done if the terminal lacks support for Unicode; we +// always prefer to emit Unicode glyphs when we are able. +func (t *tScreen) buildAcsMap() { + acsstr := t.ti.AltChars + t.acs = make(map[rune]string) + for len(acsstr) > 2 { + srcv := acsstr[0] + dstv := string(acsstr[1]) + if r, ok := vtACSNames[srcv]; ok { + t.acs[r] = t.ti.EnterAcs + dstv + t.ti.ExitAcs + } + acsstr = acsstr[2:] + } +} + func (t *tScreen) PostEventWait(ev Event) { t.evch <- ev } @@ -189,6 +1203,487 @@ func (t *tScreen) clip(x, y int) (int, int) { return x, y } +// buildMouseEvent returns an event based on the supplied coordinates and button +// state. Note that the screen's mouse button state is updated based on the +// input to this function (i.e. it mutates the receiver). +func (t *tScreen) buildMouseEvent(x, y, btn int) *EventMouse { + + // XTerm mouse events only report at most one button at a time, + // which may include a wheel button. Wheel motion events are + // reported as single impulses, while other button events are reported + // as separate press & release events. + + button := ButtonNone + mod := ModNone + + // Mouse wheel has bit 6 set, no release events. It should be noted + // that wheel events are sometimes misdelivered as mouse button events + // during a click-drag, so we debounce these, considering them to be + // button press events unless we see an intervening release event. + switch btn & 0x43 { + case 0: + button = Button1 + case 1: + button = Button3 // Note we prefer to treat right as button 2 + case 2: + button = Button2 // And the middle button as button 3 + case 3: + button = ButtonNone + case 0x40: + button = WheelUp + case 0x41: + button = WheelDown + } + + if btn&0x4 != 0 { + mod |= ModShift + } + if btn&0x8 != 0 { + mod |= ModAlt + } + if btn&0x10 != 0 { + mod |= ModCtrl + } + + // Some terminals will report mouse coordinates outside the + // screen, especially with click-drag events. Clip the coordinates + // to the screen in that case. + x, y = t.clip(x, y) + + return NewEventMouse(x, y, button, mod) +} + +// parseSgrMouse attempts to locate an SGR mouse record at the start of the +// buffer. It returns true, true if it found one, and the associated bytes +// be removed from the buffer. It returns true, false if the buffer might +// contain such an event, but more bytes are necessary (partial match), and +// false, false if the content is definitely *not* an SGR mouse record. +func (t *tScreen) parseSgrMouse(buf *bytes.Buffer, evs *[]Event) (bool, bool) { + + b := buf.Bytes() + + var x, y, btn, state int + dig := false + neg := false + motion := false + i := 0 + val := 0 + + for i = range b { + switch b[i] { + case '\x1b': + if state != 0 { + return false, false + } + state = 1 + + case '\x9b': + if state != 0 { + return false, false + } + state = 2 + + case '[': + if state != 1 { + return false, false + } + state = 2 + + case '<': + if state != 2 { + return false, false + } + val = 0 + dig = false + neg = false + state = 3 + + case '-': + if state != 3 && state != 4 && state != 5 { + return false, false + } + if dig || neg { + return false, false + } + neg = true // stay in state + + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + if state != 3 && state != 4 && state != 5 { + return false, false + } + val *= 10 + val += int(b[i] - '0') + dig = true // stay in state + + case ';': + if neg { + val = -val + } + switch state { + case 3: + btn, val = val, 0 + neg, dig, state = false, false, 4 + case 4: + x, val = val-1, 0 + neg, dig, state = false, false, 5 + default: + return false, false + } + + case 'm', 'M': + if state != 5 { + return false, false + } + if neg { + val = -val + } + y = val - 1 + + motion = (btn & 32) != 0 + btn &^= 32 + if b[i] == 'm' { + // mouse release, clear all buttons + btn |= 3 + btn &^= 0x40 + t.buttondn = false + } else if motion { + /* + * Some broken terminals appear to send + * mouse button one motion events, instead of + * encoding 35 (no buttons) into these events. + * We resolve these by looking for a non-motion + * event first. + */ + if !t.buttondn { + btn |= 3 + btn &^= 0x40 + } + } else { + t.buttondn = true + } + // consume the event bytes + for i >= 0 { + _, _ = buf.ReadByte() + i-- + } + *evs = append(*evs, t.buildMouseEvent(x, y, btn)) + return true, true + } + } + + // incomplete & inconclusive at this point + return true, false +} + +// parseXtermMouse is like parseSgrMouse, but it parses a legacy +// X11 mouse record. +func (t *tScreen) parseXtermMouse(buf *bytes.Buffer, evs *[]Event) (bool, bool) { + + b := buf.Bytes() + + state := 0 + btn := 0 + x := 0 + y := 0 + + for i := range b { + switch state { + case 0: + switch b[i] { + case '\x1b': + state = 1 + case '\x9b': + state = 2 + default: + return false, false + } + case 1: + if b[i] != '[' { + return false, false + } + state = 2 + case 2: + if b[i] != 'M' { + return false, false + } + state++ + case 3: + btn = int(b[i]) + state++ + case 4: + x = int(b[i]) - 32 - 1 + state++ + case 5: + y = int(b[i]) - 32 - 1 + for i >= 0 { + _, _ = buf.ReadByte() + i-- + } + *evs = append(*evs, t.buildMouseEvent(x, y, btn)) + return true, true + } + } + return true, false +} + +func (t *tScreen) parseFunctionKey(buf *bytes.Buffer, evs *[]Event) (bool, bool) { + b := buf.Bytes() + partial := false + for e, k := range t.keycodes { + esc := []byte(e) + if (len(esc) == 1) && (esc[0] == '\x1b') { + continue + } + if bytes.HasPrefix(b, esc) { + // matched + var r rune + if len(esc) == 1 { + r = rune(b[0]) + } + mod := k.mod + if t.escaped { + mod |= ModAlt + t.escaped = false + } + switch k.key { + case keyPasteStart: + *evs = append(*evs, NewEventPaste(true)) + case keyPasteEnd: + *evs = append(*evs, NewEventPaste(false)) + default: + *evs = append(*evs, NewEventKey(k.key, r, mod)) + } + for i := 0; i < len(esc); i++ { + _, _ = buf.ReadByte() + } + return true, true + } + if bytes.HasPrefix(esc, b) { + partial = true + } + } + return partial, false +} + +func (t *tScreen) parseRune(buf *bytes.Buffer, evs *[]Event) (bool, bool) { + b := buf.Bytes() + if b[0] >= ' ' && b[0] <= 0x7F { + // printable ASCII easy to deal with -- no encodings + mod := ModNone + if t.escaped { + mod = ModAlt + t.escaped = false + } + *evs = append(*evs, NewEventKey(KeyRune, rune(b[0]), mod)) + _, _ = buf.ReadByte() + return true, true + } + + if b[0] < 0x80 { + // Low numbered values are control keys, not runes. + return false, false + } + + utf := make([]byte, 12) + for l := 1; l <= len(b); l++ { + t.decoder.Reset() + nOut, nIn, e := t.decoder.Transform(utf, b[:l], true) + if e == transform.ErrShortSrc { + continue + } + if nOut != 0 { + r, _ := utf8.DecodeRune(utf[:nOut]) + if r != utf8.RuneError { + mod := ModNone + if t.escaped { + mod = ModAlt + t.escaped = false + } + *evs = append(*evs, NewEventKey(KeyRune, r, mod)) + } + for nIn > 0 { + _, _ = buf.ReadByte() + nIn-- + } + return true, true + } + } + // Looks like potential escape + return true, false +} + +func (t *tScreen) scanInput(buf *bytes.Buffer, expire bool) { + evs := t.collectEventsFromInput(buf, expire) + + for _, ev := range evs { + t.PostEventWait(ev) + } +} + +// Return an array of Events extracted from the supplied buffer. This is done +// while holding the screen's lock - the events can then be queued for +// application processing with the lock released. +func (t *tScreen) collectEventsFromInput(buf *bytes.Buffer, expire bool) []Event { + + res := make([]Event, 0, 20) + + t.Lock() + defer t.Unlock() + + for { + b := buf.Bytes() + if len(b) == 0 { + buf.Reset() + return res + } + + partials := 0 + + if part, comp := t.parseRune(buf, &res); comp { + continue + } else if part { + partials++ + } + + if part, comp := t.parseFunctionKey(buf, &res); comp { + continue + } else if part { + partials++ + } + + // Only parse mouse records if this term claims to have + // mouse support + + if t.ti.Mouse != "" { + if part, comp := t.parseXtermMouse(buf, &res); comp { + continue + } else if part { + partials++ + } + + if part, comp := t.parseSgrMouse(buf, &res); comp { + continue + } else if part { + partials++ + } + } + + if partials == 0 || expire { + if b[0] == '\x1b' { + if len(b) == 1 { + res = append(res, NewEventKey(KeyEsc, 0, ModNone)) + t.escaped = false + } else { + t.escaped = true + } + _, _ = buf.ReadByte() + continue + } + // Nothing was going to match, or we timed out + // waiting for more data -- just deliver the characters + // to the app & let them sort it out. Possibly we + // should only do this for control characters like ESC. + by, _ := buf.ReadByte() + mod := ModNone + if t.escaped { + t.escaped = false + mod = ModAlt + } + res = append(res, NewEventKey(KeyRune, rune(by), mod)) + continue + } + + // well we have some partial data, wait until we get + // some more + break + } + + return res +} + +func (t *tScreen) mainLoop(stopQ chan struct{}) { + defer t.wg.Done() + buf := &bytes.Buffer{} + for { + select { + case <-stopQ: + return + case <-t.quit: + return + case <-t.resizeQ: + t.Lock() + t.cx = -1 + t.cy = -1 + t.resize() + t.cells.Invalidate() + t.draw() + t.Unlock() + continue + case <-t.keytimer.C: + // If the timer fired, and the current time + // is after the expiration of the escape sequence, + // then we assume the escape sequence reached its + // conclusion, and process the chunk independently. + // This lets us detect conflicts such as a lone ESC. + if buf.Len() > 0 { + if time.Now().After(t.keyexpire) { + t.scanInput(buf, true) + } + } + if buf.Len() > 0 { + if !t.keytimer.Stop() { + select { + case <-t.keytimer.C: + default: + } + } + t.keytimer.Reset(time.Millisecond * 50) + } + case chunk := <-t.keychan: + buf.Write(chunk) + t.keyexpire = time.Now().Add(time.Millisecond * 50) + t.scanInput(buf, false) + if !t.keytimer.Stop() { + select { + case <-t.keytimer.C: + default: + } + } + if buf.Len() > 0 { + t.keytimer.Reset(time.Millisecond * 50) + } + } + } +} + +func (t *tScreen) inputLoop(stopQ chan struct{}) { + + defer t.wg.Done() + for { + select { + case <-stopQ: + return + default: + } + chunk := make([]byte, 128) + n, e := t.tty.Read(chunk) + switch e { + case nil: + default: + t.Lock() + running := t.running + t.Unlock() + if running { + _ = t.PostEvent(NewEventError(e)) + } + return + } + if n > 0 { + t.keychan <- chunk[:n] + } + } +} + func (t *tScreen) Sync() { t.Lock() t.cx = -1 @@ -202,6 +1697,10 @@ func (t *tScreen) Sync() { t.Unlock() } +func (t *tScreen) CharacterSet() string { + return t.charset +} + func (t *tScreen) RegisterRuneFallback(orig rune, fallback string) { t.Lock() t.fallback[orig] = fallback @@ -214,4 +1713,154 @@ func (t *tScreen) UnregisterRuneFallback(orig rune) { t.Unlock() } +func (t *tScreen) CanDisplay(r rune, checkFallbacks bool) bool { + + if enc := t.encoder; enc != nil { + nb := make([]byte, 6) + ob := make([]byte, 6) + num := utf8.EncodeRune(ob, r) + + enc.Reset() + dst, _, err := enc.Transform(nb, ob[:num], true) + if dst != 0 && err == nil && nb[0] != '\x1A' { + return true + } + } + // Terminal fallbacks always permitted, since we assume they are + // basically nearly perfect renditions. + if _, ok := t.acs[r]; ok { + return true + } + if !checkFallbacks { + return false + } + if _, ok := t.fallback[r]; ok { + return true + } + return false +} + +func (t *tScreen) HasMouse() bool { + return len(t.mouse) != 0 +} + +func (t *tScreen) HasKey(k Key) bool { + if k == KeyRune { + return true + } + return t.keyexist[k] +} + +func (t *tScreen) SetSize(w, h int) { + if t.setWinSize != "" { + t.TPuts(t.ti.TParm(t.setWinSize, w, h)) + } + t.cells.Invalidate() + t.resize() +} + func (t *tScreen) Resize(int, int, int, int) {} + +func (t *tScreen) Suspend() error { + t.disengage() + return nil +} + +func (t *tScreen) Resume() error { + return t.engage() +} + +// engage is used to place the terminal in raw mode and establish screen size, etc. +// Think of this is as tcell "engaging" the clutch, as it's going to be driving the +// terminal interface. +func (t *tScreen) engage() error { + t.Lock() + defer t.Unlock() + if t.tty == nil { + return ErrNoScreen + } + t.tty.NotifyResize(func() { + select { + case t.resizeQ <- true: + default: + } + }) + if t.running { + return errors.New("already engaged") + } + if err := t.tty.Start(); err != nil { + return err + } + t.running = true + if w, h, err := t.tty.WindowSize(); err == nil && w != 0 && h != 0 { + t.cells.Resize(w, h) + } + stopQ := make(chan struct{}) + t.stopQ = stopQ + t.enableMouse(t.mouseFlags) + t.enablePasting(t.pasteEnabled) + + ti := t.ti + t.TPuts(ti.EnterCA) + t.TPuts(ti.EnterKeypad) + t.TPuts(ti.HideCursor) + t.TPuts(ti.EnableAcs) + t.TPuts(ti.Clear) + + t.wg.Add(2) + go t.inputLoop(stopQ) + go t.mainLoop(stopQ) + return nil +} + +// disengage is used to release the terminal back to support from the caller. +// Think of this as tcell disengaging the clutch, so that another application +// can take over the terminal interface. This restores the TTY mode that was +// present when the application was first started. +func (t *tScreen) disengage() { + + t.Lock() + if !t.running { + t.Unlock() + return + } + t.running = false + stopQ := t.stopQ + close(stopQ) + _ = t.tty.Drain() + t.Unlock() + + t.tty.NotifyResize(nil) + // wait for everything to shut down + t.wg.Wait() + + // shutdown the screen and disable special modes (e.g. mouse and bracketed paste) + ti := t.ti + t.cells.Resize(0, 0) + t.TPuts(ti.ShowCursor) + if t.cursorStyles != nil && t.cursorStyle != CursorStyleDefault { + t.TPuts(t.cursorStyles[t.cursorStyle]) + } + t.TPuts(ti.ResetFgBg) + t.TPuts(ti.AttrOff) + t.TPuts(ti.Clear) + t.TPuts(ti.ExitCA) + t.TPuts(ti.ExitKeypad) + t.enableMouse(0) + t.enablePasting(false) + + _ = t.tty.Stop() +} + +// Beep emits a beep to the terminal. +func (t *tScreen) Beep() error { + t.writeString(string(byte(7))) + return nil +} + +// finalize is used to at application shutdown, and restores the terminal +// to it's initial state. It should not be called more than once. +func (t *tScreen) finalize() { + t.disengage() + _ = t.tty.Close() +} diff --git a/tscreen_term.go b/tscreen_term.go deleted file mode 100644 index 09fa771..0000000 --- a/tscreen_term.go +++ /dev/null @@ -1,1665 +0,0 @@ -// Copyright 2023 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 ( - "bytes" - "errors" - "io" - "os" - "strconv" - "strings" - "sync" - "time" - "unicode/utf8" - - "github.com/gdamore/tcell/v2/terminfo" - "golang.org/x/term" - "golang.org/x/text/transform" -) - -// NewTerminfoScreen returns a Screen that uses the stock TTY interface -// and POSIX terminal control, combined with a terminfo description taken from -// the $TERM environment variable. It returns an error if the terminal -// is not supported for any reason. -// -// For terminals that do not support dynamic resize events, the $LINES -// $COLUMNS environment variables can be set to the actual window size, -// otherwise defaults taken from the terminal database are used. -func NewTerminfoScreen() (Screen, error) { - return NewTerminfoScreenFromTty(nil) -} - -// LookupTerminfo attempts to find a definition for the named $TERM falling -// back to attempting to parse the output from infocmp. -func LookupTerminfo(name string) (ti *terminfo.Terminfo, e error) { - ti, e = terminfo.LookupTerminfo(name) - if e != nil { - ti, e = loadDynamicTerminfo(name) - if e != nil { - return nil, e - } - terminfo.AddTerminfo(ti) - } - - return -} - -// NewTerminfoScreenFromTtyTerminfo returns a Screen using a custom Tty -// implementation and custom terminfo specification. -// If the passed in tty is nil, then a reasonable default (typically /dev/tty) -// is presumed, at least on UNIX hosts. (Windows hosts will typically fail this -// call altogether.) -// If passed terminfo is nil, then TERM environment variable is queried for -// terminal specification. -func NewTerminfoScreenFromTtyTerminfo(tty Tty, ti *terminfo.Terminfo) (s Screen, e error) { - if ti == nil { - ti, e = LookupTerminfo(os.Getenv("TERM")) - if e != nil { - return - } - } - - t := &tScreen{ti: ti, tty: tty} - - t.keyexist = make(map[Key]bool) - t.keycodes = make(map[string]*tKeyCode) - if len(ti.Mouse) > 0 { - t.mouse = []byte(ti.Mouse) - } - t.prepareKeys() - t.buildAcsMap() - t.resizeQ = make(chan bool, 1) - t.fallback = make(map[rune]string) - for k, v := range RuneFallbacks { - t.fallback[k] = v - } - - return t, nil -} - -// NewTerminfoScreenFromTty returns a Screen using a custom Tty implementation. -// If the passed in tty is nil, then a reasonable default (typically /dev/tty) -// is presumed, at least on UNIX hosts. (Windows hosts will typically fail this -// call altogether.) -func NewTerminfoScreenFromTty(tty Tty) (Screen, error) { - return NewTerminfoScreenFromTtyTerminfo(tty, nil) -} - -// tScreen represents a screen backed by a terminfo implementation. -type tScreen struct { - ti *terminfo.Terminfo - tty Tty - h int - w int - fini bool - cells CellBuffer - buffering bool // true if we are collecting writes to buf instead of sending directly to out - buf bytes.Buffer - curstyle Style - style Style - evch chan Event - resizeQ chan bool - quit chan struct{} - keyexist map[Key]bool - keycodes map[string]*tKeyCode - keychan chan []byte - keytimer *time.Timer - keyexpire time.Time - cx int - cy int - mouse []byte - clear bool - cursorx int - cursory int - acs map[rune]string - charset string - encoder transform.Transformer - decoder transform.Transformer - fallback map[rune]string - colors map[Color]Color - palette []Color - truecolor bool - escaped bool - buttondn bool - finiOnce sync.Once - enablePaste string - disablePaste string - enterUrl string - exitUrl string - setWinSize string - cursorStyles map[CursorStyle]string - cursorStyle CursorStyle - saved *term.State - stopQ chan struct{} - running bool - wg sync.WaitGroup - mouseFlags MouseFlags - pasteEnabled bool - - sync.Mutex -} - -func (t *tScreen) Init() error { - if e := t.initialize(); e != nil { - return e - } - - t.evch = make(chan Event, 10) - t.keychan = make(chan []byte, 10) - t.keytimer = time.NewTimer(time.Millisecond * 50) - t.charset = "UTF-8" - - t.charset = getCharset() - if enc := GetEncoding(t.charset); enc != nil { - t.encoder = enc.NewEncoder() - t.decoder = enc.NewDecoder() - } else { - return ErrNoCharset - } - ti := t.ti - - // environment overrides - w := ti.Columns - h := ti.Lines - if i, _ := strconv.Atoi(os.Getenv("LINES")); i != 0 { - h = i - } - if i, _ := strconv.Atoi(os.Getenv("COLUMNS")); i != 0 { - w = i - } - if t.ti.SetFgBgRGB != "" || t.ti.SetFgRGB != "" || t.ti.SetBgRGB != "" { - t.truecolor = true - } - // A user who wants to have his themes honored can - // set this environment variable. - if os.Getenv("TCELL_TRUECOLOR") == "disable" { - t.truecolor = false - } - nColors := t.nColors() - if nColors > 256 { - nColors = 256 // clip to reasonable limits - } - t.colors = make(map[Color]Color, nColors) - t.palette = make([]Color, nColors) - for i := 0; i < nColors; i++ { - t.palette[i] = Color(i) | ColorValid - // identity map for our builtin colors - t.colors[Color(i)|ColorValid] = Color(i) | ColorValid - } - - t.quit = make(chan struct{}) - - t.Lock() - t.cx = -1 - t.cy = -1 - t.style = StyleDefault - t.cells.Resize(w, h) - t.cursorx = -1 - t.cursory = -1 - t.resize() - t.Unlock() - - if err := t.engage(); err != nil { - return err - } - - return nil -} - -func (t *tScreen) prepareKeyMod(key Key, mod ModMask, val string) { - if val != "" { - // Do not override codes that already exist - if _, exist := t.keycodes[val]; !exist { - t.keyexist[key] = true - t.keycodes[val] = &tKeyCode{key: key, mod: mod} - } - } -} - -func (t *tScreen) prepareKeyModReplace(key Key, replace Key, mod ModMask, val string) { - if val != "" { - // Do not override codes that already exist - if old, exist := t.keycodes[val]; !exist || old.key == replace { - t.keyexist[key] = true - t.keycodes[val] = &tKeyCode{key: key, mod: mod} - } - } -} - -func (t *tScreen) prepareKeyModXTerm(key Key, val string) { - - if strings.HasPrefix(val, "\x1b[") && strings.HasSuffix(val, "~") { - - // Drop the trailing ~ - val = val[:len(val)-1] - - // These suffixes are calculated assuming Xterm style modifier suffixes. - // Please see https://invisible-island.net/xterm/ctlseqs/ctlseqs.pdf for - // more information (specifically "PC-Style Function Keys"). - t.prepareKeyModReplace(key, key+12, ModShift, val+";2~") - t.prepareKeyModReplace(key, key+48, ModAlt, val+";3~") - t.prepareKeyModReplace(key, key+60, ModAlt|ModShift, val+";4~") - t.prepareKeyModReplace(key, key+24, ModCtrl, val+";5~") - t.prepareKeyModReplace(key, key+36, ModCtrl|ModShift, val+";6~") - t.prepareKeyMod(key, ModAlt|ModCtrl, val+";7~") - t.prepareKeyMod(key, ModShift|ModAlt|ModCtrl, val+";8~") - t.prepareKeyMod(key, ModMeta, val+";9~") - t.prepareKeyMod(key, ModMeta|ModShift, val+";10~") - t.prepareKeyMod(key, ModMeta|ModAlt, val+";11~") - t.prepareKeyMod(key, ModMeta|ModAlt|ModShift, val+";12~") - t.prepareKeyMod(key, ModMeta|ModCtrl, val+";13~") - t.prepareKeyMod(key, ModMeta|ModCtrl|ModShift, val+";14~") - t.prepareKeyMod(key, ModMeta|ModCtrl|ModAlt, val+";15~") - t.prepareKeyMod(key, ModMeta|ModCtrl|ModAlt|ModShift, val+";16~") - } else if strings.HasPrefix(val, "\x1bO") && len(val) == 3 { - val = val[2:] - t.prepareKeyModReplace(key, key+12, ModShift, "\x1b[1;2"+val) - t.prepareKeyModReplace(key, key+48, ModAlt, "\x1b[1;3"+val) - t.prepareKeyModReplace(key, key+24, ModCtrl, "\x1b[1;5"+val) - t.prepareKeyModReplace(key, key+36, ModCtrl|ModShift, "\x1b[1;6"+val) - t.prepareKeyModReplace(key, key+60, ModAlt|ModShift, "\x1b[1;4"+val) - t.prepareKeyMod(key, ModAlt|ModCtrl, "\x1b[1;7"+val) - t.prepareKeyMod(key, ModShift|ModAlt|ModCtrl, "\x1b[1;8"+val) - t.prepareKeyMod(key, ModMeta, "\x1b[1;9"+val) - t.prepareKeyMod(key, ModMeta|ModShift, "\x1b[1;10"+val) - t.prepareKeyMod(key, ModMeta|ModAlt, "\x1b[1;11"+val) - t.prepareKeyMod(key, ModMeta|ModAlt|ModShift, "\x1b[1;12"+val) - t.prepareKeyMod(key, ModMeta|ModCtrl, "\x1b[1;13"+val) - t.prepareKeyMod(key, ModMeta|ModCtrl|ModShift, "\x1b[1;14"+val) - t.prepareKeyMod(key, ModMeta|ModCtrl|ModAlt, "\x1b[1;15"+val) - t.prepareKeyMod(key, ModMeta|ModCtrl|ModAlt|ModShift, "\x1b[1;16"+val) - } -} - -func (t *tScreen) prepareXtermModifiers() { - if t.ti.Modifiers != terminfo.ModifiersXTerm { - return - } - t.prepareKeyModXTerm(KeyRight, t.ti.KeyRight) - t.prepareKeyModXTerm(KeyLeft, t.ti.KeyLeft) - t.prepareKeyModXTerm(KeyUp, t.ti.KeyUp) - t.prepareKeyModXTerm(KeyDown, t.ti.KeyDown) - t.prepareKeyModXTerm(KeyInsert, t.ti.KeyInsert) - t.prepareKeyModXTerm(KeyDelete, t.ti.KeyDelete) - t.prepareKeyModXTerm(KeyPgUp, t.ti.KeyPgUp) - t.prepareKeyModXTerm(KeyPgDn, t.ti.KeyPgDn) - t.prepareKeyModXTerm(KeyHome, t.ti.KeyHome) - t.prepareKeyModXTerm(KeyEnd, t.ti.KeyEnd) - t.prepareKeyModXTerm(KeyF1, t.ti.KeyF1) - t.prepareKeyModXTerm(KeyF2, t.ti.KeyF2) - t.prepareKeyModXTerm(KeyF3, t.ti.KeyF3) - t.prepareKeyModXTerm(KeyF4, t.ti.KeyF4) - t.prepareKeyModXTerm(KeyF5, t.ti.KeyF5) - t.prepareKeyModXTerm(KeyF6, t.ti.KeyF6) - t.prepareKeyModXTerm(KeyF7, t.ti.KeyF7) - t.prepareKeyModXTerm(KeyF8, t.ti.KeyF8) - t.prepareKeyModXTerm(KeyF9, t.ti.KeyF9) - t.prepareKeyModXTerm(KeyF10, t.ti.KeyF10) - t.prepareKeyModXTerm(KeyF11, t.ti.KeyF11) - t.prepareKeyModXTerm(KeyF12, t.ti.KeyF12) -} - -func (t *tScreen) prepareBracketedPaste() { - // Another workaround for lack of reporting in terminfo. - // We assume if the terminal has a mouse entry, that it - // offers bracketed paste. But we allow specific overrides - // via our terminal database. - if t.ti.EnablePaste != "" { - t.enablePaste = t.ti.EnablePaste - t.disablePaste = t.ti.DisablePaste - t.prepareKey(keyPasteStart, t.ti.PasteStart) - t.prepareKey(keyPasteEnd, t.ti.PasteEnd) - } else if t.ti.Mouse != "" { - t.enablePaste = "\x1b[?2004h" - t.disablePaste = "\x1b[?2004l" - t.prepareKey(keyPasteStart, "\x1b[200~") - t.prepareKey(keyPasteEnd, "\x1b[201~") - } -} - -func (t *tScreen) prepareExtendedOSC() { - // More stuff for limits in terminfo. This time we are applying - // the most common OSC (operating system commands). Generally - // terminals that don't understand these will ignore them. - // Again, we condition this based on mouse capabilities. - if t.ti.EnterUrl != "" { - t.enterUrl = t.ti.EnterUrl - t.exitUrl = t.ti.ExitUrl - } else if t.ti.Mouse != "" { - t.enterUrl = "\x1b]8;%p2%s;%p1%s\x1b\\" - t.exitUrl = "\x1b]8;;\x1b\\" - } - - if t.ti.SetWindowSize != "" { - t.setWinSize = t.ti.SetWindowSize - } else if t.ti.Mouse != "" { - t.setWinSize = "\x1b[8;%p1%p2%d;%dt" - } -} - -func (t *tScreen) prepareCursorStyles() { - // Another workaround for lack of reporting in terminfo. - // We assume if the terminal has a mouse entry, that it - // offers bracketed paste. But we allow specific overrides - // via our terminal database. - if t.ti.CursorDefault != "" { - t.cursorStyles = map[CursorStyle]string{ - CursorStyleDefault: t.ti.CursorDefault, - CursorStyleBlinkingBlock: t.ti.CursorBlinkingBlock, - CursorStyleSteadyBlock: t.ti.CursorSteadyBlock, - CursorStyleBlinkingUnderline: t.ti.CursorBlinkingUnderline, - CursorStyleSteadyUnderline: t.ti.CursorSteadyUnderline, - CursorStyleBlinkingBar: t.ti.CursorBlinkingBar, - CursorStyleSteadyBar: t.ti.CursorSteadyBar, - } - } else if t.ti.Mouse != "" { - t.cursorStyles = map[CursorStyle]string{ - CursorStyleDefault: "\x1b[0 q", - CursorStyleBlinkingBlock: "\x1b[1 q", - CursorStyleSteadyBlock: "\x1b[2 q", - CursorStyleBlinkingUnderline: "\x1b[3 q", - CursorStyleSteadyUnderline: "\x1b[4 q", - CursorStyleBlinkingBar: "\x1b[5 q", - CursorStyleSteadyBar: "\x1b[6 q", - } - } -} - -func (t *tScreen) prepareKey(key Key, val string) { - t.prepareKeyMod(key, ModNone, val) -} - -func (t *tScreen) prepareKeys() { - ti := t.ti - t.prepareKey(KeyBackspace, ti.KeyBackspace) - t.prepareKey(KeyF1, ti.KeyF1) - t.prepareKey(KeyF2, ti.KeyF2) - t.prepareKey(KeyF3, ti.KeyF3) - t.prepareKey(KeyF4, ti.KeyF4) - t.prepareKey(KeyF5, ti.KeyF5) - t.prepareKey(KeyF6, ti.KeyF6) - t.prepareKey(KeyF7, ti.KeyF7) - t.prepareKey(KeyF8, ti.KeyF8) - t.prepareKey(KeyF9, ti.KeyF9) - t.prepareKey(KeyF10, ti.KeyF10) - t.prepareKey(KeyF11, ti.KeyF11) - t.prepareKey(KeyF12, ti.KeyF12) - t.prepareKey(KeyF13, ti.KeyF13) - t.prepareKey(KeyF14, ti.KeyF14) - t.prepareKey(KeyF15, ti.KeyF15) - t.prepareKey(KeyF16, ti.KeyF16) - t.prepareKey(KeyF17, ti.KeyF17) - t.prepareKey(KeyF18, ti.KeyF18) - t.prepareKey(KeyF19, ti.KeyF19) - t.prepareKey(KeyF20, ti.KeyF20) - t.prepareKey(KeyF21, ti.KeyF21) - t.prepareKey(KeyF22, ti.KeyF22) - t.prepareKey(KeyF23, ti.KeyF23) - t.prepareKey(KeyF24, ti.KeyF24) - t.prepareKey(KeyF25, ti.KeyF25) - t.prepareKey(KeyF26, ti.KeyF26) - t.prepareKey(KeyF27, ti.KeyF27) - t.prepareKey(KeyF28, ti.KeyF28) - t.prepareKey(KeyF29, ti.KeyF29) - t.prepareKey(KeyF30, ti.KeyF30) - t.prepareKey(KeyF31, ti.KeyF31) - t.prepareKey(KeyF32, ti.KeyF32) - t.prepareKey(KeyF33, ti.KeyF33) - t.prepareKey(KeyF34, ti.KeyF34) - t.prepareKey(KeyF35, ti.KeyF35) - t.prepareKey(KeyF36, ti.KeyF36) - t.prepareKey(KeyF37, ti.KeyF37) - t.prepareKey(KeyF38, ti.KeyF38) - t.prepareKey(KeyF39, ti.KeyF39) - t.prepareKey(KeyF40, ti.KeyF40) - t.prepareKey(KeyF41, ti.KeyF41) - t.prepareKey(KeyF42, ti.KeyF42) - t.prepareKey(KeyF43, ti.KeyF43) - t.prepareKey(KeyF44, ti.KeyF44) - t.prepareKey(KeyF45, ti.KeyF45) - t.prepareKey(KeyF46, ti.KeyF46) - t.prepareKey(KeyF47, ti.KeyF47) - t.prepareKey(KeyF48, ti.KeyF48) - t.prepareKey(KeyF49, ti.KeyF49) - t.prepareKey(KeyF50, ti.KeyF50) - t.prepareKey(KeyF51, ti.KeyF51) - t.prepareKey(KeyF52, ti.KeyF52) - t.prepareKey(KeyF53, ti.KeyF53) - t.prepareKey(KeyF54, ti.KeyF54) - t.prepareKey(KeyF55, ti.KeyF55) - t.prepareKey(KeyF56, ti.KeyF56) - t.prepareKey(KeyF57, ti.KeyF57) - t.prepareKey(KeyF58, ti.KeyF58) - t.prepareKey(KeyF59, ti.KeyF59) - t.prepareKey(KeyF60, ti.KeyF60) - t.prepareKey(KeyF61, ti.KeyF61) - t.prepareKey(KeyF62, ti.KeyF62) - t.prepareKey(KeyF63, ti.KeyF63) - t.prepareKey(KeyF64, ti.KeyF64) - t.prepareKey(KeyInsert, ti.KeyInsert) - t.prepareKey(KeyDelete, ti.KeyDelete) - t.prepareKey(KeyHome, ti.KeyHome) - t.prepareKey(KeyEnd, ti.KeyEnd) - t.prepareKey(KeyUp, ti.KeyUp) - t.prepareKey(KeyDown, ti.KeyDown) - t.prepareKey(KeyLeft, ti.KeyLeft) - t.prepareKey(KeyRight, ti.KeyRight) - t.prepareKey(KeyPgUp, ti.KeyPgUp) - t.prepareKey(KeyPgDn, ti.KeyPgDn) - t.prepareKey(KeyHelp, ti.KeyHelp) - t.prepareKey(KeyPrint, ti.KeyPrint) - t.prepareKey(KeyCancel, ti.KeyCancel) - t.prepareKey(KeyExit, ti.KeyExit) - t.prepareKey(KeyBacktab, ti.KeyBacktab) - - t.prepareKeyMod(KeyRight, ModShift, ti.KeyShfRight) - t.prepareKeyMod(KeyLeft, ModShift, ti.KeyShfLeft) - t.prepareKeyMod(KeyUp, ModShift, ti.KeyShfUp) - t.prepareKeyMod(KeyDown, ModShift, ti.KeyShfDown) - t.prepareKeyMod(KeyHome, ModShift, ti.KeyShfHome) - t.prepareKeyMod(KeyEnd, ModShift, ti.KeyShfEnd) - t.prepareKeyMod(KeyPgUp, ModShift, ti.KeyShfPgUp) - t.prepareKeyMod(KeyPgDn, ModShift, ti.KeyShfPgDn) - - t.prepareKeyMod(KeyRight, ModCtrl, ti.KeyCtrlRight) - t.prepareKeyMod(KeyLeft, ModCtrl, ti.KeyCtrlLeft) - t.prepareKeyMod(KeyUp, ModCtrl, ti.KeyCtrlUp) - t.prepareKeyMod(KeyDown, ModCtrl, ti.KeyCtrlDown) - t.prepareKeyMod(KeyHome, ModCtrl, ti.KeyCtrlHome) - t.prepareKeyMod(KeyEnd, ModCtrl, ti.KeyCtrlEnd) - - // Sadly, xterm handling of keycodes is somewhat erratic. In - // particular, different codes are sent depending on application - // mode is in use or not, and the entries for many of these are - // simply absent from terminfo on many systems. So we insert - // a number of escape sequences if they are not already used, in - // order to have the widest correct usage. Note that prepareKey - // will not inject codes if the escape sequence is already known. - // We also only do this for terminals that have the application - // mode present. - - // Cursor mode - if ti.EnterKeypad != "" { - t.prepareKey(KeyUp, "\x1b[A") - t.prepareKey(KeyDown, "\x1b[B") - t.prepareKey(KeyRight, "\x1b[C") - t.prepareKey(KeyLeft, "\x1b[D") - t.prepareKey(KeyEnd, "\x1b[F") - t.prepareKey(KeyHome, "\x1b[H") - t.prepareKey(KeyDelete, "\x1b[3~") - t.prepareKey(KeyHome, "\x1b[1~") - t.prepareKey(KeyEnd, "\x1b[4~") - t.prepareKey(KeyPgUp, "\x1b[5~") - t.prepareKey(KeyPgDn, "\x1b[6~") - - // Application mode - t.prepareKey(KeyUp, "\x1bOA") - t.prepareKey(KeyDown, "\x1bOB") - t.prepareKey(KeyRight, "\x1bOC") - t.prepareKey(KeyLeft, "\x1bOD") - t.prepareKey(KeyHome, "\x1bOH") - } - - t.prepareKey(keyPasteStart, ti.PasteStart) - t.prepareKey(keyPasteEnd, ti.PasteEnd) - t.prepareXtermModifiers() - t.prepareBracketedPaste() - t.prepareCursorStyles() - t.prepareExtendedOSC() - -outer: - // Add key mappings for control keys. - for i := 0; i < ' '; i++ { - // Do not insert direct key codes for ambiguous keys. - // For example, ESC is used for lots of other keys, so - // when parsing this we don't want to fast path handling - // of it, but instead wait a bit before parsing it as in - // isolation. - for esc := range t.keycodes { - if []byte(esc)[0] == byte(i) { - continue outer - } - } - - t.keyexist[Key(i)] = true - - mod := ModCtrl - switch Key(i) { - case KeyBS, KeyTAB, KeyESC, KeyCR: - // directly type-able- no control sequence - mod = ModNone - } - t.keycodes[string(rune(i))] = &tKeyCode{key: Key(i), mod: mod} - } -} - -func (t *tScreen) Fini() { - t.finiOnce.Do(t.finish) -} - -func (t *tScreen) finish() { - close(t.quit) - t.finalize() -} - -func (t *tScreen) encodeRune(r rune, buf []byte) []byte { - - nb := make([]byte, 6) - ob := make([]byte, 6) - num := utf8.EncodeRune(ob, r) - ob = ob[:num] - dst := 0 - var err error - if enc := t.encoder; enc != nil { - enc.Reset() - dst, _, err = enc.Transform(nb, ob, true) - } - if err != nil || dst == 0 || nb[0] == '\x1a' { - // Combining characters are elided - if len(buf) == 0 { - if acs, ok := t.acs[r]; ok { - buf = append(buf, []byte(acs)...) - } else if fb, ok := t.fallback[r]; ok { - buf = append(buf, []byte(fb)...) - } else { - buf = append(buf, '?') - } - } - } else { - buf = append(buf, nb[:dst]...) - } - - return buf -} - -func (t *tScreen) sendFgBg(fg Color, bg Color, attr AttrMask) AttrMask { - ti := t.ti - if ti.Colors == 0 { - // foreground vs background, we calculate luminance - // and possibly do a reverse video - if !fg.Valid() { - return attr - } - v, ok := t.colors[fg] - if !ok { - v = FindColor(fg, []Color{ColorBlack, ColorWhite}) - t.colors[fg] = v - } - switch v { - case ColorWhite: - return attr - case ColorBlack: - return attr ^ AttrReverse - } - } - - if fg == ColorReset || bg == ColorReset { - t.TPuts(ti.ResetFgBg) - } - if t.truecolor { - if ti.SetFgBgRGB != "" && fg.IsRGB() && bg.IsRGB() { - r1, g1, b1 := fg.RGB() - r2, g2, b2 := bg.RGB() - t.TPuts(ti.TParm(ti.SetFgBgRGB, - int(r1), int(g1), int(b1), - int(r2), int(g2), int(b2))) - return attr - } - - if fg.IsRGB() && ti.SetFgRGB != "" { - r, g, b := fg.RGB() - t.TPuts(ti.TParm(ti.SetFgRGB, int(r), int(g), int(b))) - fg = ColorDefault - } - - if bg.IsRGB() && ti.SetBgRGB != "" { - r, g, b := bg.RGB() - t.TPuts(ti.TParm(ti.SetBgRGB, - int(r), int(g), int(b))) - bg = ColorDefault - } - } - - if fg.Valid() { - if v, ok := t.colors[fg]; ok { - fg = v - } else { - v = FindColor(fg, t.palette) - t.colors[fg] = v - fg = v - } - } - - if bg.Valid() { - if v, ok := t.colors[bg]; ok { - bg = v - } else { - v = FindColor(bg, t.palette) - t.colors[bg] = v - bg = v - } - } - - if fg.Valid() && bg.Valid() && ti.SetFgBg != "" { - t.TPuts(ti.TParm(ti.SetFgBg, int(fg&0xff), int(bg&0xff))) - } else { - if fg.Valid() && ti.SetFg != "" { - t.TPuts(ti.TParm(ti.SetFg, int(fg&0xff))) - } - if bg.Valid() && ti.SetBg != "" { - t.TPuts(ti.TParm(ti.SetBg, int(bg&0xff))) - } - } - return attr -} - -func (t *tScreen) drawCell(x, y int) int { - - ti := t.ti - - mainc, combc, style, width := t.cells.GetContent(x, y) - if !t.cells.Dirty(x, y) { - return width - } - - if y == t.h-1 && x == t.w-1 && t.ti.AutoMargin && ti.InsertChar != "" { - // our solution is somewhat goofy. - // we write to the second to the last cell what we want in the last cell, then we - // insert a character at that 2nd to last position to shift the last column into - // place, then we rewrite that 2nd to last cell. Old terminals suck. - t.TPuts(ti.TGoto(x-1, y)) - defer func() { - t.TPuts(ti.TGoto(x-1, y)) - t.TPuts(ti.InsertChar) - t.cy = y - t.cx = x - 1 - t.cells.SetDirty(x-1, y, true) - _ = t.drawCell(x-1, y) - t.TPuts(t.ti.TGoto(0, 0)) - t.cy = 0 - t.cx = 0 - }() - } else if t.cy != y || t.cx != x { - t.TPuts(ti.TGoto(x, y)) - t.cx = x - t.cy = y - } - - if style == StyleDefault { - style = t.style - } - if style != t.curstyle { - fg, bg, attrs := style.Decompose() - - t.TPuts(ti.AttrOff) - - attrs = t.sendFgBg(fg, bg, attrs) - if attrs&AttrBold != 0 { - t.TPuts(ti.Bold) - } - if attrs&AttrUnderline != 0 { - t.TPuts(ti.Underline) - } - if attrs&AttrReverse != 0 { - t.TPuts(ti.Reverse) - } - if attrs&AttrBlink != 0 { - t.TPuts(ti.Blink) - } - if attrs&AttrDim != 0 { - t.TPuts(ti.Dim) - } - if attrs&AttrItalic != 0 { - t.TPuts(ti.Italic) - } - if attrs&AttrStrikeThrough != 0 { - t.TPuts(ti.StrikeThrough) - } - - // URL string can be long, so don't send it unless we really need to - if t.enterUrl != "" && t.curstyle != style { - if style.url != "" { - t.TPuts(ti.TParm(t.enterUrl, style.url, style.urlId)) - } else { - t.TPuts(t.exitUrl) - } - } - - t.curstyle = style - } - - // now emit runes - taking care to not overrun width with a - // wide character, and to ensure that we emit exactly one regular - // character followed up by any residual combing characters - - if width < 1 { - width = 1 - } - - var str string - - buf := make([]byte, 0, 6) - - buf = t.encodeRune(mainc, buf) - for _, r := range combc { - buf = t.encodeRune(r, buf) - } - - str = string(buf) - if width > 1 && str == "?" { - // No FullWidth character support - str = "? " - t.cx = -1 - } - - if x > t.w-width { - // too wide to fit; emit a single space instead - width = 1 - str = " " - } - t.writeString(str) - t.cx += width - t.cells.SetDirty(x, y, false) - if width > 1 { - t.cx = -1 - } - - return width -} - -func (t *tScreen) ShowCursor(x, y int) { - t.Lock() - t.cursorx = x - t.cursory = y - t.Unlock() -} - -func (t *tScreen) SetCursorStyle(cs CursorStyle) { - t.Lock() - t.cursorStyle = cs - t.Unlock() -} - -func (t *tScreen) showCursor() { - - x, y := t.cursorx, t.cursory - w, h := t.cells.Size() - if x < 0 || y < 0 || x >= w || y >= h { - t.hideCursor() - return - } - t.TPuts(t.ti.TGoto(x, y)) - t.TPuts(t.ti.ShowCursor) - if t.cursorStyles != nil { - if esc, ok := t.cursorStyles[t.cursorStyle]; ok { - t.TPuts(esc) - } - } - t.cx = x - t.cy = y -} - -// writeString sends a string to the terminal. The string is sent as-is and -// this function does not expand inline padding indications (of the form -// $<[delay]> where [delay] is msec). In order to have these expanded, use -// TPuts. If the screen is "buffering", the string is collected in a buffer, -// with the intention that the entire buffer be sent to the terminal in one -// write operation at some point later. -func (t *tScreen) writeString(s string) { - if t.buffering { - _, _ = io.WriteString(&t.buf, s) - } else { - _, _ = io.WriteString(t.tty, s) - } -} - -func (t *tScreen) TPuts(s string) { - if t.buffering { - t.ti.TPuts(&t.buf, s) - } else { - t.ti.TPuts(t.tty, s) - } -} - -func (t *tScreen) clearScreen() { - t.TPuts(t.ti.AttrOff) - t.TPuts(t.exitUrl) - fg, bg, _ := t.style.Decompose() - _ = t.sendFgBg(fg, bg, AttrNone) - t.TPuts(t.ti.Clear) - t.clear = false -} - -func (t *tScreen) hideCursor() { - // does not update cursor position - if t.ti.HideCursor != "" { - t.TPuts(t.ti.HideCursor) - } else { - // No way to hide cursor, stick it - // at bottom right of screen - t.cx, t.cy = t.cells.Size() - t.TPuts(t.ti.TGoto(t.cx, t.cy)) - } -} - -func (t *tScreen) draw() { - // clobber cursor position, because we're going to change it all - t.cx = -1 - t.cy = -1 - // make no style assumptions - t.curstyle = styleInvalid - - t.buf.Reset() - t.buffering = true - defer func() { - t.buffering = false - }() - - // hide the cursor while we move stuff around - t.hideCursor() - - if t.clear { - t.clearScreen() - } - - for y := 0; y < t.h; y++ { - for x := 0; x < t.w; x++ { - width := t.drawCell(x, y) - if width > 1 { - if x+1 < t.w { - // this is necessary so that if we ever - // go back to drawing that cell, we - // actually will *draw* it. - t.cells.SetDirty(x+1, y, true) - } - } - x += width - 1 - } - } - - // restore the cursor - t.showCursor() - - _, _ = t.buf.WriteTo(t.tty) -} - -func (t *tScreen) enableMouse(f MouseFlags) { - // Rather than using terminfo to find mouse escape sequences, we rely on the fact that - // pretty much *every* terminal that supports mouse tracking follows the - // XTerm standards (the modern ones). - if len(t.mouse) != 0 { - // start by disabling all tracking. - t.TPuts("\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l") - if f&MouseButtonEvents != 0 { - t.TPuts("\x1b[?1000h") - } - if f&MouseDragEvents != 0 { - t.TPuts("\x1b[?1002h") - } - if f&MouseMotionEvents != 0 { - t.TPuts("\x1b[?1003h") - } - if f&(MouseButtonEvents|MouseDragEvents|MouseMotionEvents) != 0 { - t.TPuts("\x1b[?1006h") - } - } - -} - -func (t *tScreen) enablePasting(on bool) { - var s string - if on { - s = t.enablePaste - } else { - s = t.disablePaste - } - if s != "" { - t.TPuts(s) - } -} - -func (t *tScreen) resize() { - if w, h, e := t.tty.WindowSize(); e == nil { - if w != t.w || h != t.h { - t.cx = -1 - t.cy = -1 - - t.cells.Resize(w, h) - t.cells.Invalidate() - t.h = h - t.w = w - ev := NewEventResize(w, h) - _ = t.PostEvent(ev) - } - } -} - -func (t *tScreen) Colors() int { - // this doesn't change, no need for lock - if t.truecolor { - return 1 << 24 - } - return t.ti.Colors -} - -// nColors returns the size of the built-in palette. -// This is distinct from Colors(), as it will generally -// always be a small number. (<= 256) -func (t *tScreen) nColors() int { - return t.ti.Colors -} - -// vtACSNames is a map of bytes defined by terminfo that are used in -// the terminals Alternate Character Set to represent other glyphs. -// For example, the upper left corner of the box drawing set can be -// displayed by printing "l" while in the alternate character set. -// It's not quite that simple, since the "l" is the terminfo name, -// and it may be necessary to use a different character based on -// the terminal implementation (or the terminal may lack support for -// this altogether). See buildAcsMap below for detail. -var vtACSNames = map[byte]rune{ - '+': RuneRArrow, - ',': RuneLArrow, - '-': RuneUArrow, - '.': RuneDArrow, - '0': RuneBlock, - '`': RuneDiamond, - 'a': RuneCkBoard, - 'b': '␉', // VT100, Not defined by terminfo - 'c': '␌', // VT100, Not defined by terminfo - 'd': '␋', // VT100, Not defined by terminfo - 'e': '␊', // VT100, Not defined by terminfo - 'f': RuneDegree, - 'g': RunePlMinus, - 'h': RuneBoard, - 'i': RuneLantern, - 'j': RuneLRCorner, - 'k': RuneURCorner, - 'l': RuneULCorner, - 'm': RuneLLCorner, - 'n': RunePlus, - 'o': RuneS1, - 'p': RuneS3, - 'q': RuneHLine, - 'r': RuneS7, - 's': RuneS9, - 't': RuneLTee, - 'u': RuneRTee, - 'v': RuneBTee, - 'w': RuneTTee, - 'x': RuneVLine, - 'y': RuneLEqual, - 'z': RuneGEqual, - '{': RunePi, - '|': RuneNEqual, - '}': RuneSterling, - '~': RuneBullet, -} - -// buildAcsMap builds a map of characters that we translate from Unicode to -// alternate character encodings. To do this, we use the standard VT100 ACS -// maps. This is only done if the terminal lacks support for Unicode; we -// always prefer to emit Unicode glyphs when we are able. -func (t *tScreen) buildAcsMap() { - acsstr := t.ti.AltChars - t.acs = make(map[rune]string) - for len(acsstr) > 2 { - srcv := acsstr[0] - dstv := string(acsstr[1]) - if r, ok := vtACSNames[srcv]; ok { - t.acs[r] = t.ti.EnterAcs + dstv + t.ti.ExitAcs - } - acsstr = acsstr[2:] - } -} - -// buildMouseEvent returns an event based on the supplied coordinates and button -// state. Note that the screen's mouse button state is updated based on the -// input to this function (i.e. it mutates the receiver). -func (t *tScreen) buildMouseEvent(x, y, btn int) *EventMouse { - - // XTerm mouse events only report at most one button at a time, - // which may include a wheel button. Wheel motion events are - // reported as single impulses, while other button events are reported - // as separate press & release events. - - button := ButtonNone - mod := ModNone - - // Mouse wheel has bit 6 set, no release events. It should be noted - // that wheel events are sometimes misdelivered as mouse button events - // during a click-drag, so we debounce these, considering them to be - // button press events unless we see an intervening release event. - switch btn & 0x43 { - case 0: - button = Button1 - case 1: - button = Button3 // Note we prefer to treat right as button 2 - case 2: - button = Button2 // And the middle button as button 3 - case 3: - button = ButtonNone - case 0x40: - button = WheelUp - case 0x41: - button = WheelDown - } - - if btn&0x4 != 0 { - mod |= ModShift - } - if btn&0x8 != 0 { - mod |= ModAlt - } - if btn&0x10 != 0 { - mod |= ModCtrl - } - - // Some terminals will report mouse coordinates outside the - // screen, especially with click-drag events. Clip the coordinates - // to the screen in that case. - x, y = t.clip(x, y) - - return NewEventMouse(x, y, button, mod) -} - -// parseSgrMouse attempts to locate an SGR mouse record at the start of the -// buffer. It returns true, true if it found one, and the associated bytes -// be removed from the buffer. It returns true, false if the buffer might -// contain such an event, but more bytes are necessary (partial match), and -// false, false if the content is definitely *not* an SGR mouse record. -func (t *tScreen) parseSgrMouse(buf *bytes.Buffer, evs *[]Event) (bool, bool) { - - b := buf.Bytes() - - var x, y, btn, state int - dig := false - neg := false - motion := false - i := 0 - val := 0 - - for i = range b { - switch b[i] { - case '\x1b': - if state != 0 { - return false, false - } - state = 1 - - case '\x9b': - if state != 0 { - return false, false - } - state = 2 - - case '[': - if state != 1 { - return false, false - } - state = 2 - - case '<': - if state != 2 { - return false, false - } - val = 0 - dig = false - neg = false - state = 3 - - case '-': - if state != 3 && state != 4 && state != 5 { - return false, false - } - if dig || neg { - return false, false - } - neg = true // stay in state - - case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': - if state != 3 && state != 4 && state != 5 { - return false, false - } - val *= 10 - val += int(b[i] - '0') - dig = true // stay in state - - case ';': - if neg { - val = -val - } - switch state { - case 3: - btn, val = val, 0 - neg, dig, state = false, false, 4 - case 4: - x, val = val-1, 0 - neg, dig, state = false, false, 5 - default: - return false, false - } - - case 'm', 'M': - if state != 5 { - return false, false - } - if neg { - val = -val - } - y = val - 1 - - motion = (btn & 32) != 0 - btn &^= 32 - if b[i] == 'm' { - // mouse release, clear all buttons - btn |= 3 - btn &^= 0x40 - t.buttondn = false - } else if motion { - // - // Some broken terminals appear to send - // mouse button one motion events, instead of - // encoding 35 (no buttons) into these events. - // We resolve these by looking for a non-motion - // event first. - - if !t.buttondn { - btn |= 3 - btn &^= 0x40 - } - } else { - t.buttondn = true - } - // consume the event bytes - for i >= 0 { - _, _ = buf.ReadByte() - i-- - } - *evs = append(*evs, t.buildMouseEvent(x, y, btn)) - return true, true - } - } - - // incomplete & inconclusive at this point - return true, false -} - -// parseXtermMouse is like parseSgrMouse, but it parses a legacy -// X11 mouse record. -func (t *tScreen) parseXtermMouse(buf *bytes.Buffer, evs *[]Event) (bool, bool) { - - b := buf.Bytes() - - state := 0 - btn := 0 - x := 0 - y := 0 - - for i := range b { - switch state { - case 0: - switch b[i] { - case '\x1b': - state = 1 - case '\x9b': - state = 2 - default: - return false, false - } - case 1: - if b[i] != '[' { - return false, false - } - state = 2 - case 2: - if b[i] != 'M' { - return false, false - } - state++ - case 3: - btn = int(b[i]) - state++ - case 4: - x = int(b[i]) - 32 - 1 - state++ - case 5: - y = int(b[i]) - 32 - 1 - for i >= 0 { - _, _ = buf.ReadByte() - i-- - } - *evs = append(*evs, t.buildMouseEvent(x, y, btn)) - return true, true - } - } - return true, false -} - -func (t *tScreen) parseFunctionKey(buf *bytes.Buffer, evs *[]Event) (bool, bool) { - b := buf.Bytes() - partial := false - for e, k := range t.keycodes { - esc := []byte(e) - if (len(esc) == 1) && (esc[0] == '\x1b') { - continue - } - if bytes.HasPrefix(b, esc) { - // matched - var r rune - if len(esc) == 1 { - r = rune(b[0]) - } - mod := k.mod - if t.escaped { - mod |= ModAlt - t.escaped = false - } - switch k.key { - case keyPasteStart: - *evs = append(*evs, NewEventPaste(true)) - case keyPasteEnd: - *evs = append(*evs, NewEventPaste(false)) - default: - *evs = append(*evs, NewEventKey(k.key, r, mod)) - } - for i := 0; i < len(esc); i++ { - _, _ = buf.ReadByte() - } - return true, true - } - if bytes.HasPrefix(esc, b) { - partial = true - } - } - return partial, false -} - -func (t *tScreen) parseRune(buf *bytes.Buffer, evs *[]Event) (bool, bool) { - b := buf.Bytes() - if b[0] >= ' ' && b[0] <= 0x7F { - // printable ASCII easy to deal with -- no encodings - mod := ModNone - if t.escaped { - mod = ModAlt - t.escaped = false - } - *evs = append(*evs, NewEventKey(KeyRune, rune(b[0]), mod)) - _, _ = buf.ReadByte() - return true, true - } - - if b[0] < 0x80 { - // Low numbered values are control keys, not runes. - return false, false - } - - utf := make([]byte, 12) - for l := 1; l <= len(b); l++ { - t.decoder.Reset() - nOut, nIn, e := t.decoder.Transform(utf, b[:l], true) - if e == transform.ErrShortSrc { - continue - } - if nOut != 0 { - r, _ := utf8.DecodeRune(utf[:nOut]) - if r != utf8.RuneError { - mod := ModNone - if t.escaped { - mod = ModAlt - t.escaped = false - } - *evs = append(*evs, NewEventKey(KeyRune, r, mod)) - } - for nIn > 0 { - _, _ = buf.ReadByte() - nIn-- - } - return true, true - } - } - // Looks like potential escape - return true, false -} - -func (t *tScreen) scanInput(buf *bytes.Buffer, expire bool) { - evs := t.collectEventsFromInput(buf, expire) - - for _, ev := range evs { - t.PostEventWait(ev) - } -} - -// Return an array of Events extracted from the supplied buffer. This is done -// while holding the screen's lock - the events can then be queued for -// application processing with the lock released. -func (t *tScreen) collectEventsFromInput(buf *bytes.Buffer, expire bool) []Event { - - res := make([]Event, 0, 20) - - t.Lock() - defer t.Unlock() - - for { - b := buf.Bytes() - if len(b) == 0 { - buf.Reset() - return res - } - - partials := 0 - - if part, comp := t.parseRune(buf, &res); comp { - continue - } else if part { - partials++ - } - - if part, comp := t.parseFunctionKey(buf, &res); comp { - continue - } else if part { - partials++ - } - - // Only parse mouse records if this term claims to have - // mouse support - - if t.ti.Mouse != "" { - if part, comp := t.parseXtermMouse(buf, &res); comp { - continue - } else if part { - partials++ - } - - if part, comp := t.parseSgrMouse(buf, &res); comp { - continue - } else if part { - partials++ - } - } - - if partials == 0 || expire { - if b[0] == '\x1b' { - if len(b) == 1 { - res = append(res, NewEventKey(KeyEsc, 0, ModNone)) - t.escaped = false - } else { - t.escaped = true - } - _, _ = buf.ReadByte() - continue - } - // Nothing was going to match, or we timed out - // waiting for more data -- just deliver the characters - // to the app & let them sort it out. Possibly we - // should only do this for control characters like ESC. - by, _ := buf.ReadByte() - mod := ModNone - if t.escaped { - t.escaped = false - mod = ModAlt - } - res = append(res, NewEventKey(KeyRune, rune(by), mod)) - continue - } - - // well we have some partial data, wait until we get - // some more - break - } - - return res -} - -func (t *tScreen) mainLoop(stopQ chan struct{}) { - defer t.wg.Done() - buf := &bytes.Buffer{} - for { - select { - case <-stopQ: - return - case <-t.quit: - return - case <-t.resizeQ: - t.Lock() - t.cx = -1 - t.cy = -1 - t.resize() - t.cells.Invalidate() - t.draw() - t.Unlock() - continue - case <-t.keytimer.C: - // If the timer fired, and the current time - // is after the expiration of the escape sequence, - // then we assume the escape sequence reached its - // conclusion, and process the chunk independently. - // This lets us detect conflicts such as a lone ESC. - if buf.Len() > 0 { - if time.Now().After(t.keyexpire) { - t.scanInput(buf, true) - } - } - if buf.Len() > 0 { - if !t.keytimer.Stop() { - select { - case <-t.keytimer.C: - default: - } - } - t.keytimer.Reset(time.Millisecond * 50) - } - case chunk := <-t.keychan: - buf.Write(chunk) - t.keyexpire = time.Now().Add(time.Millisecond * 50) - t.scanInput(buf, false) - if !t.keytimer.Stop() { - select { - case <-t.keytimer.C: - default: - } - } - if buf.Len() > 0 { - t.keytimer.Reset(time.Millisecond * 50) - } - } - } -} - -func (t *tScreen) inputLoop(stopQ chan struct{}) { - - defer t.wg.Done() - for { - select { - case <-stopQ: - return - default: - } - chunk := make([]byte, 128) - n, e := t.tty.Read(chunk) - switch e { - case nil: - default: - t.Lock() - running := t.running - t.Unlock() - if running { - _ = t.PostEvent(NewEventError(e)) - } - return - } - if n > 0 { - t.keychan <- chunk[:n] - } - } -} - -func (t *tScreen) CharacterSet() string { - return t.charset -} - -func (t *tScreen) CanDisplay(r rune, checkFallbacks bool) bool { - - if enc := t.encoder; enc != nil { - nb := make([]byte, 6) - ob := make([]byte, 6) - num := utf8.EncodeRune(ob, r) - - enc.Reset() - dst, _, err := enc.Transform(nb, ob[:num], true) - if dst != 0 && err == nil && nb[0] != '\x1A' { - return true - } - } - // Terminal fallbacks always permitted, since we assume they are - // basically nearly perfect renditions. - if _, ok := t.acs[r]; ok { - return true - } - if !checkFallbacks { - return false - } - if _, ok := t.fallback[r]; ok { - return true - } - return false -} - -func (t *tScreen) HasMouse() bool { - return len(t.mouse) != 0 -} - -func (t *tScreen) HasKey(k Key) bool { - if k == KeyRune { - return true - } - return t.keyexist[k] -} - -func (t *tScreen) SetSize(w, h int) { - if t.setWinSize != "" { - t.TPuts(t.ti.TParm(t.setWinSize, w, h)) - } - t.cells.Invalidate() - t.resize() -} - -func (t *tScreen) Suspend() error { - t.disengage() - return nil -} - -func (t *tScreen) Resume() error { - return t.engage() -} - -// engage is used to place the terminal in raw mode and establish screen size, etc. -// Think of this is as tcell "engaging" the clutch, as it's going to be driving the -// terminal interface. -func (t *tScreen) engage() error { - t.Lock() - defer t.Unlock() - if t.tty == nil { - return ErrNoScreen - } - t.tty.NotifyResize(func() { - select { - case t.resizeQ <- true: - default: - } - }) - if t.running { - return errors.New("already engaged") - } - if err := t.tty.Start(); err != nil { - return err - } - t.running = true - if w, h, err := t.tty.WindowSize(); err == nil && w != 0 && h != 0 { - t.cells.Resize(w, h) - } - stopQ := make(chan struct{}) - t.stopQ = stopQ - t.enableMouse(t.mouseFlags) - t.enablePasting(t.pasteEnabled) - - ti := t.ti - t.TPuts(ti.EnterCA) - t.TPuts(ti.EnterKeypad) - t.TPuts(ti.HideCursor) - t.TPuts(ti.EnableAcs) - t.TPuts(ti.Clear) - - t.wg.Add(2) - go t.inputLoop(stopQ) - go t.mainLoop(stopQ) - return nil -} - -// disengage is used to release the terminal back to support from the caller. -// Think of this as tcell disengaging the clutch, so that another application -// can take over the terminal interface. This restores the TTY mode that was -// present when the application was first started. -func (t *tScreen) disengage() { - - t.Lock() - if !t.running { - t.Unlock() - return - } - t.running = false - stopQ := t.stopQ - close(stopQ) - _ = t.tty.Drain() - t.Unlock() - - t.tty.NotifyResize(nil) - // wait for everything to shut down - t.wg.Wait() - - // shutdown the screen and disable special modes (e.g. mouse and bracketed paste) - ti := t.ti - t.cells.Resize(0, 0) - t.TPuts(ti.ShowCursor) - if t.cursorStyles != nil && t.cursorStyle != CursorStyleDefault { - t.TPuts(t.cursorStyles[t.cursorStyle]) - } - t.TPuts(ti.ResetFgBg) - t.TPuts(ti.AttrOff) - t.TPuts(ti.Clear) - t.TPuts(ti.ExitCA) - t.TPuts(ti.ExitKeypad) - t.enableMouse(0) - t.enablePasting(false) - - _ = t.tty.Stop() -} - -// Beep emits a beep to the terminal. -func (t *tScreen) Beep() error { - t.writeString(string(byte(7))) - return nil -} - -// finalize is used to at application shutdown, and restores the terminal -// to it's initial state. It should not be called more than once. -func (t *tScreen) finalize() { - t.disengage() - _ = t.tty.Close() -} diff --git a/webfiles/tcell.js b/webfiles/tcell.js index 8d81555..cd48dd8 100644 --- a/webfiles/tcell.js +++ b/webfiles/tcell.js @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +const wasmFilePath = "main.wasm" const term = document.getElementById("terminal") var width = 80; var height = 24 const beepAudio = new Audio("beep.wav"); @@ -192,6 +193,6 @@ document.addEventListener("paste", e => { }); const go = new Go(); -WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => { +WebAssembly.instantiateStreaming(fetch(wasmFilePath), go.importObject).then((result) => { go.run(result.instance); }); \ No newline at end of file diff --git a/tscreen_web.go b/wscreen.go similarity index 73% rename from tscreen_web.go rename to wscreen.go index a34e70b..de6cd22 100644 --- a/tscreen_web.go +++ b/wscreen.go @@ -26,26 +26,24 @@ import ( ) func NewTerminfoScreen() (Screen, error) { - t := &tScreen{} + t := &wScreen{} t.fallback = make(map[rune]string) return t, nil } -type tScreen struct { +type wScreen struct { w, h int style Style cells CellBuffer running bool - fini bool // dummy var; html doesn't need to get restored or "shut down" clear bool flagsPresent bool pasteEnabled bool mouseFlags MouseFlags cursorStyle CursorStyle - cx, cy int // dummies so web tScreen can use generic tScreen.Sync quit chan struct{} evch chan Event @@ -54,14 +52,13 @@ type tScreen struct { sync.Mutex } -func (t *tScreen) Init() error { +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.cx, t.cy = -1, -1 t.style = StyleDefault t.cells.Resize(t.w, t.h) t.Unlock() @@ -71,11 +68,48 @@ func (t *tScreen) Init() error { return nil } -func (t *tScreen) Fini() { +func (t *wScreen) Fini() { close(t.quit) } -func (t *tScreen) drawCell(x, y int) int { +func (t *wScreen) SetStyle(style Style) { + t.Lock() + t.style = style + t.Unlock() +} + +func (t *wScreen) Clear() { + t.Fill(' ', t.style) +} + +func (t *wScreen) Fill(r rune, style Style) { + t.Lock() + t.cells.Fill(r, style) + t.Unlock() +} + +func (t *wScreen) SetContent(x, y int, mainc rune, combc []rune, style Style) { + t.Lock() + t.cells.SetContent(x, y, mainc, combc, style) + t.Unlock() +} + +func (t *wScreen) GetContent(x, y int) (rune, []rune, Style, int) { + t.Lock() + mainc, combc, style, width := t.cells.GetContent(x, y) + t.Unlock() + return mainc, combc, style, width +} + +func (t *wScreen) SetCell(x, y int, style Style, ch ...rune) { + if len(ch) > 0 { + t.SetContent(x, y, ch[0], ch[1:], style) + } else { + t.SetContent(x, y, ' ', nil, style) + } +} + +func (t *wScreen) drawCell(x, y int) int { mainc, combc, style, width := t.cells.GetContent(x, y) if !t.cells.Dirty(x, y) { @@ -99,24 +133,35 @@ func (t *tScreen) drawCell(x, y int) int { return width } -func (t *tScreen) ShowCursor(x, y int) { +func (t *wScreen) ShowCursor(x, y int) { t.Lock() js.Global().Call("showCursor", x, y) t.Unlock() } -func (t *tScreen) SetCursorStyle(cs CursorStyle) { +func (t *wScreen) SetCursorStyle(cs CursorStyle) { t.Lock() js.Global().Call("setCursorStyle", curStyleClasses[cs]) t.Unlock() } -func (t *tScreen) clearScreen() { +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 *tScreen) draw() { +func (t *wScreen) draw() { if t.clear { t.clearScreen() } @@ -131,7 +176,24 @@ func (t *tScreen) draw() { js.Global().Call("show") } -func (t *tScreen) enableMouse(f MouseFlags) { +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 { @@ -145,7 +207,28 @@ func (t *tScreen) enableMouse(f MouseFlags) { } } -func (t *tScreen) enablePasting(on bool) { +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 { @@ -153,15 +236,85 @@ func (t *tScreen) enablePasting(on bool) { } } +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 *tScreen) resize() {} +func (t *wScreen) resize() {} -func (t *tScreen) Colors() int { +func (t *wScreen) Colors() int { return 16777216 // 256 ^ 3 } -func (t *tScreen) onMouseEvent(this js.Value, args []js.Value) interface{} { +func (t *wScreen) ChannelEvents(ch chan<- Event, quit <-chan struct{}) { + defer close(ch) + for { + select { + case <-quit: + return + case <-t.quit: + return + case ev := <-t.evch: + select { + case <-quit: + return + case <-t.quit: + return + case ch <- ev: + } + } + } +} + +func (t *wScreen) PollEvent() Event { + select { + case <-t.quit: + return nil + case ev := <-t.evch: + return ev + } +} + +func (t *wScreen) HasPendingEvent() bool { + return len(t.evch) > 0 +} + +func (t *wScreen) PostEventWait(ev Event) { + t.evch <- ev +} + +func (t *wScreen) PostEvent(ev Event) error { + select { + case t.evch <- ev: + return nil + default: + return ErrEventQFull + } +} + +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) onMouseEvent(this js.Value, args []js.Value) interface{} { mod := ModNone button := ButtonNone @@ -196,7 +349,7 @@ func (t *tScreen) onMouseEvent(this js.Value, args []js.Value) interface{} { return nil } -func (t *tScreen) onKeyEvent(this js.Value, args []js.Value) interface{} { +func (t *wScreen) onKeyEvent(this js.Value, args []js.Value) interface{} { key := args[0].String() // don't accept any modifier keys as their own @@ -241,7 +394,7 @@ func (t *tScreen) onKeyEvent(this js.Value, args []js.Value) interface{} { return nil } -func (t *tScreen) onPaste(this js.Value, args []js.Value) interface{} { +func (t *wScreen) onPaste(this js.Value, args []js.Value) interface{} { t.PostEventWait(NewEventPaste(args[0].Bool())) return nil } @@ -250,15 +403,36 @@ func (t *tScreen) onPaste(this js.Value, args []js.Value) interface{} { // 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 *tScreen) unset(this js.Value, args []js.Value) interface{} { +func (t *wScreen) unset(this js.Value, args []js.Value) interface{} { return nil } -func (t *tScreen) CharacterSet() string { +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 *tScreen) CanDisplay(r rune, checkFallbacks bool) bool { +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 } @@ -271,15 +445,15 @@ func (t *tScreen) CanDisplay(r rune, checkFallbacks bool) bool { return false } -func (t *tScreen) HasMouse() bool { +func (t *wScreen) HasMouse() bool { return true } -func (t *tScreen) HasKey(k Key) bool { +func (t *wScreen) HasKey(k Key) bool { return true } -func (t *tScreen) SetSize(w, h int) { +func (t *wScreen) SetSize(w, h int) { if w == t.w && h == t.h { return } @@ -291,9 +465,11 @@ func (t *tScreen) SetSize(w, h int) { 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 *tScreen) Suspend() error { +func (t *wScreen) Suspend() error { t.Lock() if !t.running { t.Unlock() @@ -307,7 +483,7 @@ func (t *tScreen) Suspend() error { return nil } -func (t *tScreen) Resume() error { +func (t *wScreen) Resume() error { t.Lock() if t.running { @@ -324,7 +500,7 @@ func (t *tScreen) Resume() error { return nil } -func (t *tScreen) Beep() error { +func (t *wScreen) Beep() error { js.Global().Call("beep") return nil }