//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 title string w int h int oscreen consoleInfo ocursor cursorInfo cursorStyle CursorStyle cursorColor Color 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" vtUnderColor = "\x1b[58:5:%dm" vtUnderColorRGB = "\x1b[58:2::%d:%d:%dm" vtUnderColorReset = "\x1b[59m" vtEnterUrl = "\x1b]8;%s;%s\x1b\\" // NB arg 1 is id, arg 2 is url vtExitUrl = "\x1b]8;;\x1b\\" vtCursorColorRGB = "\x1b]12;#%02x%02x%02x\007" vtCursorColorReset = "\x1b]112\007" vtSaveTitle = "\x1b[22;2t" vtRestoreTitle = "\x1b[23;2t" vtSetTitle = "\x1b]2;%s\x1b\\" ) 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(vtCursorColorReset) s.emitVtString(vtEnableAm) if !s.disableAlt { s.emitVtString(vtRestoreTitle) 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(vtSaveTitle) s.emitVtString(vtEnterCA) } s.emitVtString(vtDisableAm) if s.title != "" { s.emitVtString(fmt.Sprintf(vtSetTitle, s.title)) } } 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]) if s.cursorColor == ColorReset { s.emitVtString(vtCursorColorReset) } else if s.cursorColor.Valid() { r, g, b := s.cursorColor.RGB() s.emitVtString(fmt.Sprintf(vtCursorColorRGB, r, g, b)) } } 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) SetCursor(cs CursorStyle, cc Color) { s.Lock() if !s.fini { if _, ok := vtCursorStyles[cs]; ok { s.cursorStyle = cs s.cursorColor = cc 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.fg, style.bg, style.attrs 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.fg, style.bg, style.attrs us, uc := style.ulStyle, style.ulColor esc.WriteString(vtSgr0) if attrs&(AttrBold|AttrDim) == AttrBold { esc.WriteString(vtBold) } if attrs&AttrBlink != 0 { esc.WriteString(vtBlink) } if us != UnderlineStyleNone { if uc == ColorReset { esc.WriteString(vtUnderColorReset) } else if uc.IsRGB() { r, g, b := uc.RGB() _, _ = fmt.Fprintf(esc, vtUnderColorRGB, int(r), int(g), int(b)) } else if uc.Valid() { _, _ = fmt.Fprintf(esc, vtUnderColor, uc&0xff) } esc.WriteString(vtUnderline) // legacy ConHost does not understand these but Terminal does switch us { case UnderlineStyleSolid: case UnderlineStyleDouble: esc.WriteString(vtDoubleUnderline) case UnderlineStyleCurly: esc.WriteString(vtCurlyUnderline) case UnderlineStyleDotted: esc.WriteString(vtDottedUnderline) case UnderlineStyleDashed: 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) } // URL string can be long, so don't send it unless we really need to if style.url != "" { _, _ = fmt.Fprintf(esc, vtEnterUrl, style.urlId, style.url) } else { esc.WriteString(vtExitUrl) } 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() } func (s *cScreen) SetTitle(title string) { s.Lock() s.title = title if s.vten { s.emitVtString(fmt.Sprintf(vtSetTitle, title)) } 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 }