mirror of
https://github.com/gdamore/tcell.git
synced 2025-04-27 13:48:50 +08:00
473 lines
9.4 KiB
Go
473 lines
9.4 KiB
Go
// Copyright 2015 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.
|
|
|
|
package tcell
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"os"
|
|
"strconv"
|
|
"sync"
|
|
"unicode/utf8"
|
|
|
|
"github.com/mattn/go-runewidth"
|
|
)
|
|
|
|
func NewTerminfoScreen() (Screen, error) {
|
|
ti, e := LookupTerminfo(os.Getenv("TERM"))
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
t := &tScreen{ti: ti}
|
|
|
|
t.keys = make(map[Key][]byte)
|
|
if len(ti.Mouse) > 0 {
|
|
t.mouse = []byte(ti.Mouse)
|
|
}
|
|
t.prepareKeys()
|
|
t.w = ti.Columns
|
|
t.h = ti.Lines
|
|
t.sigwinch = make(chan os.Signal, 1)
|
|
// environment overrides
|
|
if i, _ := strconv.Atoi(os.Getenv("LINES")); i != 0 {
|
|
t.h = i
|
|
}
|
|
if i, _ := strconv.Atoi(os.Getenv("COLUMNS")); i != 0 {
|
|
t.w = i
|
|
}
|
|
|
|
return t, nil
|
|
}
|
|
|
|
// tScreen represents a screen backed by a terminfo implementation.
|
|
type tScreen struct {
|
|
ti *Terminfo
|
|
w int
|
|
h int
|
|
in *os.File
|
|
out *os.File
|
|
cinvis bool
|
|
curstyle Style
|
|
evch chan Event
|
|
sigwinch chan os.Signal
|
|
quit chan struct{}
|
|
keys map[Key][]byte
|
|
cx int
|
|
cy int
|
|
mouse []byte
|
|
|
|
sync.Mutex
|
|
}
|
|
|
|
func (t *tScreen) Init() error {
|
|
t.evch = make(chan Event, 2)
|
|
if e := t.termioInit(); e != nil {
|
|
return e
|
|
}
|
|
|
|
out := t.out
|
|
ti := t.ti
|
|
|
|
io.WriteString(out, ti.EnterCA)
|
|
io.WriteString(out, ti.EnterKeypad)
|
|
io.WriteString(out, ti.HideCursor)
|
|
io.WriteString(out, ti.Clear)
|
|
|
|
t.quit = make(chan struct{})
|
|
t.cx = -1
|
|
t.cy = -1
|
|
|
|
go t.inputLoop()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (t *tScreen) prepareKey(key Key, val string) {
|
|
if val != "" {
|
|
t.keys[key] = []byte(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(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)
|
|
}
|
|
|
|
func (t *tScreen) Fini() {
|
|
ti := t.ti
|
|
out := t.out
|
|
io.WriteString(out, ti.ShowCursor)
|
|
io.WriteString(out, ti.AttrOff)
|
|
io.WriteString(out, ti.Clear)
|
|
io.WriteString(out, ti.ExitCA)
|
|
io.WriteString(out, ti.ExitKeypad)
|
|
io.WriteString(out, ti.ExitMouse)
|
|
|
|
t.w = 0
|
|
t.h = 0
|
|
t.curstyle = Style(-1)
|
|
t.cinvis = false
|
|
if t.quit != nil {
|
|
close(t.quit)
|
|
} else {
|
|
t.termioFini()
|
|
}
|
|
}
|
|
|
|
func (t *tScreen) Clear() {
|
|
return
|
|
t.Lock()
|
|
t.curstyle = Style(-1)
|
|
t.cx = -1
|
|
t.cy = -1
|
|
io.WriteString(t.out, t.ti.Clear)
|
|
t.Unlock()
|
|
}
|
|
|
|
func (t *tScreen) SetCell(x, y int, style Style, ch ...rune) {
|
|
// XXX: this would be a place to check for hazeltine not being able
|
|
// to display ~, or possibly non-UTF-8 locales, etc.
|
|
|
|
t.Lock()
|
|
if x < 0 || y < 0 || x >= t.w || y >= t.h {
|
|
t.Unlock()
|
|
return
|
|
}
|
|
ti := t.ti
|
|
|
|
if t.cy != y || t.cx != x {
|
|
io.WriteString(t.out, ti.TGoto(x, y))
|
|
}
|
|
if style != t.curstyle {
|
|
fg, bg, attrs := style.Decompose()
|
|
|
|
io.WriteString(t.out, ti.AttrOff)
|
|
if attrs&AttrBold != 0 {
|
|
io.WriteString(t.out, ti.Bold)
|
|
}
|
|
if attrs&AttrUnderline != 0 {
|
|
io.WriteString(t.out, ti.Underline)
|
|
}
|
|
if attrs&AttrReverse != 0 {
|
|
io.WriteString(t.out, ti.Reverse)
|
|
}
|
|
if attrs&AttrBlink != 0 {
|
|
io.WriteString(t.out, ti.Blink)
|
|
}
|
|
if attrs&AttrDim != 0 {
|
|
io.WriteString(t.out, ti.Dim)
|
|
}
|
|
if fg != ColorDefault {
|
|
c := int(fg) - 1
|
|
io.WriteString(t.out, ti.TParm(ti.SetFg, c))
|
|
}
|
|
if bg != ColorDefault {
|
|
c := int(bg) - 1
|
|
io.WriteString(t.out, ti.TParm(ti.SetBg, c))
|
|
}
|
|
t.curstyle = style
|
|
}
|
|
// now emit a character - 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
|
|
mainc := ' '
|
|
combc := ""
|
|
width := 1
|
|
|
|
for _, c := range ch {
|
|
if c < ' ' {
|
|
// no control charcters allowed
|
|
continue
|
|
}
|
|
switch runewidth.RuneWidth(c) {
|
|
case 0:
|
|
combc = combc + string(c)
|
|
case 1:
|
|
mainc = c
|
|
width = 1
|
|
case 2:
|
|
mainc = c
|
|
width = 2
|
|
if x >= t.w-1 {
|
|
// too wide to fit; emit space instead
|
|
mainc = ' '
|
|
width = 1
|
|
}
|
|
}
|
|
}
|
|
io.WriteString(t.out, string(mainc))
|
|
io.WriteString(t.out, combc)
|
|
t.cy = y
|
|
t.cx = x + width
|
|
t.Unlock()
|
|
}
|
|
|
|
func (t *tScreen) ShowCursor(x, y int) {
|
|
t.Lock()
|
|
if x < 0 || y < 0 || x >= t.w || y >= t.h {
|
|
t.cinvis = true
|
|
io.WriteString(t.out, t.ti.HideCursor)
|
|
t.Unlock()
|
|
return
|
|
}
|
|
if t.cx != x || t.cy != y {
|
|
io.WriteString(t.out, t.ti.TGoto(x, y))
|
|
}
|
|
io.WriteString(t.out, t.ti.ShowCursor)
|
|
|
|
t.cinvis = false
|
|
t.cx = x
|
|
t.cy = y
|
|
t.Unlock()
|
|
}
|
|
|
|
func (t *tScreen) HideCursor() {
|
|
t.ShowCursor(-1, -1)
|
|
}
|
|
|
|
func (t *tScreen) EnableMouse() {
|
|
if len(t.mouse) != 0 {
|
|
io.WriteString(t.out, t.ti.EnterMouse)
|
|
}
|
|
}
|
|
|
|
func (t *tScreen) DisableMouse() {
|
|
if len(t.mouse) != 0 {
|
|
io.WriteString(t.out, t.ti.ExitMouse)
|
|
}
|
|
}
|
|
|
|
func (t *tScreen) Size() (int, int) {
|
|
// XXX: get underlying size
|
|
t.Lock()
|
|
w, h := t.w, t.h
|
|
t.Unlock()
|
|
return w, h
|
|
}
|
|
|
|
func (t *tScreen) resize() {
|
|
var ev Event
|
|
t.Lock()
|
|
if w, h, e := t.getWinSize(); e == nil {
|
|
if w != t.w || h != t.h {
|
|
ev = NewEventResize(w, h)
|
|
t.w = w
|
|
t.h = h
|
|
t.cx = -1
|
|
t.cy = -1
|
|
}
|
|
}
|
|
t.Unlock()
|
|
if ev != nil {
|
|
t.PostEvent(ev)
|
|
}
|
|
}
|
|
|
|
func (t *tScreen) Colors() int {
|
|
// this doesn't change, no need for lock
|
|
return t.ti.Colors
|
|
}
|
|
|
|
func (t *tScreen) PollEvent() Event {
|
|
select {
|
|
case <-t.quit:
|
|
return nil
|
|
case ev := <-t.evch:
|
|
return ev
|
|
}
|
|
}
|
|
|
|
func (t *tScreen) PostEvent(ev Event) {
|
|
t.evch <- ev
|
|
}
|
|
|
|
func (t *tScreen) scanInput(buf *bytes.Buffer, expire bool) {
|
|
|
|
for {
|
|
b := buf.Bytes()
|
|
if len(b) == 0 {
|
|
buf.Reset()
|
|
return
|
|
}
|
|
if b[0] >= ' ' && b[0] <= 0x7F {
|
|
// printable ASCII easy to deal with -- no encodings
|
|
buf.ReadByte()
|
|
ev := NewEventKey(KeyRune, rune(b[0]), ModNone)
|
|
t.PostEvent(ev)
|
|
continue
|
|
}
|
|
// We assume that the first character of any terminal escape
|
|
// sequence will be in ASCII -- most often (by far) it is ESC.
|
|
if b[0] >= 0x80 && utf8.FullRune(b) {
|
|
r, _, e := buf.ReadRune()
|
|
if e == nil {
|
|
ev := NewEventKey(KeyRune, r, ModNone)
|
|
t.PostEvent(ev)
|
|
continue
|
|
}
|
|
}
|
|
// Now check the codes we know about
|
|
partials := 0
|
|
matched := false
|
|
for k, esc := range t.keys {
|
|
if bytes.HasPrefix(b, esc) {
|
|
// matched
|
|
var r rune
|
|
if len(esc) == 1 {
|
|
r = rune(b[0])
|
|
}
|
|
ev := NewEventKey(k, r, ModNone)
|
|
t.PostEvent(ev)
|
|
matched = true
|
|
for i := 0; i < len(esc); i++ {
|
|
buf.ReadByte()
|
|
}
|
|
break
|
|
}
|
|
if bytes.HasPrefix(esc, b) {
|
|
partials++
|
|
}
|
|
}
|
|
|
|
// Mouse events are special, as they carry parameters
|
|
if !matched && len(t.mouse) != 0 &&
|
|
bytes.HasPrefix(b, t.mouse) {
|
|
|
|
if len(b) >= len(t.mouse)+3 {
|
|
// mouse record
|
|
b = b[len(t.mouse):]
|
|
btns := ButtonNone
|
|
mod := ModNone
|
|
switch b[0] & 3 {
|
|
case 0:
|
|
btns = Button1
|
|
case 1:
|
|
btns = Button2
|
|
case 2:
|
|
btns = Button3
|
|
case 3:
|
|
btns = 0
|
|
}
|
|
if b[0]&4 != 0 {
|
|
mod |= ModShift
|
|
}
|
|
if b[0]&8 != 0 {
|
|
mod |= ModMeta
|
|
}
|
|
if b[0]&16 != 0 {
|
|
mod |= ModCtrl
|
|
}
|
|
x := int(b[1]) - 33
|
|
y := int(b[2]) - 33
|
|
for i := 0; i < len(t.mouse)+3; i++ {
|
|
buf.ReadByte()
|
|
}
|
|
matched = true
|
|
ev := NewEventMouse(x, y, btns, mod)
|
|
t.PostEvent(ev)
|
|
continue
|
|
} else {
|
|
partials++
|
|
}
|
|
|
|
} else {
|
|
partials++
|
|
}
|
|
|
|
// if we expired, we implicitly fail matches
|
|
if expire {
|
|
partials = 0
|
|
}
|
|
// If we had no partial matches, just send first character as
|
|
// a rune. Others might still work.
|
|
if partials == 0 && !matched {
|
|
ev := NewEventKey(KeyRune, rune(b[0]), ModNone)
|
|
t.PostEvent(ev)
|
|
buf.ReadByte()
|
|
}
|
|
|
|
if partials > 0 {
|
|
// We had one or more partial matches, wait for more
|
|
// data.
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (t *tScreen) inputLoop() {
|
|
buf := &bytes.Buffer{}
|
|
chunk := make([]byte, 128)
|
|
for {
|
|
select {
|
|
case <-t.quit:
|
|
t.termioFini()
|
|
return
|
|
case <-t.sigwinch:
|
|
t.resize()
|
|
continue
|
|
default:
|
|
}
|
|
n, e := t.in.Read(chunk)
|
|
switch e {
|
|
case io.EOF:
|
|
// If we timeout waiting for more bytes, then it's
|
|
// time to give up on it. Even at 300 baud it takes
|
|
// less than 0.5 ms to transmit a whole byte.
|
|
if buf.Len() > 0 {
|
|
t.scanInput(buf, true)
|
|
}
|
|
continue
|
|
case nil:
|
|
default:
|
|
// XXX: post error event?
|
|
return
|
|
}
|
|
buf.Write(chunk[:n])
|
|
// Now we need to parse the input buffer for events
|
|
t.scanInput(buf, false)
|
|
}
|
|
}
|