Help on Unix project

Hello. I am someone who has been developing for a atmega328p, and am trying to create a embedded Unix subset for it: yes, we need to consider memory limitations, but do you need to see my plan first? I need help implementing it..

***** Embedded Unix Subset (EUS) Project *****

  • Implementation of /dev *
    For /dev/random: Non-blocking random bytes, generated by the C random() function.
    For /dev/console: Write/read to the console buffer.
    For /dev/null: A null device. Any writes get discarded.
    For /dev/fb0: A framebuffer device. We can control 4 pixels per character, so we need to use an ANSI code that
    retreives the terminal's width and height every 5ms; what happens next is we use these characters to control the
    state of the 4 pixels, exposing a API class fb and .draw(int type, ...) - foreground and background colors
    ▖▗ ▘ and ▝. Also, you are allowed to do such things as ▚ and ▞, as well as ▙ ▛ ▜ and ▟.
    For /dev/mem: A raw memory device. Read only for security reasons. If cat is used, only 16 bytes at a time are written to the terminal.
    For /dev/log: A kernel log device. dmesg is cat /dev/log, and will show millis() since boot in the form of [ 0.000000 ] Did x thing in y.
    For /dev/zero: A zero device.
    For /dev/full: Any reads return NULL; but writes return No space left on device.
  • Implementation of /proc *
    For /proc/cpuinfo: I have assembled the cpuinfo data for the target platform, the Arduino Uno Rev3. Store it in flash.
    processor : 0
    vendor_id : Atmel (Microchip)
    cpu family : 8-bit AVR
    model : ATmega328P
    model name : AVR Enhanced RISC
    stepping : 3
    cpu MHz : 16.000
    fpu : no
    wp : yes (lockbits)
    flags : avr_risc multi lpm spm picopower wdt eeprom adc10 usart spi i2c debugwire

For /proc/{0-5}: process 0 is always /sbin/init, which is baked in to flash at runtime (a flash variable) and mounts filesystems. And in each folder: there should be
arch_status cwd limits numa_maps sessionid timens_offsets
attr environ loginuid oom_adj setgroups timers
autogroup exe map_files oom_score smaps timerslack_ns
auxv fd maps oom_score_adj smaps_rollup uid_map
cgroup fdinfo mem pagemap stack wchan
clear_refs gid_map mountinfo personality stat
cmdline io mounts projid_map statm
comm ksm_merging_pages mountstats root status
cpuset latency ns schedstat task
inside of it. And these variables can be small.
For /proc/uptime: millis() converted into nanoseconds returned as file contents. Stored as a unsigned 64 bit integer, NOT a 8-bit integer.

  • Implementation of /bin/sh *
    For each command listed: Strip out any networking utilities. Strip out any archive utilities. Keep everything else. Up to 8 arguments allowed.
    [, [[, acpid, adjtimex, ar, arp, arping, ash, awk, basename,
    blockdev, brctl, bunzip2, bzcat, bzip2, cal, cat, chgrp, chmod,
    chown, chroot, chvt, clear, cmp, cp, cpio, crond, crontab, cttyhack,
    cut, date, dc, dd, deallocvt, depmod, devmem, df, diff, dirname,
    dmesg, dnsdomainname, dos2unix, dpkg, dpkg-deb, du, dumpkmap,
    dumpleases, echo, ed, egrep, env, expand, expr, false, fdisk, fgrep,
    find, fold, free, freeramdisk, fstrim, ftpget, ftpput, getopt,
    getty, grep, groups, gunzip, gzip, halt, head, hexdump, hostid,
    hostname, httpd, hwclock, id, ifconfig, ifdown, ifup, init, insmod,
    ionice, ip, ipcalc, kill, killall, klogd, last, less, ln, loadfont,
    loadkmap, logger, login, logname, logread, losetup, ls, lsmod,
    lzcat, lzma, lzop, lzopcat, md5sum, mdev, microcom, mkdir, mkfifo,
    mknod, mkswap, mktemp, modinfo, modprobe, more, mount, mt, mv,
    nameif, nc, netstat, nslookup, od, openvt, passwd, patch, pidof,
    ping, ping6, pivot_root, poweroff, printf, ps, pwd, rdate, readlink,
    realpath, reboot, renice, reset, rev, rm, rmdir, rmmod, route, rpm,
    rpm2cpio, run-parts, sed, seq, setkeycodes, setsid, sh, sha1sum,
    sha256sum, sha512sum, sleep, sort, start-stop-daemon, stat, strings,
    stty, su, sulogin, swapoff, swapon, switch_root, sync, sysctl,
    syslogd, tac, tail, tar, taskset, tee, telnet, telnetd, test, tftp,
    time, timeout, top, touch, tr, traceroute, traceroute6, true, tty,
    tunctl, udhcpc, udhcpd, umount, uname, uncompress, unexpand, uniq,
    unix2dos, unlzma, unlzop, unxz, unzip, uptime, usleep, uudecode,
    uuencode, vconfig, vi, watch, watchdog, wc, wget, which, who,
    whoami, xargs, xz, xzcat, yes, zcat

  • Implementation of /bin/init and /sbin/init *
    For each: Symlinks to the baked in script.

  • Login *
    Users are stored in /etc/passwd, and the passwords must be hashed and salted.
    And the hostname should be in /etc/hostname.
    Default hostname is box.
    Login prompt: (hostname) login: and a blinking underline cursor..

And, my code for now. ```#include
#include
#include
#include
#include
#include
#include
#include
#include
#include

/**

  • Embedded Unix Subset (EUS) - Host PC Simulation
  • Target: Arduino Uno Rev3 Simulation Logic
    */

// --- Utilities ---
uint64_t get_millis() {
auto now = std::chrono::system_clock::now();
return std::chrono::duration_caststd::chrono::milliseconds(now.time_since_epoch()).count();
}

// --- VFS Base Classes ---
class File {
public:
virtual ~File() {}
virtual std::string read(size_t size) { return ""; }
virtual int write(const std::string& data) { return 0; }
virtual std::string get_name() = 0;
};

class Device : public File {
protected:
std::string name;
public:
Device(std::string n) : name(n) {}
std::string get_name() override { return name; }
};

// --- /dev Implementation ---

class DevNull : public Device {
public:
DevNull() : Device("null") {}
int write(const std::string& data) override { return data.length(); }
std::string read(size_t size) override { return ""; }
};

class DevZero : public Device {
public:
DevZero() : Device("zero") {}
std::string read(size_t size) override { return std::string(size, '\0'); }
};

class DevFull : public Device {
public:
DevFull() : Device("full") {}
int write(const std::string& data) override {
std::cerr << "EUS: /dev/full: No space left on device" << std::endl;
return -1;
}
std::string read(size_t size) override { return ""; }
};

class DevRandom : public Device {
public:
DevRandom() : Device("random") { srand(time(0)); }
std::string read(size_t size) override {
std::string res = "";
for(size_t i=0; i<size; ++i) res += (char)(rand() % 256);
return res;
}
};

class DevLog : public Device {
std::vectorstd::string logs;
public:
DevLog() : Device("log") {
log("Kernel version 0.0.1-eus booting...");
log("Mounting /proc...");
log("Initializing ATmega328P subsystems...");
}
void log(const std::string& msg) {
std::stringstream ss;
double secs = get_millis() / 1000.0;
ss << "[ " << std::fixed << std::setprecision(6) << secs << " ] " << msg;
logs.push_back(ss.str());
}
std::string read(size_t size) override {
std::string res = "";
for(const auto& l : logs) res += l + "\n";
return res;
}
};

// --- Framebuffer /dev/fb0 ---
class DevFB0 : public Device {
public:
DevFB0() : Device("fb0") {}
// Simplified API: in a real implementation, write() would update a buffer
// and a background thread would render the Unicode blocks.
void draw() {
std::cout << "\033[2J\033[H"; // Clear screen
std::cout << "EUS FB0 Render (Mockup using ▖▗ ▘▝ ▚ ▞ ▙ ▛ ▜ ▟)" << std::endl;
std::cout << "▛▀▀▀▀▀▀▀▜" << std::endl;
std::cout << "▌ ▚ ▞ ▐" << std::endl;
std::cout << "▙▄▄▄▄▄▄▄▟" << std::endl;
}
};

// --- /proc Implementation ---

class ProcCPUInfo : public Device {
public:
ProcCPUInfo() : Device("cpuinfo") {}
std::string read(size_t size) override {
return "processor : 0\n"
"vendor_id : Atmel (Microchip)\n"
"cpu family : 8-bit AVR\n"
"model : ATmega328P\n"
"model name : AVR Enhanced RISC\n"
"stepping : 3\n"
"cpu MHz : 16.000\n"
"fpu : no\n"
"wp : yes (lockbits)\n"
"flags : avr_risc multi lpm spm picopower wdt eeprom adc10 usart spi i2c debugwire\n";
}
};

class ProcUptime : public Device {
public:
ProcUptime() : Device("uptime") {}
std::string read(size_t size) override {
uint64_t ns = get_millis() * 1000000ULL;
return std::to_string(ns) + "\n";
}
};

// --- Kernel / Shell Logic ---

class Kernel {
std::map<std::string, File*> vfs;
DevLog* klog;
public:
Kernel() {
klog = new DevLog();
vfs["/dev/null"] = new DevNull();
vfs["/dev/zero"] = new DevZero();
vfs["/dev/full"] = new DevFull();
vfs["/dev/random"] = new DevRandom();
vfs["/dev/log"] = klog;
vfs["/dev/fb0"] = new DevFB0();

    vfs["/proc/cpuinfo"] = new ProcCPUInfo();
    vfs["/proc/uptime"] = new ProcUptime();
    
    // Mock init script
    klog->log("Executing /sbin/init...");
}

void execute(std::string cmd) {
    std::stringstream ss(cmd);
    std::string bin;
    std::vector<std::string> args;
    ss >> bin;
    std::string arg;
    while(ss >> arg) args.push_back(arg);

    if (bin == "cat") {
        for(auto& path : args) {
            if(vfs.count(path)) std::cout << vfs[path]->read(1024);
            else std::cout << "cat: " << path << ": No such file or directory" << std::endl;
        }
    } else if (bin == "dmesg") {
        std::cout << vfs["/dev/log"]->read(0);
    } else if (bin == "ls") {
        for(auto const& [path, file] : vfs) std::cout << path << "  ";
        std::cout << std::endl;
    } else if (bin == "fbtest") {
        static_cast<DevFB0*>(vfs["/dev/fb0"])->draw();
    } else if (bin == "help") {
        std::cout << "EUS Commands: cat, dmesg, ls, fbtest, help, exit" << std::endl;
    } else if (bin != "") {
        std::cout << bin << ": command not found" << std::endl;
    }
}

};

int main() {
Kernel eus;
std::string input;

std::cout << "***** Embedded Unix Subset (EUS) Booting *****" << std::endl;
std::cout << "Target: Arduino Uno Rev3 (Host Simulation)" << std::endl;

while(true) {
    std::cout << "eus# ";
    if(!std::getline(std::cin, input) || input == "exit") break;
    eus.execute(input);
}

return 0;

}```

You need to edit your post and put the three "backticks" on their own line.

Not this...

But this...

Resulting in this...

Same with the closing brace and backticks...

At a minimum unreadable,.

so have you ?

on a 16MHz MCU? does it make any sense ?

std::string on a UNO?

That would eliminate at least:

Archive utilities:
bunzip2, bzcat, bzip, gunzip, gzip, lzcat, lzma, lzop, lzopcat, uncompress, tar, unexpand, unlzma, unlzop, unxz, unzip, xz, xzcat, zcat

Network utilities:
dnsdomainname, ftpget, ftpput, httpd, ifconfig, ifdown, ifup, login, nc, netstat, nslookup, ping, ping6, telnet, telnetd, traceroute, traceroute6, udhcp, udhcpd, wget, whoami

You might also consider eliminating:
swapon, swapoff
As where would you swap to/from?

There may be others that have been missed. Even then, what is left would be more than ambitious in just 32kb of flash and 2kb of RAM memory. The other problem is that there is nowhere to create a filesystem or store any files?

On something like an ESP32 where you can run LittleFS, this might be achievable and apparently it is possible to run Linux on an ESP32-S3-N8R8 and apparently on the Raspberry Pico/Pico2, but on a UNO this is just not going to be realistic.

There was a project to run Linux on an UNO. Maybe you can get some ideas there. There are performance problems :grinning_face:.

That RAM stick in the linked "Worst PC Ever" article with bits or wire soldered on so that it could be plugged into the breadboard must have taken some patience to solder up.

I took the lazy approach and asked AI how much memory it would take to implement this. Here is what I got:
On an Arduino Mega (ATmega2560), you have:

  • Flash: 256 KB total (real usable for your code is a bit less)
  • SRAM: 8 KB
  • EEPROM: 4 KB

What you listed is essentially "most of BusyBox userland". Even with networking and archive utilities stripped, implementing "everything else" with up to 8 arguments is far beyond what fits (and RAM is an even bigger problem than flash).

  1. Flash reality check

A) Just the framework costs
To make a command set like that usable, you need at least:

  • Command dispatcher / shell parser (tokenizer, quoting/escaping rules, redirection, pipes if you want them, error handling)
  • Core I/O and formatting code (printf-like, number parsing, hex, etc.)
  • File system layer (or an emulation of it)
  • Device abstractions (stdin/stdout, block devices, /dev style nodes) if you want things like dd, mount, mknod, etc.

Even a small "toy shell + dispatcher + help" starts eating flash fast because:

  • string tables for command names and help
  • option parsing per applet
  • error messages

B) The commands themselves
Many of those commands are not "small utilities" when implemented correctly:

  • find, grep, awk, sed: these are interpreters / pattern engines
  • tar, gzip, bzip2, xz: compression formats and containers (you said strip archive utilities, but even one of these is huge)
  • mount, fdisk, losetup, mkswap: require block device + filesystem + partition parsing
  • ps, top, dmesg, sysctl: require an OS kernel interface that AVR does not have
  • login, passwd, su: require user database, hashing, permissions model
  • init, start-stop-daemon, crond: require process model (again, OS)

On a microcontroller with no MMU and no Unix kernel, a large fraction of those names have no meaningful implementation unless you also build a whole OS-like environment. That OS-like environment is what costs megabytes, not kilobytes.

  1. A defensible size estimate

If you mean "implement equivalents that behave like BusyBox on Linux":

  • BusyBox typical build sizes on Linux are on the order of hundreds of KB to a few MB depending on features, and that is on architectures with an OS, libc, and filesystem already present.
  • Porting that concept to a bare-metal AVR means you would have to include the missing infrastructure in your firmware image.

A realistic flash number for "most of that list, even after removing networking + archive" is not 256 KB. It is more like:

  • multiple megabytes (very roughly 1 to 5+ MB) for something that feels like a Unix-ish userland, even if simplified.

And the SRAM requirement would also blow past 8 KB quickly:

  • awk/sed/grep/find need buffers
  • line editing needs buffers
  • path manipulation needs buffers
  • filesystem caching needs buffers
  • stack usage for parsers can get large
  1. What could fit on a Mega

If you narrow the scope to a small, practical subset (and accept "microcontroller semantics" rather than Unix semantics), you can fit something useful.

As a rough order of magnitude:

  • Simple command dispatcher + 20-40 small commands (gpio, analogRead, i2c scan, spi, hexdump, mem read/write, simple file read/write on SD, date/time via RTC, etc.)
    • Flash: ~20 KB to 80 KB depending on how fancy
    • SRAM: ~1 KB to 3 KB typical steady-state, more if you do big buffers
  • Adding any of these jumps complexity a lot:
    • grep-like pattern matching: +5 KB to +20 KB (simple) to much more (regex)
    • find-like directory walking on FAT: +5 KB to +20 KB
    • awk/sed equivalents: usually not worth it on AVR

If you want something "shell-like", you can do it, but it will not be that BusyBox list.

  1. Practical alternatives that do match your list

If the real goal is "that command list, but embedded":

  • Use a Linux-capable board (Raspberry Pi, BeagleBone, etc.) and run actual BusyBox.
  • Or use an ESP32-S3 / STM32 / RP2040-class MCU with external PSRAM / large flash and a lightweight RTOS + filesystem, but even then the full list is still basically "run Linux" territory.
  1. Bottom line
  • Flash needed for "everything else on that list" (even removing networking + archive) is in the megabytes.
  • The Arduino Mega has 256 KB flash and 8 KB RAM.
  • So: not feasible as stated.

If you tell me which 20-40 commands you actually care about (for example: cat, ls, rm, cp, hexdump, date, sleep, printf, i2c tools, spi tools, GPIO tools, maybe a small FAT SD layer), I can give you a much tighter flash estimate and a design that will actually fit on a Mega.

I agree with the AI statements and even if you get it will have extremly poor performance. I suggest you pick one of the units that already support LInuc such as Raspberry Pi or Arduino Portenta X8, which comes with Linux preloaded and is designed for high-performance applications.

Thanks for showing me the article. I will test that . . . But yes, I know that even (I am compiling NuttX as I speak for the chip, and hopefully it doesn't blow up...) small unix systems blow up in terms of MEMORY, not flash.

Good luck and let us know how you do. I used Unix/Mac years ago at work and switched to Linux at home when Windoz 95 updated, it was not reliable and I had many other things to put my money in. My home automation runs on Wiondox 95 Dos, has been that way for lots of years. I am in the process of updating it and putting it on a CAN network, that will allow me to remove bundles of low voltage wiring.

Hi! Looks like a window manager has been created, as evidenced. Heavy, buggy as expected, but Build 50 is ready finally.

/*
 * Arduino OS Windowing System API (Build 50)
 * Features: Single Window System, Scrollbars, Dragging.
 * Note: Taskbar and Debugger modules stripped. Background: White.
 * Fixed: Corner labels (Build String) now redraw correctly when occluded.
 */

#include <Arduino.h>

#define BAUD_RATE 500000 
#define BUILD_NUMBER 50

// --- System Constants & API Enums ---
enum DialogType { DLG_ERROR, DLG_WARNING, DLG_QUESTION };

struct Theme {
  int desktop;
  int winFrame;
  int winTitle;
  int winTitleText;
  int clientBg;
  int clientText;
  int scrollBar;
  int scrollTrack;
  int taskbarBg;
  int taskbarTile;
  int taskbarActive;
};

// Background set to white (ANSI 231 or 15)
const Theme LIGHT_THEME = {231, 7, 12, 15, 7, 0, 244, 252, 235, 240, 250}; 

// --- Window Structure ---
struct Window {
  int x, y, w, h;
  char title[20];
  bool active;
  bool minimized;
  bool maximized;
  bool isDialog;
  DialogType dType;
  int oldX, oldY, oldW, oldH;
  float scrollPos; 
};

// --- Global System State ---
Window mainWin = {5, 3, 50, 14, "System Explorer", true, false, false, false, DLG_ERROR, 5, 3, 50, 14, 0.0};
Window diagWin = {20, 7, 32, 8, "System Message", false, false, false, true, DLG_ERROR, 20, 7, 32, 8, 0.0};

Theme currentTheme = LIGHT_THEME;
int lastMouseX = 1, lastMouseY = 1;
int termW = 80, termH = 24;
bool isSystemPanicked = false;

// Dragging State
bool isDragging = false;
int dragOffX = 0, dragOffY = 0;
int ghostX = 0, ghostY = 0; 

bool isScrolling = false;

const char* BUILD_STR = "Arduino (R) MiniOS (R) Build 50";
const char* LOREM_TEXT = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Build 50 UI Engine.";

// --- Core API: System Functions ---

void setCursor(int x, int y) { 
  Serial.print("\033["); Serial.print(y); 
  Serial.print(";"); Serial.print(x); Serial.print("H"); 
}

void resetFormat() { Serial.print("\033[0m"); }

void setSysColor(int fg, int bg = -1) {
  if (fg != -1) { Serial.print("\033[38;5;"); Serial.print(fg); Serial.print("m"); }
  if (bg != -1) { Serial.print("\033[48;5;"); Serial.print(bg); Serial.print("m"); }
}

void drawCornerLabels() {
  int len = strlen(BUILD_STR);
  setSysColor(0, currentTheme.desktop); 
  
  setCursor(1, 1); Serial.print(BUILD_STR);
  setCursor(termW - len + 1, 1); Serial.print(BUILD_STR);
  setCursor(1, termH); Serial.print(BUILD_STR);
  setCursor(termW - len + 1, termH); Serial.print(BUILD_STR);
}

void drawWireframe(int x, int y, int w, int h, bool erase) {
  setSysColor(erase ? 0 : 15, erase ? currentTheme.desktop : -1);
  char edge = erase ? ' ' : ':';
  for(int i=0; i<w; i++) {
    setCursor(x+i, y); Serial.print(edge);
    setCursor(x+i, y+h-1); Serial.print(edge);
  }
  for(int i=1; i<h-1; i++) {
    setCursor(x, y+i); Serial.print(edge);
    setCursor(x+w-1, y+i); Serial.print(edge);
  }
}

void redrawPoint(int x, int y) {
  if (isSystemPanicked || (x == termW && y == termH)) return;

  // 1. Check if window covers this point
  if (mainWin.active && !mainWin.minimized && x >= mainWin.x && x < mainWin.x + mainWin.w && y >= mainWin.y && y < mainWin.y + mainWin.h) {
    int rx = x - mainWin.x;
    int ry = y - mainWin.y;
    setCursor(x, y);
    if (ry == 0) {
      setSysColor(currentTheme.winTitleText, currentTheme.winTitle);
      if (rx == mainWin.w - 2) Serial.print("✕");
      else if (rx == mainWin.w - 4) Serial.print(mainWin.maximized ? "❒" : "☐");
      else if (rx == mainWin.w - 6) Serial.print("-");
      else if (rx > 0 && rx <= (int)strlen(mainWin.title)) Serial.print(mainWin.title[rx-1]);
      else Serial.print(" ");
    } else if (rx == mainWin.w - 1) {
      int thumbPos = 1 + (int)(mainWin.scrollPos * (mainWin.h - 2));
      if (ry == thumbPos) { setSysColor(currentTheme.scrollBar, 7); Serial.print("▐"); }
      else { setSysColor(currentTheme.scrollTrack, 7); Serial.print("▕"); }
    } else {
      setSysColor(currentTheme.clientText, 7);
      int charsPerLine = mainWin.w - 2;
      int totalTextLen = strlen(LOREM_TEXT);
      int maxLines = (totalTextLen + charsPerLine - 1) / charsPerLine;
      int scrollRange = maxLines - (mainWin.h - 1);
      if (scrollRange < 0) scrollRange = 0;
      int startLine = (int)(mainWin.scrollPos * scrollRange);
      
      int charIdx = (startLine + ry - 1) * charsPerLine + (rx - 1);
      if (charIdx >= 0 && charIdx < totalTextLen) Serial.print(LOREM_TEXT[charIdx]);
      else Serial.print(" ");
    }
  } 
  // 2. Check if this is a corner label point
  else {
    int len = strlen(BUILD_STR);
    setCursor(x, y);
    setSysColor(0, currentTheme.desktop);
    
    bool isCorner = false;
    // Top Left / Top Right / Bottom Left / Bottom Right hits
    if (y == 1) {
        if (x >= 1 && x <= len) { Serial.print(BUILD_STR[x-1]); isCorner = true; }
        else if (x >= termW - len + 1 && x <= termW) { Serial.print(BUILD_STR[x - (termW - len + 1)]); isCorner = true; }
    } else if (y == termH) {
        if (x >= 1 && x <= len) { Serial.print(BUILD_STR[x-1]); isCorner = true; }
        else if (x >= termW - len + 1 && x <= termW) { Serial.print(BUILD_STR[x - (termW - len + 1)]); isCorner = true; }
    }
    
    if (!isCorner) Serial.print(" ");
  }
}

void checkResolution() {
  Serial.print("\033[999;999H\033[6n");
  unsigned long start = millis();
  char resBuf[16];
  int rIdx = 0;
  while (millis() - start < 150) {
    if (Serial.available()) {
      char c = Serial.read();
      if (c == 'R') break;
      if ((c >= '0' && c <= '9') || c == ';') resBuf[rIdx++] = c;
    }
  }
  resBuf[rIdx] = '\0';
  String res = String(resBuf);
  int sep = res.indexOf(';');
  if (sep != -1) {
    int newH = res.substring(0, sep).toInt();
    int newW = res.substring(sep + 1).toInt();
    if (newW > 20 && newH > 10 && (newW != termW || newH != termH)) {
      termW = newW; termH = newH;
      refreshAll();
    }
  }
}

void drawWindow(Window &win) {
  if (!win.active || win.minimized) return;
  int tBg = win.isDialog ? (win.dType == DLG_ERROR ? 160 : 27) : currentTheme.winTitle;
  
  setCursor(win.x, win.y);
  setSysColor(15, tBg);
  for(int i=0; i < win.w; i++) {
    if (i == win.w - 2 && !win.isDialog) Serial.print("✕");
    else if (i == win.w - 4 && !win.isDialog) Serial.print(win.maximized ? "❒" : "☐");
    else if (i == win.w - 6 && !win.isDialog) Serial.print("-");
    else if (i > 0 && i <= (int)strlen(win.title)) Serial.print(win.title[i-1]);
    else Serial.print(" ");
  }

  int charsPerLine = win.w - 2;
  int totalTextLen = strlen(LOREM_TEXT);
  int maxLines = (totalTextLen + charsPerLine - 1) / charsPerLine;
  int scrollRange = maxLines - (win.h - 1);
  if (scrollRange < 0) scrollRange = 0;
  int startLine = (int)(win.scrollPos * scrollRange);

  for(int i=1; i < win.h; i++) {
    if (win.y + i > termH) break;
    setCursor(win.x, win.y + i);
    setSysColor(0, 7);
    int lineStartIdx = (startLine + i - 1) * charsPerLine;
    for(int j=0; j < win.w - 1; j++) {
      int charIdx = lineStartIdx + j;
      if (!win.isDialog && charIdx >= 0 && charIdx < totalTextLen) Serial.print(LOREM_TEXT[charIdx]);
      else Serial.print(" ");
    }
    int thumbPos = 1 + (int)(win.scrollPos * (win.h - 2));
    if (i == thumbPos) { setSysColor(currentTheme.scrollBar, 7); Serial.print("▐"); }
    else { setSysColor(currentTheme.scrollTrack, 7); Serial.print("▕"); }
  }
}

void refreshAll() {
  if (isSystemPanicked) return;
  resetFormat();
  Serial.print("\033[2J\033[H");
  setSysColor(0, currentTheme.desktop);
  for(int i=0; i < termH; i++) {
    setCursor(1, i+1);
    for(int j=0; j < termW; j++) Serial.print(" ");
  }
  drawCornerLabels();
  drawWindow(mainWin);
  if (diagWin.active) drawWindow(diagWin);
}

void handleInput() {
  static char buf[64];
  static int idx = 0;
  while (Serial.available() > 0) {
    char c = Serial.read();
    buf[idx++] = c;
    if (c == 'M' || c == 'm' || (c >= 'a' && c <= 'z')) {
      buf[idx] = '\0';
      if (buf[0] == '\033') processMouse(buf);
      idx = 0;
    }
    if (idx >= 63) idx = 0;
  }
}

void processMouse(char* seq) {
  String s = String(seq);
  int f = s.indexOf(';');
  int sec = s.indexOf(';', f + 1);
  if (f == -1 || sec == -1) return;

  int btn = s.substring(s.indexOf('<') + 1, f).toInt();
  int mx = s.substring(f + 1, sec).toInt();
  int my = s.substring(sec + 1, s.length() - 1).toInt();
  bool isRelease = s.endsWith("m");

  if (mx != lastMouseX || my != lastMouseY) {
    if (isDragging) {
      drawWireframe(ghostX, ghostY, mainWin.w, mainWin.h, true);
      ghostX = mx - dragOffX; ghostY = my - dragOffY;
      drawWireframe(ghostX, ghostY, mainWin.w, mainWin.h, false);
    } else if (isScrolling) {
      float newPos = (float)(my - mainWin.y - 1) / (mainWin.h - 2);
      if (newPos < 0) newPos = 0; if (newPos > 1.0) newPos = 1.0;
      if (newPos != mainWin.scrollPos) { mainWin.scrollPos = newPos; drawWindow(mainWin); }
    } else {
      redrawPoint(lastMouseX, lastMouseY);
    }
    lastMouseX = mx; lastMouseY = my;
    setCursor(mx, my);
    if (!(mx == termW && my == termH)) { setSysColor(11, -1); Serial.print("🮰"); }
  }

  if (btn == 0 && !isRelease) {
    if (my == mainWin.y && mx >= mainWin.x && mx < mainWin.x + mainWin.w) {
      int rx = mx - mainWin.x;
      if (rx == mainWin.w - 2) { 
        mainWin.active = false;
        refreshAll();
      } else if (rx == mainWin.w - 4) { 
        mainWin.maximized = !mainWin.maximized;
        if (mainWin.maximized) {
          mainWin.oldX = mainWin.x; mainWin.oldY = mainWin.y;
          mainWin.oldW = mainWin.w; mainWin.oldH = mainWin.h;
          mainWin.x = 1; mainWin.y = 1; mainWin.w = termW; mainWin.h = termH;
        } else {
          mainWin.x = mainWin.oldX; mainWin.y = mainWin.oldY;
          mainWin.w = mainWin.oldW; mainWin.h = mainWin.oldH;
        }
        refreshAll();
      } else if (rx == mainWin.w - 6) { 
        mainWin.minimized = true;
        refreshAll();
      } else if (rx < mainWin.w - 8 && !mainWin.maximized) { 
        isDragging = true;
        dragOffX = mx - mainWin.x;
        dragOffY = my - mainWin.y;
        ghostX = mainWin.x; ghostY = mainWin.y;
        drawWireframe(ghostX, ghostY, mainWin.w, mainWin.h, false);
      }
    }
    else if (mx == mainWin.x + mainWin.w - 1 && my > mainWin.y && my < mainWin.y + mainWin.h) {
      isScrolling = true;
    }
  } else if (isRelease) {
    if (isDragging) {
      isDragging = false;
      drawWireframe(ghostX, ghostY, mainWin.w, mainWin.h, true); 
      mainWin.x = ghostX; mainWin.y = ghostY;
      refreshAll(); 
    }
    isScrolling = false;
  }
}

void setup() {
  Serial.begin(BAUD_RATE);
  delay(500);
  Serial.print("\033[?1003h\033[?1006h\033[?25l");
  checkResolution();
  refreshAll();
}

void loop() {
  handleInput();
  static unsigned long lastResCheck = 0;
  if (millis() - lastResCheck > 4000) {
    checkResolution();
    lastResCheck = millis();
  }
}

Did you forget to use code tags when you posted your sketch ?

I tried to format it, but it kept complaining, even when I did what it said. I can provide a online version of it (this is build 55, but it is updated)

Looks like this project is unfeasible; another was started.

You need to put a carriage return before the ``` so they are on their own line. That will fix it.

I see. This piece of help is answered, and I created a separate post, now that I have a API base to build on.

Both ``` marks need to be on their own line... one just before your code, and one just after your code. Your code is still not inside a code block.

Still tried, and it gives me the same error: Do I need ``` on EVERY single line?