
Pseudo-terminal interface to spawn and control child processes in a real-terminal environment: execute commands, read/write IO, resize terminal, monitor, interrupt or forcefully terminate processes.
KJPty is a pseudo-terminal (PTY) interface library for Kotlin/Native, migrated from JPty, allowing you to communicate with processes as if they were running in a real terminal.
| Platform | Target | Runtime Support | Notes |
|---|---|---|---|
| Linux x64 | linuxX64 |
Yes | Uses POSIX PTY API |
| Linux ARM64 | linuxArm64 |
Yes | Uses POSIX PTY API |
| macOS x64 | macosX64 |
Yes | Uses POSIX PTY API |
| macOS ARM64 | macosArm64 |
Yes | Uses POSIX PTY API |
| Windows x64 | mingwX64 |
Yes | Uses ConPTY |
[!IMPORTANT] The Windows platform uses ConPTY implementation, requiring Windows 10 1809+ (or Windows Server 2019+).
A singleton object that provides PTY-related static methods and constants.
Signal constant for terminating processes.
Platform Differences:
platform.posix.SIGKILL (typically 9)Example:
JPty.signal(pid, JPty.SIGKILL) // Send SIGKILL signalNon-blocking option constant for the waitpid function.
Platform Differences:
platform.posix.WNOHANG (typically 1)Execute a command in a pseudo-terminal. This is the only public method of the JPty object.
fun execInPTY(
command: String,
arguments: List<String> = emptyList(),
environment: Map<String, String>? = null,
workingDirectory: String? = null,
winSize: WinSize? = null
): PtyParameters:
command: Path to the command to execute (e.g., /bin/sh)arguments: List of command arguments (optional, defaults to empty list)environment: Environment variable mapping (optional, defaults to null which inherits parent process environment)workingDirectory: Initial working directory for the child process (optional, defaults to null which inherits parent process working directory)winSize: Terminal window size (optional, defaults to null)Returns:
Pty instance for communicating with the child processThrows:
JPtyException: When PTY creation failsPlatform Differences:
forkpty() system call to create PTY, uses chdir() in child process to change working directoryCreatePseudoConsole + CreateProcessW), sets working directory via lpCurrentDirectory parameterImplementation Notes (Unix/Linux/macOS):
command to the beginning of the argument list (if not already present)workingDirectory is specified, calls chdir() before executing execve()
execve() to replace the process image in the child process-lutil libraryExample:
val pty = JPty.execInPTY(
command = "/bin/bash",
arguments = listOf("-l"),
environment = mapOf("TERM" to "xterm-256color"),
workingDirectory = "/home/user/projects",
winSize = WinSize(columns = 120, rows = 30)
)Represents a pseudo-terminal instance for communicating with a child process.
[!CAUTION] Do not instantiate this class directly. Use
JPty.execInPTY()to createPtyinstances.
Read data from the PTY.
fun read(
buffer: ByteArray,
offset: Int = 0,
length: Int = buffer.size - offset
): IntParameters:
buffer: Byte array for storing the read dataoffset: Starting offset in the buffer (defaults to 0)length: Maximum number of bytes to read (defaults to from offset to end of buffer)Returns:
Throws:
JPtyException: When read operation failsIllegalStateException: When PTY is already closedIllegalArgumentException: When offset or length is invalidPlatform Differences:
read() system callReadFile() to read from ConPTY output pipeExample:
val buffer = ByteArray(1024)
val bytesRead = pty.read(buffer)
if (bytesRead > 0) {
val output = buffer.decodeToString(0, bytesRead)
println(output)
}Write data to the PTY.
fun write(
buffer: ByteArray,
offset: Int = 0,
length: Int = buffer.size - offset
): IntParameters:
buffer: Byte array containing the data to writeoffset: Starting offset in the buffer (defaults to 0)length: Number of bytes to write (defaults to from offset to end of buffer)Returns:
Throws:
JPtyException: When write operation failsIllegalStateException: When PTY is already closedIllegalArgumentException: When offset or length is invalidPlatform Differences:
write() system call, loops until all data is writtenWriteFile() to write to ConPTY input pipeImplementation Notes (Unix/Linux/macOS):
length bytes are writtenlength (unless an exception is thrown)Example:
pty.write("ls -la\n".encodeToByteArray())Close the PTY and optionally terminate the child process.
fun close(terminateChild: Boolean = true)Parameters:
terminateChild: Whether to terminate the child process (defaults to true)Platform Differences:
terminateChild is true and the child process is still running, sends SIGKILL signalTerminateProcess
Example:
pty.close(terminateChild = true) // Close and terminate child processCheck if the child process is still running.
fun isAlive(): BooleanReturns:
true if the child process is alive, false otherwisePlatform Differences:
JPty.isProcessAlive()
JPty.isProcessAlive()
Wait for the child process to exit and return its exit status.
fun waitFor(): IntReturns:
Platform Differences:
waitpid() to block and wait for child process exitWaitForSingleObject to block and wait for child process exitExample:
val exitCode = pty.waitFor()
println("Process exited with code: $exitCode")Attempt to gracefully interrupt the child process.
Returns:
Platform Differences:
SIGINT
CTRL_C_EVENT (requires process group support)Forcibly terminate the child process.
Returns:
Platform Differences:
SIGKILL
TerminateProcess
Get the PTY's window size.
fun getWinSize(): WinSizeReturns:
WinSize object containing the current window sizeThrows:
JPtyException: When getting window size failsPlatform Differences:
JPty.getWinSize() with error handling wrapperSet the PTY's window size.
fun setWinSize(winSize: WinSize)Parameters:
winSize: WinSize object containing the new window sizeThrows:
JPtyException: When setting window size failsPlatform Differences:
JPty.setWinSize() with error handling wrapperResizePseudoConsole to resize ConPTYExample:
val newSize = WinSize(columns = 80, rows = 24)
pty.setWinSize(newSize)A data class representing terminal window size.
class WinSize(
var columns: Int = 0,
var rows: Int = 0,
var width: Int = 0,
var height: Int = 0
)| Property | Type | Description |
|---|---|---|
columns |
Int |
Number of columns (characters) |
rows |
Int |
Number of rows (characters) |
width |
Int |
Width (pixels) |
height |
Int |
Height (pixels) |
[!NOTE]
widthandheightare typically not used; most applications only need to setcolumnsandrows.
The following are conversion methods for internal use:
toShortRows(): Short - Convert rows to ShorttoShortCols(): Short - Convert columns to ShorttoShortWidth(): Short - Convert width to ShorttoShortHeight(): Short - Convert height to ShortExample:
val winSize = WinSize(
columns = 120,
rows = 30,
width = 0,
height = 0
)Exception thrown when PTY operations fail.
class JPtyException(message: String, val errno: Int) : RuntimeException(message)Error code from the system call.
Read-only property
Example:
try {
val pty = JPty.execInPTY("/nonexistent")
} catch (e: JPtyException) {
println("PTY error: ${e.message}, errno: ${e.errno}")
}All features are fully supported, using the following system calls:
| Feature | System Call | Description |
|---|---|---|
| Create PTY | forkpty() |
Create child process and attach PTY |
| Execute Command | execve() |
Replace child process image |
| Read Data | read() |
Read from master PTY |
| Write Data | write() |
Write to master PTY |
| Window Size | ioctl(TIOCGWINSZ/TIOCSWINSZ) |
Get/set terminal window size |
| Process Signal | kill() |
Send signal to process |
| Wait for Process | waitpid() |
Wait for process state change |
| File Control | fcntl() |
Set file descriptor flags |
Linux and macOS platforms require linking with the libutil library:
// build.gradle.kts
kotlin.targets
.withType<org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget>()
.configureEach {
binaries.all {
if (konanTarget.family == Family.LINUX || konanTarget.family == Family.OSX) {
linkerOpts("-lutil")
}
}
}Windows (mingwX64) uses ConPTY to implement PTY:
CreatePseudoConsole
STARTUPINFOEXW + PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE
ReadFile/WriteFile
ResizePseudoConsole
SIGKILL is supported, mapped to TerminateProcess
getWinSize() returns the most recently set value (cached)import jpty.JPty
import jpty.WinSize
fun main() {
// Create PTY and execute command
val pty = JPty.execInPTY(
command = "/bin/sh",
arguments = listOf("-l"),
environment = mapOf("TERM" to "xterm"),
winSize = WinSize(columns = 120, rows = 30)
)
// Read initial output
val buffer = ByteArray(1024)
val bytesRead = pty.read(buffer)
println(buffer.decodeToString(0, bytesRead))
// Send command
pty.write("echo 'Hello, PTY!'\n".encodeToByteArray())
// Read response
val response = pty.read(buffer)
println(buffer.decodeToString(0, response))
// Wait for process to exit
val exitCode = pty.waitFor()
println("Exit code: $exitCode")
// Close PTY
pty.close()
}import jpty.JPty
import jpty.WinSize
import kotlin.concurrent.thread
fun interactiveShell() {
val pty = JPty.execInPTY(
command = "/bin/bash",
winSize = WinSize(columns = 80, rows = 24)
)
// Launch read thread
thread {
val buffer = ByteArray(4096)
while (pty.isAlive()) {
try {
val n = pty.read(buffer)
if (n > 0) {
print(buffer.decodeToString(0, n))
}
} catch (e: Exception) {
break
}
}
}
// Main thread handles input
val stdin = System.`in`.bufferedReader()
while (pty.isAlive()) {
val line = stdin.readLine() ?: break
pty.write("$line\n".encodeToByteArray())
}
pty.close()
}import jpty.JPty
import jpty.WinSize
fun resizeDemo() {
val pty = JPty.execInPTY("/bin/bash")
// Get current window size
val currentSize = pty.getWinSize()
println("Current size: ${currentSize.columns}x${currentSize.rows}")
// Resize window
val newSize = WinSize(columns = 100, rows = 40)
pty.setWinSize(newSize)
println("Resized to: ${newSize.columns}x${newSize.rows}")
// Verify new size
val verifySize = pty.getWinSize()
println("Verified size: ${verifySize.columns}x${verifySize.rows}")
pty.close()
}import jpty.JPty
fun processManagement() {
val pty = JPty.execInPTY("/usr/bin/sleep", listOf("10"))
println("Is alive: ${pty.isAlive()}")
// Forcibly terminate process
pty.destroy()
// Wait for process to exit
val status = pty.waitFor()
println("Exit status: $status")
println("Is alive: ${pty.isAlive()}")
pty.close(terminateChild = false) // Already terminated, no need to terminate again
}import jpty.JPty
fun platformCheck() {
try {
val pty = JPty.execInPTY("/bin/echo", listOf("test"))
println("PTY is supported on this platform")
pty.close()
} catch (e: JPtyException) {
println("PTY creation failed: ${e.message}")
}
}try {
val pty = JPty.execInPTY("/nonexistent/command")
} catch (e: JPtyException) {
println("Failed to create PTY: ${e.message}")
println("Error code: ${e.errno}")
}val pty = JPty.execInPTY("/bin/sh")
try {
val buffer = ByteArray(1024)
val n = pty.read(buffer)
} catch (e: JPtyException) {
println("Read failed: ${e.message}")
} catch (e: IllegalStateException) {
println("PTY is closed")
}val pty = JPty.execInPTY("/bin/sh")
try {
pty.setWinSize(WinSize(columns = -1, rows = -1)) // Invalid values
} catch (e: JPtyException) {
println("Failed to set window size: ${e.message}")
}try {
val pty = JPty.execInPTY("/bin/sh")
} catch (e: JPtyException) {
println("PTY creation failed: ${e.message}")
println("Check ConPTY requirements on Windows (10 1809+ / Server 2019+).")
}try-finally or use pattern to ensure resource cleanupval pty = JPty.execInPTY("/bin/sh")
try {
// Use pty
} finally {
pty.close()
}if (pty.isAlive()) {
pty.write(command.encodeToByteArray())
}val n = pty.read(buffer)
if (n == -1) {
println("Reached end of stream")
}val buffer = ByteArray(4096) // Recommendedexpect/actual or runtime detectionexpect fun isPtySupported(): Boolean
// Unix/Linux/macOS
actual fun isPtySupported(): Boolean = true
// Windows (ConPTY required)
actual fun isPtySupported(): Boolean = trueKJPty is a pseudo-terminal (PTY) interface library for Kotlin/Native, migrated from JPty, allowing you to communicate with processes as if they were running in a real terminal.
| Platform | Target | Runtime Support | Notes |
|---|---|---|---|
| Linux x64 | linuxX64 |
Yes | Uses POSIX PTY API |
| Linux ARM64 | linuxArm64 |
Yes | Uses POSIX PTY API |
| macOS x64 | macosX64 |
Yes | Uses POSIX PTY API |
| macOS ARM64 | macosArm64 |
Yes | Uses POSIX PTY API |
| Windows x64 | mingwX64 |
Yes | Uses ConPTY |
[!IMPORTANT] The Windows platform uses ConPTY implementation, requiring Windows 10 1809+ (or Windows Server 2019+).
A singleton object that provides PTY-related static methods and constants.
Signal constant for terminating processes.
Platform Differences:
platform.posix.SIGKILL (typically 9)Example:
JPty.signal(pid, JPty.SIGKILL) // Send SIGKILL signalNon-blocking option constant for the waitpid function.
Platform Differences:
platform.posix.WNOHANG (typically 1)Execute a command in a pseudo-terminal. This is the only public method of the JPty object.
fun execInPTY(
command: String,
arguments: List<String> = emptyList(),
environment: Map<String, String>? = null,
workingDirectory: String? = null,
winSize: WinSize? = null
): PtyParameters:
command: Path to the command to execute (e.g., /bin/sh)arguments: List of command arguments (optional, defaults to empty list)environment: Environment variable mapping (optional, defaults to null which inherits parent process environment)workingDirectory: Initial working directory for the child process (optional, defaults to null which inherits parent process working directory)winSize: Terminal window size (optional, defaults to null)Returns:
Pty instance for communicating with the child processThrows:
JPtyException: When PTY creation failsPlatform Differences:
forkpty() system call to create PTY, uses chdir() in child process to change working directoryCreatePseudoConsole + CreateProcessW), sets working directory via lpCurrentDirectory parameterImplementation Notes (Unix/Linux/macOS):
command to the beginning of the argument list (if not already present)workingDirectory is specified, calls chdir() before executing execve()
execve() to replace the process image in the child process-lutil libraryExample:
val pty = JPty.execInPTY(
command = "/bin/bash",
arguments = listOf("-l"),
environment = mapOf("TERM" to "xterm-256color"),
workingDirectory = "/home/user/projects",
winSize = WinSize(columns = 120, rows = 30)
)Represents a pseudo-terminal instance for communicating with a child process.
[!CAUTION] Do not instantiate this class directly. Use
JPty.execInPTY()to createPtyinstances.
Read data from the PTY.
fun read(
buffer: ByteArray,
offset: Int = 0,
length: Int = buffer.size - offset
): IntParameters:
buffer: Byte array for storing the read dataoffset: Starting offset in the buffer (defaults to 0)length: Maximum number of bytes to read (defaults to from offset to end of buffer)Returns:
Throws:
JPtyException: When read operation failsIllegalStateException: When PTY is already closedIllegalArgumentException: When offset or length is invalidPlatform Differences:
read() system callReadFile() to read from ConPTY output pipeExample:
val buffer = ByteArray(1024)
val bytesRead = pty.read(buffer)
if (bytesRead > 0) {
val output = buffer.decodeToString(0, bytesRead)
println(output)
}Write data to the PTY.
fun write(
buffer: ByteArray,
offset: Int = 0,
length: Int = buffer.size - offset
): IntParameters:
buffer: Byte array containing the data to writeoffset: Starting offset in the buffer (defaults to 0)length: Number of bytes to write (defaults to from offset to end of buffer)Returns:
Throws:
JPtyException: When write operation failsIllegalStateException: When PTY is already closedIllegalArgumentException: When offset or length is invalidPlatform Differences:
write() system call, loops until all data is writtenWriteFile() to write to ConPTY input pipeImplementation Notes (Unix/Linux/macOS):
length bytes are writtenlength (unless an exception is thrown)Example:
pty.write("ls -la\n".encodeToByteArray())Close the PTY and optionally terminate the child process.
fun close(terminateChild: Boolean = true)Parameters:
terminateChild: Whether to terminate the child process (defaults to true)Platform Differences:
terminateChild is true and the child process is still running, sends SIGKILL signalTerminateProcess
Example:
pty.close(terminateChild = true) // Close and terminate child processCheck if the child process is still running.
fun isAlive(): BooleanReturns:
true if the child process is alive, false otherwisePlatform Differences:
JPty.isProcessAlive()
JPty.isProcessAlive()
Wait for the child process to exit and return its exit status.
fun waitFor(): IntReturns:
Platform Differences:
waitpid() to block and wait for child process exitWaitForSingleObject to block and wait for child process exitExample:
val exitCode = pty.waitFor()
println("Process exited with code: $exitCode")Attempt to gracefully interrupt the child process.
Returns:
Platform Differences:
SIGINT
CTRL_C_EVENT (requires process group support)Forcibly terminate the child process.
Returns:
Platform Differences:
SIGKILL
TerminateProcess
Get the PTY's window size.
fun getWinSize(): WinSizeReturns:
WinSize object containing the current window sizeThrows:
JPtyException: When getting window size failsPlatform Differences:
JPty.getWinSize() with error handling wrapperSet the PTY's window size.
fun setWinSize(winSize: WinSize)Parameters:
winSize: WinSize object containing the new window sizeThrows:
JPtyException: When setting window size failsPlatform Differences:
JPty.setWinSize() with error handling wrapperResizePseudoConsole to resize ConPTYExample:
val newSize = WinSize(columns = 80, rows = 24)
pty.setWinSize(newSize)A data class representing terminal window size.
class WinSize(
var columns: Int = 0,
var rows: Int = 0,
var width: Int = 0,
var height: Int = 0
)| Property | Type | Description |
|---|---|---|
columns |
Int |
Number of columns (characters) |
rows |
Int |
Number of rows (characters) |
width |
Int |
Width (pixels) |
height |
Int |
Height (pixels) |
[!NOTE]
widthandheightare typically not used; most applications only need to setcolumnsandrows.
The following are conversion methods for internal use:
toShortRows(): Short - Convert rows to ShorttoShortCols(): Short - Convert columns to ShorttoShortWidth(): Short - Convert width to ShorttoShortHeight(): Short - Convert height to ShortExample:
val winSize = WinSize(
columns = 120,
rows = 30,
width = 0,
height = 0
)Exception thrown when PTY operations fail.
class JPtyException(message: String, val errno: Int) : RuntimeException(message)Error code from the system call.
Read-only property
Example:
try {
val pty = JPty.execInPTY("/nonexistent")
} catch (e: JPtyException) {
println("PTY error: ${e.message}, errno: ${e.errno}")
}All features are fully supported, using the following system calls:
| Feature | System Call | Description |
|---|---|---|
| Create PTY | forkpty() |
Create child process and attach PTY |
| Execute Command | execve() |
Replace child process image |
| Read Data | read() |
Read from master PTY |
| Write Data | write() |
Write to master PTY |
| Window Size | ioctl(TIOCGWINSZ/TIOCSWINSZ) |
Get/set terminal window size |
| Process Signal | kill() |
Send signal to process |
| Wait for Process | waitpid() |
Wait for process state change |
| File Control | fcntl() |
Set file descriptor flags |
Linux and macOS platforms require linking with the libutil library:
// build.gradle.kts
kotlin.targets
.withType<org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget>()
.configureEach {
binaries.all {
if (konanTarget.family == Family.LINUX || konanTarget.family == Family.OSX) {
linkerOpts("-lutil")
}
}
}Windows (mingwX64) uses ConPTY to implement PTY:
CreatePseudoConsole
STARTUPINFOEXW + PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE
ReadFile/WriteFile
ResizePseudoConsole
SIGKILL is supported, mapped to TerminateProcess
getWinSize() returns the most recently set value (cached)import jpty.JPty
import jpty.WinSize
fun main() {
// Create PTY and execute command
val pty = JPty.execInPTY(
command = "/bin/sh",
arguments = listOf("-l"),
environment = mapOf("TERM" to "xterm"),
winSize = WinSize(columns = 120, rows = 30)
)
// Read initial output
val buffer = ByteArray(1024)
val bytesRead = pty.read(buffer)
println(buffer.decodeToString(0, bytesRead))
// Send command
pty.write("echo 'Hello, PTY!'\n".encodeToByteArray())
// Read response
val response = pty.read(buffer)
println(buffer.decodeToString(0, response))
// Wait for process to exit
val exitCode = pty.waitFor()
println("Exit code: $exitCode")
// Close PTY
pty.close()
}import jpty.JPty
import jpty.WinSize
import kotlin.concurrent.thread
fun interactiveShell() {
val pty = JPty.execInPTY(
command = "/bin/bash",
winSize = WinSize(columns = 80, rows = 24)
)
// Launch read thread
thread {
val buffer = ByteArray(4096)
while (pty.isAlive()) {
try {
val n = pty.read(buffer)
if (n > 0) {
print(buffer.decodeToString(0, n))
}
} catch (e: Exception) {
break
}
}
}
// Main thread handles input
val stdin = System.`in`.bufferedReader()
while (pty.isAlive()) {
val line = stdin.readLine() ?: break
pty.write("$line\n".encodeToByteArray())
}
pty.close()
}import jpty.JPty
import jpty.WinSize
fun resizeDemo() {
val pty = JPty.execInPTY("/bin/bash")
// Get current window size
val currentSize = pty.getWinSize()
println("Current size: ${currentSize.columns}x${currentSize.rows}")
// Resize window
val newSize = WinSize(columns = 100, rows = 40)
pty.setWinSize(newSize)
println("Resized to: ${newSize.columns}x${newSize.rows}")
// Verify new size
val verifySize = pty.getWinSize()
println("Verified size: ${verifySize.columns}x${verifySize.rows}")
pty.close()
}import jpty.JPty
fun processManagement() {
val pty = JPty.execInPTY("/usr/bin/sleep", listOf("10"))
println("Is alive: ${pty.isAlive()}")
// Forcibly terminate process
pty.destroy()
// Wait for process to exit
val status = pty.waitFor()
println("Exit status: $status")
println("Is alive: ${pty.isAlive()}")
pty.close(terminateChild = false) // Already terminated, no need to terminate again
}import jpty.JPty
fun platformCheck() {
try {
val pty = JPty.execInPTY("/bin/echo", listOf("test"))
println("PTY is supported on this platform")
pty.close()
} catch (e: JPtyException) {
println("PTY creation failed: ${e.message}")
}
}try {
val pty = JPty.execInPTY("/nonexistent/command")
} catch (e: JPtyException) {
println("Failed to create PTY: ${e.message}")
println("Error code: ${e.errno}")
}val pty = JPty.execInPTY("/bin/sh")
try {
val buffer = ByteArray(1024)
val n = pty.read(buffer)
} catch (e: JPtyException) {
println("Read failed: ${e.message}")
} catch (e: IllegalStateException) {
println("PTY is closed")
}val pty = JPty.execInPTY("/bin/sh")
try {
pty.setWinSize(WinSize(columns = -1, rows = -1)) // Invalid values
} catch (e: JPtyException) {
println("Failed to set window size: ${e.message}")
}try {
val pty = JPty.execInPTY("/bin/sh")
} catch (e: JPtyException) {
println("PTY creation failed: ${e.message}")
println("Check ConPTY requirements on Windows (10 1809+ / Server 2019+).")
}try-finally or use pattern to ensure resource cleanupval pty = JPty.execInPTY("/bin/sh")
try {
// Use pty
} finally {
pty.close()
}if (pty.isAlive()) {
pty.write(command.encodeToByteArray())
}val n = pty.read(buffer)
if (n == -1) {
println("Reached end of stream")
}val buffer = ByteArray(4096) // Recommendedexpect/actual or runtime detectionexpect fun isPtySupported(): Boolean
// Unix/Linux/macOS
actual fun isPtySupported(): Boolean = true
// Windows (ConPTY required)
actual fun isPtySupported(): Boolean = true