// 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 set title GetTitle() string } // 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 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 }