Skip to content

Commit

Permalink
feat: add xpty
Browse files Browse the repository at this point in the history
  • Loading branch information
aymanbagabas committed May 8, 2024
1 parent 1c59f1a commit c321e5c
Show file tree
Hide file tree
Showing 9 changed files with 421 additions and 0 deletions.
40 changes: 40 additions & 0 deletions xpty/conpty.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package xpty

import (
"os/exec"

"github.com/charmbracelet/x/conpty"
)

// ConPty is a Windows console pty.
type ConPty struct {
*conpty.ConPty
}

var _ Pty = &ConPty{}

// NewConPty creates a new ConPty.
func NewConPty(width, height int, opts ...PtyOption) (*ConPty, error) {
var opt Options
for _, o := range opts {
o(opt)
}

c, err := conpty.New(width, height, opt.Flags)
if err != nil {
return nil, err
}

return &ConPty{c}, nil
}

// Name returns the name of the ConPty.
func (c *ConPty) Name() string {
return "windows-pty"
}

// Start starts a command on the ConPty.
// This is a wrapper around conpty.Spawn.
func (c *ConPty) Start(cmd *exec.Cmd) error {
return c.start(cmd)
}
10 changes: 10 additions & 0 deletions xpty/conpty_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//go:build !windows
// +build !windows

package xpty

import "os/exec"

func (c *ConPty) start(*exec.Cmd) error {
return ErrUnsupported
}
37 changes: 37 additions & 0 deletions xpty/conpty_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//go:build windows
// +build windows

package xpty

import (
"fmt"
"os"
"os/exec"
"syscall"

"golang.org/x/sys/windows"
)

func (c *ConPty) start(cmd *exec.Cmd) error {
pid, proc, err := c.ConPty.Spawn(cmd.Path, cmd.Args, &syscall.ProcAttr{
Dir: cmd.Dir,
Env: cmd.Env,
Sys: cmd.SysProcAttr,
})
if err != nil {
return err
}

cmd.Process, err = os.FindProcess(pid)
if err != nil {
// If we can't find the process via os.FindProcess, terminate the
// process as that's what we rely on for all further operations on the
// object.
if tErr := windows.TerminateProcess(windows.Handle(proc), 1); tErr != nil {
return fmt.Errorf("failed to terminate process after process not found: %w", tErr)
}
return fmt.Errorf("failed to find process after starting: %w", err)
}

return nil
}
22 changes: 22 additions & 0 deletions xpty/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
module github.com/charmbracelet/x/xpty

go 1.18

require (
github.com/charmbracelet/x/conpty v0.1.0
github.com/charmbracelet/x/term v0.1.0
github.com/charmbracelet/x/termios v0.1.0
github.com/creack/pty v1.1.21
golang.org/x/sys v0.20.0
)

require (
github.com/charmbracelet/x/ansi v0.1.0 // indirect
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 // indirect
github.com/charmbracelet/x/input v0.1.0 // indirect
github.com/charmbracelet/x/windows v0.1.0 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
)
28 changes: 28 additions & 0 deletions xpty/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
github.com/charmbracelet/x/ansi v0.1.0 h1:o4NbQQCoVgbLpD5RC1cI687baoLwrLZyCOTGlF0gne4=
github.com/charmbracelet/x/ansi v0.1.0/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
github.com/charmbracelet/x/input v0.1.0 h1:TEsGSfZYQyOtp+STIjyBq6tpRaorH0qpwZUj8DavAhQ=
github.com/charmbracelet/x/input v0.1.0/go.mod h1:ZZwaBxPF7IG8gWWzPUVqHEtWhc1+HXJPNuerJGRGZ28=
github.com/charmbracelet/x/term v0.1.0 h1:yOhOuAKvqAIIrUg2TDHnURdxVXOBQIjMhfgg/avR6j0=
github.com/charmbracelet/x/term v0.1.0/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw=
github.com/charmbracelet/x/termios v0.1.0 h1:y4rjAHeFksBAfGbkRDmVinMg7x7DELIGAFbdNvxg97k=
github.com/charmbracelet/x/termios v0.1.0/go.mod h1:H/EVv/KRnrYjz+fCYa9bsKdqF3S8ouDK0AZEbG7r+/U=
github.com/charmbracelet/x/windows v0.1.0 h1:gTaxdvzDM5oMa/I2ZNF7wN78X/atWemG9Wph7Ika2k4=
github.com/charmbracelet/x/windows v0.1.0/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ=
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
129 changes: 129 additions & 0 deletions xpty/pty.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package xpty

import (
"os"
"os/exec"

"github.com/creack/pty"
)

// UnixPty represents a classic Unix PTY (pseudo-terminal).
type UnixPty struct {
master, slave *os.File
}

var _ Pty = &UnixPty{}

// NewUnixPty creates a new Unix PTY.
func NewUnixPty(width, height int, _ ...PtyOption) (*UnixPty, error) {
ptm, pts, err := pty.Open()
if err != nil {
return nil, err
}

p := &UnixPty{
master: ptm,
slave: pts,
}

if width >= 0 && height >= 0 {
if err := p.Resize(width, height); err != nil {
return nil, err
}
}

return p, nil
}

// Close implements XPTY.
func (p *UnixPty) Close() (err error) {
defer func() {
serr := p.slave.Close()
if err == nil {
err = serr
}
}()
if err := p.master.Close(); err != nil {
return err
}
return
}

// Fd implements XPTY.
func (p *UnixPty) Fd() uintptr {
return p.master.Fd()
}

// Name implements XPTY.
func (p *UnixPty) Name() string {
return p.master.Name()
}

// SlaveName returns the name of the slave PTY.
// This is usually used for remote sessions to identify the running TTY. You
// can find this in SSH sessions defined as $SSH_TTY.
func (p *UnixPty) SlaveName() string {
return p.slave.Name()
}

// Read implements XPTY.
func (p *UnixPty) Read(b []byte) (n int, err error) {
return p.master.Read(b)
}

// Resize implements XPTY.
func (p *UnixPty) Resize(width int, height int) (err error) {
return p.setWinsize(width, height, 0, 0)
}

// SetWinsize sets window size for the PTY.
func (p *UnixPty) SetWinsize(width, height, x, y int) error {
return p.setWinsize(width, height, x, y)
}

// Size returns the size of the PTY.
func (p *UnixPty) Size() (width, height int, err error) {
return p.size()
}

// Start implements XPTY.
func (p *UnixPty) Start(c *exec.Cmd) error {
if c.Stdout == nil {
c.Stdout = p.slave
}
if c.Stderr == nil {
c.Stderr = p.slave
}
if c.Stdin == nil {
c.Stdin = p.slave
}
if err := c.Start(); err != nil {
return err
}
return nil
}

// Write implements XPTY.
func (p *UnixPty) Write(b []byte) (n int, err error) {
return p.master.Write(b)
}

// Master returns the master end of the PTY.
func (p *UnixPty) Master() *os.File {
return p.master
}

// Slave returns the slave end of the PTY.
func (p *UnixPty) Slave() *os.File {
return p.slave
}

// Control runs the given function with the file descriptor of the master PTY.
func (p *UnixPty) Control(fn func(fd uintptr)) error {
conn, err := p.master.SyscallConn()
if err != nil {
return err
}

return conn.Control(fn)
}
12 changes: 12 additions & 0 deletions xpty/pty_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//go:build !linux && !darwin && !freebsd && !dragonfly && !netbsd && !openbsd && !solaris
// +build !linux,!darwin,!freebsd,!dragonfly,!netbsd,!openbsd,!solaris

package xpty

func (p *UnixPty) setWinsize(int, int, int, int) error {
return ErrUnsupported
}

func (*UnixPty) size() (int, int, error) {
return 0, 0, ErrUnsupported
}
43 changes: 43 additions & 0 deletions xpty/pty_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
// +build darwin dragonfly freebsd linux netbsd openbsd solaris

package xpty

import (
"github.com/charmbracelet/x/termios"
"golang.org/x/sys/unix"
)

// setWinsize sets window size for the PTY.
func (p *UnixPty) setWinsize(width, height, x, y int) error {
var rErr error
if err := p.Control(func(fd uintptr) {
rErr = termios.SetWinsize(int(fd), &unix.Winsize{
Row: uint16(height),
Col: uint16(width),
Xpixel: uint16(x),
Ypixel: uint16(y),
})
}); err != nil {
rErr = err
}
return rErr
}

// size returns the size of the PTY.
func (p *UnixPty) size() (width, height int, err error) {
var rErr error
if err := p.Control(func(fd uintptr) {
ws, err := termios.GetWinsize(int(fd))
if err != nil {
rErr = err
return
}
width = int(ws.Col)
height = int(ws.Row)
}); err != nil {
rErr = err
}

return width, height, rErr
}
Loading

0 comments on commit c321e5c

Please sign in to comment.