mirror of
https://github.com/gdamore/tcell.git
synced 2025-04-26 13:48:53 +08:00

This works well on the new Windows 10 Terminal, as well as recent Windows 10 ConHost. Support for this is automatically enabled if we detect that the terminal supports ANSI escapes, except for ConEmu, which has a fairly severe scrolling bug with truecolor. To opt-in anyway, set TCELL_TRUECOLOR to "enable" in your environment. To opt-out, set TCELL_TRUECOLOR to "disable" in your environment.
1171 lines
24 KiB
Go
1171 lines
24 KiB
Go
// +build windows
|
|
|
|
// Copyright 2019 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 (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"unicode/utf16"
|
|
"unsafe"
|
|
)
|
|
|
|
type cScreen struct {
|
|
in syscall.Handle
|
|
out syscall.Handle
|
|
cancelflag syscall.Handle
|
|
scandone chan struct{}
|
|
evch chan Event
|
|
quit chan struct{}
|
|
curx int
|
|
cury int
|
|
style Style
|
|
clear bool
|
|
fini bool
|
|
vten bool
|
|
truecolor bool
|
|
|
|
w int
|
|
h int
|
|
|
|
oscreen consoleInfo
|
|
ocursor cursorInfo
|
|
oimode uint32
|
|
oomode uint32
|
|
cells CellBuffer
|
|
|
|
sync.Mutex
|
|
}
|
|
|
|
var winLock sync.Mutex
|
|
|
|
var winPalette = []Color{
|
|
ColorBlack,
|
|
ColorMaroon,
|
|
ColorGreen,
|
|
ColorNavy,
|
|
ColorOlive,
|
|
ColorPurple,
|
|
ColorTeal,
|
|
ColorSilver,
|
|
ColorGray,
|
|
ColorRed,
|
|
ColorLime,
|
|
ColorBlue,
|
|
ColorYellow,
|
|
ColorFuchsia,
|
|
ColorAqua,
|
|
ColorWhite,
|
|
}
|
|
|
|
var winColors = map[Color]Color{
|
|
ColorBlack: ColorBlack,
|
|
ColorMaroon: ColorMaroon,
|
|
ColorGreen: ColorGreen,
|
|
ColorNavy: ColorNavy,
|
|
ColorOlive: ColorOlive,
|
|
ColorPurple: ColorPurple,
|
|
ColorTeal: ColorTeal,
|
|
ColorSilver: ColorSilver,
|
|
ColorGray: ColorGray,
|
|
ColorRed: ColorRed,
|
|
ColorLime: ColorLime,
|
|
ColorBlue: ColorBlue,
|
|
ColorYellow: ColorYellow,
|
|
ColorFuchsia: ColorFuchsia,
|
|
ColorAqua: ColorAqua,
|
|
ColorWhite: ColorWhite,
|
|
}
|
|
|
|
var (
|
|
k32 = syscall.NewLazyDLL("kernel32.dll")
|
|
u32 = syscall.NewLazyDLL("user32.dll")
|
|
)
|
|
|
|
// We have to bring in the kernel32 and user32 DLLs directly, so we can get
|
|
// access to some system calls that the core Go API lacks.
|
|
//
|
|
// Note that Windows appends some functions with W to indicate that wide
|
|
// characters (Unicode) are in use. The documentation refers to them
|
|
// without this suffix, as the resolution is made via preprocessor.
|
|
var (
|
|
procReadConsoleInput = k32.NewProc("ReadConsoleInputW")
|
|
procWaitForMultipleObjects = k32.NewProc("WaitForMultipleObjects")
|
|
procCreateEvent = k32.NewProc("CreateEventW")
|
|
procSetEvent = k32.NewProc("SetEvent")
|
|
procGetConsoleCursorInfo = k32.NewProc("GetConsoleCursorInfo")
|
|
procSetConsoleCursorInfo = k32.NewProc("SetConsoleCursorInfo")
|
|
procSetConsoleCursorPosition = k32.NewProc("SetConsoleCursorPosition")
|
|
procSetConsoleMode = k32.NewProc("SetConsoleMode")
|
|
procGetConsoleMode = k32.NewProc("GetConsoleMode")
|
|
procGetConsoleScreenBufferInfo = k32.NewProc("GetConsoleScreenBufferInfo")
|
|
procFillConsoleOutputAttribute = k32.NewProc("FillConsoleOutputAttribute")
|
|
procFillConsoleOutputCharacter = k32.NewProc("FillConsoleOutputCharacterW")
|
|
procSetConsoleWindowInfo = k32.NewProc("SetConsoleWindowInfo")
|
|
procSetConsoleScreenBufferSize = k32.NewProc("SetConsoleScreenBufferSize")
|
|
procSetConsoleTextAttribute = k32.NewProc("SetConsoleTextAttribute")
|
|
procMessageBeep = u32.NewProc("MessageBeep")
|
|
)
|
|
|
|
const (
|
|
w32Infinite = ^uintptr(0)
|
|
w32WaitObject0 = uintptr(0)
|
|
)
|
|
|
|
const (
|
|
// VT100/XTerm escapes understood by the console
|
|
vtShowCursor = "\x1b[?25h"
|
|
vtHideCursor = "\x1b[?25l"
|
|
vtCursorPos = "\x1b[%d;%dH" // Note that it is Y then X
|
|
vtSgr0 = "\x1b[0m"
|
|
vtBold = "\x1b[1m"
|
|
vtUnderline = "\x1b[4m"
|
|
vtBlink = "\x1b[5m" // Not sure this is processed
|
|
vtReverse = "\x1b[7m"
|
|
vtSetFg = "\x1b[38;2;%d;%d;%dm" // RGB
|
|
vtSetBg = "\x1b[48;2;%d;%d;%dm" // RGB
|
|
)
|
|
|
|
// NewConsoleScreen returns a Screen for the Windows console associated
|
|
// with the current process. The Screen makes use of the Windows Console
|
|
// API to display content and read events.
|
|
func NewConsoleScreen() (Screen, error) {
|
|
return &cScreen{}, nil
|
|
}
|
|
|
|
func (s *cScreen) Init() error {
|
|
s.evch = make(chan Event, 10)
|
|
s.quit = make(chan struct{})
|
|
s.scandone = make(chan struct{})
|
|
|
|
in, e := syscall.Open("CONIN$", syscall.O_RDWR, 0)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
s.in = in
|
|
out, e := syscall.Open("CONOUT$", syscall.O_RDWR, 0)
|
|
if e != nil {
|
|
syscall.Close(s.in)
|
|
return e
|
|
}
|
|
s.out = out
|
|
|
|
s.truecolor = true
|
|
|
|
// ConEmu handling of colors and scrolling when in terminal
|
|
// mode is extremely problematic at the best. The color
|
|
// palette will scroll even though characters do not, when
|
|
// emiting stuff for the last character. In the future we
|
|
// might change this to look at specific versions of ConEmu
|
|
// if they fix the bug.
|
|
if os.Getenv("ConEmuPID") != "" {
|
|
s.truecolor = false
|
|
}
|
|
switch os.Getenv("TCELL_TRUECOLOR") {
|
|
case "disable":
|
|
s.truecolor = false
|
|
case "enable":
|
|
s.truecolor = true
|
|
}
|
|
|
|
cf, _, e := procCreateEvent.Call(
|
|
uintptr(0),
|
|
uintptr(1),
|
|
uintptr(0),
|
|
uintptr(0))
|
|
if cf == uintptr(0) {
|
|
return e
|
|
}
|
|
s.cancelflag = syscall.Handle(cf)
|
|
|
|
s.Lock()
|
|
|
|
s.curx = -1
|
|
s.cury = -1
|
|
s.style = StyleDefault
|
|
s.getCursorInfo(&s.ocursor)
|
|
s.getConsoleInfo(&s.oscreen)
|
|
s.getOutMode(&s.oomode)
|
|
s.getInMode(&s.oimode)
|
|
s.resize()
|
|
|
|
s.fini = false
|
|
s.setInMode(modeResizeEn | modeExtndFlg)
|
|
|
|
// 24-bit color is opt-in for now, because we can't figure out
|
|
// to make it work consistently.
|
|
if s.truecolor {
|
|
s.setOutMode(modeVtOutput | modeNoAutoNL | modeCookedOut)
|
|
var omode uint32
|
|
s.getOutMode(&omode)
|
|
if omode&modeVtOutput == modeVtOutput {
|
|
s.vten = true
|
|
} else {
|
|
s.truecolor = false
|
|
}
|
|
} else {
|
|
s.setOutMode(0)
|
|
}
|
|
|
|
s.clearScreen(s.style)
|
|
s.hideCursor()
|
|
s.Unlock()
|
|
go s.scanInput()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *cScreen) CharacterSet() string {
|
|
// We are always UTF-16LE on Windows
|
|
return "UTF-16LE"
|
|
}
|
|
|
|
func (s *cScreen) EnableMouse() {
|
|
s.setInMode(modeResizeEn | modeMouseEn | modeExtndFlg)
|
|
}
|
|
|
|
func (s *cScreen) DisableMouse() {
|
|
s.setInMode(modeResizeEn | modeExtndFlg)
|
|
}
|
|
|
|
func (s *cScreen) Fini() {
|
|
s.Lock()
|
|
s.style = StyleDefault
|
|
s.curx = -1
|
|
s.cury = -1
|
|
s.fini = true
|
|
s.vten = false
|
|
s.Unlock()
|
|
|
|
s.setCursorInfo(&s.ocursor)
|
|
s.setInMode(s.oimode)
|
|
s.setOutMode(s.oomode)
|
|
s.setBufferSize(int(s.oscreen.size.x), int(s.oscreen.size.y))
|
|
s.clearScreen(StyleDefault)
|
|
s.setCursorPos(0, 0)
|
|
procSetConsoleTextAttribute.Call(
|
|
uintptr(s.out),
|
|
uintptr(s.mapStyle(StyleDefault)))
|
|
|
|
close(s.quit)
|
|
procSetEvent.Call(uintptr(s.cancelflag))
|
|
// Block until scanInput returns; this prevents a race condition on Win 8+
|
|
// which causes syscall.Close to block until another keypress is read.
|
|
<-s.scandone
|
|
syscall.Close(s.in)
|
|
syscall.Close(s.out)
|
|
}
|
|
|
|
func (s *cScreen) PostEventWait(ev Event) {
|
|
s.evch <- ev
|
|
}
|
|
|
|
func (s *cScreen) PostEvent(ev Event) error {
|
|
select {
|
|
case s.evch <- ev:
|
|
return nil
|
|
default:
|
|
return ErrEventQFull
|
|
}
|
|
}
|
|
|
|
func (s *cScreen) PollEvent() Event {
|
|
select {
|
|
case <-s.quit:
|
|
return nil
|
|
case ev := <-s.evch:
|
|
return ev
|
|
}
|
|
}
|
|
|
|
type cursorInfo struct {
|
|
size uint32
|
|
visible uint32
|
|
}
|
|
|
|
type coord struct {
|
|
x int16
|
|
y int16
|
|
}
|
|
|
|
func (c coord) uintptr() uintptr {
|
|
// little endian, put x first
|
|
return uintptr(c.x) | (uintptr(c.y) << 16)
|
|
}
|
|
|
|
type rect struct {
|
|
left int16
|
|
top int16
|
|
right int16
|
|
bottom int16
|
|
}
|
|
|
|
func (s *cScreen) emitVtString(vs string) {
|
|
esc := utf16.Encode([]rune(vs))
|
|
syscall.WriteConsole(s.out, &esc[0], uint32(len(esc)), nil, nil)
|
|
}
|
|
|
|
func (s *cScreen) showCursor() {
|
|
if s.vten {
|
|
s.emitVtString(vtShowCursor)
|
|
} else {
|
|
s.setCursorInfo(&cursorInfo{size: 100, visible: 1})
|
|
}
|
|
}
|
|
|
|
func (s *cScreen) hideCursor() {
|
|
if s.vten {
|
|
s.emitVtString(vtHideCursor)
|
|
} else {
|
|
s.setCursorInfo(&cursorInfo{size: 1, visible: 0})
|
|
}
|
|
}
|
|
|
|
func (s *cScreen) ShowCursor(x, y int) {
|
|
s.Lock()
|
|
if !s.fini {
|
|
s.curx = x
|
|
s.cury = y
|
|
}
|
|
s.doCursor()
|
|
s.Unlock()
|
|
}
|
|
|
|
func (s *cScreen) doCursor() {
|
|
x, y := s.curx, s.cury
|
|
|
|
if x < 0 || y < 0 || x >= s.w || y >= s.h {
|
|
s.hideCursor()
|
|
} else {
|
|
s.setCursorPos(x, y)
|
|
s.showCursor()
|
|
}
|
|
}
|
|
|
|
func (s *cScreen) HideCursor() {
|
|
s.ShowCursor(-1, -1)
|
|
}
|
|
|
|
type inputRecord struct {
|
|
typ uint16
|
|
_ uint16
|
|
data [16]byte
|
|
}
|
|
|
|
const (
|
|
keyEvent uint16 = 1
|
|
mouseEvent uint16 = 2
|
|
resizeEvent uint16 = 4
|
|
menuEvent uint16 = 8 // don't use
|
|
focusEvent uint16 = 16 // don't use
|
|
)
|
|
|
|
type mouseRecord struct {
|
|
x int16
|
|
y int16
|
|
btns uint32
|
|
mod uint32
|
|
flags uint32
|
|
}
|
|
|
|
const (
|
|
mouseDoubleClick uint32 = 0x2
|
|
mouseHWheeled uint32 = 0x8
|
|
mouseVWheeled uint32 = 0x4
|
|
mouseMoved uint32 = 0x1
|
|
)
|
|
|
|
type resizeRecord struct {
|
|
x int16
|
|
y int16
|
|
}
|
|
|
|
type keyRecord struct {
|
|
isdown int32
|
|
repeat uint16
|
|
kcode uint16
|
|
scode uint16
|
|
ch uint16
|
|
mod uint32
|
|
}
|
|
|
|
const (
|
|
// Constants per Microsoft. We don't put the modifiers
|
|
// here.
|
|
vkCancel = 0x03
|
|
vkBack = 0x08 // Backspace
|
|
vkTab = 0x09
|
|
vkClear = 0x0c
|
|
vkReturn = 0x0d
|
|
vkPause = 0x13
|
|
vkEscape = 0x1b
|
|
vkSpace = 0x20
|
|
vkPrior = 0x21 // PgUp
|
|
vkNext = 0x22 // PgDn
|
|
vkEnd = 0x23
|
|
vkHome = 0x24
|
|
vkLeft = 0x25
|
|
vkUp = 0x26
|
|
vkRight = 0x27
|
|
vkDown = 0x28
|
|
vkPrint = 0x2a
|
|
vkPrtScr = 0x2c
|
|
vkInsert = 0x2d
|
|
vkDelete = 0x2e
|
|
vkHelp = 0x2f
|
|
vkF1 = 0x70
|
|
vkF2 = 0x71
|
|
vkF3 = 0x72
|
|
vkF4 = 0x73
|
|
vkF5 = 0x74
|
|
vkF6 = 0x75
|
|
vkF7 = 0x76
|
|
vkF8 = 0x77
|
|
vkF9 = 0x78
|
|
vkF10 = 0x79
|
|
vkF11 = 0x7a
|
|
vkF12 = 0x7b
|
|
vkF13 = 0x7c
|
|
vkF14 = 0x7d
|
|
vkF15 = 0x7e
|
|
vkF16 = 0x7f
|
|
vkF17 = 0x80
|
|
vkF18 = 0x81
|
|
vkF19 = 0x82
|
|
vkF20 = 0x83
|
|
vkF21 = 0x84
|
|
vkF22 = 0x85
|
|
vkF23 = 0x86
|
|
vkF24 = 0x87
|
|
)
|
|
|
|
var vkKeys = map[uint16]Key{
|
|
vkCancel: KeyCancel,
|
|
vkBack: KeyBackspace,
|
|
vkTab: KeyTab,
|
|
vkClear: KeyClear,
|
|
vkPause: KeyPause,
|
|
vkPrint: KeyPrint,
|
|
vkPrtScr: KeyPrint,
|
|
vkPrior: KeyPgUp,
|
|
vkNext: KeyPgDn,
|
|
vkReturn: KeyEnter,
|
|
vkEnd: KeyEnd,
|
|
vkHome: KeyHome,
|
|
vkLeft: KeyLeft,
|
|
vkUp: KeyUp,
|
|
vkRight: KeyRight,
|
|
vkDown: KeyDown,
|
|
vkInsert: KeyInsert,
|
|
vkDelete: KeyDelete,
|
|
vkHelp: KeyHelp,
|
|
vkF1: KeyF1,
|
|
vkF2: KeyF2,
|
|
vkF3: KeyF3,
|
|
vkF4: KeyF4,
|
|
vkF5: KeyF5,
|
|
vkF6: KeyF6,
|
|
vkF7: KeyF7,
|
|
vkF8: KeyF8,
|
|
vkF9: KeyF9,
|
|
vkF10: KeyF10,
|
|
vkF11: KeyF11,
|
|
vkF12: KeyF12,
|
|
vkF13: KeyF13,
|
|
vkF14: KeyF14,
|
|
vkF15: KeyF15,
|
|
vkF16: KeyF16,
|
|
vkF17: KeyF17,
|
|
vkF18: KeyF18,
|
|
vkF19: KeyF19,
|
|
vkF20: KeyF20,
|
|
vkF21: KeyF21,
|
|
vkF22: KeyF22,
|
|
vkF23: KeyF23,
|
|
vkF24: KeyF24,
|
|
}
|
|
|
|
// NB: All Windows platforms are little endian. We assume this
|
|
// never, ever change. The following code is endian safe. and does
|
|
// not use unsafe pointers.
|
|
func getu32(v []byte) uint32 {
|
|
return uint32(v[0]) + (uint32(v[1]) << 8) + (uint32(v[2]) << 16) + (uint32(v[3]) << 24)
|
|
}
|
|
func geti32(v []byte) int32 {
|
|
return int32(getu32(v))
|
|
}
|
|
func getu16(v []byte) uint16 {
|
|
return uint16(v[0]) + (uint16(v[1]) << 8)
|
|
}
|
|
func geti16(v []byte) int16 {
|
|
return int16(getu16(v))
|
|
}
|
|
|
|
// Convert windows dwControlKeyState to modifier mask
|
|
func mod2mask(cks uint32) ModMask {
|
|
mm := ModNone
|
|
// Left or right control
|
|
if (cks & (0x0008 | 0x0004)) != 0 {
|
|
mm |= ModCtrl
|
|
}
|
|
// Left or right alt
|
|
if (cks & (0x0002 | 0x0001)) != 0 {
|
|
mm |= ModAlt
|
|
}
|
|
// Any shift
|
|
if (cks & 0x0010) != 0 {
|
|
mm |= ModShift
|
|
}
|
|
return mm
|
|
}
|
|
|
|
func mrec2btns(mbtns, flags uint32) ButtonMask {
|
|
btns := ButtonNone
|
|
if mbtns&0x1 != 0 {
|
|
btns |= Button1
|
|
}
|
|
if mbtns&0x2 != 0 {
|
|
btns |= Button2
|
|
}
|
|
if mbtns&0x4 != 0 {
|
|
btns |= Button3
|
|
}
|
|
if mbtns&0x8 != 0 {
|
|
btns |= Button4
|
|
}
|
|
if mbtns&0x10 != 0 {
|
|
btns |= Button5
|
|
}
|
|
if mbtns&0x20 != 0 {
|
|
btns |= Button6
|
|
}
|
|
if mbtns&0x40 != 0 {
|
|
btns |= Button7
|
|
}
|
|
if mbtns&0x80 != 0 {
|
|
btns |= Button8
|
|
}
|
|
|
|
if flags&mouseVWheeled != 0 {
|
|
if mbtns&0x80000000 == 0 {
|
|
btns |= WheelUp
|
|
} else {
|
|
btns |= WheelDown
|
|
}
|
|
}
|
|
if flags&mouseHWheeled != 0 {
|
|
if mbtns&0x80000000 == 0 {
|
|
btns |= WheelRight
|
|
} else {
|
|
btns |= WheelLeft
|
|
}
|
|
}
|
|
return btns
|
|
}
|
|
|
|
func (s *cScreen) getConsoleInput() error {
|
|
// cancelFlag comes first as WaitForMultipleObjects returns the lowest index
|
|
// in the event that both events are signalled.
|
|
waitObjects := []syscall.Handle{s.cancelflag, s.in}
|
|
// As arrays are contiguous in memory, a pointer to the first object is the
|
|
// same as a pointer to the array itself.
|
|
pWaitObjects := unsafe.Pointer(&waitObjects[0])
|
|
|
|
rv, _, er := procWaitForMultipleObjects.Call(
|
|
uintptr(len(waitObjects)),
|
|
uintptr(pWaitObjects),
|
|
uintptr(0),
|
|
w32Infinite)
|
|
// WaitForMultipleObjects returns WAIT_OBJECT_0 + the index.
|
|
switch rv {
|
|
case w32WaitObject0: // s.cancelFlag
|
|
return errors.New("cancelled")
|
|
case w32WaitObject0 + 1: // s.in
|
|
rec := &inputRecord{}
|
|
var nrec int32
|
|
rv, _, er := procReadConsoleInput.Call(
|
|
uintptr(s.in),
|
|
uintptr(unsafe.Pointer(rec)),
|
|
uintptr(1),
|
|
uintptr(unsafe.Pointer(&nrec)))
|
|
if rv == 0 {
|
|
return er
|
|
}
|
|
if nrec != 1 {
|
|
return nil
|
|
}
|
|
switch rec.typ {
|
|
case keyEvent:
|
|
krec := &keyRecord{}
|
|
krec.isdown = geti32(rec.data[0:])
|
|
krec.repeat = getu16(rec.data[4:])
|
|
krec.kcode = getu16(rec.data[6:])
|
|
krec.scode = getu16(rec.data[8:])
|
|
krec.ch = getu16(rec.data[10:])
|
|
krec.mod = getu32(rec.data[12:])
|
|
|
|
if krec.isdown == 0 || krec.repeat < 1 {
|
|
// its a key release event, ignore it
|
|
return nil
|
|
}
|
|
if krec.ch != 0 {
|
|
// synthesized key code
|
|
for krec.repeat > 0 {
|
|
// convert shift+tab to backtab
|
|
if mod2mask(krec.mod) == ModShift && krec.ch == vkTab {
|
|
s.PostEvent(NewEventKey(KeyBacktab, 0,
|
|
ModNone))
|
|
} else {
|
|
s.PostEvent(NewEventKey(KeyRune, rune(krec.ch),
|
|
mod2mask(krec.mod)))
|
|
}
|
|
krec.repeat--
|
|
}
|
|
return nil
|
|
}
|
|
key := KeyNUL // impossible on Windows
|
|
ok := false
|
|
if key, ok = vkKeys[krec.kcode]; !ok {
|
|
return nil
|
|
}
|
|
for krec.repeat > 0 {
|
|
s.PostEvent(NewEventKey(key, rune(krec.ch),
|
|
mod2mask(krec.mod)))
|
|
krec.repeat--
|
|
}
|
|
|
|
case mouseEvent:
|
|
var mrec mouseRecord
|
|
mrec.x = geti16(rec.data[0:])
|
|
mrec.y = geti16(rec.data[2:])
|
|
mrec.btns = getu32(rec.data[4:])
|
|
mrec.mod = getu32(rec.data[8:])
|
|
mrec.flags = getu32(rec.data[12:])
|
|
btns := mrec2btns(mrec.btns, mrec.flags)
|
|
// we ignore double click, events are delivered normally
|
|
s.PostEvent(NewEventMouse(int(mrec.x), int(mrec.y), btns,
|
|
mod2mask(mrec.mod)))
|
|
|
|
case resizeEvent:
|
|
var rrec resizeRecord
|
|
rrec.x = geti16(rec.data[0:])
|
|
rrec.y = geti16(rec.data[2:])
|
|
s.PostEvent(NewEventResize(int(rrec.x), int(rrec.y)))
|
|
|
|
default:
|
|
}
|
|
default:
|
|
return er
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *cScreen) scanInput() {
|
|
for {
|
|
if e := s.getConsoleInput(); e != nil {
|
|
close(s.scandone)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Windows console can display 8 characters, in either low or high intensity
|
|
func (s *cScreen) Colors() int {
|
|
if s.vten {
|
|
return 1 << 24
|
|
}
|
|
return 16
|
|
}
|
|
|
|
var vgaColors = map[Color]uint16{
|
|
ColorBlack: 0,
|
|
ColorMaroon: 0x4,
|
|
ColorGreen: 0x2,
|
|
ColorNavy: 0x1,
|
|
ColorOlive: 0x6,
|
|
ColorPurple: 0x5,
|
|
ColorTeal: 0x3,
|
|
ColorSilver: 0x7,
|
|
ColorGrey: 0x8,
|
|
ColorRed: 0xc,
|
|
ColorLime: 0xa,
|
|
ColorBlue: 0x9,
|
|
ColorYellow: 0xe,
|
|
ColorFuchsia: 0xd,
|
|
ColorAqua: 0xb,
|
|
ColorWhite: 0xf,
|
|
}
|
|
|
|
// Windows uses RGB signals
|
|
func mapColor2RGB(c Color) uint16 {
|
|
winLock.Lock()
|
|
if v, ok := winColors[c]; ok {
|
|
c = v
|
|
} else {
|
|
v = FindColor(c, winPalette)
|
|
winColors[c] = v
|
|
c = v
|
|
}
|
|
winLock.Unlock()
|
|
|
|
if vc, ok := vgaColors[c]; ok {
|
|
return vc
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// Map a tcell style to Windows attributes
|
|
func (s *cScreen) mapStyle(style Style) uint16 {
|
|
f, b, a := style.Decompose()
|
|
fa := s.oscreen.attrs & 0xf
|
|
ba := (s.oscreen.attrs) >> 4 & 0xf
|
|
if f != ColorDefault {
|
|
fa = mapColor2RGB(f)
|
|
}
|
|
if b != ColorDefault {
|
|
ba = mapColor2RGB(b)
|
|
}
|
|
var attr uint16
|
|
// We simulate reverse by doing the color swap ourselves.
|
|
// Apparently windows cannot really do this except in DBCS
|
|
// views.
|
|
if a&AttrReverse != 0 {
|
|
attr = ba
|
|
attr |= (fa << 4)
|
|
} else {
|
|
attr = fa
|
|
attr |= (ba << 4)
|
|
}
|
|
if a&AttrBold != 0 {
|
|
attr |= 0x8
|
|
}
|
|
if a&AttrDim != 0 {
|
|
attr &^= 0x8
|
|
}
|
|
if a&AttrUnderline != 0 {
|
|
// Best effort -- doesn't seem to work though.
|
|
attr |= 0x8000
|
|
}
|
|
// Blink is unsupported
|
|
return attr
|
|
}
|
|
|
|
func (s *cScreen) 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 *cScreen) SetContent(x, y int, mainc rune, combc []rune, style Style) {
|
|
s.Lock()
|
|
if !s.fini {
|
|
s.cells.SetContent(x, y, mainc, combc, style)
|
|
}
|
|
s.Unlock()
|
|
}
|
|
|
|
func (s *cScreen) GetContent(x, y int) (rune, []rune, Style, int) {
|
|
s.Lock()
|
|
mainc, combc, style, width := s.cells.GetContent(x, y)
|
|
s.Unlock()
|
|
return mainc, combc, style, width
|
|
}
|
|
|
|
func (s *cScreen) sendVtStyle(style Style) {
|
|
esc := &strings.Builder{}
|
|
|
|
fg, bg, attrs := style.Decompose()
|
|
|
|
esc.WriteString(vtSgr0)
|
|
|
|
if attrs&(AttrBold|AttrDim) == AttrBold {
|
|
esc.WriteString(vtBold)
|
|
}
|
|
if attrs&AttrBlink != 0 {
|
|
esc.WriteString(vtBlink)
|
|
}
|
|
if attrs&AttrUnderline != 0 {
|
|
esc.WriteString(vtUnderline)
|
|
}
|
|
if attrs&AttrReverse != 0 {
|
|
esc.WriteString(vtReverse)
|
|
}
|
|
if fg != ColorDefault {
|
|
r, g, b := fg.RGB()
|
|
fmt.Fprintf(esc, vtSetFg, r, g, b)
|
|
}
|
|
if bg != ColorDefault {
|
|
r, g, b := bg.RGB()
|
|
fmt.Fprintf(esc, vtSetBg, r, g, b)
|
|
}
|
|
s.emitVtString(esc.String())
|
|
}
|
|
|
|
func (s *cScreen) writeString(x, y int, style Style, ch []uint16) {
|
|
// we assume the caller has hidden the cursor
|
|
if len(ch) == 0 {
|
|
return
|
|
}
|
|
s.setCursorPos(x, y)
|
|
|
|
if s.vten {
|
|
s.sendVtStyle(style)
|
|
} else {
|
|
procSetConsoleTextAttribute.Call(
|
|
uintptr(s.out),
|
|
uintptr(s.mapStyle(style)))
|
|
}
|
|
syscall.WriteConsole(s.out, &ch[0], uint32(len(ch)), nil, nil)
|
|
}
|
|
|
|
func (s *cScreen) draw() {
|
|
// allocate a scratch line bit enough for no combining chars.
|
|
// if you have combining characters, you may pay for extra allocs.
|
|
if s.clear {
|
|
s.clearScreen(s.style)
|
|
s.clear = false
|
|
s.cells.Invalidate()
|
|
}
|
|
buf := make([]uint16, 0, s.w)
|
|
wcs := buf[:]
|
|
lstyle := Style(-1) // invalid attribute
|
|
|
|
lx, ly := -1, -1
|
|
ra := make([]rune, 1)
|
|
|
|
for y := 0; y < int(s.h); y++ {
|
|
for x := 0; x < int(s.w); x++ {
|
|
mainc, combc, style, width := s.cells.GetContent(x, y)
|
|
dirty := s.cells.Dirty(x, y)
|
|
if style == StyleDefault {
|
|
style = s.style
|
|
}
|
|
|
|
if !dirty || style != lstyle {
|
|
// write out any data queued thus far
|
|
// because we are going to skip over some
|
|
// cells, or because we need to change styles
|
|
s.writeString(lx, ly, lstyle, wcs)
|
|
wcs = buf[0:0]
|
|
lstyle = Style(-1)
|
|
if !dirty {
|
|
continue
|
|
}
|
|
}
|
|
if x > s.w-width {
|
|
mainc = ' '
|
|
combc = nil
|
|
width = 1
|
|
}
|
|
if len(wcs) == 0 {
|
|
lstyle = style
|
|
lx = x
|
|
ly = y
|
|
}
|
|
ra[0] = mainc
|
|
wcs = append(wcs, utf16.Encode(ra)...)
|
|
if len(combc) != 0 {
|
|
wcs = append(wcs, utf16.Encode(combc)...)
|
|
}
|
|
for dx := 0; dx < width; dx++ {
|
|
s.cells.SetDirty(x+dx, y, false)
|
|
}
|
|
x += width - 1
|
|
}
|
|
s.writeString(lx, ly, lstyle, wcs)
|
|
wcs = buf[0:0]
|
|
lstyle = Style(-1)
|
|
}
|
|
}
|
|
|
|
func (s *cScreen) Show() {
|
|
s.Lock()
|
|
if !s.fini {
|
|
s.hideCursor()
|
|
s.resize()
|
|
s.draw()
|
|
s.doCursor()
|
|
}
|
|
s.Unlock()
|
|
}
|
|
|
|
func (s *cScreen) Sync() {
|
|
s.Lock()
|
|
if !s.fini {
|
|
s.cells.Invalidate()
|
|
s.hideCursor()
|
|
s.resize()
|
|
s.draw()
|
|
s.doCursor()
|
|
}
|
|
s.Unlock()
|
|
}
|
|
|
|
type consoleInfo struct {
|
|
size coord
|
|
pos coord
|
|
attrs uint16
|
|
win rect
|
|
maxsz coord
|
|
}
|
|
|
|
func (s *cScreen) getConsoleInfo(info *consoleInfo) {
|
|
procGetConsoleScreenBufferInfo.Call(
|
|
uintptr(s.out),
|
|
uintptr(unsafe.Pointer(info)))
|
|
}
|
|
|
|
func (s *cScreen) getCursorInfo(info *cursorInfo) {
|
|
procGetConsoleCursorInfo.Call(
|
|
uintptr(s.out),
|
|
uintptr(unsafe.Pointer(info)))
|
|
}
|
|
|
|
func (s *cScreen) setCursorInfo(info *cursorInfo) {
|
|
procSetConsoleCursorInfo.Call(
|
|
uintptr(s.out),
|
|
uintptr(unsafe.Pointer(info)))
|
|
|
|
}
|
|
|
|
func (s *cScreen) setCursorPos(x, y int) {
|
|
if s.vten {
|
|
// Note that the string is Y first. Origin is 1,1.
|
|
s.emitVtString(fmt.Sprintf(vtCursorPos, y+1, x+1))
|
|
} else {
|
|
procSetConsoleCursorPosition.Call(
|
|
uintptr(s.out),
|
|
coord{int16(x), int16(y)}.uintptr())
|
|
}
|
|
}
|
|
|
|
func (s *cScreen) setBufferSize(x, y int) {
|
|
procSetConsoleScreenBufferSize.Call(
|
|
uintptr(s.out),
|
|
coord{int16(x), int16(y)}.uintptr())
|
|
}
|
|
|
|
func (s *cScreen) Size() (int, int) {
|
|
s.Lock()
|
|
w, h := s.w, s.h
|
|
s.Unlock()
|
|
|
|
return w, h
|
|
}
|
|
|
|
func (s *cScreen) resize() {
|
|
info := consoleInfo{}
|
|
s.getConsoleInfo(&info)
|
|
|
|
w := int((info.win.right - info.win.left) + 1)
|
|
h := int((info.win.bottom - info.win.top) + 1)
|
|
|
|
if s.w == w && s.h == h {
|
|
return
|
|
}
|
|
|
|
s.cells.Resize(w, h)
|
|
s.w = w
|
|
s.h = h
|
|
|
|
s.setBufferSize(w, h)
|
|
|
|
r := rect{0, 0, int16(w - 1), int16(h - 1)}
|
|
procSetConsoleWindowInfo.Call(
|
|
uintptr(s.out),
|
|
uintptr(1),
|
|
uintptr(unsafe.Pointer(&r)))
|
|
s.PostEvent(NewEventResize(w, h))
|
|
}
|
|
|
|
func (s *cScreen) Clear() {
|
|
s.Fill(' ', s.style)
|
|
}
|
|
|
|
func (s *cScreen) Fill(r rune, style Style) {
|
|
s.Lock()
|
|
if !s.fini {
|
|
s.cells.Fill(r, style)
|
|
s.clear = true
|
|
}
|
|
s.Unlock()
|
|
}
|
|
|
|
func (s *cScreen) clearScreen(style Style) {
|
|
if s.vten {
|
|
s.sendVtStyle(style)
|
|
row := strings.Repeat(" ", s.w)
|
|
for y := 0; y < s.h; y++ {
|
|
s.setCursorPos(0, y)
|
|
s.emitVtString(row)
|
|
}
|
|
s.setCursorPos(0, 0)
|
|
|
|
} else {
|
|
pos := coord{0, 0}
|
|
attr := s.mapStyle(style)
|
|
x, y := s.w, s.h
|
|
scratch := uint32(0)
|
|
count := uint32(x * y)
|
|
|
|
procFillConsoleOutputAttribute.Call(
|
|
uintptr(s.out),
|
|
uintptr(attr),
|
|
uintptr(count),
|
|
pos.uintptr(),
|
|
uintptr(unsafe.Pointer(&scratch)))
|
|
procFillConsoleOutputCharacter.Call(
|
|
uintptr(s.out),
|
|
uintptr(' '),
|
|
uintptr(count),
|
|
pos.uintptr(),
|
|
uintptr(unsafe.Pointer(&scratch)))
|
|
}
|
|
}
|
|
|
|
const (
|
|
// Input modes
|
|
modeExtndFlg uint32 = 0x0080
|
|
modeMouseEn = 0x0010
|
|
modeResizeEn = 0x0008
|
|
modeCooked = 0x0001
|
|
modeVtInput = 0x0200
|
|
|
|
// Output modes
|
|
modeCookedOut uint32 = 0x0001
|
|
modeWrapEOL = 0x0002
|
|
modeVtOutput = 0x0004
|
|
modeNoAutoNL = 0x0008
|
|
)
|
|
|
|
func (s *cScreen) setInMode(mode uint32) error {
|
|
rv, _, err := procSetConsoleMode.Call(
|
|
uintptr(s.in),
|
|
uintptr(mode))
|
|
if rv == 0 {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *cScreen) setOutMode(mode uint32) error {
|
|
rv, _, err := procSetConsoleMode.Call(
|
|
uintptr(s.out),
|
|
uintptr(mode))
|
|
if rv == 0 {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *cScreen) getInMode(v *uint32) {
|
|
procGetConsoleMode.Call(
|
|
uintptr(s.in),
|
|
uintptr(unsafe.Pointer(v)))
|
|
}
|
|
|
|
func (s *cScreen) getOutMode(v *uint32) {
|
|
procGetConsoleMode.Call(
|
|
uintptr(s.out),
|
|
uintptr(unsafe.Pointer(v)))
|
|
}
|
|
|
|
func (s *cScreen) SetStyle(style Style) {
|
|
s.Lock()
|
|
s.style = style
|
|
s.Unlock()
|
|
}
|
|
|
|
// No fallback rune support, since we have Unicode. Yay!
|
|
|
|
func (s *cScreen) RegisterRuneFallback(r rune, subst string) {
|
|
}
|
|
|
|
func (s *cScreen) UnregisterRuneFallback(r rune) {
|
|
}
|
|
|
|
func (s *cScreen) CanDisplay(r rune, checkFallbacks bool) bool {
|
|
// We presume we can display anything -- we're Unicode.
|
|
// (Sadly this not precisely true. Combinings are especially
|
|
// poorly supported under Windows.)
|
|
return true
|
|
}
|
|
|
|
func (s *cScreen) HasMouse() bool {
|
|
return true
|
|
}
|
|
|
|
func (s *cScreen) Resize(int, int, int, int) {}
|
|
|
|
func (s *cScreen) HasKey(k Key) bool {
|
|
// Microsoft has codes for some keys, but they are unusual,
|
|
// so we don't include them. We include all the typical
|
|
// 101, 105 key layout keys.
|
|
valid := map[Key]bool{
|
|
KeyBackspace: true,
|
|
KeyTab: true,
|
|
KeyEscape: true,
|
|
KeyPause: true,
|
|
KeyPrint: true,
|
|
KeyPgUp: true,
|
|
KeyPgDn: true,
|
|
KeyEnter: true,
|
|
KeyEnd: true,
|
|
KeyHome: true,
|
|
KeyLeft: true,
|
|
KeyUp: true,
|
|
KeyRight: true,
|
|
KeyDown: true,
|
|
KeyInsert: true,
|
|
KeyDelete: true,
|
|
KeyF1: true,
|
|
KeyF2: true,
|
|
KeyF3: true,
|
|
KeyF4: true,
|
|
KeyF5: true,
|
|
KeyF6: true,
|
|
KeyF7: true,
|
|
KeyF8: true,
|
|
KeyF9: true,
|
|
KeyF10: true,
|
|
KeyF11: true,
|
|
KeyF12: true,
|
|
KeyRune: true,
|
|
}
|
|
|
|
return valid[k]
|
|
}
|
|
|
|
func (s *cScreen) Beep() error {
|
|
// A simple beep. If the sound card is not available, the sound is generated
|
|
// using the speaker.
|
|
//
|
|
// Reference:
|
|
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-messagebeep
|
|
const simpleBeep = 0xffffffff
|
|
if rv, _, err := procMessageBeep.Call(simpleBeep); rv == 0 {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|