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() }