// Copyright 2021 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" } s := &simscreen{charset: charset} return s } // 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 { // 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) // SetSize resizes the underlying physical screen. It also causes // a resize event to be injected during the next Show() or Sync(). // A new physical contents array will be allocated (with data from // the old copied), so any prior value obtained with GetContents // won't be used anymore SetSize(width, height int) // 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) Screen } // 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 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) Clear() { s.Fill(' ', s.style) } func (s *simscreen) Fill(r rune, style Style) { s.Lock() s.back.Fill(r, style) s.Unlock() } func (s *simscreen) SetCell(x, y int, style Style, ch ...rune) { if len(ch) > 0 { s.SetContent(x, y, ch[0], ch[1:], style) } else { s.SetContent(x, y, ' ', nil, style) } } func (s *simscreen) SetContent(x, y int, mainc rune, combc []rune, st Style) { s.Lock() s.back.SetContent(x, y, mainc, combc, st) s.Unlock() } func (s *simscreen) GetContent(x, y int) (rune, []rune, Style, int) { var mainc rune var combc []rune var style Style var width int s.Lock() mainc, combc, style, width = s.back.GetContent(x, y) s.Unlock() return mainc, combc, style, width } 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) SetCursorStyle(CursorStyle) {} 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) 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) ChannelEvents(ch chan<- Event, quit <-chan struct{}) { defer close(ch) for { select { case <-quit: return case <-s.quit: return case ev := <-s.evch: select { case <-quit: return case <-s.quit: return case ch <- ev: } } } } func (s *simscreen) PollEvent() Event { select { case <-s.quit: return nil case ev := <-s.evch: return ev } } func (s *simscreen) HasPendingEvent() bool { return len(s.evch) > 0 } func (s *simscreen) PostEventWait(ev Event) { s.evch <- ev } func (s *simscreen) PostEvent(ev Event) error { select { case s.evch <- ev: return nil default: return ErrEventQFull } } 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 }