pty

    Cross-platform PTY (pseudo-terminal) spawning for MoonBit native targets, integrated with moonbitlang/async.

    pty
    pseudo-terminal
    async
    native
    Download zip
    Version
    0.4.3
    License
    Apache-2.0
    Last updated
    3 days ago
    Downloads
    26K

    Dependencies

    #moonbit-community/pty

    Cross-platform PTY (pseudo-terminal) spawning for MoonBit native targets, integrated with moonbitlang/async so reads and writes go through the async event loop instead of blocking the thread.

    #API

    @async.with_task_group(group => {
    let pty = @pty.spawn(
    group,
    "sh", ["-c", "echo hello"],
    rows=24, cols=80,
    cwd="/workspace",
    )
    let text = pty.read_all().text() // Pty implements @io.Reader
    pty.write(b"ls\n") // ... and @io.Writer
    pty.resize(rows=40, cols=120)
    let pid : Int = pty.pid()
    let exit_code : Int = pty.wait()
    })

    The pty's resources are released when the task group exits. On cancellation the child gets a 5 s grace period, then a hard kill. Pty::wait returns the child's exit code; call it explicitly when the exit code matters.

    By default the task group waits for the child to exit. Pass no_wait=true (same semantics as @async/process.spawn) to let the group exit as soon as its own tasks are done, terminating the child instead of waiting for it.

    #Drain the pty concurrently

    Do not wait() first and read() afterwards. A pty master is a bounded kernel queue, and on macOS the kernel discards whatever is still queued a few hundred milliseconds after the child exits. Read concurrently while waiting; reading late yields a clean but empty EOF.

    Relatedly, ConPTY output is a screen rendering, not a byte stream: expect VT escape sequences (including an initial clear-screen) surrounding your child's output. Strip or escape them before asserting on pty output in tests — a failing diff that prints raw pty output replays those sequences into your terminal.

    #Further reading

    • docs/internals.md — executable resolution, environment merging, and platform architecture: unix fork/execve, Windows ConPTY startup, argv encoding.
    • docs/invariants.md — hard-won debugging conclusions and decision records that must not regress.

    Pty

    type Pty

    A pseudo-terminal attached to a spawned child process.

    Pty implements @async/io.Reader and @async/io.Writer: writes send input to the child's terminal, reads carry the terminal's output (stdout and stderr merged). On Windows the output is a ConPTY screen rendering, so it is wrapped in VT escape sequences (including an initial clear-screen) rather than being the child's raw bytes.

    The pty's resources are released when the task group passed to spawn exits. If the group is cancelled, the child gets a 5 s grace period, then a hard kill.
    impl Reader for Pty
    impl Writer for Pty

    Pty::drop

    async fn Pty::drop(self : Pty, len : Int) -> Int

    Reads the child's terminal output (stdout and stderr merged). These are the standard @async/io.Reader methods with their usual semantics: read reads what is immediately available, read_some returns as soon as some data arrives, read_all collects until EOF, and read_until / read_exactly / drop behave as documented on @async/io.Reader.

    Read concurrently with Pty::wait — do not wait for the child and only then read: the pty master is a bounded kernel queue, and on macOS the kernel discards whatever is still queued a few hundred milliseconds after the child exits. Reading late yields a clean but empty EOF.

    On Windows the output is a ConPTY screen rendering rather than a raw byte stream: expect VT escape sequences (including an initial clear-screen) surrounding the child's output.

    Pty::pid

    fn Pty::pid(self : Pty) -> Int

    The process ID of the spawned child process.

    Pty::read

    async fn Pty::read(self : Pty, dst : FixedArray[Byte], offset? : Int, max_len? : Int) -> Int

    Reads the child's terminal output (stdout and stderr merged). These are the standard @async/io.Reader methods with their usual semantics: read reads what is immediately available, read_some returns as soon as some data arrives, read_all collects until EOF, and read_until / read_exactly / drop behave as documented on @async/io.Reader.

    Read concurrently with Pty::wait — do not wait for the child and only then read: the pty master is a bounded kernel queue, and on macOS the kernel discards whatever is still queued a few hundred milliseconds after the child exits. Reading late yields a clean but empty EOF.

    On Windows the output is a ConPTY screen rendering rather than a raw byte stream: expect VT escape sequences (including an initial clear-screen) surrounding the child's output.

    Pty::read_all

    async fn Pty::read_all(self : Pty) -> &
    Data

    Reads the child's terminal output (stdout and stderr merged). These are the standard @async/io.Reader methods with their usual semantics: read reads what is immediately available, read_some returns as soon as some data arrives, read_all collects until EOF, and read_until / read_exactly / drop behave as documented on @async/io.Reader.

    Read concurrently with Pty::wait — do not wait for the child and only then read: the pty master is a bounded kernel queue, and on macOS the kernel discards whatever is still queued a few hundred milliseconds after the child exits. Reading late yields a clean but empty EOF.

    On Windows the output is a ConPTY screen rendering rather than a raw byte stream: expect VT escape sequences (including an initial clear-screen) surrounding the child's output.

    Pty::read_exactly

    async fn Pty::read_exactly(self : Pty, len : Int) -> Bytes

    Reads the child's terminal output (stdout and stderr merged). These are the standard @async/io.Reader methods with their usual semantics: read reads what is immediately available, read_some returns as soon as some data arrives, read_all collects until EOF, and read_until / read_exactly / drop behave as documented on @async/io.Reader.

    Read concurrently with Pty::wait — do not wait for the child and only then read: the pty master is a bounded kernel queue, and on macOS the kernel discards whatever is still queued a few hundred milliseconds after the child exits. Reading late yields a clean but empty EOF.

    On Windows the output is a ConPTY screen rendering rather than a raw byte stream: expect VT escape sequences (including an initial clear-screen) surrounding the child's output.

    Pty::read_some

    async fn Pty::read_some(self : Pty, max_len? : Int) -> Bytes?

    Reads the child's terminal output (stdout and stderr merged). These are the standard @async/io.Reader methods with their usual semantics: read reads what is immediately available, read_some returns as soon as some data arrives, read_all collects until EOF, and read_until / read_exactly / drop behave as documented on @async/io.Reader.

    Read concurrently with Pty::wait — do not wait for the child and only then read: the pty master is a bounded kernel queue, and on macOS the kernel discards whatever is still queued a few hundred milliseconds after the child exits. Reading late yields a clean but empty EOF.

    On Windows the output is a ConPTY screen rendering rather than a raw byte stream: expect VT escape sequences (including an initial clear-screen) surrounding the child's output.

    Pty::read_until

    async fn Pty::read_until(self : Pty, sep : StringView) -> String?

    Reads the child's terminal output (stdout and stderr merged). These are the standard @async/io.Reader methods with their usual semantics: read reads what is immediately available, read_some returns as soon as some data arrives, read_all collects until EOF, and read_until / read_exactly / drop behave as documented on @async/io.Reader.

    Read concurrently with Pty::wait — do not wait for the child and only then read: the pty master is a bounded kernel queue, and on macOS the kernel discards whatever is still queued a few hundred milliseconds after the child exits. Reading late yields a clean but empty EOF.

    On Windows the output is a ConPTY screen rendering rather than a raw byte stream: expect VT escape sequences (including an initial clear-screen) surrounding the child's output.

    Pty::resize

    fn Pty::resize(self : Pty, rows~ : Int, cols~ : Int) -> Unit raise
    OSError

    Resizes the child's terminal, in characters. Raises @os_error.OSError if the resize fails, including when the pty has already been closed (for example after the owning task group has exited).

    Pty::wait

    async fn Pty::wait(self : Pty) -> Int

    Waits for the child process to terminate and returns its exit code. If the process was killed by a signal, the result is -signal_number (the same convention as @async/process.wait_pid).

    Do not wait first and read afterwards: the pty master is a bounded kernel queue, and on macOS the kernel discards whatever is still queued a few hundred milliseconds after the child exits. Read concurrently while waiting; reading late yields a clean but empty EOF.

    Pty::write

    async fn Pty::write(self : Pty, data : &
    Data
    ) -> Unit

    Sends input to the child's terminal. These are the standard @async/io.Writer methods with their usual semantics: write_once performs a single write and reports how many bytes were accepted, write writes the whole buffer before returning, and write_reader forwards everything read from another reader.

    Pty::write_once

    async fn Pty::write_once(self : Pty, buf : Bytes, offset~ : Int, len~ : Int) -> Int

    Sends input to the child's terminal. These are the standard @async/io.Writer methods with their usual semantics: write_once performs a single write and reports how many bytes were accepted, write writes the whole buffer before returning, and write_reader forwards everything read from another reader.

    Pty::write_reader

    async fn Pty::write_reader(self : Pty, reader : &
    Reader
    ) -> Unit

    Sends input to the child's terminal. These are the standard @async/io.Writer methods with their usual semantics: write_once performs a single write and reports how many bytes were accepted, write writes the whole buffer before returning, and write_reader forwards everything read from another reader.

    spawn

    async fn[X] spawn(group :
    TaskGroup
    [X], rows? : Int, cols? : Int, file : StringView, args : ArrayView[StringView], extra_env? : Map[String, String], inherit_env? : Bool, cwd? : StringView, no_wait? : Bool) -> Pty

    Spawns a child process attached to a new pseudo-terminal and returns the Pty handle.

    The child runs file with args as its arguments; its argv[0] is file verbatim, so programs that dispatch on argv[0] (busybox-style multi-call binaries, shells checking for a leading -) see what the caller wrote. file is looked up through PATH when it does not contain a path separator, and used directly otherwise.

    • rows and cols set the initial terminal size in characters.
    • extra_env adds or overrides environment variables; by default (inherit_env=true) the child also inherits the current process's environment.
    • cwd sets the child's working directory; by default the child starts in the current directory.
    • no_wait has the same semantics as @async/process.spawn: by default the task group waits for the child to terminate; with no_wait=true the group exits as soon as its own tasks are done, terminating the child.

    The returned Pty implements @async/io.Reader and @async/io.Writer, and its resources are released when group exits. Raises @os_error.OSError if the pty cannot be opened or the child cannot be spawned.