feat: e100e Ethernet driver, ramdisk reorganization, shell rewrite, web server/client, and more

This commit is contained in:
2026-02-19 01:18:11 +01:00
parent 1a5d943649
commit d355d376f9
63 changed files with 5368 additions and 614 deletions
+42 -8
View File
@@ -56,29 +56,63 @@ BINDIR := bin
# Discover all programs (each subdirectory under src/ is a program).
PROGRAMS := $(notdir $(wildcard src/*))
# Build targets: one ELF per program.
TARGETS := $(addprefix $(BINDIR)/,$(addsuffix .elf,$(PROGRAMS)))
# Games are built separately (doom has its own Makefile).
GAMES := doom
SYSTEM_PROGRAMS := $(filter-out $(GAMES),$(PROGRAMS))
# Build targets: system programs go to bin/os/, games are handled separately.
TARGETS := $(addprefix $(BINDIR)/os/,$(addsuffix .elf,$(SYSTEM_PROGRAMS)))
# Game data files to copy into bin/games/.
GAME_DATA := $(BINDIR)/games/doom1.wad
# Man pages source directory.
MANDIR := man
MANSRC := $(wildcard $(MANDIR)/*.*)
MANDST := $(patsubst $(MANDIR)/%,$(BINDIR)/man/%,$(MANSRC))
.PHONY: all clean
# Web content source directory.
WWWDIR := www
WWWSRC := $(wildcard $(WWWDIR)/*.*)
WWWDST := $(patsubst $(WWWDIR)/%,$(BINDIR)/www/%,$(WWWSRC))
all: $(TARGETS) $(MANDST)
# Home directory placeholder.
HOMEKEEP := $(BINDIR)/home/.keep
.PHONY: all clean doom
all: $(TARGETS) doom $(GAME_DATA) $(MANDST) $(WWWDST) $(HOMEKEEP)
# Build doom via its own Makefile.
doom:
$(MAKE) -C src/doom
# Copy doom1.wad from data/games/ into bin/games/.
$(BINDIR)/games/doom1.wad: data/games/doom1.wad
mkdir -p $(BINDIR)/games
cp $< $@
# Copy man pages into bin/man/ so mkramdisk.sh picks them up.
$(BINDIR)/man/%: $(MANDIR)/%
mkdir -p $(BINDIR)/man
cp $< $@
# Build each program from its source files.
# For now each program is a single .cpp file compiled and linked directly.
$(BINDIR)/%.elf: src/%/main.cpp link.ld GNUmakefile
mkdir -p $(BINDIR) obj/$*
# Copy web content into bin/www/ so mkramdisk.sh picks them up.
$(BINDIR)/www/%: $(WWWDIR)/%
mkdir -p $(BINDIR)/www
cp $< $@
# Create empty home directory with a keep file.
$(HOMEKEEP):
mkdir -p $(BINDIR)/home
touch $@
# Build each system program from its source files.
$(BINDIR)/os/%.elf: src/%/main.cpp link.ld GNUmakefile
mkdir -p $(BINDIR)/os obj/$*
$(CXX) $(CXXFLAGS) -c src/$*/main.cpp -o obj/$*/main.o
$(CXX) $(CXXFLAGS) $(LDFLAGS) obj/$*/main.o -o $@
clean:
rm -rf $(BINDIR) obj
$(MAKE) -C src/doom clean
Binary file not shown.
Binary file not shown.
Binary file not shown.
+15
View File
@@ -48,8 +48,23 @@ namespace Zenith {
static constexpr uint64_t SYS_SEND = 34;
static constexpr uint64_t SYS_RECV = 35;
static constexpr uint64_t SYS_CLOSESOCK = 36;
static constexpr uint64_t SYS_GETNETCFG = 37;
static constexpr uint64_t SYS_SETNETCFG = 38;
static constexpr uint64_t SYS_SENDTO = 39;
static constexpr uint64_t SYS_RECVFROM = 40;
static constexpr uint64_t SYS_FWRITE = 41;
static constexpr uint64_t SYS_FCREATE = 42;
static constexpr int SOCK_TCP = 1;
static constexpr int SOCK_UDP = 2;
struct NetCfg {
uint32_t ipAddress; // network byte order
uint32_t subnetMask; // network byte order
uint32_t gateway; // network byte order
uint8_t macAddress[6];
uint8_t _pad[2];
};
struct DateTime {
uint16_t Year;
+71
View File
@@ -0,0 +1,71 @@
/*
* string.h
* Common string and memory utility functions for ZenithOS programs
* Copyright (c) 2025-2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
namespace zenith {
inline int slen(const char* s) {
int n = 0;
while (s[n]) n++;
return n;
}
inline bool streq(const char* a, const char* b) {
while (*a && *b) {
if (*a != *b) return false;
a++; b++;
}
return *a == *b;
}
inline bool starts_with(const char* str, const char* prefix) {
while (*prefix) {
if (*str != *prefix) return false;
str++; prefix++;
}
return true;
}
inline const char* skip_spaces(const char* s) {
while (*s == ' ') s++;
return s;
}
inline void memcpy(void* dst, const void* src, uint64_t n) {
auto* d = (uint8_t*)dst;
auto* s = (const uint8_t*)src;
for (uint64_t i = 0; i < n; i++) d[i] = s[i];
}
inline void memmove(void* dst, const void* src, uint64_t n) {
auto* d = (uint8_t*)dst;
auto* s = (const uint8_t*)src;
if (d < s) {
for (uint64_t i = 0; i < n; i++) d[i] = s[i];
} else {
for (uint64_t i = n; i > 0; i--) d[i-1] = s[i-1];
}
}
inline void memset(void* dst, int val, uint64_t n) {
auto* d = (uint8_t*)dst;
for (uint64_t i = 0; i < n; i++) d[i] = (uint8_t)val;
}
inline void strcpy(char* dst, const char* src) {
while (*src) *dst++ = *src++;
*dst = '\0';
}
inline 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';
}
}
+20
View File
@@ -136,6 +136,14 @@ namespace zenith {
return (int)syscall3(Zenith::SYS_READDIR, (uint64_t)path, (uint64_t)names, (uint64_t)max);
}
// File write/create
inline int fwrite(int handle, const uint8_t* buf, uint64_t off, uint64_t size) {
return (int)syscall4(Zenith::SYS_FWRITE, (uint64_t)handle, (uint64_t)buf, off, size);
}
inline int fcreate(const char* path) {
return (int)syscall1(Zenith::SYS_FCREATE, (uint64_t)path);
}
// Memory
inline void* alloc(uint64_t size) { return (void*)syscall1(Zenith::SYS_ALLOC, size); }
inline void free(void* ptr) { syscall1(Zenith::SYS_FREE, (uint64_t)ptr); }
@@ -157,6 +165,10 @@ namespace zenith {
return (int32_t)syscall2(Zenith::SYS_PING, (uint64_t)ip, (uint64_t)timeoutMs);
}
// Network configuration
inline void get_netcfg(Zenith::NetCfg* out) { syscall1(Zenith::SYS_GETNETCFG, (uint64_t)out); }
inline int set_netcfg(const Zenith::NetCfg* cfg) { return (int)syscall1(Zenith::SYS_SETNETCFG, (uint64_t)cfg); }
// Sockets
inline int socket(int type) {
return (int)syscall1(Zenith::SYS_SOCKET, (uint64_t)type);
@@ -182,6 +194,14 @@ namespace zenith {
inline int closesocket(int fd) {
return (int)syscall1(Zenith::SYS_CLOSESOCK, (uint64_t)fd);
}
inline int sendto(int fd, const void* data, uint32_t len, uint32_t destIp, uint16_t destPort) {
return (int)syscall5(Zenith::SYS_SENDTO, (uint64_t)fd, (uint64_t)data,
(uint64_t)len, (uint64_t)destIp, (uint64_t)destPort);
}
inline int recvfrom(int fd, void* buf, uint32_t maxLen, uint32_t* srcIp, uint16_t* srcPort) {
return (int)syscall5(Zenith::SYS_RECVFROM, (uint64_t)fd, (uint64_t)buf,
(uint64_t)maxLen, (uint64_t)srcIp, (uint64_t)srcPort);
}
// Process management
inline void waitpid(int pid) { syscall1(Zenith::SYS_WAITPID, (uint64_t)pid); }
+48
View File
@@ -0,0 +1,48 @@
.TH DHCP 1
.SH NAME
dhcp - obtain network configuration via DHCP
.SH SYNOPSIS
dhcp
.SH DESCRIPTION
The DHCP client automatically obtains an IP address, subnet mask,
default gateway, and other network parameters from a DHCP server
on the local network using the Dynamic Host Configuration Protocol
(RFC 2131).
On success the network configuration is applied immediately via
set_netcfg(). On failure the original configuration is restored.
The client is run automatically by the init system at boot, but
may also be invoked manually from the shell.
.SH PROTOCOL
The client performs the standard four-message DHCP exchange:
1. DHCPDISCOVER Broadcast to 255.255.255.255:67
2. DHCPOFFER Server offers an IP address
3. DHCPREQUEST Client accepts the offered address
4. DHCPACK Server confirms the lease
The BROADCAST flag (0x8000) is set so that server replies are
sent to the broadcast address, since the client has no IP yet.
Each step has a 10-second timeout. If no response is received
the client exits with an error and restores the previous config.
.SH OUTPUT
On success the client prints the assigned configuration:
IP Address, Subnet Mask, Gateway, DNS Server, Lease Time
.SH OPTIONS
The DHCP client requests the following options from the server:
1 Subnet Mask
3 Router (default gateway)
6 DNS Server
51 Lease Time
.SH SEE ALSO
ifconfig(1), shell(1), syscalls(2)
+51
View File
@@ -0,0 +1,51 @@
.TH EDIT 1
.SH NAME
edit - text editor for ZenithOS
.SH SYNOPSIS
edit [filename]
.SH DESCRIPTION
edit is an interactive text editor. When invoked with a filename,
it opens the file for editing. If the file does not exist, a new
empty buffer is created and will be saved to that path on write.
When invoked without arguments, edit opens an empty buffer. You
will be prompted for a filename when saving.
.SH KEYBOARD SHORTCUTS
.SS Navigation
Arrow Keys Move cursor up/down/left/right
Home Move to start of line
End Move to end of line
Page Up Scroll up one page
Page Down Scroll down one page
.SS Editing
Backspace Delete character before cursor
Delete Delete character at cursor
Enter Insert new line
Tab Insert 4 spaces
.SS Commands
Ctrl+S Save file
Ctrl+Q Quit (warns if unsaved changes)
Ctrl+F Search for text
Ctrl+G Find next occurrence
.SH DISPLAY
The top line shows the filename, a modified indicator [+],
and the current cursor position (Ln, Col).
The bottom line shows keyboard shortcuts or status messages.
Line numbers are displayed in a gutter on the left side.
Lines past the end of the file are marked with ~.
.SH EXAMPLES
edit intro.1 Edit a file
edit Open a new empty buffer
.SH SEE ALSO
cat(1), shell(1)
+6 -4
View File
@@ -12,13 +12,13 @@
.SH DESCRIPTION
ZenithOS provides a simple read-only Virtual File System (VFS)
backed by the boot ramdisk. Files are accessed via paths in the
format "<drive>:/<name>", where drive 0 is the ramdisk.
format "<drive>:/<path>", where drive 0 is the ramdisk.
.SS open
Opens a file and returns a non-negative handle on success, or a
negative value on error (file not found, no free handles).
int h = zenith::open("0:/shell.elf");
int h = zenith::open("0:/os/hello.elf");
.SS read
Reads up to 'size' bytes starting at 'offset' into 'buf'.
@@ -42,15 +42,17 @@
.SS readdir
Lists entries in a directory. Up to 'max' entry names (max 64)
are written to the 'names' array. The kernel allocates a user-
accessible page for the string data automatically.
accessible page for the string data automatically. Directory
entries are returned with a trailing slash.
const char* entries[64];
int count = zenith::readdir("0:/", entries, 64);
// entries: "os/", "games/", "man/", "www/", "home/"
.SH READING PATTERN
The standard pattern for reading a file:
int h = zenith::open("0:/myfile.txt");
int h = zenith::open("0:/man/intro.1");
uint64_t size = zenith::getsize(h);
uint8_t buf[512];
uint64_t off = 0;
+39
View File
@@ -0,0 +1,39 @@
.TH INIT 1
.SH NAME
init - ZenithOS init system
.SH SYNOPSIS
Spawned automatically by the kernel as PID 0.
.SH DESCRIPTION
init is the first userspace process started by the ZenithOS
kernel. It chains system services in sequence, then launches
the interactive shell.
Each service is spawned as a child process. init waits for it
to exit before starting the next one. If a service fails to
spawn, init logs an error and continues to the next stage.
Log output is timestamped and color-coded:
HH:MM:SS INFO init Starting dhcp
HH:MM:SS OK init dhcp finished (pid 1)
.SH BOOT SEQUENCE
The following services are started in order:
1. 0:/os/dhcp.elf Obtain network configuration via DHCP
2. 0:/os/shell.elf Launch the interactive shell
After the shell exits, init enters an idle loop.
.SH LOG LEVELS
init uses four log levels, each with a distinct color:
OK Green Service completed successfully
INFO Cyan Informational (service starting, etc.)
WARN Yellow Non-fatal warning
FAIL Red Service failed to start
.SH SEE ALSO
dhcp(1), shell(1), syscalls(2)
+22 -6
View File
@@ -19,26 +19,42 @@
extern "C" void _start() { ... }
There is no argc/argv. Include <zenith/syscall.h> for the full
typed syscall API. Include <zenith/heap.h> for malloc/mfree.
There is no argc/argv. Use zenith::getargs() to retrieve any
arguments passed by the parent process. Include <zenith/syscall.h>
for the full typed syscall API.
Build with:
cd programs && make
The resulting ELF binary appears in programs/bin/.
The resulting ELF binary appears in programs/bin/os/.
.SH RAMDISK LAYOUT
The boot ramdisk is mounted as drive 0 with the following
directory structure:
0:/os/ System binaries (shell, init, man, etc.)
0:/games/ Games (doom)
0:/man/ Manual pages
0:/www/ Web server content
0:/home/ User home directory
.SH SHELL
The built-in shell is the primary way to interact with ZenithOS.
Type 'help' at the shell prompt for a list of commands. Use
'man shell' for detailed shell documentation.
The interactive shell is the primary way to interact with
ZenithOS. Commands are resolved by searching the PATH
directories (0:/os/, 0:/games/) for matching .elf binaries.
Type 'help' at the shell prompt for a list of commands.
Use 'man shell' for detailed shell documentation.
.SH MAN PAGES
The following man pages are available:
intro(1) This page
shell(1) Shell commands reference
init(1) Init system
dhcp(1) DHCP client
man(1) The man command itself
legal(7) Copyright and legal information
syscalls(2) Overview of all syscalls
spawn(2) Process spawning
file(2) File I/O syscalls
+4 -2
View File
@@ -11,7 +11,7 @@
fullscreen pager. Pages are stored as plain text files with
simple formatting directives.
If no section is specified, sections 1, 2, and 3 are searched
If no section is specified, sections 1 through 7 are searched
in order. If a section number is given, only that section is
checked.
@@ -27,9 +27,10 @@
q Quit
.SH SECTIONS
1 User commands (shell built-ins)
1 User commands and programs
2 System calls (kernel interface)
3 Library functions (userspace libraries)
7 Miscellaneous (legal, conventions)
.SH FILES
Man pages are stored on the ramdisk at:
@@ -42,6 +43,7 @@
man intro View the introduction
man 2 syscalls View syscall overview (section 2)
man malloc View malloc documentation
man legal View copyright information
.SH SEE ALSO
intro(1), shell(1), syscalls(2)
+77 -42
View File
@@ -3,66 +3,101 @@
shell - ZenithOS interactive command shell
.SH DESCRIPTION
The ZenithOS shell is a simple command interpreter that runs as
the first userspace process. It provides basic file inspection,
process management, networking, and documentation access.
The ZenithOS shell is a command interpreter launched by init
after system services have started. It provides command
execution, file navigation, and command history.
.SH COMMANDS
Commands are either shell builtins or external programs. When
a command is not a builtin, the shell searches for a matching
ELF binary and executes it as a child process.
.SH COMMAND RESOLUTION
When a non-builtin command is entered, the shell searches for
a matching binary in the following order:
1. 0:/os/<command>.elf
2. 0:/games/<command>.elf
3. 0:/<cwd>/<command>.elf (if cwd is set)
4. 0:/<command>.elf
The first match is spawned and the shell waits for it to exit.
If no match is found, the shell prints:
<command>: command not found
Arguments after the command name are passed to the spawned
process. File path arguments are resolved against the current
working directory before being passed to external programs.
.SH BUILTINS
.SS help
Display a list of available commands.
.SS info
Show the OS name, version, and syscall API version number.
.SS man <topic>
Open a manual page in the fullscreen pager. See man(1).
Display a categorized list of available commands.
.SS ls [dir]
List files in the current directory, or in the specified
directory. When a directory argument is given, the output
shows only the entries inside that directory with the
directory prefix stripped.
Examples: ls, ls man
directory prefix stripped. Directory entries are shown
with a trailing slash.
Examples: ls, ls man, ls os
.SS cd [dir]
Change the working directory. With no argument or with /,
returns to the root (0:/). Use cd .. to go up one level.
Trailing slashes on directory names are stripped.
The shell prompt reflects the current directory.
Examples: cd man, cd .., cd
.SS cat <file>
Print the contents of a file to the terminal. The file
path is resolved relative to the current directory.
Example: cat hello.elf
.SS run <file>
Spawn a new process from an ELF binary and wait for it to
exit. The file path is resolved relative to the current
directory. The shell blocks until the child process terminates.
Example: run hello.elf
.SS ping <ip>
Send 4 ICMP echo requests to the given IP address and display
round-trip times. Timeout is 3 seconds per request.
Example: ping 10.0.2.2
.SS date
Display the current date and time in UTC.
.SS uptime
Display the system uptime in minutes, seconds, and milliseconds.
.SS clear
Clear the terminal screen.
Examples: cd os, cd .., cd
.SS exit
Terminate the shell process.
.SH EXTERNAL COMMANDS
.SS System commands (0:/os/)
man <topic> View manual pages
cat <file> Display file contents
info Show system information
date Show current date and time
uptime Show system uptime
clear Clear the screen and framebuffer
reset Reboot the system
shutdown Shut down the system
.SS Network commands (0:/os/)
ping <ip> Send ICMP echo requests
ifconfig Show/set network configuration
tcpconnect <ip> <port> Interactive TCP client
irc IRC client
dhcp DHCP client
fetch HTTP client
httpd HTTP server
.SS Games (0:/games/)
doom DOOM
.SH INPUT
The shell reads input character by character using SYS_GETCHAR.
Backspace is supported. Lines are limited to 255 characters.
There is no command history or tab completion.
The shell uses non-blocking keyboard input via SYS_GETKEY to
support arrow key detection. Lines are limited to 255
characters.
.SS Editing
Backspace Delete character before cursor
Enter Execute command
.SS History
The shell stores the last 32 unique commands. Duplicate
consecutive entries are suppressed.
Up Arrow Recall previous command
Down Arrow Recall next command (or clear line)
.SH PROMPT
The prompt displays the current working directory:
0:/> _ (at root)
0:/os> _ (in os/ directory)
0:/man> _ (in man/ directory)
.SH SEE ALSO
man(1), intro(1), syscalls(2)
+3 -3
View File
@@ -13,11 +13,11 @@
Loads the ELF64 binary at the given VFS path and creates a new
process. The path must include the drive prefix, for example:
int pid = zenith::spawn("0:/hello.elf");
int pid = zenith::spawn("0:/os/hello.elf");
An optional second argument passes a string to the child:
int pid = zenith::spawn("0:/man.elf", "intro");
int pid = zenith::spawn("0:/os/man.elf", "intro");
The new process gets its own PML4 page table, a 16 KiB stack
(at 0x7FFFFEF000-0x7FFFFFF000), and begins executing at the
@@ -40,7 +40,7 @@
.SH EXAMPLES
Spawn a program and wait for it:
int pid = zenith::spawn("0:/hello.elf");
int pid = zenith::spawn("0:/os/hello.elf");
if (pid < 0) {
zenith::print("spawn failed\n");
} else {
+23 -3
View File
@@ -3,7 +3,7 @@
syscalls - overview of ZenithOS system calls
.SH DESCRIPTION
ZenithOS provides 37 system calls (numbers 0-36) for userspace
ZenithOS provides 41 system calls (numbers 0-40) for userspace
programs. Syscalls use the x86-64 SYSCALL instruction with the
following register convention:
@@ -120,8 +120,18 @@
Send an ICMP echo request and wait for reply.
int32_t zenith::ping(uint32_t ip, uint32_t timeoutMs);
.B SYS_GETNETCFG (37)
Get the current network configuration (IP, mask, gateway, MAC).
void zenith::get_netcfg(Zenith::NetCfg* out);
.B SYS_SETNETCFG (38)
Set the network configuration (IP, mask, gateway).
int zenith::set_netcfg(const Zenith::NetCfg* cfg);
.SH SOCKETS
.B SYS_SOCKET (29)
Create a socket. type=SOCK_TCP (1). Returns fd or -1.
Create a socket. type=SOCK_TCP (1) or SOCK_UDP (2).
Returns fd or -1.
int zenith::socket(int type);
.B SYS_CONNECT (30)
@@ -147,13 +157,23 @@
.B SYS_RECV (35)
Receive data from a connected socket. Returns bytes
received, 0 on connection close, or -1 on error.
received, 0 if no data available, or -1 on close/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);
.B SYS_SENDTO (39)
Send a UDP datagram to a specific destination.
int zenith::sendto(int fd, const void* data, uint32_t len,
uint32_t destIp, uint16_t destPort);
.B SYS_RECVFROM (40)
Receive a UDP datagram. Returns the source address.
int zenith::recvfrom(int fd, void* buf, uint32_t maxLen,
uint32_t* srcIp, uint16_t* srcPort);
.SH FRAMEBUFFER
.B SYS_FBINFO (21)
Get framebuffer dimensions and format.
Binary file not shown.
Binary file not shown.
+66
View File
@@ -0,0 +1,66 @@
/*
* main.cpp
* cat - Display file contents
* Copyright (c) 2025-2026 Daniel Hammer
*/
#include <zenith/syscall.h>
extern "C" void _start() {
char args[256];
int len = zenith::getargs(args, sizeof(args));
if (len <= 0 || args[0] == '\0') {
zenith::print("Usage: cat <filename>\n");
zenith::exit(1);
}
// Build VFS path. If the path already starts with "<digit>:", use as-is.
// Otherwise, prefix "0:/" to it.
char path[256];
bool hasPrefix = false;
if (args[0] >= '0' && args[0] <= '9' && args[1] == ':') {
hasPrefix = true;
}
int i = 0;
if (!hasPrefix) {
path[0] = '0'; path[1] = ':'; path[2] = '/';
i = 3;
}
int j = 0;
while (args[j] && i < 255) {
path[i++] = args[j++];
}
path[i] = '\0';
int handle = zenith::open(path);
if (handle < 0) {
zenith::print("cat: cannot open '");
zenith::print(args);
zenith::print("'\n");
zenith::exit(1);
}
uint64_t size = zenith::getsize(handle);
if (size == 0) {
zenith::close(handle);
zenith::exit(0);
}
uint8_t buf[512];
uint64_t offset = 0;
while (offset < size) {
uint64_t chunk = size - offset;
if (chunk > sizeof(buf) - 1) chunk = sizeof(buf) - 1;
int bytesRead = zenith::read(handle, buf, offset, chunk);
if (bytesRead <= 0) break;
buf[bytesRead] = '\0';
zenith::print((const char*)buf);
offset += bytesRead;
}
zenith::close(handle);
zenith::putchar('\n');
zenith::exit(0);
}
+27
View File
@@ -0,0 +1,27 @@
/*
* main.cpp
* clear - Clear terminal screen and framebuffer
* Copyright (c) 2025-2026 Daniel Hammer
*/
#include <zenith/syscall.h>
extern "C" void _start() {
// Clear the raw framebuffer (needed after graphical programs like DOOM)
Zenith::FbInfo fb;
zenith::fb_info(&fb);
uint8_t* pixels = (uint8_t*)zenith::fb_map();
if (pixels) {
for (uint64_t y = 0; y < fb.height; y++) {
uint32_t* row = (uint32_t*)(pixels + y * fb.pitch);
for (uint64_t x = 0; x < fb.width; x++) {
row[x] = 0x00000000;
}
}
}
// Reset the text console
zenith::print("\033[2J"); // Clear entire screen
zenith::print("\033[H"); // Move cursor to top-left
zenith::exit(0);
}
+56
View File
@@ -0,0 +1,56 @@
/*
* main.cpp
* date - Show current date and time
* Copyright (c) 2025-2026 Daniel Hammer
*/
#include <zenith/syscall.h>
static void print_int(uint64_t n) {
if (n == 0) {
zenith::putchar('0');
return;
}
char buf[20];
int i = 0;
while (n > 0) {
buf[i++] = '0' + (n % 10);
n /= 10;
}
for (int j = i - 1; j >= 0; j--) {
zenith::putchar(buf[j]);
}
}
static void print_int_padded(uint64_t n) {
if (n < 10) zenith::putchar('0');
print_int(n);
}
static const char* month_name(int m) {
static const char* months[] = {
"", "January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
};
if (m >= 1 && m <= 12) return months[m];
return "?";
}
extern "C" void _start() {
Zenith::DateTime dt;
zenith::gettime(&dt);
print_int(dt.Day);
zenith::putchar(' ');
zenith::print(month_name(dt.Month));
zenith::putchar(' ');
print_int(dt.Year);
zenith::print(", ");
print_int(dt.Hour);
zenith::putchar(':');
print_int_padded(dt.Minute);
zenith::putchar(':');
print_int_padded(dt.Second);
zenith::print(" UTC\n");
zenith::exit(0);
}
+495
View File
@@ -0,0 +1,495 @@
/*
* main.cpp
* DHCP client for ZenithOS
* Obtains network configuration automatically via DHCP (RFC 2131)
* Copyright (c) 2025-2026 Daniel Hammer
*/
#include <zenith/syscall.h>
#include <zenith/string.h>
using zenith::memcpy;
using zenith::memset;
// ---- 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;
}
// ---- DHCP constants ----
static constexpr uint8_t BOOTREQUEST = 1;
static constexpr uint8_t BOOTREPLY = 2;
static constexpr uint8_t HTYPE_ETH = 1;
static constexpr uint8_t HLEN_ETH = 6;
static constexpr uint16_t DHCP_SERVER_PORT = 67;
static constexpr uint16_t DHCP_CLIENT_PORT = 68;
static constexpr uint32_t DHCP_MAGIC = 0x63825363; // network byte order
static constexpr uint16_t BROADCAST_FLAG = 0x8000;
// DHCP message types
static constexpr uint8_t DHCPDISCOVER = 1;
static constexpr uint8_t DHCPOFFER = 2;
static constexpr uint8_t DHCPREQUEST = 3;
static constexpr uint8_t DHCPACK = 5;
static constexpr uint8_t DHCPNAK = 6;
// DHCP option codes
static constexpr uint8_t OPT_SUBNET = 1;
static constexpr uint8_t OPT_ROUTER = 3;
static constexpr uint8_t OPT_DNS = 6;
static constexpr uint8_t OPT_REQUESTED_IP = 50;
static constexpr uint8_t OPT_LEASE_TIME = 51;
static constexpr uint8_t OPT_MSG_TYPE = 53;
static constexpr uint8_t OPT_SERVER_ID = 54;
static constexpr uint8_t OPT_PARAM_LIST = 55;
static constexpr uint8_t OPT_END = 255;
// ---- DHCP packet structure ----
struct DhcpPacket {
uint8_t op;
uint8_t htype;
uint8_t hlen;
uint8_t hops;
uint32_t xid;
uint16_t secs;
uint16_t flags;
uint32_t ciaddr;
uint32_t yiaddr;
uint32_t siaddr;
uint32_t giaddr;
uint8_t chaddr[16];
uint8_t sname[64];
uint8_t file[128];
uint8_t options[312];
} __attribute__((packed));
// ---- Byte order helpers (DHCP uses network byte order) ----
static uint16_t htons(uint16_t v) {
return (uint16_t)((v >> 8) | (v << 8));
}
static uint32_t htonl(uint32_t v) {
return ((v >> 24) & 0xFF) | ((v >> 8) & 0xFF00) |
((v << 8) & 0xFF0000) | ((v << 24) & 0xFF000000u);
}
static uint32_t ntohl(uint32_t v) { return htonl(v); }
// ---- IP formatting ----
static void format_ip(char* buf, int size, uint32_t ip) {
const uint8_t* b = (const uint8_t*)&ip;
snprintf(buf, size, "%d.%d.%d.%d", b[0], b[1], b[2], b[3]);
}
static void format_mac(char* buf, int size, const uint8_t* mac) {
snprintf(buf, size, "%02x:%02x:%02x:%02x:%02x:%02x",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
}
// ---- Build DHCP packets ----
static uint32_t g_xid = 0x5A454E49; // "ZENI"
static void build_base(DhcpPacket* pkt, const uint8_t* mac) {
memset(pkt, 0, sizeof(DhcpPacket));
pkt->op = BOOTREQUEST;
pkt->htype = HTYPE_ETH;
pkt->hlen = HLEN_ETH;
pkt->xid = g_xid;
pkt->flags = htons(BROADCAST_FLAG);
memcpy(pkt->chaddr, mac, 6);
// Magic cookie
pkt->options[0] = 0x63;
pkt->options[1] = 0x82;
pkt->options[2] = 0x53;
pkt->options[3] = 0x63;
}
static int build_discover(DhcpPacket* pkt, const uint8_t* mac) {
build_base(pkt, mac);
int off = 4; // after magic cookie
// Option 53: DHCP Message Type = DISCOVER
pkt->options[off++] = OPT_MSG_TYPE;
pkt->options[off++] = 1;
pkt->options[off++] = DHCPDISCOVER;
// Option 55: Parameter Request List
pkt->options[off++] = OPT_PARAM_LIST;
pkt->options[off++] = 4;
pkt->options[off++] = OPT_SUBNET;
pkt->options[off++] = OPT_ROUTER;
pkt->options[off++] = OPT_DNS;
pkt->options[off++] = OPT_LEASE_TIME;
pkt->options[off++] = OPT_END;
return (int)sizeof(DhcpPacket) - 312 + off;
}
static int build_request(DhcpPacket* pkt, const uint8_t* mac,
uint32_t requestedIp, uint32_t serverId) {
build_base(pkt, mac);
int off = 4;
// Option 53: DHCP Message Type = REQUEST
pkt->options[off++] = OPT_MSG_TYPE;
pkt->options[off++] = 1;
pkt->options[off++] = DHCPREQUEST;
// Option 50: Requested IP Address
pkt->options[off++] = OPT_REQUESTED_IP;
pkt->options[off++] = 4;
memcpy(&pkt->options[off], &requestedIp, 4);
off += 4;
// Option 54: Server Identifier
pkt->options[off++] = OPT_SERVER_ID;
pkt->options[off++] = 4;
memcpy(&pkt->options[off], &serverId, 4);
off += 4;
// Option 55: Parameter Request List
pkt->options[off++] = OPT_PARAM_LIST;
pkt->options[off++] = 4;
pkt->options[off++] = OPT_SUBNET;
pkt->options[off++] = OPT_ROUTER;
pkt->options[off++] = OPT_DNS;
pkt->options[off++] = OPT_LEASE_TIME;
pkt->options[off++] = OPT_END;
return (int)sizeof(DhcpPacket) - 312 + off;
}
// ---- Parse DHCP options ----
struct DhcpOffer {
uint32_t offeredIp;
uint32_t serverId;
uint32_t subnetMask;
uint32_t router;
uint32_t dns;
uint32_t leaseTime;
uint8_t msgType;
bool valid;
};
static void parse_options(const DhcpPacket* pkt, DhcpOffer* offer) {
offer->offeredIp = pkt->yiaddr;
offer->serverId = 0;
offer->subnetMask = 0;
offer->router = 0;
offer->dns = 0;
offer->leaseTime = 0;
offer->msgType = 0;
offer->valid = false;
// Verify magic cookie
if (pkt->options[0] != 0x63 || pkt->options[1] != 0x82 ||
pkt->options[2] != 0x53 || pkt->options[3] != 0x63) {
return;
}
int off = 4;
while (off < 312) {
uint8_t code = pkt->options[off++];
if (code == OPT_END) break;
if (code == 0) continue; // pad
if (off >= 312) break;
uint8_t len = pkt->options[off++];
if (off + len > 312) break;
switch (code) {
case OPT_MSG_TYPE:
if (len >= 1) offer->msgType = pkt->options[off];
break;
case OPT_SUBNET:
if (len >= 4) memcpy(&offer->subnetMask, &pkt->options[off], 4);
break;
case OPT_ROUTER:
if (len >= 4) memcpy(&offer->router, &pkt->options[off], 4);
break;
case OPT_DNS:
if (len >= 4) memcpy(&offer->dns, &pkt->options[off], 4);
break;
case OPT_SERVER_ID:
if (len >= 4) memcpy(&offer->serverId, &pkt->options[off], 4);
break;
case OPT_LEASE_TIME:
if (len >= 4) {
uint32_t raw;
memcpy(&raw, &pkt->options[off], 4);
offer->leaseTime = ntohl(raw);
}
break;
}
off += len;
}
offer->valid = (offer->msgType != 0);
}
// ---- Broadcast destination ----
static constexpr uint32_t BROADCAST_IP = 0xFFFFFFFF;
// ---- Main ----
extern "C" void _start() {
char msg[256];
zenith::print("ZenithOS DHCP Client\n");
// 1. Get MAC address
Zenith::NetCfg origCfg;
zenith::get_netcfg(&origCfg);
char macStr[32];
format_mac(macStr, sizeof(macStr), origCfg.macAddress);
snprintf(msg, sizeof(msg), "MAC address: %s\n", macStr);
zenith::print(msg);
// 2. Set IP to 0.0.0.0 to allow broadcast send/receive
Zenith::NetCfg zeroCfg;
zeroCfg.ipAddress = 0;
zeroCfg.subnetMask = 0;
zeroCfg.gateway = 0;
zenith::set_netcfg(&zeroCfg);
// 3. Create UDP socket and bind to port 68
int fd = zenith::socket(Zenith::SOCK_UDP);
if (fd < 0) {
zenith::print("Error: failed to create UDP socket\n");
zenith::set_netcfg(&origCfg);
zenith::exit(1);
}
if (zenith::bind(fd, DHCP_CLIENT_PORT) < 0) {
zenith::print("Error: failed to bind to port 68\n");
zenith::closesocket(fd);
zenith::set_netcfg(&origCfg);
zenith::exit(1);
}
// 4. Send DISCOVER
DhcpPacket pkt;
int pktLen = build_discover(&pkt, origCfg.macAddress);
zenith::print("Sending DHCPDISCOVER...\n");
if (zenith::sendto(fd, (const void*)&pkt, pktLen, BROADCAST_IP, DHCP_SERVER_PORT) < 0) {
zenith::print("Error: failed to send DISCOVER\n");
zenith::closesocket(fd);
zenith::set_netcfg(&origCfg);
zenith::exit(1);
}
// 5. Wait for OFFER
DhcpPacket resp;
DhcpOffer offer;
uint64_t startMs = zenith::get_milliseconds();
bool gotOffer = false;
zenith::print("Waiting for DHCPOFFER...\n");
while (zenith::get_milliseconds() - startMs < 10000) {
uint32_t srcIp;
uint16_t srcPort;
int r = zenith::recvfrom(fd, (void*)&resp, sizeof(resp), &srcIp, &srcPort);
if (r > 0) {
if (resp.op == BOOTREPLY && resp.xid == g_xid) {
parse_options(&resp, &offer);
if (offer.valid && offer.msgType == DHCPOFFER) {
gotOffer = true;
break;
}
}
}
zenith::yield();
}
if (!gotOffer) {
zenith::print("Error: no DHCPOFFER received (timeout)\n");
zenith::closesocket(fd);
zenith::set_netcfg(&origCfg);
zenith::exit(1);
}
char ipStr[32];
format_ip(ipStr, sizeof(ipStr), offer.offeredIp);
snprintf(msg, sizeof(msg), "Received OFFER: %s\n", ipStr);
zenith::print(msg);
// 6. Send REQUEST
pktLen = build_request(&pkt, origCfg.macAddress, offer.offeredIp, offer.serverId);
zenith::print("Sending DHCPREQUEST...\n");
if (zenith::sendto(fd, (const void*)&pkt, pktLen, BROADCAST_IP, DHCP_SERVER_PORT) < 0) {
zenith::print("Error: failed to send REQUEST\n");
zenith::closesocket(fd);
zenith::set_netcfg(&origCfg);
zenith::exit(1);
}
// 7. Wait for ACK
bool gotAck = false;
startMs = zenith::get_milliseconds();
zenith::print("Waiting for DHCPACK...\n");
while (zenith::get_milliseconds() - startMs < 10000) {
uint32_t srcIp;
uint16_t srcPort;
int r = zenith::recvfrom(fd, (void*)&resp, sizeof(resp), &srcIp, &srcPort);
if (r > 0) {
if (resp.op == BOOTREPLY && resp.xid == g_xid) {
parse_options(&resp, &offer);
if (offer.valid && offer.msgType == DHCPACK) {
gotAck = true;
break;
}
if (offer.valid && offer.msgType == DHCPNAK) {
zenith::print("Error: received DHCPNAK from server\n");
zenith::closesocket(fd);
zenith::set_netcfg(&origCfg);
zenith::exit(1);
}
}
}
zenith::yield();
}
zenith::closesocket(fd);
if (!gotAck) {
zenith::print("Error: no DHCPACK received (timeout)\n");
zenith::set_netcfg(&origCfg);
zenith::exit(1);
}
// 8. Apply configuration
Zenith::NetCfg newCfg;
newCfg.ipAddress = offer.offeredIp;
newCfg.subnetMask = offer.subnetMask;
newCfg.gateway = offer.router;
zenith::set_netcfg(&newCfg);
// 9. Print results
zenith::print("\nDHCP configuration applied:\n");
format_ip(ipStr, sizeof(ipStr), offer.offeredIp);
snprintf(msg, sizeof(msg), " IP Address: %s\n", ipStr);
zenith::print(msg);
format_ip(ipStr, sizeof(ipStr), offer.subnetMask);
snprintf(msg, sizeof(msg), " Subnet Mask: %s\n", ipStr);
zenith::print(msg);
format_ip(ipStr, sizeof(ipStr), offer.router);
snprintf(msg, sizeof(msg), " Gateway: %s\n", ipStr);
zenith::print(msg);
if (offer.dns != 0) {
format_ip(ipStr, sizeof(ipStr), offer.dns);
snprintf(msg, sizeof(msg), " DNS Server: %s\n", ipStr);
zenith::print(msg);
}
if (offer.leaseTime != 0) {
snprintf(msg, sizeof(msg), " Lease Time: %u seconds\n", offer.leaseTime);
zenith::print(msg);
}
zenith::exit(0);
}
+2 -2
View File
@@ -163,14 +163,14 @@ ALL_OBJS := $(DOOM_OBJS) $(LOCAL_OBJS)
# ---- Target ----
TARGET := $(BINDIR)/doom.elf
TARGET := $(BINDIR)/games/doom.elf
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(ALL_OBJS) $(LINK_LD) Makefile
mkdir -p $(BINDIR)
mkdir -p $(BINDIR)/games
$(LD) $(CFLAGS) $(LDFLAGS) $(ALL_OBJS) -o $@
# DOOM source files (from doomgeneric directory)
+1 -1
View File
@@ -222,7 +222,7 @@ void DG_SetWindowTitle(const char* title) {
/* ---- Entry point ---- */
void _start(void) {
char *argv[] = { "doom", "-iwad", "0:/doom1.wad", 0 };
char *argv[] = { "doom", "-iwad", "0:/games/doom1.wad", 0 };
doomgeneric_Create(3, argv);
for (;;) {
doomgeneric_Tick();
+832
View File
@@ -0,0 +1,832 @@
/*
* main.cpp
* edit - Text editor for ZenithOS
* Copyright (c) 2025-2026 Daniel Hammer
*/
#include <zenith/syscall.h>
#include <zenith/heap.h>
#include <zenith/string.h>
using zenith::slen;
using zenith::memcpy;
using zenith::memmove;
// ---- Integer to string ----
static int itoa(int val, char* buf) {
if (val == 0) { buf[0] = '0'; buf[1] = '\0'; return 1; }
char tmp[12];
int i = 0;
bool neg = false;
if (val < 0) { neg = true; val = -val; }
while (val > 0) { tmp[i++] = '0' + val % 10; val /= 10; }
int len = 0;
if (neg) buf[len++] = '-';
for (int j = i - 1; j >= 0; j--) buf[len++] = tmp[j];
buf[len] = '\0';
return len;
}
// ---- Terminal output helpers ----
static void print(const char* s) { zenith::print(s); }
static void putch(char c) { zenith::putchar(c); }
static void print_int(int v) {
char buf[12];
itoa(v, buf);
print(buf);
}
// ANSI escape helpers
static void esc(const char* seq) {
putch('\033');
putch('[');
print(seq);
}
static void cursor_to(int row, int col) {
putch('\033'); putch('[');
print_int(row); putch(';');
print_int(col); putch('H');
}
static void clear_line() { esc("2K"); }
static void hide_cursor() { esc("?25l"); }
static void show_cursor() { esc("?25h"); }
static void enter_alt_screen() { esc("?1049h"); }
static void exit_alt_screen() { esc("?1049l"); }
static void reset_attrs() { esc("0m"); }
static void reverse_video() { esc("7m"); }
static void dim_text() { esc("2m"); }
// ---- Line buffer ----
struct Line {
char* data;
int len;
int cap;
};
static constexpr int MAX_LINES = 10000;
static constexpr int INITIAL_LINE_CAP = 64;
static constexpr int TAB_WIDTH = 4;
static Line* lines = nullptr;
static int numLines = 0;
// Editor state
static int cursorRow = 0; // cursor position in document (0-indexed)
static int cursorCol = 0;
static int topLine = 0; // first visible line
static int leftCol = 0; // horizontal scroll offset
static int screenRows = 24;
static int screenCols = 80;
static int editorRows = 0; // screenRows - 2 (status + hint bars)
static int gutterWidth = 4; // line number gutter width
static bool modified = false;
static bool running = true;
static bool fullRedraw = true;
static char filename[256] = "";
static bool hasFilename = false;
// Search state
static char searchQuery[128] = "";
static int searchLen = 0;
// Status message
static char statusMsg[128] = "";
static uint64_t statusMsgTime = 0;
// ---- Line operations ----
static void line_init(Line* ln) {
ln->cap = INITIAL_LINE_CAP;
ln->data = (char*)zenith::malloc(ln->cap);
ln->len = 0;
ln->data[0] = '\0';
}
static void line_ensure(Line* ln, int needed) {
if (needed + 1 <= ln->cap) return;
int newCap = ln->cap;
while (newCap < needed + 1) newCap *= 2;
ln->data = (char*)zenith::realloc(ln->data, newCap);
ln->cap = newCap;
}
static void line_insert_char(Line* ln, int pos, char c) {
if (pos < 0) pos = 0;
if (pos > ln->len) pos = ln->len;
line_ensure(ln, ln->len + 1);
memmove(ln->data + pos + 1, ln->data + pos, ln->len - pos);
ln->data[pos] = c;
ln->len++;
ln->data[ln->len] = '\0';
}
static void line_delete_char(Line* ln, int pos) {
if (pos < 0 || pos >= ln->len) return;
memmove(ln->data + pos, ln->data + pos + 1, ln->len - pos - 1);
ln->len--;
ln->data[ln->len] = '\0';
}
static void line_append(Line* ln, const char* s, int slen) {
line_ensure(ln, ln->len + slen);
memcpy(ln->data + ln->len, s, slen);
ln->len += slen;
ln->data[ln->len] = '\0';
}
// ---- Document operations ----
static void insert_line(int at) {
if (numLines >= MAX_LINES) return;
if (at < 0) at = 0;
if (at > numLines) at = numLines;
// Shift lines down
for (int i = numLines; i > at; i--) {
lines[i] = lines[i - 1];
}
line_init(&lines[at]);
numLines++;
}
static void delete_line(int at) {
if (at < 0 || at >= numLines) return;
if (numLines <= 1) {
// Don't delete the last line, just clear it
lines[at].len = 0;
lines[at].data[0] = '\0';
return;
}
zenith::mfree(lines[at].data);
for (int i = at; i < numLines - 1; i++) {
lines[i] = lines[i + 1];
}
numLines--;
}
// ---- Compute gutter width from line count ----
static void update_gutter_width() {
int digits = 1;
int n = numLines;
while (n >= 10) { digits++; n /= 10; }
gutterWidth = digits + 2; // digits + space + separator
if (gutterWidth < 4) gutterWidth = 4;
}
// ---- File I/O ----
static void set_status(const char* msg) {
int i = 0;
while (msg[i] && i < 126) { statusMsg[i] = msg[i]; i++; }
statusMsg[i] = '\0';
statusMsgTime = zenith::get_milliseconds();
}
// Build VFS path from filename
static void build_path(const char* fname, char* out, int outMax) {
int i = 0;
// Check if already has drive prefix
bool hasPrefix = (fname[0] >= '0' && fname[0] <= '9' && fname[1] == ':');
if (!hasPrefix) {
out[i++] = '0'; out[i++] = ':'; out[i++] = '/';
}
int j = 0;
while (fname[j] && i < outMax - 1) {
out[i++] = fname[j++];
}
out[i] = '\0';
}
static void load_file(const char* fname) {
char path[256];
build_path(fname, path, sizeof(path));
int handle = zenith::open(path);
if (handle < 0) {
// New file
numLines = 1;
line_init(&lines[0]);
set_status("(New file)");
return;
}
uint64_t size = zenith::getsize(handle);
// Read entire file into a temp buffer
uint8_t* buf = nullptr;
if (size > 0) {
buf = (uint8_t*)zenith::malloc(size + 1);
uint64_t off = 0;
while (off < size) {
int r = zenith::read(handle, buf + off, off, size - off);
if (r <= 0) break;
off += r;
}
buf[size] = '\0';
}
zenith::close(handle);
// Parse into lines
numLines = 0;
if (buf && size > 0) {
uint64_t lineStart = 0;
for (uint64_t i = 0; i <= size; i++) {
if (i == size || buf[i] == '\n') {
if (numLines >= MAX_LINES) break;
line_init(&lines[numLines]);
int lineLen = (int)(i - lineStart);
if (lineLen > 0) {
line_append(&lines[numLines], (const char*)buf + lineStart, lineLen);
}
numLines++;
lineStart = i + 1;
}
}
}
if (buf) zenith::mfree(buf);
if (numLines == 0) {
numLines = 1;
line_init(&lines[0]);
}
set_status("File loaded");
}
static bool save_file() {
if (!hasFilename) {
set_status("No filename! Use Ctrl+S after setting a name");
return false;
}
char path[256];
build_path(filename, path, sizeof(path));
// Calculate total size
uint64_t totalSize = 0;
for (int i = 0; i < numLines; i++) {
totalSize += lines[i].len;
if (i < numLines - 1) totalSize++; // newline
}
// Try to open existing file first
int handle = zenith::open(path);
if (handle < 0) {
// Create new file
handle = zenith::fcreate(path);
if (handle < 0) {
set_status("Error: could not create file");
return false;
}
}
// Build content buffer and write
uint8_t* buf = (uint8_t*)zenith::malloc(totalSize + 1);
uint64_t off = 0;
for (int i = 0; i < numLines; i++) {
if (lines[i].len > 0) {
memcpy(buf + off, lines[i].data, lines[i].len);
off += lines[i].len;
}
if (i < numLines - 1) {
buf[off++] = '\n';
}
}
int result = zenith::fwrite(handle, buf, 0, totalSize);
zenith::close(handle);
zenith::mfree(buf);
if (result < 0) {
set_status("Error: write failed");
return false;
}
modified = false;
set_status("File saved");
return true;
}
// ---- Prompt for input in the hint bar area ----
static int prompt_input(const char* promptStr, char* out, int outMax) {
// Draw prompt on the last row
cursor_to(screenRows, 1);
reverse_video();
clear_line();
print(promptStr);
reset_attrs();
show_cursor();
int pos = 0;
out[0] = '\0';
while (true) {
if (!zenith::is_key_available()) {
zenith::yield();
continue;
}
Zenith::KeyEvent ev;
zenith::getkey(&ev);
if (!ev.pressed) continue;
if (ev.ascii == '\033' || (ev.ctrl && ev.ascii == 'q')) {
// Cancel
return -1;
}
if (ev.ascii == '\n') {
out[pos] = '\0';
return pos;
}
if (ev.ascii == '\b') {
if (pos > 0) {
pos--;
putch('\b'); putch(' '); putch('\b');
}
} else if (ev.ascii >= ' ' && pos < outMax - 1) {
out[pos++] = ev.ascii;
out[pos] = '\0';
putch(ev.ascii);
}
}
}
// ---- Rendering ----
static void draw_status_bar() {
cursor_to(1, 1);
reverse_video();
clear_line();
// Left side: "edit: filename [+]"
print(" edit: ");
if (hasFilename) {
print(filename);
} else {
print("[No Name]");
}
if (modified) print(" [+]");
// Right side: cursor position
// Calculate right-side text
char posBuf[32];
int p = 0;
posBuf[p++] = 'L';
posBuf[p++] = 'n';
posBuf[p++] = ' ';
p += itoa(cursorRow + 1, posBuf + p);
posBuf[p++] = ',';
posBuf[p++] = ' ';
posBuf[p++] = 'C';
posBuf[p++] = 'o';
posBuf[p++] = 'l';
posBuf[p++] = ' ';
p += itoa(cursorCol + 1, posBuf + p);
posBuf[p++] = ' ';
posBuf[p++] = ' ';
posBuf[p] = '\0';
// Pad to right-align
// We need to figure out how many chars we've printed on the left
int leftLen = 8 + (hasFilename ? slen(filename) : 9) + (modified ? 4 : 0);
int rightLen = p;
int padding = screenCols - leftLen - rightLen;
for (int i = 0; i < padding; i++) putch(' ');
print(posBuf);
reset_attrs();
}
static void draw_hint_bar() {
cursor_to(screenRows, 1);
reverse_video();
clear_line();
// Show status message if recent (within 3 seconds)
uint64_t now = zenith::get_milliseconds();
if (statusMsg[0] && (now - statusMsgTime) < 3000) {
print(" ");
print(statusMsg);
} else {
print(" ^S Save ^Q Quit ^F Find ^G Find Next");
}
// Pad rest of line
reset_attrs();
}
static void draw_line(int screenRow, int docLine) {
cursor_to(screenRow + 2, 1); // +2 because row 1 is status bar
clear_line();
if (docLine < numLines) {
// Line number gutter
dim_text();
char numBuf[12];
int numLen = itoa(docLine + 1, numBuf);
// Right-align the number in the gutter
int pad = gutterWidth - 2 - numLen; // -2 for trailing space+pipe
for (int i = 0; i < pad; i++) putch(' ');
print(numBuf);
putch(' ');
reset_attrs();
// Line content
Line* ln = &lines[docLine];
int startCol = leftCol;
int maxChars = screenCols - gutterWidth;
for (int c = 0; c < maxChars && startCol + c < ln->len; c++) {
putch(ln->data[startCol + c]);
}
} else {
// Past end of file
dim_text();
putch('~');
reset_attrs();
}
}
static void render() {
hide_cursor();
update_gutter_width();
draw_status_bar();
if (fullRedraw) {
for (int i = 0; i < editorRows; i++) {
draw_line(i, topLine + i);
}
fullRedraw = false;
}
draw_hint_bar();
// Position cursor
int screenY = cursorRow - topLine + 2; // +2 for status bar
int screenX = cursorCol - leftCol + gutterWidth;
cursor_to(screenY, screenX);
show_cursor();
}
// ---- Scrolling ----
static void scroll() {
// Vertical scrolling
if (cursorRow < topLine) {
topLine = cursorRow;
fullRedraw = true;
}
if (cursorRow >= topLine + editorRows) {
topLine = cursorRow - editorRows + 1;
fullRedraw = true;
}
// Horizontal scrolling
int textCols = screenCols - gutterWidth;
if (cursorCol < leftCol) {
leftCol = cursorCol;
fullRedraw = true;
}
if (cursorCol >= leftCol + textCols) {
leftCol = cursorCol - textCols + 1;
fullRedraw = true;
}
}
// ---- Editing operations ----
static void insert_char(char c) {
line_insert_char(&lines[cursorRow], cursorCol, c);
cursorCol++;
modified = true;
fullRedraw = true;
}
static void insert_tab() {
for (int i = 0; i < TAB_WIDTH; i++) {
insert_char(' ');
}
}
static void insert_newline() {
Line* current = &lines[cursorRow];
// Split current line at cursor position
insert_line(cursorRow + 1);
// Re-get pointer since insert_line shifted memory
current = &lines[cursorRow];
Line* newLine = &lines[cursorRow + 1];
// Move text after cursor to new line
if (cursorCol < current->len) {
line_append(newLine, current->data + cursorCol, current->len - cursorCol);
current->len = cursorCol;
current->data[current->len] = '\0';
}
cursorRow++;
cursorCol = 0;
modified = true;
fullRedraw = true;
}
static void delete_char_backspace() {
if (cursorCol > 0) {
line_delete_char(&lines[cursorRow], cursorCol - 1);
cursorCol--;
modified = true;
fullRedraw = true;
} else if (cursorRow > 0) {
// Join with previous line
int prevLen = lines[cursorRow - 1].len;
line_append(&lines[cursorRow - 1], lines[cursorRow].data, lines[cursorRow].len);
delete_line(cursorRow);
cursorRow--;
cursorCol = prevLen;
modified = true;
fullRedraw = true;
}
}
static void delete_char_forward() {
if (cursorCol < lines[cursorRow].len) {
line_delete_char(&lines[cursorRow], cursorCol);
modified = true;
fullRedraw = true;
} else if (cursorRow < numLines - 1) {
// Join with next line
line_append(&lines[cursorRow], lines[cursorRow + 1].data, lines[cursorRow + 1].len);
delete_line(cursorRow + 1);
modified = true;
fullRedraw = true;
}
}
// ---- Search ----
static void find_next(bool fromPrompt) {
if (searchLen == 0) return;
// Start search from cursor position + 1
int startRow = cursorRow;
int startCol = cursorCol + (fromPrompt ? 0 : 1);
for (int i = 0; i < numLines; i++) {
int row = (startRow + i) % numLines;
int colStart = (i == 0) ? startCol : 0;
Line* ln = &lines[row];
for (int c = colStart; c <= ln->len - searchLen; c++) {
bool match = true;
for (int k = 0; k < searchLen; k++) {
if (ln->data[c + k] != searchQuery[k]) {
match = false;
break;
}
}
if (match) {
cursorRow = row;
cursorCol = c;
fullRedraw = true;
set_status("Found");
return;
}
}
}
set_status("Not found");
}
static void do_search() {
char query[128];
int len = prompt_input("Search: ", query, sizeof(query));
if (len < 0) {
fullRedraw = true;
return;
}
if (len == 0) {
fullRedraw = true;
return;
}
// Copy query
for (int i = 0; i < len; i++) searchQuery[i] = query[i];
searchQuery[len] = '\0';
searchLen = len;
find_next(true);
fullRedraw = true;
}
// ---- Navigation scancodes ----
static constexpr uint8_t SC_UP = 0x48;
static constexpr uint8_t SC_DOWN = 0x50;
static constexpr uint8_t SC_LEFT = 0x4B;
static constexpr uint8_t SC_RIGHT = 0x4D;
static constexpr uint8_t SC_HOME = 0x47;
static constexpr uint8_t SC_END = 0x4F;
static constexpr uint8_t SC_PGUP = 0x49;
static constexpr uint8_t SC_PGDN = 0x51;
static constexpr uint8_t SC_DELETE = 0x53;
// ---- Input handling ----
static void handle_key(const Zenith::KeyEvent& ev) {
if (!ev.pressed) return;
// Ctrl key combinations
if (ev.ctrl) {
switch (ev.ascii) {
case 'q':
if (modified) {
set_status("Unsaved changes! Press Ctrl+Q again to quit");
// Set a flag so next Ctrl+Q quits
static bool warnedOnce = false;
if (warnedOnce) {
running = false;
}
warnedOnce = true;
return;
}
running = false;
return;
case 's':
if (!hasFilename) {
char nameBuf[256];
int len = prompt_input("Save as: ", nameBuf, sizeof(nameBuf));
if (len > 0) {
for (int i = 0; i <= len; i++) filename[i] = nameBuf[i];
hasFilename = true;
}
fullRedraw = true;
if (!hasFilename) return;
}
save_file();
fullRedraw = true;
return;
case 'f':
do_search();
return;
case 'g':
find_next(false);
return;
default:
break;
}
}
// Non-ASCII keys (scancode-based)
if (ev.ascii == 0) {
switch (ev.scancode) {
case SC_UP:
if (cursorRow > 0) {
cursorRow--;
if (cursorCol > lines[cursorRow].len)
cursorCol = lines[cursorRow].len;
fullRedraw = true;
}
return;
case SC_DOWN:
if (cursorRow < numLines - 1) {
cursorRow++;
if (cursorCol > lines[cursorRow].len)
cursorCol = lines[cursorRow].len;
fullRedraw = true;
}
return;
case SC_LEFT:
if (cursorCol > 0) {
cursorCol--;
} else if (cursorRow > 0) {
cursorRow--;
cursorCol = lines[cursorRow].len;
}
fullRedraw = true;
return;
case SC_RIGHT:
if (cursorCol < lines[cursorRow].len) {
cursorCol++;
} else if (cursorRow < numLines - 1) {
cursorRow++;
cursorCol = 0;
}
fullRedraw = true;
return;
case SC_HOME:
cursorCol = 0;
fullRedraw = true;
return;
case SC_END:
cursorCol = lines[cursorRow].len;
fullRedraw = true;
return;
case SC_PGUP:
cursorRow -= editorRows;
if (cursorRow < 0) cursorRow = 0;
if (cursorCol > lines[cursorRow].len)
cursorCol = lines[cursorRow].len;
fullRedraw = true;
return;
case SC_PGDN:
cursorRow += editorRows;
if (cursorRow >= numLines) cursorRow = numLines - 1;
if (cursorCol > lines[cursorRow].len)
cursorCol = lines[cursorRow].len;
fullRedraw = true;
return;
case SC_DELETE:
delete_char_forward();
return;
default:
return;
}
}
// Regular keys
switch (ev.ascii) {
case '\n':
insert_newline();
break;
case '\b':
delete_char_backspace();
break;
case '\t':
insert_tab();
break;
default:
if (ev.ascii >= ' ') {
insert_char(ev.ascii);
}
break;
}
}
// ---- Entry point ----
extern "C" void _start() {
// Allocate line buffer
lines = (Line*)zenith::malloc(sizeof(Line) * MAX_LINES);
// Get terminal size
zenith::termsize(&screenCols, &screenRows);
editorRows = screenRows - 2;
// Parse arguments
char args[256];
int argLen = zenith::getargs(args, sizeof(args));
if (argLen > 0 && args[0] != '\0') {
// Copy filename
int i = 0;
while (args[i] && i < 255) { filename[i] = args[i]; i++; }
filename[i] = '\0';
hasFilename = true;
load_file(filename);
} else {
// New empty buffer
numLines = 1;
line_init(&lines[0]);
}
// Enter alternate screen
enter_alt_screen();
fullRedraw = true;
// Main loop
while (running) {
scroll();
render();
// Wait for input
while (!zenith::is_key_available()) {
zenith::yield();
}
Zenith::KeyEvent ev;
zenith::getkey(&ev);
handle_key(ev);
}
// Exit alternate screen
exit_alt_screen();
show_cursor();
reset_attrs();
zenith::exit(0);
}
+359
View File
@@ -0,0 +1,359 @@
/*
* main.cpp
* HTTP/1.0 client for ZenithOS
* Usage: run fetch.elf <server_ip> <port> <path>
* Copyright (c) 2025-2026 Daniel Hammer
*/
#include <zenith/syscall.h>
#include <zenith/string.h>
using zenith::skip_spaces;
using zenith::memcpy;
// ---- 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;
}
// ---- IP/port parsing ----
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;
}
// ---- HTTP response parser ----
// Find "\r\n\r\n" boundary between headers and body
static int find_header_end(const char* buf, int len) {
for (int i = 0; i + 3 < len; i++) {
if (buf[i] == '\r' && buf[i+1] == '\n' && buf[i+2] == '\r' && buf[i+3] == '\n')
return i + 4;
}
return -1;
}
// Extract HTTP status code from first line: "HTTP/1.x NNN ..."
static int parse_status_code(const char* buf, int len) {
// Find first space
int i = 0;
while (i < len && buf[i] != ' ') i++;
if (i >= len) return -1;
i++; // skip space
// Parse 3-digit status
if (i + 2 >= len) return -1;
if (buf[i] < '0' || buf[i] > '9') return -1;
int code = (buf[i] - '0') * 100 + (buf[i+1] - '0') * 10 + (buf[i+2] - '0');
return code;
}
// Extract status text from first line: "HTTP/1.x NNN Status Text\r\n"
static void parse_status_text(const char* buf, int len, char* out, int outMax) {
// Skip "HTTP/1.x NNN "
int i = 0;
while (i < len && buf[i] != ' ') i++;
i++; // skip first space (after HTTP/1.x)
while (i < len && buf[i] != ' ') i++;
i++; // skip second space (after status code)
// Copy until \r or end
int j = 0;
while (i < len && buf[i] != '\r' && buf[i] != '\n' && j < outMax - 1) {
out[j++] = buf[i++];
}
out[j] = '\0';
}
// ---- Main ----
extern "C" void _start() {
// Parse arguments: <server_ip> <port> <path>
char argbuf[512];
zenith::getargs(argbuf, sizeof(argbuf));
const char* arg = skip_spaces(argbuf);
if (*arg == '\0') {
zenith::print("Usage: fetch.elf <server_ip> <port> <path>\n");
zenith::print("Example: run fetch.elf 10.0.68.1 80 /\n");
zenith::print(" run fetch.elf 93.184.216.34 80 /index.html\n");
zenith::exit(0);
}
// 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);
uint32_t serverIp;
if (!parse_ip(ipStr, &serverIp)) {
zenith::print("Invalid IP address: ");
zenith::print(ipStr);
zenith::putchar('\n');
zenith::exit(1);
}
// 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);
uint16_t serverPort;
if (!parse_uint16(portStr, &serverPort)) {
zenith::print("Invalid port: ");
zenith::print(portStr);
zenith::putchar('\n');
zenith::exit(1);
}
// Parse path (rest of args, default to "/" if empty)
char path[256];
if (*arg) {
i = 0;
while (arg[i] && i < 255) { path[i] = arg[i]; i++; }
path[i] = '\0';
} else {
path[0] = '/'; path[1] = '\0';
}
// Print connection info
char msg[256];
snprintf(msg, sizeof(msg), "Connecting to %s:%d...\n", ipStr, (int)serverPort);
zenith::print(msg);
// Create socket
int fd = zenith::socket(Zenith::SOCK_TCP);
if (fd < 0) {
zenith::print("Error: failed to create socket\n");
zenith::exit(1);
}
// Connect
if (zenith::connect(fd, serverIp, serverPort) < 0) {
zenith::print("Error: connection failed\n");
zenith::closesocket(fd);
zenith::exit(1);
}
// Build and send HTTP request
char request[512];
int reqLen = snprintf(request, sizeof(request),
"GET %s HTTP/1.0\r\n"
"Host: %s\r\n"
"User-Agent: ZenithOS/1.0\r\n"
"Connection: close\r\n"
"\r\n",
path, ipStr);
snprintf(msg, sizeof(msg), "GET %s\n", path);
zenith::print(msg);
if (zenith::send(fd, request, reqLen) < 0) {
zenith::print("Error: failed to send request\n");
zenith::closesocket(fd);
zenith::exit(1);
}
// Receive response
// We accumulate the full response to parse headers, then print the body.
// Use a large static buffer (32 KB) since we can't save to files anyway.
static char respBuf[32768];
int respLen = 0;
bool aborted = false;
int idleCount = 0;
while (respLen < (int)sizeof(respBuf) - 1) {
// Check for Ctrl+Q to abort
if (zenith::is_key_available()) {
Zenith::KeyEvent ev;
zenith::getkey(&ev);
if (ev.pressed && ev.ctrl && ev.ascii == 'q') {
aborted = true;
break;
}
}
int r = zenith::recv(fd, respBuf + respLen, sizeof(respBuf) - 1 - respLen);
if (r > 0) {
respLen += r;
idleCount = 0;
} else if (r == 0) {
// Connection closed by server — done
break;
} else {
// No data available or error
idleCount++;
if (idleCount > 2000) {
// Assume connection closed after extended idle
break;
}
zenith::yield();
}
}
respBuf[respLen] = '\0';
zenith::closesocket(fd);
if (aborted) {
zenith::print("\nAborted.\n");
zenith::exit(0);
}
if (respLen == 0) {
zenith::print("Error: empty response\n");
zenith::exit(1);
}
// Parse response headers
int headerEnd = find_header_end(respBuf, respLen);
if (headerEnd < 0) {
// No proper header/body separator — just print everything
zenith::print("Warning: malformed response (no header boundary)\n\n");
zenith::print(respBuf);
zenith::putchar('\n');
zenith::exit(0);
}
int statusCode = parse_status_code(respBuf, headerEnd);
char statusText[64];
parse_status_text(respBuf, headerEnd, statusText, sizeof(statusText));
int bodyLen = respLen - headerEnd;
// Print summary
snprintf(msg, sizeof(msg), "HTTP/1.0 %d %s (%d bytes)\n\n", statusCode, statusText, bodyLen);
zenith::print(msg);
// Print body — it may contain null bytes in binary content, but for text we
// can just print as a string. For binary, we print what we can.
if (bodyLen > 0) {
// Print body in chunks (print expects null-terminated)
const char* body = respBuf + headerEnd;
int printed = 0;
char chunk[512];
while (printed < bodyLen) {
int n = bodyLen - printed;
if (n > (int)sizeof(chunk) - 1) n = (int)sizeof(chunk) - 1;
memcpy(chunk, body + printed, n);
chunk[n] = '\0';
zenith::print(chunk);
printed += n;
}
zenith::putchar('\n');
}
zenith::exit(0);
}
+562
View File
@@ -0,0 +1,562 @@
/*
* main.cpp
* HTTP/1.0 server for ZenithOS
* Usage: run httpd.elf [port] (default: 80)
* Serves a built-in index page and files from the VFS
* Copyright (c) 2025-2026 Daniel Hammer
*/
#include <zenith/syscall.h>
#include <zenith/string.h>
using zenith::slen;
using zenith::streq;
using zenith::starts_with;
using zenith::skip_spaces;
// ---- 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;
}
// ---- IP/port parsing ----
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;
}
// ---- Content type detection ----
static bool ends_with(const char* str, const char* suffix) {
int sl = slen(str);
int xl = slen(suffix);
if (xl > sl) return false;
for (int i = 0; i < xl; i++) {
char a = str[sl - xl + i];
char b = suffix[i];
// Case-insensitive for file extensions
if (a >= 'A' && a <= 'Z') a += 32;
if (b >= 'A' && b <= 'Z') b += 32;
if (a != b) return false;
}
return true;
}
static const char* content_type_for(const char* path) {
if (ends_with(path, ".html") || ends_with(path, ".htm"))
return "text/html";
if (ends_with(path, ".txt"))
return "text/plain";
if (ends_with(path, ".css"))
return "text/css";
if (ends_with(path, ".js"))
return "application/javascript";
return "application/octet-stream";
}
// ---- HTTP response helpers ----
// Send a complete HTTP response with headers and body
static void send_response(int clientFd, int statusCode, const char* statusText,
const char* contentType, const char* body, int bodyLen) {
char header[512];
int hlen = snprintf(header, sizeof(header),
"HTTP/1.0 %d %s\r\n"
"Content-Type: %s\r\n"
"Content-Length: %d\r\n"
"Connection: close\r\n"
"Server: ZenithOS/1.0\r\n"
"\r\n",
statusCode, statusText, contentType, bodyLen);
zenith::send(clientFd, header, hlen);
if (bodyLen > 0) {
zenith::send(clientFd, body, bodyLen);
}
}
// Send a file from the VFS
static int send_file_response(int clientFd, const char* vfsPath, const char* urlPath) {
int handle = zenith::open(vfsPath);
if (handle < 0) return -1;
uint64_t size = zenith::getsize(handle);
const char* ctype = content_type_for(urlPath);
// Send header
char header[512];
int hlen = snprintf(header, sizeof(header),
"HTTP/1.0 200 OK\r\n"
"Content-Type: %s\r\n"
"Content-Length: %u\r\n"
"Connection: close\r\n"
"Server: ZenithOS/1.0\r\n"
"\r\n",
ctype, (unsigned)size);
zenith::send(clientFd, header, hlen);
// Send file body in chunks
uint8_t buf[512];
uint64_t offset = 0;
while (offset < size) {
uint64_t chunk = size - offset;
if (chunk > sizeof(buf)) chunk = sizeof(buf);
int bytesRead = zenith::read(handle, buf, offset, chunk);
if (bytesRead <= 0) break;
zenith::send(clientFd, buf, bytesRead);
offset += bytesRead;
}
zenith::close(handle);
return (int)size;
}
// ---- Request parsing ----
// Extract the request path from "GET /path HTTP/1.x\r\n..."
// Returns length of path written, or -1 on parse error
static int parse_request_path(const char* req, int reqLen, char* pathOut, int pathMax) {
// Find "GET "
if (reqLen < 4) return -1;
if (req[0] != 'G' || req[1] != 'E' || req[2] != 'T' || req[3] != ' ')
return -1;
int i = 4;
int j = 0;
while (i < reqLen && req[i] != ' ' && req[i] != '\r' && req[i] != '\n') {
if (j < pathMax - 1) pathOut[j++] = req[i];
i++;
}
pathOut[j] = '\0';
return j;
}
// ---- Logging ----
static void log_request(const char* method, const char* path, int status, int bodyLen) {
// Get timestamp
Zenith::DateTime dt;
zenith::gettime(&dt);
char msg[256];
snprintf(msg, sizeof(msg), "[%02d:%02d:%02d] %s %s -> %d (%d bytes)\n",
(int)dt.Hour, (int)dt.Minute, (int)dt.Second,
method, path, status, bodyLen);
zenith::print(msg);
}
// ---- Page generators ----
static int generate_index_page(char* buf, int bufSize) {
Zenith::SysInfo info;
zenith::get_info(&info);
uint64_t ms = zenith::get_milliseconds();
uint64_t secs = ms / 1000;
uint64_t mins = secs / 60;
uint64_t hours = mins / 60;
secs %= 60;
mins %= 60;
return snprintf(buf, bufSize,
"<!DOCTYPE html>\n"
"<html>\n"
"<head><title>ZenithOS Web Server</title></head>\n"
"<body>\n"
"<h1>ZenithOS Web Server</h1>\n"
"<p>Welcome! This page is being served by <b>httpd</b> running on ZenithOS.</p>\n"
"<h2>System Information</h2>\n"
"<table>\n"
"<tr><td><b>OS:</b></td><td>%s</td></tr>\n"
"<tr><td><b>Version:</b></td><td>%s</td></tr>\n"
"<tr><td><b>Uptime:</b></td><td>%uh %um %us</td></tr>\n"
"</table>\n"
"<h2>Browse Files</h2>\n"
"<p><a href=\"/files/\">Browse VFS files</a></p>\n"
"</body>\n"
"</html>\n",
info.osName, info.osVersion,
(unsigned)hours, (unsigned)mins, (unsigned)secs);
}
static int generate_404_page(char* buf, int bufSize, const char* path) {
return snprintf(buf, bufSize,
"<!DOCTYPE html>\n"
"<html>\n"
"<head><title>404 Not Found</title></head>\n"
"<body>\n"
"<h1>404 Not Found</h1>\n"
"<p>The requested path <code>%s</code> was not found on this server.</p>\n"
"<p><a href=\"/\">Back to home</a></p>\n"
"</body>\n"
"</html>\n",
path);
}
static int generate_dir_listing(char* buf, int bufSize, const char* urlPath, const char* vfsDir) {
int pos = 0;
pos += snprintf(buf + pos, bufSize - pos,
"<!DOCTYPE html>\n"
"<html>\n"
"<head><title>Index of %s</title></head>\n"
"<body>\n"
"<h1>Index of %s</h1>\n"
"<hr>\n"
"<ul>\n",
urlPath, urlPath);
// Add parent directory link if not at /files/
if (!streq(urlPath, "/files/")) {
pos += snprintf(buf + pos, bufSize - pos,
"<li><a href=\"..\">..</a></li>\n");
}
// List directory entries
const char* entries[64];
int count = zenith::readdir(vfsDir, entries, 64);
// Find the prefix to strip from entry names
// vfsDir is like "0:/" or "0:/subdir"
// entries come back as "subdir/file" for "0:/subdir"
// or "file" for "0:/"
// The prefix in entry names is the part after "0:/"
const char* dirRel = vfsDir + 3; // skip "0:/"
int dirRelLen = slen(dirRel);
for (int i = 0; i < count && pos < bufSize - 128; i++) {
// Extract just the filename portion
const char* name = entries[i];
// If directory is not root, entries have "dir/name" format — strip the dir prefix
if (dirRelLen > 0 && starts_with(name, dirRel)) {
name = name + dirRelLen;
if (*name == '/') name++;
}
if (*name == '\0') continue;
// Build the URL for this entry
pos += snprintf(buf + pos, bufSize - pos,
"<li><a href=\"%s%s\">%s</a></li>\n",
urlPath, name, name);
}
pos += snprintf(buf + pos, bufSize - pos,
"</ul>\n"
"<hr>\n"
"<p><i>ZenithOS httpd</i></p>\n"
"</body>\n"
"</html>\n");
return pos;
}
// ---- Request handler ----
static void handle_client(int clientFd) {
// Read request (HTTP requests are small, 4 KB is plenty)
char reqBuf[4096];
int reqLen = 0;
int idleCount = 0;
// Read until we get the full header (ends with \r\n\r\n)
while (reqLen < (int)sizeof(reqBuf) - 1) {
int r = zenith::recv(clientFd, reqBuf + reqLen, sizeof(reqBuf) - 1 - reqLen);
if (r > 0) {
reqLen += r;
idleCount = 0;
// Check if we have the full header
bool done = false;
for (int i = 0; i + 3 < reqLen; i++) {
if (reqBuf[i] == '\r' && reqBuf[i+1] == '\n' &&
reqBuf[i+2] == '\r' && reqBuf[i+3] == '\n') {
done = true;
break;
}
}
if (done) break;
} else if (r == 0) {
break; // Connection closed
} else {
idleCount++;
if (idleCount > 500) break; // Timeout
zenith::yield();
}
}
reqBuf[reqLen] = '\0';
if (reqLen == 0) {
zenith::closesocket(clientFd);
return;
}
// Parse request path
char path[256];
if (parse_request_path(reqBuf, reqLen, path, sizeof(path)) < 0) {
// Bad request
static char body[] = "<!DOCTYPE html><html><body><h1>400 Bad Request</h1></body></html>";
send_response(clientFd, 400, "Bad Request", "text/html", body, slen(body));
log_request("???", "???", 400, slen(body));
zenith::closesocket(clientFd);
return;
}
// Route the request
static char pageBuf[16384];
if (streq(path, "/")) {
// Try to serve 0:/www/index.html from disk first
int handle = zenith::open("0:/www/index.html");
if (handle >= 0) {
zenith::close(handle);
int bodyLen = send_file_response(clientFd, "0:/www/index.html", "/index.html");
log_request("GET", path, 200, bodyLen);
} else {
// Fall back to built-in index page
int bodyLen = generate_index_page(pageBuf, sizeof(pageBuf));
send_response(clientFd, 200, "OK", "text/html", pageBuf, bodyLen);
log_request("GET", path, 200, bodyLen);
}
} else if (streq(path, "/files") || streq(path, "/files/")) {
// Root directory listing
int bodyLen = generate_dir_listing(pageBuf, sizeof(pageBuf), "/files/", "0:/");
send_response(clientFd, 200, "OK", "text/html", pageBuf, bodyLen);
log_request("GET", path, 200, bodyLen);
} else if (starts_with(path, "/files/")) {
// Serve file or directory from VFS
const char* relPath = path + 7; // skip "/files/"
// Build VFS path: "0:/<relPath>"
char vfsPath[256];
int pi = 0;
vfsPath[pi++] = '0'; vfsPath[pi++] = ':'; vfsPath[pi++] = '/';
int ri = 0;
while (relPath[ri] && pi < (int)sizeof(vfsPath) - 1) {
vfsPath[pi++] = relPath[ri++];
}
// Strip trailing slash for VFS lookup
if (pi > 3 && vfsPath[pi-1] == '/') pi--;
vfsPath[pi] = '\0';
// Try to open as file first
int handle = zenith::open(vfsPath);
if (handle >= 0) {
// It's a file — serve it
zenith::close(handle);
int bodyLen = send_file_response(clientFd, vfsPath, path);
if (bodyLen >= 0) {
log_request("GET", path, 200, bodyLen);
} else {
static char body[] = "<!DOCTYPE html><html><body><h1>500 Internal Server Error</h1></body></html>";
send_response(clientFd, 500, "Internal Server Error", "text/html", body, slen(body));
log_request("GET", path, 500, slen(body));
}
} else {
// Try as directory
const char* entries[64];
int count = zenith::readdir(vfsPath, entries, 64);
if (count >= 0) {
// Make sure urlPath ends with /
char urlPath[256];
int up = 0;
int pp = 0;
while (path[pp] && up < (int)sizeof(urlPath) - 2) urlPath[up++] = path[pp++];
if (up > 0 && urlPath[up-1] != '/') urlPath[up++] = '/';
urlPath[up] = '\0';
int bodyLen = generate_dir_listing(pageBuf, sizeof(pageBuf), urlPath, vfsPath);
send_response(clientFd, 200, "OK", "text/html", pageBuf, bodyLen);
log_request("GET", path, 200, bodyLen);
} else {
// Not found
int bodyLen = generate_404_page(pageBuf, sizeof(pageBuf), path);
send_response(clientFd, 404, "Not Found", "text/html", pageBuf, bodyLen);
log_request("GET", path, 404, bodyLen);
}
}
} else {
// 404 for anything else
int bodyLen = generate_404_page(pageBuf, sizeof(pageBuf), path);
send_response(clientFd, 404, "Not Found", "text/html", pageBuf, bodyLen);
log_request("GET", path, 404, bodyLen);
}
zenith::closesocket(clientFd);
}
// ---- Entry point ----
extern "C" void _start() {
// Parse arguments: [port]
char argbuf[64];
zenith::getargs(argbuf, sizeof(argbuf));
const char* arg = skip_spaces(argbuf);
uint16_t port = 80;
if (*arg) {
if (!parse_uint16(arg, &port)) {
zenith::print("Invalid port: ");
zenith::print(arg);
zenith::putchar('\n');
zenith::exit(1);
}
}
// Create server socket
int listenFd = zenith::socket(Zenith::SOCK_TCP);
if (listenFd < 0) {
zenith::print("Error: failed to create socket\n");
zenith::exit(1);
}
// Bind
if (zenith::bind(listenFd, port) < 0) {
zenith::print("Error: failed to bind to port ");
char tmp[8];
snprintf(tmp, sizeof(tmp), "%d", (int)port);
zenith::print(tmp);
zenith::putchar('\n');
zenith::closesocket(listenFd);
zenith::exit(1);
}
// Listen
if (zenith::listen(listenFd) < 0) {
zenith::print("Error: failed to listen\n");
zenith::closesocket(listenFd);
zenith::exit(1);
}
char msg[128];
snprintf(msg, sizeof(msg), "ZenithOS httpd listening on port %d\n", (int)port);
zenith::print(msg);
zenith::print("Press Ctrl+Q between requests to stop.\n\n");
bool running = true;
while (running) {
// Check for Ctrl+Q before blocking on accept
while (zenith::is_key_available()) {
Zenith::KeyEvent ev;
zenith::getkey(&ev);
if (ev.pressed && ev.ctrl && ev.ascii == 'q') {
running = false;
break;
}
}
if (!running) break;
// Accept next client (blocks until a connection arrives)
int clientFd = zenith::accept(listenFd);
if (clientFd < 0) {
zenith::print("Warning: accept failed\n");
zenith::yield();
continue;
}
handle_client(clientFd);
// After serving, check for Ctrl+Q
while (zenith::is_key_available()) {
Zenith::KeyEvent ev;
zenith::getkey(&ev);
if (ev.pressed && ev.ctrl && ev.ascii == 'q') {
running = false;
break;
}
}
}
zenith::print("\nShutting down httpd...\n");
zenith::closesocket(listenFd);
zenith::exit(0);
}
+149
View File
@@ -0,0 +1,149 @@
/*
* main.cpp
* ifconfig - Show or set network configuration
* Copyright (c) 2025-2026 Daniel Hammer
*/
#include <zenith/syscall.h>
#include <zenith/string.h>
using zenith::starts_with;
using zenith::skip_spaces;
static void print_int(uint64_t n) {
if (n == 0) {
zenith::putchar('0');
return;
}
char buf[20];
int i = 0;
while (n > 0) {
buf[i++] = '0' + (n % 10);
n /= 10;
}
for (int j = i - 1; j >= 0; j--) {
zenith::putchar(buf[j]);
}
}
static void print_ip(uint32_t ip) {
print_int(ip & 0xFF);
zenith::putchar('.');
print_int((ip >> 8) & 0xFF);
zenith::putchar('.');
print_int((ip >> 16) & 0xFF);
zenith::putchar('.');
print_int((ip >> 24) & 0xFF);
}
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;
}
extern "C" void _start() {
char args[256];
int len = zenith::getargs(args, sizeof(args));
if (len <= 0 || args[0] == '\0') {
// Show current network configuration
Zenith::NetCfg cfg;
zenith::get_netcfg(&cfg);
zenith::print(" IP Address: ");
print_ip(cfg.ipAddress);
zenith::putchar('\n');
zenith::print(" Subnet Mask: ");
print_ip(cfg.subnetMask);
zenith::putchar('\n');
zenith::print(" Gateway: ");
print_ip(cfg.gateway);
zenith::putchar('\n');
zenith::exit(0);
}
if (!starts_with(args, "set ")) {
zenith::print("Usage: ifconfig Show network config\n");
zenith::print(" ifconfig set <ip> <mask> <gateway>\n");
zenith::exit(1);
}
// Parse: set <ip> <mask> <gateway>
const char* p = skip_spaces(args + 4);
// Parse IP
char tok[32];
int i = 0;
while (p[i] && p[i] != ' ' && i < 31) { tok[i] = p[i]; i++; }
tok[i] = '\0';
uint32_t ip;
if (!parse_ip(tok, &ip)) {
zenith::print("Invalid IP address: ");
zenith::print(tok);
zenith::putchar('\n');
zenith::exit(1);
}
p = skip_spaces(p + i);
// Parse subnet mask
i = 0;
while (p[i] && p[i] != ' ' && i < 31) { tok[i] = p[i]; i++; }
tok[i] = '\0';
uint32_t mask;
if (!parse_ip(tok, &mask)) {
zenith::print("Invalid subnet mask: ");
zenith::print(tok);
zenith::putchar('\n');
zenith::exit(1);
}
p = skip_spaces(p + i);
// Parse gateway
i = 0;
while (p[i] && p[i] != ' ' && i < 31) { tok[i] = p[i]; i++; }
tok[i] = '\0';
uint32_t gw;
if (!parse_ip(tok, &gw)) {
zenith::print("Invalid gateway: ");
zenith::print(tok);
zenith::putchar('\n');
zenith::exit(1);
}
Zenith::NetCfg cfg;
cfg.ipAddress = ip;
cfg.subnetMask = mask;
cfg.gateway = gw;
if (zenith::set_netcfg(&cfg) < 0) {
zenith::print("Error: failed to set network config\n");
zenith::exit(1);
}
zenith::print("Network config updated:\n");
zenith::print(" IP Address: "); print_ip(ip); zenith::putchar('\n');
zenith::print(" Subnet Mask: "); print_ip(mask); zenith::putchar('\n');
zenith::print(" Gateway: "); print_ip(gw); zenith::putchar('\n');
zenith::exit(0);
}
+36
View File
@@ -0,0 +1,36 @@
/*
* main.cpp
* info - Show system information
* Copyright (c) 2025-2026 Daniel Hammer
*/
#include <zenith/syscall.h>
static void print_int(uint64_t n) {
if (n == 0) {
zenith::putchar('0');
return;
}
char buf[20];
int i = 0;
while (n > 0) {
buf[i++] = '0' + (n % 10);
n /= 10;
}
for (int j = i - 1; j >= 0; j--) {
zenith::putchar(buf[j]);
}
}
extern "C" void _start() {
Zenith::SysInfo info;
zenith::get_info(&info);
zenith::print(info.osName);
zenith::print(" v");
zenith::print(info.osVersion);
zenith::putchar('\n');
zenith::print("Syscall API version: ");
print_int(info.apiVersion);
zenith::putchar('\n');
zenith::exit(0);
}
+164
View File
@@ -0,0 +1,164 @@
/*
* main.cpp
* Init system for ZenithOS (PID 0)
* Chains system services then launches the shell.
* Copyright (c) 2026 Daniel Hammer
*/
#include <zenith/syscall.h>
// ---- Minimal snprintf ----
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;
}
// ---- ANSI color codes ----
#define C_RESET "\033[0m"
#define C_BOLD "\033[1m"
#define C_DIM "\033[2m"
#define C_RED "\033[31m"
#define C_GREEN "\033[32m"
#define C_YELLOW "\033[33m"
#define C_BLUE "\033[34m"
#define C_CYAN "\033[36m"
#define C_WHITE "\033[37m"
// ---- Logging ----
static void log_timestamp(char* buf, int size) {
Zenith::DateTime dt;
zenith::gettime(&dt);
snprintf(buf, size, "%02d:%02d:%02d", dt.Hour, dt.Minute, dt.Second);
}
enum LogLevel { LOG_OK, LOG_INFO, LOG_WARN, LOG_ERR };
static void log(LogLevel level, const char* msg) {
char line[512];
char ts[16];
log_timestamp(ts, sizeof(ts));
const char* tag;
const char* color;
switch (level) {
case LOG_OK: tag = " OK "; color = C_GREEN; break;
case LOG_INFO: tag = " INFO "; color = C_CYAN; break;
case LOG_WARN: tag = " WARN "; color = C_YELLOW; break;
case LOG_ERR: tag = " FAIL "; color = C_RED; break;
}
snprintf(line, sizeof(line),
C_DIM "%s" C_RESET " %s%s" C_RESET " " C_BOLD "init" C_RESET " %s\n",
ts, color, tag, msg);
zenith::print(line);
}
static void log_ok(const char* msg) { log(LOG_OK, msg); }
static void log_info(const char* msg) { log(LOG_INFO, msg); }
static void log_warn(const char* msg) { log(LOG_WARN, msg); }
static void log_err(const char* msg) { log(LOG_ERR, msg); }
// ---- Service runner ----
static bool run_service(const char* path, const char* name) {
char msg[128];
snprintf(msg, sizeof(msg), "Starting %s", name);
log_info(msg);
int pid = zenith::spawn(path);
if (pid < 0) {
snprintf(msg, sizeof(msg), "Failed to start %s", name);
log_err(msg);
return false;
}
zenith::waitpid(pid);
snprintf(msg, sizeof(msg), "%s finished (pid %d)", name, pid);
log_ok(msg);
return true;
}
// ---- Main ----
extern "C" void _start() {
log_info("Init system starting (PID 0)");
// ---- Stage 1: Network configuration ----
run_service("0:/os/dhcp.elf", "dhcp");
// ---- Stage 2: Interactive shell ----
run_service("0:/os/shell.elf", "shell");
log_warn("All services exited");
for (;;) {
zenith::yield();
}
}
+10 -45
View File
@@ -6,6 +6,16 @@
*/
#include <zenith/syscall.h>
#include <zenith/string.h>
using zenith::slen;
using zenith::streq;
using zenith::starts_with;
using zenith::skip_spaces;
using zenith::strcpy;
using zenith::strncpy;
using zenith::memcpy;
using zenith::memmove;
// ---- Minimal snprintf (no libc available) ----
@@ -91,51 +101,6 @@ static int snprintf(char* buf, int size, const char* fmt, ...) {
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) {
+4 -20
View File
@@ -7,27 +7,11 @@
#include <zenith/syscall.h>
#include <zenith/heap.h>
#include <zenith/string.h>
// ---- Utility functions ----
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 int slen(const char* s) {
int n = 0;
while (s[n]) n++;
return n;
}
using zenith::slen;
using zenith::starts_with;
using zenith::skip_spaces;
static void print_int(uint64_t n) {
if (n == 0) {
+101
View File
@@ -0,0 +1,101 @@
/*
* main.cpp
* ping - Send ICMP echo requests
* Copyright (c) 2025-2026 Daniel Hammer
*/
#include <zenith/syscall.h>
static void print_int(uint64_t n) {
if (n == 0) {
zenith::putchar('0');
return;
}
char buf[20];
int i = 0;
while (n > 0) {
buf[i++] = '0' + (n % 10);
n /= 10;
}
for (int j = i - 1; j >= 0; j--) {
zenith::putchar(buf[j]);
}
}
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 void print_ip(uint32_t ip) {
print_int(ip & 0xFF);
zenith::putchar('.');
print_int((ip >> 8) & 0xFF);
zenith::putchar('.');
print_int((ip >> 16) & 0xFF);
zenith::putchar('.');
print_int((ip >> 24) & 0xFF);
}
extern "C" void _start() {
char args[256];
int len = zenith::getargs(args, sizeof(args));
if (len <= 0 || args[0] == '\0') {
zenith::print("Usage: ping <ip address>\n");
zenith::exit(1);
}
uint32_t ip;
if (!parse_ip(args, &ip)) {
zenith::print("Invalid IP address: ");
zenith::print(args);
zenith::putchar('\n');
zenith::exit(1);
}
zenith::print("PING ");
print_ip(ip);
zenith::putchar('\n');
for (int i = 0; i < 4; i++) {
int32_t rtt = zenith::ping(ip, 3000);
if (rtt < 0) {
zenith::print(" Request timed out\n");
} else {
zenith::print(" Reply from ");
print_ip(ip);
zenith::print(": time=");
print_int((uint64_t)rtt);
zenith::print("ms\n");
}
if (i < 3) {
zenith::sleep_ms(1000);
}
}
zenith::exit(0);
}
+12
View File
@@ -0,0 +1,12 @@
/*
* main.cpp
* reset - Reboot the system
* Copyright (c) 2025-2026 Daniel Hammer
*/
#include <zenith/syscall.h>
extern "C" void _start() {
zenith::print("Rebooting...\n");
zenith::reset();
}
+285 -450
View File
@@ -5,52 +5,33 @@
*/
#include <zenith/syscall.h>
#include <zenith/string.h>
static bool streq(const char* a, const char* b) {
while (*a && *b) {
if (*a != *b) return false;
a++; b++;
using zenith::slen;
using zenith::streq;
using zenith::starts_with;
using zenith::skip_spaces;
static void scopy(char* dst, const char* src, int maxLen) {
int i = 0;
while (src[i] && i < maxLen - 1) { dst[i] = src[i]; i++; }
dst[i] = '\0';
}
static void scat(char* dst, const char* src, int maxLen) {
int dLen = slen(dst);
int i = 0;
while (src[i] && dLen + i < maxLen - 1) {
dst[dLen + i] = src[i];
i++;
}
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 int slen(const char* s) {
int n = 0;
while (s[n]) n++;
return n;
dst[dLen + i] = '\0';
}
// Current working directory (relative to 0:/).
// "" = root, "man" = 0:/man, "man/sub" = 0:/man/sub
static char cwd[128] = "";
// Build full VFS path: "0:/" + cwd + "/" + name
static void resolve_path(const char* name, char* out, int outMax) {
int i = 0;
out[i++] = '0'; out[i++] = ':'; out[i++] = '/';
if (cwd[0]) {
int j = 0;
while (cwd[j] && i < outMax - 2) out[i++] = cwd[j++];
out[i++] = '/';
}
int j = 0;
while (name[j] && i < outMax - 1) out[i++] = name[j++];
out[i] = '\0';
}
// Build VFS directory path: "0:/" or "0:/<dir>"
static void build_dir_path(const char* dir, char* out, int outMax) {
int i = 0;
@@ -62,58 +43,100 @@ static void build_dir_path(const char* dir, char* out, int outMax) {
out[i] = '\0';
}
static void print_int(uint64_t n) {
if (n == 0) {
zenith::putchar('0');
return;
}
char buf[20];
int i = 0;
while (n > 0) {
buf[i++] = '0' + (n % 10);
n /= 10;
}
for (int j = i - 1; j >= 0; j--) {
zenith::putchar(buf[j]);
// ---- Command history ----
static constexpr int HISTORY_MAX = 32;
static char history[HISTORY_MAX][256];
static int history_count = 0;
static int history_next = 0; // ring-buffer write index
static void history_add(const char* line) {
if (line[0] == '\0') return;
// Don't add duplicate of last entry
if (history_count > 0) {
int prev = (history_next + HISTORY_MAX - 1) % HISTORY_MAX;
if (streq(history[prev], line)) return;
}
scopy(history[history_next], line, 256);
history_next = (history_next + 1) % HISTORY_MAX;
if (history_count < HISTORY_MAX) history_count++;
}
// Get history entry by index (0 = most recent, 1 = one before that, ...)
static const char* history_get(int idx) {
if (idx < 0 || idx >= history_count) return nullptr;
int pos = (history_next + HISTORY_MAX - 1 - idx) % HISTORY_MAX;
return history[pos];
}
// ---- Prompt ----
static void prompt() {
zenith::print("0:/");
if (cwd[0]) zenith::print(cwd);
zenith::print("> ");
}
// ---- Erase current input line on screen ----
static void erase_input(int len) {
// Move cursor to start of input and overwrite with spaces
for (int i = 0; i < len; i++) zenith::putchar('\b');
for (int i = 0; i < len; i++) zenith::putchar(' ');
for (int i = 0; i < len; i++) zenith::putchar('\b');
}
// ---- Replace visible line with new content ----
static void replace_line(char* line, int* pos, const char* newContent) {
int oldLen = *pos;
erase_input(oldLen);
int newLen = slen(newContent);
if (newLen > 255) newLen = 255;
for (int i = 0; i < newLen; i++) {
line[i] = newContent[i];
zenith::putchar(newContent[i]);
}
line[newLen] = '\0';
*pos = newLen;
}
// ---- Builtin: help ----
static void cmd_help() {
zenith::print("Available commands:\n");
zenith::print("Shell builtins:\n");
zenith::print(" help Show this help message\n");
zenith::print(" info Show system information\n");
zenith::print(" man <topic> View manual pages\n");
zenith::print(" ls [dir] List files in directory\n");
zenith::print(" cd [dir] Change working directory\n");
zenith::print(" exit Exit the shell\n");
zenith::print("\n");
zenith::print("System commands:\n");
zenith::print(" man <topic> View manual pages\n");
zenith::print(" cat <file> Display file contents\n");
zenith::print(" run <file> Spawn a new process from an ELF file\n");
zenith::print(" ping <ip> Send ICMP echo requests\n");
zenith::print(" tcpconnect <ip> <port> Connect to a TCP server\n");
zenith::print(" edit [file] Text editor\n");
zenith::print(" info Show system information\n");
zenith::print(" date Show current date and time\n");
zenith::print(" uptime Show uptime in milliseconds\n");
zenith::print(" uptime Show uptime\n");
zenith::print(" clear Clear the screen\n");
zenith::print(" reset Reboot the system\n");
zenith::print(" shutdown Shut down the system\n");
zenith::print(" exit Exit the shell\n");
zenith::print("\n");
zenith::print("Network commands:\n");
zenith::print(" ping <ip> Send ICMP echo requests\n");
zenith::print(" ifconfig Show/set network configuration\n");
zenith::print(" tcpconnect Connect to a TCP server\n");
zenith::print(" irc IRC client\n");
zenith::print(" dhcp DHCP client\n");
zenith::print(" fetch <url> HTTP client\n");
zenith::print(" httpd HTTP server\n");
zenith::print("\n");
zenith::print("Games:\n");
zenith::print(" doom DOOM\n");
zenith::print("\n");
zenith::print("Any .elf on the ramdisk is executable.\n");
}
static void cmd_info() {
Zenith::SysInfo info;
zenith::get_info(&info);
zenith::print(info.osName);
zenith::print(" v");
zenith::print(info.osVersion);
zenith::print("\n");
zenith::print("Syscall API version: ");
print_int(info.apiVersion);
zenith::putchar('\n');
}
// ---- Builtin: ls ----
static void cmd_ls(const char* arg) {
arg = skip_spaces(arg);
@@ -121,7 +144,7 @@ static void cmd_ls(const char* arg) {
// Build the target directory (relative path from root)
char dir[128];
if (*arg) {
// ls <dir> combine cwd and arg
// ls <dir> -- combine cwd and arg
if (cwd[0]) {
int i = 0, j = 0;
while (cwd[j] && i < 126) dir[i++] = cwd[j++];
@@ -135,7 +158,7 @@ static void cmd_ls(const char* arg) {
dir[i] = '\0';
}
} else {
// ls with no arg use cwd
// ls with no arg -- use cwd
int i = 0;
while (cwd[i] && i < 126) { dir[i] = cwd[i]; i++; }
dir[i] = '\0';
@@ -166,310 +189,26 @@ static void cmd_ls(const char* arg) {
}
}
static void cmd_cat(const char* arg) {
arg = skip_spaces(arg);
if (*arg == '\0') {
zenith::print("Usage: cat <filename>\n");
return;
}
char path[128];
resolve_path(arg, path, sizeof(path));
int handle = zenith::open(path);
if (handle < 0) {
zenith::print("Error: cannot open '");
zenith::print(arg);
zenith::print("'\n");
return;
}
uint64_t size = zenith::getsize(handle);
if (size == 0) {
zenith::close(handle);
return;
}
// Read in chunks
uint8_t buf[512];
uint64_t offset = 0;
while (offset < size) {
uint64_t chunk = size - offset;
if (chunk > sizeof(buf) - 1) chunk = sizeof(buf) - 1;
int bytesRead = zenith::read(handle, buf, offset, chunk);
if (bytesRead <= 0) break;
buf[bytesRead] = '\0';
zenith::print((const char*)buf);
offset += bytesRead;
}
zenith::close(handle);
zenith::putchar('\n');
}
static void cmd_uptime() {
uint64_t ms = zenith::get_milliseconds();
uint64_t secs = ms / 1000;
uint64_t mins = secs / 60;
secs %= 60;
ms %= 1000;
zenith::print("Uptime: ");
print_int(mins);
zenith::print("m ");
print_int(secs);
zenith::print("s ");
print_int(ms);
zenith::print("ms\n");
}
static bool parse_ip(const char* s, uint32_t* out) {
// Parse "a.b.c.d" into a uint32_t in network byte order (little-endian stored)
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 void print_ip(uint32_t ip) {
print_int(ip & 0xFF);
zenith::putchar('.');
print_int((ip >> 8) & 0xFF);
zenith::putchar('.');
print_int((ip >> 16) & 0xFF);
zenith::putchar('.');
print_int((ip >> 24) & 0xFF);
}
static void cmd_ping(const char* arg) {
arg = skip_spaces(arg);
if (*arg == '\0') {
zenith::print("Usage: ping <ip address>\n");
return;
}
uint32_t ip;
if (!parse_ip(arg, &ip)) {
zenith::print("Invalid IP address: ");
zenith::print(arg);
zenith::putchar('\n');
return;
}
zenith::print("PING ");
print_ip(ip);
zenith::putchar('\n');
for (int i = 0; i < 4; i++) {
int32_t rtt = zenith::ping(ip, 3000);
if (rtt < 0) {
zenith::print(" Request timed out\n");
} else {
zenith::print(" Reply from ");
print_ip(ip);
zenith::print(": time=");
print_int((uint64_t)rtt);
zenith::print("ms\n");
}
if (i < 3) {
zenith::sleep_ms(1000);
}
}
}
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: tcpconnect <ip> <port>\n");
return;
}
// 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';
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 <ip> <port>\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 <filename> [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(filename);
zenith::print("'\n");
} else {
zenith::waitpid(pid);
}
}
// ---- Builtin: cd ----
static void cmd_cd(const char* arg) {
arg = skip_spaces(arg);
// cd or cd / → go to root
// Strip trailing slashes from argument (ls shows dirs as "www/", user may type that)
static char argBuf[128];
int aLen = 0;
while (arg[aLen] && aLen < 127) { argBuf[aLen] = arg[aLen]; aLen++; }
argBuf[aLen] = '\0';
while (aLen > 0 && argBuf[aLen - 1] == '/') argBuf[--aLen] = '\0';
arg = argBuf;
// cd or cd / -> go to root
if (*arg == '\0' || streq(arg, "/")) {
cwd[0] = '\0';
return;
}
// cd .. go up one level
// cd .. -> go up one level
if (streq(arg, "..")) {
int len = slen(cwd);
int last = -1;
@@ -517,6 +256,8 @@ static void cmd_cd(const char* arg) {
cwd[i] = '\0';
}
// ---- Builtin: man ----
static void cmd_man(const char* arg) {
arg = skip_spaces(arg);
if (*arg == '\0') {
@@ -526,7 +267,7 @@ static void cmd_man(const char* arg) {
return;
}
int pid = zenith::spawn("0:/man.elf", arg);
int pid = zenith::spawn("0:/os/man.elf", arg);
if (pid < 0) {
zenith::print("Error: failed to start man viewer\n");
} else {
@@ -534,102 +275,155 @@ static void cmd_man(const char* arg) {
}
}
static void print_int_padded(uint64_t n) {
if (n < 10) zenith::putchar('0');
print_int(n);
// ---- External command execution ----
// Try to spawn an ELF at the given path. Returns true on success.
static bool try_exec(const char* path, const char* args) {
// Check if the file exists before asking the kernel to load it
int h = zenith::open(path);
if (h < 0) return false;
zenith::close(h);
int pid = zenith::spawn(path, args);
if (pid < 0) return false;
zenith::waitpid(pid);
return true;
}
static const char* month_name(int m) {
static const char* months[] = {
"", "January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
};
if (m >= 1 && m <= 12) return months[m];
return "?";
// Check if a string already has a VFS drive prefix (e.g. "0:/")
static bool has_drive_prefix(const char* s) {
return s[0] >= '0' && s[0] <= '9' && s[1] == ':';
}
static void cmd_date() {
Zenith::DateTime dt;
zenith::gettime(&dt);
// Resolve arguments: expand relative file paths against CWD.
// Tokens that already have a drive prefix (e.g. "0:/foo") are left as-is.
// Everything before the first space-delimited token that looks like a path
// option (starts with '-') is also left as-is.
static void resolve_args(const char* args, char* out, int outMax) {
if (!args || !args[0]) { out[0] = '\0'; return; }
print_int(dt.Day);
zenith::putchar(' ');
zenith::print(month_name(dt.Month));
zenith::putchar(' ');
print_int(dt.Year);
zenith::print(", ");
print_int(dt.Hour);
zenith::putchar(':');
print_int_padded(dt.Minute);
zenith::putchar(':');
print_int_padded(dt.Second);
zenith::print(" UTC\n");
int o = 0;
const char* p = args;
while (*p && o < outMax - 1) {
// Skip spaces, copy them through
while (*p == ' ' && o < outMax - 1) { out[o++] = *p++; }
if (!*p) break;
// Extract the token
const char* tokStart = p;
int tokLen = 0;
while (p[tokLen] && p[tokLen] != ' ') tokLen++;
// Decide whether to resolve this token as a path.
// Don't resolve if it already has a drive prefix, or starts with '-'
bool resolve = cwd[0] && !has_drive_prefix(tokStart) && tokStart[0] != '-';
if (resolve) {
// Write "0:/<cwd>/<token>"
if (o + 3 < outMax) { out[o++] = '0'; out[o++] = ':'; out[o++] = '/'; }
int j = 0;
while (cwd[j] && o < outMax - 1) out[o++] = cwd[j++];
if (o < outMax - 1) out[o++] = '/';
for (int k = 0; k < tokLen && o < outMax - 1; k++) out[o++] = tokStart[k];
} else {
for (int k = 0; k < tokLen && o < outMax - 1; k++) out[o++] = tokStart[k];
}
p = tokStart + tokLen;
}
out[o] = '\0';
}
static void cmd_clear() {
zenith::print("\033[2J"); // Clear entire screen
zenith::print("\033[H"); // Move cursor to top-left
static void exec_external(const char* cmd, const char* args) {
char path[256];
// Resolve arguments against CWD so external programs get full VFS paths
char resolvedArgs[512];
resolve_args(args, resolvedArgs, sizeof(resolvedArgs));
const char* finalArgs = resolvedArgs[0] ? resolvedArgs : nullptr;
// 1. Try 0:/os/<cmd>.elf
scopy(path, "0:/os/", sizeof(path));
scat(path, cmd, sizeof(path));
scat(path, ".elf", sizeof(path));
if (try_exec(path, finalArgs)) return;
// 2. Try 0:/games/<cmd>.elf
scopy(path, "0:/games/", sizeof(path));
scat(path, cmd, sizeof(path));
scat(path, ".elf", sizeof(path));
if (try_exec(path, finalArgs)) return;
// 3. Try 0:/<cwd>/<cmd>.elf (if cwd is set)
if (cwd[0]) {
scopy(path, "0:/", sizeof(path));
scat(path, cwd, sizeof(path));
scat(path, "/", sizeof(path));
scat(path, cmd, sizeof(path));
scat(path, ".elf", sizeof(path));
if (try_exec(path, finalArgs)) return;
}
// 4. Try 0:/<cmd>.elf
scopy(path, "0:/", sizeof(path));
scat(path, cmd, sizeof(path));
scat(path, ".elf", sizeof(path));
if (try_exec(path, finalArgs)) return;
// Not found
zenith::print(cmd);
zenith::print(": command not found\n");
}
// ---- Command dispatch ----
static void process_command(const char* line) {
// Skip leading spaces
line = skip_spaces(line);
if (*line == '\0') return;
if (streq(line, "help")) {
// Parse command name and arguments
char cmd[128];
int i = 0;
while (line[i] && line[i] != ' ' && i < 127) {
cmd[i] = line[i];
i++;
}
cmd[i] = '\0';
const char* args = nullptr;
if (line[i] == ' ') {
args = skip_spaces(line + i);
if (*args == '\0') args = nullptr;
}
// Builtins
if (streq(cmd, "help")) {
cmd_help();
} else if (streq(line, "info")) {
cmd_info();
} else if (starts_with(line, "ls ")) {
cmd_ls(line + 3);
} else if (streq(line, "ls")) {
cmd_ls("");
} else if (starts_with(line, "cd ")) {
cmd_cd(line + 3);
} else if (streq(line, "cd")) {
cmd_cd("");
} else if (starts_with(line, "man ")) {
cmd_man(line + 4);
} else if (streq(line, "man")) {
cmd_man("");
} else if (starts_with(line, "cat ")) {
cmd_cat(line + 4);
} else if (streq(line, "cat")) {
cmd_cat("");
} else if (starts_with(line, "run ")) {
cmd_run(line + 4);
} else if (streq(line, "run")) {
cmd_run("");
} else if (starts_with(line, "ping ")) {
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")) {
cmd_uptime();
} else if (streq(line, "clear")) {
cmd_clear();
} else if (streq(line, "reset")) {
zenith::print("Rebooting...\n");
zenith::reset();
} else if (streq(line, "shutdown")) {
zenith::print("Shutting down...\n");
zenith::shutdown();
} else if (streq(line, "exit")) {
} else if (streq(cmd, "ls")) {
cmd_ls(args ? args : "");
} else if (streq(cmd, "cd")) {
cmd_cd(args ? args : "");
} else if (streq(cmd, "man")) {
cmd_man(args ? args : "");
} else if (streq(cmd, "exit")) {
zenith::print("Goodbye.\n");
zenith::exit(0);
} else {
zenith::print("Unknown command: ");
zenith::print(line);
zenith::print("\nType 'help' for available commands.\n");
// External command -- pass full argument string
exec_external(cmd, args);
}
}
// ---- Arrow key scancodes ----
static constexpr uint8_t SC_UP = 0x48;
static constexpr uint8_t SC_DOWN = 0x50;
static constexpr uint8_t SC_LEFT = 0x4B;
static constexpr uint8_t SC_RIGHT = 0x4D;
// ---- Entry point ----
extern "C" void _start() {
zenith::print("\n");
zenith::print(" ZenithOS\n");
@@ -641,28 +435,69 @@ extern "C" void _start() {
char line[256];
int pos = 0;
int hist_nav = -1; // -1 = not navigating history
prompt();
while (true) {
char c = zenith::getchar();
if (!zenith::is_key_available()) {
zenith::yield();
continue;
}
if (c == '\n') {
Zenith::KeyEvent ev;
zenith::getkey(&ev);
if (!ev.pressed) continue;
// Arrow keys: ascii == 0, check scancode
if (ev.ascii == 0) {
if (ev.scancode == SC_UP) {
// Navigate to older history entry
int next = hist_nav + 1;
const char* entry = history_get(next);
if (entry) {
hist_nav = next;
replace_line(line, &pos, entry);
}
} else if (ev.scancode == SC_DOWN) {
// Navigate to newer history entry
if (hist_nav > 0) {
hist_nav--;
const char* entry = history_get(hist_nav);
if (entry) {
replace_line(line, &pos, entry);
}
} else if (hist_nav == 0) {
// Back to empty line
hist_nav = -1;
erase_input(pos);
pos = 0;
line[0] = '\0';
}
}
// Left/Right arrows: ignore for now (no cursor movement within line)
continue;
}
if (ev.ascii == '\n') {
zenith::putchar('\n');
line[pos] = '\0';
history_add(line);
process_command(line);
pos = 0;
hist_nav = -1;
prompt();
} else if (c == '\b') {
} else if (ev.ascii == '\b') {
if (pos > 0) {
pos--;
zenith::putchar('\b');
zenith::putchar(' ');
zenith::putchar('\b');
}
} else if (c >= ' ' && pos < 255) {
line[pos++] = c;
zenith::putchar(c);
} else if (ev.ascii >= ' ' && pos < 255) {
line[pos++] = ev.ascii;
zenith::putchar(ev.ascii);
}
}
}
+12
View File
@@ -0,0 +1,12 @@
/*
* main.cpp
* shutdown - Shut down the system
* Copyright (c) 2025-2026 Daniel Hammer
*/
#include <zenith/syscall.h>
extern "C" void _start() {
zenith::print("Shutting down...\n");
zenith::shutdown();
}
+194
View File
@@ -0,0 +1,194 @@
/*
* main.cpp
* tcpconnect - Interactive TCP client
* Copyright (c) 2025-2026 Daniel Hammer
*/
#include <zenith/syscall.h>
#include <zenith/string.h>
using zenith::skip_spaces;
static void print_int(uint64_t n) {
if (n == 0) {
zenith::putchar('0');
return;
}
char buf[20];
int i = 0;
while (n > 0) {
buf[i++] = '0' + (n % 10);
n /= 10;
}
for (int j = i - 1; j >= 0; j--) {
zenith::putchar(buf[j]);
}
}
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 void print_ip(uint32_t ip) {
print_int(ip & 0xFF);
zenith::putchar('.');
print_int((ip >> 8) & 0xFF);
zenith::putchar('.');
print_int((ip >> 16) & 0xFF);
zenith::putchar('.');
print_int((ip >> 24) & 0xFF);
}
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;
}
extern "C" void _start() {
char args[256];
int len = zenith::getargs(args, sizeof(args));
if (len <= 0 || args[0] == '\0') {
zenith::print("Usage: tcpconnect <ip> <port>\n");
zenith::exit(1);
}
// Parse IP address (up to first space)
char ipStr[32];
int i = 0;
while (args[i] && args[i] != ' ' && i < 31) {
ipStr[i] = args[i];
i++;
}
ipStr[i] = '\0';
uint32_t ip;
if (!parse_ip(ipStr, &ip)) {
zenith::print("Invalid IP address: ");
zenith::print(ipStr);
zenith::putchar('\n');
zenith::exit(1);
}
// Parse port
const char* portStr = skip_spaces(args + i);
if (*portStr == '\0') {
zenith::print("Usage: tcpconnect <ip> <port>\n");
zenith::exit(1);
}
uint16_t port;
if (!parse_uint16(portStr, &port)) {
zenith::print("Invalid port: ");
zenith::print(portStr);
zenith::putchar('\n');
zenith::exit(1);
}
// Create socket
int fd = zenith::socket(Zenith::SOCK_TCP);
if (fd < 0) {
zenith::print("Error: failed to create socket\n");
zenith::exit(1);
}
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);
zenith::exit(1);
}
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);
zenith::exit(0);
}
+40
View File
@@ -0,0 +1,40 @@
/*
* main.cpp
* uptime - Show system uptime
* Copyright (c) 2025-2026 Daniel Hammer
*/
#include <zenith/syscall.h>
static void print_int(uint64_t n) {
if (n == 0) {
zenith::putchar('0');
return;
}
char buf[20];
int i = 0;
while (n > 0) {
buf[i++] = '0' + (n % 10);
n /= 10;
}
for (int j = i - 1; j >= 0; j--) {
zenith::putchar(buf[j]);
}
}
extern "C" void _start() {
uint64_t ms = zenith::get_milliseconds();
uint64_t secs = ms / 1000;
uint64_t mins = secs / 60;
secs %= 60;
ms %= 1000;
zenith::print("Uptime: ");
print_int(mins);
zenith::print("m ");
print_int(secs);
zenith::print("s ");
print_int(ms);
zenith::print("ms\n");
zenith::exit(0);
}
+31
View File
@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>ZenithOS</title>
</head>
<body style="margin:0;padding:0;font-family:Georgia,'Times New Roman',serif;color:#222;background:#fff">
<div style="max-width:38em;margin:4em auto;padding:0 1.5em">
<h1 style="font-size:2.4em;font-weight:normal;letter-spacing:-0.02em;margin:0 0 0.15em">
ZenithOS
</h1>
<hr style="border:none;border-top:1px solid #ccc;margin:0 0 2em">
<h2 style="font-size:1.2em;font-weight:normal;margin:0 0 0.8em">Browse</h2>
<ul style="line-height:1.8;padding-left:1.2em;margin:0 0 2.5em">
<li><a href="/files/" style="color:#1a5c9e">Filesystem</a> &mdash; browse the VFS</li>
</ul>
<hr style="border:none;border-top:1px solid #ccc;margin:0 0 1.5em">
<p style="font-size:0.85em;color:#999;margin:0">
ZenithOS &middot; httpd/1.0
</p>
</div>
</body>
</html>