Standard library · System and tools

os.pty

Running a program on a pseudo terminal: waiting for its prompt, typing keys, and changing the window size.

import os.pty

Some programs behave differently when they are not on a terminal: a shell shows no prompt, a REPL does not echo or edit the line, stty has no size to report. The os.pty package starts a program attached to a pseudo terminal, a pair of devices that look like a real terminal to the program, and gives you the other end. What you write is typed keys; what you read is what a person would see on the screen, including the characters the terminal echoes back.

Open takes the same Command as os.process (program, arguments, folder and environment; the stream modes do not apply, because all three streams are the terminal) and returns the same Child. So Write, ReadUntil, ReadFor, ReadAvailable, Wait, Kill and Close work as described there. Two things differ: output and error output arrive mixed in one stream, and CloseStdin sends Ctrl-D, the end-of-input key, because closing the terminal would hang the program up.

What people get wrong:

  • Sending \n for Enter. A terminal sends \r when Enter is pressed and the line discipline turns it into a newline for programs that read lines, so type \r. A program in raw mode sees the \r itself.
  • Expecting \n in the output. The terminal turns each newline into \r\n, so look for \r\n when waiting for a line end.
  • Reading without a time limit. Use ReadUntil with a prompt and a number of milliseconds, and check timed_out, so a program that never prints the prompt cannot hang yours.
  • Forgetting that your own typing is echoed. After Write the first thing to come back is the echo of what you typed.
  • Setting TERM. Programs that draw on the screen read it; put it in the command with Env("TERM", "xterm").

This package works on Linux, macOS and the BSDs. On Windows Supported returns false and Open returns a child whose ok is false.

Open a terminal and type into it

Start the program with Open and the window size in rows and columns (0 means 24 by 80). Writing the text of a key press, with \r for Enter, reaches the program as input; reading returns the terminal echo first and then the program's answer. Calling CloseStdin ends the input of a program such as cat.

os-pty-guide-open.salam
import os.process
import os.pty

func main:
    mut cmd := process.NewCommand("cat")
    mut term := pty.Open(cmd, 24, 80)
    term.Write("typed on a terminal\r")
    echoed := term.ReadUntil("terminal\r\n", 2000)
    println "the terminal echoed it:", not echoed.timed_out
    echoed.Free()
    answer := term.ReadUntil("terminal\r\n", 2000)
    println "cat answered:", not answer.timed_out
    answer.Free()
    term.CloseStdin()
    println "exit:", term.Wait(2000).code
    term.Close()
    cmd.Free()
end
Outputthe terminal echoed it: true cat answered: true exit: 0

Wait for a prompt

An interactive program prints a prompt and then waits. ReadUntil returns as soon as the prompt text has arrived, or reports timed_out after the time you gave. That is the loop of every terminal test: wait for the prompt, type a line, wait for the answer.

os-pty-guide-prompt.salam
import os.process
import os.pty

func main:
    mut cmd := process.NewCommand("sh")
    cmd.Arg("-c")
    cmd.Arg("printf 'name? '; read name; echo \"hi $name\"")
    mut term := pty.Open(cmd, 24, 80)
    ask := term.ReadUntil("name? ", 2000)
    println "prompt seen:", not ask.timed_out
    ask.Free()
    term.Write("Sara\r")
    reply := term.ReadUntil("hi Sara", 2000)
    println "reply seen:", not reply.timed_out
    reply.Free()
    println "exit:", term.Wait(2000).code
    term.Close()
    cmd.Free()
end
Outputprompt seen: true reply seen: true exit: 0

Change the window size

Resize sets the size of the terminal and the system tells the program, which is how a full-screen program learns to redraw. GetSize reads the size back. The program sees the new size the next time it asks, for example through stty size.

os-pty-guide-resize.salam
import os.process
import os.pty

func main:
    mut cmd := process.NewCommand("sh")
    cmd.Arg("-c")
    cmd.Arg("stty size; read x; stty size")
    mut term := pty.Open(cmd, 24, 80)
    first := term.ReadUntil("24 80", 2000)
    println "first size seen:", not first.timed_out
    first.Free()
    println "resize:", pty.Resize(term, 50, 132)
    term.Write("\r")
    second := term.ReadUntil("50 132", 2000)
    println "second size seen:", not second.timed_out
    second.Free()
    println "exit:", term.Wait(2000).code
    term.Close()
    cmd.Free()
end
Outputfirst size seen: true resize: true second size seen: true exit: 0
NeedUse
Start a program on a terminalOpen
Type keysChild.Write, with \r for Enter
Wait for a promptChild.ReadUntil with a time limit
See everything printed in a periodChild.ReadFor
Send the end-of-input keyChild.CloseStdin
Change or read the window sizeResize, GetSize
Know whether terminals are availableSupported
Pipes instead of a terminalos.process

Types

Size

struct Size
The size of a terminal window in character cells.
NameTypeDescription
rowsint = 0The number of rows.
colsint = 0The number of columns.
os-pty-Resize.salam
import os.process
import os.pty

func main:
    mut cmd := process.NewCommand("sh")
    cmd.Arg("-c")
    cmd.Arg("read line; stty size")
    mut term := pty.Open(cmd, 24, 80)
    println "start:", pty.GetSize(term).rows, pty.GetSize(term).cols
    println "resized:", pty.Resize(term, 40, 120)
    println "now:", pty.GetSize(term).rows, pty.GetSize(term).cols
    term.Write("go\r")
    out := term.ReadUntil("40 120", 2000)
    println "child sees the new size:", not out.timed_out
    out.Free()
    println "exit:", term.Wait(2000).code
    term.Close()
    cmd.Free()
end
Outputstart: 24 80 resized: true now: 40 120 child sees the new size: true exit: 0

Functions

GetSize

func GetSize(c: process.Child): Size
Reads the size of the terminal; zero rows and columns when it cannot be read.

The example for this item is shown with the ones above: os-pty-Resize.salam

Supported

func Supported(): bool
True where pseudo terminals exist: Linux, macOS and the BSDs. False on Windows.
os-pty-Open.salam
import os.process
import os.pty

func main:
    mut cmd := process.NewCommand("sh")
    cmd.Arg("-c")
    cmd.Arg("stty size")
    mut term := pty.Open(cmd, 30, 100)
    out := term.ReadFor(1000)
    println out.data.trim()
    out.Free()
    println "exit:", term.Wait(2000).code
    term.Close()
    cmd.Free()
end
Output30 100 exit: 0

Open

func Open(c: process.Command, rows: int, cols: int): process.Child
Starts the command on a new pseudo terminal of the given rows and columns (0 for 24 by 80) and returns a Child whose reads and writes go through the terminal. The child gets its own session with the terminal as its controlling terminal. When the program cannot be found or started, ok is false and error says why.

The example for this item is shown with the ones above: os-pty-Open.salam