mirror of https://github.com/gdamore/tcell.git
529 lines
10 KiB
Go
529 lines
10 KiB
Go
// 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.
|
|
|
|
package tcell
|
|
|
|
import (
|
|
"sync"
|
|
"unicode/utf8"
|
|
|
|
"golang.org/x/text/transform"
|
|
)
|
|
|
|
// NewSimulationScreen returns a SimulationScreen. Note that
|
|
// SimulationScreen is also a Screen.
|
|
func NewSimulationScreen(charset string) SimulationScreen {
|
|
if charset == "" {
|
|
charset = "UTF-8"
|
|
}
|
|
ss := &simscreen{charset: charset}
|
|
ss.Screen = &baseScreen{screenImpl: ss}
|
|
return ss
|
|
}
|
|
|
|
// SimulationScreen represents a screen simulation. This is intended to
|
|
// be a superset of normal Screens, but also adds some important interfaces
|
|
// for testing.
|
|
type SimulationScreen interface {
|
|
Screen
|
|
|
|
// InjectKeyBytes injects a stream of bytes corresponding to
|
|
// the native encoding (see charset). It turns true if the entire
|
|
// set of bytes were processed and delivered as KeyEvents, false
|
|
// if any bytes were not fully understood. Any bytes that are not
|
|
// fully converted are discarded.
|
|
InjectKeyBytes(buf []byte) bool
|
|
|
|
// InjectKey injects a key event. The rune is a UTF-8 rune, post
|
|
// any translation.
|
|
InjectKey(key Key, r rune, mod ModMask)
|
|
|
|
// InjectMouse injects a mouse event.
|
|
InjectMouse(x, y int, buttons ButtonMask, mod ModMask)
|
|
|
|
// GetContents returns screen contents as an array of
|
|
// cells, along with the physical width & height. Note that the
|
|
// physical contents will be used until the next time SetSize()
|
|
// is called.
|
|
GetContents() (cells []SimCell, width int, height int)
|
|
|
|
// GetCursor returns the cursor details.
|
|
GetCursor() (x int, y int, visible bool)
|
|
|
|
// GetTitle gets the previously set title.
|
|
GetTitle() string
|
|
|
|
// GetClipboardData gets the actual data for the clipboard.
|
|
GetClipboardData() []byte
|
|
}
|
|
|
|
// SimCell represents a simulated screen cell. The purpose of this
|
|
// is to track on screen content.
|
|
type SimCell struct {
|
|
// Bytes is the actual character bytes. Normally this is
|
|
// rune data, but it could be be data in another encoding system.
|
|
Bytes []byte
|
|
|
|
// Style is the style used to display the data.
|
|
Style Style
|
|
|
|
// Runes is the list of runes, unadulterated, in UTF-8.
|
|
Runes []rune
|
|
}
|
|
|
|
type simscreen struct {
|
|
physw int
|
|
physh int
|
|
fini bool
|
|
style Style
|
|
evch chan Event
|
|
quit chan struct{}
|
|
|
|
front []SimCell
|
|
back CellBuffer
|
|
clear bool
|
|
cursorx int
|
|
cursory int
|
|
cursorvis bool
|
|
mouse bool
|
|
paste bool
|
|
charset string
|
|
encoder transform.Transformer
|
|
decoder transform.Transformer
|
|
fillchar rune
|
|
fillstyle Style
|
|
fallback map[rune]string
|
|
title string
|
|
clipboard []byte
|
|
|
|
Screen
|
|
sync.Mutex
|
|
}
|
|
|
|
func (s *simscreen) Init() error {
|
|
s.evch = make(chan Event, 10)
|
|
s.quit = make(chan struct{})
|
|
s.fillchar = 'X'
|
|
s.fillstyle = StyleDefault
|
|
s.mouse = false
|
|
s.physw = 80
|
|
s.physh = 25
|
|
s.cursorx = -1
|
|
s.cursory = -1
|
|
s.style = StyleDefault
|
|
|
|
if enc := GetEncoding(s.charset); enc != nil {
|
|
s.encoder = enc.NewEncoder()
|
|
s.decoder = enc.NewDecoder()
|
|
} else {
|
|
return ErrNoCharset
|
|
}
|
|
|
|
s.front = make([]SimCell, s.physw*s.physh)
|
|
s.back.Resize(80, 25)
|
|
|
|
// default fallbacks
|
|
s.fallback = make(map[rune]string)
|
|
for k, v := range RuneFallbacks {
|
|
s.fallback[k] = v
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *simscreen) Fini() {
|
|
s.Lock()
|
|
s.fini = true
|
|
s.back.Resize(0, 0)
|
|
s.Unlock()
|
|
if s.quit != nil {
|
|
close(s.quit)
|
|
}
|
|
s.physw = 0
|
|
s.physh = 0
|
|
s.front = nil
|
|
}
|
|
|
|
func (s *simscreen) SetStyle(style Style) {
|
|
s.Lock()
|
|
s.style = style
|
|
s.Unlock()
|
|
}
|
|
|
|
func (s *simscreen) drawCell(x, y int) int {
|
|
|
|
mainc, combc, style, width := s.back.GetContent(x, y)
|
|
if !s.back.Dirty(x, y) {
|
|
return width
|
|
}
|
|
if x >= s.physw || y >= s.physh || x < 0 || y < 0 {
|
|
return width
|
|
}
|
|
simc := &s.front[(y*s.physw)+x]
|
|
|
|
if style == StyleDefault {
|
|
style = s.style
|
|
}
|
|
simc.Style = style
|
|
simc.Runes = append([]rune{mainc}, combc...)
|
|
|
|
// 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
|
|
|
|
simc.Bytes = nil
|
|
|
|
if x > s.physw-width {
|
|
simc.Runes = []rune{' '}
|
|
simc.Bytes = []byte{' '}
|
|
return width
|
|
}
|
|
|
|
lbuf := make([]byte, 12)
|
|
ubuf := make([]byte, 12)
|
|
nout := 0
|
|
|
|
for _, r := range simc.Runes {
|
|
|
|
l := utf8.EncodeRune(ubuf, r)
|
|
|
|
nout, _, _ = s.encoder.Transform(lbuf, ubuf[:l], true)
|
|
|
|
if nout == 0 || lbuf[0] == '\x1a' {
|
|
|
|
// skip combining
|
|
|
|
if subst, ok := s.fallback[r]; ok {
|
|
simc.Bytes = append(simc.Bytes,
|
|
[]byte(subst)...)
|
|
|
|
} else if r >= ' ' && r <= '~' {
|
|
simc.Bytes = append(simc.Bytes, byte(r))
|
|
|
|
} else if simc.Bytes == nil {
|
|
simc.Bytes = append(simc.Bytes, '?')
|
|
}
|
|
} else {
|
|
simc.Bytes = append(simc.Bytes, lbuf[:nout]...)
|
|
}
|
|
}
|
|
s.back.SetDirty(x, y, false)
|
|
return width
|
|
}
|
|
|
|
func (s *simscreen) ShowCursor(x, y int) {
|
|
s.Lock()
|
|
s.cursorx, s.cursory = x, y
|
|
s.showCursor()
|
|
s.Unlock()
|
|
}
|
|
|
|
func (s *simscreen) HideCursor() {
|
|
s.ShowCursor(-1, -1)
|
|
}
|
|
|
|
func (s *simscreen) showCursor() {
|
|
|
|
x, y := s.cursorx, s.cursory
|
|
if x < 0 || y < 0 || x >= s.physw || y >= s.physh {
|
|
s.cursorvis = false
|
|
} else {
|
|
s.cursorvis = true
|
|
}
|
|
}
|
|
|
|
func (s *simscreen) hideCursor() {
|
|
// does not update cursor position
|
|
s.cursorvis = false
|
|
}
|
|
|
|
func (s *simscreen) SetCursor(CursorStyle, Color) {}
|
|
|
|
func (s *simscreen) Show() {
|
|
s.Lock()
|
|
s.resize()
|
|
s.draw()
|
|
s.Unlock()
|
|
}
|
|
|
|
func (s *simscreen) clearScreen() {
|
|
// We emulate a hardware clear by filling with a specific pattern
|
|
for i := range s.front {
|
|
s.front[i].Style = s.fillstyle
|
|
s.front[i].Runes = []rune{s.fillchar}
|
|
s.front[i].Bytes = []byte{byte(s.fillchar)}
|
|
}
|
|
s.clear = false
|
|
}
|
|
|
|
func (s *simscreen) draw() {
|
|
s.hideCursor()
|
|
if s.clear {
|
|
s.clearScreen()
|
|
}
|
|
|
|
w, h := s.back.Size()
|
|
for y := 0; y < h; y++ {
|
|
for x := 0; x < w; x++ {
|
|
width := s.drawCell(x, y)
|
|
x += width - 1
|
|
}
|
|
}
|
|
s.showCursor()
|
|
}
|
|
|
|
func (s *simscreen) EnableMouse(...MouseFlags) {
|
|
s.mouse = true
|
|
}
|
|
|
|
func (s *simscreen) DisableMouse() {
|
|
s.mouse = false
|
|
}
|
|
|
|
func (s *simscreen) EnablePaste() {
|
|
s.paste = true
|
|
}
|
|
|
|
func (s *simscreen) DisablePaste() {
|
|
s.paste = false
|
|
}
|
|
|
|
func (s *simscreen) EnableFocus() {
|
|
}
|
|
|
|
func (s *simscreen) DisableFocus() {
|
|
}
|
|
|
|
func (s *simscreen) Size() (int, int) {
|
|
s.Lock()
|
|
w, h := s.back.Size()
|
|
s.Unlock()
|
|
return w, h
|
|
}
|
|
|
|
func (s *simscreen) resize() {
|
|
w, h := s.physw, s.physh
|
|
ow, oh := s.back.Size()
|
|
if w != ow || h != oh {
|
|
s.back.Resize(w, h)
|
|
ev := NewEventResize(w, h)
|
|
s.postEvent(ev)
|
|
}
|
|
}
|
|
|
|
func (s *simscreen) Colors() int {
|
|
return 256
|
|
}
|
|
|
|
func (s *simscreen) postEvent(ev Event) {
|
|
select {
|
|
case s.evch <- ev:
|
|
case <-s.quit:
|
|
}
|
|
}
|
|
|
|
func (s *simscreen) InjectMouse(x, y int, buttons ButtonMask, mod ModMask) {
|
|
ev := NewEventMouse(x, y, buttons, mod)
|
|
s.postEvent(ev)
|
|
}
|
|
|
|
func (s *simscreen) InjectKey(key Key, r rune, mod ModMask) {
|
|
ev := NewEventKey(key, r, mod)
|
|
s.postEvent(ev)
|
|
}
|
|
|
|
func (s *simscreen) InjectKeyBytes(b []byte) bool {
|
|
failed := false
|
|
|
|
outer:
|
|
for len(b) > 0 {
|
|
if b[0] >= ' ' && b[0] <= 0x7F {
|
|
// printable ASCII easy to deal with -- no encodings
|
|
ev := NewEventKey(KeyRune, rune(b[0]), ModNone)
|
|
s.postEvent(ev)
|
|
b = b[1:]
|
|
continue
|
|
}
|
|
|
|
if b[0] < 0x80 {
|
|
mod := ModNone
|
|
// No encodings start with low numbered values
|
|
if Key(b[0]) >= KeyCtrlA && Key(b[0]) <= KeyCtrlZ {
|
|
mod = ModCtrl
|
|
}
|
|
ev := NewEventKey(Key(b[0]), 0, mod)
|
|
s.postEvent(ev)
|
|
b = b[1:]
|
|
continue
|
|
}
|
|
|
|
utfb := make([]byte, len(b)*4) // worst case
|
|
for l := 1; l < len(b); l++ {
|
|
s.decoder.Reset()
|
|
nout, nin, _ := s.decoder.Transform(utfb, b[:l], true)
|
|
|
|
if nout != 0 {
|
|
r, _ := utf8.DecodeRune(utfb[:nout])
|
|
if r != utf8.RuneError {
|
|
ev := NewEventKey(KeyRune, r, ModNone)
|
|
s.postEvent(ev)
|
|
}
|
|
b = b[nin:]
|
|
continue outer
|
|
}
|
|
}
|
|
failed = true
|
|
b = b[1:]
|
|
continue
|
|
}
|
|
|
|
return !failed
|
|
}
|
|
|
|
func (s *simscreen) Sync() {
|
|
s.Lock()
|
|
s.clear = true
|
|
s.resize()
|
|
s.back.Invalidate()
|
|
s.draw()
|
|
s.Unlock()
|
|
}
|
|
|
|
func (s *simscreen) CharacterSet() string {
|
|
return s.charset
|
|
}
|
|
|
|
func (s *simscreen) SetSize(w, h int) {
|
|
s.Lock()
|
|
newc := make([]SimCell, w*h)
|
|
for row := 0; row < h && row < s.physh; row++ {
|
|
for col := 0; col < w && col < s.physw; col++ {
|
|
newc[(row*w)+col] = s.front[(row*s.physw)+col]
|
|
}
|
|
}
|
|
s.cursorx, s.cursory = -1, -1
|
|
s.physw, s.physh = w, h
|
|
s.front = newc
|
|
s.back.Resize(w, h)
|
|
s.Unlock()
|
|
}
|
|
|
|
func (s *simscreen) GetContents() ([]SimCell, int, int) {
|
|
s.Lock()
|
|
cells, w, h := s.front, s.physw, s.physh
|
|
s.Unlock()
|
|
return cells, w, h
|
|
}
|
|
|
|
func (s *simscreen) GetCursor() (int, int, bool) {
|
|
s.Lock()
|
|
x, y, vis := s.cursorx, s.cursory, s.cursorvis
|
|
s.Unlock()
|
|
return x, y, vis
|
|
}
|
|
|
|
func (s *simscreen) RegisterRuneFallback(r rune, subst string) {
|
|
s.Lock()
|
|
s.fallback[r] = subst
|
|
s.Unlock()
|
|
}
|
|
|
|
func (s *simscreen) UnregisterRuneFallback(r rune) {
|
|
s.Lock()
|
|
delete(s.fallback, r)
|
|
s.Unlock()
|
|
}
|
|
|
|
func (s *simscreen) CanDisplay(r rune, checkFallbacks bool) bool {
|
|
|
|
if enc := s.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
|
|
}
|
|
}
|
|
if !checkFallbacks {
|
|
return false
|
|
}
|
|
if _, ok := s.fallback[r]; ok {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (s *simscreen) HasMouse() bool {
|
|
return false
|
|
}
|
|
|
|
func (s *simscreen) Resize(int, int, int, int) {}
|
|
|
|
func (s *simscreen) HasKey(Key) bool {
|
|
return true
|
|
}
|
|
|
|
func (s *simscreen) Beep() error {
|
|
return nil
|
|
}
|
|
|
|
func (s *simscreen) Suspend() error {
|
|
return nil
|
|
}
|
|
|
|
func (s *simscreen) Resume() error {
|
|
return nil
|
|
}
|
|
|
|
func (s *simscreen) Tty() (Tty, bool) {
|
|
return nil, false
|
|
}
|
|
|
|
func (s *simscreen) GetCells() *CellBuffer {
|
|
return &s.back
|
|
}
|
|
|
|
func (s *simscreen) EventQ() chan Event {
|
|
return s.evch
|
|
}
|
|
|
|
func (s *simscreen) StopQ() <-chan struct{} {
|
|
return s.quit
|
|
}
|
|
|
|
func (s *simscreen) SetTitle(title string) {
|
|
s.title = title
|
|
}
|
|
|
|
func (s *simscreen) GetTitle() string {
|
|
return s.title
|
|
}
|
|
|
|
func (s *simscreen) SetClipboard(data []byte) {
|
|
s.clipboard = data
|
|
}
|
|
|
|
func (s *simscreen) GetClipboard() {
|
|
if s.clipboard != nil {
|
|
ev := NewEventClipboard(s.clipboard)
|
|
s.postEvent(ev)
|
|
}
|
|
}
|
|
|
|
func (s *simscreen) GetClipboardData() []byte {
|
|
return s.clipboard
|
|
}
|