rumpk/core/pty.nim

233 lines
6.6 KiB
Nim

# SPDX-License-Identifier: LSL-1.0
# Copyright (c) 2026 Markus Maiwald
# Stewardship: Self Sovereign Society Foundation
#
# This file is part of the Nexus Sovereign Core.
# See legal/LICENSE_SOVEREIGN.md for license terms.
## Nexus Core: Pseudo-Terminal (PTY) Subsystem
##
## Provides a POSIX-like PTY interface for terminal emulation.
## Master fd is held by terminal emulator, slave fd by shell.
##
## Phase 40: The Soul Bridge (PTY Implementation)
import ../libs/membrane/term
const
MAX_PTYS* = 8
PTY_BUFFER_SIZE* = 4096
# File descriptor ranges
PTY_MASTER_BASE* = 100 # Master fds: 100-107
PTY_SLAVE_BASE* = 200 # Slave fds: 200-207
type
LineMode* = enum
lmRaw, # No processing (binary mode)
lmCanon # Canonical mode (line buffering, echo)
PtyPair* = object
active*: bool
id*: int
# Buffers (bidirectional)
master_to_slave*: array[PTY_BUFFER_SIZE, byte]
mts_head*, mts_tail*: int
slave_to_master*: array[PTY_BUFFER_SIZE, byte]
stm_head*, stm_tail*: int
# Line discipline
mode*: LineMode
echo*: bool
# Window size
rows*, cols*: int
var ptys*: array[MAX_PTYS, PtyPair]
var next_pty_id: int = 0
# --- Logging ---
proc kprint(s: cstring) {.importc, cdecl.}
proc kprintln(s: cstring) {.importc, cdecl.}
proc kprint_hex(v: uint64) {.importc, cdecl.}
proc pty_init*() {.exportc, cdecl.} =
for i in 0 ..< MAX_PTYS:
ptys[i].active = false
ptys[i].id = -1
next_pty_id = 0
kprintln("[PTY] Subsystem Initialized")
proc pty_alloc*(): int {.exportc, cdecl.} =
## Allocate a new PTY pair. Returns PTY ID or -1 on failure.
for i in 0 ..< MAX_PTYS:
if not ptys[i].active:
ptys[i].active = true
ptys[i].id = next_pty_id
ptys[i].mts_head = 0
ptys[i].mts_tail = 0
ptys[i].stm_head = 0
ptys[i].stm_tail = 0
ptys[i].mode = lmCanon
ptys[i].echo = true
ptys[i].rows = 37
ptys[i].cols = 100
next_pty_id += 1
kprint("[PTY] Allocated ID=")
kprint_hex(uint64(ptys[i].id))
kprintln("")
return ptys[i].id
kprintln("[PTY] ERROR: Max PTYs allocated")
return -1
proc pty_get_master_fd*(pty_id: int): int =
## Get the master file descriptor for a PTY.
if pty_id < 0 or pty_id >= MAX_PTYS: return -1
if not ptys[pty_id].active: return -1
return PTY_MASTER_BASE + pty_id
proc pty_get_slave_fd*(pty_id: int): int =
## Get the slave file descriptor for a PTY.
if pty_id < 0 or pty_id >= MAX_PTYS: return -1
if not ptys[pty_id].active: return -1
return PTY_SLAVE_BASE + pty_id
proc is_pty_master_fd*(fd: int): bool =
return fd >= PTY_MASTER_BASE and fd < PTY_MASTER_BASE + MAX_PTYS
proc is_pty_slave_fd*(fd: int): bool =
return fd >= PTY_SLAVE_BASE and fd < PTY_SLAVE_BASE + MAX_PTYS
proc get_pty_from_fd*(fd: int): ptr PtyPair =
if is_pty_master_fd(fd):
let idx = fd - PTY_MASTER_BASE
if ptys[idx].active: return addr ptys[idx]
elif is_pty_slave_fd(fd):
let idx = fd - PTY_SLAVE_BASE
if ptys[idx].active: return addr ptys[idx]
return nil
# --- Buffer Operations ---
proc ring_push(buf: var array[PTY_BUFFER_SIZE, byte], head, tail: var int, data: byte): bool =
let next = (tail + 1) mod PTY_BUFFER_SIZE
if next == head: return false # Buffer full
buf[tail] = data
tail = next
return true
proc ring_pop(buf: var array[PTY_BUFFER_SIZE, byte], head, tail: var int): int =
if head == tail: return -1 # Buffer empty
let b = int(buf[head])
head = (head + 1) mod PTY_BUFFER_SIZE
return b
proc ring_count(head, tail: int): int =
if tail >= head:
return tail - head
else:
return PTY_BUFFER_SIZE - head + tail
# --- I/O Operations ---
proc pty_write_master*(fd: int, data: ptr byte, len: int): int =
## Write to master (goes to slave input). Called by terminal emulator.
let pty = get_pty_from_fd(fd)
if pty == nil: return -1
var written = 0
for i in 0 ..< len:
let b = cast[ptr UncheckedArray[byte]](data)[i]
if ring_push(pty.master_to_slave, pty.mts_head, pty.mts_tail, b):
written += 1
else:
break # Buffer full
return written
proc pty_read_master*(fd: int, data: ptr byte, len: int): int =
## Read from master (gets slave output). Called by terminal emulator.
let pty = get_pty_from_fd(fd)
if pty == nil: return -1
var read_count = 0
let buf = cast[ptr UncheckedArray[byte]](data)
for i in 0 ..< len:
let b = ring_pop(pty.slave_to_master, pty.stm_head, pty.stm_tail)
if b < 0: break
buf[i] = byte(b)
read_count += 1
return read_count
proc pty_write_slave*(fd: int, data: ptr byte, len: int): int {.exportc, cdecl.} =
## Write to slave (output from shell). Goes to master read buffer.
## Also renders to FB terminal.
let pty = get_pty_from_fd(fd)
if pty == nil: return -1
var written = 0
let buf = cast[ptr UncheckedArray[byte]](data)
for i in 0 ..< len:
let b = buf[i]
# Push to slave-to-master buffer (for terminal emulator)
if ring_push(pty.slave_to_master, pty.stm_head, pty.stm_tail, b):
written += 1
# Mirror to UART console
var c_buf: array[2, char]
c_buf[0] = char(b)
c_buf[1] = '\0'
kprint(cast[cstring](addr c_buf[0]))
# Also render to FB terminal
term_putc(char(b))
else:
break
# Render frame after batch write
if written > 0:
term_render()
return written
proc pty_read_slave*(fd: int, data: ptr byte, len: int): int {.exportc, cdecl.} =
## Read from slave (input to shell). Gets master input.
let pty = get_pty_from_fd(fd)
if pty == nil: return -1
var read_count = 0
let buf = cast[ptr UncheckedArray[byte]](data)
for i in 0 ..< len:
let b = ring_pop(pty.master_to_slave, pty.mts_head, pty.mts_tail)
if b < 0: break
buf[i] = byte(b)
read_count += 1
# Echo if enabled
if pty.echo and pty.mode == lmCanon:
discard ring_push(pty.slave_to_master, pty.stm_head, pty.stm_tail, byte(b))
term_putc(char(b))
if read_count > 0 and pty.echo:
term_render()
return read_count
proc pty_has_data_for_slave*(pty_id: int): bool {.exportc, cdecl.} =
## Check if there's input waiting for the slave.
if pty_id < 0 or pty_id >= MAX_PTYS: return false
if not ptys[pty_id].active: return false
return ring_count(ptys[pty_id].mts_head, ptys[pty_id].mts_tail) > 0
proc pty_push_input*(pty_id: int, ch: char) {.exportc, cdecl.} =
## Push a character to the master-to-slave buffer (keyboard input).
if pty_id < 0 or pty_id >= MAX_PTYS: return
if not ptys[pty_id].active: return
discard ring_push(ptys[pty_id].master_to_slave,
ptys[pty_id].mts_head,
ptys[pty_id].mts_tail,
byte(ch))