251227.15 · 33bb0b2e0d834ec7362da86f6c71de99ce4b146b
chore: move Tty to sub-subpackage (including WindowSize)
We also added compatibility aliases. The purpose of this is to break potential circular dependencies so that we can use symbols for TTYs and the WindowSize in the vt emulator and mock packages.
We want to then be able to add tests in the main tcell package, which can use these -- this move breaks the circular dependency.
reading declarations
mock/term.go
@@ -15,7 +15,7 @@ package mock import (- "github.com/gdamore/tcell/v3"+ "github.com/gdamore/tcell/v3/tty" "github.com/gdamore/tcell/v3/vt" ) @@ -55,10 +55,10 @@ func (mt *mockTerm) Write(b []byte) (n int, err error) { } // WindowSize obtains the dimensions of the window.-func (mt *mockTerm) WindowSize() (tcell.WindowSize, error) {+func (mt *mockTerm) WindowSize() (tty.WindowSize, error) { sz := mt.mb.GetSize() // No pixel sizes for now- return tcell.WindowSize{Width: int(sz.X), Height: int(sz.Y)}, nil+ return tty.WindowSize{Width: int(sz.X), Height: int(sz.Y)}, nil } // NotifyResize registers a channel to be signaled when a resize has occurred.@@ -95,7 +95,7 @@ func (mt *mockTerm) Bells() int { // uses the terminal. It also implements the Tty interface used // by tcell itself. type MockTerm interface {- tcell.Tty+ tty.Tty // Pos reports the current cursor position. Pos() vt.Coord
nonblock_bsd.go
@@ -1,43 +0,0 @@-// Copyright 2021 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 darwin || dragonfly || freebsd || netbsd || openbsd-// +build darwin dragonfly freebsd netbsd openbsd--package tcell--import (- "syscall"-- "golang.org/x/sys/unix"-)--// BSD systems use TIOC style ioctls.--// tcSetBufParams is used by the tty driver on UNIX systems to configure the-// buffering parameters (minimum character count and minimum wait time in msec.)-// This also waits for output to drain first.-func tcSetBufParams(fd int, vMin uint8, vTime uint8) error {- _ = syscall.SetNonblock(fd, true)- tio, err := unix.IoctlGetTermios(fd, unix.TIOCGETA)- if err != nil {- return err- }- tio.Cc[unix.VMIN] = vMin- tio.Cc[unix.VTIME] = vTime- if err = unix.IoctlSetTermios(fd, unix.TIOCSETAW, tio); err != nil {- return err- }- return nil-}
nonblock_unix.go
@@ -1,41 +0,0 @@-// Copyright 2021 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 linux || aix || zos || solaris-// +build linux aix zos solaris--package tcell--import (- "syscall"-- "golang.org/x/sys/unix"-)--// tcSetBufParams is used by the tty driver on UNIX systems to configure the-// buffering parameters (minimum character count and minimum wait time in msec.)-// This also waits for output to drain first.-func tcSetBufParams(fd int, vMin uint8, vTime uint8) error {- _ = syscall.SetNonblock(fd, true)- tio, err := unix.IoctlGetTermios(fd, unix.TCGETS)- if err != nil {- return err- }- tio.Cc[unix.VMIN] = vMin- tio.Cc[unix.VTIME] = vTime- if err = unix.IoctlSetTermios(fd, unix.TCSETSW, tio); err != nil {- return err- }- return nil-}
@@ -14,6 +14,8 @@ package tcell +import "github.com/gdamore/tcell/v3/tty"+ // EventResize is sent when the window size changes. type EventResize struct { EventTime@@ -43,17 +45,4 @@ func (ev *EventResize) PixelSize() (int, int) { return ev.ws.PixelWidth, ev.ws.PixelHeight } -type WindowSize struct {- Width int- Height int- PixelWidth int- PixelHeight int-}--// CellDimensions returns the dimensions of a single cell, in pixels-func (ws WindowSize) CellDimensions() (int, int) {- if ws.PixelWidth == 0 || ws.PixelHeight == 0 {- return 0, 0- }- return (ws.PixelWidth / ws.Width), (ws.PixelHeight / ws.Height)-}+type WindowSize = tty.WindowSize
stdin_unix.go
@@ -1,173 +0,0 @@-// Copyright 2021 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 aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos-// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris zos--package tcell--import (- "errors"- "fmt"- "os"- "os/signal"- "strconv"- "syscall"- "time"-- "golang.org/x/sys/unix"- "golang.org/x/term"-)--// stdIoTty is an implementation of the Tty API based upon stdin/stdout.-type stdIoTty struct {- fd int- in *os.File- out *os.File- saved *term.State- sig chan os.Signal- started bool-}--func (tty *stdIoTty) Read(b []byte) (int, error) {- return tty.in.Read(b)-}--func (tty *stdIoTty) Write(b []byte) (int, error) {- return tty.out.Write(b)-}--func (tty *stdIoTty) Close() error {- return nil-}--func (tty *stdIoTty) Start() error {- if tty.started {- return nil- }-- var err error- tty.in = os.Stdin- tty.out = os.Stdout- tty.fd = int(tty.in.Fd())-- if !term.IsTerminal(tty.fd) {- return errors.New("device is not a terminal")- }-- _ = tty.in.SetReadDeadline(time.Time{})- saved, err := term.MakeRaw(tty.fd) // also sets vMin and vTime- if err != nil {- return err- }- tty.saved = saved- tty.started = true-- return nil-}--func (tty *stdIoTty) Drain() error {- _ = tty.in.SetReadDeadline(time.Now())- if err := tcSetBufParams(tty.fd, 0, 0); err != nil {- return err- }- return nil-}--func (tty *stdIoTty) Stop() error {- if err := term.Restore(tty.fd, tty.saved); err != nil {- return err- }- _ = tty.in.SetReadDeadline(time.Now())-- tty.NotifyResize(nil)-- tty.started = false-- return nil-}--func (tty *stdIoTty) WindowSize() (WindowSize, error) {- size := WindowSize{}- ws, err := unix.IoctlGetWinsize(tty.fd, unix.TIOCGWINSZ)- if err != nil {- return size, err- }- w := int(ws.Col)- h := int(ws.Row)- if w == 0 {- w, _ = strconv.Atoi(os.Getenv("COLUMNS"))- }- if w == 0 {- w = 80 // default- }- if h == 0 {- h, _ = strconv.Atoi(os.Getenv("LINES"))- }- if h == 0 {- h = 25 // default- }- size.Width = w- size.Height = h- size.PixelWidth = int(ws.Xpixel)- size.PixelHeight = int(ws.Ypixel)- return size, nil-}--func (tty *stdIoTty) NotifyResize(resizeQ chan<- bool) {-- sigQ := tty.sig- tty.sig = nil-- if sigQ != nil {- signal.Stop(sigQ)- close(sigQ)- }-- if resizeQ == nil {- return- }-- sigQ = make(chan os.Signal, 1)- signal.Notify(sigQ, syscall.SIGWINCH)-- tty.sig = sigQ-- go func() {- for range sigQ {- select {- case resizeQ <- true:- default: // queue full, so nvm.- }- }- }()-}--// NewStdioTty opens a tty using standard input/output.-func NewStdIoTty() (Tty, error) {- tty := &stdIoTty{- sig: make(chan os.Signal),- in: os.Stdin,- out: os.Stdout,- }- var err error- tty.fd = int(tty.in.Fd())- if !term.IsTerminal(tty.fd) {- return nil, errors.New("not a terminal")- }- if tty.saved, err = term.GetState(tty.fd); err != nil {- return nil, fmt.Errorf("failed to get state: %w", err)- }- return tty, nil-}
@@ -14,56 +14,21 @@ package tcell -import "io"+import "github.com/gdamore/tcell/v3/tty" -// Tty is an abstraction of a tty (traditionally "teletype"). This allows applications to-// provide for alternate backends, as there are situations where the traditional /dev/tty-// does not work, or where more flexible handling is required. This interface is for use-// with the terminfo-style based API. It extends the io.ReadWriter API. It is reasonable-// that the implementation might choose to use different underlying files for the Reader-// and Writer sides of this API, as part of it's internal implementation.-//-// Note that the consumer of these interfaces will provide mutual exclusion guarantees-// for the methods. Implementations need only be concerned about locking for any-// asynchronous functions that they use (e.g. signal handlers.) The exception to this-// is that Read and Write may be called concurrently to each other (but only after-// a successful Start), and Stop may be called while an outstanding Read or Write-// call is pending. (Stop should interrupt any blocking read.)-type Tty interface {- // Start is used to activate the Tty for use. Upon return the terminal should be- // in raw mode, non-blocking, etc. The implementation should take care of saving- // any state that is required so that it may be restored when Stop is called.- // Start must be idempotent.- Start() error-- // Stop is used to stop using this Tty instance. This may be a suspend, so that other- // terminal based applications can run in the foreground. Implementations should- // restore any state collected at Start(), and return to ordinary blocking mode, etc.- // Drain is called first to drain the input. Once this is called, no more Read- // or Write calls will be made until Start is called again.- Stop() error-- // Drain is called before Stop, and ensures that the reader will wake up appropriately- // if it was blocked. This workaround is required for /dev/tty on certain UNIX systems- // to ensure that Read() does not block forever. This typically arranges for the tty driver- // to send data immediately (e.g. VMIN and VTIME both set zero) and sets a deadline on input.- // Implementations may reasonably make this a no-op. There will still be control sequences- // emitted between the time this is called, and when Stop is called.- Drain() error+type Tty = tty.Tty - // NotifyResize is used to post a signal that will be written to (non-blocking) if the- // system detects that a resize event happened. If the channel is null, then the caller- // does not desire such notifications (or no longer desires them.)- // The standard UNIX implementation links this to a handler for SIGWINCH.- //- // If window resize events are delivered inline as part of Read, then the implementation may stub this.- // If the caller determines that the underlying terminal can deliver notifications without OS support- // (i.e. the terminal supports in-band resize notifications), then it may not call this function at all.- NotifyResize(chan<- bool)+// NewDevTty obtains a default tty from the console or TTY (e.g. /dev/tty) for the process.+func NewDevTty() (Tty, error) {+ return tty.NewDevTty()+} - // WindowSize is called to determine the terminal dimensions. This might be determined- // by an ioctl or other means.- WindowSize() (WindowSize, error)+// NewDevTtyFromDev obtains a tty from the given device path. Not supported on Windows.+func NewDevTtyFromDev(dev string) (Tty, error) {+ return tty.NewDevTtyFromDev(dev)+} - io.ReadWriteCloser+// NewStdIoTty obtains a tty from stdin and stdout.+func NewStdIoTty() (Tty, error) {+ return tty.NewStdIoTty() }
@@ -0,0 +1,43 @@+// Copyright 2021 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 darwin || dragonfly || freebsd || netbsd || openbsd+// +build darwin dragonfly freebsd netbsd openbsd++package tty++import (+ "syscall"++ "golang.org/x/sys/unix"+)++// BSD systems use TIOC style ioctls.++// tcSetBufParams is used by the tty driver on UNIX systems to configure the+// buffering parameters (minimum character count and minimum wait time in msec.)+// This also waits for output to drain first.+func tcSetBufParams(fd int, vMin uint8, vTime uint8) error {+ _ = syscall.SetNonblock(fd, true)+ tio, err := unix.IoctlGetTermios(fd, unix.TIOCGETA)+ if err != nil {+ return err+ }+ tio.Cc[unix.VMIN] = vMin+ tio.Cc[unix.VTIME] = vTime+ if err = unix.IoctlSetTermios(fd, unix.TIOCSETAW, tio); err != nil {+ return err+ }+ return nil+}
@@ -0,0 +1,41 @@+// Copyright 2021 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 linux || aix || zos || solaris+// +build linux aix zos solaris++package tty++import (+ "syscall"++ "golang.org/x/sys/unix"+)++// tcSetBufParams is used by the tty driver on UNIX systems to configure the+// buffering parameters (minimum character count and minimum wait time in msec.)+// This also waits for output to drain first.+func tcSetBufParams(fd int, vMin uint8, vTime uint8) error {+ _ = syscall.SetNonblock(fd, true)+ tio, err := unix.IoctlGetTermios(fd, unix.TCGETS)+ if err != nil {+ return err+ }+ tio.Cc[unix.VMIN] = vMin+ tio.Cc[unix.VTIME] = vTime+ if err = unix.IoctlSetTermios(fd, unix.TCSETSW, tio); err != nil {+ return err+ }+ return nil+}
@@ -0,0 +1,173 @@+// Copyright 2021 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 aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos+// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris zos++package tty++import (+ "errors"+ "fmt"+ "os"+ "os/signal"+ "strconv"+ "syscall"+ "time"++ "golang.org/x/sys/unix"+ "golang.org/x/term"+)++// stdIoTty is an implementation of the Tty API based upon stdin/stdout.+type stdIoTty struct {+ fd int+ in *os.File+ out *os.File+ saved *term.State+ sig chan os.Signal+ started bool+}++func (tty *stdIoTty) Read(b []byte) (int, error) {+ return tty.in.Read(b)+}++func (tty *stdIoTty) Write(b []byte) (int, error) {+ return tty.out.Write(b)+}++func (tty *stdIoTty) Close() error {+ return nil+}++func (tty *stdIoTty) Start() error {+ if tty.started {+ return nil+ }++ var err error+ tty.in = os.Stdin+ tty.out = os.Stdout+ tty.fd = int(tty.in.Fd())++ if !term.IsTerminal(tty.fd) {+ return errors.New("device is not a terminal")+ }++ _ = tty.in.SetReadDeadline(time.Time{})+ saved, err := term.MakeRaw(tty.fd) // also sets vMin and vTime+ if err != nil {+ return err+ }+ tty.saved = saved+ tty.started = true++ return nil+}++func (tty *stdIoTty) Drain() error {+ _ = tty.in.SetReadDeadline(time.Now())+ if err := tcSetBufParams(tty.fd, 0, 0); err != nil {+ return err+ }+ return nil+}++func (tty *stdIoTty) Stop() error {+ if err := term.Restore(tty.fd, tty.saved); err != nil {+ return err+ }+ _ = tty.in.SetReadDeadline(time.Now())++ tty.NotifyResize(nil)++ tty.started = false++ return nil+}++func (tty *stdIoTty) WindowSize() (WindowSize, error) {+ size := WindowSize{}+ ws, err := unix.IoctlGetWinsize(tty.fd, unix.TIOCGWINSZ)+ if err != nil {+ return size, err+ }+ w := int(ws.Col)+ h := int(ws.Row)+ if w == 0 {+ w, _ = strconv.Atoi(os.Getenv("COLUMNS"))+ }+ if w == 0 {+ w = 80 // default+ }+ if h == 0 {+ h, _ = strconv.Atoi(os.Getenv("LINES"))+ }+ if h == 0 {+ h = 25 // default+ }+ size.Width = w+ size.Height = h+ size.PixelWidth = int(ws.Xpixel)+ size.PixelHeight = int(ws.Ypixel)+ return size, nil+}++func (tty *stdIoTty) NotifyResize(resizeQ chan<- bool) {++ sigQ := tty.sig+ tty.sig = nil++ if sigQ != nil {+ signal.Stop(sigQ)+ close(sigQ)+ }++ if resizeQ == nil {+ return+ }++ sigQ = make(chan os.Signal, 1)+ signal.Notify(sigQ, syscall.SIGWINCH)++ tty.sig = sigQ++ go func() {+ for range sigQ {+ select {+ case resizeQ <- true:+ default: // queue full, so nvm.+ }+ }+ }()+}++// NewStdioTty opens a tty using standard input/output.+func NewStdIoTty() (Tty, error) {+ tty := &stdIoTty{+ sig: make(chan os.Signal),+ in: os.Stdin,+ out: os.Stdout,+ }+ var err error+ tty.fd = int(tty.in.Fd())+ if !term.IsTerminal(tty.fd) {+ return nil, errors.New("not a terminal")+ }+ if tty.saved, err = term.GetState(tty.fd); err != nil {+ return nil, fmt.Errorf("failed to get state: %w", err)+ }+ return tty, nil+}
@@ -0,0 +1,69 @@+// Copyright 2025 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 tty++import "io"++// Tty is an abstraction of a tty (traditionally "teletype"). This allows applications to+// provide for alternate backends, as there are situations where the traditional /dev/tty+// does not work, or where more flexible handling is required. This interface is for use+// with the terminfo-style based API. It extends the io.ReadWriter API. It is reasonable+// that the implementation might choose to use different underlying files for the Reader+// and Writer sides of this API, as part of it's internal implementation.+//+// Note that the consumer of these interfaces will provide mutual exclusion guarantees+// for the methods. Implementations need only be concerned about locking for any+// asynchronous functions that they use (e.g. signal handlers.) The exception to this+// is that Read and Write may be called concurrently to each other (but only after+// a successful Start), and Stop may be called while an outstanding Read or Write+// call is pending. (Stop should interrupt any blocking read.)+type Tty interface {+ // Start is used to activate the Tty for use. Upon return the terminal should be+ // in raw mode, non-blocking, etc. The implementation should take care of saving+ // any state that is required so that it may be restored when Stop is called.+ // Start must be idempotent.+ Start() error++ // Stop is used to stop using this Tty instance. This may be a suspend, so that other+ // terminal based applications can run in the foreground. Implementations should+ // restore any state collected at Start(), and return to ordinary blocking mode, etc.+ // Drain is called first to drain the input. Once this is called, no more Read+ // or Write calls will be made until Start is called again.+ Stop() error++ // Drain is called before Stop, and ensures that the reader will wake up appropriately+ // if it was blocked. This workaround is required for /dev/tty on certain UNIX systems+ // to ensure that Read() does not block forever. This typically arranges for the tty driver+ // to send data immediately (e.g. VMIN and VTIME both set zero) and sets a deadline on input.+ // Implementations may reasonably make this a no-op. There will still be control sequences+ // emitted between the time this is called, and when Stop is called.+ Drain() error++ // NotifyResize is used to post a signal that will be written to (non-blocking) if the+ // system detects that a resize event happened. If the channel is null, then the caller+ // does not desire such notifications (or no longer desires them.)+ // The standard UNIX implementation links this to a handler for SIGWINCH.+ //+ // If window resize events are delivered inline as part of Read, then the implementation may stub this.+ // If the caller determines that the underlying terminal can deliver notifications without OS support+ // (i.e. the terminal supports in-band resize notifications), then it may not call this function at all.+ NotifyResize(chan<- bool)++ // WindowSize is called to determine the terminal dimensions. This might be determined+ // by an ioctl or other means.+ WindowSize() (WindowSize, error)++ io.ReadWriteCloser+}
@@ -0,0 +1,266 @@+//go:build plan9+// +build plan9++// Copyright 2025 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 tty++import (+ "bufio"+ "errors"+ "fmt"+ "io"+ "os"+ "strconv"+ "strings"+ "sync"+ "sync/atomic"+)++// p9Tty implements tcell.Tty using Plan 9's /dev/cons and /dev/consctl.+// Raw mode is enabled by writing "rawon" to /dev/consctl while the fd stays open.+// Resize notifications are read from /dev/wctl: the first read returns geometry,+// subsequent reads block until the window changes (rio(4)).+//+// References:+// - kbdfs(8): cons/consctl rawon|rawoff semantics+// - rio(4): wctl geometry and blocking-on-change behavior+// - vt(1): VT100 emulator typically used for TUI programs on Plan 9+//+// Limitations:+// - We assume VT100-level capabilities (often no colors, no mouse).+// - Window size is conservative: we return 80x24 unless overridden.+// Set LINES/COLUMNS (or TCELL_LINES/TCELL_COLS) to refine.+// - Mouse and bracketed paste are not wired; terminfo/xterm queries+// are not attempted because vt(1) may not support them.+type p9Tty struct {+ cons *os.File // /dev/cons (read+write)+ consctl *os.File // /dev/consctl (write "rawon"/"rawoff")+ wctl *os.File // /dev/wctl (resize notifications)++ closed atomic.Bool+ started bool++ onResize atomic.Value // resize channel+ wg sync.WaitGroup+ stopCh chan struct{}+}++func NewDevTty() (Tty, error) { // tcell signature+ return newPlan9TTY()+}++func NewStdIoTty() (Tty, error) { // also required by tcell+ // On Plan 9 there is no POSIX tty discipline on stdin/stdout;+ // use /dev/cons explicitly for robustness.+ return newPlan9TTY()+}++func NewDevTtyFromDev(_ string) (Tty, error) { // required by tcell+ // Plan 9 does not have multiple "ttys" in the POSIX sense;+ // always bind to /dev/cons and /dev/consctl.+ return newPlan9TTY()+}++func newPlan9TTY() (Tty, error) {+ cons, err := os.OpenFile("/dev/cons", os.O_RDWR, 0)+ if err != nil {+ return nil, fmt.Errorf("open /dev/cons: %w", err)+ }+ consctl, err := os.OpenFile("/dev/consctl", os.O_WRONLY, 0)+ if err != nil {+ _ = cons.Close()+ return nil, fmt.Errorf("open /dev/consctl: %w", err)+ }+ // /dev/wctl may not exist (console without rio); best-effort.+ wctl, _ := os.OpenFile("/dev/wctl", os.O_RDWR, 0)++ t := &p9Tty{+ cons: cons,+ consctl: consctl,+ wctl: wctl,+ stopCh: make(chan struct{}),+ }+ return t, nil+}++func (t *p9Tty) Start() error {++ if t.started {+ return nil+ }+ if t.closed.Load() {+ return errors.New("tty closed")+ }++ // Recreate stop channel if absent or closed (supports resume).+ if t.stopCh == nil || isClosed(t.stopCh) {+ t.stopCh = make(chan struct{})+ }++ // Put console into raw mode; remains active while consctl is open.+ if _, err := t.consctl.Write([]byte("rawon")); err != nil {+ return fmt.Errorf("enable raw mode: %w", err)+ }++ // Reopen /dev/wctl on resume; best-effort (system console may lack it).+ if t.wctl == nil {+ if f, err := os.OpenFile("/dev/wctl", os.O_RDWR, 0); err == nil {+ t.wctl = f+ }+ }++ if t.wctl != nil {+ t.wg.Add(1)+ go t.watchResize()+ }+ t.started = true+ return nil+}++func (t *p9Tty) Drain() error {+ // Per tcell docs, this may reasonably be a no-op on non-POSIX ttys.+ // Read deadlines are not available on plan9 os.File; we rely on Stop().+ return nil+}++func (t *p9Tty) Stop() error {++ // Signal watcher to stop (if not already).+ if t.stopCh != nil && !isClosed(t.stopCh) {+ close(t.stopCh)+ }++ // Exit raw mode first.+ _, _ = t.consctl.Write([]byte("rawoff"))++ // Closing wctl unblocks watchResize; nil it so Start() can reopen later.+ if t.wctl != nil {+ _ = t.wctl.Close()+ t.wctl = nil+ }++ // Ensure watcher goroutine has exited before returning.+ t.wg.Wait()+ t.started = false+ return nil+}++func (t *p9Tty) Close() error {++ if t.closed.Swap(true) {+ return nil+ }++ if t.stopCh != nil && !isClosed(t.stopCh) {+ close(t.stopCh)+ }+ _, _ = t.consctl.Write([]byte("rawoff"))++ _ = t.cons.Close()+ _ = t.consctl.Close()+ if t.wctl != nil {+ _ = t.wctl.Close()+ t.wctl = nil+ }++ t.wg.Wait()+ return nil+}++func (t *p9Tty) Read(p []byte) (int, error) {+ return t.cons.Read(p)+}++func (t *p9Tty) Write(p []byte) (int, error) {+ return t.cons.Write(p)+}++func (t *p9Tty) NotifyResize(resizeQ chan<- bool) {+ t.onResize.Store(resizeQ)+}++func (t *p9Tty) WindowSize() (WindowSize, error) {+ // Strategy:+ // 1) honor explicit overrides (TCELL_LINES/TCELL_COLS, LINES/COLUMNS),+ // 2) otherwise return conservative 80x24.+ // Reading /dev/wctl gives pixel geometry, but char cell metrics are+ // not generally available to non-draw clients; vt(1) is fixed-cell.+ lines, cols := envInt("TCELL_LINES"), envInt("TCELL_COLS")+ if lines == 0 {+ lines = envInt("LINES")+ }+ if cols == 0 {+ cols = envInt("COLUMNS")+ }+ if lines <= 0 {+ lines = 24+ }+ if cols <= 0 {+ cols = 80+ }+ return WindowSize{Width: cols, Height: lines}, nil+}++// watchResize blocks on /dev/wctl reads; each read returns when the window+// changes size/position/state, per rio(4). We ignore the parsed geometry and+// just notify tcell to re-query WindowSize().+func (t *p9Tty) watchResize() {+ defer t.wg.Done()++ r := bufio.NewReader(t.wctl)+ for {+ select {+ case <-t.stopCh:+ return+ default:+ }+ // Each read delivers something like:+ // " minx miny maxx maxy visible current\n"+ // We don't need to parse here; just signal.+ _, err := r.ReadString('\n')+ if err != nil {+ if errors.Is(err, io.EOF) {+ return+ }+ // transient errors: continue+ }+ if rq, ok := t.onResize.Load().(chan<- bool); ok && rq != nil {+ select {+ case rq <- true:+ default:+ }+ }+ }+}++func envInt(name string) int {+ if s := strings.TrimSpace(os.Getenv(name)); s != "" {+ if v, err := strconv.Atoi(s); err == nil {+ return v+ }+ }+ return 0+}++// helper: safe check if a channel is closed+func isClosed(ch <-chan struct{}) bool {+ select {+ case <-ch:+ return true+ default:+ return false+ }+}
@@ -0,0 +1,201 @@+// Copyright 2025 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 aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos+// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris zos++package tty++import (+ "errors"+ "fmt"+ "os"+ "os/signal"+ "strconv"+ "syscall"+ "time"++ "golang.org/x/sys/unix"+ "golang.org/x/term"+)++// devTty is an implementation of the Tty API based upon /dev/tty.+type devTty struct {+ fd int+ f *os.File+ saved *term.State+ sig chan os.Signal+ dev string+ started bool+}++func (tty *devTty) Read(b []byte) (int, error) {+ return tty.f.Read(b)+}++func (tty *devTty) Write(b []byte) (int, error) {+ return tty.f.Write(b)+}++func (tty *devTty) Close() error {+ return tty.f.Close()+}++func (tty *devTty) Start() error {++ if tty.started {+ return nil+ }+ // We open another copy of /dev/tty. This is a workaround for unusual behavior+ // observed in macOS, apparently caused when a subshell (for example) closes our+ // own tty device (when it exits for example). Getting a fresh new one seems to+ // resolve the problem. (We believe this is a bug in the macOS tty driver that+ // fails to account for dup() references to the same file before applying close()+ // related behaviors to the tty.) (Note that when using stdin/stdout instead of+ // /dev/tty this problem is not observed.)+ var err error+ if tty.f, err = os.OpenFile(tty.dev, os.O_RDWR, 0); err != nil {+ return err+ }++ tty.fd = int(tty.f.Fd())++ if !term.IsTerminal(tty.fd) {+ tty.f.Close()+ return errors.New("device is not a terminal")+ }++ _ = tty.f.SetReadDeadline(time.Time{})+ saved, err := term.MakeRaw(tty.fd) // also sets vMin and vTime+ if err != nil {+ tty.f.Close()+ return err+ }+ tty.saved = saved+ tty.started = true++ return nil+}++func (tty *devTty) Drain() error {+ _ = tty.f.SetReadDeadline(time.Now())+ if err := tcSetBufParams(tty.fd, 0, 0); err != nil {+ return err+ }+ return nil+}++func (tty *devTty) Stop() error {+ // unconditionally set this, because we cannot recover+ // if we fail anyway, so this gives the best hope of+ // picking up the pieces in such a circumstance+ tty.started = false++ if err := term.Restore(tty.fd, tty.saved); err != nil {+ return err+ }+ _ = tty.f.SetReadDeadline(time.Now())++ tty.NotifyResize(nil)++ // close our tty device -- we'll get another one if we Start again later.+ _ = tty.f.Close()++ return nil+}++func (tty *devTty) WindowSize() (WindowSize, error) {+ size := WindowSize{}+ ws, err := unix.IoctlGetWinsize(tty.fd, unix.TIOCGWINSZ)+ if err != nil {+ return size, err+ }+ w := int(ws.Col)+ h := int(ws.Row)+ if w == 0 {+ w, _ = strconv.Atoi(os.Getenv("COLUMNS"))+ }+ if w == 0 {+ w = 80 // default+ }+ if h == 0 {+ h, _ = strconv.Atoi(os.Getenv("LINES"))+ }+ if h == 0 {+ h = 25 // default+ }+ size.Width = w+ size.Height = h+ size.PixelWidth = int(ws.Xpixel)+ size.PixelHeight = int(ws.Ypixel)+ return size, nil+}++func (tty *devTty) NotifyResize(resizeQ chan<- bool) {++ sigQ := tty.sig+ tty.sig = nil++ if sigQ != nil {+ signal.Stop(sigQ)+ close(sigQ)+ }++ if resizeQ == nil {+ return+ }++ sigQ = make(chan os.Signal, 1)+ signal.Notify(sigQ, syscall.SIGWINCH)++ tty.sig = sigQ++ go func() {+ for range sigQ {+ select {+ case resizeQ <- true:+ default: // queue full, so nvm.+ }+ }+ }()+}++// NewDevTty opens a /dev/tty based Tty.+func NewDevTty() (Tty, error) {+ return NewDevTtyFromDev("/dev/tty")+}++// NewDevTtyFromDev opens a tty device given a path. This can be useful to bind to other nodes.+func NewDevTtyFromDev(dev string) (Tty, error) {+ tty := &devTty{+ dev: dev,+ sig: make(chan os.Signal),+ }+ // Only open the file long enough to check that the device+ // represents a TTY. We will reopen it in start. We do collect+ // the terminal state so we can restore it later though.+ if f, err := os.OpenFile(dev, os.O_RDWR, 0); err != nil {+ return nil, err+ } else {+ defer func() { _ = f.Close() }()+ fd := int(f.Fd())+ if !term.IsTerminal(fd) {+ return nil, errors.New("not a terminal")+ }+ if tty.saved, err = term.GetState(fd); err != nil {+ return nil, fmt.Errorf("failed to get state: %w", err)+ }+ }+ return tty, nil+}
@@ -0,0 +1,21 @@+//go:build wasm || js+// +build wasm js++package tty++import "errors"++// NewDevTty obtains a default tty from the console or TTY (e.g. /dev/tty) for the process.+func NewDevTty() (Tty, error) {+ return nil, errors.New("No tty device on wasm")+}++// NewDevTtyFromDev obtains a tty from the given device path. Not supported on Windows.+func NewDevTtyFromDev(dev string) (Tty, error) {+ return nil, errors.New("No tty device on wasm")+}++// NewStdIoTty obtains a tty from stdin and stdout.+func NewStdIoTty() (Tty, error) {+ return nil, errors.New("No tty device on wasm")+}
@@ -0,0 +1,337 @@+// Copyright 2025 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 tty++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+)++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+}++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{}+ 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+ 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 signalled.+ 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+ }+ for i := range nrec {+ ir := rec[i]+ switch ir.typ {+ case keyEvent:+ chr := ir.data[10] // we only see ASCII, key down events in VT mode++ // because we use win32-input-mode, we will only+ // see US-ASCII characters - (Q: will they be+ // 16-bit values with possible surrogate pairs?)+ select {+ case w.buf <- chr:+ case <-w.stopQ:+ break+ }++ case resizeEvent:+ w.Lock()+ w.cols = binary.LittleEndian.Uint16(ir.data[0:])+ w.rows = binary.LittleEndian.Uint16(ir.data[2:])+ 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.rows = uint16(w.oscreen.size.y)+ w.cols = uint16(w.oscreen.size.x)++ 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)))+ _, _, _ = 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+}
@@ -0,0 +1,31 @@+// Copyright 2025 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 tty++// WindowSize represents the dimensions of the window of a terminal.+type WindowSize struct {+ Width int // Width in characters+ Height int // Height in characters+ PixelWidth int // Width in pixels (zero if not available or known)+ PixelHeight int // Height in pixels (zero if not available or known)+}++// CellDimensions returns the dimensions of a single cell, in pixels+func (ws WindowSize) CellDimensions() (int, int) {+ if ws.PixelWidth == 0 || ws.PixelHeight == 0 {+ return 0, 0+ }+ return (ws.PixelWidth / ws.Width), (ws.PixelHeight / ws.Height)+}
tty_plan9.go
@@ -1,266 +0,0 @@-//go:build plan9-// +build plan9--// Copyright 2025 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 (- "bufio"- "errors"- "fmt"- "io"- "os"- "strconv"- "strings"- "sync"- "sync/atomic"-)--// p9Tty implements tcell.Tty using Plan 9's /dev/cons and /dev/consctl.-// Raw mode is enabled by writing "rawon" to /dev/consctl while the fd stays open.-// Resize notifications are read from /dev/wctl: the first read returns geometry,-// subsequent reads block until the window changes (rio(4)).-//-// References:-// - kbdfs(8): cons/consctl rawon|rawoff semantics-// - rio(4): wctl geometry and blocking-on-change behavior-// - vt(1): VT100 emulator typically used for TUI programs on Plan 9-//-// Limitations:-// - We assume VT100-level capabilities (often no colors, no mouse).-// - Window size is conservative: we return 80x24 unless overridden.-// Set LINES/COLUMNS (or TCELL_LINES/TCELL_COLS) to refine.-// - Mouse and bracketed paste are not wired; terminfo/xterm queries-// are not attempted because vt(1) may not support them.-type p9Tty struct {- cons *os.File // /dev/cons (read+write)- consctl *os.File // /dev/consctl (write "rawon"/"rawoff")- wctl *os.File // /dev/wctl (resize notifications)-- closed atomic.Bool- started bool-- onResize atomic.Value // resize channel- wg sync.WaitGroup- stopCh chan struct{}-}--func NewDevTty() (Tty, error) { // tcell signature- return newPlan9TTY()-}--func NewStdIoTty() (Tty, error) { // also required by tcell- // On Plan 9 there is no POSIX tty discipline on stdin/stdout;- // use /dev/cons explicitly for robustness.- return newPlan9TTY()-}--func NewDevTtyFromDev(_ string) (Tty, error) { // required by tcell- // Plan 9 does not have multiple "ttys" in the POSIX sense;- // always bind to /dev/cons and /dev/consctl.- return newPlan9TTY()-}--func newPlan9TTY() (Tty, error) {- cons, err := os.OpenFile("/dev/cons", os.O_RDWR, 0)- if err != nil {- return nil, fmt.Errorf("open /dev/cons: %w", err)- }- consctl, err := os.OpenFile("/dev/consctl", os.O_WRONLY, 0)- if err != nil {- _ = cons.Close()- return nil, fmt.Errorf("open /dev/consctl: %w", err)- }- // /dev/wctl may not exist (console without rio); best-effort.- wctl, _ := os.OpenFile("/dev/wctl", os.O_RDWR, 0)-- t := &p9Tty{- cons: cons,- consctl: consctl,- wctl: wctl,- stopCh: make(chan struct{}),- }- return t, nil-}--func (t *p9Tty) Start() error {-- if t.started {- return nil- }- if t.closed.Load() {- return errors.New("tty closed")- }-- // Recreate stop channel if absent or closed (supports resume).- if t.stopCh == nil || isClosed(t.stopCh) {- t.stopCh = make(chan struct{})- }-- // Put console into raw mode; remains active while consctl is open.- if _, err := t.consctl.Write([]byte("rawon")); err != nil {- return fmt.Errorf("enable raw mode: %w", err)- }-- // Reopen /dev/wctl on resume; best-effort (system console may lack it).- if t.wctl == nil {- if f, err := os.OpenFile("/dev/wctl", os.O_RDWR, 0); err == nil {- t.wctl = f- }- }-- if t.wctl != nil {- t.wg.Add(1)- go t.watchResize()- }- t.started = true- return nil-}--func (t *p9Tty) Drain() error {- // Per tcell docs, this may reasonably be a no-op on non-POSIX ttys.- // Read deadlines are not available on plan9 os.File; we rely on Stop().- return nil-}--func (t *p9Tty) Stop() error {-- // Signal watcher to stop (if not already).- if t.stopCh != nil && !isClosed(t.stopCh) {- close(t.stopCh)- }-- // Exit raw mode first.- _, _ = t.consctl.Write([]byte("rawoff"))-- // Closing wctl unblocks watchResize; nil it so Start() can reopen later.- if t.wctl != nil {- _ = t.wctl.Close()- t.wctl = nil- }-- // Ensure watcher goroutine has exited before returning.- t.wg.Wait()- t.started = false- return nil-}--func (t *p9Tty) Close() error {-- if t.closed.Swap(true) {- return nil- }-- if t.stopCh != nil && !isClosed(t.stopCh) {- close(t.stopCh)- }- _, _ = t.consctl.Write([]byte("rawoff"))-- _ = t.cons.Close()- _ = t.consctl.Close()- if t.wctl != nil {- _ = t.wctl.Close()- t.wctl = nil- }-- t.wg.Wait()- return nil-}--func (t *p9Tty) Read(p []byte) (int, error) {- return t.cons.Read(p)-}--func (t *p9Tty) Write(p []byte) (int, error) {- return t.cons.Write(p)-}--func (t *p9Tty) NotifyResize(resizeQ chan<- bool) {- t.onResize.Store(resizeQ)-}--func (t *p9Tty) WindowSize() (WindowSize, error) {- // Strategy:- // 1) honor explicit overrides (TCELL_LINES/TCELL_COLS, LINES/COLUMNS),- // 2) otherwise return conservative 80x24.- // Reading /dev/wctl gives pixel geometry, but char cell metrics are- // not generally available to non-draw clients; vt(1) is fixed-cell.- lines, cols := envInt("TCELL_LINES"), envInt("TCELL_COLS")- if lines == 0 {- lines = envInt("LINES")- }- if cols == 0 {- cols = envInt("COLUMNS")- }- if lines <= 0 {- lines = 24- }- if cols <= 0 {- cols = 80- }- return WindowSize{Width: cols, Height: lines}, nil-}--// watchResize blocks on /dev/wctl reads; each read returns when the window-// changes size/position/state, per rio(4). We ignore the parsed geometry and-// just notify tcell to re-query WindowSize().-func (t *p9Tty) watchResize() {- defer t.wg.Done()-- r := bufio.NewReader(t.wctl)- for {- select {- case <-t.stopCh:- return- default:- }- // Each read delivers something like:- // " minx miny maxx maxy visible current\n"- // We don't need to parse here; just signal.- _, err := r.ReadString('\n')- if err != nil {- if errors.Is(err, io.EOF) {- return- }- // transient errors: continue- }- if rq, ok := t.onResize.Load().(chan<- bool); ok && rq != nil {- select {- case rq <- true:- default:- }- }- }-}--func envInt(name string) int {- if s := strings.TrimSpace(os.Getenv(name)); s != "" {- if v, err := strconv.Atoi(s); err == nil {- return v- }- }- return 0-}--// helper: safe check if a channel is closed-func isClosed(ch <-chan struct{}) bool {- select {- case <-ch:- return true- default:- return false- }-}
tty_unix.go
@@ -1,201 +0,0 @@-// Copyright 2025 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 aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos-// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris zos--package tcell--import (- "errors"- "fmt"- "os"- "os/signal"- "strconv"- "syscall"- "time"-- "golang.org/x/sys/unix"- "golang.org/x/term"-)--// devTty is an implementation of the Tty API based upon /dev/tty.-type devTty struct {- fd int- f *os.File- saved *term.State- sig chan os.Signal- dev string- started bool-}--func (tty *devTty) Read(b []byte) (int, error) {- return tty.f.Read(b)-}--func (tty *devTty) Write(b []byte) (int, error) {- return tty.f.Write(b)-}--func (tty *devTty) Close() error {- return tty.f.Close()-}--func (tty *devTty) Start() error {-- if tty.started {- return nil- }- // We open another copy of /dev/tty. This is a workaround for unusual behavior- // observed in macOS, apparently caused when a subshell (for example) closes our- // own tty device (when it exits for example). Getting a fresh new one seems to- // resolve the problem. (We believe this is a bug in the macOS tty driver that- // fails to account for dup() references to the same file before applying close()- // related behaviors to the tty.) (Note that when using stdin/stdout instead of- // /dev/tty this problem is not observed.)- var err error- if tty.f, err = os.OpenFile(tty.dev, os.O_RDWR, 0); err != nil {- return err- }-- tty.fd = int(tty.f.Fd())-- if !term.IsTerminal(tty.fd) {- tty.f.Close()- return errors.New("device is not a terminal")- }-- _ = tty.f.SetReadDeadline(time.Time{})- saved, err := term.MakeRaw(tty.fd) // also sets vMin and vTime- if err != nil {- tty.f.Close()- return err- }- tty.saved = saved- tty.started = true-- return nil-}--func (tty *devTty) Drain() error {- _ = tty.f.SetReadDeadline(time.Now())- if err := tcSetBufParams(tty.fd, 0, 0); err != nil {- return err- }- return nil-}--func (tty *devTty) Stop() error {- // unconditionally set this, because we cannot recover- // if we fail anyway, so this gives the best hope of- // picking up the pieces in such a circumstance- tty.started = false-- if err := term.Restore(tty.fd, tty.saved); err != nil {- return err- }- _ = tty.f.SetReadDeadline(time.Now())-- tty.NotifyResize(nil)-- // close our tty device -- we'll get another one if we Start again later.- _ = tty.f.Close()-- return nil-}--func (tty *devTty) WindowSize() (WindowSize, error) {- size := WindowSize{}- ws, err := unix.IoctlGetWinsize(tty.fd, unix.TIOCGWINSZ)- if err != nil {- return size, err- }- w := int(ws.Col)- h := int(ws.Row)- if w == 0 {- w, _ = strconv.Atoi(os.Getenv("COLUMNS"))- }- if w == 0 {- w = 80 // default- }- if h == 0 {- h, _ = strconv.Atoi(os.Getenv("LINES"))- }- if h == 0 {- h = 25 // default- }- size.Width = w- size.Height = h- size.PixelWidth = int(ws.Xpixel)- size.PixelHeight = int(ws.Ypixel)- return size, nil-}--func (tty *devTty) NotifyResize(resizeQ chan<- bool) {-- sigQ := tty.sig- tty.sig = nil-- if sigQ != nil {- signal.Stop(sigQ)- close(sigQ)- }-- if resizeQ == nil {- return- }-- sigQ = make(chan os.Signal, 1)- signal.Notify(sigQ, syscall.SIGWINCH)-- tty.sig = sigQ-- go func() {- for range sigQ {- select {- case resizeQ <- true:- default: // queue full, so nvm.- }- }- }()-}--// NewDevTty opens a /dev/tty based Tty.-func NewDevTty() (Tty, error) {- return NewDevTtyFromDev("/dev/tty")-}--// NewDevTtyFromDev opens a tty device given a path. This can be useful to bind to other nodes.-func NewDevTtyFromDev(dev string) (Tty, error) {- tty := &devTty{- dev: dev,- sig: make(chan os.Signal),- }- // Only open the file long enough to check that the device- // represents a TTY. We will reopen it in start. We do collect- // the terminal state so we can restore it later though.- if f, err := os.OpenFile(dev, os.O_RDWR, 0); err != nil {- return nil, err- } else {- defer func() { _ = f.Close() }()- fd := int(f.Fd())- if !term.IsTerminal(fd) {- return nil, errors.New("not a terminal")- }- if tty.saved, err = term.GetState(fd); err != nil {- return nil, fmt.Errorf("failed to get state: %w", err)- }- }- return tty, nil-}
tty_win.go
@@ -1,318 +0,0 @@-// Copyright 2025 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-)--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-}--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{}- 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- 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 signalled.- 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- }- for i := range nrec {- ir := rec[i]- switch ir.typ {- case keyEvent:- chr := ir.data[10] // we only see ASCII, key down events in VT mode-- // because we use win32-input-mode, we will only- // see US-ASCII characters - (Q: will they be- // 16-bit values with possible surrogate pairs?)- select {- case w.buf <- chr:- case <-w.stopQ:- break- }-- case resizeEvent:- w.Lock()- w.cols = binary.LittleEndian.Uint16(ir.data[0:])- w.rows = binary.LittleEndian.Uint16(ir.data[2:])- 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.rows = uint16(w.oscreen.size.y)- w.cols = uint16(w.oscreen.size.x)-- return w, nil-}