233 lines
6.6 KiB
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))
|