// Copyright 2026 The TCell Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this 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.
//go:build windows
// +build windows
package tty
import (
"encoding/binary"
"errors"
"fmt"
"sync"
"syscall"
"time"
"unicode/utf16"
"unsafe"
)
var (
k32 = syscall.NewLazyDLL("kernel32.dll")
)
var (
procReadConsoleInput = k32.NewProc("ReadConsoleInputW")
procGetNumberOfConsoleInputEvents = k32.NewProc("GetNumberOfConsoleInputEvents")
procFlushConsoleInputBuffer = k32.NewProc("FlushConsoleInputBuffer")
procWaitForMultipleObjects = k32.NewProc("WaitForMultipleObjects")
procSetConsoleMode = k32.NewProc("SetConsoleMode")
procGetConsoleMode = k32.NewProc("GetConsoleMode")
procGetConsoleScreenBufferInfo = k32.NewProc("GetConsoleScreenBufferInfo")
procCreateEvent = k32.NewProc("CreateEventW")
procSetEvent = k32.NewProc("SetEvent")
)
const (
keyEvent uint16 = 1
mouseEvent uint16 = 2
resizeEvent uint16 = 4
menuEvent uint16 = 8 // don't use
focusEvent uint16 = 16
)
const (
w32Infinite = ^uintptr(0)
w32WaitObject0 = uintptr(0)
)
const (
// Input modes
modeExtendFlg = uint32(0x0080)
modeMouseEn = uint32(0x0010)
modeResizeEn = uint32(0x0008)
modeVtInput = uint32(0x0200)
// modeCooked = uint32(0x0001)
// Output modes
modeCookedOut = uint32(0x0001)
modeVtOutput = uint32(0x0004)
modeNoAutoNL = uint32(0x0008)
modeUnderline = uint32(0x0010) // ENABLE_LVB_GRID_WORLDWIDE, needed for underlines
// modeWrapEOL = uint32(0x0002)
)
type coord struct {
x int16
y int16
}
type rect struct {
left int16
top int16
right int16
bottom int16
}
type consoleInfo struct {
size coord
pos coord
attrs uint16
win rect
maxsz coord
}
// windowSize returns the size of the visible window. Before the alternate
// screen is entered, the buffer includes scrollback and is taller.
func (ci *consoleInfo) windowSize() (cols, rows uint16) {
return uint16(ci.win.right - ci.win.left + 1), uint16(ci.win.bottom - ci.win.top + 1)
}
type inputRecord struct {
typ uint16
_ uint16
data [16]byte
}
func encodeWinKeyRecord(data [16]byte, surrogate *rune) []byte {
keyDown := binary.LittleEndian.Uint32(data[0:]) != 0
repeat := binary.LittleEndian.Uint16(data[4:])
virtualKey := binary.LittleEndian.Uint16(data[6:])
scanCode := binary.LittleEndian.Uint16(data[8:])
// we normally only expect to see ascii, but paste data may come in as UTF-16.
wc := rune(binary.LittleEndian.Uint16(data[10:]))
controlState := binary.LittleEndian.Uint32(data[12:])
if virtualKey != 0 || scanCode != 0 {
kd := 0
if keyDown {
kd = 1
}
return fmt.Appendf(nil, "\x1b[%d;%d;%d;%d;%d;%d_",
virtualKey, scanCode, wc, kd, controlState, max(1, repeat))
}
if !keyDown {
return nil
}
var encoded []byte
decodedRunes := decodeUTF16Rune(surrogate, wc)
for range max(1, repeat) {
for _, decoded := range decodedRunes {
encoded = append(encoded, []byte(string(decoded))...)
}
}
return encoded
}
type winTty struct {
buf chan byte
out syscall.Handle
in syscall.Handle
cancelFlag syscall.Handle
running bool
stopQ chan struct{}
resizeQ chan<- bool
cols uint16
rows uint16
pair []uint16 // for surrogate pairs (UTF-16)
oimode uint32 // original input mode
oomode uint32 // original output mode
oscreen consoleInfo
wg sync.WaitGroup
surrogate rune
sync.Mutex
}
func (w *winTty) Read(b []byte) (int, error) {
// first character read blocks
var num int
select {
case c := <-w.buf:
b[0] = c
num++
case <-w.stopQ:
// stopping, so make sure we eat everything, which might require
// very short sleeps to ensure all buffered data is consumed.
}
// second character read is non-blocking
for ; num < len(b); num++ {
select {
case c := <-w.buf:
b[num] = c
case <-time.After(time.Millisecond * 10):
return num, nil
}
}
return num, nil
}
func (w *winTty) Write(b []byte) (int, error) {
esc := utf16.Encode([]rune(string(b)))
if len(esc) > 0 {
err := syscall.WriteConsole(w.out, &esc[0], uint32(len(esc)), nil, nil)
if err != nil {
return 0, err
}
}
return len(b), nil
}
func (w *winTty) Close() error {
_ = syscall.Close(w.in)
_ = syscall.Close(w.out)
return nil
}
func (w *winTty) Drain() error {
close(w.stopQ)
time.Sleep(time.Millisecond * 10)
_, _, _ = procSetEvent.Call(uintptr(w.cancelFlag))
return nil
}
func (w *winTty) getConsoleInput() error {
// cancelFlag comes first as WaitForMultipleObjects returns the lowest index
// in the event that both events are signaled.
waitObjects := []syscall.Handle{w.cancelFlag, w.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: // w.cancelFlag
return errors.New("cancelled")
case w32WaitObject0 + 1: // w.in
var nrec int32
rv, _, er := procGetNumberOfConsoleInputEvents.Call(
uintptr(w.in),
uintptr(unsafe.Pointer(&nrec)))
if rv == 0 {
return er
}
rec := make([]inputRecord, max(nrec, 1))
rv, _, er = procReadConsoleInput.Call(
uintptr(w.in),
uintptr(unsafe.Pointer(&rec[0])),
uintptr(nrec),
uintptr(unsafe.Pointer(&nrec)))
if rv == 0 {
return er
}
loop:
for i := range nrec {
ir := rec[i]
switch ir.typ {
case keyEvent:
for _, chr := range encodeWinKeyRecord(ir.data, &w.surrogate) {
select {
case w.buf <- chr:
case <-w.stopQ:
break loop
}
}
case resizeEvent:
// The event carries the buffer size, which includes scrollback
// outside the alternate screen, so read the window instead.
var ci consoleInfo
rv, _, _ := procGetConsoleScreenBufferInfo.Call(uintptr(w.out), uintptr(unsafe.Pointer(&ci)))
w.Lock()
if rv != 0 {
w.cols, w.rows = ci.windowSize()
}
if w.resizeQ != nil {
select {
case w.resizeQ <- true:
default:
}
}
w.Unlock()
default:
}
}
return nil
default:
return er
}
}
func (w *winTty) scanInput() {
defer w.wg.Done()
for {
if e := w.getConsoleInput(); e != nil {
return
}
}
}
func (w *winTty) Start() error {
if w.running {
return nil
}
_, _, _ = procFlushConsoleInputBuffer.Call(uintptr(w.in))
w.stopQ = make(chan struct{})
cf, _, err := procCreateEvent.Call(
uintptr(0),
uintptr(1),
uintptr(0),
uintptr(0))
if cf == uintptr(0) {
return err
}
w.running = true
w.cancelFlag = syscall.Handle(cf)
_, _, _ = procSetConsoleMode.Call(uintptr(w.in),
uintptr(modeVtInput|modeResizeEn|modeExtendFlg))
_, _, _ = procSetConsoleMode.Call(uintptr(w.out),
uintptr(modeVtOutput|modeNoAutoNL|modeCookedOut|modeUnderline))
w.wg.Add(1)
go w.scanInput()
return nil
}
func (w *winTty) Stop() error {
w.wg.Wait()
_, _, _ = procSetConsoleMode.Call(uintptr(w.in), uintptr(w.oimode))
_, _, _ = procSetConsoleMode.Call(uintptr(w.out), uintptr(w.oomode))
_, _, _ = procFlushConsoleInputBuffer.Call(uintptr(w.in))
w.running = false
return nil
}
func (tty *winTty) NotifyResize(resizeQ chan<- bool) {
tty.Lock()
tty.resizeQ = resizeQ
tty.Unlock()
}
func (w *winTty) WindowSize() (WindowSize, error) {
w.Lock()
defer w.Unlock()
return WindowSize{Width: int(w.cols), Height: int(w.rows)}, nil
}
func NewDevTty() (Tty, error) {
w := &winTty{}
var err error
w.in, err = syscall.Open("CONIN$", syscall.O_RDWR, 0)
if err != nil {
return nil, err
}
w.out, err = syscall.Open("CONOUT$", syscall.O_RDWR, 0)
if err != nil {
_ = syscall.Close(w.in)
return nil, err
}
w.buf = make(chan byte, 128)
_, _, _ = procGetConsoleScreenBufferInfo.Call(uintptr(w.out), uintptr(unsafe.Pointer(&w.oscreen)))
_, _, _ = procGetConsoleMode.Call(uintptr(w.out), uintptr(unsafe.Pointer(&w.oomode)))
_, _, _ = procGetConsoleMode.Call(uintptr(w.in), uintptr(unsafe.Pointer(&w.oimode)))
w.cols, w.rows = w.oscreen.windowSize()
return w, nil
}
func NewDevTtyFromDev(dev string) (Tty, error) {
return nil, errors.New("No tty device on Windows")
}
func NewStdIoTty() (Tty, error) {
w := &winTty{}
w.in = syscall.Stdin
w.out = syscall.Stdout
w.buf = make(chan byte, 128)
_, _, _ = procGetConsoleScreenBufferInfo.Call(uintptr(w.out), uintptr(unsafe.Pointer(&w.oscreen)))
if r, _, err := procGetConsoleMode.Call(uintptr(w.out), uintptr(unsafe.Pointer(&w.oomode))); r == 0 || err != nil {
return nil, errors.New("output is not a terminal")
}
if r, _, err := procGetConsoleMode.Call(uintptr(w.in), uintptr(unsafe.Pointer(&w.oimode))); r == 0 || err != nil {
return nil, errors.New("input is not a terminal")
}
w.cols, w.rows = w.oscreen.windowSize()
return w, nil
}