1
0
mirror of https://github.com/gdamore/tcell.git synced 2025-04-27 13:48:50 +08:00
tcell/tscreen.go
2015-09-28 23:42:13 -07:00

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)
}
}