summaryrefslogtreecommitdiff
path: root/na/drivers/tty
diff options
context:
space:
mode:
Diffstat (limited to 'na/drivers/tty')
-rw-r--r--na/drivers/tty/module.mk5
-rw-r--r--na/drivers/tty/n_tty.cc539
-rw-r--r--na/drivers/tty/tty_core.cc536
-rw-r--r--na/drivers/tty/tty_uart.cc251
4 files changed, 1331 insertions, 0 deletions
diff --git a/na/drivers/tty/module.mk b/na/drivers/tty/module.mk
new file mode 100644
index 0000000..49190d4
--- /dev/null
+++ b/na/drivers/tty/module.mk
@@ -0,0 +1,5 @@
+# drivers/tty/module.mk — TTY subsystem build rules
+
+SRCS_CC += drivers/tty/tty_core.cc
+SRCS_CC += drivers/tty/n_tty.cc
+SRCS_CC += drivers/tty/tty_uart.cc
diff --git a/na/drivers/tty/n_tty.cc b/na/drivers/tty/n_tty.cc
new file mode 100644
index 0000000..0e312b0
--- /dev/null
+++ b/na/drivers/tty/n_tty.cc
@@ -0,0 +1,539 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * n_tty.cc — N_TTY line discipline
+ *
+ * Implements the standard terminal line discipline:
+ *
+ * Canonical mode (ICANON):
+ * Input is buffered until newline/EOF. Backspace (VERASE) and
+ * kill-line (VKILL) editing are supported.
+ *
+ * Raw mode (!ICANON):
+ * Characters are available immediately (subject to VMIN/VTIME).
+ *
+ * Signal generation (ISIG):
+ * VINTR → SIGINT, VQUIT → SIGQUIT, VSUSP → SIGTSTP
+ *
+ * Echo (ECHO):
+ * Typed characters are reflected to output.
+ *
+ * Input processing (c_iflag):
+ * ICRNL: CR → NL mapping
+ * INLCR: NL → CR mapping
+ * IGNCR: ignore CR
+ * ISTRIP: strip high bit
+ *
+ * Output processing (c_oflag):
+ * OPOST + ONLCR: NL → CR+NL on output
+ *
+ * Reference: Linux drivers/tty/n_tty.c
+ */
+
+#include <lume/tty.h>
+#include <lume/tty_ioctl.h>
+#include <lume/signal.h>
+#include <lume/signal_nr.h>
+#include <lume/slab.h>
+#include <lume/new.h>
+#include <lume/errno.h>
+#include <lume/task.h>
+#include <lume/sched.h>
+
+#ifndef ENOIOCTLCMD
+#define ENOIOCTLCMD 515
+#endif
+
+extern void schedule();
+
+/* ================================================================== */
+/* N_TTY internal data */
+/* ================================================================== */
+
+inline constexpr int N_TTY_BUF_SIZE = 4096;
+
+struct n_tty_data {
+ char read_buf[N_TTY_BUF_SIZE];
+ int read_head; /* producer writes here (from receive_buf) */
+ int read_tail; /* consumer reads from here (from read()) */
+ int canon_head; /* in canonical mode: end of last complete line */
+ bool eof_pending; /* Ctrl+D was pressed */
+ Spinlock lock;
+};
+
+static inline int buf_count(const n_tty_data* d)
+{
+ return (d->read_head - d->read_tail + N_TTY_BUF_SIZE) % N_TTY_BUF_SIZE;
+}
+
+static inline int canon_count(const n_tty_data* d)
+{
+ return (d->canon_head - d->read_tail + N_TTY_BUF_SIZE) % N_TTY_BUF_SIZE;
+}
+
+static inline bool buf_full(const n_tty_data* d)
+{
+ return buf_count(d) >= N_TTY_BUF_SIZE - 1;
+}
+
+static inline void buf_put(n_tty_data* d, char c)
+{
+ d->read_buf[d->read_head] = c;
+ d->read_head = (d->read_head + 1) % N_TTY_BUF_SIZE;
+}
+
+static inline char buf_get(n_tty_data* d)
+{
+ char c = d->read_buf[d->read_tail];
+ d->read_tail = (d->read_tail + 1) % N_TTY_BUF_SIZE;
+ return c;
+}
+
+/* ================================================================== */
+/* Output helpers */
+/* ================================================================== */
+
+static void tty_put_char(tty_struct* tty, char c)
+{
+ if (tty->driver && tty->driver->ops && tty->driver->ops->put_char) {
+ tty->driver->ops->put_char(tty, c);
+ }
+}
+
+/*
+ * Process and output a single character, applying c_oflag transformations.
+ */
+static void tty_output_char(tty_struct* tty, char c)
+{
+ if (tty->termios_data.c_oflag & OPOST) {
+ if (c == '\n' && (tty->termios_data.c_oflag & ONLCR)) {
+ tty_put_char(tty, '\r');
+ tty_put_char(tty, '\n');
+ return;
+ }
+ if (c == '\r' && (tty->termios_data.c_oflag & OCRNL)) {
+ tty_put_char(tty, '\n');
+ return;
+ }
+ }
+ tty_put_char(tty, c);
+}
+
+/*
+ * Echo a character back to the terminal.
+ * Control characters are displayed as ^X.
+ */
+static void echo_char(tty_struct* tty, char c)
+{
+ if (!(tty->termios_data.c_lflag & ECHO))
+ return;
+
+ if (c == '\n') {
+ tty_output_char(tty, '\n');
+ return;
+ }
+
+ if (c == '\t') {
+ tty_put_char(tty, '\t');
+ return;
+ }
+
+ /* Control characters: echo as ^X */
+ if ((tty->termios_data.c_lflag & ECHOCTL) && c < 0x20 && c != '\t' && c != '\n') {
+ tty_put_char(tty, '^');
+ tty_put_char(tty, c + '@');
+ return;
+ }
+
+ if (c == 0x7f) { /* DEL */
+ tty_put_char(tty, '^');
+ tty_put_char(tty, '?');
+ return;
+ }
+
+ tty_output_char(tty, c);
+}
+
+/*
+ * Erase one character visually (backspace + space + backspace).
+ */
+static void erase_char(tty_struct* tty, char erased)
+{
+ if (!(tty->termios_data.c_lflag & ECHO))
+ return;
+
+ if ((tty->termios_data.c_lflag & ECHOE)) {
+ /* If the erased char was a control char displayed as ^X, erase 2 cols */
+ if ((erased < 0x20 && erased != '\t' && erased != '\n') || erased == 0x7f) {
+ tty_put_char(tty, '\b');
+ tty_put_char(tty, ' ');
+ tty_put_char(tty, '\b');
+ tty_put_char(tty, '\b');
+ tty_put_char(tty, ' ');
+ tty_put_char(tty, '\b');
+ } else {
+ tty_put_char(tty, '\b');
+ tty_put_char(tty, ' ');
+ tty_put_char(tty, '\b');
+ }
+ }
+}
+
+/* ================================================================== */
+/* Input processing — receive_buf (called from IRQ or driver) */
+/* ================================================================== */
+
+static void n_tty_receive_buf(tty_struct* tty, const char* cp, int count)
+{
+ auto* d = static_cast<n_tty_data*>(tty->ldisc.data);
+ if (!d) return;
+
+ const termios& t = tty->termios_data;
+
+ LockGuard guard(d->lock);
+
+ for (int i = 0; i < count; i++) {
+ char c = cp[i];
+
+ /* Strip high bit if requested */
+ if (t.c_iflag & ISTRIP)
+ c &= 0x7f;
+
+ /* Input CR/NL processing */
+ if (c == '\r') {
+ if (t.c_iflag & IGNCR)
+ continue;
+ if (t.c_iflag & ICRNL)
+ c = '\n';
+ } else if (c == '\n') {
+ if (t.c_iflag & INLCR)
+ c = '\r';
+ }
+
+ /* Signal generation */
+ if (t.c_lflag & ISIG) {
+ if (c == t.c_cc[VINTR]) {
+ tty_signal_pgrp(tty, SIGINT);
+ echo_char(tty, c);
+ if (!(t.c_lflag & NOFLSH)) {
+ /* Flush input buffer */
+ d->read_head = d->read_tail;
+ d->canon_head = d->read_tail;
+ }
+ continue;
+ }
+ if (c == t.c_cc[VQUIT]) {
+ tty_signal_pgrp(tty, SIGQUIT);
+ echo_char(tty, c);
+ if (!(t.c_lflag & NOFLSH)) {
+ d->read_head = d->read_tail;
+ d->canon_head = d->read_tail;
+ }
+ continue;
+ }
+ if (c == t.c_cc[VSUSP]) {
+ tty_signal_pgrp(tty, SIGTSTP);
+ echo_char(tty, c);
+ continue;
+ }
+ }
+
+ /* Canonical mode processing */
+ if (t.c_lflag & ICANON) {
+ /* EOF (Ctrl+D) */
+ if (c == t.c_cc[VEOF]) {
+ d->eof_pending = true;
+ d->canon_head = d->read_head;
+ tty->read_wait.wake_all();
+ continue;
+ }
+
+ /* ERASE (backspace/DEL) */
+ if (c == t.c_cc[VERASE]) {
+ if (d->read_head != d->canon_head &&
+ d->read_head != d->read_tail) {
+ int prev = (d->read_head - 1 + N_TTY_BUF_SIZE) % N_TTY_BUF_SIZE;
+ char erased = d->read_buf[prev];
+ d->read_head = prev;
+ erase_char(tty, erased);
+ }
+ continue;
+ }
+
+ /* KILL (Ctrl+U — erase entire line) */
+ if (c == t.c_cc[VKILL]) {
+ while (d->read_head != d->canon_head &&
+ d->read_head != d->read_tail) {
+ int prev = (d->read_head - 1 + N_TTY_BUF_SIZE) % N_TTY_BUF_SIZE;
+ char erased = d->read_buf[prev];
+ d->read_head = prev;
+ erase_char(tty, erased);
+ }
+ if (t.c_lflag & ECHOK) {
+ tty_output_char(tty, '\n');
+ }
+ continue;
+ }
+
+ /* WERASE (Ctrl+W — erase word) */
+ if ((t.c_lflag & IEXTEN) && c == t.c_cc[VWERASE]) {
+ /* Skip trailing spaces */
+ while (d->read_head != d->canon_head) {
+ int prev = (d->read_head - 1 + N_TTY_BUF_SIZE) % N_TTY_BUF_SIZE;
+ if (d->read_buf[prev] != ' ' && d->read_buf[prev] != '\t')
+ break;
+ erase_char(tty, d->read_buf[prev]);
+ d->read_head = prev;
+ }
+ /* Erase word */
+ while (d->read_head != d->canon_head) {
+ int prev = (d->read_head - 1 + N_TTY_BUF_SIZE) % N_TTY_BUF_SIZE;
+ if (d->read_buf[prev] == ' ' || d->read_buf[prev] == '\t')
+ break;
+ erase_char(tty, d->read_buf[prev]);
+ d->read_head = prev;
+ }
+ continue;
+ }
+
+ /* REPRINT (Ctrl+R) */
+ if ((t.c_lflag & IEXTEN) && c == t.c_cc[VREPRINT]) {
+ tty_output_char(tty, '\n');
+ int pos = d->canon_head;
+ while (pos != d->read_head) {
+ echo_char(tty, d->read_buf[pos]);
+ pos = (pos + 1) % N_TTY_BUF_SIZE;
+ }
+ continue;
+ }
+
+ /* Regular character — buffer it */
+ if (!buf_full(d)) {
+ buf_put(d, c);
+ echo_char(tty, c);
+ }
+
+ /* Line complete on newline or EOL */
+ if (c == '\n' || c == t.c_cc[VEOL] ||
+ (t.c_cc[VEOL2] && c == t.c_cc[VEOL2])) {
+ d->canon_head = d->read_head;
+ tty->read_wait.wake_all();
+ }
+ } else {
+ /* Raw / non-canonical mode */
+ if (!buf_full(d)) {
+ buf_put(d, c);
+ echo_char(tty, c);
+ tty->read_wait.wake_all();
+ }
+ }
+ }
+}
+
+/* ================================================================== */
+/* N_TTY read — blocking read from the input buffer */
+/* ================================================================== */
+
+static int n_tty_read(tty_struct* tty, file* fp, char* buf, uint64 count)
+{
+ auto* d = static_cast<n_tty_data*>(tty->ldisc.data);
+ if (!d) return -EIO;
+
+ const termios& t = tty->termios_data;
+ TaskControlBlock* curr = current_task();
+ int copied = 0;
+
+ if (t.c_lflag & ICANON) {
+ /* Canonical mode: block until a complete line is available */
+ while (canon_count(d) == 0 && !d->eof_pending) {
+ if (tty->flags & TTY_IO_ERROR)
+ return -EIO;
+
+ /* Check for pending signals — return -EINTR so the
+ * trap return path can deliver them via do_signal() */
+ if (signal_pending(curr))
+ return -EINTR;
+
+ tty->read_wait.prepare_to_wait(curr, TaskState::Sleeping);
+ if (canon_count(d) == 0 && !d->eof_pending)
+ schedule();
+ tty->read_wait.finish_wait(curr);
+ }
+
+ if (d->eof_pending && canon_count(d) == 0) {
+ d->eof_pending = false;
+ return 0; /* EOF */
+ }
+
+ /* Copy up to one line */
+ LockGuard guard(d->lock);
+ while (copied < static_cast<int>(count) && d->read_tail != d->canon_head) {
+ buf[copied++] = buf_get(d);
+ }
+ } else {
+ /* Non-canonical mode */
+ int vmin = t.c_cc[VMIN];
+ /* VTIME not implemented (would need timers) */
+
+ if (vmin == 0) vmin = 1;
+
+ while (buf_count(d) < vmin) {
+ if (tty->flags & TTY_IO_ERROR)
+ return -EIO;
+
+ if (signal_pending(curr))
+ return -EINTR;
+
+ tty->read_wait.prepare_to_wait(curr, TaskState::Sleeping);
+ if (buf_count(d) < vmin)
+ schedule();
+ tty->read_wait.finish_wait(curr);
+ }
+
+ LockGuard guard(d->lock);
+ while (copied < static_cast<int>(count) && buf_count(d) > 0) {
+ buf[copied++] = buf_get(d);
+ }
+ }
+
+ return copied;
+}
+
+/* ================================================================== */
+/* N_TTY write — output through the driver with post-processing */
+/* ================================================================== */
+
+static int n_tty_write(tty_struct* tty, file* fp, const char* buf, uint64 count)
+{
+ if (!tty->driver || !tty->driver->ops || !tty->driver->ops->write)
+ return -EIO;
+
+ /* Apply output processing (OPOST) */
+ if (tty->termios_data.c_oflag & OPOST) {
+ for (uint64 i = 0; i < count; i++) {
+ tty_output_char(tty, buf[i]);
+ }
+ return static_cast<int>(count);
+ }
+
+ /* Raw output: pass directly to driver */
+ return tty->driver->ops->write(tty, buf, static_cast<int>(count));
+}
+
+/* ================================================================== */
+/* N_TTY ioctl */
+/* ================================================================== */
+
+static int n_tty_ioctl(tty_struct* tty, unsigned int cmd, uint64 arg)
+{
+ auto* d = static_cast<n_tty_data*>(tty->ldisc.data);
+
+ switch (cmd) {
+ case FIONREAD: {
+ int count = buf_count(d);
+ auto* p = reinterpret_cast<int*>(arg);
+ *p = count;
+ return 0;
+ }
+ default:
+ return -ENOIOCTLCMD;
+ }
+}
+
+/* ================================================================== */
+/* N_TTY open / close */
+/* ================================================================== */
+
+static int n_tty_open(tty_struct* tty)
+{
+ auto* d = new n_tty_data{};
+ if (!d) return -ENOMEM;
+
+ d->read_head = 0;
+ d->read_tail = 0;
+ d->canon_head = 0;
+ d->eof_pending = false;
+ d->lock.init("n_tty");
+
+ tty->ldisc.data = d;
+ return 0;
+}
+
+static void n_tty_close(tty_struct* tty)
+{
+ auto* d = static_cast<n_tty_data*>(tty->ldisc.data);
+ if (d) {
+ delete d;
+ tty->ldisc.data = nullptr;
+ }
+}
+
+static void n_tty_flush_buffer(tty_struct* tty)
+{
+ auto* d = static_cast<n_tty_data*>(tty->ldisc.data);
+ if (!d) return;
+ LockGuard guard(d->lock);
+ d->read_head = 0;
+ d->read_tail = 0;
+ d->canon_head = 0;
+ d->eof_pending = false;
+}
+
+static int n_tty_chars_in_buffer(tty_struct* tty)
+{
+ auto* d = static_cast<n_tty_data*>(tty->ldisc.data);
+ if (!d) return 0;
+ return buf_count(d);
+}
+
+/*
+ * Probe input readiness for poll(). In canonical mode the read syscall
+ * blocks until a whole line is queued, so we mirror that: POLLIN only
+ * when canon_count() shows a finalized line (or pending EOF). In raw
+ * mode any character satisfies VMIN==1 immediately. Without this
+ * mirror, ppoll() would falsely signal POLLIN on partial canonical
+ * input, then read() would block — exactly the busy-loop the shell's
+ * ppoll-then-read pattern is trying to avoid.
+ */
+bool n_tty_input_ready(tty_struct* tty)
+{
+ auto* d = static_cast<n_tty_data*>(tty->ldisc.data);
+ if (!d) return false;
+ const termios& t = tty->termios_data;
+ if (t.c_lflag & ICANON)
+ return canon_count(d) != 0 || d->eof_pending;
+ int vmin = t.c_cc[VMIN];
+ if (vmin == 0) vmin = 1;
+ return buf_count(d) >= vmin;
+}
+
+static void n_tty_set_termios(tty_struct* /*tty*/, const termios* /*old*/)
+{
+ /* Nothing special needed for now */
+}
+
+static void n_tty_hangup(tty_struct* tty)
+{
+ n_tty_flush_buffer(tty);
+ tty->read_wait.wake_all();
+}
+
+/* ================================================================== */
+/* N_TTY ops table */
+/* ================================================================== */
+
+const tty_ldisc_ops n_tty_ops = {
+ .name = "n_tty",
+ .num = 0, /* N_TTY */
+ .open = n_tty_open,
+ .close = n_tty_close,
+ .read = n_tty_read,
+ .write = n_tty_write,
+ .receive_buf = n_tty_receive_buf,
+ .ioctl = n_tty_ioctl,
+ .flush_buffer = n_tty_flush_buffer,
+ .chars_in_buffer = n_tty_chars_in_buffer,
+ .set_termios = n_tty_set_termios,
+ .hangup = n_tty_hangup,
+};
diff --git a/na/drivers/tty/tty_core.cc b/na/drivers/tty/tty_core.cc
new file mode 100644
index 0000000..5985bd9
--- /dev/null
+++ b/na/drivers/tty/tty_core.cc
@@ -0,0 +1,536 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * tty_core.cc — TTY subsystem core
+ *
+ * Provides the tty_struct lifecycle, file_operations dispatch,
+ * and ioctl routing between the line discipline and driver.
+ *
+ * Call chain:
+ * sys_read → tty_fops.read → tty_read → ldisc.read
+ * sys_write → tty_fops.write → tty_write → ldisc.write
+ * sys_ioctl → tty_fops.ioctl → tty_ioctl → {core / ldisc / driver}
+ */
+
+#include <lume/tty.h>
+#include <lume/tty_ioctl.h>
+#include <lume/file.h>
+#include <lume/fs_types.h>
+#include <lume/chardev.h>
+#include <lume/signal.h>
+#include <lume/signal_types.h>
+#include <lume/signal_nr.h>
+#include <lume/task.h>
+#include <lume/pgrp.h>
+#include <lume/poll.h>
+#include <lume/new.h>
+#include <lume/errno.h>
+#include <lume/klog.h>
+#include <lume/slab.h>
+
+#ifndef ENOIOCTLCMD
+#define ENOIOCTLCMD 515
+#endif
+
+/* ================================================================== */
+/* Global console TTY */
+/* ================================================================== */
+
+tty_struct* g_console_tty = nullptr;
+
+/* ================================================================== */
+/* tty_struct allocation */
+/* ================================================================== */
+
+tty_struct* tty_alloc(const tty_driver* driver, int index)
+{
+ auto* tty = new tty_struct{};
+ if (!tty) return nullptr;
+
+ tty->index = index;
+ tty->driver = driver;
+ tty->link = nullptr;
+ tty->pgrp = 0;
+ tty->session = 0;
+ tty->write_cnt = 0;
+ tty->flags = 0;
+ tty->open_count = 0;
+ tty->driver_data = nullptr;
+
+ tty_default_termios(&tty->termios_data);
+
+ tty->ws.ws_row = 24;
+ tty->ws.ws_col = 80;
+ tty->ws.ws_xpixel = 0;
+ tty->ws.ws_ypixel = 0;
+
+ tty->lock.init("tty");
+ tty->read_wait.init("tty_rd");
+ tty->write_wait.init("tty_wr");
+
+ /* Attach default line discipline (N_TTY) */
+ tty->ldisc.ops = &n_tty_ops;
+ tty->ldisc.data = nullptr;
+
+ return tty;
+}
+
+/* ================================================================== */
+/* tty_open / tty_close */
+/* ================================================================== */
+
+int tty_open(tty_struct* tty)
+{
+ if (!tty) return -EINVAL;
+
+ if (tty->open_count == 0) {
+ /* Initialize line discipline */
+ if (tty->ldisc.ops && tty->ldisc.ops->open) {
+ int ret = tty->ldisc.ops->open(tty);
+ if (ret < 0) return ret;
+ }
+
+ /* Initialize driver */
+ if (tty->driver && tty->driver->ops && tty->driver->ops->open) {
+ int ret = tty->driver->ops->open(tty);
+ if (ret < 0) {
+ if (tty->ldisc.ops && tty->ldisc.ops->close)
+ tty->ldisc.ops->close(tty);
+ return ret;
+ }
+ }
+ }
+
+ tty->open_count++;
+ return 0;
+}
+
+void tty_close(tty_struct* tty)
+{
+ if (!tty) return;
+
+ tty->open_count--;
+ if (tty->open_count <= 0) {
+ tty->open_count = 0;
+
+ if (tty->driver && tty->driver->ops && tty->driver->ops->close)
+ tty->driver->ops->close(tty);
+
+ if (tty->ldisc.ops && tty->ldisc.ops->close)
+ tty->ldisc.ops->close(tty);
+ }
+}
+
+/* ================================================================== */
+/* Core I/O */
+/* ================================================================== */
+
+int64 tty_read(tty_struct* tty, file* fp, char* buf, uint64 count)
+{
+ if (!tty || !tty->ldisc.ops || !tty->ldisc.ops->read)
+ return -EIO;
+
+ return tty->ldisc.ops->read(tty, fp, buf, count);
+}
+
+int64 tty_write(tty_struct* tty, file* fp, const char* buf, uint64 count)
+{
+ if (!tty || !tty->ldisc.ops || !tty->ldisc.ops->write)
+ return -EIO;
+
+ return tty->ldisc.ops->write(tty, fp, buf, count);
+}
+
+/* ================================================================== */
+/* tty_receive_buf — called by driver on RX */
+/* ================================================================== */
+
+void tty_receive_buf(tty_struct* tty, const char* buf, int count)
+{
+ if (!tty || !tty->ldisc.ops || !tty->ldisc.ops->receive_buf)
+ return;
+
+ tty->ldisc.ops->receive_buf(tty, buf, count);
+}
+
+/* ================================================================== */
+/* tty_signal_pgrp — send signal to foreground process group */
+/* ================================================================== */
+
+extern int send_signal(int sig, kernel_siginfo* info, TaskControlBlock* task);
+
+void tty_signal_pgrp(tty_struct* tty, int sig)
+{
+ if (!tty) return;
+
+ int32 pgid;
+ {
+ LockGuard guard(tty->lock);
+ pgid = tty->pgrp;
+ }
+ if (pgid <= 0) return;
+
+ /* Deliver to the leader of every thread group in the foreground
+ * process group. ldisc paths call this for SIGINT/SIGQUIT/SIGWINCH
+ * etc.; previously we only signalled the current task, which was
+ * incorrect when the fg pgrp had multiple processes. */
+ kernel_siginfo info{};
+ info.si_signo = sig;
+ info.si_code = 0; /* SI_KERNEL would be 0x80; user rarely checks */
+ for_each_pgrp_member(pgid, [&](signal_struct* s) {
+ if (s->leader)
+ send_signal(sig, &info, s->leader);
+ });
+}
+
+/* ================================================================== */
+/* tty_ioctl — ioctl dispatch */
+/* ================================================================== */
+
+long tty_ioctl(tty_struct* tty, file* /*fp*/, unsigned int cmd, uint64 arg)
+{
+ if (!tty) return -EINVAL;
+
+ switch (cmd) {
+ case TCGETS: {
+ auto* p = reinterpret_cast<termios*>(arg);
+ if (!p) return -EINVAL;
+ *p = tty->termios_data;
+ return 0;
+ }
+
+ case TCSETS:
+ case TCSETSW:
+ case TCSETSF: {
+ auto* p = reinterpret_cast<const termios*>(arg);
+ if (!p) return -EINVAL;
+ termios old = tty->termios_data;
+ tty->termios_data = *p;
+
+ /* Notify line discipline of termios change */
+ if (tty->ldisc.ops && tty->ldisc.ops->set_termios)
+ tty->ldisc.ops->set_termios(tty, &old);
+
+ /* If TCSETSF, flush input buffer */
+ if (cmd == TCSETSF) {
+ if (tty->ldisc.ops && tty->ldisc.ops->flush_buffer)
+ tty->ldisc.ops->flush_buffer(tty);
+ }
+ return 0;
+ }
+
+ case TIOCGWINSZ: {
+ auto* p = reinterpret_cast<winsize*>(arg);
+ if (!p) return -EINVAL;
+ *p = tty->ws;
+ return 0;
+ }
+
+ case TIOCSWINSZ: {
+ auto* p = reinterpret_cast<const winsize*>(arg);
+ if (!p) return -EINVAL;
+ winsize old = tty->ws;
+ tty->ws = *p;
+ if (old.ws_row != tty->ws.ws_row || old.ws_col != tty->ws.ws_col) {
+ tty_signal_pgrp(tty, SIGWINCH);
+ }
+ return 0;
+ }
+
+ case TIOCGPGRP: {
+ auto* p = reinterpret_cast<int32*>(arg);
+ if (!p) return -EINVAL;
+
+ TaskControlBlock* curr = current_task();
+ if (!curr || !curr->signal) return -ENOTTY;
+ if (curr->signal->tty != tty) return -ENOTTY;
+
+ int32 pgid;
+ {
+ LockGuard guard(tty->lock);
+ pgid = tty->pgrp != 0 ? tty->pgrp : curr->signal->pgid;
+ }
+ *p = pgid;
+ return 0;
+ }
+
+ case TIOCSPGRP: {
+ auto* p = reinterpret_cast<const int32*>(arg);
+ if (!p) return -EINVAL;
+
+ TaskControlBlock* curr = current_task();
+ if (!curr || !curr->signal) return -ENOTTY;
+ if (curr->signal->tty != tty) return -ENOTTY;
+
+ int32 new_pgid = *p;
+ if (new_pgid <= 0) return -EINVAL;
+
+ /* new_pgid must be in the caller's session. */
+ bool ok = false;
+ for_each_pgrp_member(new_pgid, [&](signal_struct* s) {
+ if (s->sid == curr->signal->sid) ok = true;
+ });
+ if (!ok) return -EPERM;
+
+ LockGuard guard(tty->lock);
+ tty->pgrp = new_pgid;
+ return 0;
+ }
+
+ case TIOCSCTTY: {
+ /* Become the controlling terminal of the caller's session.
+ * Linux: only session leader without ctty may do this; with
+ * arg==1, CAP_SYS_ADMIN may steal a ctty from another session.
+ * We support the basic path; "steal" is left unimplemented. */
+ TaskControlBlock* curr = current_task();
+ if (!curr || !curr->signal) return -EPERM;
+ signal_struct* sig = curr->signal;
+
+ if (sig->sid != curr->pid) return -EPERM; /* not leader */
+
+ LockGuard sg(sig->lock);
+ if (sig->tty != nullptr) return -EPERM; /* already have one */
+
+ LockGuard tg(tty->lock);
+ if (tty->session != 0 && tty->session != sig->sid) {
+ return -EPERM; /* owned elsewhere */
+ }
+ tty->session = sig->sid;
+ tty->pgrp = sig->pgid;
+ sig->tty = tty;
+ return 0;
+ }
+
+ case TIOCNOTTY: {
+ /* Detach from controlling terminal. */
+ TaskControlBlock* curr = current_task();
+ if (!curr || !curr->signal) return -ENOTTY;
+ signal_struct* sig = curr->signal;
+ if (sig->tty != tty) return -ENOTTY;
+
+ /* Session leader: HUP everyone left in the old fg pgrp.
+ * Job-control conformance — without this, daemons spawned by
+ * a relinquishing leader keep blocking on a dead terminal. */
+ bool was_leader = (sig->sid == curr->pid);
+ int32 hup_pgrp = 0;
+ {
+ LockGuard tg(tty->lock);
+ hup_pgrp = tty->pgrp;
+ if (was_leader) {
+ tty->session = 0;
+ tty->pgrp = 0;
+ }
+ }
+ {
+ LockGuard sg(sig->lock);
+ sig->tty = nullptr;
+ }
+ if (was_leader && hup_pgrp > 0) {
+ tty_signal_pgrp(tty, SIGHUP);
+ tty_signal_pgrp(tty, SIGCONT);
+ (void)hup_pgrp;
+ }
+ return 0;
+ }
+
+ case FIONREAD: {
+ /* Delegate to line discipline */
+ if (tty->ldisc.ops && tty->ldisc.ops->ioctl) {
+ int ret = tty->ldisc.ops->ioctl(tty, cmd, arg);
+ if (ret != -ENOIOCTLCMD)
+ return ret;
+ }
+ return 0;
+ }
+
+ case TCFLSH: {
+ int queue = static_cast<int>(arg);
+ if (queue == 0 || queue == 2) {
+ /* Flush input */
+ if (tty->ldisc.ops && tty->ldisc.ops->flush_buffer)
+ tty->ldisc.ops->flush_buffer(tty);
+ }
+ if (queue == 1 || queue == 2) {
+ /* Flush output */
+ if (tty->driver && tty->driver->ops && tty->driver->ops->flush_buffer)
+ tty->driver->ops->flush_buffer(tty);
+ }
+ return 0;
+ }
+
+ default:
+ break;
+ }
+
+ /* Try line discipline ioctl */
+ if (tty->ldisc.ops && tty->ldisc.ops->ioctl) {
+ int ret = tty->ldisc.ops->ioctl(tty, cmd, arg);
+ if (ret != -ENOIOCTLCMD)
+ return ret;
+ }
+
+ /* Try driver ioctl */
+ if (tty->driver && tty->driver->ops && tty->driver->ops->ioctl) {
+ int ret = tty->driver->ops->ioctl(tty, cmd, arg);
+ if (ret != -ENOIOCTLCMD)
+ return ret;
+ }
+
+ return -ENOTTY;
+}
+
+/* ================================================================== */
+/* file_operations for TTY devices */
+/* ================================================================== */
+
+static int64 tty_fops_read(file* fp, char* buf, uint64 count, int64* /*pos*/)
+{
+ auto* tty = static_cast<tty_struct*>(fp->private_data);
+ return tty_read(tty, fp, buf, count);
+}
+
+static int64 tty_fops_write(file* fp, const char* buf, uint64 count, int64* /*pos*/)
+{
+ auto* tty = static_cast<tty_struct*>(fp->private_data);
+ return tty_write(tty, fp, buf, count);
+}
+
+static long tty_fops_ioctl(file* fp, unsigned int cmd, uint64 arg)
+{
+ auto* tty = static_cast<tty_struct*>(fp->private_data);
+ return tty_ioctl(tty, fp, cmd, arg);
+}
+
+/*
+ * tty poll callback — Linux drivers/tty/tty_io.c:tty_poll() does the
+ * same thing: register tty->read_wait so the poller is woken on input,
+ * then ask the ldisc whether data is currently available. Write side
+ * is always considered ready since our writes don't block.
+ */
+extern bool n_tty_input_ready(tty_struct* tty);
+extern void poll_wait(file*, WaitQueue*, poll_table*);
+
+static uint32 tty_fops_poll(file* fp, void* pt)
+{
+ auto* tty = static_cast<tty_struct*>(fp->private_data);
+ if (!tty) return POLLNVAL;
+
+ poll_wait(fp, &tty->read_wait, static_cast<poll_table*>(pt));
+
+ uint32 mask = POLLOUT | POLLWRNORM; /* always writable */
+ if (n_tty_input_ready(tty))
+ mask |= POLLIN | POLLRDNORM;
+ return mask;
+}
+
+/*
+ * Auto-acquire a controlling terminal on open.
+ *
+ * POSIX rule: when a session leader without a controlling tty opens a
+ * terminal that itself has no controlling session, and the caller did
+ * not pass O_NOCTTY, that terminal becomes the controlling terminal of
+ * the caller's session. tty->session and tty->pgrp are seeded from the
+ * caller's sid/pgid so that the very first tcgetpgrp() call returns a
+ * value matching the caller's pgid — which is precisely what BusyBox
+ * ash:setjobctl() needs to exit its setup loop.
+ *
+ * Reference: Linux drivers/tty/tty_jobctrl.c:tty_open_proc_set_tty()
+ */
+static void tty_acquire_ctty(tty_struct* tty, file* fp)
+{
+ TaskControlBlock* curr = current_task();
+ if (!curr || !curr->signal) return;
+ if (fp->f_flags & O_NOCTTY) return;
+
+ signal_struct* sig = curr->signal;
+
+ /* Must be session leader (sid == pid). */
+ if (sig->sid != curr->pid) return;
+
+ LockGuard sig_guard(sig->lock);
+ if (sig->tty != nullptr) return; /* already have one */
+
+ LockGuard tty_guard(tty->lock);
+ if (tty->session != 0) return; /* tty owned by another sess */
+
+ tty->session = sig->sid;
+ tty->pgrp = sig->pgid;
+ sig->tty = tty;
+}
+
+static int tty_fops_open(inode* /*ip*/, file* fp)
+{
+ /* The console TTY is pre-allocated; just attach it */
+ if (!g_console_tty) return -ENXIO;
+ fp->private_data = g_console_tty;
+ int err = tty_open(g_console_tty);
+ if (err == 0) {
+ tty_acquire_ctty(g_console_tty, fp);
+ }
+ return err;
+}
+
+static int tty_fops_release(inode* /*ip*/, file* fp)
+{
+ auto* tty = static_cast<tty_struct*>(fp->private_data);
+ tty_close(tty);
+ return 0;
+}
+
+file_operations tty_fops;
+
+static void init_tty_fops()
+{
+ tty_fops.read = tty_fops_read;
+ tty_fops.write = tty_fops_write;
+ tty_fops.unlocked_ioctl = tty_fops_ioctl;
+ tty_fops.poll = tty_fops_poll;
+ tty_fops.open = tty_fops_open;
+ tty_fops.release = tty_fops_release;
+}
+
+/* ================================================================== */
+/* TTY chardev registration */
+/* ================================================================== */
+
+static cdev tty_cdev;
+
+void tty_init()
+{
+ init_tty_fops();
+
+ /* Allocate the console TTY */
+ g_console_tty = tty_alloc(&uart_tty_driver, 0);
+ if (!g_console_tty) {
+ return;
+ }
+
+ /* Open the TTY (initializes ldisc + driver) */
+ tty_open(g_console_tty);
+
+ /* Leave tty->session and tty->pgrp at 0. The first session-leader
+ * to open /dev/console without O_NOCTTY claims it as its controlling
+ * terminal via tty_acquire_ctty(), which fills both fields. Pre-
+ * seeding them here (as we used to with the hardcoded pid==1) would
+ * make every subsequent acquire path see "owned by another session"
+ * and bail out, leaving sh's tcgetpgrp() returning -ENOTTY and
+ * disabling job control. */
+
+ /* Initialize UART RX interrupts */
+ uart_tty_init(g_console_tty);
+
+ /* Register UART as a struct console driver.
+ * After this, printk/klog output goes through console_write_all()
+ * → uart_console_write() instead of raw early_putc(). */
+ extern void uart_register_console();
+ uart_register_console();
+
+ /* Register as /dev/console chardev — replace the old console_cdev.
+ * We use the same major/minor (5,1) so that existing opens
+ * to /dev/console will get the TTY fops. */
+ tty_cdev.name = "console";
+ tty_cdev.dev = MKDEV(5, 1);
+ tty_cdev.fops = &tty_fops;
+ cdev_register(&tty_cdev);
+}
diff --git a/na/drivers/tty/tty_uart.cc b/na/drivers/tty/tty_uart.cc
new file mode 100644
index 0000000..973a6e9
--- /dev/null
+++ b/na/drivers/tty/tty_uart.cc
@@ -0,0 +1,251 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * tty_uart.cc — UART → tty_driver adapter
+ *
+ * Bridges the 16550A UART hardware to the TTY subsystem.
+ *
+ * TX: Calls early_putc() for each character (same as before).
+ * RX: Registers a PLIC IRQ handler. On UART RX interrupt,
+ * reads available bytes and feeds them to the line discipline
+ * via tty_receive_buf().
+ *
+ * QEMU virt UART:
+ * - IRQ 10 on the PLIC
+ * - 16550A register layout at the base address discovered from FDT
+ */
+
+#include <lume/tty.h>
+#include <lume/irq.h>
+#include <lume/addr.h>
+#include <lume/fdt.h>
+#include <lume/vmm.h>
+#include <lume/errno.h>
+#include <lume/klog.h>
+
+#ifndef ENOIOCTLCMD
+#define ENOIOCTLCMD 515
+#endif
+
+extern "C" void early_putc(char c);
+
+/* 16550A register offsets */
+static constexpr int UART_RBR = 0; /* Receive Buffer Register */
+static constexpr int UART_THR = 0; /* Transmit Holding Register */
+static constexpr int UART_IER = 1; /* Interrupt Enable Register */
+static constexpr int UART_IIR = 2; /* Interrupt Identification Register */
+static constexpr int UART_FCR = 2; /* FIFO Control Register */
+static constexpr int UART_LCR = 3; /* Line Control Register */
+static constexpr int UART_MCR = 4; /* Modem Control Register */
+static constexpr int UART_LSR = 5; /* Line Status Register */
+
+/* LSR bits */
+static constexpr uint8 UART_LSR_DR = 0x01; /* Data Ready */
+static constexpr uint8 UART_LSR_THRE = 0x20; /* TX Holding Register Empty */
+
+/* IER bits */
+static constexpr uint8 UART_IER_RDI = 0x01; /* Received Data Available */
+
+/* MCR bits */
+static constexpr uint8 UART_MCR_DTR = 0x01; /* Data Terminal Ready */
+static constexpr uint8 UART_MCR_RTS = 0x02; /* Request To Send */
+static constexpr uint8 UART_MCR_OUT2 = 0x08; /* Auxiliary output 2 — gates the
+ IRQ pin on PC-style 16550A.
+ Without this the UART
+ internally asserts but does
+ NOT propagate to the PLIC. */
+
+/* QEMU virt default UART IRQ */
+static constexpr uint32 UART_IRQ = 10;
+
+/* UART MMIO base (virtual address) */
+static volatile uint8* g_uart_base;
+static tty_struct* g_uart_tty;
+
+/* ================================================================== */
+/* UART MMIO helpers */
+/* ================================================================== */
+
+static inline uint8 uart_read(int reg)
+{
+ return g_uart_base[reg];
+}
+
+static inline void uart_write(int reg, uint8 val)
+{
+ g_uart_base[reg] = val;
+}
+
+static inline bool uart_rx_ready()
+{
+ return (uart_read(UART_LSR) & UART_LSR_DR) != 0;
+}
+
+static inline char uart_rx_char()
+{
+ return static_cast<char>(uart_read(UART_RBR));
+}
+
+/* ================================================================== */
+/* UART RX interrupt handler */
+/* ================================================================== */
+
+static void uart_irq_handler(uint32 /*irq*/, void* arg)
+{
+ auto* tty = static_cast<tty_struct*>(arg);
+
+ /* Read all available bytes from the FIFO */
+ char buf[16];
+ int count = 0;
+
+ while (uart_rx_ready() && count < 16) {
+ buf[count++] = uart_rx_char();
+ }
+
+ if (count > 0) {
+ tty_receive_buf(tty, buf, count);
+ }
+}
+
+/* ================================================================== */
+/* tty_driver_ops for UART */
+/* ================================================================== */
+
+static int uart_tty_open_op(tty_struct* /*tty*/)
+{
+ return 0;
+}
+
+static void uart_tty_close_op(tty_struct* /*tty*/)
+{
+ /* Nothing to do */
+}
+
+static int uart_tty_write_op(tty_struct* /*tty*/, const char* buf, int count)
+{
+ for (int i = 0; i < count; i++) {
+ early_putc(buf[i]);
+ }
+ return count;
+}
+
+static int uart_tty_write_room_op(tty_struct* /*tty*/)
+{
+ return TTY_BUF_SIZE; /* Always room (blocking putc) */
+}
+
+static void uart_tty_put_char_op(tty_struct* /*tty*/, char ch)
+{
+ early_putc(ch);
+}
+
+static void uart_tty_flush_chars_op(tty_struct* /*tty*/)
+{
+ /* No buffering in early_putc, nothing to flush */
+}
+
+static int uart_tty_chars_in_buffer_op(tty_struct* /*tty*/)
+{
+ return 0; /* No TX buffer */
+}
+
+static void uart_tty_flush_buffer_op(tty_struct* /*tty*/)
+{
+ /* No TX buffer to flush */
+}
+
+static int uart_tty_ioctl_op(tty_struct* /*tty*/, unsigned int /*cmd*/, uint64 /*arg*/)
+{
+ return -ENOIOCTLCMD;
+}
+
+static void uart_tty_hangup_op(tty_struct* /*tty*/)
+{
+ /* Disable UART RX interrupts */
+ if (g_uart_base) {
+ uart_write(UART_IER, 0);
+ }
+}
+
+static const tty_driver_ops uart_ops = {
+ .open = uart_tty_open_op,
+ .close = uart_tty_close_op,
+ .write = uart_tty_write_op,
+ .write_room = uart_tty_write_room_op,
+ .put_char = uart_tty_put_char_op,
+ .flush_chars = uart_tty_flush_chars_op,
+ .chars_in_buffer = uart_tty_chars_in_buffer_op,
+ .flush_buffer = uart_tty_flush_buffer_op,
+ .ioctl = uart_tty_ioctl_op,
+ .throttle = nullptr,
+ .unthrottle = nullptr,
+ .hangup = uart_tty_hangup_op,
+};
+
+const tty_driver uart_tty_driver = {
+ .name = "ttyS",
+ .major = 4,
+ .minor_start = 64,
+ .num = 1,
+ .ops = &uart_ops,
+};
+
+/* ================================================================== */
+/* UART TTY init — discover base address, enable RX IRQ */
+/* ================================================================== */
+
+void uart_tty_init(tty_struct* tty)
+{
+ g_uart_tty = tty;
+
+ /* Discover UART base from FDT */
+ DeviceNode* node = fdt_find_compatible("ns16550a");
+ if (!node) {
+ /* Try alternative compatible string */
+ node = fdt_find_compatible("snps,dw-apb-uart");
+ }
+
+ uint64 uart_pa = 0;
+ if (node) {
+ uart_pa = node->get_prop_u64("reg", 0);
+ }
+
+ if (uart_pa == 0) {
+ /* Fallback: QEMU virt default */
+ uart_pa = 0x10000000;
+ }
+
+ g_uart_base = reinterpret_cast<volatile uint8*>(
+ pa_to_va(phys_addr(uart_pa)).raw);
+
+ /* Set FIFO control: enable FIFOs, clear RX/TX FIFOs, 1-byte trigger */
+ uart_write(UART_FCR, 0x07);
+
+ /* MCR: assert OUT2 (gates the IRQ pin on PC-style 16550A), plus
+ * DTR/RTS for completeness. Mirrors Linux drivers/tty/serial/8250/
+ * 8250_port.c: serial8250_do_startup() does the same. */
+ uart_write(UART_MCR, UART_MCR_DTR | UART_MCR_RTS | UART_MCR_OUT2);
+
+ /* Enable UART receive-data-available interrupt */
+ uart_write(UART_IER, UART_IER_RDI);
+
+ /* Register IRQ handler with PLIC */
+ uint32 irq = UART_IRQ;
+ if (node) {
+ /* Try to get IRQ number from FDT */
+ const FdtProperty* irq_prop = node->get_prop("interrupts");
+ if (irq_prop && irq_prop->len >= 4) {
+ const uint8* val = static_cast<const uint8*>(irq_prop->value);
+ irq = (uint32(val[0]) << 24) | (uint32(val[1]) << 16) |
+ (uint32(val[2]) << 8) | val[3];
+ }
+ }
+
+ g_irq_manager.register_handler(irq, uart_irq_handler, tty);
+ g_irq_manager.enable(irq);
+
+ printk(KERN_INFO "uart_tty_init: base=0x%llx irq=%u IER=0x%x LSR=0x%x\n",
+ (unsigned long long)uart_pa, irq,
+ uart_read(UART_IER), uart_read(UART_LSR));
+}