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

This supports now curly, double, dashed, and dotted underline styles where trhe terminal supports it. This works well on Windows Terminal, reasonably on iTerm2, Alacritty, Kitty, and probably others. The wasm mode terminal includes support for this, dependent on the browser capabilities. The macOS Terminal just changes the background color. Legacy Windows console does nothing. We will try to provide a regular underscore as a fallback. A new style.go demo is included to see some style combinations.
1341 lines
29 KiB
Go
1341 lines
29 KiB
Go
//go:build windows
|
|
// +build windows
|
|
|
|
// 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 (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"unicode/utf16"
|
|
"unsafe"
|
|
)
|
|
|
|
type cScreen struct {
|
|
in syscall.Handle
|
|
out syscall.Handle
|
|
cancelflag syscall.Handle
|
|
scandone chan struct{}
|
|
quit chan struct{}
|
|
curx int
|
|
cury int
|
|
style Style
|
|
fini bool
|
|
vten bool
|
|
truecolor bool
|
|
running bool
|
|
disableAlt bool // disable the alternate screen
|
|
|
|
w int
|
|
h int
|
|
|
|
oscreen consoleInfo
|
|
ocursor cursorInfo
|
|
cursorStyle CursorStyle
|
|
oimode uint32
|
|
oomode uint32
|
|
cells CellBuffer
|
|
focusEnable bool
|
|
|
|
mouseEnabled bool
|
|
wg sync.WaitGroup
|
|
eventQ chan Event
|
|
stopQ chan struct{}
|
|
finiOnce sync.Once
|
|
|
|
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")
|
|
procGetLargestConsoleWindowSize = k32.NewProc("GetLargestConsoleWindowSize")
|
|
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 if this is processed
|
|
vtReverse = "\x1b[7m"
|
|
vtSetFg = "\x1b[38;5;%dm"
|
|
vtSetBg = "\x1b[48;5;%dm"
|
|
vtSetFgRGB = "\x1b[38;2;%d;%d;%dm" // RGB
|
|
vtSetBgRGB = "\x1b[48;2;%d;%d;%dm" // RGB
|
|
vtCursorDefault = "\x1b[0 q"
|
|
vtCursorBlinkingBlock = "\x1b[1 q"
|
|
vtCursorSteadyBlock = "\x1b[2 q"
|
|
vtCursorBlinkingUnderline = "\x1b[3 q"
|
|
vtCursorSteadyUnderline = "\x1b[4 q"
|
|
vtCursorBlinkingBar = "\x1b[5 q"
|
|
vtCursorSteadyBar = "\x1b[6 q"
|
|
vtDisableAm = "\x1b[?7l"
|
|
vtEnableAm = "\x1b[?7h"
|
|
vtEnterCA = "\x1b[?1049h\x1b[22;0;0t"
|
|
vtExitCA = "\x1b[?1049l\x1b[23;0;0t"
|
|
vtDoubleUnderline = "\x1b[4:2m"
|
|
vtCurlyUnderline = "\x1b[4:3m"
|
|
vtDottedUnderline = "\x1b[4:4m"
|
|
vtDashedUnderline = "\x1b[4:5m"
|
|
)
|
|
|
|
var vtCursorStyles = map[CursorStyle]string{
|
|
CursorStyleDefault: vtCursorDefault,
|
|
CursorStyleBlinkingBlock: vtCursorBlinkingBlock,
|
|
CursorStyleSteadyBlock: vtCursorSteadyBlock,
|
|
CursorStyleBlinkingUnderline: vtCursorBlinkingUnderline,
|
|
CursorStyleSteadyUnderline: vtCursorSteadyUnderline,
|
|
CursorStyleBlinkingBar: vtCursorBlinkingBar,
|
|
CursorStyleSteadyBar: vtCursorSteadyBar,
|
|
}
|
|
|
|
// 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 &baseScreen{screenImpl: &cScreen{}}, nil
|
|
}
|
|
|
|
func (s *cScreen) Init() error {
|
|
s.eventQ = 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 VT output mode is extremely poor.
|
|
// The color palette will scroll even though characters do not, when
|
|
// emitting stuff for the last character. In the future we might change this to
|
|
// look at specific versions of ConEmu if they fix the bug.
|
|
// We can also try disabling auto margin mode.
|
|
tryVt := true
|
|
if os.Getenv("ConEmuPID") != "" {
|
|
s.truecolor = false
|
|
tryVt = false
|
|
}
|
|
switch os.Getenv("TCELL_TRUECOLOR") {
|
|
case "disable":
|
|
s.truecolor = false
|
|
case "enable":
|
|
s.truecolor = true
|
|
tryVt = true
|
|
}
|
|
|
|
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 | modeExtendFlg)
|
|
|
|
// If a user needs to force old style console, they may do so
|
|
// by setting TCELL_VTMODE to disable. This is an undocumented safety net for now.
|
|
// It may be removed in the future. (This mostly exists because of ConEmu.)
|
|
switch os.Getenv("TCELL_VTMODE") {
|
|
case "disable":
|
|
tryVt = false
|
|
case "enable":
|
|
tryVt = true
|
|
}
|
|
switch os.Getenv("TCELL_ALTSCREEN") {
|
|
case "enable":
|
|
s.disableAlt = false // also the default
|
|
case "disable":
|
|
s.disableAlt = true
|
|
}
|
|
if tryVt {
|
|
s.setOutMode(modeVtOutput | modeNoAutoNL | modeCookedOut | modeUnderline)
|
|
var om uint32
|
|
s.getOutMode(&om)
|
|
if om&modeVtOutput == modeVtOutput {
|
|
s.vten = true
|
|
} else {
|
|
s.truecolor = false
|
|
s.setOutMode(0)
|
|
}
|
|
} else {
|
|
s.setOutMode(0)
|
|
}
|
|
|
|
s.Unlock()
|
|
|
|
return s.engage()
|
|
}
|
|
|
|
func (s *cScreen) CharacterSet() string {
|
|
// We are always UTF-16LE on Windows
|
|
return "UTF-16LE"
|
|
}
|
|
|
|
func (s *cScreen) EnableMouse(...MouseFlags) {
|
|
s.Lock()
|
|
s.mouseEnabled = true
|
|
s.enableMouse(true)
|
|
s.Unlock()
|
|
}
|
|
|
|
func (s *cScreen) DisableMouse() {
|
|
s.Lock()
|
|
s.mouseEnabled = false
|
|
s.enableMouse(false)
|
|
s.Unlock()
|
|
}
|
|
|
|
func (s *cScreen) enableMouse(on bool) {
|
|
if on {
|
|
s.setInMode(modeResizeEn | modeMouseEn | modeExtendFlg)
|
|
} else {
|
|
s.setInMode(modeResizeEn | modeExtendFlg)
|
|
}
|
|
}
|
|
|
|
// Windows lacks bracketed paste (for now)
|
|
|
|
func (s *cScreen) EnablePaste() {}
|
|
|
|
func (s *cScreen) DisablePaste() {}
|
|
|
|
func (s *cScreen) EnableFocus() {
|
|
s.Lock()
|
|
s.focusEnable = true
|
|
s.Unlock()
|
|
}
|
|
|
|
func (s *cScreen) DisableFocus() {
|
|
s.Lock()
|
|
s.focusEnable = false
|
|
s.Unlock()
|
|
}
|
|
|
|
func (s *cScreen) Fini() {
|
|
s.finiOnce.Do(func() {
|
|
close(s.quit)
|
|
s.disengage()
|
|
})
|
|
}
|
|
|
|
func (s *cScreen) disengage() {
|
|
s.Lock()
|
|
if !s.running {
|
|
s.Unlock()
|
|
return
|
|
}
|
|
s.running = false
|
|
stopQ := s.stopQ
|
|
_, _, _ = procSetEvent.Call(uintptr(s.cancelflag))
|
|
close(stopQ)
|
|
s.Unlock()
|
|
|
|
s.wg.Wait()
|
|
|
|
if s.vten {
|
|
s.emitVtString(vtCursorStyles[CursorStyleDefault])
|
|
s.emitVtString(vtEnableAm)
|
|
if !s.disableAlt {
|
|
s.emitVtString(vtExitCA)
|
|
}
|
|
} else if !s.disableAlt {
|
|
s.clearScreen(StyleDefault, s.vten)
|
|
s.setCursorPos(0, 0, false)
|
|
}
|
|
s.setCursorInfo(&s.ocursor)
|
|
s.setBufferSize(int(s.oscreen.size.x), int(s.oscreen.size.y))
|
|
s.setInMode(s.oimode)
|
|
s.setOutMode(s.oomode)
|
|
_, _, _ = procSetConsoleTextAttribute.Call(
|
|
uintptr(s.out),
|
|
uintptr(s.mapStyle(StyleDefault)))
|
|
}
|
|
|
|
func (s *cScreen) engage() error {
|
|
s.Lock()
|
|
defer s.Unlock()
|
|
if s.running {
|
|
return errors.New("already engaged")
|
|
}
|
|
s.stopQ = make(chan struct{})
|
|
cf, _, e := procCreateEvent.Call(
|
|
uintptr(0),
|
|
uintptr(1),
|
|
uintptr(0),
|
|
uintptr(0))
|
|
if cf == uintptr(0) {
|
|
return e
|
|
}
|
|
s.running = true
|
|
s.cancelflag = syscall.Handle(cf)
|
|
s.enableMouse(s.mouseEnabled)
|
|
|
|
if s.vten {
|
|
s.setOutMode(modeVtOutput | modeNoAutoNL | modeCookedOut | modeUnderline)
|
|
if !s.disableAlt {
|
|
s.emitVtString(vtEnterCA)
|
|
}
|
|
s.emitVtString(vtDisableAm)
|
|
} else {
|
|
s.setOutMode(0)
|
|
}
|
|
|
|
s.clearScreen(s.style, s.vten)
|
|
s.hideCursor()
|
|
|
|
s.cells.Invalidate()
|
|
s.hideCursor()
|
|
s.resize()
|
|
s.draw()
|
|
s.doCursor()
|
|
|
|
s.wg.Add(1)
|
|
go s.scanInput(s.stopQ)
|
|
return nil
|
|
}
|
|
|
|
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)
|
|
s.emitVtString(vtCursorStyles[s.cursorStyle])
|
|
} 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) SetCursorStyle(cs CursorStyle) {
|
|
s.Lock()
|
|
if !s.fini {
|
|
if _, ok := vtCursorStyles[cs]; ok {
|
|
s.cursorStyle = cs
|
|
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.vten)
|
|
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
|
|
)
|
|
|
|
type mouseRecord struct {
|
|
x int16
|
|
y int16
|
|
btns uint32
|
|
mod uint32
|
|
flags uint32
|
|
}
|
|
|
|
type focusRecord struct {
|
|
focused int32 // actually BOOL
|
|
}
|
|
|
|
const (
|
|
mouseHWheeled uint32 = 0x8
|
|
mouseVWheeled uint32 = 0x4
|
|
// mouseDoubleClick uint32 = 0x2
|
|
// 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,
|
|
vkEscape: KeyEscape,
|
|
vkSpace: ' ',
|
|
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
|
|
ctrl := (cks & (0x0008 | 0x0004)) != 0
|
|
// Left or right alt
|
|
alt := (cks & (0x0002 | 0x0001)) != 0
|
|
// Filter out ctrl+alt (it means AltGr)
|
|
if !(ctrl && alt) {
|
|
if ctrl {
|
|
mm |= ModCtrl
|
|
}
|
|
if alt {
|
|
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) postEvent(ev Event) {
|
|
select {
|
|
case s.eventQ <- ev:
|
|
case <-s.quit:
|
|
}
|
|
}
|
|
|
|
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 {
|
|
// it's 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)))
|
|
|
|
case focusEvent:
|
|
var focus focusRecord
|
|
focus.focused = geti32(rec.data[0:])
|
|
s.Lock()
|
|
enabled := s.focusEnable
|
|
s.Unlock()
|
|
if enabled {
|
|
s.postEvent(NewEventFocus(focus.focused != 0))
|
|
}
|
|
|
|
default:
|
|
}
|
|
default:
|
|
return er
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *cScreen) scanInput(stopQ chan struct{}) {
|
|
defer s.wg.Done()
|
|
for {
|
|
select {
|
|
case <-stopQ:
|
|
return
|
|
default:
|
|
}
|
|
if e := s.getConsoleInput(); e != nil {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *cScreen) Colors() int {
|
|
if s.vten {
|
|
return 1 << 24
|
|
}
|
|
// Windows console can display 8 colors, in either low or high intensity
|
|
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 && f != ColorReset {
|
|
fa = mapColor2RGB(f)
|
|
}
|
|
if b != ColorDefault && b != ColorReset {
|
|
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) 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|AttrDoubleUnderline|AttrCurlyUnderline|AttrDottedUnderline|AttrDashedUnderline) != 0 {
|
|
esc.WriteString(vtUnderline)
|
|
// legacy ConHost does not understand these but Terminal does
|
|
if (attrs & AttrDoubleUnderline) != 0 {
|
|
esc.WriteString(vtDoubleUnderline)
|
|
} else if (attrs & AttrCurlyUnderline) != 0 {
|
|
esc.WriteString(vtCurlyUnderline)
|
|
} else if (attrs & AttrDottedUnderline) != 0 {
|
|
esc.WriteString(vtDottedUnderline)
|
|
} else if (attrs & AttrDashedUnderline) != 0 {
|
|
esc.WriteString(vtDashedUnderline)
|
|
}
|
|
}
|
|
if attrs&AttrReverse != 0 {
|
|
esc.WriteString(vtReverse)
|
|
}
|
|
if fg.IsRGB() {
|
|
r, g, b := fg.RGB()
|
|
_, _ = fmt.Fprintf(esc, vtSetFgRGB, r, g, b)
|
|
} else if fg.Valid() {
|
|
_, _ = fmt.Fprintf(esc, vtSetFg, fg&0xff)
|
|
}
|
|
if bg.IsRGB() {
|
|
r, g, b := bg.RGB()
|
|
_, _ = fmt.Fprintf(esc, vtSetBgRGB, r, g, b)
|
|
} else if bg.Valid() {
|
|
_, _ = fmt.Fprintf(esc, vtSetBg, bg&0xff)
|
|
}
|
|
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, s.vten)
|
|
|
|
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 allocations.
|
|
buf := make([]uint16, 0, s.w)
|
|
wcs := buf[:]
|
|
lstyle := styleInvalid
|
|
|
|
lx, ly := -1, -1
|
|
ra := make([]rune, 1)
|
|
|
|
for y := 0; y < s.h; y++ {
|
|
for x := 0; x < 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 = StyleDefault
|
|
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 = styleInvalid
|
|
}
|
|
}
|
|
|
|
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, vtEnable bool) {
|
|
if vtEnable {
|
|
// 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) SetSize(w, h int) {
|
|
xy, _, _ := procGetLargestConsoleWindowSize.Call(uintptr(s.out))
|
|
|
|
// xy is little endian packed
|
|
y := int(xy >> 16)
|
|
x := int(xy & 0xffff)
|
|
|
|
if x == 0 || y == 0 {
|
|
return
|
|
}
|
|
|
|
// This is a hacky workaround for Windows Terminal.
|
|
// Essentially Windows Terminal (Windows 11) does not support application
|
|
// initiated resizing. To detect this, we look for an extremely large size
|
|
// for the maximum width. If it is > 500, then this is almost certainly
|
|
// Windows Terminal, and won't support this. (Note that the legacy console
|
|
// does support application resizing.)
|
|
if x >= 500 {
|
|
return
|
|
}
|
|
|
|
s.setBufferSize(x, y)
|
|
r := rect{0, 0, int16(w - 1), int16(h - 1)}
|
|
_, _, _ = procSetConsoleWindowInfo.Call(
|
|
uintptr(s.out),
|
|
uintptr(1),
|
|
uintptr(unsafe.Pointer(&r)))
|
|
|
|
s.resize()
|
|
}
|
|
|
|
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)))
|
|
select {
|
|
case s.eventQ <- NewEventResize(w, h):
|
|
default:
|
|
}
|
|
}
|
|
|
|
func (s *cScreen) clearScreen(style Style, vtEnable bool) {
|
|
if vtEnable {
|
|
s.sendVtStyle(style)
|
|
row := strings.Repeat(" ", s.w)
|
|
for y := 0; y < s.h; y++ {
|
|
s.setCursorPos(0, y, vtEnable)
|
|
s.emitVtString(row)
|
|
}
|
|
s.setCursorPos(0, 0, vtEnable)
|
|
|
|
} 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
|
|
modeExtendFlg uint32 = 0x0080
|
|
modeMouseEn = 0x0010
|
|
modeResizeEn = 0x0008
|
|
// modeCooked = 0x0001
|
|
// modeVtInput = 0x0200
|
|
|
|
// Output modes
|
|
modeCookedOut uint32 = 0x0001
|
|
modeVtOutput = 0x0004
|
|
modeNoAutoNL = 0x0008
|
|
modeUnderline = 0x0010 // ENABLE_LVB_GRID_WORLDWIDE, needed for underlines
|
|
// modeWrapEOL = 0x0002
|
|
)
|
|
|
|
func (s *cScreen) setInMode(mode uint32) {
|
|
_, _, _ = procSetConsoleMode.Call(
|
|
uintptr(s.in),
|
|
uintptr(mode))
|
|
}
|
|
|
|
func (s *cScreen) setOutMode(mode uint32) {
|
|
_, _, _ = procSetConsoleMode.Call(
|
|
uintptr(s.out),
|
|
uintptr(mode))
|
|
}
|
|
|
|
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(_ rune, _ string) {
|
|
}
|
|
|
|
func (s *cScreen) UnregisterRuneFallback(_ rune) {
|
|
}
|
|
|
|
func (s *cScreen) CanDisplay(_ rune, _ bool) bool {
|
|
// We presume we can display anything -- we're Unicode.
|
|
// (Sadly this not precisely true. Combining characters 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
|
|
}
|
|
|
|
func (s *cScreen) Suspend() error {
|
|
s.disengage()
|
|
return nil
|
|
}
|
|
|
|
func (s *cScreen) Resume() error {
|
|
return s.engage()
|
|
}
|
|
|
|
func (s *cScreen) Tty() (Tty, bool) {
|
|
return nil, false
|
|
}
|
|
|
|
func (s *cScreen) GetCells() *CellBuffer {
|
|
return &s.cells
|
|
}
|
|
|
|
func (s *cScreen) EventQ() chan Event {
|
|
return s.eventQ
|
|
}
|
|
|
|
func (s *cScreen) StopQ() <-chan struct{} {
|
|
return s.quit
|
|
}
|