~rockorager/vaxis

50757bf088ac72800439c7cca15468317fb36e25 — Tim Culverhouse 1 year, 27 days ago 736b9f3 windows
wip: pty interface and windows support

Signed-off-by: Tim Culverhouse <tim@timculverhouse.com>
9 files changed, 202 insertions(+), 36 deletions(-)

M go.mod
M go.sum
M image.go
A term/pty.go
A term/pty_unix.go
A term/pty_windows.go
M vaxis.go
M vaxis_unix.go
M writer.go
M go.mod => go.mod +2 -1
@@ 11,7 11,8 @@ require (
	github.com/stretchr/testify v1.8.3
	golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
	golang.org/x/image v0.9.0
	golang.org/x/sys v0.10.0
	golang.org/x/sys v0.14.0
	golang.org/x/term v0.14.0
)

require (

M go.sum => go.sum +4 -2
@@ 40,11 40,13 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8=
golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=

M image.go => image.go +1 -1
@@ 114,7 114,7 @@ func (k *KittyImage) Draw(win Window) {

// Destroy deletes this image from memory
func (k *KittyImage) Destroy() {
	fmt.Fprintf(k.vx.console, "\x1B_Ga=d,d=I,i=%d\x1B\\", k.id)
	fmt.Fprintf(k.vx.pty, "\x1B_Ga=d,d=I,i=%d\x1B\\", k.id)
}

func (k *KittyImage) CellSize() (w int, h int) {

A term/pty.go => term/pty.go +32 -0
@@ 0,0 1,32 @@
package term

import (
	"io"
	"os"
)

type Pty interface {
	io.ReadWriteCloser

	MakeRaw() error
	Restore() error
	// Size reports the Pty's current size
	Size() (Size, error)
	// Notify reports terminal events which can't otherwise be included in
	// the input stream. Currently only size change signals will be sent.
	// The provided channel should have a buffer of at least 1: signals will
	// be dropped if they cannot immediately be sent to the channel
	Notify(chan os.Signal)
}

// OpenPty opens a handle to the controlling terminal's pty
func OpenPty() (Pty, error) {
	return openPty()
}

type Size struct {
	Row    int
	Col    int
	XPixel int
	YPixel int
}

A term/pty_unix.go => term/pty_unix.go +71 -0
@@ 0,0 1,71 @@
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos

package term

import (
	"os"
	"os/signal"
	"syscall"

	"golang.org/x/sys/unix"
	"golang.org/x/term"
)

// pty is a unix pseudo-terminal
type pty struct {
	state *term.State
	fd    int
}

func openPty() (Pty, error) {
	fd, err := syscall.Open("/dev/tty", os.O_RDWR, 0)
	if err != nil {
		return nil, err
	}
	pty := &pty{
		fd: fd,
	}
	return pty, nil
}

func (p *pty) Read(b []byte) (n int, err error) {
	return syscall.Read(p.fd, b)
}

func (p *pty) Write(b []byte) (n int, err error) {
	return syscall.Write(p.fd, b)
}

func (p *pty) Close() error {
	return syscall.Close(p.fd)
}

func (p *pty) MakeRaw() error {
	termios, err := term.MakeRaw(p.fd)
	if err != nil {
		return err
	}
	p.state = termios
	return nil
}

func (p *pty) Restore() error {
	return term.Restore(p.fd, p.state)
}

func (p *pty) Size() (Size, error) {
	ws, err := unix.IoctlGetWinsize(p.fd, unix.TIOCGWINSZ)
	if err != nil {
		return Size{}, err
	}
	return Size{
		Row:    int(ws.Row),
		Col:    int(ws.Col),
		XPixel: int(ws.Xpixel),
		YPixel: int(ws.Ypixel),
	}, nil
}

func (p *pty) Notify(ch chan os.Signal) {
	signal.Notify(ch, syscall.SIGWINCH)
}

A term/pty_windows.go => term/pty_windows.go +64 -0
@@ 0,0 1,64 @@
package term

import (
	"os"
	"syscall"
)

var k32 = syscall.NewLazyDLL("kernel32.dll")

type conPty struct {
	stdin  syscall.Handle
	stdout syscall.Handle
}

func openPty() (Pty, error) {
	stdin, err := syscall.Open("CONIN$", os.O_RDWR, 0)
	if err != nil {
		return nil, err
	}
	stdout, err := syscall.Open("CONOUT$", os.O_RDWR, 0)
	if err != nil {
		return nil, err
	}
	pty := &conPty{
		stdin:  stdin,
		stdout: stdout,
	}
	return pty, nil
}

func (pty *conPty) Read(p []byte) (n int, err error) {
	panic("not implemented") // TODO: Implement
}

func (pty *conPty) Write(p []byte) (n int, err error) {
	panic("not implemented") // TODO: Implement
}

func (pty *conPty) Close() error {
	syscall.Close(pty.stdin)
	syscall.Close(pty.stdout)
	return nil
}

func (pty *conPty) MakeRaw() error {
	panic("not implemented") // TODO: Implement
}

func (pty *conPty) Restore() error {
	panic("not implemented") // TODO: Implement
}

// Size reports the Pty's current size
func (pty *conPty) Size() (term.Size, error) {
	panic("not implemented") // TODO: Implement
}

// Notify reports terminal events which can't otherwise be included in
// the input stream. Currently only size change signals will be sent.
// The provided channel should have a buffer of at least 1: signals will
// be dropped if they cannot immediately be sent to the channel
func (pty *conPty) Notify(_ chan os.Signal) {
	panic("not implemented") // TODO: Implement
}

M vaxis.go => vaxis.go +21 -23
@@ 13,12 13,12 @@ import (
	"sync/atomic"
	"time"

	"github.com/containerd/console"
	"github.com/mattn/go-runewidth"
	"github.com/rivo/uniseg"

	"git.sr.ht/~rockorager/vaxis/ansi"
	"git.sr.ht/~rockorager/vaxis/log"
	"git.sr.ht/~rockorager/vaxis/term"
)

type capabilities struct {


@@ 58,8 58,9 @@ type Options struct {
}

type Vaxis struct {
	queue            chan Event
	console          console.Console
	queue chan Event
	// console          console.Console
	pty              term.Pty
	tw               *writer
	screenNext       *screen
	screenLast       *screen


@@ 1025,27 1026,24 @@ func (vx *Vaxis) Suspend() error {
	vx.exitAltScreen()
	signal.Stop(vx.chSigKill)
	signal.Stop(vx.chSigWinSz)
	vx.console.Reset()
	vx.pty.Restore()
	// vx.console.Reset()
	return nil
}

// makeRaw opens the /dev/tty device, makes it raw, and starts an input parser
// openTty opens the /dev/tty device, makes it raw, and starts an input parser
func (vx *Vaxis) openTty() error {
	for _, s := range []*os.File{os.Stderr, os.Stdout, os.Stdin} {
		if c, err := console.ConsoleFromFile(s); err == nil {
			vx.console = c
			break
		}
	}
	if vx.console == nil {
		return console.ErrNotAConsole
	pty, err := term.OpenPty()
	if err != nil {
		return err
	}
	err := vx.console.SetRaw()
	vx.pty = pty
	err = vx.pty.MakeRaw()
	if err != nil {
		return err
	}
	vx.tw = newWriter(vx)
	parser := ansi.NewParser(vx.console)
	parser := ansi.NewParser(vx.pty)
	go func() {
		defer func() {
			if err := recover(); err != nil {


@@ 1083,7 1081,7 @@ func (vx *Vaxis) Resume() error {
	// if err != nil {
	// 	return err
	// }
	err := vx.console.SetRaw()
	err := vx.pty.MakeRaw()
	if err != nil {
		return err
	}


@@ 1122,7 1120,7 @@ func (vx *Vaxis) showCursor() string {
func (vx *Vaxis) CursorPosition() (row int, col int) {
	// DSRCPR - reports cursor position
	atomicStore(&vx.reqCursorPos, true)
	_, _ = io.WriteString(vx.console, dsrcpr)
	_, _ = io.WriteString(vx.pty, dsrcpr)
	timeout := time.NewTimer(50 * time.Millisecond)
	select {
	case <-timeout.C:


@@ 1157,7 1155,7 @@ func (vx *Vaxis) cursorStyle() string {
// ClipboardPush copies the provided string to the system clipboard
func (vx *Vaxis) ClipboardPush(s string) {
	b64 := base64.StdEncoding.EncodeToString([]byte(s))
	_, _ = io.WriteString(vx.console, tparm(osc52put, b64))
	_, _ = io.WriteString(vx.pty, tparm(osc52put, b64))
}

// ClipboardPop requests the content from the system clipboard. ClipboardPop works by


@@ 1166,7 1164,7 @@ func (vx *Vaxis) ClipboardPush(s string) {
// a context to set a deadline for this function to return. An error will be
// returned if the context is cancelled.
func (vx *Vaxis) ClipboardPop(ctx context.Context) (string, error) {
	_, _ = io.WriteString(vx.console, osc52pop)
	_, _ = io.WriteString(vx.pty, osc52pop)
	select {
	case str := <-vx.chClipboard:
		return str, nil


@@ 1179,20 1177,20 @@ func (vx *Vaxis) ClipboardPop(ctx context.Context) (string, error) {
// string, OSC9 will be used - otherwise osc777 is used
func (vx *Vaxis) Notify(title string, body string) {
	if title == "" {
		_, _ = io.WriteString(vx.console, tparm(osc9notify, body))
		_, _ = io.WriteString(vx.pty, tparm(osc9notify, body))
		return
	}
	_, _ = io.WriteString(vx.console, tparm(osc777notify, title, body))
	_, _ = io.WriteString(vx.pty, tparm(osc777notify, title, body))
}

// SetTitle sets the terminal's title via OSC 2
func (vx *Vaxis) SetTitle(s string) {
	_, _ = io.WriteString(vx.console, tparm(setTitle, s))
	_, _ = io.WriteString(vx.pty, tparm(setTitle, s))
}

// Bell sends a BEL control signal to the terminal
func (vx *Vaxis) Bell() {
	_, _ = vx.console.Write([]byte{0x07})
	_, _ = vx.pty.Write([]byte{0x07})
}

// advance returns the extra amount to advance the column by when rendering

M vaxis_unix.go => vaxis_unix.go +6 -8
@@ 10,13 10,10 @@ import (
	"time"

	"git.sr.ht/~rockorager/vaxis/log"
	"golang.org/x/sys/unix"
)

func (vx *Vaxis) setupSignals() {
	signal.Notify(vx.chSigWinSz,
		syscall.SIGWINCH,
	)
	vx.pty.Notify(vx.chSigWinSz)
	signal.Notify(vx.chSigKill,
		// kill signals
		syscall.SIGABRT,


@@ 34,7 31,7 @@ func (vx *Vaxis) setupSignals() {
func (vx *Vaxis) reportWinsize() (Resize, error) {
	if vx.caps.reportSizeChars && vx.caps.reportSizePixels {
		log.Trace("requesting screen size from terminal")
		io.WriteString(vx.console, textAreaSize)
		io.WriteString(vx.pty, textAreaSize)
		deadline := time.NewTimer(100 * time.Millisecond)
		select {
		case <-deadline.C:


@@ 44,14 41,15 @@ func (vx *Vaxis) reportWinsize() (Resize, error) {
		}
	}
	log.Trace("requesting screen size from ioctl")
	ws, err := unix.IoctlGetWinsize(int(vx.console.Fd()), unix.TIOCGWINSZ)
	ws, err := vx.pty.Size()
	// ws, err := unix.IoctlGetWinsize(int(vx.pty.Fd()), unix.TIOCGWINSZ)
	if err != nil {
		return Resize{}, err
	}
	return Resize{
		Cols:   int(ws.Col),
		Rows:   int(ws.Row),
		XPixel: int(ws.Xpixel),
		YPixel: int(ws.Ypixel),
		XPixel: int(ws.XPixel),
		YPixel: int(ws.YPixel),
	}, nil
}

M writer.go => writer.go +1 -1
@@ 18,7 18,7 @@ type writer struct {
func newWriter(vx *Vaxis) *writer {
	return &writer{
		buf: bytes.NewBuffer(make([]byte, 8192)),
		w:   vx.console,
		w:   vx.pty,
		vx:  vx,
	}
}