Skip to content

Commit

Permalink
feat: add term
Browse files Browse the repository at this point in the history
  • Loading branch information
aymanbagabas committed May 8, 2024
1 parent e3b41ab commit 1c59f1a
Show file tree
Hide file tree
Showing 12 changed files with 549 additions and 0 deletions.
17 changes: 17 additions & 0 deletions term/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module github.com/charmbracelet/x/term

go 1.18

require (
github.com/charmbracelet/x/ansi v0.1.0
github.com/charmbracelet/x/input v0.1.0
golang.org/x/sys v0.20.0
)

require (
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
)
18 changes: 18 additions & 0 deletions term/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
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/input v0.1.0 h1:TEsGSfZYQyOtp+STIjyBq6tpRaorH0qpwZUj8DavAhQ=
github.com/charmbracelet/x/input v0.1.0/go.mod h1:ZZwaBxPF7IG8gWWzPUVqHEtWhc1+HXJPNuerJGRGZ28=
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/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=
49 changes: 49 additions & 0 deletions term/term.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package term

// State contains platform-specific state of a terminal.
type State struct {
state
}

// IsTerminal returns whether the given file descriptor is a terminal.
func IsTerminal(fd uintptr) bool {
return isTerminal(fd)
}

// MakeRaw puts the terminal connected to the given file descriptor into raw
// mode and returns the previous state of the terminal so that it can be
// restored.
func MakeRaw(fd uintptr) (*State, error) {
return makeRaw(fd)
}

// GetState returns the current state of a terminal which may be useful to
// restore the terminal after a signal.
func GetState(fd uintptr) (*State, error) {
return getState(fd)
}

// SetState sets the given state of the terminal.
func SetState(fd uintptr, state *State) error {
return setState(fd, state)
}

// Restore restores the terminal connected to the given file descriptor to a
// previous state.
func Restore(fd uintptr, oldState *State) error {
return restore(fd, oldState)
}

// GetSize returns the visible dimensions of the given terminal.
//
// These dimensions don't include any scrollback buffer height.
func GetSize(fd uintptr) (width, height int, err error) {
return getSize(fd)
}

// ReadPassword reads a line of input from a terminal without local echo. This
// is commonly used for inputting passwords and other sensitive data. The slice
// returned does not include the \n.
func ReadPassword(fd uintptr) ([]byte, error) {
return readPassword(fd)
}
39 changes: 39 additions & 0 deletions term/term_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !zos && !windows && !solaris && !plan9
// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!zos,!windows,!solaris,!plan9

package term

import (
"fmt"
"runtime"
)

type state struct{}

func isTerminal(fd uintptr) bool {
return false
}

func makeRaw(fd uintptr) (*State, error) {
return nil, fmt.Errorf("terminal: MakeRaw not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
}

func getState(fd uintptr) (*State, error) {
return nil, fmt.Errorf("terminal: GetState not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
}

func restore(fd uintptr, state *State) error {
return fmt.Errorf("terminal: Restore not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
}

func getSize(fd uintptr) (width, height int, err error) {
return 0, 0, fmt.Errorf("terminal: GetSize not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
}

func setState(fd uintptr, state *State) error {
return fmt.Errorf("terminal: SetState not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
}

func readPassword(fd uintptr) ([]byte, error) {
return nil, fmt.Errorf("terminal: ReadPassword not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
}
37 changes: 37 additions & 0 deletions term/term_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package term_test

import (
"os"
"runtime"
"testing"

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

func TestIsTerminalTempFile(t *testing.T) {
file, err := os.CreateTemp("", "TestIsTerminalTempFile")
if err != nil {
t.Fatal(err)
}
defer os.Remove(file.Name())
defer file.Close()

if term.IsTerminal(file.Fd()) {
t.Fatalf("IsTerminal unexpectedly returned true for temporary file %s", file.Name())
}
}

func TestIsTerminalTerm(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skipf("unknown terminal path for GOOS %v", runtime.GOOS)
}
file, err := os.OpenFile("/dev/ptmx", os.O_RDWR, 0)
if err != nil {
t.Fatal(err)
}
defer file.Close()

if !term.IsTerminal(file.Fd()) {
t.Fatalf("IsTerminal unexpectedly returned false for terminal file %s", file.Name())
}
}
96 changes: 96 additions & 0 deletions term/term_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos
// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris zos

package term

import (
"golang.org/x/sys/unix"
)

type state struct {
unix.Termios
}

func isTerminal(fd uintptr) bool {
_, err := unix.IoctlGetTermios(int(fd), ioctlReadTermios)
return err == nil
}

func makeRaw(fd uintptr) (*State, error) {
termios, err := unix.IoctlGetTermios(int(fd), ioctlReadTermios)
if err != nil {
return nil, err
}

oldState := State{state{Termios: *termios}}

// This attempts to replicate the behaviour documented for cfmakeraw in
// the termios(3) manpage.
termios.Iflag &^= unix.IGNBRK | unix.BRKINT | unix.PARMRK | unix.ISTRIP | unix.INLCR | unix.IGNCR | unix.ICRNL | unix.IXON
termios.Oflag &^= unix.OPOST

This comment has been minimized.

Copy link
@cwarden

cwarden Aug 28, 2024

When using bubbletea's nilRenderer, the terminal is placed in raw mode. unix.OPOST messes up writes to stderr/stdout. Here's a simple application to illustrate:
https://gist.github.com/cwarden/5bc56a96481c1145231f6e5973187487

Should this be addressed here, or should the terminal not be put in raw mode when using the nil renderer?

This comment has been minimized.

Copy link
@cwarden

cwarden Sep 4, 2024

This was addressed in charmbracelet/bubbletea#1120.

termios.Lflag &^= unix.ECHO | unix.ECHONL | unix.ICANON | unix.ISIG | unix.IEXTEN
termios.Cflag &^= unix.CSIZE | unix.PARENB
termios.Cflag |= unix.CS8
termios.Cc[unix.VMIN] = 1
termios.Cc[unix.VTIME] = 0
if err := unix.IoctlSetTermios(int(fd), ioctlWriteTermios, termios); err != nil {
return nil, err
}

return &oldState, nil
}

func setState(fd uintptr, state *State) error {
var termios *unix.Termios
if state != nil {
termios = &state.Termios
}
return unix.IoctlSetTermios(int(fd), ioctlWriteTermios, termios)
}

func getState(fd uintptr) (*State, error) {
termios, err := unix.IoctlGetTermios(int(fd), ioctlReadTermios)
if err != nil {
return nil, err
}

return &State{state{Termios: *termios}}, nil
}

func restore(fd uintptr, state *State) error {
return unix.IoctlSetTermios(int(fd), ioctlWriteTermios, &state.Termios)
}

func getSize(fd uintptr) (width, height int, err error) {
ws, err := unix.IoctlGetWinsize(int(fd), unix.TIOCGWINSZ)
if err != nil {
return 0, 0, err
}
return int(ws.Col), int(ws.Row), nil
}

// passwordReader is an io.Reader that reads from a specific file descriptor.
type passwordReader int

func (r passwordReader) Read(buf []byte) (int, error) {
return unix.Read(int(r), buf)
}

func readPassword(fd uintptr) ([]byte, error) {
termios, err := unix.IoctlGetTermios(int(fd), ioctlReadTermios)
if err != nil {
return nil, err
}

newState := *termios
newState.Lflag &^= unix.ECHO
newState.Lflag |= unix.ICANON | unix.ISIG
newState.Iflag |= unix.ICRNL
if err := unix.IoctlSetTermios(int(fd), ioctlWriteTermios, &newState); err != nil {
return nil, err
}

defer unix.IoctlSetTermios(int(fd), ioctlWriteTermios, termios)

return readPasswordLine(passwordReader(fd))
}
11 changes: 11 additions & 0 deletions term/term_unix_bsd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//go:build darwin || dragonfly || freebsd || netbsd || openbsd
// +build darwin dragonfly freebsd netbsd openbsd

package term

import "golang.org/x/sys/unix"

const (
ioctlReadTermios = unix.TIOCGETA
ioctlWriteTermios = unix.TIOCSETA
)
11 changes: 11 additions & 0 deletions term/term_unix_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//go:build aix || linux || solaris || zos
// +build aix linux solaris zos

package term

import "golang.org/x/sys/unix"

const (
ioctlReadTermios = unix.TCGETS
ioctlWriteTermios = unix.TCSETS
)
86 changes: 86 additions & 0 deletions term/term_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
//go:build windows
// +build windows

package term

import (
"os"

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

type state struct {
Mode uint32
}

func isTerminal(fd uintptr) bool {
var st uint32
err := windows.GetConsoleMode(windows.Handle(fd), &st)
return err == nil
}

func makeRaw(fd uintptr) (*State, error) {
var st uint32
if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil {
return nil, err
}
raw := st &^ (windows.ENABLE_ECHO_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_LINE_INPUT | windows.ENABLE_PROCESSED_OUTPUT)
if err := windows.SetConsoleMode(windows.Handle(fd), raw); err != nil {
return nil, err
}
return &State{state{st}}, nil
}

func setState(fd uintptr, state *State) error {
var mode uint32
if state != nil {
mode = state.Mode
}
return windows.SetConsoleMode(windows.Handle(fd), mode)
}

func getState(fd uintptr) (*State, error) {
var st uint32
if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil {
return nil, err
}
return &State{state{st}}, nil
}

func restore(fd uintptr, state *State) error {
return windows.SetConsoleMode(windows.Handle(fd), state.Mode)
}

func getSize(fd uintptr) (width, height int, err error) {
var info windows.ConsoleScreenBufferInfo
if err := windows.GetConsoleScreenBufferInfo(windows.Handle(fd), &info); err != nil {
return 0, 0, err
}
return int(info.Window.Right - info.Window.Left + 1), int(info.Window.Bottom - info.Window.Top + 1), nil
}

func readPassword(fd uintptr) ([]byte, error) {
var st uint32
if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil {
return nil, err
}
old := st

st &^= (windows.ENABLE_ECHO_INPUT | windows.ENABLE_LINE_INPUT)
st |= (windows.ENABLE_PROCESSED_OUTPUT | windows.ENABLE_PROCESSED_INPUT)
if err := windows.SetConsoleMode(windows.Handle(fd), st); err != nil {
return nil, err
}

defer windows.SetConsoleMode(windows.Handle(fd), old)

var h windows.Handle
p, _ := windows.GetCurrentProcess()
if err := windows.DuplicateHandle(p, windows.Handle(fd), p, &h, 0, false, windows.DUPLICATE_SAME_ACCESS); err != nil {
return nil, err
}

f := os.NewFile(uintptr(h), "stdin")
defer f.Close()
return readPasswordLine(f)
}
Loading

0 comments on commit 1c59f1a

Please sign in to comment.