#include "FS.h"
#include "SD.h"
#include "SPI.h"
#include <TFT_eSPI.h>
TFT_eSPI tft = TFT_eSPI();
#define HISTORY_SIZE 10
#define TFT_LINE_HEIGHT 10
String commandHistory[HISTORY_SIZE];
int historyIndex = 0;
int historyCount = 0;
String currentDir = "/";
String inputLine = "";
int tftY = 0;
// --- Utility: resolve absolute path ---
String resolvePath(String path) {
String fullPath = path.startsWith("/") ? path : (currentDir.endsWith("/") ? currentDir + path : currentDir + "/" + path);
String result = "";
int start = 0;
while (start < fullPath.length()) {
int slash = fullPath.indexOf('/', start);
if (slash == -1) slash = fullPath.length();
String part = fullPath.substring(start, slash);
start = slash + 1;
if (part == "" || part == ".") continue;
else if (part == "..") {
int lastSlash = result.lastIndexOf('/');
if (lastSlash >= 0) result = result.substring(0, lastSlash);
else result = "";
} else {
result += "/" + part;
}
}
if (result == "") result = "/";
return result;
}
// --- File operations ---
void listSimple(fs::FS &fs, const char * dirname) {
File root = fs.open(dirname);
if (!root || !root.isDirectory()) {
Serial.println("Error: cannot open directory");
tftPrintLn("Error: cannot open directory");
return;
}
File file = root.openNextFile();
while (file) {
String line = "";
if (file.isDirectory()) line = file.name();
else line = String(file.name()) + " (" + String(file.size()) + " bytes)";
Serial.println(line);
tftPrintLn(line);
file.close();
file = root.openNextFile();
}
}
void deleteDirRecursive(fs::FS &fs, const char * path)
{
File dir = fs.open(path);
if (!dir || !dir.isDirectory()) {
Serial.println("Error: directory not found");
tftPrintLn("Error: directory not found");
return;
}
File file = dir.openNextFile();
while (file) {
//String fullPath = String(path) + "/" + file.name();
String fullPath = String(path);
fullPath += "/";
fullPath += file.name();
if (file.isDirectory()) {
file.close();
deleteDirRecursive(fs, fullPath.c_str());
}
else {
file.close();
if (!fs.remove(fullPath.c_str())) {
Serial.println("Error: cannot delete " + fullPath);
tftPrintLn("Error: cannot delete " + fullPath);
}
}
file = dir.openNextFile();
}
dir.close();
// Now remove the empty directory itself
if (!fs.rmdir(path)) {
Serial.println("Error: cannot remove directory " + String(path));
tftPrintLn("Error: cannot remove directory " + String(path));
}
}
void printTree(fs::FS &fs, const char * dirname, String prefix = "")
{
File root = fs.open(dirname);
if (!root || !root.isDirectory()) {
Serial.println("Error: cannot open directory");
tftPrintLn("Error: cannot open directory");
return;
}
// --- Count entries ---
int count = 0;
File temp = root.openNextFile();
while (temp) {
count++;
temp.close(); // ✅ CLOSE TEMP
temp = root.openNextFile();
}
root.rewindDirectory();
// --- Print entries ---
int index = 0;
File file = root.openNextFile();
while (file) {
index++;
bool isLast = (index == count);
String name = file.name(); // store before closing
bool isDir = file.isDirectory();
size_t size = file.size();
String line = prefix + (isLast ? "└── " : "├── ") + name;
if (!isDir) line += " (" + String(size) + " bytes)";
Serial.println(line);
tftPrintLn(line);
if (isDir) {
String newPrefix = prefix + (isLast ? " " : "│ ");
file.close(); // ✅ CLOSE BEFORE RECURSION
printTree(fs, (String(dirname) + "/" + name).c_str(), newPrefix);
}
else {
file.close(); // ✅ CLOSE NORMAL FILE
}
file = root.openNextFile();
}
root.close(); // ✅ CLOSE ROOT
}
void statFile(fs::FS &fs, const char * path)
{
File file = fs.open(path);
if(!file) {
Serial.println("Error: file not found");
tftPrintLn("Error: file not found");
return;
}
String name = String(file.name());
String type = file.isDirectory() ? "Directory" : "File";
Serial.println("Name: " + name);
tftPrintLn("Name: " + name);
Serial.println("Path: " + String(path));
tftPrintLn("Path: " + String(path));
Serial.println("Type: " + type);
tftPrintLn("Type: " + type);
if(!file.isDirectory()) {
String sizeStr = String(file.size()) + " bytes";
Serial.println("Size: " + sizeStr);
tftPrintLn("Size: " + sizeStr);
}
file.close();
}
void createDir(fs::FS &fs, const char * path) { if (!fs.mkdir(path)) { Serial.println("Error: mkdir failed"); tftPrintLn("Error: mkdir failed"); } }
void removeDir(fs::FS &fs, const char * path) { if (!fs.rmdir(path)) { Serial.println("Error: rmdir failed"); tftPrintLn("Error: rmdir failed"); } }
void readFile(fs::FS &fs, const char * path) {
File file = fs.open(path);
if(!file) { Serial.println("Error: cannot read file"); tftPrintLn("Error: cannot read file"); return; }
String line = "";
while(file.available()) {
char c = file.read();
line += c;
if(c == '\n') {
Serial.println(line);
tftPrintLn(line);
line = "";
}
}
if(line.length() > 0) { Serial.println(line); tftPrintLn(line); } // remaining text
file.close();
}
void writeFile(fs::FS &fs, const char * path, const char * message) {
File f = fs.open(path, FILE_WRITE);
if(!f) {
Serial.println("Error: write failed");
tftPrintLn("Error: write failed");
return;
}
if (strlen(message) > 0) {
if (!f.print(message)) {
Serial.println("Error: write failed");
tftPrintLn("Error: write failed");
}
}
f.close();
}
void appendFile(fs::FS &fs, const char * path, const char * message) {
File f = fs.open(path, FILE_APPEND);
if(!f || !f.print(message)) { Serial.println("Error: append failed"); tftPrintLn("Error: append failed"); }
if(f) f.close();
}
void deleteFile(fs::FS &fs, const char * path) { if (!fs.remove(path)) { Serial.println("Error: delete failed"); tftPrintLn("Error: delete failed"); } }
bool movePath(fs::FS &fs, const char * from, const char * to) {
if (fs.rename(from, to)) return true;
// fallback copy+delete
File src = fs.open(from);
if (!src) return false;
File dst = fs.open(to, FILE_WRITE);
if (!dst) {
src.close();
return false;
}
while (src.available()) {
dst.write(src.read());
}
src.close();
dst.close();
return fs.remove(from);
}
bool copyFile(fs::FS &fs, const char * from, const char * to) {
File src = fs.open(from);
if (!src || src.isDirectory()) return false;
File dst = fs.open(to, FILE_WRITE);
if (!dst) {
src.close();
return false;
}
while (src.available()) {
dst.write(src.read());
}
src.close();
dst.close();
return true;
}
// --- TFT printing helpers ---
void tftPrintLn(const String &txt) {
tft.setCursor(0, tftY);
tft.println(txt);
tftY += TFT_LINE_HEIGHT;
if (tftY >= tft.height()) { tft.fillScreen(TFT_BLACK); tftY = 0; }
}
void redrawInputLine(const String &input) {
tft.fillRect(0, tftY, tft.width(), TFT_LINE_HEIGHT, TFT_BLACK); // clear only current input line
tft.setCursor(0, tftY);
tft.print(currentDir + "> " + input);
}
// --- Command handler ---
void handleCommand(String cmdLine);
// --- Setup ---
void setup() {
Serial.begin(115200);
// Initialize SD card first
if (!SD.begin(15)) {
Serial.println("Error: SD card mount failed");
tft.fillScreen(TFT_BLACK);
tft.setCursor(0,0);
tft.println("Error: SD mount failed");
while (1);
}
// TFT init
tft.init();
tft.setRotation(1);
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_WHITE, TFT_BLACK);
tft.setTextSize(1);
Serial.println("CYD OS Shell Ready");
tftPrintLn("CYD OS Shell Ready");
Serial.print(currentDir); Serial.print("> ");
tft.setCursor(0, tftY); tft.print(currentDir); tft.print("> ");
}
// --- Execute commands with && chaining ---
void executeChained(String line) {
int start = 0;
while (start < line.length()) {
int idx = line.indexOf("&&", start);
String part;
if (idx == -1) { part = line.substring(start); start = line.length(); }
else { part = line.substring(start, idx); start = idx + 2; }
part.trim();
if (part != "") handleCommand(part);
}
}
// --- Main loop ---
void loop() {
while(Serial.available()) {
char c = Serial.read();
if (c == '\n') {
inputLine.trim();
if(inputLine != "") {
// Save to history
commandHistory[historyIndex] = inputLine;
historyIndex = (historyIndex + 1) % HISTORY_SIZE;
if(historyCount < HISTORY_SIZE) historyCount++;
Serial.println(inputLine);
tftPrintLn(currentDir + "> " + inputLine);
executeChained(inputLine);
inputLine = "";
}
Serial.print(currentDir); Serial.print("> ");
tft.setCursor(0, tftY); tft.print(currentDir); tft.print("> ");
}
else if(c != '\r') {
inputLine += c;
redrawInputLine(inputLine);
}
}
}
// --- Actual command execution ---
void handleCommand(String cmdLine) {
cmdLine.trim();
if (cmdLine == "") return;
// HELP
if (cmdLine == "help")
{
String helpTxt[] = {
"help - Show this help menu",
"history - Show command history",
"clear - Clear screen",
"ls [dir] - List files in directory",
"tree - Show directory tree",
"cd <dir> - Change directory",
"pwd - Show current directory",
"",
"mkdir <dir> - Create directory",
"rmdir <dir> - Remove EMPTY directory",
"mv <src> <dst> - Move or rename file/directory",
"cp <src> <dst> - Copy file",
"rm <file> - Delete file",
"rm -r <dir> - Recursively delete directory",
"touch <file> - Create empty file",
"",
"cat <file> - Show file contents",
"write <f> <text> - Overwrite file with text",
"append <f> <text> - Append text to file",
"",
"echo <text> - Print text",
"echo <text> > f - Write text to file",
"echo <text> >> f - Append text to file"
};
for (auto line : helpTxt) {
Serial.println(line);
tftPrintLn(line);
}
return;
}
// CLEAR
if (cmdLine == "clear") {
tft.fillScreen(TFT_BLACK);
tftY = 0;
inputLine = "";
Serial.println("\n\n--- CLEAR ---\n");
tft.setCursor(0, tftY);
return;
}
// PWD
if (cmdLine == "pwd") {
Serial.println(currentDir);
tftPrintLn(currentDir);
return;
}
// HISTORY
if (cmdLine == "history") {
for(int i=0;i<historyCount;i++){
int pos = (historyIndex - historyCount + i + HISTORY_SIZE)%HISTORY_SIZE;
Serial.println(commandHistory[pos]);
tftPrintLn(commandHistory[pos]);
}
return;
}
// LS
if (cmdLine.startsWith("ls")) {
String arg = cmdLine.length() > 2 ? cmdLine.substring(3) : currentDir;
arg.trim(); if(arg=="") arg=currentDir;
listSimple(SD, resolvePath(arg).c_str());
return;
}
// TREE
if (cmdLine == "tree") { printTree(SD, currentDir.c_str()); return; }
// CD
if (cmdLine.startsWith("cd ")) {
String arg = resolvePath(cmdLine.substring(3));
File dir = SD.open(arg.c_str());
if (dir && dir.isDirectory()) { currentDir = arg; }
else { Serial.println("Error: directory not found"); tftPrintLn("Error: directory not found"); }
return;
}
// FILE COMMANDS
if (cmdLine.startsWith("mkdir ")) { createDir(SD, resolvePath(cmdLine.substring(6)).c_str()); return; }
if (cmdLine.startsWith("rmdir "))
{
String arg = resolvePath(cmdLine.substring(6));
removeDir(SD, arg.c_str());
return;
}
if (cmdLine.startsWith("touch ")) { writeFile(SD, resolvePath(cmdLine.substring(6)).c_str(), ""); return; }
if (cmdLine.startsWith("rm "))
{
String arg = cmdLine.substring(3);
arg.trim();
// Check if it starts with -r
if (arg.startsWith("-r ")) {
String pathStr = arg.substring(3);
pathStr.trim();
String fullPath = resolvePath(pathStr);
deleteDirRecursive(SD, fullPath.c_str());
}
else {
String fullPath = resolvePath(arg);
deleteFile(SD, fullPath.c_str());
}
return;
}
// MV
if (cmdLine.startsWith("mv ")) {
int space = cmdLine.indexOf(' ', 3);
if (space < 0) {
Serial.println("Error: invalid syntax");
tftPrintLn("Error: invalid syntax");
return;
}
String from = resolvePath(cmdLine.substring(3, space));
String to = resolvePath(cmdLine.substring(space + 1));
if (!movePath(SD, from.c_str(), to.c_str())) {
Serial.println("Error: move failed");
tftPrintLn("Error: move failed");
}
return;
}
// CP
if (cmdLine.startsWith("cp ")) {
int space = cmdLine.indexOf(' ', 3);
if (space < 0) {
Serial.println("Error: invalid syntax");
tftPrintLn("Error: invalid syntax");
return;
}
String from = resolvePath(cmdLine.substring(3, space));
String to = resolvePath(cmdLine.substring(space + 1));
if (!copyFile(SD, from.c_str(), to.c_str())) {
Serial.println("Error: copy failed");
tftPrintLn("Error: copy failed");
}
return;
}
if (cmdLine.startsWith("cat ")) { readFile(SD, resolvePath(cmdLine.substring(4)).c_str()); return; }
if (cmdLine.startsWith("stat ")) {
String path = resolvePath(cmdLine.substring(5));
statFile(SD, path.c_str());
return;
}
// WRITE / APPEND
if (cmdLine.startsWith("write ")) {
int space = cmdLine.indexOf(' ',6);
if(space<0){ Serial.println("Error: invalid syntax"); tftPrintLn("Error: invalid syntax"); return; }
String path = resolvePath(cmdLine.substring(6,space));
String txt = cmdLine.substring(space+1);
writeFile(SD, path.c_str(), txt.c_str()); return;
}
if (cmdLine.startsWith("append ")) {
int space = cmdLine.indexOf(' ',7);
if(space<0){ Serial.println("Error: invalid syntax"); tftPrintLn("Error: invalid syntax"); return; }
String path = resolvePath(cmdLine.substring(7,space));
String txt = cmdLine.substring(space+1);
appendFile(SD, path.c_str(), txt.c_str()); return;
}
// ECHO
if (cmdLine.startsWith("echo ")) {
String rest = cmdLine.substring(5);
int appendPos = rest.indexOf(">>");
int writePos = rest.indexOf(">");
if (appendPos >= 0) {
String txt = rest.substring(0, appendPos); txt.trim();
String pathStr = rest.substring(appendPos+2); pathStr.trim();
appendFile(SD, resolvePath(pathStr).c_str(), txt.c_str());
return;
}
else if (writePos >= 0) {
String txt = rest.substring(0, writePos); txt.trim();
String pathStr = rest.substring(writePos+1); pathStr.trim();
writeFile(SD, resolvePath(pathStr).c_str(), txt.c_str());
return;
}
else { Serial.println(rest); tftPrintLn(rest); return; }
}
Serial.println("Error: unknown command");
tftPrintLn("Error: unknown command");
}