// Copyright 2026 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. //go:build windows // +build windows package tcell import ( "encoding/binary" "errors" "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 ) type inputRecord struct { typ uint16 _ uint16 data [16]byte } type winTty struct { buf chan byte out syscall.Handle in syscall.Handle cancelFlag syscall.Handle running bool stopQ chan struct{} resizeCb func() 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. break } // 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 // rec := &inputRecord{} var nrec int32 rv, _, er := procGetNumberOfConsoleInputEvents.Call( uintptr(w.in), uintptr(unsafe.Pointer(&nrec))) rec := make([]inputRecord, nrec) 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: // we normally only expect to see ascii, but paste data may come in as UTF-16. wc := rune(binary.LittleEndian.Uint16(ir.data[10:])) if wc >= 0xD800 && wc <= 0xDBFF { // if it was a high surrogate, which happens for pasted UTF-16, // then save it until we get the low and can decode it. w.surrogate = wc continue } else if wc >= 0xDC00 && wc <= 0xDFFF { wc = utf16.DecodeRune(w.surrogate, wc) } w.surrogate = 0 for _, chr := range []byte(string(wc)) { // We normally expect only to see ASCII (win32-input-mode), // but apparently pasted data can arrive in UTF-16 here. select { case w.buf <- chr: case <-w.stopQ: break loop } } case resizeEvent: w.Lock() w.cols = binary.LittleEndian.Uint16(ir.data[0:]) w.rows = binary.LittleEndian.Uint16(ir.data[2:]) cb := w.resizeCb w.Unlock() if cb != nil { cb() } 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 { w.Lock() defer w.Unlock() if w.running { return errors.New("already engaged") } _, _, _ = 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() w.Lock() defer w.Unlock() _, _, _ = 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 (w *winTty) NotifyResize(cb func()) { w.resizeCb = cb } 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.rows = uint16(w.oscreen.size.y) w.cols = uint16(w.oscreen.size.x) return w, nil }