turbo-editors/turbo-corepublic Fork 0
main
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

🛟 Updated. 28d5985 · on main · k33g · 4h ago
pty.go · 149 lines · 5.1 KBGo Blame HistoryRaw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
package terminal

import (
	"errors"
	"os"
	"runtime"
)

// ErrUnsupported is returned by Start on a platform where this editor cannot
// open a pseudo-terminal.
//
// It is not a failure of the editor: everything else goes on working, and only
// terminal windows are unavailable. Linux, macOS and Windows all have one;
// anything else does not, yet.
var ErrUnsupported = errors.New("terminal: not supported on " + runtime.GOOS + " yet")

// Session is a shell running in its own pseudo-terminal.
//
// Reading from it gives whatever the shell has written; writing to it is what
// the shell reads as its input. Both are safe to use from separate goroutines,
// which is how a terminal window is driven: one goroutine reads, the main one
// writes.
//
//	session, err := terminal.Start(terminal.Options{Dir: ".", Width: 80, Height: 24})
//	if err != nil {
//		return err
//	}
//	defer session.Close()
//	go io.Copy(parser, session)
type Session struct {
	child child
}

// child is the platform's half of a session: the process on the far side of
// the pseudo-terminal, and the two ends of it this side holds.
//
// Unix has one implementation over /dev/ptmx and an exec.Cmd; Windows has one
// over a pseudo-console and a process created by hand, because Go's exec
// cannot attach a process to a pseudo-console. The Session above is the same
// on both, which is what lets the emulator and the view know nothing about
// which one they are talking to.
type child interface {
	// Read returns what the shell has written; io.EOF once it has gone.
	Read(b []byte) (int, error)
	// Write sends input to the shell.
	Write(b []byte) (int, error)
	// Resize tells the shell its terminal has changed size.
	Resize(width, height int) error
	// Close ends the shell and releases everything held for it.
	Close() error
	// Command returns the name of the program that was started.
	Command() string
}

// Options say what shell to run and how big its terminal is.
type Options struct {
	// Shell is the program to run. Empty means the user's own shell: $SHELL on
	// Unix, %COMSPEC% on Windows.
	Shell string
	// Args are the arguments to give it. Empty means none, which is how an
	// interactive shell is started; ["-c", "go test ./..."] is how one command
	// is run instead — or ["/S", "/C", "go test ./..."] for cmd.exe.
	Args []string
	// Dir is the directory to start in. Empty means the current one.
	Dir string
	// Width and Height are the size of the terminal, in cells.
	Width  int
	Height int
	// Env is the environment for the shell. Empty means the editor's own,
	// with TERM set to what this emulator implements.
	Env []string
}

// TermName is what the shell is told its terminal is.
//
// It has to be a terminal the emulator can live up to: promising xterm-256color
// and then not understanding what a program sends back is worse than promising
// less.
const TermName = "xterm-256color"

// Start runs a shell in a new pseudo-terminal.
//
// On a platform without support it returns ErrUnsupported, which callers are
// expected to report rather than treat as a failure.
func Start(options Options) (*Session, error) {
	c, err := startChild(options)
	if err != nil {
		return nil, err
	}
	return &Session{child: c}, nil
}

// shellOrDefault returns the shell to run: the one asked for, or the user's
// own, which each platform names in its own way.
func shellOrDefault(shell string) string {
	if shell != "" {
		return shell
	}
	return defaultShell()
}

// environment returns the environment for the shell, with TERM set to the
// terminal this emulator actually implements.
//
// Inheriting the editor's own TERM would tell the shell about the *outer*
// terminal, which may promise capabilities this window does not have.
func environment(env []string) []string {
	if env == nil {
		env = os.Environ()
	}

	out := make([]string, 0, len(env)+1)
	for _, entry := range env {
		if !hasPrefix(entry, "TERM=") {
			out = append(out, entry)
		}
	}
	return append(out, "TERM="+TermName)
}

// hasPrefix reports whether a string starts with a prefix.
func hasPrefix(s, prefix string) bool {
	return len(s) >= len(prefix) && s[:len(prefix)] == prefix
}

// Read returns what the shell has written. It blocks until there is something,
// and returns io.EOF once the shell has gone.
func (s *Session) Read(b []byte) (int, error) { return s.child.Read(b) }

// Write sends input to the shell, as typing at it would.
func (s *Session) Write(b []byte) (int, error) { return s.child.Write(b) }

// Resize tells the shell its terminal has changed size, which is what makes a
// full-screen program repaint at the new one.
func (s *Session) Resize(width, height int) error {
	return s.child.Resize(max(width, 1), max(height, 1))
}

// Close ends the session: the pseudo-terminal is closed, which the shell sees
// as its input ending, and the process is waited for so that it leaves nothing
// behind.
//
// A shell that does not take the hint is killed. Closing must not be able to
// hang the editor.
func (s *Session) Close() error { return s.child.Close() }

// Command returns the name of the shell that was started, without its path,
// which is what a window title wants.
func (s *Session) Command() string { return s.child.Command() }