1
0
mirror of https://github.com/gdamore/tcell.git synced 2025-04-24 13:48:51 +08:00
tcell/simulation.go
Garrett D'Amore 78110e30f8 fixes #561 Add clipboard support.
This is not supported for Windows or WebAssembly yet.
It's possible for applications to post to the clipboard using
Screen.SetClipboard (any data), and they can retrieve the clipboard
(if permitted) using GetClipboard.  The terminal may well reject either
of these.

Retrieval will arrive as a new EventClipboard, if it can.  (There is
no good way to make this synchronous.)

This work was inspired by a PR submitted by Consolatis (#562), and
has some work based on it, but it was also substantially improved and
now includes both sides of the clipboard access pattern.
2024-03-10 14:05:48 -07:00

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
}