#include <Arduino.h>
#include <TFT_eSPI.h>
#include <vector>
TFT_eSPI tft;
// ========================= CONFIG =========================
#define SCREEN_W 170
#define SCREEN_H 320
#define CHAR_W 6
#define CHAR_H 8
const int TERM_COLS = SCREEN_W / CHAR_W; // ~28
const int TERM_ROWS = SCREEN_H / CHAR_H; // 40
// Colors
#define COL_NORMAL TFT_GREEN
#define COL_PROMPT TFT_WHITE
#define COL_INFO TFT_BLUE
#define COL_ERROR TFT_RED
#define COL_FATAL TFT_PURPLE
#define COL_NOTIFY TFT_YELLOW
// ========================= TERMINAL =========================
struct TermLine {
String text;
uint16_t color;
};
std::vector<TermLine> termBuf;
String inputLine = "";
bool inputReady = false;
void termClear() {
termBuf.clear();
}
void termPrintColor(const String &s, uint16_t color) {
TermLine l;
l.text = s;
l.color = color;
termBuf.push_back(l);
if ((int)termBuf.size() > TERM_ROWS) {
termBuf.erase(termBuf.begin());
}
}
void termPrint(const String &s) {
termPrintColor(s, COL_NORMAL);
}
void termRender() {
tft.fillScreen(TFT_BLACK);
tft.setTextSize(1);
int start = 0;
if ((int)termBuf.size() > TERM_ROWS - 1) {
start = termBuf.size() - (TERM_ROWS - 1);
}
int row = 0;
for (int i = start; i < (int)termBuf.size(); i++) {
tft.setCursor(0, row * CHAR_H);
tft.setTextColor(termBuf[i].color, TFT_BLACK);
tft.print(termBuf[i].text);
row++;
}
// input line
tft.setCursor(0, row * CHAR_H);
tft.setTextColor(COL_PROMPT, TFT_BLACK);
tft.print(inputLine);
}
void handleSerialInput() {
while (Serial.available()) {
char c = Serial.read();
if (c == '\r') continue;
if (c == '\n') {
inputReady = true;
termPrintColor(inputLine, COL_PROMPT);
return;
} else if (c == 8 || c == 127) {
if (inputLine.length() > 0) inputLine.remove(inputLine.length() - 1);
} else {
inputLine += c;
}
}
}
// ========================= VFS (IN-RAM) =========================
struct VFile {
String name;
String content;
};
struct VDir {
String name;
VDir *parent;
std::vector<VDir*> dirs;
std::vector<VFile*> files;
};
VDir *rootDir = nullptr;
VDir *currentDir = nullptr;
VDir* vfsMakeDir(VDir *parent, const String &name) {
VDir *d = new VDir();
d->name = name;
d->parent = parent;
if (parent) parent->dirs.push_back(d);
return d;
}
VFile* vfsMakeFile(VDir *parent, const String &name) {
VFile *f = new VFile();
f->name = name;
f->content = "";
parent->files.push_back(f);
return f;
}
VDir* vfsFindDir(VDir *parent, const String &name) {
if (!parent) return nullptr;
for (auto d : parent->dirs) {
if (d->name == name) return d;
}
return nullptr;
}
VFile* vfsFindFile(VDir *parent, const String &name) {
if (!parent) return nullptr;
for (auto f : parent->files) {
if (f->name == name) return f;
}
return nullptr;
}
void vfsList(VDir *d, bool recursive, int depth = 0) {
if (!d) return;
String indent = "";
for (int i = 0; i < depth; i++) indent += " ";
for (auto dir : d->dirs) {
termPrintColor(indent + "[" + dir->name + "]", COL_NORMAL);
if (recursive) vfsList(dir, true, depth + 1);
}
for (auto f : d->files) {
termPrintColor(indent + f->name, COL_NORMAL);
}
}
String vfsPath(VDir *d) {
if (!d) return "/";
String path = "";
VDir *cur = d;
while (cur) {
if (cur->parent == nullptr) {
path = "/" + path;
break;
}
path = "/" + cur->name + path;
cur = cur->parent;
}
return path;
}
// ========================= OS STATE =========================
enum SystemMode {
MODE_BOOT_SHELL,
MODE_BOOT_ANIM,
MODE_OS_SHELL,
MODE_APP,
MODE_KERNEL,
MODE_FATAL
};
SystemMode mode = MODE_BOOT_SHELL;
unsigned long bootAnimStart = 0;
unsigned long bootAnimDuration = 3000;
String currentPrompt = ">>> ";
String currentApp = "";
bool inApp = false;
// Notifications
struct Notification {
unsigned long triggerMillis;
String message;
bool fired;
};
std::vector<Notification> notifications;
// ========================= HELPERS =========================
std::vector<String> splitWords(const String &line) {
std::vector<String> out;
String cur = "";
for (size_t i = 0; i < line.length(); i++) {
char c = line[i];
if (c == ' ' || c == '\t') {
if (cur.length() > 0) {
out.push_back(cur);
cur = "";
}
} else {
cur += c;
}
}
if (cur.length() > 0) out.push_back(cur);
return out;
}
String joinWords(const std::vector<String> &v, int start) {
String s = "";
for (size_t i = start; i < v.size(); i++) {
if (i > (size_t)start) s += " ";
s += v[i];
}
return s;
}
// ========================= FATAL HANDLER =========================
void fatalCrash(const String &reason) {
mode = MODE_FATAL;
termClear();
termPrintColor("SYSTEM FATAL ERROR", COL_FATAL);
termPrintColor("Reason: " + reason, COL_FATAL);
termPrintColor("Dumping memory (fake)...", COL_FATAL);
termPrintColor("Dumping binary (fake)...", COL_FATAL);
termPrintColor(":(", COL_FATAL);
termPrintColor("THIS COMPUTER HAS EXPERIENCED FATAL ISSUES", COL_FATAL);
termPrintColor("THIS PC HAS PERFORMED ILLEGAL ACTIONS AND WILL BE TERMINATED...", COL_FATAL);
}
// ========================= KERNEL MODE =========================
void kernelHelp() {
termPrintColor("KERNEL COMMANDS:", COL_INFO);
termPrint("SYS_INFO");
termPrint("MEM_DUMP");
termPrint("BIN_DUMP");
termPrint("REBOOT");
termPrint("EXIT_KERNEL");
}
void kernelExec(const String &line) {
auto parts = splitWords(line);
if (parts.size() == 0) return;
String cmd = parts[0];
if (cmd == "SYS_INFO") {
termPrintColor("Kernel: internal debug console", COL_INFO);
termPrintColor("Path: " + vfsPath(currentDir), COL_INFO);
} else if (cmd == "MEM_DUMP") {
termPrintColor("[MEM_DUMP] (placeholder)", COL_INFO);
} else if (cmd == "BIN_DUMP") {
termPrintColor("[BIN_DUMP] (placeholder)", COL_INFO);
} else if (cmd == "REBOOT") {
ESP.restart();
} else if (cmd == "EXIT_KERNEL") {
mode = MODE_OS_SHELL;
currentPrompt = ">>>> ";
termPrintColor("Exiting kernel...", COL_INFO);
} else if (cmd == "HELP") {
kernelHelp();
} else {
termPrintColor("Unknown kernel command", COL_ERROR);
}
}
// ========================= APPS: TIME / DATE =========================
void appTime() {
unsigned long ms = millis();
unsigned long s = ms / 1000;
unsigned long m = s / 60;
unsigned long h = m / 60;
s %= 60;
m %= 60;
char buf[32];
snprintf(buf, sizeof(buf), "TIME %02lu:%02lu:%02lu", h, m, s);
termPrintColor(String(buf), COL_INFO);
}
void appDate() {
termPrintColor("DATE: (no RTC, placeholder)", COL_INFO);
}
// ========================= APPS: WORD / NOTES =========================
// WORD: edit a single file line-by-line
void appWord(const String &filename) {
VFile *f = vfsFindFile(currentDir, filename);
if (!f) f = vfsMakeFile(currentDir, filename);
termPrintColor("[WORD] Editing file: " + filename, COL_INFO);
termPrintColor("Type lines. Type .SAVE on a new line to save and exit.", COL_INFO);
termPrintColor("Current content:", COL_INFO);
termPrint(f->content);
String buffer = "";
while (true) {
handleSerialInput();
if (inputReady) {
String line = inputLine;
inputLine = "";
inputReady = false;
if (line == ".SAVE") {
f->content = buffer;
termPrintColor("[WORD] Saved.", COL_INFO);
break;
} else {
buffer += line + "\n";
termPrintColor(line, COL_PROMPT);
}
termRender();
}
termRender();
delay(10);
}
}
// NOTES: append/view notes in a fixed file "notes.txt"
void appNotes() {
VFile *f = vfsFindFile(currentDir, "notes.txt");
if (!f) f = vfsMakeFile(currentDir, "notes.txt");
termPrintColor("[NOTES] App", COL_INFO);
termPrintColor("Type lines to add notes.", COL_INFO);
termPrintColor("Type .SHOW to view, .EXIT to quit.", COL_INFO);
while (true) {
handleSerialInput();
if (inputReady) {
String line = inputLine;
inputLine = "";
inputReady = false;
if (line == ".EXIT") {
termPrintColor("[NOTES] Exit.", COL_INFO);
break;
} else if (line == ".SHOW") {
termPrintColor("Notes content:", COL_INFO);
termPrint(f->content);
} else {
f->content += line + "\n";
termPrintColor("[NOTES] Added.", COL_INFO);
}
termRender();
}
termRender();
delay(10);
}
}
// ========================= NOTIFICATIONS =========================
void checkNotifications() {
unsigned long now = millis();
for (auto &n : notifications) {
if (!n.fired && now >= n.triggerMillis) {
termPrintColor("[NOTIFY] " + n.message, COL_NOTIFY);
n.fired = true;
}
}
}
// ========================= COMMAND HELP =========================
void cmdHelpBoot() {
termPrintColor("BOOT COMMANDS:", COL_INFO);
termPrint("DIR");
termPrint("DIR /S");
termPrint("CD <dir>");
termPrint("MKDIR <name>");
termPrint("START");
termPrint("DEBUG");
}
void cmdHelpOS() {
termPrintColor("OS COMMANDS:", COL_INFO);
termPrint("HELP");
termPrint("HELP_APP");
termPrint("DIR");
termPrint("DIR /S");
termPrint("CD <dir>");
termPrint("MKDIR <name>");
termPrint("RMDIR <name>");
termPrint("WRITE <file> <text>");
termPrint("APPEND <file> <text>");
termPrint("ADD <a> <b>");
termPrint("SUBTRACT <a> <b>");
termPrint("START_APPNAME");
termPrint("END_APPNAME");
termPrint("NOTIFY <seconds> <message>");
termPrint("TIME");
termPrint("DATE");
termPrint("INFO");
termPrint("REBOOT");
termPrint("DEBUG");
}
void cmdHelpApp() {
termPrintColor("APPS:", COL_INFO);
termPrint("TIME");
termPrint("DATE");
termPrint("WORD");
termPrint("NOTES");
}
// ========================= COMMAND EXECUTION =========================
void execBootCommand(const String &line) {
auto parts = splitWords(line);
if (parts.size() == 0) return;
String cmd = parts[0];
if (cmd == "HELP") {
cmdHelpBoot();
} else if (cmd == "DIR") {
bool recursive = (parts.size() > 1 && parts[1] == "/S");
vfsList(currentDir, recursive);
} else if (cmd == "CD") {
if (parts.size() < 2) {
termPrintColor("Usage: CD <dir>", COL_ERROR);
} else {
String name = parts[1];
if (name == "..") {
if (currentDir->parent) currentDir = currentDir->parent;
} else {
VDir *d = vfsFindDir(currentDir, name);
if (!d) termPrintColor("No such directory", COL_ERROR);
else currentDir = d;
}
}
} else if (cmd == "MKDIR") {
if (parts.size() < 2) {
termPrintColor("Usage: MKDIR <name>", COL_ERROR);
} else {
vfsMakeDir(currentDir, parts[1]);
}
} else if (cmd == "START") {
mode = MODE_BOOT_ANIM;
bootAnimStart = millis();
termClear();
termPrintColor("Starting system...", COL_INFO);
} else if (cmd == "DEBUG") {
mode = MODE_KERNEL;
termPrintColor("Entering kernel...", COL_INFO);
currentPrompt = "ADMIN$>>> ";
} else {
termPrintColor("Unknown command", COL_ERROR);
}
}
void execOSCommand(const String &line) {
auto parts = splitWords(line);
if (parts.size() == 0) return;
String cmd = parts[0];
if (cmd == "HELP") {
cmdHelpOS();
} else if (cmd == "HELP_APP") {
cmdHelpApp();
} else if (cmd == "DIR") {
bool recursive = (parts.size() > 1 && parts[1] == "/S");
vfsList(currentDir, recursive);
} else if (cmd == "CD") {
if (parts.size() < 2) {
termPrintColor("Usage: CD <dir>", COL_ERROR);
} else {
String name = parts[1];
if (name == "..") {
if (currentDir->parent) currentDir = currentDir->parent;
} else {
VDir *d = vfsFindDir(currentDir, name);
if (!d) termPrintColor("No such directory", COL_ERROR);
else currentDir = d;
}
}
} else if (cmd == "MKDIR") {
if (parts.size() < 2) {
termPrintColor("Usage: MKDIR <name>", COL_ERROR);
} else {
vfsMakeDir(currentDir, parts[1]);
}
} else if (cmd == "RMDIR") {
termPrintColor("[RMDIR] not fully implemented", COL_ERROR);
} else if (cmd == "WRITE") {
if (parts.size() < 3) {
termPrintColor("Usage: WRITE <file> <text>", COL_ERROR);
} else {
String name = parts[1];
VFile *f = vfsFindFile(currentDir, name);
if (!f) f = vfsMakeFile(currentDir, name);
String content = joinWords(parts, 2);
f->content = content;
termPrintColor("Written to " + name, COL_INFO);
}
} else if (cmd == "APPEND") {
if (parts.size() < 3) {
termPrintColor("Usage: APPEND <file> <text>", COL_ERROR);
} else {
String name = parts[1];
VFile *f = vfsFindFile(currentDir, name);
if (!f) f = vfsMakeFile(currentDir, name);
String content = joinWords(parts, 2);
f->content += content;
termPrintColor("Appended to " + name, COL_INFO);
}
} else if (cmd == "ADD") {
if (parts.size() < 3) {
termPrintColor("Usage: ADD <a> <b>", COL_ERROR);
} else {
long a = parts[1].toInt();
long b = parts[2].toInt();
long c = a + b;
termPrintColor("Result: " + String(c), COL_INFO);
}
} else if (cmd == "SUBTRACT") {
if (parts.size() < 3) {
termPrintColor("Usage: SUBTRACT <a> <b>", COL_ERROR);
} else {
long a = parts[1].toInt();
long b = parts[2].toInt();
long c = a - b;
termPrintColor("Result: " + String(c), COL_INFO);
}
} else if (cmd.startsWith("START_")) {
String appName = cmd.substring(6);
if (appName == "TIME") appTime();
else if (appName == "DATE") appDate();
else if (appName == "WORD") {
if (parts.size() < 2) {
termPrintColor("Usage: START_WORD <filename>", COL_ERROR);
} else {
String fname = parts[1];
appWord(fname);
}
} else if (appName == "NOTES") {
appNotes();
} else {
termPrintColor("Unknown app", COL_ERROR);
}
} else if (cmd.startsWith("END_")) {
termPrintColor("Apps exit themselves (WORD/NOTES).", COL_INFO);
} else if (cmd == "NOTIFY") {
if (parts.size() < 3) {
termPrintColor("Usage: NOTIFY <seconds> <message>", COL_ERROR);
} else {
unsigned long sec = parts[1].toInt();
String msg = joinWords(parts, 2);
Notification n;
n.triggerMillis = millis() + sec * 1000UL;
n.message = msg;
n.fired = false;
notifications.push_back(n);
termPrintColor("Notification set", COL_NOTIFY);
}
} else if (cmd == "TIME") {
appTime();
} else if (cmd == "DATE") {
appDate();
} else if (cmd == "INFO") {
termPrintColor("System: AharshiOS v0.2", COL_INFO);
termPrintColor("Path: " + vfsPath(currentDir), COL_INFO);
} else if (cmd == "REBOOT") {
ESP.restart();
} else if (cmd == "DEBUG") {
mode = MODE_KERNEL;
termPrintColor("Entering kernel...", COL_INFO);
currentPrompt = "ADMIN$>>> ";
} else {
termPrintColor("Unknown command", COL_ERROR);
}
}
// ========================= BOOT ANIMATION =========================
void updateBootAnim() {
unsigned long now = millis();
unsigned long elapsed = now - bootAnimStart;
if (elapsed > bootAnimDuration) {
mode = MODE_OS_SHELL;
termClear();
currentPrompt = ">>>> ";
termPrintColor("Welcome to AharshiOS", COL_INFO);
return;
}
termClear();
termPrintColor("AharshiOS booting...", COL_INFO);
int width = TERM_COLS - 2;
float progress = (float)elapsed / (float)bootAnimDuration;
if (progress > 1.0f) progress = 1.0f;
int filled = (int)(width * progress);
String bar = "[";
for (int i = 0; i < width; i++) {
bar += (i < filled) ? "#" : " ";
}
bar += "]";
termPrintColor(bar, COL_INFO);
char buf[32];
snprintf(buf, sizeof(buf), "%lu ms", elapsed);
termPrintColor(String(buf), COL_INFO);
}
// ========================= SETUP / LOOP =========================
void setupVFS() {
rootDir = vfsMakeDir(nullptr, "");
currentDir = rootDir;
vfsMakeDir(rootDir, "SYS");
vfsMakeDir(rootDir, "USR");
}
void setup() {
Serial.begin(115200);
tft.init();
tft.setRotation(1); // 170x320
tft.fillScreen(TFT_BLACK);
termClear();
setupVFS();
mode = MODE_BOOT_SHELL;
currentPrompt = ">>> ";
termPrintColor("BOOT TERMINAL (type HELP)", COL_INFO);
}
void loop() {
handleSerialInput();
checkNotifications();
if (mode == MODE_BOOT_ANIM) {
updateBootAnim();
} else if (mode == MODE_FATAL) {
// frozen
} else if (inputReady) {
String line = inputLine;
inputLine = "";
inputReady = false;
if (mode == MODE_BOOT_SHELL) {
execBootCommand(line);
} else if (mode == MODE_OS_SHELL) {
execOSCommand(line);
} else if (mode == MODE_KERNEL) {
kernelExec(line);
}
}
// maintain prompt prefix
if (!inputReady) {
if (mode == MODE_BOOT_SHELL) {
if (!inputLine.startsWith(">>> ")) inputLine = ">>> ";
} else if (mode == MODE_OS_SHELL) {
if (!inputLine.startsWith(">>>> ")) inputLine = ">>>> ";
} else if (mode == MODE_KERNEL) {
if (!inputLine.startsWith("ADMIN$>>> ")) inputLine = "ADMIN$>>> ";
}
}
termRender();
delay(20);
}