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.
What people get wrong:
- Sending
\nfor Enter. A terminal sends\rwhen 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\ritself. - Expecting
\nin the output. The terminal turns each newline into\r\n, so look for\r\nwhen waiting for a line end. - Reading without a time limit. Use
ReadUntilwith a prompt and a number of milliseconds, and checktimed_out, so a program that never prints the prompt cannot hang yours. - Forgetting that your own typing is echoed. After
Writethe 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 withEnv("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.
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()
endWait 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.
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()
endChange 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.
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| Need | Use |
|---|---|
| Start a program on a terminal | Open |
| Type keys | Child.Write, with \r for Enter |
| Wait for a prompt | Child.ReadUntil with a time limit |
| See everything printed in a period | Child.ReadFor |
| Send the end-of-input key | Child.CloseStdin |
| Change or read the window size | Resize, GetSize |
| Know whether terminals are available | Supported |
| Pipes instead of a terminal | os.process |
Types
Size
struct Size| Name | Type | Description |
|---|---|---|
rows | int = 0 | The number of rows. |
cols | int = 0 | The number of columns. |
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()
endFunctions
Resize
func Resize(c: process.Child, rows: int, cols: int): boolThe example for this item is shown with the ones above: os-pty-Resize.salam
GetSize
func GetSize(c: process.Child): SizeThe example for this item is shown with the ones above: os-pty-Resize.salam
Supported
func Supported(): boolimport 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()
endOpen
func Open(c: process.Command, rows: int, cols: int): process.ChildChild 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