/* * main.cpp * Interactive shell for MontaukOS * Copyright (c) 2025 Daniel Hammer */ #include #include using montauk::slen; using montauk::streq; using montauk::starts_with; using montauk::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++; } dst[dLen + i] = '\0'; } // Current working directory (relative to N:/). // "" = root, "man" = 0:/man, "man/sub" = 0:/man/sub static char cwd[128] = ""; static int current_drive = 0; // Build VFS path with explicit drive number static void build_drive_path(int drive, const char* dir, char* out, int outMax) { int i = 0; if (drive >= 10) { out[i++] = '0' + drive / 10; } out[i++] = '0' + drive % 10; out[i++] = ':'; out[i++] = '/'; if (dir && dir[0]) { int j = 0; while (dir[j] && i < outMax - 1) out[i++] = dir[j++]; } out[i] = '\0'; } // Build VFS directory path: "N:/" or "N:/" using current_drive static void build_dir_path(const char* dir, char* out, int outMax) { build_drive_path(current_drive, dir, out, outMax); } // Parse drive number from a drive prefix like "0:" or "12:". Returns -1 if invalid. static int parse_drive_prefix(const char* s) { if (s[0] < '0' || s[0] > '9') return -1; int n = s[0] - '0'; if (s[1] >= '0' && s[1] <= '9') { n = n * 10 + (s[1] - '0'); if (s[2] != ':') return -1; } else if (s[1] == ':') { // single digit drive } else { return -1; } return n; } // Length of drive prefix ("0:" = 2, "12:" = 3). Assumes valid prefix. static int drive_prefix_len(const char* s) { if (s[1] >= '0' && s[1] <= '9') return 3; // e.g. "12:" return 2; // e.g. "0:" } // ---- 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() { char drv[4]; if (current_drive >= 10) { drv[0] = '0' + current_drive / 10; drv[1] = '0' + current_drive % 10; drv[2] = '\0'; } else { drv[0] = '0' + current_drive; drv[1] = '\0'; } montauk::print(drv); montauk::print(":/"); if (cwd[0]) montauk::print(cwd); montauk::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++) montauk::putchar('\b'); for (int i = 0; i < len; i++) montauk::putchar(' '); for (int i = 0; i < len; i++) montauk::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]; montauk::putchar(newContent[i]); } line[newLen] = '\0'; *pos = newLen; } // ---- Builtin: help ---- static void cmd_help() { montauk::print("Shell builtins:\n"); montauk::print(" help Show this help message\n"); montauk::print(" ls [dir] List files in directory\n"); montauk::print(" cd [dir] Change working directory\n"); montauk::print(" N: Switch to drive N (e.g. 1:)\n"); montauk::print(" exit Exit the shell\n"); montauk::print("\n"); montauk::print("System commands:\n"); montauk::print(" man View manual pages\n"); montauk::print(" cat Display file contents\n"); montauk::print(" edit [file] Text editor\n"); montauk::print(" info Show system information\n"); montauk::print(" date Show current date and time\n"); montauk::print(" uptime Show uptime\n"); montauk::print(" clear Clear the screen\n"); montauk::print(" fontscale [n] Set terminal font scale (1-8)\n"); montauk::print(" reset Reboot the system\n"); montauk::print(" shutdown Shut down the system\n"); montauk::print("\n"); montauk::print("Network commands:\n"); montauk::print(" ping Send ICMP echo requests\n"); montauk::print(" nslookup DNS lookup\n"); montauk::print(" ifconfig Show/set network configuration\n"); montauk::print(" tcpconnect Connect to a TCP server\n"); montauk::print(" irc IRC client\n"); montauk::print(" dhcp DHCP client\n"); montauk::print(" fetch HTTP client\n"); montauk::print(" httpd HTTP server\n"); montauk::print("\n"); montauk::print("Games:\n"); montauk::print(" doom DOOM\n"); montauk::print("\n"); montauk::print("Any .elf on the ramdisk is executable.\n"); } // Check if a string already has a VFS drive prefix (e.g. "0:/" or "12:/") static bool has_drive_prefix(const char* s) { return parse_drive_prefix(s) >= 0; } // ---- Builtin: ls ---- static void cmd_ls(const char* arg) { arg = skip_spaces(arg); // Build the target directory (relative path from root) char dir[128]; int drive = current_drive; if (*arg) { if (has_drive_prefix(arg)) { // Absolute VFS path: "N:/something" drive = parse_drive_prefix(arg); int plen = drive_prefix_len(arg); scopy(dir, arg + plen + 1, sizeof(dir)); // skip "N:/" } else if (arg[0] == '/') { // Absolute path from root: "/something" scopy(dir, arg + 1, sizeof(dir)); } else if (cwd[0]) { // Relative path with CWD scopy(dir, cwd, sizeof(dir)); scat(dir, "/", sizeof(dir)); scat(dir, arg, sizeof(dir)); } else { // Relative path at root scopy(dir, arg, sizeof(dir)); } } else { // ls with no arg -- use cwd scopy(dir, cwd, sizeof(dir)); } char path[128]; build_drive_path(drive, dir, path, sizeof(path)); const char* entries[64]; int count = montauk::readdir(path, entries, 64); if (count <= 0) { montauk::print("(empty)\n"); return; } // Prefix to strip: "dir/" (if dir is non-empty) int prefixLen = 0; if (dir[0]) prefixLen = slen(dir) + 1; for (int i = 0; i < count; i++) { montauk::print(" "); if (prefixLen > 0 && starts_with(entries[i], dir)) { montauk::print(entries[i] + prefixLen); } else { montauk::print(entries[i]); } montauk::putchar('\n'); } } // ---- Builtin: cd ---- // Switch to a drive. Returns true if valid. static bool switch_drive(int drive) { // Validate drive exists by trying to readdir its root char path[8]; build_drive_path(drive, "", path, sizeof(path)); const char* entries[1]; if (montauk::readdir(path, entries, 1) < 0) return false; current_drive = drive; cwd[0] = '\0'; return true; } static void cmd_cd(const char* arg) { arg = skip_spaces(arg); // 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 if (streq(arg, "..")) { int len = slen(cwd); int last = -1; for (int i = 0; i < len; i++) { if (cwd[i] == '/') last = i; } if (last >= 0) { cwd[last] = '\0'; } else { cwd[0] = '\0'; } return; } // cd /path -> absolute path from root if (arg[0] == '/') { arg++; if (*arg == '\0') { cwd[0] = '\0'; return; } char path[128]; build_dir_path(arg, path, sizeof(path)); const char* entries[1]; if (montauk::readdir(path, entries, 1) < 0) { montauk::print("cd: no such directory: "); montauk::print(arg); montauk::putchar('\n'); return; } scopy(cwd, arg, sizeof(cwd)); return; } // cd N:/ or cd N:/path -> switch drive and optionally cd into path if (has_drive_prefix(arg)) { int drive = parse_drive_prefix(arg); int plen = drive_prefix_len(arg); const char* rel = arg + plen; // points at ":/" or ":" if (*rel == '/') rel++; // skip the '/' // Validate drive exists char rootPath[8]; build_drive_path(drive, "", rootPath, sizeof(rootPath)); const char* rootEntries[1]; if (montauk::readdir(rootPath, rootEntries, 1) < 0) { montauk::print("cd: no such drive: "); montauk::print(arg); montauk::putchar('\n'); return; } // If there's a path after the drive, validate it if (*rel != '\0') { char path[128]; build_drive_path(drive, rel, path, sizeof(path)); const char* entries[1]; if (montauk::readdir(path, entries, 1) < 0) { montauk::print("cd: no such directory: "); montauk::print(arg); montauk::putchar('\n'); return; } current_drive = drive; scopy(cwd, rel, sizeof(cwd)); } else { current_drive = drive; cwd[0] = '\0'; } return; } // Build target directory path (relative to CWD) char target[128]; if (cwd[0]) { scopy(target, cwd, sizeof(target)); scat(target, "/", sizeof(target)); scat(target, arg, sizeof(target)); } else { scopy(target, arg, sizeof(target)); } // Validate: try readdir on the target char path[128]; build_dir_path(target, path, sizeof(path)); const char* entries[1]; int count = montauk::readdir(path, entries, 1); if (count < 0) { montauk::print("cd: no such directory: "); montauk::print(arg); montauk::putchar('\n'); return; } // Set cwd scopy(cwd, target, sizeof(cwd)); } // ---- Builtin: man ---- static void cmd_man(const char* arg) { arg = skip_spaces(arg); if (*arg == '\0') { montauk::print("Usage: man \n"); montauk::print(" man
\n"); montauk::print("Try: man intro\n"); return; } int pid = montauk::spawn("0:/os/man.elf", arg); if (pid < 0) { montauk::print("Error: failed to start man viewer\n"); } else { montauk::waitpid(pid); } } // ---- 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 = montauk::open(path); if (h < 0) return false; montauk::close(h); int pid = montauk::spawn(path, args); if (pid < 0) return false; montauk::waitpid(pid); return true; } // 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; } 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] || current_drive != 0) && !has_drive_prefix(tokStart) && tokStart[0] != '-'; if (resolve) { // Build candidate resolved path and check if the file exists char candidate[256]; int r = 0; if (current_drive >= 10) candidate[r++] = '0' + current_drive / 10; candidate[r++] = '0' + current_drive % 10; candidate[r++] = ':'; candidate[r++] = '/'; int j = 0; while (cwd[j] && r < 255) candidate[r++] = cwd[j++]; if (cwd[0] && r < 255) candidate[r++] = '/'; for (int k = 0; k < tokLen && r < 255; k++) candidate[r++] = tokStart[k]; candidate[r] = '\0'; // Use the resolved path if the file exists. If it doesn't // exist, still use the resolved path when the token looks // like a filename (contains a dot) so that programs creating // new files get the correct drive/directory prefix. int h = montauk::open(candidate); if (h >= 0) { montauk::close(h); for (int k = 0; k < r && o < outMax - 1; k++) out[o++] = candidate[k]; } else { bool looksLikeFile = false; for (int k = 0; k < tokLen; k++) { if (tokStart[k] == '.') { looksLikeFile = true; break; } } if (looksLikeFile) { for (int k = 0; k < r && o < outMax - 1; k++) out[o++] = candidate[k]; } else { 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 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; // Always search drive 0 for system commands (os/, games/) // 1. Try 0:/os/.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/.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 N://.elf on current drive (if cwd is set) if (cwd[0]) { build_drive_path(current_drive, "", path, 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 N:/.elf on current drive build_drive_path(current_drive, "", path, sizeof(path)); scat(path, cmd, sizeof(path)); scat(path, ".elf", sizeof(path)); if (try_exec(path, finalArgs)) return; // 5. If on a non-zero drive, also try 0:/.elf if (current_drive != 0) { scopy(path, "0:/", sizeof(path)); scat(path, cmd, sizeof(path)); scat(path, ".elf", sizeof(path)); if (try_exec(path, finalArgs)) return; } // Not found montauk::print(cmd); montauk::print(": command not found\n"); } // ---- Command dispatch ---- static void process_command(const char* line) { line = skip_spaces(line); if (*line == '\0') return; // 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; } // Bare drive switch: "1:", "2:", etc. if (has_drive_prefix(cmd) && cmd[drive_prefix_len(cmd)] == '\0' && args == nullptr) { int drive = parse_drive_prefix(cmd); if (!switch_drive(drive)) { montauk::print("No such drive: "); montauk::print(cmd); montauk::print("/\n"); } return; } // Builtins if (streq(cmd, "help")) { cmd_help(); } 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")) { montauk::print("Goodbye.\n"); montauk::exit(0); } else { // 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() { montauk::print("\n"); montauk::print(" MontaukOS\n"); montauk::print(" Copyright (c) 2025-2026 Daniel Hammer\n"); montauk::print("\n"); montauk::print(" Type 'help' for available commands.\n"); montauk::print("\n"); char line[256]; int pos = 0; int hist_nav = -1; // -1 = not navigating history prompt(); while (true) { if (!montauk::is_key_available()) { montauk::yield(); continue; } Montauk::KeyEvent ev; montauk::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') { montauk::putchar('\n'); line[pos] = '\0'; history_add(line); process_command(line); pos = 0; hist_nav = -1; prompt(); } else if (ev.ascii == '\b') { if (pos > 0) { pos--; montauk::putchar('\b'); montauk::putchar(' '); montauk::putchar('\b'); } } else if (ev.ascii >= ' ' && pos < 255) { line[pos++] = ev.ascii; montauk::putchar(ev.ascii); } } }