diff --git a/kernel/src/Api/Syscall.cpp b/kernel/src/Api/Syscall.cpp index fca5605..a91de18 100644 --- a/kernel/src/Api/Syscall.cpp +++ b/kernel/src/Api/Syscall.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -268,6 +269,40 @@ namespace Zenith { out->Second = dt.Second; } + // ---- Socket syscalls ---- + + static int Sys_Socket(int type) { + return Net::Socket::Create(type, Sched::GetCurrentPid()); + } + + static int Sys_Connect(int fd, uint32_t ip, uint16_t port) { + return Net::Socket::Connect(fd, ip, port, Sched::GetCurrentPid()); + } + + static int Sys_Bind(int fd, uint16_t port) { + return Net::Socket::Bind(fd, port, Sched::GetCurrentPid()); + } + + static int Sys_Listen(int fd) { + return Net::Socket::Listen(fd, Sched::GetCurrentPid()); + } + + static int Sys_Accept(int fd) { + return Net::Socket::Accept(fd, Sched::GetCurrentPid()); + } + + static int Sys_Send(int fd, const uint8_t* data, uint32_t len) { + return Net::Socket::Send(fd, data, len, Sched::GetCurrentPid()); + } + + static int Sys_Recv(int fd, uint8_t* buf, uint32_t maxLen) { + return Net::Socket::Recv(fd, buf, maxLen, Sched::GetCurrentPid()); + } + + static void Sys_CloseSock(int fd) { + Net::Socket::Close(fd, Sched::GetCurrentPid()); + } + static void Sys_Reset() { /* Triple fault for now; TODO: implement UEFI runtime function for clean reboot. @@ -364,6 +399,23 @@ namespace Zenith { case SYS_GETTIME: Sys_GetTime((DateTime*)frame->arg1); return 0; + case SYS_SOCKET: + return (int64_t)Sys_Socket((int)frame->arg1); + case SYS_CONNECT: + return (int64_t)Sys_Connect((int)frame->arg1, (uint32_t)frame->arg2, (uint16_t)frame->arg3); + case SYS_BIND: + return (int64_t)Sys_Bind((int)frame->arg1, (uint16_t)frame->arg2); + case SYS_LISTEN: + return (int64_t)Sys_Listen((int)frame->arg1); + case SYS_ACCEPT: + return (int64_t)Sys_Accept((int)frame->arg1); + case SYS_SEND: + return (int64_t)Sys_Send((int)frame->arg1, (const uint8_t*)frame->arg2, (uint32_t)frame->arg3); + case SYS_RECV: + return (int64_t)Sys_Recv((int)frame->arg1, (uint8_t*)frame->arg2, (uint32_t)frame->arg3); + case SYS_CLOSESOCK: + Sys_CloseSock((int)frame->arg1); + return 0; default: return -1; } @@ -390,7 +442,7 @@ namespace Zenith { Hal::WriteMSR(Hal::IA32_FMASK, 0x200); Kt::KernelLogStream(Kt::OK, "Syscall") << "SYSCALL/SYSRET initialized (LSTAR=" - << kcp::hex << (uint64_t)SyscallEntry << kcp::dec << ", 29 syscalls)"; + << kcp::hex << (uint64_t)SyscallEntry << kcp::dec << ", 37 syscalls)"; } } diff --git a/kernel/src/Api/Syscall.hpp b/kernel/src/Api/Syscall.hpp index 2077fb1..9b03f4a 100644 --- a/kernel/src/Api/Syscall.hpp +++ b/kernel/src/Api/Syscall.hpp @@ -40,6 +40,16 @@ namespace Zenith { static constexpr uint64_t SYS_RESET = 26; static constexpr uint64_t SYS_SHUTDOWN = 27; static constexpr uint64_t SYS_GETTIME = 28; + static constexpr uint64_t SYS_SOCKET = 29; + static constexpr uint64_t SYS_CONNECT = 30; + static constexpr uint64_t SYS_BIND = 31; + static constexpr uint64_t SYS_LISTEN = 32; + static constexpr uint64_t SYS_ACCEPT = 33; + static constexpr uint64_t SYS_SEND = 34; + static constexpr uint64_t SYS_RECV = 35; + static constexpr uint64_t SYS_CLOSESOCK = 36; + + static constexpr int SOCK_TCP = 1; struct DateTime { uint16_t Year; diff --git a/kernel/src/Net/Net.cpp b/kernel/src/Net/Net.cpp index ba16d03..23af4cd 100644 --- a/kernel/src/Net/Net.cpp +++ b/kernel/src/Net/Net.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -33,6 +34,7 @@ namespace Net { Icmp::Initialize(); Udp::Initialize(); Tcp::Initialize(); + Socket::Initialize(); // Hook E1000 RX to our Ethernet dispatcher Drivers::Net::E1000::SetRxCallback(Ethernet::OnFrameReceived); diff --git a/kernel/src/Net/Socket.cpp b/kernel/src/Net/Socket.cpp new file mode 100644 index 0000000..efa4452 --- /dev/null +++ b/kernel/src/Net/Socket.cpp @@ -0,0 +1,145 @@ +/* + * Socket.cpp + * Socket descriptor table wrapping kernel TCP layer + * Copyright (c) 2025 Daniel Hammer +*/ + +#include "Socket.hpp" +#include +#include +#include +#include + +using namespace Kt; + +namespace Net::Socket { + + static SocketEntry g_sockets[MAX_SOCKETS] = {}; + static uint16_t g_nextEphemeralPort = 49152; + + static uint16_t AllocEphemeralPort() { + uint16_t port = g_nextEphemeralPort++; + if (g_nextEphemeralPort == 0) g_nextEphemeralPort = 49152; + return port; + } + + static bool ValidFd(int fd, int pid) { + if (fd < 0 || fd >= MAX_SOCKETS) return false; + if (!g_sockets[fd].Active) return false; + if (g_sockets[fd].OwnerPid != pid) return false; + return true; + } + + void Initialize() { + for (int i = 0; i < MAX_SOCKETS; i++) { + g_sockets[i].Active = false; + } + KernelLogStream(OK, "Net") << "Socket table initialized"; + } + + int Create(int type, int pid) { + if (type != SOCK_TCP) return -1; + + for (int i = 0; i < MAX_SOCKETS; i++) { + if (!g_sockets[i].Active) { + g_sockets[i].Active = true; + g_sockets[i].Type = type; + g_sockets[i].OwnerPid = pid; + g_sockets[i].TcpConn = nullptr; + g_sockets[i].LocalPort = 0; + return i; + } + } + return -1; + } + + int Connect(int fd, uint32_t ip, uint16_t port, int pid) { + if (!ValidFd(fd, pid)) return -1; + if (g_sockets[fd].TcpConn != nullptr) return -1; + + uint16_t srcPort = AllocEphemeralPort(); + g_sockets[fd].LocalPort = srcPort; + + Tcp::Connection* conn = Tcp::Connect(ip, port, srcPort); + if (conn == nullptr) return -1; + + g_sockets[fd].TcpConn = conn; + return 0; + } + + int Bind(int fd, uint16_t port, int pid) { + if (!ValidFd(fd, pid)) return -1; + g_sockets[fd].LocalPort = port; + return 0; + } + + int Listen(int fd, int pid) { + if (!ValidFd(fd, pid)) return -1; + if (g_sockets[fd].LocalPort == 0) return -1; + if (g_sockets[fd].TcpConn != nullptr) return -1; + + Tcp::Connection* conn = Tcp::Listen(g_sockets[fd].LocalPort); + if (conn == nullptr) return -1; + + g_sockets[fd].TcpConn = conn; + return 0; + } + + int Accept(int fd, int pid) { + if (!ValidFd(fd, pid)) return -1; + if (g_sockets[fd].TcpConn == nullptr) return -1; + + Tcp::Connection* clientConn = Tcp::Accept(g_sockets[fd].TcpConn); + if (clientConn == nullptr) return -1; + + // Allocate a new socket entry for the accepted connection + for (int i = 0; i < MAX_SOCKETS; i++) { + if (!g_sockets[i].Active) { + g_sockets[i].Active = true; + g_sockets[i].Type = SOCK_TCP; + g_sockets[i].OwnerPid = pid; + g_sockets[i].TcpConn = clientConn; + g_sockets[i].LocalPort = g_sockets[fd].LocalPort; + return i; + } + } + + // No free socket slot — close the accepted connection + Tcp::Close(clientConn); + return -1; + } + + int Send(int fd, const uint8_t* data, uint32_t len, int pid) { + if (!ValidFd(fd, pid)) return -1; + if (g_sockets[fd].TcpConn == nullptr) return -1; + return Tcp::Send(g_sockets[fd].TcpConn, data, (uint16_t)len); + } + + int Recv(int fd, uint8_t* buf, uint32_t maxLen, int pid) { + if (!ValidFd(fd, pid)) return -1; + if (g_sockets[fd].TcpConn == nullptr) return -1; + return Tcp::ReceiveNonBlocking(g_sockets[fd].TcpConn, buf, (uint16_t)maxLen); + } + + void Close(int fd, int pid) { + if (!ValidFd(fd, pid)) return; + if (g_sockets[fd].TcpConn != nullptr) { + Tcp::Close(g_sockets[fd].TcpConn); + g_sockets[fd].TcpConn = nullptr; + } + g_sockets[fd].Active = false; + } + + void CleanupProcess(int pid) { + for (int i = 0; i < MAX_SOCKETS; i++) { + if (g_sockets[i].Active && g_sockets[i].OwnerPid == pid) { + if (g_sockets[i].TcpConn != nullptr) { + Tcp::Close(g_sockets[i].TcpConn); + g_sockets[i].TcpConn = nullptr; + } + g_sockets[i].Active = false; + } + } + } + +} diff --git a/kernel/src/Net/Socket.hpp b/kernel/src/Net/Socket.hpp new file mode 100644 index 0000000..f681bd6 --- /dev/null +++ b/kernel/src/Net/Socket.hpp @@ -0,0 +1,54 @@ +/* + * Socket.hpp + * Socket descriptor table for userspace TCP access + * Copyright (c) 2025 Daniel Hammer +*/ + +#pragma once +#include +#include + +namespace Net::Socket { + + static constexpr int SOCK_TCP = 1; + static constexpr int SOCK_UDP = 2; + static constexpr int MAX_SOCKETS = 64; + + struct SocketEntry { + bool Active; + int Type; + int OwnerPid; + Tcp::Connection* TcpConn; + uint16_t LocalPort; + }; + + void Initialize(); + + // Create a socket of the given type. Returns fd or -1. + int Create(int type, int pid); + + // Connect socket fd to remote ip:port. Returns 0 or -1. + int Connect(int fd, uint32_t ip, uint16_t port, int pid); + + // Bind socket fd to a local port. Returns 0 or -1. + int Bind(int fd, uint16_t port, int pid); + + // Start listening on a bound socket. Returns 0 or -1. + int Listen(int fd, int pid); + + // Accept an incoming connection. Returns new fd or -1. + int Accept(int fd, int pid); + + // Send data on a connected socket. Returns bytes sent or -1. + int Send(int fd, const uint8_t* data, uint32_t len, int pid); + + // Receive data from a connected socket. Returns bytes received, 0 on close, or -1. + int Recv(int fd, uint8_t* buf, uint32_t maxLen, int pid); + + // Close a socket. + void Close(int fd, int pid); + + // Close all sockets owned by a process (called on process exit). + void CleanupProcess(int pid); + +} diff --git a/kernel/src/Net/Tcp.cpp b/kernel/src/Net/Tcp.cpp index 85527e9..da2fe37 100644 --- a/kernel/src/Net/Tcp.cpp +++ b/kernel/src/Net/Tcp.cpp @@ -523,9 +523,11 @@ namespace Net::Tcp { return -1; } + // Phase 1: Send data segments with interrupts disabled (lock-safe) + uint64_t flags; + asm volatile("pushfq; pop %0; cli" : "=r"(flags) :: "memory"); conn->Lock.Acquire(); - // Send data in segments up to MSS (we use a simple 1460 byte MSS) constexpr uint16_t MSS = 1460; uint16_t sent = 0; @@ -538,6 +540,7 @@ namespace Net::Tcp { bool ok = SendSegment(conn, FLAG_ACK | FLAG_PSH, data + sent, segLen); if (!ok) { conn->Lock.Release(); + asm volatile("push %0; popfq" :: "r"(flags) : "memory"); return sent > 0 ? sent : -1; } @@ -554,30 +557,36 @@ namespace Net::Tcp { sent += segLen; } - // Simple wait for ACK with retransmission + conn->Lock.Release(); + asm volatile("push %0; popfq" :: "r"(flags) : "memory"); + + // Phase 2: Wait for ACK without holding the lock (interrupts enabled) uint64_t startTime = Timekeeping::GetMilliseconds(); while (conn->SendUnack != conn->SendNext) { uint64_t now = Timekeeping::GetMilliseconds(); if ((now - startTime) > (RETRANSMIT_TIMEOUT_MS * MAX_RETRANSMITS)) { - break; // Give up + break; } if ((now - conn->RetransmitTime) > RETRANSMIT_TIMEOUT_MS && conn->RetransmitLen > 0) { conn->RetransmitCount++; if (conn->RetransmitCount > MAX_RETRANSMITS) { break; } - // Retransmit: rewind SendNext temporarily + // Retransmit with brief interrupt-disabled window + asm volatile("pushfq; pop %0; cli" : "=r"(flags) :: "memory"); + conn->Lock.Acquire(); uint32_t savedNext = conn->SendNext; conn->SendNext = conn->SendUnack; SendSegment(conn, FLAG_ACK | FLAG_PSH, conn->RetransmitBuffer, conn->RetransmitLen); conn->SendNext = savedNext; - conn->RetransmitTime = now; + conn->RetransmitTime = Timekeeping::GetMilliseconds(); + conn->Lock.Release(); + asm volatile("push %0; popfq" :: "r"(flags) : "memory"); } Timekeeping::Sleep(10); } - conn->Lock.Release(); return sent; } @@ -588,6 +597,8 @@ namespace Net::Tcp { // Block until data is available or connection is closing while (true) { + uint64_t flags; + asm volatile("pushfq; pop %0; cli" : "=r"(flags) :: "memory"); conn->Lock.Acquire(); if (conn->RecvCount > 0) { @@ -603,6 +614,7 @@ namespace Net::Tcp { conn->RecvCount -= toRead; conn->Lock.Release(); + asm volatile("push %0; popfq" :: "r"(flags) : "memory"); return toRead; } @@ -610,19 +622,62 @@ namespace Net::Tcp { conn->CurrentState == State::Closed || conn->CurrentState == State::TimeWait) { conn->Lock.Release(); + asm volatile("push %0; popfq" :: "r"(flags) : "memory"); return 0; // Connection closed } conn->Lock.Release(); + asm volatile("push %0; popfq" :: "r"(flags) : "memory"); Timekeeping::Sleep(10); } } + int ReceiveNonBlocking(Connection* conn, uint8_t* buffer, uint16_t bufferSize) { + if (conn == nullptr) { + return -1; + } + + // Disable interrupts while holding the lock to prevent deadlock + // with OnPacketReceived (called from the network interrupt handler) + uint64_t flags; + asm volatile("pushfq; pop %0; cli" : "=r"(flags) :: "memory"); + + conn->Lock.Acquire(); + + int result; + if (conn->RecvCount > 0) { + uint16_t toRead = conn->RecvCount; + if (toRead > bufferSize) { + toRead = bufferSize; + } + + for (uint16_t i = 0; i < toRead; i++) { + buffer[i] = conn->RecvBuffer[conn->RecvHead]; + conn->RecvHead = (conn->RecvHead + 1) % RECV_BUFFER_SIZE; + } + conn->RecvCount -= toRead; + result = toRead; + } else if (conn->CurrentState == State::CloseWait || + conn->CurrentState == State::Closed || + conn->CurrentState == State::TimeWait) { + result = -1; // Connection closed + } else { + result = 0; // No data available + } + + conn->Lock.Release(); + asm volatile("push %0; popfq" :: "r"(flags) : "memory"); + + return result; + } + void Close(Connection* conn) { if (conn == nullptr) { return; } + uint64_t flags; + asm volatile("pushfq; pop %0; cli" : "=r"(flags) :: "memory"); conn->Lock.Acquire(); switch (conn->CurrentState) { @@ -631,6 +686,7 @@ namespace Net::Tcp { SendSegment(conn, FLAG_FIN | FLAG_ACK, nullptr, 0); conn->SendNext++; conn->Lock.Release(); + asm volatile("push %0; popfq" :: "r"(flags) : "memory"); // Wait for close to complete for (int i = 0; i < 100; i++) { @@ -649,6 +705,7 @@ namespace Net::Tcp { SendSegment(conn, FLAG_FIN | FLAG_ACK, nullptr, 0); conn->SendNext++; conn->Lock.Release(); + asm volatile("push %0; popfq" :: "r"(flags) : "memory"); // Wait for final ACK for (int i = 0; i < 100; i++) { @@ -666,11 +723,13 @@ namespace Net::Tcp { conn->CurrentState = State::Closed; conn->Active = false; conn->Lock.Release(); + asm volatile("push %0; popfq" :: "r"(flags) : "memory"); return; } default: conn->Lock.Release(); + asm volatile("push %0; popfq" :: "r"(flags) : "memory"); conn->Active = false; return; } diff --git a/kernel/src/Net/Tcp.hpp b/kernel/src/Net/Tcp.hpp index 24de7d8..c523394 100644 --- a/kernel/src/Net/Tcp.hpp +++ b/kernel/src/Net/Tcp.hpp @@ -71,6 +71,9 @@ namespace Net::Tcp { // Blocks until data is available or connection is closed. int Receive(Connection* conn, uint8_t* buffer, uint16_t bufferSize); + // Non-blocking receive. Returns bytes read, 0 if no data available, or -1 on closed/error. + int ReceiveNonBlocking(Connection* conn, uint8_t* buffer, uint16_t bufferSize); + // Close a TCP connection gracefully void Close(Connection* conn); diff --git a/programs/bin/shell.elf b/programs/bin/shell.elf index bee4551..4719627 100755 Binary files a/programs/bin/shell.elf and b/programs/bin/shell.elf differ diff --git a/programs/include/Api/Syscall.hpp b/programs/include/Api/Syscall.hpp index 2f5ec78..a56566f 100644 --- a/programs/include/Api/Syscall.hpp +++ b/programs/include/Api/Syscall.hpp @@ -40,6 +40,16 @@ namespace Zenith { static constexpr uint64_t SYS_RESET = 26; static constexpr uint64_t SYS_SHUTDOWN = 27; static constexpr uint64_t SYS_GETTIME = 28; + static constexpr uint64_t SYS_SOCKET = 29; + static constexpr uint64_t SYS_CONNECT = 30; + static constexpr uint64_t SYS_BIND = 31; + static constexpr uint64_t SYS_LISTEN = 32; + static constexpr uint64_t SYS_ACCEPT = 33; + static constexpr uint64_t SYS_SEND = 34; + static constexpr uint64_t SYS_RECV = 35; + static constexpr uint64_t SYS_CLOSESOCK = 36; + + static constexpr int SOCK_TCP = 1; struct DateTime { uint16_t Year; diff --git a/programs/include/zenith/syscall.h b/programs/include/zenith/syscall.h index 48d1065..a457071 100644 --- a/programs/include/zenith/syscall.h +++ b/programs/include/zenith/syscall.h @@ -157,6 +157,32 @@ namespace zenith { return (int32_t)syscall2(Zenith::SYS_PING, (uint64_t)ip, (uint64_t)timeoutMs); } + // Sockets + inline int socket(int type) { + return (int)syscall1(Zenith::SYS_SOCKET, (uint64_t)type); + } + inline int connect(int fd, uint32_t ip, uint16_t port) { + return (int)syscall3(Zenith::SYS_CONNECT, (uint64_t)fd, (uint64_t)ip, (uint64_t)port); + } + inline int bind(int fd, uint16_t port) { + return (int)syscall2(Zenith::SYS_BIND, (uint64_t)fd, (uint64_t)port); + } + inline int listen(int fd) { + return (int)syscall1(Zenith::SYS_LISTEN, (uint64_t)fd); + } + inline int accept(int fd) { + return (int)syscall1(Zenith::SYS_ACCEPT, (uint64_t)fd); + } + inline int send(int fd, const void* data, uint32_t len) { + return (int)syscall3(Zenith::SYS_SEND, (uint64_t)fd, (uint64_t)data, (uint64_t)len); + } + inline int recv(int fd, void* buf, uint32_t maxLen) { + return (int)syscall3(Zenith::SYS_RECV, (uint64_t)fd, (uint64_t)buf, (uint64_t)maxLen); + } + inline int closesocket(int fd) { + return (int)syscall1(Zenith::SYS_CLOSESOCK, (uint64_t)fd); + } + // Process management inline void waitpid(int pid) { syscall1(Zenith::SYS_WAITPID, (uint64_t)pid); } diff --git a/programs/man/syscalls.2 b/programs/man/syscalls.2 index 72f482f..7a5b101 100644 --- a/programs/man/syscalls.2 +++ b/programs/man/syscalls.2 @@ -3,7 +3,7 @@ syscalls - overview of ZenithOS system calls .SH DESCRIPTION - ZenithOS provides 29 system calls (numbers 0-28) for userspace + ZenithOS provides 37 system calls (numbers 0-36) for userspace programs. Syscalls use the x86-64 SYSCALL instruction with the following register convention: @@ -120,6 +120,40 @@ Send an ICMP echo request and wait for reply. int32_t zenith::ping(uint32_t ip, uint32_t timeoutMs); +.B SYS_SOCKET (29) + Create a socket. type=SOCK_TCP (1). Returns fd or -1. + int zenith::socket(int type); + +.B SYS_CONNECT (30) + Connect a TCP socket to a remote host. + int zenith::connect(int fd, uint32_t ip, uint16_t port); + +.B SYS_BIND (31) + Bind a socket to a local port for listening. + int zenith::bind(int fd, uint16_t port); + +.B SYS_LISTEN (32) + Start listening for incoming TCP connections. + int zenith::listen(int fd); + +.B SYS_ACCEPT (33) + Accept an incoming connection on a listening socket. + Returns a new socket fd for the client connection. + int zenith::accept(int fd); + +.B SYS_SEND (34) + Send data on a connected socket. Returns bytes sent. + int zenith::send(int fd, const void* data, uint32_t len); + +.B SYS_RECV (35) + Receive data from a connected socket. Returns bytes + received, 0 on connection close, or -1 on error. + int zenith::recv(int fd, void* buf, uint32_t maxLen); + +.B SYS_CLOSESOCK (36) + Close a socket and release its resources. + int zenith::closesocket(int fd); + .SH FRAMEBUFFER .B SYS_FBINFO (21) Get framebuffer dimensions and format. diff --git a/programs/obj/shell/main.o b/programs/obj/shell/main.o index cceca0a..7c2be88 100644 Binary files a/programs/obj/shell/main.o and b/programs/obj/shell/main.o differ diff --git a/programs/src/irc/main.cpp b/programs/src/irc/main.cpp new file mode 100644 index 0000000..1221737 --- /dev/null +++ b/programs/src/irc/main.cpp @@ -0,0 +1,1079 @@ +/* + * main.cpp + * IRC client for ZenithOS + * Split-screen terminal UI with ANSI escape codes + * Copyright (c) 2025-2026 Daniel Hammer +*/ + +#include + +// ---- Minimal snprintf (no libc available) ---- + +using va_list = __builtin_va_list; +#define va_start __builtin_va_start +#define va_end __builtin_va_end +#define va_arg __builtin_va_arg + +struct PfState { + char* buf; + int pos; + int max; +}; + +static void pf_putc(PfState* st, char c) { + if (st->pos < st->max) st->buf[st->pos] = c; + st->pos++; +} + +static void pf_putnum(PfState* st, unsigned long val, int base, int width, char pad, int neg) { + char tmp[24]; + int i = 0; + const char* digits = "0123456789abcdef"; + if (val == 0) { tmp[i++] = '0'; } + else { while (val > 0) { tmp[i++] = digits[val % base]; val /= base; } } + int total = (neg ? 1 : 0) + i; + if (neg && pad == '0') pf_putc(st, '-'); + for (int w = total; w < width; w++) pf_putc(st, pad); + if (neg && pad != '0') pf_putc(st, '-'); + while (i > 0) pf_putc(st, tmp[--i]); +} + +static int vsnprintf(char* buf, int size, const char* fmt, va_list ap) { + PfState st; + st.buf = buf; + st.pos = 0; + st.max = size > 0 ? size - 1 : 0; + while (*fmt) { + if (*fmt != '%') { pf_putc(&st, *fmt++); continue; } + fmt++; + char pad = ' '; + if (*fmt == '0') { pad = '0'; fmt++; } + int width = 0; + while (*fmt >= '0' && *fmt <= '9') { width = width * 10 + (*fmt - '0'); fmt++; } + if (*fmt == 'l') fmt++; + switch (*fmt) { + case 'd': case 'i': { + long val = va_arg(ap, int); + int neg = 0; unsigned long uval; + if (val < 0) { neg = 1; uval = (unsigned long)(-val); } + else uval = (unsigned long)val; + pf_putnum(&st, uval, 10, width, pad, neg); + break; + } + case 'u': { unsigned val = va_arg(ap, unsigned); pf_putnum(&st, val, 10, width, pad, 0); break; } + case 'x': { unsigned val = va_arg(ap, unsigned); pf_putnum(&st, val, 16, width, pad, 0); break; } + case 's': { + const char* s = va_arg(ap, const char*); + if (!s) s = "(null)"; + int slen = 0; while (s[slen]) slen++; + for (int w = slen; w < width; w++) pf_putc(&st, ' '); + for (int j = 0; j < slen; j++) pf_putc(&st, s[j]); + break; + } + case 'c': { char c = (char)va_arg(ap, int); pf_putc(&st, c); break; } + case '%': pf_putc(&st, '%'); break; + default: pf_putc(&st, '%'); pf_putc(&st, *fmt); break; + } + if (*fmt) fmt++; + } + if (size > 0) { + if (st.pos < size) st.buf[st.pos] = '\0'; + else st.buf[size - 1] = '\0'; + } + return st.pos; +} + +static int snprintf(char* buf, int size, const char* fmt, ...) { + va_list ap; + va_start(ap, fmt); + int ret = vsnprintf(buf, size, fmt, ap); + va_end(ap); + return ret; +} + +// ---- String utilities ---- + +static int slen(const char* s) { + int n = 0; + while (s[n]) n++; + return n; +} + +static bool streq(const char* a, const char* b) { + while (*a && *b) { if (*a != *b) return false; a++; b++; } + return *a == *b; +} + +static bool starts_with(const char* str, const char* prefix) { + while (*prefix) { if (*str != *prefix) return false; str++; prefix++; } + return true; +} + +static const char* skip_spaces(const char* s) { + while (*s == ' ') s++; + return s; +} + +static void strcpy(char* dst, const char* src) { + while (*src) *dst++ = *src++; + *dst = '\0'; +} + +static void strncpy(char* dst, const char* src, int max) { + int i = 0; + while (src[i] && i < max - 1) { dst[i] = src[i]; i++; } + dst[i] = '\0'; +} + +static void memcpy(void* dst, const void* src, int n) { + auto d = (char*)dst; auto s = (const char*)src; + for (int i = 0; i < n; i++) d[i] = s[i]; +} + +static void memmove(void* dst, const void* src, int n) { + auto d = (char*)dst; auto s = (const char*)src; + if (d < s) { for (int i = 0; i < n; i++) d[i] = s[i]; } + else { for (int i = n - 1; i >= 0; i--) d[i] = s[i]; } +} + +// Case-insensitive comparison for IRC commands +static bool streqi(const char* a, const char* b) { + while (*a && *b) { + char ca = *a, cb = *b; + if (ca >= 'A' && ca <= 'Z') ca += 32; + if (cb >= 'A' && cb <= 'Z') cb += 32; + if (ca != cb) return false; + a++; b++; + } + return *a == *b; +} + +// ---- IP parsing (from shell) ---- + +static bool parse_ip(const char* s, uint32_t* out) { + uint32_t octets[4]; + int idx = 0; + uint32_t val = 0; + bool hasDigit = false; + for (int i = 0; ; i++) { + char c = s[i]; + if (c >= '0' && c <= '9') { + val = val * 10 + (c - '0'); + if (val > 255) return false; + hasDigit = true; + } else if (c == '.' || c == '\0') { + if (!hasDigit || idx >= 4) return false; + octets[idx++] = val; + val = 0; hasDigit = false; + if (c == '\0') break; + } else return false; + } + if (idx != 4) return false; + *out = octets[0] | (octets[1] << 8) | (octets[2] << 16) | (octets[3] << 24); + return true; +} + +static bool parse_uint16(const char* s, uint16_t* out) { + uint32_t val = 0; + if (*s == '\0') return false; + while (*s) { + if (*s < '0' || *s > '9') return false; + val = val * 10 + (*s - '0'); + if (val > 65535) return false; + s++; + } + *out = (uint16_t)val; + return true; +} + +// ---- Data structures ---- + +static constexpr int MAX_NICK_LEN = 32; +static constexpr int MAX_CHANNEL_LEN = 64; +static constexpr int MAX_LINE_LEN = 256; +static constexpr int MAX_DISPLAY_LINES = 512; +static constexpr int IRC_MAX_MSG = 512; + +struct IrcState { + int fd; + uint32_t serverIp; + uint16_t serverPort; + char nick[MAX_NICK_LEN]; + char channel[MAX_CHANNEL_LEN]; + bool registered; + bool connected; + bool inChannel; + int nickRetries; +}; + +struct RecvBuffer { + char buf[2048]; + int len; +}; + +struct DisplayLine { + char text[MAX_LINE_LEN]; + int len; +}; + +struct MessageBuffer { + DisplayLine lines[MAX_DISPLAY_LINES]; + int head; + int count; + int scrollOffset; +}; + +struct InputState { + char buf[512]; + int pos; + int len; +}; + +struct TermState { + int cols; + int rows; + int msgAreaRows; +}; + +// ---- Terminal helpers ---- + +static int int_to_buf(char* buf, int n) { + if (n == 0) { buf[0] = '0'; return 1; } + char tmp[12]; int i = 0; + while (n > 0) { tmp[i++] = '0' + (n % 10); n /= 10; } + for (int j = 0; j < i; j++) buf[j] = tmp[i - 1 - j]; + return i; +} + +// ---- Screen buffer for flicker-free rendering ---- + +static constexpr int SCREEN_BUF_SIZE = 32768; + +struct ScreenBuf { + char buf[SCREEN_BUF_SIZE]; + int pos; +}; + +static ScreenBuf screen; + +static void sb_reset() { screen.pos = 0; } + +static void sb_putc(char c) { + if (screen.pos < SCREEN_BUF_SIZE - 1) screen.buf[screen.pos++] = c; +} + +static void sb_puts(const char* s) { + while (*s && screen.pos < SCREEN_BUF_SIZE - 1) screen.buf[screen.pos++] = *s++; +} + +static void sb_cursor_to(int row, int col) { + sb_puts("\033["); + char tmp[12]; int n = int_to_buf(tmp, row); for (int i = 0; i < n; i++) sb_putc(tmp[i]); + sb_putc(';'); + n = int_to_buf(tmp, col); for (int i = 0; i < n; i++) sb_putc(tmp[i]); + sb_putc('H'); +} + +static void sb_flush() { + screen.buf[screen.pos] = '\0'; + zenith::print(screen.buf); +} + +// ---- Globals ---- + +static IrcState irc; +static RecvBuffer recvBuf; +static MessageBuffer msgBuf; +static InputState input; +static TermState term; +static bool running; +static bool dirty; + +// ---- Message buffer ---- + +static void msg_add(const char* text) { + int idx = (msgBuf.head + msgBuf.count) % MAX_DISPLAY_LINES; + if (msgBuf.count >= MAX_DISPLAY_LINES) { + // Overwrite oldest + msgBuf.head = (msgBuf.head + 1) % MAX_DISPLAY_LINES; + } else { + msgBuf.count++; + } + DisplayLine& line = msgBuf.lines[idx]; + int i = 0; + while (text[i] && i < MAX_LINE_LEN - 1) { + line.text[i] = text[i]; + i++; + } + line.text[i] = '\0'; + line.len = i; + + // Auto-scroll to bottom when new message arrives + if (msgBuf.scrollOffset == 0 || msgBuf.count <= term.msgAreaRows) { + msgBuf.scrollOffset = 0; + } + dirty = true; +} + +static void msg_add_fmt(const char* fmt, ...) { + char tmp[MAX_LINE_LEN]; + va_list ap; + va_start(ap, fmt); + vsnprintf(tmp, sizeof(tmp), fmt, ap); + va_end(ap); + msg_add(tmp); +} + +static void msg_clear() { + msgBuf.head = 0; + msgBuf.count = 0; + msgBuf.scrollOffset = 0; +} + +// ---- IRC send helpers ---- + +static void irc_send(const char* fmt, ...) { + char buf[IRC_MAX_MSG]; + va_list ap; + va_start(ap, fmt); + int len = vsnprintf(buf, sizeof(buf) - 2, fmt, ap); + va_end(ap); + // Ensure \r\n termination + if (len > IRC_MAX_MSG - 3) len = IRC_MAX_MSG - 3; + buf[len] = '\r'; + buf[len + 1] = '\n'; + buf[len + 2] = '\0'; + zenith::send(irc.fd, buf, len + 2); +} + +// ---- Sanitize incoming text (strip control chars) ---- + +static void sanitize(char* dst, const char* src, int maxLen) { + int j = 0; + for (int i = 0; src[i] && j < maxLen - 1; i++) { + unsigned char c = (unsigned char)src[i]; + if (c == 0x1B) { + // Skip ESC sequences entirely + continue; + } + if (c < 0x20 && c != ' ') continue; + dst[j++] = (char)c; + } + dst[j] = '\0'; +} + +// ---- IRC prefix parsing ---- + +// Extract nick from ":nick!user@host" prefix +static void parse_prefix_nick(const char* prefix, char* nick, int maxLen) { + int i = 0; + if (prefix[0] == ':') prefix++; + while (prefix[i] && prefix[i] != '!' && prefix[i] != '@' && prefix[i] != ' ' && i < maxLen - 1) { + nick[i] = prefix[i]; + i++; + } + nick[i] = '\0'; +} + +// ---- IRC protocol handlers ---- + +static void irc_handle_ping(const char* params) { + char buf[IRC_MAX_MSG]; + snprintf(buf, sizeof(buf), "PONG %s", params); + irc_send("%s", buf); +} + +static void irc_handle_privmsg(const char* prefix, const char* params) { + char senderNick[MAX_NICK_LEN]; + parse_prefix_nick(prefix, senderNick, sizeof(senderNick)); + + // Find the message text after " :" + const char* text = params; + // Skip target + while (*text && *text != ' ') text++; + text = skip_spaces(text); + if (*text == ':') text++; + + char clean[MAX_LINE_LEN]; + sanitize(clean, text, sizeof(clean)); + + // Check for CTCP ACTION (\001ACTION ... \001) + if (starts_with(clean, "\001ACTION ")) { + const char* action = clean + 8; + // Strip trailing \001 + char actionBuf[MAX_LINE_LEN]; + strncpy(actionBuf, action, sizeof(actionBuf)); + int alen = slen(actionBuf); + if (alen > 0 && actionBuf[alen - 1] == '\001') actionBuf[alen - 1] = '\0'; + msg_add_fmt("\033[35m* %s %s\033[0m", senderNick, actionBuf); + return; + } + + // Color own nick green, others cyan + if (streq(senderNick, irc.nick)) { + msg_add_fmt("\033[1;32m<%s>\033[0m %s", senderNick, clean); + } else { + msg_add_fmt("\033[1;36m<%s>\033[0m %s", senderNick, clean); + } +} + +static void irc_handle_notice(const char* prefix, const char* params) { + char senderNick[MAX_NICK_LEN]; + if (prefix[0]) { + parse_prefix_nick(prefix, senderNick, sizeof(senderNick)); + } else { + strcpy(senderNick, "*"); + } + + const char* text = params; + while (*text && *text != ' ') text++; + text = skip_spaces(text); + if (*text == ':') text++; + + char clean[MAX_LINE_LEN]; + sanitize(clean, text, sizeof(clean)); + msg_add_fmt("\033[1m-%s-\033[0m %s", senderNick, clean); +} + +static void irc_handle_join(const char* prefix, const char* params) { + char nick[MAX_NICK_LEN]; + parse_prefix_nick(prefix, nick, sizeof(nick)); + + const char* chan = params; + if (*chan == ':') chan++; + + if (streq(nick, irc.nick)) { + strncpy(irc.channel, chan, sizeof(irc.channel)); + irc.inChannel = true; + msg_add_fmt("\033[33m* Now talking in %s\033[0m", chan); + } else { + msg_add_fmt("\033[33m* %s has joined %s\033[0m", nick, chan); + } +} + +static void irc_handle_part(const char* prefix, const char* params) { + char nick[MAX_NICK_LEN]; + parse_prefix_nick(prefix, nick, sizeof(nick)); + + const char* chan = params; + while (*chan && *chan != ' ') chan++; // get channel text + // Actually parse channel from params start + chan = params; + char chanBuf[MAX_CHANNEL_LEN]; + int i = 0; + while (chan[i] && chan[i] != ' ' && i < MAX_CHANNEL_LEN - 1) { + chanBuf[i] = chan[i]; i++; + } + chanBuf[i] = '\0'; + + if (streq(nick, irc.nick)) { + irc.inChannel = false; + irc.channel[0] = '\0'; + msg_add_fmt("\033[33m* You have left %s\033[0m", chanBuf); + } else { + msg_add_fmt("\033[33m* %s has left %s\033[0m", nick, chanBuf); + } +} + +static void irc_handle_quit(const char* prefix, const char* params) { + char nick[MAX_NICK_LEN]; + parse_prefix_nick(prefix, nick, sizeof(nick)); + + const char* reason = params; + if (*reason == ':') reason++; + + char clean[MAX_LINE_LEN]; + sanitize(clean, reason, sizeof(clean)); + if (clean[0]) { + msg_add_fmt("\033[33m* %s has quit (%s)\033[0m", nick, clean); + } else { + msg_add_fmt("\033[33m* %s has quit\033[0m", nick); + } +} + +static void irc_handle_nick(const char* prefix, const char* params) { + char oldNick[MAX_NICK_LEN]; + parse_prefix_nick(prefix, oldNick, sizeof(oldNick)); + + const char* newNick = params; + if (*newNick == ':') newNick++; + + if (streq(oldNick, irc.nick)) { + strncpy(irc.nick, newNick, sizeof(irc.nick)); + msg_add_fmt("\033[33m* You are now known as %s\033[0m", newNick); + } else { + msg_add_fmt("\033[33m* %s is now known as %s\033[0m", oldNick, newNick); + } +} + +static void irc_handle_numeric(int num, const char* params) { + // Extract trailing text (after the last " :") + const char* text = params; + const char* lastColon = nullptr; + for (int i = 0; text[i]; i++) { + if (text[i] == ':' && (i == 0 || text[i - 1] == ' ')) { + lastColon = text + i + 1; + } + } + + switch (num) { + case 1: // RPL_WELCOME + irc.registered = true; + if (lastColon) { + char clean[MAX_LINE_LEN]; + sanitize(clean, lastColon, sizeof(clean)); + msg_add_fmt("\033[1m*** %s\033[0m", clean); + } + // Auto-join channel if specified + if (irc.channel[0]) { + irc_send("JOIN %s", irc.channel); + } + break; + + case 433: // ERR_NICKNAMEINUSE + if (irc.nickRetries < 3) { + int nlen = slen(irc.nick); + if (nlen < MAX_NICK_LEN - 2) { + irc.nick[nlen] = '_'; + irc.nick[nlen + 1] = '\0'; + } + irc.nickRetries++; + irc_send("NICK %s", irc.nick); + msg_add_fmt("\033[33m* Nick in use, trying %s\033[0m", irc.nick); + } else { + msg_add("\033[31m*** Could not find available nickname\033[0m"); + } + break; + + case 332: // RPL_TOPIC + case 353: // RPL_NAMREPLY + case 366: // RPL_ENDOFNAMES + case 372: // RPL_MOTD + case 375: // RPL_MOTDSTART + case 376: // RPL_ENDOFMOTD + default: + if (lastColon) { + char clean[MAX_LINE_LEN]; + sanitize(clean, lastColon, sizeof(clean)); + msg_add_fmt("\033[1m*** %s\033[0m", clean); + } + break; + } +} + +// ---- IRC line parser ---- + +static void irc_process_line(char* line) { + // Strip trailing \r\n + int len = slen(line); + while (len > 0 && (line[len - 1] == '\r' || line[len - 1] == '\n')) { + line[--len] = '\0'; + } + if (len == 0) return; + + char prefix[256] = ""; + char* cmd = line; + + // Parse optional prefix + if (line[0] == ':') { + cmd = line + 1; + int i = 0; + while (cmd[i] && cmd[i] != ' ') i++; + if (i > 0) { + memcpy(prefix, cmd, i < 255 ? i : 255); + prefix[i < 255 ? i : 255] = '\0'; + } + cmd += i; + while (*cmd == ' ') cmd++; + } + + // Extract command word + char command[32] = ""; + { + int i = 0; + while (cmd[i] && cmd[i] != ' ' && i < 31) { + command[i] = cmd[i]; i++; + } + command[i] = '\0'; + cmd += i; + while (*cmd == ' ') cmd++; + } + + // cmd now points to params + + // Handle PING first (highest priority) + if (streqi(command, "PING")) { + irc_handle_ping(cmd); + return; + } + + // Check for numeric command + bool isNumeric = true; + for (int i = 0; command[i]; i++) { + if (command[i] < '0' || command[i] > '9') { isNumeric = false; break; } + } + + if (isNumeric && command[0]) { + int num = 0; + for (int i = 0; command[i]; i++) num = num * 10 + (command[i] - '0'); + irc_handle_numeric(num, cmd); + return; + } + + if (streqi(command, "PRIVMSG")) irc_handle_privmsg(prefix, cmd); + else if (streqi(command, "NOTICE")) irc_handle_notice(prefix, cmd); + else if (streqi(command, "JOIN")) irc_handle_join(prefix, cmd); + else if (streqi(command, "PART")) irc_handle_part(prefix, cmd); + else if (streqi(command, "QUIT")) irc_handle_quit(prefix, cmd); + else if (streqi(command, "NICK")) irc_handle_nick(prefix, cmd); + else if (streqi(command, "PONG")) { /* Ignore PONG replies */ } + else if (streqi(command, "ERROR")) { + char clean[MAX_LINE_LEN]; + sanitize(clean, cmd, sizeof(clean)); + msg_add_fmt("\033[31m*** Error: %s\033[0m", clean); + } +} + +// ---- TCP recv with fragment assembly ---- + +static void recv_process() { + char tmp[512]; + int r = zenith::recv(irc.fd, tmp, sizeof(tmp)); + if (r < 0) { + irc.connected = false; + msg_add("\033[31m*** Connection lost\033[0m"); + return; + } + if (r == 0) return; + + // Append to recv buffer + int space = (int)sizeof(recvBuf.buf) - recvBuf.len; + if (r > space) r = space; + memcpy(recvBuf.buf + recvBuf.len, tmp, r); + recvBuf.len += r; + + // Scan for complete lines (\r\n) + int start = 0; + for (int i = 0; i < recvBuf.len - 1; i++) { + if (recvBuf.buf[i] == '\r' && recvBuf.buf[i + 1] == '\n') { + // Extract line [start..i) + int lineLen = i - start; + char lineStr[IRC_MAX_MSG]; + if (lineLen > IRC_MAX_MSG - 1) lineLen = IRC_MAX_MSG - 1; + memcpy(lineStr, recvBuf.buf + start, lineLen); + lineStr[lineLen] = '\0'; + irc_process_line(lineStr); + start = i + 2; + i++; // skip \n + } + } + + // Move unprocessed remainder to front + if (start > 0) { + int remaining = recvBuf.len - start; + if (remaining > 0) { + memmove(recvBuf.buf, recvBuf.buf + start, remaining); + } + recvBuf.len = remaining; + } + + // Prevent overflow if no \r\n found in a full buffer + if (recvBuf.len >= (int)sizeof(recvBuf.buf) - 1) { + recvBuf.len = 0; + } +} + +// ---- UI rendering (buffered, single flush) ---- + +static void ui_render() { + sb_reset(); + + // Hide cursor during redraw + sb_puts("\033[?25l"); + + // Status bar (row 1) + sb_cursor_to(1, 1); + sb_puts("\033[7m\033[2K"); + char status[256]; + if (irc.connected) { + if (irc.inChannel) { + snprintf(status, sizeof(status), " IRC | %s | %s | Connected ", irc.nick, irc.channel); + } else { + snprintf(status, sizeof(status), " IRC | %s | (no channel) | Connected ", irc.nick); + } + } else { + snprintf(status, sizeof(status), " IRC | %s | Disconnected ", irc.nick); + } + sb_puts(status); + int statusLen = slen(status); + for (int i = statusLen; i < term.cols; i++) sb_putc(' '); + sb_puts("\033[0m"); + + // Message area (rows 2..N-2) + int startLine; + if (msgBuf.count <= term.msgAreaRows) { + startLine = 0; + } else { + startLine = msgBuf.count - term.msgAreaRows - msgBuf.scrollOffset; + if (startLine < 0) startLine = 0; + } + for (int r = 0; r < term.msgAreaRows; r++) { + sb_cursor_to(r + 2, 1); + sb_puts("\033[2K"); + int msgIdx = startLine + r; + if (msgIdx < msgBuf.count) { + int realIdx = (msgBuf.head + msgIdx) % MAX_DISPLAY_LINES; + sb_puts(msgBuf.lines[realIdx].text); + } + } + + // Separator (row N-1) + sb_cursor_to(term.rows - 1, 1); + sb_puts("\033[2K\033[90m"); + for (int i = 0; i < term.cols; i++) sb_putc('-'); + sb_puts("\033[0m"); + + // Input line (row N) + sb_cursor_to(term.rows, 1); + sb_puts("\033[2K\033[1m>\033[0m "); + for (int i = 0; i < input.len; i++) sb_putc(input.buf[i]); + + // Position cursor at input insertion point + sb_cursor_to(term.rows, input.pos + 3); + + // Single flush — entire screen in one print call + sb_flush(); +} + +// ---- User command processing ---- + +static void handle_user_input() { + input.buf[input.len] = '\0'; + const char* text = input.buf; + + if (text[0] != '/') { + // Plain message to current channel + if (!irc.inChannel) { + msg_add("\033[31m*** Not in a channel. Use /join #channel\033[0m"); + return; + } + irc_send("PRIVMSG %s :%s", irc.channel, text); + // Echo own message + msg_add_fmt("\033[1;32m<%s>\033[0m %s", irc.nick, text); + return; + } + + // Parse command + const char* cmd = text + 1; + + if (starts_with(cmd, "join ") || starts_with(cmd, "JOIN ")) { + const char* chan = skip_spaces(cmd + 5); + if (*chan == '\0') { + msg_add("\033[31m*** Usage: /join #channel\033[0m"); + return; + } + strncpy(irc.channel, chan, sizeof(irc.channel)); + irc_send("JOIN %s", chan); + } + else if (starts_with(cmd, "part") && (cmd[4] == '\0' || cmd[4] == ' ')) { + if (!irc.inChannel) { + msg_add("\033[31m*** Not in a channel\033[0m"); + return; + } + const char* reason = cmd[4] ? skip_spaces(cmd + 5) : ""; + if (*reason) { + irc_send("PART %s :%s", irc.channel, reason); + } else { + irc_send("PART %s", irc.channel); + } + } + else if (starts_with(cmd, "msg ") || starts_with(cmd, "MSG ")) { + const char* rest = skip_spaces(cmd + 4); + char target[MAX_NICK_LEN]; + int i = 0; + while (rest[i] && rest[i] != ' ' && i < MAX_NICK_LEN - 1) { + target[i] = rest[i]; i++; + } + target[i] = '\0'; + const char* msg = skip_spaces(rest + i); + if (!target[0] || !*msg) { + msg_add("\033[31m*** Usage: /msg nick message\033[0m"); + return; + } + irc_send("PRIVMSG %s :%s", target, msg); + msg_add_fmt("\033[1;35m-> %s:\033[0m %s", target, msg); + } + else if (starts_with(cmd, "nick ") || starts_with(cmd, "NICK ")) { + const char* newNick = skip_spaces(cmd + 5); + if (!*newNick) { + msg_add("\033[31m*** Usage: /nick newnick\033[0m"); + return; + } + irc_send("NICK %s", newNick); + strncpy(irc.nick, newNick, sizeof(irc.nick)); + } + else if (starts_with(cmd, "quit") && (cmd[4] == '\0' || cmd[4] == ' ')) { + const char* reason = cmd[4] ? skip_spaces(cmd + 5) : ""; + if (*reason) { + irc_send("QUIT :%s", reason); + } else { + irc_send("QUIT :Leaving"); + } + irc.connected = false; + running = false; + } + else if (starts_with(cmd, "me ") || starts_with(cmd, "ME ")) { + if (!irc.inChannel) { + msg_add("\033[31m*** Not in a channel\033[0m"); + return; + } + const char* action = skip_spaces(cmd + 3); + irc_send("PRIVMSG %s :\001ACTION %s\001", irc.channel, action); + msg_add_fmt("\033[35m* %s %s\033[0m", irc.nick, action); + } + else if (starts_with(cmd, "raw ") || starts_with(cmd, "RAW ")) { + const char* raw = cmd + 4; + irc_send("%s", raw); + msg_add_fmt("\033[90m>> %s\033[0m", raw); + } + else if (streqi(cmd, "help") || starts_with(cmd, "help ")) { + msg_add("\033[1m--- Help ---\033[0m"); + msg_add(" /join #channel - Join a channel"); + msg_add(" /part [reason] - Leave current channel"); + msg_add(" /msg nick text - Send private message"); + msg_add(" /nick newnick - Change nickname"); + msg_add(" /quit [reason] - Disconnect and exit"); + msg_add(" /me action - Send action"); + msg_add(" /raw text - Send raw IRC line"); + msg_add(" /clear - Clear message area"); + msg_add(" /help - Show this help"); + msg_add(" Ctrl+Q - Quit"); + msg_add(" PgUp/PgDn - Scroll messages"); + } + else if (streqi(cmd, "clear")) { + msg_clear(); + } + else { + char cmdWord[32]; + int i = 0; + while (cmd[i] && cmd[i] != ' ' && i < 31) { cmdWord[i] = cmd[i]; i++; } + cmdWord[i] = '\0'; + msg_add_fmt("\033[31m*** Unknown command: /%s (try /help)\033[0m", cmdWord); + } +} + +// ---- Entry point ---- + +extern "C" void _start() { + // Parse arguments: [#channel] + char argbuf[256]; + zenith::getargs(argbuf, sizeof(argbuf)); + const char* arg = skip_spaces(argbuf); + + if (*arg == '\0') { + zenith::print("Usage: irc.elf [#channel]\n"); + zenith::print("Example: run irc.elf 10.0.68.1 6667 ZenithUser #general\n"); + return; + } + + // Parse IP + char ipStr[32]; + int i = 0; + while (arg[i] && arg[i] != ' ' && i < 31) { ipStr[i] = arg[i]; i++; } + ipStr[i] = '\0'; + arg = skip_spaces(arg + i); + + if (!parse_ip(ipStr, &irc.serverIp)) { + zenith::print("Invalid IP address: "); + zenith::print(ipStr); + zenith::putchar('\n'); + return; + } + + // Parse port + char portStr[16]; + i = 0; + while (arg[i] && arg[i] != ' ' && i < 15) { portStr[i] = arg[i]; i++; } + portStr[i] = '\0'; + arg = skip_spaces(arg + i); + + if (!parse_uint16(portStr, &irc.serverPort)) { + zenith::print("Invalid port: "); + zenith::print(portStr); + zenith::putchar('\n'); + return; + } + + // Parse nickname + i = 0; + while (arg[i] && arg[i] != ' ' && i < MAX_NICK_LEN - 1) { irc.nick[i] = arg[i]; i++; } + irc.nick[i] = '\0'; + arg = skip_spaces(arg + i); + + if (!irc.nick[0]) { + zenith::print("Missing nickname\n"); + return; + } + + // Parse optional channel + i = 0; + while (arg[i] && arg[i] != ' ' && i < MAX_CHANNEL_LEN - 1) { irc.channel[i] = arg[i]; i++; } + irc.channel[i] = '\0'; + + // Initialize state + irc.fd = -1; + irc.registered = false; + irc.connected = false; + irc.inChannel = false; + irc.nickRetries = 0; + recvBuf.len = 0; + msgBuf.head = 0; + msgBuf.count = 0; + msgBuf.scrollOffset = 0; + input.pos = 0; + input.len = 0; + running = true; + + // Get terminal size + zenith::termsize(&term.cols, &term.rows); + term.msgAreaRows = term.rows - 3; + if (term.msgAreaRows < 1) term.msgAreaRows = 1; + + // Enter alternate screen buffer, hide cursor + zenith::print("\033[?1049h"); + zenith::print("\033[?25l"); + + // Initial draw + msg_add("\033[1m*** ZenithOS IRC Client\033[0m"); + msg_add_fmt("*** Connecting to %s:%d as %s...", ipStr, (int)irc.serverPort, irc.nick); + ui_render(); + + // Create socket and connect + irc.fd = zenith::socket(Zenith::SOCK_TCP); + if (irc.fd < 0) { + msg_add("\033[31m*** Failed to create socket\033[0m"); + ui_render(); + // Wait for keypress then exit + while (!zenith::is_key_available()) zenith::yield(); + goto cleanup; + } + + if (zenith::connect(irc.fd, irc.serverIp, irc.serverPort) < 0) { + msg_add("\033[31m*** Connection failed\033[0m"); + ui_render(); + zenith::closesocket(irc.fd); + irc.fd = -1; + while (!zenith::is_key_available()) zenith::yield(); + goto cleanup; + } + + irc.connected = true; + msg_add("\033[32m*** Connected!\033[0m"); + + // Send IRC registration + irc_send("NICK %s", irc.nick); + irc_send("USER %s 0 * :%s", irc.nick, irc.nick); + + ui_render(); + + // ---- Main loop ---- + while (running && irc.connected) { + dirty = false; + + // Process incoming data + int prevCount = msgBuf.count; + recv_process(); + if (msgBuf.count != prevCount) dirty = true; + + // Poll keyboard + if (zenith::is_key_available()) { + Zenith::KeyEvent ev; + zenith::getkey(&ev); + + if (!ev.pressed) { + if (dirty) ui_render(); + continue; + } + + dirty = true; + + // Ctrl+Q to quit + if (ev.ctrl && (ev.ascii == 'q' || ev.ascii == 'Q')) { + if (irc.connected) { + irc_send("QUIT :Leaving"); + } + running = false; + continue; + } + + // Handle special keys by scancode + switch (ev.scancode) { + case 0x49: // Page Up + if (msgBuf.scrollOffset < msgBuf.count - term.msgAreaRows) { + msgBuf.scrollOffset += term.msgAreaRows / 2; + int maxScroll = msgBuf.count - term.msgAreaRows; + if (maxScroll < 0) maxScroll = 0; + if (msgBuf.scrollOffset > maxScroll) msgBuf.scrollOffset = maxScroll; + } + break; + case 0x51: // Page Down + msgBuf.scrollOffset -= term.msgAreaRows / 2; + if (msgBuf.scrollOffset < 0) msgBuf.scrollOffset = 0; + break; + case 0x47: // Home (scroll to top) + if (msgBuf.count > term.msgAreaRows) { + msgBuf.scrollOffset = msgBuf.count - term.msgAreaRows; + } + break; + case 0x4F: // End (scroll to bottom) + msgBuf.scrollOffset = 0; + break; + default: + if (ev.ascii == '\n') { + if (input.len > 0) { + input.buf[input.len] = '\0'; + handle_user_input(); + input.pos = 0; + input.len = 0; + } + } else if (ev.ascii == '\b') { + if (input.pos > 0) { + for (int j = input.pos - 1; j < input.len - 1; j++) { + input.buf[j] = input.buf[j + 1]; + } + input.pos--; + input.len--; + } + } else if (ev.ascii >= ' ' && ev.ascii <= '~') { + if (input.len < 510) { + for (int j = input.len; j > input.pos; j--) { + input.buf[j] = input.buf[j - 1]; + } + input.buf[input.pos] = ev.ascii; + input.pos++; + input.len++; + } + } else { + dirty = false; // Unhandled key, no redraw needed + } + break; + } + } else { + if (!dirty) { + zenith::yield(); + continue; + } + } + + if (dirty) ui_render(); + } + + if (irc.fd >= 0) { + zenith::closesocket(irc.fd); + } + +cleanup: + // Restore terminal + zenith::print("\033[?25h"); + zenith::print("\033[?1049l"); +} diff --git a/programs/src/shell/main.cpp b/programs/src/shell/main.cpp index 2593fb5..2181688 100644 --- a/programs/src/shell/main.cpp +++ b/programs/src/shell/main.cpp @@ -94,6 +94,7 @@ static void cmd_help() { zenith::print(" cat Display file contents\n"); zenith::print(" run Spawn a new process from an ELF file\n"); zenith::print(" ping Send ICMP echo requests\n"); + zenith::print(" tcpconnect Connect to a TCP server\n"); zenith::print(" date Show current date and time\n"); zenith::print(" uptime Show uptime in milliseconds\n"); zenith::print(" clear Clear the screen\n"); @@ -297,20 +298,162 @@ static void cmd_ping(const char* arg) { } } -static void cmd_run(const char* arg) { +static bool parse_uint16(const char* s, uint16_t* out) { + uint32_t val = 0; + if (*s == '\0') return false; + while (*s) { + if (*s < '0' || *s > '9') return false; + val = val * 10 + (*s - '0'); + if (val > 65535) return false; + s++; + } + *out = (uint16_t)val; + return true; +} + +static void cmd_tcpconnect(const char* arg) { arg = skip_spaces(arg); if (*arg == '\0') { - zenith::print("Usage: run \n"); + zenith::print("Usage: tcpconnect \n"); return; } - char path[128]; - resolve_path(arg, path, sizeof(path)); + // Parse IP address (up to first space) + char ipStr[32]; + int i = 0; + while (arg[i] && arg[i] != ' ' && i < 31) { + ipStr[i] = arg[i]; + i++; + } + ipStr[i] = '\0'; - int pid = zenith::spawn(path); + uint32_t ip; + if (!parse_ip(ipStr, &ip)) { + zenith::print("Invalid IP address: "); + zenith::print(ipStr); + zenith::putchar('\n'); + return; + } + + // Parse port + const char* portStr = skip_spaces(arg + i); + if (*portStr == '\0') { + zenith::print("Usage: tcpconnect \n"); + return; + } + uint16_t port; + if (!parse_uint16(portStr, &port)) { + zenith::print("Invalid port: "); + zenith::print(portStr); + zenith::putchar('\n'); + return; + } + + // Create socket + int fd = zenith::socket(Zenith::SOCK_TCP); + if (fd < 0) { + zenith::print("Error: failed to create socket\n"); + return; + } + + zenith::print("Connecting to "); + print_ip(ip); + zenith::putchar(':'); + print_int(port); + zenith::print("...\n"); + + if (zenith::connect(fd, ip, port) < 0) { + zenith::print("Error: connection failed\n"); + zenith::closesocket(fd); + return; + } + + zenith::print("Connected! Type to send, Ctrl+Q to disconnect.\n"); + + // Interactive send/receive loop + char sendBuf[256]; + int sendPos = 0; + uint8_t recvBuf[512]; + + while (true) { + // Poll for received data (non-blocking) + int r = zenith::recv(fd, recvBuf, sizeof(recvBuf) - 1); + if (r < 0) { + zenith::print("\nConnection closed by remote.\n"); + break; + } + if (r > 0) { + recvBuf[r] = '\0'; + zenith::print((const char*)recvBuf); + } + + // Poll keyboard + if (zenith::is_key_available()) { + Zenith::KeyEvent ev; + zenith::getkey(&ev); + + if (!ev.pressed) continue; + + // Ctrl+Q to quit + if (ev.ctrl && (ev.ascii == 'q' || ev.ascii == 'Q')) { + zenith::print("\nDisconnecting...\n"); + break; + } + + if (ev.ascii == '\n') { + sendBuf[sendPos++] = '\n'; + zenith::putchar('\n'); + zenith::send(fd, sendBuf, sendPos); + sendPos = 0; + } else if (ev.ascii == '\b') { + if (sendPos > 0) { + sendPos--; + zenith::putchar('\b'); + zenith::putchar(' '); + zenith::putchar('\b'); + } + } else if (ev.ascii >= ' ' && sendPos < 254) { + sendBuf[sendPos++] = ev.ascii; + zenith::putchar(ev.ascii); + } + } else { + // No key and no data — yield to avoid busy-spinning + zenith::yield(); + } + } + + zenith::closesocket(fd); +} + +static void cmd_run(const char* arg) { + arg = skip_spaces(arg); + if (*arg == '\0') { + zenith::print("Usage: run [args...]\n"); + return; + } + + // Split filename from arguments at first space + char filename[128]; + int i = 0; + while (arg[i] && arg[i] != ' ' && i < 127) { + filename[i] = arg[i]; + i++; + } + filename[i] = '\0'; + + const char* args = nullptr; + if (arg[i] == ' ') { + args = skip_spaces(arg + i); + if (*args == '\0') args = nullptr; + } + + char path[128]; + resolve_path(filename, path, sizeof(path)); + + int pid = zenith::spawn(path, args); if (pid < 0) { zenith::print("Error: failed to spawn '"); - zenith::print(arg); + zenith::print(filename); zenith::print("'\n"); } else { zenith::waitpid(pid); @@ -461,6 +604,10 @@ static void process_command(const char* line) { cmd_ping(line + 5); } else if (streq(line, "ping")) { cmd_ping(""); + } else if (starts_with(line, "tcpconnect ")) { + cmd_tcpconnect(line + 11); + } else if (streq(line, "tcpconnect")) { + cmd_tcpconnect(""); } else if (streq(line, "date")) { cmd_date(); } else if (streq(line, "uptime")) { diff --git a/ramdisk.tar b/ramdisk.tar index ff32589..a3dd819 100644 Binary files a/ramdisk.tar and b/ramdisk.tar differ