From be47ab4ae1deed6e69291d25e50cb7d0b330f8c9 Mon Sep 17 00:00:00 2001 From: Markus Maiwald Date: Mon, 5 Jan 2026 01:39:53 +0100 Subject: [PATCH] feat(rumpk): Implement PTY subsystem for terminal semantics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 40: The Soul Bridge IMPLEMENTED: - PTY subsystem with master/slave fd pairs (100-107 / 200-207) - Ring buffer-based bidirectional I/O (4KB each direction) - Line discipline (CANON/RAW modes, echo support) - Integration with FB terminal renderer CHANGES: - [NEW] core/pty.nim - Complete PTY implementation - [MODIFY] kernel.nim - Wire PTY to syscalls, add pty_init() to boot DATA FLOW: Keyboard → ION chan_input → pty_push_input → master_to_slave buffer → pty_read_slave → mksh stdin → mksh stdout → pty_write_slave → term_putc/term_render → Framebuffer VERIFICATION: [PTY] Subsystem Initialized [PTY] Allocated ID=0x0000000000000000 [PTY] Console PTY Allocated REMAINING: /dev/tty device node for full TTY support Co-authored-by: Voxis Forge --- core/kernel.nim | 33 +++++++ core/pty.nim | 225 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 258 insertions(+) create mode 100644 core/pty.nim diff --git a/core/kernel.nim b/core/kernel.nim index 532296a..ac383e2 100644 --- a/core/kernel.nim +++ b/core/kernel.nim @@ -22,8 +22,11 @@ import fs/tar import fs/sfs import fs/vfs import netswitch +import ../libs/membrane/libc import ../libs/membrane/net_glue import ../libs/membrane/compositor +import ../libs/membrane/term +import pty import sched # Phase 31: Memory Manager imports (must be before usage) @@ -531,11 +534,28 @@ proc k_handle_syscall*(nr, a0, a1, a2: uint): uint {.exportc, cdecl.} = if a0 == 0: # STDIN Special Case kprintln("[Kernel] sys_read(0) - STDIN request") if (current_fiber.promises and PLEDGE_STDIO) == 0: return cast[uint](-1) + + # Check if we have an active PTY for this fiber + # For now, use PTY 0 as the console PTY + if pty_has_data_for_slave(0): + var buf: array[1, byte] + let n = pty_read_slave(PTY_SLAVE_BASE, addr buf[0], 1) + if n > 0: + cast[ptr UncheckedArray[byte]](a1)[0] = buf[0] + return uint(n) + + # Fallback to ION channel var pkt: IonPacket if chan_input.recv(pkt): kprintln("[Kernel] sys_read(0) - Data available") let n = if uint64(pkt.len) < a2: uint64(pkt.len) else: a2 if n > 0: copyMem(cast[pointer](a1), cast[pointer](pkt.data), int(n)) + + # Also push to PTY for echo + let data = cast[ptr UncheckedArray[byte]](pkt.data) + for i in 0 ..< int(n): + pty_push_input(0, char(data[i])) + ion_free_raw(pkt.id) return n else: @@ -558,6 +578,10 @@ proc k_handle_syscall*(nr, a0, a1, a2: uint): uint {.exportc, cdecl.} = if a0 == 1 or a0 == 2: # STDOUT/STDERR kprintln("[Kernel] sys_write(stdout) called") console_write(cast[pointer](a1), csize_t(a2)) + + # Route through PTY slave (renders to FB terminal) + discard pty_write_slave(PTY_SLAVE_BASE, cast[ptr byte](a1), int(a2)) + kprintln("[Kernel] sys_write(stdout) returning") return a2 @@ -652,6 +676,15 @@ proc kmain() {.exportc, cdecl.} = ion_init_input() hal_io_init() + + # Phase 40: PTY Initialization (Soul Bridge) + pty_init() + discard pty_alloc() # Allocate PTY 0 as console PTY + kprintln("[PTY] Console PTY Allocated") + + # Phase 39: FB Terminal Initialization (Sovereign Term) + term_init() + kprintln("[Term] Framebuffer Terminal Initialized") vfs_init(addr binary_initrd_tar_start, addr binary_initrd_tar_end) vfs_mount_init() diff --git a/core/pty.nim b/core/pty.nim new file mode 100644 index 0000000..f49cec1 --- /dev/null +++ b/core/pty.nim @@ -0,0 +1,225 @@ +# 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*() = + 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 = + ## 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 = + ## 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 = + ## 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 = + ## 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) = + ## 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))