tcell/console_win.go

988 lines
19 KiB
Go

// +build windows
// Copyright 2015 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"
"syscall"
"unicode/utf16"
"unsafe"
)
type cScreen struct {
in syscall.Handle
out syscall.Handle
mbtns uint32 // debounce mouse buttons
evch chan Event
quit chan struct{}
curx int
cury int
style Style
clear bool
fini bool
w int
h int
oscreen consoleInfo
ocursor cursorInfo
oimode uint32
oomode uint32
cells CellBuffer
colors map[Color]Color
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")
// We have to bring in the kernel32.dll 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.
// We have to bring in the kernel32.dll directly, so we can get access to some
// system calls that the core Go API lacks.
var (
procReadConsoleInput = k32.NewProc("ReadConsoleInputW")
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")
)
// 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{})
if in, e := syscall.Open("CONIN$", syscall.O_RDWR, 0); e != nil {
return e
} else {
s.in = in
}
if out, e := syscall.Open("CONOUT$", syscall.O_RDWR, 0); e != nil {
syscall.Close(s.in)
return e
} else {
s.out = out
}
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)
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)
}
func (s *cScreen) DisableMouse() {
s.setInMode(modeResizeEn)
}
func (s *cScreen) Fini() {
s.Lock()
s.style = StyleDefault
s.curx = -1
s.cury = -1
s.fini = true
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(mapStyle(StyleDefault)))
close(s.quit)
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) showCursor() {
s.setCursorInfo(&cursorInfo{size: 100, visible: 1})
}
func (s *cScreen) hideCursor() {
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.setCursorPos(0, 0)
s.hideCursor()
} else {
s.setCursorPos(x, y)
s.showCursor()
}
}
func (s *cScreen) HideCursor() {
s.ShowCursor(-1, -1)
}
type charInfo struct {
ch uint16
attr uint16
}
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
)
// 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 (s *cScreen) getConsoleInput() error {
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 {
s.PostEvent(NewEventKey(KeyRune, rune(krec.ch),
mod2mask(krec.mod)))
krec.repeat--
}
return nil
}
key := KeyNUL // impossible on Windows
switch krec.kcode {
case vkCancel:
key = KeyCancel
case vkBack:
key = KeyBackspace
case vkTab:
key = KeyTab
case vkClear:
key = KeyClear
case vkPause:
key = KeyPause
case vkPrint, vkPrtScr:
key = KeyPrint
case vkPrior:
key = KeyPgUp
case vkNext:
key = KeyPgDn
case vkReturn:
key = KeyEnter
case vkEnd:
key = KeyEnd
case vkHome:
key = KeyHome
case vkLeft:
key = KeyLeft
case vkUp:
key = KeyUp
case vkRight:
key = KeyRight
case vkDown:
key = KeyDown
case vkInsert:
key = KeyInsert
case vkDelete:
key = KeyDelete
case vkHelp:
key = KeyHelp
case vkF1:
key = KeyF1
case vkF2:
key = KeyF2
case vkF3:
key = KeyF3
case vkF4:
key = KeyF4
case vkF5:
key = KeyF5
case vkF6:
key = KeyF6
case vkF7:
key = KeyF7
case vkF8:
key = KeyF8
case vkF9:
key = KeyF9
case vkF10:
key = KeyF10
case vkF11:
key = KeyF11
case vkF12:
key = KeyF12
case vkF13:
key = KeyF13
case vkF14:
key = KeyF14
case vkF15:
key = KeyF15
case vkF16:
key = KeyF16
case vkF17:
key = KeyF17
case vkF18:
key = KeyF18
case vkF19:
key = KeyF19
case vkF20:
key = KeyF20
case vkF21:
key = KeyF21
case vkF22:
key = KeyF22
case vkF23:
key = KeyF23
case vkF24:
key = KeyF24
default:
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:]) // not using yet
btns := ButtonNone
s.mbtns = mrec.btns
if mrec.btns&0x1 != 0 {
btns |= Button1
}
if mrec.btns&0x2 != 0 {
btns |= Button2
}
if mrec.btns&0x4 != 0 {
btns |= Button3
}
if mrec.btns&0x8 != 0 {
btns |= Button4
}
if mrec.btns&0x10 != 0 {
btns |= Button5
}
if mrec.btns&0x20 != 0 {
btns |= Button6
}
if mrec.btns&0x40 != 0 {
btns |= Button7
}
if mrec.btns&0x80 != 0 {
btns |= Button8
}
if mrec.flags&mouseVWheeled != 0 {
if mrec.btns&0x80000000 == 0 {
btns |= WheelUp
} else {
btns |= WheelDown
}
}
if mrec.flags&mouseHWheeled != 0 {
if mrec.btns&0x80000000 == 0 {
btns |= WheelRight
} else {
btns |= WheelLeft
}
}
// 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:
}
return nil
}
func (s *cScreen) scanInput() {
for {
if e := s.getConsoleInput(); e != nil {
return
}
}
}
// Windows console can display 8 characters, in either low or high intensity
func (s *cScreen) Colors() int {
return 16
}
// 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()
switch c {
case ColorBlack:
return 0
// primaries
case ColorMaroon:
return 0x4
case ColorGreen:
return 0x2
case ColorNavy:
return 0x1
case ColorOlive:
return 0x6
case ColorPurple:
return 0x5
case ColorTeal:
return 0x3
case ColorSilver:
return 0x7
// bright variants
case ColorGrey:
return 0x8
case ColorRed:
return 0xc
case ColorLime:
return 0xa
case ColorBlue:
return 0x9
case ColorYellow:
return 0xe
case ColorFuchsia:
return 0xd
case ColorAqua:
return 0xb
case ColorWhite:
return 0xf
}
return 0
}
// Map a tcell style to Windows attributes
func mapStyle(style Style) uint16 {
f, b, a := style.Decompose()
if f == ColorDefault {
f = ColorWhite
}
if b == ColorDefault {
b = ColorBlack
}
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 = mapColor2RGB(b)
attr |= (mapColor2RGB(f) << 4)
} else {
attr = mapColor2RGB(f)
attr |= (mapColor2RGB(b) << 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) writeString(x, y int, style Style, ch []uint16) {
// we assume the caller has hidden the cursor
if len(ch) == 0 {
return
}
nw := uint32(len(ch))
procSetConsoleTextAttribute.Call(
uintptr(s.out),
uintptr(mapStyle(style)))
s.setCursorPos(x, y)
syscall.WriteConsole(s.out, &ch[0], nw, &nw, 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)...)
}
s.cells.SetDirty(x, 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) {
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
r := rect{0, 0, int16(w - 1), int16(h - 1)}
procSetConsoleWindowInfo.Call(
uintptr(s.out),
uintptr(1),
uintptr(unsafe.Pointer(&r)))
s.setBufferSize(w, h)
s.PostEvent(NewEventResize(w, h))
}
func (s *cScreen) Clear() {
s.Lock()
if !s.fini {
s.cells.Fill(' ', s.style)
s.clear = true
}
s.Unlock()
}
func (s *cScreen) clearScreen(style Style) {
pos := coord{0, 0}
attr := 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 (
modeMouseEn uint32 = 0x0010
modeResizeEn uint32 = 0x0008
modeWrapEOL uint32 = 0x0002
modeCooked uint32 = 0x0001
)
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) Resize(int, int, int, int) {}