// Copyright 2024 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" "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 &baseScreen{screenImpl: 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 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 enableFocus string disableFocus string cursorStyles map[CursorStyle]string cursorStyle CursorStyle saved *term.State stopQ chan struct{} eventQ chan Event running bool wg sync.WaitGroup mouseFlags MouseFlags pasteEnabled bool focusEnabled bool sync.Mutex } func (t *tScreen) Init() error { if e := t.initialize(); e != nil { return e } 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.eventQ = make(chan Event, 10) 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() { // Linux is a special beast - because it has a mouse entry, but does // not swallow these OSC commands properly. if strings.Contains(t.ti.Name, "linux") { return } // 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" } if t.ti.EnableFocusReporting != "" { t.enableFocus = t.ti.EnableFocusReporting } else if t.ti.Mouse != "" { t.enableFocus = "\x1b[?1004h" } if t.ti.DisableFocusReporting != "" { t.disableFocus = t.ti.DisableFocusReporting } else if t.ti.Mouse != "" { t.disableFocus = "\x1b[?1004l" } } 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 { t.style = style } t.Unlock() } 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.DisableAutoMargin == "" && 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 { t.resize() t.draw() } 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 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 *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 t.enableMouse(0) t.Unlock() } func (t *tScreen) EnablePaste() { t.Lock() t.pasteEnabled = true t.enablePasting(true) t.Unlock() } func (t *tScreen) DisablePaste() { t.Lock() t.pasteEnabled = false t.enablePasting(false) 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) EnableFocus() { t.Lock() t.focusEnabled = true t.enableFocusReporting() t.Unlock() } func (t *tScreen) DisableFocus() { t.Lock() t.focusEnabled = false t.disableFocusReporting() t.Unlock() } func (t *tScreen) enableFocusReporting() { if t.enableFocus != "" { t.TPuts(t.enableFocus) } } func (t *tScreen) disableFocusReporting() { if t.disableFocus != "" { t.TPuts(t.disableFocus) } } func (t *tScreen) Size() (int, int) { t.Lock() w, h := t.w, t.h t.Unlock() return w, h } func (t *tScreen) resize() { ws, err := t.tty.WindowSize() if err != nil { return } if ws.Width == t.w && ws.Height == t.h { return } t.cx = -1 t.cy = -1 t.cells.Resize(ws.Width, ws.Height) t.cells.Invalidate() t.h = ws.Height t.w = ws.Width ev := &EventResize{t: time.Now(), ws: ws} select { case t.eventQ <- ev: default: } } 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:] } } func (t *tScreen) 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 } // 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 scroll := 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 scroll = (btn & 0x42) == 0x40 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 if !scroll { 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 } func (t *tScreen) parseFocus(buf *bytes.Buffer, evs *[]Event) (bool, bool) { state := 0 b := buf.Bytes() for i := range b { switch state { case 0: if b[i] != '\x1b' { return false, false } state = 1 case 1: if b[i] != '[' { return false, false } state = 2 case 2: if b[i] != 'I' && b[i] != 'O' { return false, false } *evs = append(*evs, NewEventFocus(b[i] == 'I')) _, _ = buf.ReadByte() _, _ = buf.ReadByte() _, _ = buf.ReadByte() return true, true } } 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 { select { case t.eventQ <- ev: case <-t.quit: return } } } // 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++ } if part, comp := t.parseFocus(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 { select { case t.eventQ <- NewEventError(e): case <-t.quit: } } return } if n > 0 { t.keychan <- chunk[:n] } } } func (t *tScreen) Sync() { t.Lock() t.cx = -1 t.cy = -1 if !t.fini { t.resize() t.clear = true t.cells.Invalidate() t.draw() } t.Unlock() } func (t *tScreen) CharacterSet() string { return t.charset } func (t *tScreen) RegisterRuneFallback(orig rune, fallback string) { t.Lock() t.fallback[orig] = fallback t.Unlock() } func (t *tScreen) UnregisterRuneFallback(orig rune) { t.Lock() delete(t.fallback, orig) 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() } func (t *tScreen) Tty() (Tty, bool) { return t.tty, true } // 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 ws, err := t.tty.WindowSize(); err == nil && ws.Width != 0 && ws.Height != 0 { t.cells.Resize(ws.Width, ws.Height) } stopQ := make(chan struct{}) t.stopQ = stopQ t.enableMouse(t.mouseFlags) t.enablePasting(t.pasteEnabled) if t.focusEnabled { t.enableFocusReporting() } ti := t.ti if os.Getenv("TCELL_ALTSCREEN") != "disable" { // Technically this may not be right, but every terminal we know about // (even Wyse 60) uses this to enter the alternate screen buffer, and // possibly save and restore the window title and/or icon. // (In theory there could be terminals that don't support X,Y cursor // positions without a setup command, but we don't support them.) t.TPuts(ti.EnterCA) } t.TPuts(ti.EnterKeypad) t.TPuts(ti.HideCursor) t.TPuts(ti.EnableAcs) t.TPuts(ti.DisableAutoMargin) 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[CursorStyleDefault]) } t.TPuts(ti.ResetFgBg) t.TPuts(ti.AttrOff) t.TPuts(ti.ExitKeypad) t.TPuts(ti.EnableAutoMargin) if os.Getenv("TCELL_ALTSCREEN") != "disable" { t.TPuts(ti.Clear) // only needed if ExitCA is empty t.TPuts(ti.ExitCA) } t.enableMouse(0) t.enablePasting(false) t.disableFocusReporting() _ = 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() } func (t *tScreen) StopQ() <-chan struct{} { return t.quit } func (t *tScreen) EventQ() chan Event { return t.eventQ } func (t *tScreen) GetCells() *CellBuffer { return &t.cells }