feat: expand user mode, add DOOM game, add manpages

This commit is contained in:
2026-02-18 15:13:53 +01:00
parent 605fbcbe42
commit 24af60d669
51 changed files with 4484 additions and 43 deletions
+187
View File
@@ -0,0 +1,187 @@
# Makefile for DOOM (doomgeneric) on ZenithOS
# Copyright (c) 2025 Daniel Hammer
MAKEFLAGS += -rR
.SUFFIXES:
# ---- Toolchain ----
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf-
ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
CC := $(TOOLCHAIN_PREFIX)gcc
LD := $(TOOLCHAIN_PREFIX)gcc
else
CC := gcc
LD := gcc
endif
# ---- Paths ----
DOOM_SRC := ../../../doomgeneric/doomgeneric
LIBC_INC := ../../include/libc
PROG_INC := ../../include
LINK_LD := ../../link.ld
BINDIR := ../../bin
OBJDIR := obj
# ---- Compiler flags ----
CFLAGS := \
-std=gnu11 \
-g -O2 -pipe \
-Wall \
-Wno-unused-parameter \
-Wno-unused-variable \
-Wno-missing-field-initializers \
-Wno-sign-compare \
-Wno-pointer-sign \
-nostdinc \
-ffreestanding \
-fno-stack-protector \
-fno-stack-check \
-fno-PIC \
-ffunction-sections \
-fdata-sections \
-m64 \
-march=x86-64 \
-msse \
-msse2 \
-mno-red-zone \
-mcmodel=small \
-DNORMALUNIX \
-DLINUX \
-isystem $(LIBC_INC) \
-I $(PROG_INC) \
-I $(DOOM_SRC) \
-isystem $(shell $(CC) -print-file-name=include)
# ---- Linker flags ----
LDFLAGS := \
-nostdlib \
-static \
-Wl,--build-id=none \
-Wl,--gc-sections \
-Wl,-m,elf_x86_64 \
-z max-page-size=0x1000 \
-T $(LINK_LD)
# ---- DOOM source files (excluding platform-specific ports and sound backends) ----
DOOM_SRCS := \
am_map.c \
d_event.c \
d_items.c \
d_iwad.c \
d_loop.c \
d_main.c \
d_mode.c \
d_net.c \
doomdef.c \
doomgeneric.c \
doomstat.c \
dstrings.c \
dummy.c \
f_finale.c \
f_wipe.c \
g_game.c \
gusconf.c \
hu_lib.c \
hu_stuff.c \
i_cdmus.c \
i_endoom.c \
i_input.c \
i_joystick.c \
i_scale.c \
i_sound.c \
i_system.c \
i_timer.c \
i_video.c \
info.c \
m_argv.c \
m_bbox.c \
m_cheat.c \
m_config.c \
m_controls.c \
m_fixed.c \
m_menu.c \
m_misc.c \
m_random.c \
memio.c \
mus2mid.c \
p_ceilng.c \
p_doors.c \
p_enemy.c \
p_floor.c \
p_inter.c \
p_lights.c \
p_map.c \
p_maputl.c \
p_mobj.c \
p_plats.c \
p_pspr.c \
p_saveg.c \
p_setup.c \
p_sight.c \
p_spec.c \
p_switch.c \
p_telept.c \
p_tick.c \
p_user.c \
r_bsp.c \
r_data.c \
r_draw.c \
r_main.c \
r_plane.c \
r_segs.c \
r_sky.c \
r_things.c \
s_sound.c \
sha1.c \
sounds.c \
st_lib.c \
st_stuff.c \
statdump.c \
tables.c \
v_video.c \
w_checksum.c \
w_file.c \
w_file_stdc.c \
w_main.c \
w_wad.c \
wi_stuff.c \
z_zone.c
# Local source files
LOCAL_SRCS := doomgeneric_zenith.c libc.c
# ---- Object files ----
DOOM_OBJS := $(addprefix $(OBJDIR)/,$(DOOM_SRCS:.c=.o))
LOCAL_OBJS := $(addprefix $(OBJDIR)/,$(LOCAL_SRCS:.c=.o))
ALL_OBJS := $(DOOM_OBJS) $(LOCAL_OBJS)
# ---- Target ----
TARGET := $(BINDIR)/doom.elf
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(ALL_OBJS) $(LINK_LD) Makefile
mkdir -p $(BINDIR)
$(LD) $(CFLAGS) $(LDFLAGS) $(ALL_OBJS) -o $@
# DOOM source files (from doomgeneric directory)
$(OBJDIR)/%.o: $(DOOM_SRC)/%.c Makefile
mkdir -p $(OBJDIR)
$(CC) $(CFLAGS) -c $< -o $@
# Local source files
$(OBJDIR)/%.o: %.c Makefile
mkdir -p $(OBJDIR)
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -rf $(OBJDIR) $(TARGET)
+230
View File
@@ -0,0 +1,230 @@
/*
* doomgeneric_zenith.c
* DOOM platform implementation for ZenithOS
* Copyright (c) 2025 Daniel Hammer
*/
#include "doomgeneric.h"
#include "doomkeys.h"
#include <string.h>
#include <stdio.h>
/* ---- Raw syscall interface (C versions) ---- */
static inline long _zos_syscall0(long nr) {
long ret;
__asm__ volatile("syscall" : "=a"(ret) : "a"(nr)
: "rcx", "r11", "rdi", "rsi", "rdx", "r8", "r9", "r10", "memory");
return ret;
}
static inline long _zos_syscall1(long nr, long a1) {
long ret;
__asm__ volatile(
"mov %[a1], %%rdi\n\t"
"syscall"
: "=a"(ret)
: "a"(nr), [a1] "r"(a1)
: "rcx", "r11", "rdi", "rsi", "rdx", "r8", "r9", "r10", "memory");
return ret;
}
/* Syscall numbers (must match kernel/src/Api/Syscall.hpp) */
#define SYS_EXIT 0
#define SYS_SLEEP_MS 2
#define SYS_PRINT 4
#define SYS_GETMILLISECONDS 14
#define SYS_ISKEYAVAILABLE 16
#define SYS_GETKEY 17
#define SYS_FBINFO 21
#define SYS_FBMAP 22
/* FbInfo struct (must match kernel definition) */
struct FbInfo {
unsigned long width;
unsigned long height;
unsigned long pitch;
unsigned long bpp;
unsigned long userAddr;
};
/* KeyEvent struct (must match kernel definition) */
struct KeyEvent {
unsigned char scancode;
char ascii;
unsigned char pressed;
unsigned char shift;
unsigned char ctrl;
unsigned char alt;
};
/* ---- Framebuffer state ---- */
static uint32_t* g_fbPtr = 0;
static uint32_t g_fbWidth = 0;
static uint32_t g_fbHeight = 0;
static uint32_t g_fbPitch = 0; /* bytes per scanline */
/* ---- Circular key queue ---- */
#define KEY_QUEUE_SIZE 64
struct KeyQueueEntry {
int pressed;
unsigned char doomkey;
};
static struct KeyQueueEntry g_keyQueue[KEY_QUEUE_SIZE];
static int g_keyQueueRead = 0;
static int g_keyQueueWrite = 0;
static void key_queue_push(int pressed, unsigned char doomkey) {
int next = (g_keyQueueWrite + 1) % KEY_QUEUE_SIZE;
if (next == g_keyQueueRead) return; /* full, drop */
g_keyQueue[g_keyQueueWrite].pressed = pressed;
g_keyQueue[g_keyQueueWrite].doomkey = doomkey;
g_keyQueueWrite = next;
}
static int key_queue_pop(int* pressed, unsigned char* doomkey) {
if (g_keyQueueRead == g_keyQueueWrite) return 0;
*pressed = g_keyQueue[g_keyQueueRead].pressed;
*doomkey = g_keyQueue[g_keyQueueRead].doomkey;
g_keyQueueRead = (g_keyQueueRead + 1) % KEY_QUEUE_SIZE;
return 1;
}
/* ---- PS/2 scancode to ASCII table (set 1, unshifted) ---- */
static const char scancode_to_ascii[128] = {
0, 27, '1','2','3','4','5','6','7','8','9','0','-','=','\b',
'\t','q','w','e','r','t','y','u','i','o','p','[',']','\n',
0, 'a','s','d','f','g','h','j','k','l',';','\'','`',
0, '\\','z','x','c','v','b','n','m',',','.','/', 0,
'*', 0, ' '
};
/* ---- PS/2 scancode to DOOM key mapping ---- */
static unsigned char scancode_to_doomkey(unsigned char scancode, char ascii) {
switch (scancode) {
case 0x48: return KEY_UPARROW;
case 0x50: return KEY_DOWNARROW;
case 0x4B: return KEY_LEFTARROW;
case 0x4D: return KEY_RIGHTARROW;
case 0x1C: return KEY_ENTER;
case 0x01: return KEY_ESCAPE;
case 0x39: return ' '; /* Space = use */
case 0x1D: return KEY_RCTRL; /* LCtrl = fire */
case 0x2A: return KEY_RSHIFT; /* LShift = run */
case 0x36: return KEY_RSHIFT; /* RShift = run */
case 0x38: return KEY_RALT; /* Alt = strafe */
case 0x0E: return KEY_BACKSPACE;
case 0x0F: return KEY_TAB;
/* F1-F10 */
case 0x3B: return KEY_F1;
case 0x3C: return KEY_F2;
case 0x3D: return KEY_F3;
case 0x3E: return KEY_F4;
case 0x3F: return KEY_F5;
case 0x40: return KEY_F6;
case 0x41: return KEY_F7;
case 0x42: return KEY_F8;
case 0x43: return KEY_F9;
case 0x44: return KEY_F10;
case 0x57: return KEY_F11;
case 0x58: return KEY_F12;
/* Equals and minus for screen size */
case 0x0D: return KEY_EQUALS;
case 0x0C: return KEY_MINUS;
default:
/* Pass through printable ASCII as lowercase */
if (ascii >= 'a' && ascii <= 'z') return (unsigned char)ascii;
if (ascii >= '0' && ascii <= '9') return (unsigned char)ascii;
return 0;
}
}
/* ---- Poll keyboard and enqueue events ---- */
static void poll_keyboard(void) {
while (_zos_syscall0(SYS_ISKEYAVAILABLE)) {
struct KeyEvent evt;
_zos_syscall1(SYS_GETKEY, (long)&evt);
unsigned char baseSc = evt.scancode & 0x7F; /* strip break bit */
char ascii = 0;
if (baseSc < 128)
ascii = scancode_to_ascii[baseSc];
unsigned char dk = scancode_to_doomkey(baseSc, ascii);
if (dk != 0) {
key_queue_push(evt.pressed ? 1 : 0, dk);
}
}
}
/* ---- DG platform functions ---- */
void DG_Init(void) {
struct FbInfo info;
_zos_syscall1(SYS_FBINFO, (long)&info);
g_fbWidth = (uint32_t)info.width;
g_fbHeight = (uint32_t)info.height;
g_fbPitch = (uint32_t)info.pitch;
g_fbPtr = (uint32_t*)(unsigned long)_zos_syscall0(SYS_FBMAP);
printf("DOOM: framebuffer %ux%u pitch=%u mapped at %p\n",
g_fbWidth, g_fbHeight, g_fbPitch, (void*)g_fbPtr);
}
void DG_DrawFrame(void) {
/* Poll keyboard first */
poll_keyboard();
/* Copy DG_ScreenBuffer (DOOMGENERIC_RESX x DOOMGENERIC_RESY) to framebuffer */
if (g_fbPtr == 0 || DG_ScreenBuffer == 0) return;
uint32_t copyW = DOOMGENERIC_RESX;
uint32_t copyH = DOOMGENERIC_RESY;
if (copyW > g_fbWidth) copyW = g_fbWidth;
if (copyH > g_fbHeight) copyH = g_fbHeight;
uint32_t fbStride = g_fbPitch / 4; /* pixels per scanline */
for (uint32_t y = 0; y < copyH; y++) {
uint32_t* dst = g_fbPtr + y * fbStride;
uint32_t* src = DG_ScreenBuffer + y * DOOMGENERIC_RESX;
memcpy(dst, src, copyW * sizeof(uint32_t));
}
}
void DG_SleepMs(uint32_t ms) {
_zos_syscall1(SYS_SLEEP_MS, (long)ms);
}
uint32_t DG_GetTicksMs(void) {
return (uint32_t)_zos_syscall0(SYS_GETMILLISECONDS);
}
int DG_GetKey(int* pressed, unsigned char* doomKey) {
return key_queue_pop(pressed, doomKey);
}
void DG_SetWindowTitle(const char* title) {
(void)title;
}
/* ---- Entry point ---- */
void _start(void) {
char *argv[] = { "doom", "-iwad", "0:/doom1.wad", 0 };
doomgeneric_Create(3, argv);
for (;;) {
doomgeneric_Tick();
}
}
File diff suppressed because it is too large Load Diff
+374
View File
@@ -0,0 +1,374 @@
/*
* main.cpp
* Manual page viewer for ZenithOS
* Fullscreen pager with ANSI formatting and keyboard navigation
* Copyright (c) 2025 Daniel Hammer
*/
#include <zenith/syscall.h>
#include <zenith/heap.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;
}
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]);
}
}
// ---- Pager rendering ----
static constexpr int MAN_MAX_LINES = 2048;
static int int_to_buf(char* buf, int n) {
if (n == 0) { buf[0] = '0'; return 1; }
char tmp[12];
int i = 0;
while (n > 0) { tmp[i++] = '0' + (n % 10); n /= 10; }
for (int j = 0; j < i; j++) buf[j] = tmp[i - 1 - j];
return i;
}
static void cursor_to(int row, int col) {
char seq[24] = "\033[";
int p = 2;
p += int_to_buf(seq + p, row);
seq[p++] = ';';
p += int_to_buf(seq + p, col);
seq[p++] = 'H';
seq[p] = '\0';
zenith::print(seq);
}
struct ManLine {
const char* text;
int len;
bool isSH;
bool isSS;
bool isBold;
bool isTH;
};
static void man_render(ManLine* lines, int totalLines, int scroll, int rows, int cols,
const char* name, int section) {
int contentRows = rows - 1;
for (int r = 0; r < contentRows; r++) {
cursor_to(r + 1, 1);
zenith::print("\033[2K");
int idx = scroll + r;
if (idx < 0 || idx >= totalLines) continue;
ManLine& ln = lines[idx];
if (ln.isTH) continue;
if (ln.isSH || ln.isSS || ln.isBold) {
zenith::print("\033[1m");
}
if (ln.isSS) {
zenith::print(" ");
}
int maxW = cols;
if (ln.isSS) maxW -= 3;
int printLen = ln.len;
if (printLen > maxW) printLen = maxW;
for (int c = 0; c < printLen; c++) {
zenith::putchar(ln.text[c]);
}
if (ln.isSH || ln.isSS || ln.isBold) {
zenith::print("\033[0m");
}
}
// Status bar
cursor_to(rows, 1);
zenith::print("\033[7m");
zenith::print(" Manual page ");
zenith::print(name);
zenith::putchar('(');
print_int((uint64_t)section);
zenith::putchar(')');
zenith::print(" line ");
print_int((uint64_t)(scroll + 1));
zenith::putchar('/');
print_int((uint64_t)totalLines);
int padCount = cols - 30 - slen(name);
for (int i = 0; i < padCount; i++) zenith::putchar(' ');
zenith::print("\033[0m");
}
// ---- Main ----
extern "C" void _start() {
// Get arguments passed by the shell (e.g. "intro" or "2 syscalls")
char argbuf[256];
zenith::getargs(argbuf, sizeof(argbuf));
const char* arg = skip_spaces(argbuf);
if (*arg == '\0') {
zenith::print("Usage: man <topic>\n");
zenith::print(" man <section> <topic>\n");
zenith::print("Try: man intro\n");
return;
}
// Parse optional section number and topic name
int section = 0;
const char* topic = arg;
if (arg[0] >= '1' && arg[0] <= '9' && arg[1] == ' ') {
section = arg[0] - '0';
topic = skip_spaces(arg + 2);
}
// Try to open man page file
int handle = -1;
int foundSection = 0;
char path[128];
if (section > 0) {
const char* prefix = "0:/man/";
int p = 0;
while (prefix[p]) { path[p] = prefix[p]; p++; }
int t = 0;
while (topic[t] && p < 120) { path[p++] = topic[t++]; }
path[p++] = '.';
path[p++] = '0' + section;
path[p] = '\0';
handle = zenith::open(path);
if (handle >= 0) foundSection = section;
} else {
for (int s = 1; s <= 3; s++) {
const char* prefix = "0:/man/";
int p = 0;
while (prefix[p]) { path[p] = prefix[p]; p++; }
int t = 0;
while (topic[t] && p < 120) { path[p++] = topic[t++]; }
path[p++] = '.';
path[p++] = '0' + s;
path[p] = '\0';
handle = zenith::open(path);
if (handle >= 0) {
foundSection = s;
break;
}
}
}
if (handle < 0) {
zenith::print("No manual entry for ");
zenith::print(topic);
zenith::putchar('\n');
return;
}
// Load entire file into heap
uint64_t fileSize = zenith::getsize(handle);
if (fileSize == 0) {
zenith::close(handle);
zenith::print("Empty manual page.\n");
return;
}
char* fileData = (char*)zenith::malloc(fileSize + 1);
if (fileData == nullptr) {
zenith::close(handle);
zenith::print("Out of memory.\n");
return;
}
uint64_t offset = 0;
while (offset < fileSize) {
uint64_t chunk = fileSize - offset;
if (chunk > 4096) chunk = 4096;
int bytesRead = zenith::read(handle, (uint8_t*)fileData + offset, offset, chunk);
if (bytesRead <= 0) break;
offset += bytesRead;
}
fileData[offset] = '\0';
zenith::close(handle);
// Parse into lines
ManLine* lines = (ManLine*)zenith::malloc(MAN_MAX_LINES * sizeof(ManLine));
if (lines == nullptr) {
zenith::mfree(fileData);
zenith::print("Out of memory.\n");
return;
}
int totalLines = 0;
const char* p = fileData;
while (*p && totalLines < MAN_MAX_LINES) {
const char* lineStart = p;
while (*p && *p != '\n') p++;
int lineLen = (int)(p - lineStart);
if (*p == '\n') p++;
ManLine& ln = lines[totalLines];
ln.isSH = false;
ln.isSS = false;
ln.isBold = false;
ln.isTH = false;
if (starts_with(lineStart, ".TH ")) {
ln.isTH = true;
ln.text = lineStart + 4;
ln.len = lineLen - 4;
} else if (starts_with(lineStart, ".SH ")) {
ln.isSH = true;
ln.text = lineStart + 4;
ln.len = lineLen - 4;
} else if (starts_with(lineStart, ".SS ")) {
ln.isSS = true;
ln.text = lineStart + 4;
ln.len = lineLen - 4;
} else if (starts_with(lineStart, ".B ")) {
ln.isBold = true;
ln.text = lineStart + 3;
ln.len = lineLen - 3;
} else if (starts_with(lineStart, ".BI ")) {
ln.isBold = true;
ln.text = lineStart + 4;
ln.len = lineLen - 4;
} else {
ln.text = lineStart;
ln.len = lineLen;
}
totalLines++;
}
if (totalLines == 0) {
zenith::mfree(lines);
zenith::mfree(fileData);
zenith::print("Empty manual page.\n");
return;
}
// Get terminal dimensions
int cols = 80, rows = 25;
zenith::termsize(&cols, &rows);
// Enter alternate screen, hide cursor
zenith::print("\033[?1049h");
zenith::print("\033[?25l");
int scroll = 0;
int maxScroll = totalLines - (rows - 1);
if (maxScroll < 0) maxScroll = 0;
man_render(lines, totalLines, scroll, rows, cols, topic, foundSection);
// Event loop — yield while waiting for key input
bool running = true;
while (running) {
while (!zenith::is_key_available()) {
zenith::yield();
}
Zenith::KeyEvent ev;
zenith::getkey(&ev);
if (!ev.pressed) continue;
int contentRows = rows - 1;
switch (ev.ascii) {
case 'q':
running = false;
break;
case 'j':
if (scroll < maxScroll) scroll++;
break;
case 'k':
if (scroll > 0) scroll--;
break;
case ' ':
scroll += contentRows;
if (scroll > maxScroll) scroll = maxScroll;
break;
case 'b':
scroll -= contentRows;
if (scroll < 0) scroll = 0;
break;
case 'g':
scroll = 0;
break;
case 'G':
scroll = maxScroll;
break;
default:
// Handle scancodes for special keys
switch (ev.scancode) {
case 0x48: // Up arrow
if (scroll > 0) scroll--;
break;
case 0x50: // Down arrow
if (scroll < maxScroll) scroll++;
break;
case 0x49: // Page Up
scroll -= contentRows;
if (scroll < 0) scroll = 0;
break;
case 0x51: // Page Down
scroll += contentRows;
if (scroll > maxScroll) scroll = maxScroll;
break;
case 0x47: // Home
scroll = 0;
break;
case 0x4F: // End
scroll = maxScroll;
break;
}
break;
}
if (running) {
man_render(lines, totalLines, scroll, rows, cols, topic, foundSection);
}
}
// Restore screen
zenith::print("\033[?25h");
zenith::print("\033[?1049l");
zenith::mfree(lines);
zenith::mfree(fileData);
}
+60 -6
View File
@@ -44,15 +44,17 @@ static void print_int(uint64_t n) {
}
static void prompt() {
zenith::print("zenith> ");
zenith::print("% ");
}
static void cmd_help() {
zenith::print("Available commands:\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 List ramdisk files\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(" uptime Show uptime in milliseconds\n");
zenith::print(" clear Clear the screen\n");
@@ -223,11 +225,52 @@ static void cmd_ping(const char* arg) {
}
}
static void cmd_clear() {
// Print enough newlines to scroll past visible content
for (int i = 0; i < 50; i++) {
zenith::putchar('\n');
static void cmd_run(const char* arg) {
arg = skip_spaces(arg);
if (*arg == '\0') {
zenith::print("Usage: run <filename>\n");
return;
}
// Build path "0:/<filename>"
char path[128];
const char* prefix = "0:/";
int i = 0;
while (prefix[i]) { path[i] = prefix[i]; i++; }
int j = 0;
while (arg[j] && i < 126) { path[i++] = arg[j++]; }
path[i] = '\0';
int pid = zenith::spawn(path);
if (pid < 0) {
zenith::print("Error: failed to spawn '");
zenith::print(arg);
zenith::print("'\n");
} else {
zenith::waitpid(pid);
}
}
static void cmd_man(const char* arg) {
arg = skip_spaces(arg);
if (*arg == '\0') {
zenith::print("Usage: man <topic>\n");
zenith::print(" man <section> <topic>\n");
zenith::print("Try: man intro\n");
return;
}
int pid = zenith::spawn("0:/man.elf", arg);
if (pid < 0) {
zenith::print("Error: failed to start man viewer\n");
} else {
zenith::waitpid(pid);
}
}
static void cmd_clear() {
zenith::print("\033[2J"); // Clear entire screen
zenith::print("\033[H"); // Move cursor to top-left
}
static void process_command(const char* line) {
@@ -241,10 +284,18 @@ static void process_command(const char* line) {
cmd_info();
} else if (streq(line, "ls")) {
cmd_ls();
} 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")) {
@@ -265,7 +316,10 @@ static void process_command(const char* line) {
extern "C" void _start() {
zenith::print("\n");
zenith::print(" ZenithOS Shell v0.1\n");
zenith::print(" ZenithOS\n");
zenith::print(" Copyright (c) 2025-2026 Daniel Hammer\n");
zenith::print("\n");
zenith::print(" Type 'help' for available commands.\n");
zenith::print("\n");