1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-01-24 05:36:19 +02:00
Stefan Haller 7ccb871a45 Bump gocui
... and import stefanhaller's tcell fork for real rather than just replacing it

This solves the problem that people trying to
"go install github.com/jesseduffield/lazygit@latest" would get the error

go: github.com/jesseduffield/lazygit@latest (in github.com/jesseduffield/lazygit@v0.40.0):
  The go.mod file for the module providing named packages contains one or
  more replace directives. It must not contain directives that would cause
  it to be interpreted differently than if it were the main module.
2023-08-06 12:03:23 +02:00

1334 lines
28 KiB
Go

//go:build windows
// +build windows
// Copyright 2022 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
running bool
w int
h int
oscreen consoleInfo
ocursor cursorInfo
cursorStyle CursorStyle
oimode uint32
oomode uint32
cells CellBuffer
finiOnce sync.Once
mouseEnabled bool
wg sync.WaitGroup
stopQ chan struct{}
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 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"
)
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 &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
// 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.
if os.Getenv("ConEmuPID") != "" {
s.truecolor = false
}
switch os.Getenv("TCELL_TRUECOLOR") {
case "disable":
s.truecolor = false
case "enable":
s.truecolor = 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)
// 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 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() {}
func (s *cScreen) DisableFocus() {}
func (s *cScreen) Fini() {
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.setInMode(s.oimode)
s.setOutMode(s.oomode)
s.setBufferSize(int(s.oscreen.size.x), int(s.oscreen.size.y))
s.clearScreen(StyleDefault, false)
s.setCursorPos(0, 0, false)
s.setCursorInfo(&s.ocursor)
_, _, _ = 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)
} 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
}
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) ChannelEvents(ch chan<- Event, quit <-chan struct{}) {
defer close(ch)
for {
select {
case <-quit:
return
case <-s.stopQ:
return
case ev := <-s.evch:
select {
case <-quit:
return
case <-s.stopQ:
return
case ch <- ev:
}
}
}
}
func (s *cScreen) PollEvent() Event {
select {
case <-s.stopQ:
return nil
case ev := <-s.evch:
return ev
}
}
func (s *cScreen) HasPendingEvent() bool {
return len(s.evch) > 0
}
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 // don't use
)
type mouseRecord struct {
x int16
y int16
btns uint32
mod uint32
flags uint32
}
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
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.PostEventWait(NewEventKey(KeyBacktab, 0,
ModNone))
} else {
s.PostEventWait(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.PostEventWait(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.PostEventWait(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.PostEventWait(NewEventResize(int(rrec.x), int(rrec.y)))
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) 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, primary rune, combining []rune, style Style) {
s.Lock()
if !s.fini {
s.cells.SetContent(x, y, primary, combining, style)
}
s.Unlock()
}
func (s *cScreen) GetContent(x, y int) (rune, []rune, Style, int) {
s.Lock()
primary, combining, style, width := s.cells.GetContent(x, y)
s.Unlock()
return primary, combining, 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.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.
if s.clear {
s.clearScreen(s.style, s.vten)
s.clear = false
s.cells.Invalidate()
}
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)))
_ = 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, 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
// 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()
}