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
}