# 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 # 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))