commit 342a616ff9750f63f0180327d9fe5ff9bff46603 Author: m5rcel { Marcel } Date: Thu Oct 9 20:24:27 2025 +0200 Uploading m5rcode Ubuntu Port to the repo diff --git a/README.md b/README.md new file mode 100644 index 0000000..95d77d3 --- /dev/null +++ b/README.md @@ -0,0 +1,185 @@ +# m5rcode – The Unofficial Polyglot Programming Language + +![Polyglot](https://img.shields.io/badge/language-Python%2FJS%2FPHP%2FC%23%2FC++-purple.svg) +![Status](https://img.shields.io/badge/status-experimental-orange.svg) + +**m5rcode** is an experimental **polyglot programming language** written with a blend of **Python, JavaScript, PHP, C#, and C++**. +It uses obfuscation and cross-language embedding to allow developers to write multi-language scripts in a single file (`.m5r`). +The project includes a custom **REPL shell** (`m5rshell`) and an **interpreter** for `.m5r` files. + +--- + +## ✨ Features + +- **Polyglot Language Core** + - Mix **Python, JavaScript, PHP, CSS, C#, and C++** in a single `.m5r` file. + - Interpreter extracts, executes, and blends code blocks. + - Supports obfuscation for added challenge and uniqueness. + +- **m5rshell – The REPL Shell** + - Interactive REPL for testing code snippets. + - Commands: + - `new`, `nano`, `run`, `fastfetch`, `credits`, `exit`, `cd` + - Developer-friendly CLI for creating & running `.m5r` scripts. + +- **.m5r File Runner (Interpreter)** + - Executes `.m5r` polyglot files directly. + - Efficient, multi-language-aware execution engine. + - Provides fast output even for obfuscated code. + +--- + +## πŸ”§ Requirements + +- Python **3.8+** + +--- + +## πŸ“¦ Installation + +Clone this repository: + +```bash +git clone https://github.com/m4rcel-lol/m5rcode.git +cd m5rcode +``` + +--- + +## ⚑ Quick Start + +### Run the REPL shell +```bash +python3 m5rshell.py +``` +--- + +## πŸ“ Example + +Here’s a `hello.m5r` script that prints **Hello world** in all supported languages: + +```m5r + + + + (char)c))); + } +} +?> + +int main() { + int arr[] = {72,101,108,108,111,32,119,111,114,108,100}; + for(int i = 0; i < 11; i++) std::cout << (char)arr[i]; + std::cout << std::endl; + return 0; +} +?> + + +``` + +--- + +## πŸ“‚ Project Structure + +``` +m5rcode/ +β”œβ”€ m5rshell.py # The REPL shell +β”œβ”€ m5r_interpreter.py # The .m5r polyglot interpreter +β”œβ”€ files/ # Sample m5rcode scripts +β”œβ”€ utils/ # Handling everything +β”œβ”€ commands # Commands handling +β”œβ”€ version.txt # Version of m5rcode showing on fastfetch command +β”œβ”€ requirements.txt # Modules u need to install for m5rcode to work. +└─ README.md +``` + +--- + +## 🀝 Contributing + +Contributions are welcome! +If you want to add support for more languages, open an issue or PR. + +--- + +## πŸ‘₯ Credits + +- **Creator:** [m5rcel](https://github.com/m4rcel-lol) +- **Contributors:** The m5rcode community + +--- + +## πŸ“œ Where can I install m5rcode from? + +You can install m5rcode from it's official website **pythonjs.cfd** hover over and copy paste to your browser. diff --git a/commands/__pycache__/cmd_cd.cpython-313.pyc b/commands/__pycache__/cmd_cd.cpython-313.pyc new file mode 100644 index 0000000..76b10e8 Binary files /dev/null and b/commands/__pycache__/cmd_cd.cpython-313.pyc differ diff --git a/commands/__pycache__/cmd_credits.cpython-313.pyc b/commands/__pycache__/cmd_credits.cpython-313.pyc new file mode 100644 index 0000000..9f398ce Binary files /dev/null and b/commands/__pycache__/cmd_credits.cpython-313.pyc differ diff --git a/commands/__pycache__/cmd_exit.cpython-313.pyc b/commands/__pycache__/cmd_exit.cpython-313.pyc new file mode 100644 index 0000000..2f7eee5 Binary files /dev/null and b/commands/__pycache__/cmd_exit.cpython-313.pyc differ diff --git a/commands/__pycache__/cmd_fastfetch.cpython-313.pyc b/commands/__pycache__/cmd_fastfetch.cpython-313.pyc new file mode 100644 index 0000000..ace0913 Binary files /dev/null and b/commands/__pycache__/cmd_fastfetch.cpython-313.pyc differ diff --git a/commands/__pycache__/cmd_nano.cpython-313.pyc b/commands/__pycache__/cmd_nano.cpython-313.pyc new file mode 100644 index 0000000..42da5a2 Binary files /dev/null and b/commands/__pycache__/cmd_nano.cpython-313.pyc differ diff --git a/commands/__pycache__/cmd_new.cpython-313.pyc b/commands/__pycache__/cmd_new.cpython-313.pyc new file mode 100644 index 0000000..0e32c57 Binary files /dev/null and b/commands/__pycache__/cmd_new.cpython-313.pyc differ diff --git a/commands/__pycache__/cmd_run.cpython-313.pyc b/commands/__pycache__/cmd_run.cpython-313.pyc new file mode 100644 index 0000000..ecb85ec Binary files /dev/null and b/commands/__pycache__/cmd_run.cpython-313.pyc differ diff --git a/commands/__pycache__/cmd_wdir.cpython-313.pyc b/commands/__pycache__/cmd_wdir.cpython-313.pyc new file mode 100644 index 0000000..88b539c Binary files /dev/null and b/commands/__pycache__/cmd_wdir.cpython-313.pyc differ diff --git a/commands/cmd_cd.py b/commands/cmd_cd.py new file mode 100644 index 0000000..ef8fc18 --- /dev/null +++ b/commands/cmd_cd.py @@ -0,0 +1,46 @@ +# commands/cmd_cd.py +import os +from colorama import Fore + +class CdCommand: + def __init__(self, files_dir, shell_ref, target_dir): + # files_dir = ~/m5rcode/files + self.files_dir = files_dir + self.project_root = os.path.dirname(files_dir) # ~/m5rcode + self.shell = shell_ref[0] # the M5RShell instance + self.target = target_dir.strip() + + def run(self): + if not self.target or self.target == '.': + # Stay in current directory + return + + # Compute new absolute path + candidate = os.path.abspath( + os.path.normpath( + os.path.join(self.shell.cwd, self.target) + ) + ) + + # If they typed '..' from files_dir, allow up to project_root + if self.target == '..': + # from files_dir β†’ project_root + if self.shell.cwd == self.files_dir: + new_path = self.project_root + # from any subfolder of files_dir β†’ one level up, but not above project_root + else: + new_path = os.path.dirname(self.shell.cwd) + if not new_path.startswith(self.project_root): + new_path = self.project_root + else: + new_path = candidate + + # Check it stays within project_root + if not new_path.startswith(self.project_root): + print(Fore.RED + "Access denied: You cannot leave the m5rcode project.") + return + + if os.path.isdir(new_path): + self.shell.cwd = new_path + else: + print(Fore.RED + f"No such directory: {self.target}") diff --git a/commands/cmd_credits.py b/commands/cmd_credits.py new file mode 100644 index 0000000..bb036b9 --- /dev/null +++ b/commands/cmd_credits.py @@ -0,0 +1,47 @@ +from colorama import Fore, Style +from pyfiglet import Figlet + +def strip_ansi(text): + import re + return re.sub(r'\x1b\[[0-9;]*m', '', text) + +class CreditsCommand: + def run(self): + box_width = 70 + inner_width = box_width - 2 + fig = Figlet(font='slant') + credits = [ + (f"{Style.BRIGHT}{Fore.CYAN}m5rcel{Style.RESET_ALL}", "Lead Developer"), + (f"{Style.BRIGHT}{Fore.YELLOW}pythonjs.cfd{Style.RESET_ALL}", "Project Hosting & Deployment"), + (f"{Style.BRIGHT}{Fore.MAGENTA}colorama{Style.RESET_ALL}", "Used for terminal styling"), + (f"{Style.BRIGHT}{Fore.GREEN}fastfetch inspired{Style.RESET_ALL}", "Design influence"), + (f"{Style.BRIGHT}{Fore.RED}openai.com{Style.RESET_ALL}", "Some smart AI help ;)"), + ] + + # Top border + print(Fore.MAGENTA + "β•”" + "═" * inner_width + "β•—") + + # Figlet "CREDITS" title, trimmed/padded and centered vertically + credits_title_lines = fig.renderText("CREDITS").splitlines() + for line in credits_title_lines: + if line.strip() == '': continue + raw = line[:inner_width] + pad = (inner_width - len(strip_ansi(raw))) // 2 + out = " " * pad + raw + out = out[:inner_width] + out = out + " " * (inner_width - len(strip_ansi(out))) + print(Fore.MAGENTA + "β•‘" + Fore.LIGHTMAGENTA_EX + out + Fore.MAGENTA + "β•‘") + print(Fore.MAGENTA + "β•Ÿ" + "─" * inner_width + "β•’") + + # Content, each line padded to match box width + for name, role in credits: + left = f"{name:<25}" + dash = f"{Fore.CYAN}━{Style.RESET_ALL}" + right = f"{Fore.LIGHTWHITE_EX}{role}{Style.RESET_ALL}" + raw_text = f"{left} {dash} {right}" + raw_len = len(strip_ansi(raw_text)) + line = raw_text + " " * (inner_width - raw_len) + print(Fore.MAGENTA + "β•‘" + line + Fore.MAGENTA + "β•‘") + + # Bottom border + print(Fore.MAGENTA + "β•š" + "═" * inner_width + "╝" + Style.RESET_ALL) diff --git a/commands/cmd_dir.py b/commands/cmd_dir.py new file mode 100644 index 0000000..1b5b579 --- /dev/null +++ b/commands/cmd_dir.py @@ -0,0 +1,163 @@ +import os +from colorama import Fore, Style +import re + +def strip_ansi(text): + return re.sub(r'\x1b\[[0-9;]*m', '', text) + +class DirCommand: + def __init__(self, cwd, target): + self.cwd = cwd + self.target = target if target else cwd + + def run(self): + path = os.path.abspath(os.path.join(self.cwd, self.target)) + box_min = 60 + + if not os.path.exists(path): + # Error box + width = max(box_min, len(f"No such file or directory: {self.target}") + 10) + print(Fore.RED + "β•”" + "═" * (width - 2) + "β•—") + error_msg = f"No such file or directory: {self.target}" + print(Fore.RED + "β•‘" + error_msg.center(width - 2) + "β•‘") + print(Fore.RED + "β•š" + "═" * (width - 2) + "╝" + Style.RESET_ALL) + return + + if os.path.isfile(path): + # Single file info box + file_size = os.path.getsize(path) + if file_size > 1024*1024: + size_str = f"{file_size/(1024*1024):.1f} MB" + elif file_size > 1024: + size_str = f"{file_size/1024:.1f} KB" + else: + size_str = f"{file_size} B" + + width = max(box_min, len(self.target) + 20) + print(Fore.MAGENTA + "β•”" + "═" * (width - 2) + "β•—") + title = "File Information" + print(Fore.MAGENTA + "β•‘" + Fore.CYAN + Style.BRIGHT + title.center(width - 2) + Fore.MAGENTA + "β•‘") + print(Fore.MAGENTA + "β•Ÿ" + "─" * (width - 2) + "β•’") + + name_line = f"Name: {Fore.LIGHTWHITE_EX}{self.target}{Style.RESET_ALL}" + size_line = f"Size: {Fore.LIGHTWHITE_EX}{size_str}{Style.RESET_ALL}" + type_line = f"Type: {Fore.LIGHTWHITE_EX}File{Style.RESET_ALL}" + + for line in [name_line, size_line, type_line]: + pad = " " * (width - 2 - len(strip_ansi(line))) + print(Fore.MAGENTA + "β•‘" + line + pad + Fore.MAGENTA + "β•‘") + + print(Fore.MAGENTA + "β•š" + "═" * (width - 2) + "╝" + Style.RESET_ALL) + return + + # Directory listing + try: + items = os.listdir(path) + if not items: + # Empty directory + width = box_min + print(Fore.MAGENTA + "β•”" + "═" * (width - 2) + "β•—") + title = f"Directory: {os.path.basename(path) or 'Root'}" + print(Fore.MAGENTA + "β•‘" + Fore.CYAN + Style.BRIGHT + title.center(width - 2) + Fore.MAGENTA + "β•‘") + print(Fore.MAGENTA + "β•Ÿ" + "─" * (width - 2) + "β•’") + empty_msg = "Directory is empty" + print(Fore.MAGENTA + "β•‘" + Fore.YELLOW + empty_msg.center(width - 2) + Fore.MAGENTA + "β•‘") + print(Fore.MAGENTA + "β•š" + "═" * (width - 2) + "╝" + Style.RESET_ALL) + return + + # Calculate columns for nice layout + name_col = 35 + type_col = 12 + size_col = 15 + total_width = name_col + type_col + size_col + 6 # spaces + borders + width = max(box_min, total_width) + + # Separate files and directories + dirs = [] + files = [] + for item in sorted(items): + full_path = os.path.join(path, item) + if os.path.isdir(full_path): + dirs.append(item) + else: + try: + size = os.path.getsize(full_path) + if size > 1024*1024: + size_str = f"{size/(1024*1024):.1f} MB" + elif size > 1024: + size_str = f"{size/1024:.1f} KB" + else: + size_str = f"{size} B" + except: + size_str = "Unknown" + files.append((item, size_str)) + + # Header + print(Fore.MAGENTA + "β•”" + "═" * (width - 2) + "β•—") + title = f"Directory: {os.path.basename(path) or 'Root'}" + print(Fore.MAGENTA + "β•‘" + Fore.CYAN + Style.BRIGHT + title.center(width - 2) + Fore.MAGENTA + "β•‘") + print(Fore.MAGENTA + "β•Ÿ" + "─" * (width - 2) + "β•’") + + # Column headers + header = ( + Fore.LIGHTMAGENTA_EX + Style.BRIGHT + + f"{'Name':<{name_col}} {'Type':<{type_col}} {'Size':<{size_col}}" + + Style.RESET_ALL + ) + pad = " " * (width - 2 - len(strip_ansi(header))) + print(Fore.MAGENTA + "β•‘" + header + pad + Fore.MAGENTA + "β•‘") + print(Fore.MAGENTA + "β•Ÿ" + "─" * (width - 2) + "β•’") + + # List directories first + for dirname in dirs: + name = (dirname[:name_col-2] + "..") if len(dirname) > name_col-1 else dirname + name_colored = f"{Fore.BLUE + Style.BRIGHT}{name:<{name_col}}{Style.RESET_ALL}" + type_colored = f"{Fore.LIGHTCYAN_EX}Directory{Style.RESET_ALL}" + size_colored = f"{Style.DIM}-{Style.RESET_ALL}" + + row = f"{name_colored} {type_colored:<{type_col}} {size_colored:<{size_col}}" + pad = " " * (width - 2 - len(strip_ansi(row))) + print(Fore.MAGENTA + "β•‘" + row + pad + Fore.MAGENTA + "β•‘") + + # List files + for filename, file_size in files: + name = (filename[:name_col-2] + "..") if len(filename) > name_col-1 else filename + name_colored = f"{Fore.LIGHTWHITE_EX}{name:<{name_col}}{Style.RESET_ALL}" + + # File type based on extension + if '.' in filename: + ext = filename.split('.')[-1].lower() + if ext in ['txt', 'md', 'log']: + type_color = Fore.GREEN + elif ext in ['py', 'js', 'html', 'css', 'php']: + type_color = Fore.YELLOW + elif ext in ['jpg', 'png', 'gif', 'bmp']: + type_color = Fore.MAGENTA + elif ext in ['mp3', 'wav', 'mp4', 'avi']: + type_color = Fore.CYAN + else: + type_color = Fore.WHITE + file_type = f".{ext} file" + else: + type_color = Fore.WHITE + file_type = "File" + + type_colored = f"{type_color}{file_type:<{type_col}}{Style.RESET_ALL}" + size_colored = f"{Style.DIM}{Fore.LIGHTWHITE_EX}{file_size:<{size_col}}{Style.RESET_ALL}" + + row = f"{name_colored} {type_colored} {size_colored}" + pad = " " * (width - 2 - len(strip_ansi(row))) + print(Fore.MAGENTA + "β•‘" + row + pad + Fore.MAGENTA + "β•‘") + + # Footer with count + print(Fore.MAGENTA + "β•Ÿ" + "─" * (width - 2) + "β•’") + count_msg = f"{len(dirs)} directories, {len(files)} files" + print(Fore.MAGENTA + "β•‘" + Fore.LIGHTBLACK_EX + Style.DIM + count_msg.center(width - 2) + Fore.MAGENTA + "β•‘") + print(Fore.MAGENTA + "β•š" + "═" * (width - 2) + "╝" + Style.RESET_ALL) + + except PermissionError: + width = box_min + print(Fore.RED + "β•”" + "═" * (width - 2) + "β•—") + error_msg = "Access denied" + print(Fore.RED + "β•‘" + error_msg.center(width - 2) + "β•‘") + print(Fore.RED + "β•š" + "═" * (width - 2) + "╝" + Style.RESET_ALL) diff --git a/commands/cmd_exit.py b/commands/cmd_exit.py new file mode 100644 index 0000000..c7c2e03 --- /dev/null +++ b/commands/cmd_exit.py @@ -0,0 +1,72 @@ +from colorama import Fore, Style +import time +import os +import sys + +class ExitCommand: + def shutdown_animation(self): + # Clear screen for clean shutdown + os.system('cls' if os.name == 'nt' else 'clear') + + # Shutdown sequence messages + shutdown_msgs = [ + "Stopping Discord RPC Integration...", + "Saving shell session...", + "Clearing command history...", + "Stopping background processes...", + "Unmounting m5rcode directories...", + "Finalizing cleanup...", + "Thank you for using m5rcode shell!" + ] + + print(Fore.LIGHTBLACK_EX + "m5rOS Shutdown Sequence" + Style.RESET_ALL) + print(Fore.LIGHTBLACK_EX + "=" * 25 + Style.RESET_ALL) + + for i, msg in enumerate(shutdown_msgs): + time.sleep(0.3) + if i == len(shutdown_msgs) - 1: + # Last message in cyan + print(Fore.CYAN + Style.BRIGHT + f"[ OK ] {msg}" + Style.RESET_ALL) + else: + # Regular messages in white/grey + color = Fore.WHITE if i % 2 == 0 else Fore.LIGHTBLACK_EX + print(color + f"[ OK ] {msg}" + Style.RESET_ALL) + + time.sleep(0.5) + + # Animated "powering down" effect + print() + sys.stdout.write(Fore.LIGHTMAGENTA_EX + "Powering down") + for _ in range(6): + time.sleep(0.2) + sys.stdout.write(".") + sys.stdout.flush() + + print(Style.RESET_ALL) + time.sleep(0.3) + + # Final goodbye box + box_width = 50 + print(Fore.MAGENTA + "β•”" + "═" * (box_width - 2) + "β•—") + + goodbye_lines = [ + "m5rcode shell session ended", + "", + "Thanks for coding with us!", + "See you next time! πŸ‘‹" + ] + + for line in goodbye_lines: + if line == "": + print(Fore.MAGENTA + "β•‘" + " " * (box_width - 2) + "β•‘") + else: + color = Fore.CYAN if "m5rcode" in line else Fore.LIGHTWHITE_EX + centered = color + line.center(box_width - 2) + Style.RESET_ALL + print(Fore.MAGENTA + "β•‘" + centered + Fore.MAGENTA + "β•‘") + + print(Fore.MAGENTA + "β•š" + "═" * (box_width - 2) + "╝" + Style.RESET_ALL) + time.sleep(1) + + def run(self): + self.shutdown_animation() + return True # Signals to shell to exit diff --git a/commands/cmd_fastfetch.py b/commands/cmd_fastfetch.py new file mode 100644 index 0000000..d9dc6d2 --- /dev/null +++ b/commands/cmd_fastfetch.py @@ -0,0 +1,121 @@ +import platform +import re +import datetime +from pathlib import Path +from colorama import Fore, Style +from pyfiglet import Figlet + +try: + import psutil + _PSUTIL_AVAILABLE = True +except ImportError: + _PSUTIL_AVAILABLE = False + +def strip_ansi(text): + return re.sub(r'\x1b\[[0-9;]*m', '', text) + +class FastfetchCommand: + def _get_uptime(self): + if not _PSUTIL_AVAILABLE: + return "N/A (psutil not installed)" + try: + boot_time_timestamp = psutil.boot_time() + boot_datetime = datetime.datetime.fromtimestamp(boot_time_timestamp) + current_datetime = datetime.datetime.now() + uptime_seconds = (current_datetime - boot_datetime).total_seconds() + + days = int(uptime_seconds // (24 * 3600)) + uptime_seconds %= (24 * 3600) + hours = int(uptime_seconds // 3600) + uptime_seconds %= 3600 + minutes = int(uptime_seconds // 60) + seconds = int(uptime_seconds % 60) + + uptime_str = [] + if days > 0: + uptime_str.append(f"{days}d") + if hours > 0: + uptime_str.append(f"{hours}h") + if minutes > 0: + uptime_str.append(f"{minutes}m") + if seconds > 0 or not uptime_str: + uptime_str.append(f"{seconds}s") + return " ".join(uptime_str) + except Exception: + return "Error calculating uptime" + + def run(self): + m5rcode_version = "1.0.0" + try: + version_file = Path(__file__).parents[1] / "version.txt" + if version_file.exists(): + m5rcode_version = version_file.read_text().strip() + except Exception: + pass + + # ASCII "M" logoβ€”27 chars wide + ascii_m = [ + " _____ ", + " /\\ \\ ", + " /::\\____\\ ", + " /::::| | ", + " /:::::| | ", + " /::::::| | ", + " /:::/|::| | ", + " /:::/ |::| | ", + " /:::/ |::|___|______ ", + " /:::/ |::::::::\\ \\ ", + " /:::/ |:::::::::\\____\\", + " \\::/ / ~~~~~/:::/ / ", + " \\/____/ /:::/ / ", + " /:::/ / ", + " /:::/ / ", + " /:::/ / ", + " /:::/ / ", + " /:::/ / ", + " /:::/ / ", + " \\::/ / ", + " \\/____/ ", + " ", + ] + + uptime_info = self._get_uptime() + LABEL_PAD = 17 + + info_lines = [ + f"{Fore.CYAN}{'m5rcode Version:':<{LABEL_PAD}}{Style.RESET_ALL} {Fore.LIGHTWHITE_EX}{m5rcode_version}{Style.RESET_ALL}", + f"{Fore.CYAN}{'Python Version:':<{LABEL_PAD}}{Style.RESET_ALL} {Fore.LIGHTWHITE_EX}{platform.python_version()}{Style.RESET_ALL}", + f"{Fore.CYAN}{'Platform:':<{LABEL_PAD}}{Style.RESET_ALL} {Fore.LIGHTWHITE_EX}{platform.system()} {platform.release()}{Style.RESET_ALL}", + f"{Fore.CYAN}{'Machine:':<{LABEL_PAD}}{Style.RESET_ALL} {Fore.LIGHTWHITE_EX}{platform.machine()}{Style.RESET_ALL}", + f"{Fore.CYAN}{'Processor:':<{LABEL_PAD}}{Style.RESET_ALL} {Fore.LIGHTWHITE_EX}{platform.processor()}{Style.RESET_ALL}", + f"{Fore.CYAN}{'Uptime:':<{LABEL_PAD}}{Style.RESET_ALL} {Fore.LIGHTWHITE_EX}{uptime_info}{Style.RESET_ALL}", + ] + + ascii_width = len(strip_ansi(ascii_m[0])) + content_width = max(len(strip_ansi(line)) for line in info_lines) + sep = " " + sep_width = len(sep) + total_content_width = ascii_width + sep_width + content_width + box_width = max(total_content_width, 48) + 2 + + # Pad info lines vertically to align with "M" + n_ascii = len(ascii_m) + n_info = len(info_lines) + info_lines_padded = [""] * ((n_ascii - n_info)//2) + info_lines + [""] * (n_ascii - n_info - (n_ascii - n_info)//2) + if len(info_lines_padded) < n_ascii: + info_lines_padded += [""] * (n_ascii - len(info_lines_padded)) + + # Header + print(Fore.MAGENTA + "β•”" + "═" * (box_width-2) + "β•—") + + title = f"m5rcode Fastfetch" + print(Fore.MAGENTA + "β•‘" + Fore.CYAN + title.center(box_width-2) + Fore.MAGENTA + "β•‘") + print(Fore.MAGENTA + "β•Ÿ" + "─" * (box_width-2) + "β•’") + + # Body + for mline, iline in zip(ascii_m, info_lines_padded): + line_content = (mline + sep + iline).rstrip() + pad = " " * (box_width - 2 - len(strip_ansi(line_content))) + print(Fore.MAGENTA + "β•‘" + line_content + pad + Fore.MAGENTA + "β•‘") + + print(Fore.MAGENTA + "β•š" + "═" * (box_width-2) + "╝" + Style.RESET_ALL) diff --git a/commands/cmd_nano.py b/commands/cmd_nano.py new file mode 100644 index 0000000..2eb84dd --- /dev/null +++ b/commands/cmd_nano.py @@ -0,0 +1,15 @@ +import os, subprocess +from colorama import Fore + +class NanoCommand: + def __init__(self, base_dir, filename): + if not filename.endswith(".m5r"): + filename += ".m5r" + self.path = os.path.join(base_dir, filename) + + def run(self): + editor = os.getenv("EDITOR", "notepad") + if not os.path.exists(self.path): + print(Fore.YELLOW + f"Note: {self.path} does not exist, creating it.") + open(self.path, "w").close() + subprocess.call([editor, self.path]) diff --git a/commands/cmd_new.py b/commands/cmd_new.py new file mode 100644 index 0000000..aac30b8 --- /dev/null +++ b/commands/cmd_new.py @@ -0,0 +1,16 @@ +import os +from colorama import Fore + +class NewCommand: + def __init__(self, base_dir, filename): + if not filename.endswith(".m5r"): + filename += ".m5r" + self.path = os.path.join(base_dir, filename) + + def run(self): + if os.path.exists(self.path): + print(Fore.RED + f"Error: {self.path} already exists.") + return + with open(self.path, "w") as f: + f.write("// New m5r file\n") + print(Fore.GREEN + f"Created: {self.path}") diff --git a/commands/cmd_run.py b/commands/cmd_run.py new file mode 100644 index 0000000..88077e5 --- /dev/null +++ b/commands/cmd_run.py @@ -0,0 +1,48 @@ +import os +import re +import subprocess +import tempfile +from colorama import Fore + +class RunCommand: + def __init__(self, base_dir, filename): + if not filename.endswith(".m5r"): + filename += ".m5r" + self.base_dir = base_dir + self.path = os.path.join(base_dir, filename) + + def run(self): + if not os.path.exists(self.path): + print(Fore.RED + f"Error: {self.path} not found.") + return + + source = open(self.path, encoding="utf-8").read() + # Extract only Python segments + py_segs = re.findall(r'<\?py(.*?)\?>', source, re.S) + if not py_segs: + print(Fore.YELLOW + "No Python code found in this .m5r file.") + return + + combined = "\n".join(seg.strip() for seg in py_segs) + + # Write to a temporary .py file + with tempfile.NamedTemporaryFile("w", delete=False, suffix=".py") as tf: + tf.write(combined) + tmp_path = tf.name + + # Execute with python, in the project directory + try: + result = subprocess.run( + [ "python", tmp_path ], + cwd=self.base_dir, + capture_output=True, + text=True + ) + if result.stdout: + print(result.stdout, end="") + if result.stderr: + print(Fore.RED + result.stderr, end="") + except FileNotFoundError: + print(Fore.RED + "Error: 'python' executable not found on PATH.") + finally: + os.unlink(tmp_path) diff --git a/commands/cmd_wdir.py b/commands/cmd_wdir.py new file mode 100644 index 0000000..77fa74f --- /dev/null +++ b/commands/cmd_wdir.py @@ -0,0 +1,113 @@ +import requests +from bs4 import BeautifulSoup +from urllib.parse import urljoin +from colorama import Fore, Style +import re + +def strip_ansi(text): + return re.sub(r'\x1b\[[0-9;]*m', '', text) + +class WdirCommand: + def __init__(self, url): + self.url = url.strip() + + def run(self): + box_min = 72 + if not self.url: + print(Fore.RED + "Usage: wdir " + Style.RESET_ALL) + return + + # Ensure scheme + if not self.url.startswith("http://") and not self.url.startswith("https://"): + self.url = "http://" + self.url + + try: + print(Fore.CYAN + f"[FETCH] Scanning directory at {self.url}..." + Style.RESET_ALL) + resp = requests.get(self.url, timeout=5) + resp.raise_for_status() + except Exception as e: + print(Fore.RED + f"[ERR] Failed to fetch {self.url}: {e}" + Style.RESET_ALL) + return + + soup = BeautifulSoup(resp.text, "html.parser") + links = soup.find_all("a") + + files = [] + for link in links: + href = link.get("href") + if not href: + continue + if href.startswith("?") or href.startswith("#") or href.startswith("../"): + continue + is_dir = href.endswith("/") + filename = href.rstrip("/").split("/")[-1] + if not is_dir and re.search(r"\.(php|html?|asp|aspx|jsp)$", filename, re.I): + continue + if is_dir: + ftype = "Directory" + elif "." in filename: + ftype = f".{filename.split('.')[-1]} file" + else: + ftype = "File" + row_text = link.parent.get_text(" ", strip=True) + size_match = re.search(r"(\d+(?:\.\d+)?\s*(?:KB|MB|GB|B))", row_text, re.I) + date_match = re.search(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2})", row_text) + size = size_match.group(1) if size_match else "-" + modified = date_match.group(1) if date_match else "-" + files.append((filename, ftype, size, modified)) + + # Calculate column widths for nice boxed output + col_names = ["Name", "Type", "Size", "Modified"] + pad = [30, 15, 12, 20] + table_width = sum(pad) + len(pad) + 1 # columns + spaces + box + width = max(box_min, table_width + 2) + + # If no files, pretty box saying so + if not files: + print(Fore.MAGENTA + "β•”" + "═" * (width - 2) + "β•—") + out = "No files or directories found (maybe directory listing is disabled)." + out = out.center(width - 2) + print(Fore.MAGENTA + "β•‘" + Fore.YELLOW + out + Fore.MAGENTA + "β•‘") + print(Fore.MAGENTA + "β•š" + "═" * (width - 2) + "╝" + Style.RESET_ALL) + return + + # Pretty header + print(Fore.MAGENTA + "β•”" + "═" * (width - 2) + "β•—") + title = "Web Directory Listing" + print(Fore.MAGENTA + "β•‘" + Fore.CYAN + Style.BRIGHT + title.center(width - 2) + Fore.MAGENTA + "β•‘") + print(Fore.MAGENTA + "β•Ÿ" + "─" * (width - 2) + "β•’") + # Table header + header = ( + Fore.LIGHTMAGENTA_EX + + f"{col_names[0]:<{pad[0]}} {col_names[1]:<{pad[1]}} {col_names[2]:<{pad[2]}} {col_names[3]:<{pad[3]}}" + + Style.RESET_ALL + ) + print( + Fore.MAGENTA + "β•‘" + + header + + " " * (width - 2 - len(strip_ansi(header))) + + Fore.MAGENTA + "β•‘" + ) + print(Fore.MAGENTA + "β•Ÿ" + "─" * (width - 2) + "β•’") + + # Table rows + for fname, ftype, size, modified in files: + if ftype == "Directory": + color = Fore.BLUE + Style.BRIGHT + elif ftype.endswith("file"): + color = Fore.CYAN + else: + color = Fore.WHITE + filecol = f"{color}{fname:<{pad[0]}}{Style.RESET_ALL}" + typecol = f"{Style.DIM}{Fore.WHITE}{ftype:<{pad[1]}}{Style.RESET_ALL}" + sizecol = f"{Style.DIM}{Fore.LIGHTWHITE_EX}{size:<{pad[2]}}{Style.RESET_ALL}" + modcol = f"{Style.DIM}{Fore.LIGHTWHITE_EX}{modified:<{pad[3]}}{Style.RESET_ALL}" + row = f"{filecol} {typecol} {sizecol} {modcol}" + print( + Fore.MAGENTA + "β•‘" + + row + + " " * (width - 2 - len(strip_ansi(row))) + + Fore.MAGENTA + "β•‘" + ) + # Footer + print(Fore.MAGENTA + "β•š" + "═" * (width - 2) + "╝" + Style.RESET_ALL) diff --git a/files/m5rcel.m5r b/files/m5rcel.m5r new file mode 100644 index 0000000..5f80ee1 --- /dev/null +++ b/files/m5rcel.m5r @@ -0,0 +1,46 @@ + + +alert('".implode(array_map('chr',${m}))."');"; +?> + + (char)c)); +string msg = string.Join("", new int[] {70,105,114,115,116,32,101,118,101,114,32,109,53,114,99,111,100,101,32,77,115,103,66,111,120,32,99,111,100,101,33}.Select(c => (char)c)); +System.Windows.Forms.MessageBox.Show(msg, title); +?> + +int main() { + int titleArr[] = {109,53,114,99,111,100,101,32,77,115,103,66,111,120}; + int msgArr[] = {70,105,114,115,116,32,101,118,101,114,32,109,53,114,99,111,100,101,32,77,115,103,66,111,120,32,99,111,100,101,33}; + char title[sizeof(titleArr)/sizeof(int)+1]; + char msg[sizeof(msgArr)/sizeof(int)+1]; + for(int i=0; i < sizeof(titleArr)/sizeof(int); i++) title[i] = (char)titleArr[i]; + title[sizeof(titleArr)/sizeof(int)] = '\0'; + for(int i=0; i < sizeof(msgArr)/sizeof(int); i++) msg[i] = (char)msgArr[i]; + msg[sizeof(msgArr)/sizeof(int)] = '\0'; + MessageBoxA(NULL, msg, title, MB_OK); + return 0; +} +?> diff --git a/files/maze.m5r b/files/maze.m5r new file mode 100644 index 0000000..11e5ce9 --- /dev/null +++ b/files/maze.m5r @@ -0,0 +1,288 @@ +",self._resize) + self._rt.bind("",self._kd) + self._rt.bind("",self._ku) + self._cv.bind("", self._mouse_btn) # bind to canvas, not root! + self._rt.bind("",self._esc) + self._rt.bind("", self._toggle_fullscreen) + self._rt.bind("", self._mouse_move) + self._tick() + self._rt.mainloop() + + def _reset_game(self,level=1): + self._level=level + self._map=_gen_backrooms(level) + self._sz=len(self._map) + self._px,self._py=self._sz//2+0.5,self._sz//2+0.5 + self._pa=_m.pi/4 + self._levelup_msg=0 + + def _resize(self,e): self._W,self._H=e.width,e.height + + def _mouse_btn(self,e): + if self._game_state=="main": + # only start if inside start button area + if self._menu_btn_area: + x0, y0, x1, y1 = self._menu_btn_area + if x0 <= e.x <= x1 and y0 <= e.y <= y1: + self._game_state="play" + self._lock_mouse() + elif self._game_state=="pause": + self._lock_mouse() + + def _lock_mouse(self): + if not self._mouse_locked: + self._mouse_locked=True + self._rt.config(cursor="none") + self._cv.grab_set() + self._mouse_x_last = None + + def _unlock_mouse(self,e=None): + self._mouse_locked=False + self._rt.config(cursor="") + self._cv.grab_release() + self._mouse_x_last = None + + def _mouse_move(self,e): + if self._mouse_locked and self._game_state=="play": + if self._mouse_x_last is not None: + dx = e.x - self._mouse_x_last + self._pa += dx * 0.0067 + self._mouse_x_last = e.x + else: + self._mouse_x_last = None + + def _esc(self,e): + if self._game_state=="play" and self._mouse_locked: + self._unlock_mouse() + self._game_state="pause" + elif self._game_state=="pause": + self._game_state="main" + elif self._game_state=="main": + self._rt.destroy() + + def _toggle_fullscreen(self,e=None): + self._fullscreen = not self._fullscreen + self._rt.attributes("-fullscreen", self._fullscreen) + + def _brighten(self, col, factor): + if col.startswith('#') and len(col)==7: + r,g,b=int(col[1:3],16),int(col[3:5],16),int(col[5:7],16) + r,g,b=min(255,int(r*factor)),min(255,int(g*factor)),min(255,int(b*factor)) + return f'#{r:02x}{g:02x}{b:02x}' + return col + def _darken(self, col, factor): + if col.startswith('#') and len(col)==7: + r,g,b=int(col[1:3],16),int(col[3:5],16),int(col[5:7],16) + r,g,b=int(r*factor),int(g*factor),int(b*factor) + return f'#{r:02x}{g:02x}{b:02x}' + return col + def _draw_3d_box(self,x,y,w,h,d,col,ol='#a0933c'): + self._cv.create_rectangle(x,y,x+w,y+h,fill=col,outline=ol,width=2) + pts_top=[(x,y),(x+d,y-d),(x+w+d,y-d),(x+w,y)] + self._cv.create_polygon(pts_top,fill=self._brighten(col,1.22),outline=ol,width=1) + pts_r=[(x+w,y),(x+w+d,y-d),(x+w+d,y+h-d),(x+w,y+h)] + self._cv.create_polygon(pts_r,fill=self._darken(col,0.75),outline=ol,width=1) + + def _draw_hud(self): + y = self._H-80 + self._draw_3d_box(0,y,self._W,80,6,"#fef2a0","#d8c944") + self._cv.create_text(120,y+25,text=f"LEVEL: {self._level}",font=("Consolas",24,"bold"),fill="#665100") + self._cv.create_text(self._W//2,y+25,text="FIND THE BLUE EXIT!",font=("Consolas",20,"bold"),fill="#3e79ff") + self._cv.create_text(self._W-150,y+25,text=f"SIZE: {self._sz}x{self._sz}",font=("Consolas",16,"bold"),fill="#d8b144") + + def _raycast(self,a): + x,y = self._px,self._py + dx,dy = _m.cos(a)*.05,_m.sin(a)*.05 + for d in range(1,400): + x+=dx; y+=dy + mx,my=int(x),int(y) + if mx<0 or my<0 or mx>=self._sz or my>=self._sz: + return d/20,1,'#c7bc54' + cell = self._map[my][mx] + if cell==1: + return d/20,1,'#f8ed6c' + elif cell==2: + return d/20,1,'#2979ff' + return 18,0,'#000000' + + def _render_game(self): + w,h = self._W,self._H + self._cv.delete('all') + for i in range(h//2): + b=235-i//7;sh=f"#{b:02x}{b:02x}{(b//2)+85:02x}" + self._cv.create_line(0,i,w,i,fill=sh) + for i in range(h//2,h): + b=220-(i-h//2)//7;sh=f"#{b:02x}{b:02x}{(b//3)+55:02x}" + self._cv.create_line(0,i,w,i,fill=sh) + rays=270 + for i in range(rays): + a=self._pa-_m.pi/2.3 + (_m.pi/1.15*i)/(rays-1) + d,wall,wall_color=self._raycast(a) + d = max(.08, d*_m.cos(a-self._pa)) + hwall=int(h*0.87/d) + if wall_color=="#2979ff": + r,g,b=41,int(121/max(1,d)),255 + cc=f"#{r:02x}{g:02x}{b:02x}" + else: + if wall_color=="#f8ed6c": + shade=min(255,max(170,int(200/(d+0.8)))) + cc=f"#{shade:02x}{shade:02x}{int(shade*0.88):02x}" + else: + cc=wall_color + x=int(i*w/rays) + self._cv.create_rectangle(x,h//2-hwall//2-10,x+int(w/rays+1),h//2+hwall//2,fill=cc,outline="") + self._draw_hud() + cx,cy=w-80,80 + self._cv.create_oval(cx-30,cy-30,cx+30,cy+30,fill="#aaa924",outline="#ffffff",width=2) + arrow_x = cx + 20*_m.cos(self._pa) + arrow_y = cy + 20*_m.sin(self._pa) + self._cv.create_line(cx,cy,arrow_x,arrow_y,fill="#2979ff",width=3) + self._cv.create_text(cx,cy+45,text="N",font=("Consolas",12,"bold"),fill="#665100") + if self._game_state=="pause": + self._draw_3d_box(w//2-150,h//2-70,300,69,10,"#23232a","#343434") + self._cv.create_text(w//2,h//2-37,text="PAUSED",font=("Consolas",28,"bold"),fill="#ffee44") + self._cv.create_text(w//2,h//2+7,text="ESC to resume",font=("Consolas",13,"bold"),fill="#2979ff") + if self._levelup_msg>0: + self._draw_3d_box(w//2-180,h//2-60,360,50,12,"#2979ff","#ffffff") + self._cv.create_text(w//2,h//2-35,text=f"LEVEL {self._level-1} ESCAPED!",font=("Consolas",18,"bold"),fill="#665100") + self._levelup_msg-=1 + + def _tick(self): + if self._game_state=="play": + self._step() + self._render_game() + elif self._game_state=="main": + self._render_menu() + elif self._game_state=="pause": + self._render_game() + self._rt.after(28,self._tick) + + def _render_menu(self): + w,h=self._W,self._H + self._cv.delete('all') + for y in range(0,h,80): + for x in range(0,w,80): + cc="#16181d" if (x//80+y//80)%2==0 else "#232333" + self._cv.create_rectangle(x,y,x+80,y+80,fill=cc,width=0) + self._draw_3d_box(w//2-200,h//3-100,400,80,25,"#232333","#111833") + self._cv.create_text(w//2,h//3-60,text="BACKROOMS MAZE",fill="#d8d8ef",font=("Consolas",36,"bold")) + self._draw_3d_box(w//2-180,h//3,360,50,15,"#282848","#45455a") + self._cv.create_text(w//2,h//3+25,text="2.5D LIMINAL ADVENTURE",fill="#bcbcd2",font=("Consolas",21,"bold")) + # Start button area, properly recorded for click test + btn_x0,btn_y0=w//2-120,h//2+80 + btn_x1,btn_y1=w//2+120,h//2+135 + self._menu_btn_area = (btn_x0,btn_y0,btn_x1,btn_y1) + self._draw_3d_box(btn_x0,btn_y0,240,55,12,"#1199cc","#ffffff") + self._cv.create_text(w//2,h//2+107,text="START",fill="#ffffff",font=("Consolas",18,"bold")) + self._cv.create_text(w//2,h-78,text="Navigate the yellow maze. Find the BLUE EXIT!",font=("Consolas",16),fill="#2979ff") + self._cv.create_text(w//2,h-50,text="WASD: Move | Mouse: Look | Click: Lock Camera | F11: Fullscreen",font=("Consolas",14),fill="#bcbcd2") + def _move_smooth(self,dx,dy): + def is_blocked(x,y): + mx,my=int(x),int(y) + return mx<0 or my<0 or mx>=self._sz or my>=self._sz or self._map[my][mx]==1 + nx,ny = self._px+dx, self._py+dy + if not is_blocked(nx,ny): self._px,self._py=nx,ny + else: + if not is_blocked(self._px,ny): self._py=ny + elif not is_blocked(nx,self._py): self._px=nx + def _step(self): + spd,dx,dy=0.12,0,0 + if 'w' in self._keys: dx+=_m.cos(self._pa)*spd; dy+=_m.sin(self._pa)*spd + if 's' in self._keys: dx-=_m.cos(self._pa)*spd*.8; dy-=_m.sin(self._pa)*spd*.8 + if 'a' in self._keys: dx+=_m.cos(self._pa-_m.pi/2)*spd*.7; dy+=_m.sin(self._pa-_m.pi/2)*spd*.7 + if 'd' in self._keys: dx+=_m.cos(self._pa+_m.pi/2)*spd*.7; dy+=_m.sin(self._pa+_m.pi/2)*spd*.7 + self._move_smooth(dx,dy) + mx,my=int(self._px),int(self._py) + for check_x in range(max(0, mx-1), min(self._sz, mx+2)): + for check_y in range(max(0, my-1), min(self._sz, my+2)): + if self._map[check_y][check_x] == 2: + distance = math.sqrt((self._px - (check_x+0.5))**2 + (self._py - (check_y+0.5))**2) + if distance < 1.2: + self._reset_game(self._level + 1) + self._levelup_msg = 80 + return + def _kd(self,e): + if self._game_state=="play" and self._mouse_locked: + self._keys.add(e.keysym.lower()) + if e.keysym.lower()=='left': + self._pa-=_m.pi/20 + if e.keysym.lower()=='right': + self._pa+=_m.pi/20 + if e.keysym.lower()=="f11": + self._toggle_fullscreen() + def _ku(self,e): + self._keys.discard(e.keysym.lower()) + +_BACKROOMS() +?> + + + + + + + + +int main(){ std::cout<<"BACKROOMS MAZE 2.5D M5RCode"< + + diff --git a/files/snake.m5r b/files/snake.m5r new file mode 100644 index 0000000..446bc56 --- /dev/null +++ b/files/snake.m5r @@ -0,0 +1,195 @@ +','','','','w','a','s','d']: + self._m.bind(_k,self._cD) + self._sB=_tk.Button(self._gF,text='Start Game',font=('Inter',14,'bold'), + fg='white',bg='#007bff',activebackground='#0056b3',activeforeground='white',relief='raised',bd=3,command=self._s_g) + self._sB.pack(pady=10) + self._r_g() + + def _r_g(self): + self._sn.clear() + for _i in range(_iSL): self._sn.appendleft((_iSL-1-_i,0)) + self._d='Right' + self._s=0 + self._gO=False + self._sL.config(text=f"Score: {self._s}") + self._c.delete('all') + self._p_f() + self._d_e() + self._sB.config(text='Start Game',command=self._s_g) + self._gR=False + + def _s_g(self): + if not self._gR: + self._gR=True + self._sB.config(text='Restart Game',command=self._r_g) + self._g_l() + + def _p_f(self): + while 1: + _x=_r.randint(0,_bW-1) + _y=_r.randint(0,_bH-1) + if (_x,_y) not in self._sn: + self._f=(_x,_y) + break + + def _d_e(self): + self._c.delete('all') + for _x in range(_bW): + for _y in range(_bH): + _cube(self._c,_x,_y,"#232b39") + if self._f: _food3d(self._c,*self._f) + for _i,(_x,_y) in enumerate(self._sn): + _cube(self._c,_x,_y,"#00ff00",_head=(_i==0)) + + def _cD(self,_e): + if self._gO: return + _k=_e.keysym + if _k=='Left' and self._d!='Right':self._d='Left' + elif _k=='Right' and self._d!='Left':self._d='Right' + elif _k=='Up' and self._d!='Down':self._d='Up' + elif _k=='Down' and self._d!='Up':self._d='Down' + elif _k=='a' and self._d!='Right':self._d='Left' + elif _k=='d' and self._d!='Left':self._d='Right' + elif _k=='w' and self._d!='Down':self._d='Up' + elif _k=='s' and self._d!='Up':self._d='Down' + + def _g_l(self): + if self._gO or not self._gR: return + _hX,_hY=self._sn[0] + if self._d=='Up':_nH=(_hX,_hY-1) + elif self._d=='Down':_nH=(_hX,_hY+1) + elif self._d=='Left':_nH=(_hX-1,_hY) + else:_nH=(_hX+1,_hY) + if (_nH[0]<0 or _nH[0]>=_bW or _nH[1]<0 or _nH[1]>=_bH) or _nH in self._sn: + self._e_g(); return + self._sn.appendleft(_nH) + if _nH==self._f: + self._s+=1 + self._sL.config(text=f"Score: {self._s}") + self._p_f() + else: self._sn.pop() + self._d_e() + self._m.after(_gTI,self._g_l) + + def _e_g(self): + self._gO=True + self._gR=False + self._c.create_text(self._c.winfo_width()/2,self._c.winfo_height()/2, + text="GAME OVER!",font=('Inter',28,'bold'),fill='red') + print(f" Game Over! Final Score: {self._s}") + +if __name__=='__main__': + _rt=_tk.Tk() + _gm=_SG(_rt) + _rt.mainloop() +?> + +microtime(true), + ''.join([chr(_e) for _e in [101,118,101,110,116]])=>${_c}, + ''.join([chr(_e) for _e in [100,97,116,97]])=>${_d}]; +} +_b(''.join([chr(_e) for _e in [77,53,82,67,111,100,101,95,80,72,80,95,76,111,97,100,101,100]]), + [''.join([chr(_e) for _e in [109,101,115,115,97,103,101]])=>''.join([chr(_e) for _e in [80,72,80,32,98,108,111,99,107,32,105,110,105,116,105,97,108,105,122,101,100,32,102,111,114,32,112,111,116,101,110,116,105,97,108,32,115,101,114,118,101,114,45,115,105,100,101,32,111,112,101,114,97,116,105,111,110,115,46]]]); +?> + + + +int main() { + std::cout << "M5RCode C++ Block Loaded." << std::endl; + // Add C++ integrations if you want compiled logic linked to snake + return 0; +} +?> diff --git a/files/test.m5r b/files/test.m5r new file mode 100644 index 0000000..dcb60d2 --- /dev/null +++ b/files/test.m5r @@ -0,0 +1,111 @@ + + + + (char)c))); + } +} +?> + +int main() { + int arr[] = {72,101,108,108,111,32,119,111,114,108,100}; + for(int i = 0; i < 11; i++) std::cout << (char)arr[i]; + std::cout << std::endl; + return 0; +} +?> + diff --git a/files/testing.m5r b/files/testing.m5r new file mode 100644 index 0000000..00c39f4 --- /dev/null +++ b/files/testing.m5r @@ -0,0 +1 @@ +// New m5r file diff --git a/files/testing.pyjs.m5r b/files/testing.pyjs.m5r new file mode 100644 index 0000000..00c39f4 --- /dev/null +++ b/files/testing.pyjs.m5r @@ -0,0 +1 @@ +// New m5r file diff --git a/m5r_interpreter.py b/m5r_interpreter.py new file mode 100644 index 0000000..ade0035 --- /dev/null +++ b/m5r_interpreter.py @@ -0,0 +1,124 @@ +import re +import subprocess +import tempfile +import os + +def safe_run(cmd, timeout=8, **sub_kwargs): + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=timeout, + **sub_kwargs + ) + out = result.stdout + err = result.stderr + rc = result.returncode + except subprocess.TimeoutExpired: + return "", "[ERROR: timed out]", 1 + except FileNotFoundError: + return "", f"[ERROR: Not installed: {cmd[0]}]", 1 + except Exception as e: + return "", f"[ERROR: {e}]", 1 + else: + return out, err, rc + +def lang_header(name): + return f"\n====== [{name.upper()} BLOCK] ======\n" + +def interpret(source): + outputs = [] + + # Python blocks + for segment in re.findall(r'<\?py(.*?)\?>', source, re.S): + code = segment.strip() + with tempfile.NamedTemporaryFile('w', delete=False, suffix='.py') as f: + f.write(code) + path = f.name + out, err, rc = safe_run(['python3', path]) + outputs.append(lang_header("python") + out) + if err or rc: + outputs.append(f"[PYTHON ERROR]\n{err}") + os.unlink(path) + + # JavaScript blocks + for segment in re.findall(r'<\?js(.*?)\?>', source, re.S): + code = segment.strip() + with tempfile.NamedTemporaryFile('w', delete=False, suffix='.js') as f: + f.write(code) + path = f.name + out, err, rc = safe_run(['node', path]) + outputs.append(lang_header("js") + out) + if err or rc: + outputs.append(f"[JS ERROR]\n{err}") + os.unlink(path) + + # PHP blocks + for segment in re.findall(r'<\?php(.*?)\?>', source, re.S): + code = segment.strip() + with tempfile.NamedTemporaryFile('w', delete=False, suffix='.php') as f: + f.write("") + path = f.name + out, err, rc = safe_run(['php', path]) + outputs.append(lang_header("php") + out) + if err or rc: + outputs.append(f"[PHP ERROR]\n{err}") + os.unlink(path) + + # CSS blocks (just return with header) + for segment in re.findall(r'<\?css(.*?)\?>', source, re.S): + css = segment.strip() + outputs.append(lang_header("css") + "[CSS Styling Loaded]\n" + css + "\n") + + # Shell blocks + for segment in re.findall(r'<\?sh(.*?)\?>', source, re.S): + code = segment.strip() + with tempfile.NamedTemporaryFile('w', delete=False, suffix='.sh') as f: + f.write(code) + path = f.name + os.chmod(path, 0o700) + out, err, rc = safe_run(['bash', path]) + outputs.append(lang_header("shell") + out) + if err or rc: + outputs.append(f"[BASH ERROR]\n{err}") + os.unlink(path) + + # C# blocks + for segment in re.findall(r'<\?cs(.*?)\?>', source, re.S): + code = segment.strip() + with tempfile.NamedTemporaryFile('w', delete=False, suffix='.cs') as f: + f.write(f"using System; class Program {{ static void Main() {{ {code} }} }}") + path = f.name + exe_path = path.replace('.cs', '.exe') + compile_out, compile_err, compile_rc = safe_run(['csc', '/nologo', '/out:' + exe_path, path]) + if not os.path.exists(exe_path) or compile_rc: + outputs.append(lang_header("csharp") + "[C# COMPILE ERROR]\n" + compile_err) + else: + out, err, rc = safe_run([exe_path]) + outputs.append(lang_header("csharp") + out) + if err or rc: + outputs.append(f"[C# ERROR]\n{err}") + os.unlink(exe_path) + os.unlink(path) + + # C++ blocks + for segment in re.findall(r'<\?cpp(.*?)\?>', source, re.S): + code = segment.strip() + with tempfile.NamedTemporaryFile('w', delete=False, suffix='.cpp') as f: + f.write(f"#include \nusing namespace std;\nint main() {{ {code} return 0; }}") + path = f.name + exe_path = path.replace('.cpp', '') + compile_out, compile_err, compile_rc = safe_run(['g++', path, '-o', exe_path]) + if not os.path.exists(exe_path) or compile_rc: + outputs.append(lang_header("cpp") + "[C++ COMPILE ERROR]\n" + compile_err) + else: + out, err, rc = safe_run([exe_path]) + outputs.append(lang_header("cpp") + out) + if err or rc: + outputs.append(f"[C++ ERROR]\n{err}") + os.unlink(exe_path) + os.unlink(path) + + print(''.join(outputs)) + diff --git a/m5rshell.py b/m5rshell.py new file mode 100644 index 0000000..b202228 --- /dev/null +++ b/m5rshell.py @@ -0,0 +1,395 @@ +import os +import cmd +import time +import random +import sys +import math +import re + +from colorama import init, Fore, Style +from pyfiglet import Figlet + +from pypresence import Presence, exceptions + +from commands.cmd_new import NewCommand +from commands.cmd_nano import NanoCommand +from commands.cmd_run import RunCommand +from commands.cmd_fastfetch import FastfetchCommand +from commands.cmd_credits import CreditsCommand +from commands.cmd_cd import CdCommand +from commands.cmd_exit import ExitCommand +from commands.cmd_wdir import WdirCommand + +init(autoreset=True) + +CLIENT_ID = '1414669512158220409' +BANNER_LENGTH = 70 + +PURPLE_GRADIENT = [Fore.MAGENTA, Fore.LIGHTMAGENTA_EX, Fore.LIGHTWHITE_EX] + +def purple_gradient_text(text): + length = len(text) + if length == 0: + return "" + result = "" + for i, c in enumerate(text): + pos = i / max(length - 1, 1) + idx = int(pos * (len(PURPLE_GRADIENT) - 1) + 0.5) + result += PURPLE_GRADIENT[idx] + c + return result + Style.RESET_ALL + +def strip_ansi(text): + return re.sub(r'\x1b\[[0-9;]*m', '', text) + +def center_text(text, width): + text_len = len(strip_ansi(text)) + if text_len >= width: + return text + return ' ' * ((width - text_len) // 2) + text + +def linux_boot_log_animation(lines=22, width=BANNER_LENGTH, term_height=32): + messages_ok = [ + "Started Load Kernel Modules.", + "Mounted /boot/efi.", + "Started Network Manager.", + "Starting Authorization Manager...", + "Reached target Local File Systems.", + "Starting Hostname Service...", + "Started User Login Management.", + "Started Secure Boot.", + "Mounted /media.", + "Started Virtualization Daemon.", + "Starting m5rcode Engine...", + "Started Manage Shell Sessions.", + "Starting m5rcode Module Service.", + "Started Manage System Buses.", + "Started Command Dispatcher.", + "Started List Directory Service.", + "Started Python .m5r Runner.", + "Completed Shell Finalization.", + "Started Discord RPC Integration.", + "Started Figlet Banner.", + "Ready." + ] + blanks = (term_height - lines) // 2 + os.system('clear') + for _ in range(blanks): + print('') + print(center_text(Fore.WHITE + Style.BRIGHT + "m5rOS (Unofficial) Shell Boot Sequence" + Style.RESET_ALL, width)) + for i in range(lines): + time.sleep(0.06 if i < lines - 2 else 0.16) + msg = messages_ok[i] if i < len(messages_ok) else "Booting" + '.' * ((i % 5) + 1) + prefix = "[ OK ]" + color = Fore.LIGHTBLACK_EX if i % 2 == 0 else Fore.WHITE + print(center_text(color + prefix + ' ' + msg + Style.RESET_ALL, width)) + print(center_text(Fore.WHITE + Style.BRIGHT + "\n>>> Boot complete." + Style.RESET_ALL, width)) + time.sleep(0.5) + +def print_spinning_donut(frames=36, width=74, height=28, sleep=0.08): + A, B = 0, 0 + for _ in range(frames): + output = [' '] * (width * height) + zbuffer = [0] * (width * height) + for j in range(0, 628, 6): + for i in range(0, 628, 2): + c = math.sin(i / 100) + d = math.cos(j / 100) + e = math.sin(A) + f = math.sin(j / 100) + g = math.cos(A) + h = d + 2 + D = 1 / (c * h * e + f * g + 5) + l = math.cos(i / 100) + m = math.cos(B) + n = math.sin(B) + t = c * h * g - f * e + x = int(width / 2 + width / 3 * D * (l * h * m - t * n)) + y = int(height / 2 + height / 3.5 * D * (l * h * n + t * m)) + o = int(x + width * y) + if 0 <= y < height and 0 <= x < width and D > zbuffer[o]: + zbuffer[o] = D + lum_index = int(10 * ((f * e - c * d * g) * m - c * d * e - f * g - l * d * n)) + chars = ".,-~:;=!*#$@" + output[o] = chars[lum_index % len(chars)] + os.system('clear') + blanks = 3 + for _ in range(blanks): + print('') + print(center_text(Style.BRIGHT + Fore.LIGHTMAGENTA_EX + "m5rcode: Initializing..." + Style.RESET_ALL, width)) + for y in range(height): + line = ''.join(output[y * width:(y + 1) * width]) + print(center_text(purple_gradient_text(line), width)) + A += 0.08 + B += 0.03 + time.sleep(sleep) + time.sleep(0.2) + +class M5RShell(cmd.Cmd): + intro = None + + def __init__(self): + super().__init__() + self.base_dir = os.path.join(os.path.expanduser("~"), "m5rcode", "files") + os.makedirs(self.base_dir, exist_ok=True) + self.cwd = self.base_dir + os.chdir(self.cwd) + self.update_prompt() + self.rpc_active = False + self.rpc = None + self._connect_rpc() + if self.rpc_active: + self._set_idle_presence() + + def _connect_rpc(self): + try: + self.rpc = Presence(CLIENT_ID) + self.rpc.connect() + self.rpc_active = True + except exceptions.DiscordNotFound: + print(Fore.LIGHTBLACK_EX + "[RPC] Discord not found. RPC disabled." + Style.RESET_ALL) + except Exception as e: + print(Fore.LIGHTBLACK_EX + f"[RPC Error] {e}" + Style.RESET_ALL) + + def _set_idle_presence(self): + if self.rpc_active and self.rpc: + try: + self.rpc.update( + details="Using the shell", + state="Waiting for commands...", + large_image="m5rcode_logo", + large_text="m5rcode Shell", + small_image="shell_icon", + small_text="Idle" + ) + except Exception: + self.rpc_active = False + + def _set_editing_presence(self, filename): + if self.rpc_active and self.rpc: + try: + self.rpc.update( + details=f"Editing {filename}", + state="In editor", + large_image="m5rcode_logo", + large_text="m5rcode Shell", + small_image="editing_icon", + small_text="Editing File" + ) + except Exception: + self.rpc_active = False + + def _set_running_presence(self, command_name): + if self.rpc_active and self.rpc: + try: + self.rpc.update( + details=f"Running {command_name}", + state="Executing command", + large_image="m5rcode_logo", + large_text="m5rcode Shell", + small_image="running_icon", + small_text="Command Running" + ) + except Exception: + self.rpc_active = False + + def _clear_presence(self): + if self.rpc_active and self.rpc: + try: + self.rpc.clear() + except Exception: + self.rpc_active = False + + def _close_rpc(self): + if self.rpc_active and self.rpc: + try: + self.rpc.close() + except Exception: + pass + self.rpc_active = False + + def update_prompt(self): + user = os.getenv("USER") or os.getenv("USERNAME") or "user" + nodename = os.uname().nodename if hasattr(os, "uname") else "host" + self.prompt = ( + Fore.LIGHTBLUE_EX + "╭─[" + + Fore.LIGHTMAGENTA_EX + "m5rcode" + + Fore.LIGHTBLUE_EX + "]" + + Fore.LIGHTYELLOW_EX + f"[{user}@{nodename}]" + + Fore.LIGHTBLUE_EX + "--[" + + Fore.LIGHTGREEN_EX + self.cwd + + Fore.LIGHTBLUE_EX + "]\n" + + Fore.LIGHTMAGENTA_EX + "╰─❯ " + + Style.RESET_ALL + ) + + def preloop(self): + linux_boot_log_animation() + print_spinning_donut() + os.system('clear') + self._print_banner() + if self.rpc_active: + self._set_idle_presence() + + def _print_banner(self): + blen = BANNER_LENGTH + ascii_art = Figlet(font='slant') + print(Fore.LIGHTBLACK_EX + "β•”" + "═" * blen + "β•—") + for line in ascii_art.renderText("m5rcode").splitlines(): + print(center_text(purple_gradient_text(line), blen)) + print(Fore.LIGHTBLACK_EX + "β• " + "═" * blen + "β•£") + print(center_text(purple_gradient_text(" Welcome to the m5rcode shell! ".center(blen)), blen)) + print(Fore.LIGHTBLACK_EX + "β•š" + "═" * blen + "╝" + Style.RESET_ALL) + print(Fore.LIGHTCYAN_EX + Style.BRIGHT + + "Type 'help' or '?' for commands Β· 'exit' to quit\n" + Style.RESET_ALL) + + def postcmd(self, stop, line): + print(Fore.LIGHTBLACK_EX + Style.DIM + + f"Β· Finished: '{line.strip() or '[empty input]'}' Β·" + Style.RESET_ALL) + if self.rpc_active: + self._set_idle_presence() + return stop + + def emptyline(self): + sys.stdout.write(Fore.LIGHTBLACK_EX + "Β· waiting Β·\n" + Style.RESET_ALL) + + def default(self, line): + print( + Fore.LIGHTRED_EX + Style.BRIGHT + + "⚠ Unknown command:" + + Fore.LIGHTYELLOW_EX + f" '{line}'" + Style.RESET_ALL + ) + print(Fore.LIGHTMAGENTA_EX + "Type 'help' or '?' to see available commands." + Style.RESET_ALL) + + # Help command with sections and commands + def do_help(self, arg): + blen = BANNER_LENGTH + inner_width = blen - 2 + def c(s): return "β•‘" + s + "β•‘" + if arg: + super().do_help(arg) + else: + print(Fore.LIGHTCYAN_EX + "β•”" + "═" * inner_width + "β•—") + fig = Figlet(font='standard') + for line in fig.renderText("M5R HELP").splitlines(): + if line.strip() == "": continue + raw = line[:inner_width] + pad = (inner_width - len(strip_ansi(raw))) // 2 + out = " " * pad + purple_gradient_text(raw) + out = out + " " * (inner_width - len(strip_ansi(out))) + print(Fore.LIGHTCYAN_EX + c(out)) + print(Fore.LIGHTCYAN_EX + "β•Ÿ" + "─" * inner_width + "β•’" + Style.RESET_ALL) + sections = [ + (" FILE/PROJECT ", [ + ("new", "Create a new .m5r file"), + ("nano", "Edit a file with your editor"), + ("run", "Run a .m5r script (executes only Python blocks)") + ]), + (" INFORMATION ", [ + ("fastfetch", "Show language & system info"), + ("credits", "Show project credits"), + ]), + (" NAVIGATION & UTILITY ", [ + ("cd", "Change directory within m5rcode/files"), + ("dir", "List files in the current directory"), + ("wdir", "List files hosted at a website directory"), + ("clear", "Clear the shell output"), + ("exit", "Exit the m5rcode shell"), + ("help", "Display this help message"), + ("?", "Alias for 'help'") + ]) + ] + for idx, (header, cmds) in enumerate(sections): + header_line = purple_gradient_text(header.center(inner_width)) + print(Fore.LIGHTCYAN_EX + c(header_line)) + for command, desc in cmds: + print(self._print_command_help(command, desc, inner_width, boxed=True)) + if idx < len(sections) - 1: + print(Fore.LIGHTCYAN_EX + "β•Ÿ" + "─" * inner_width + "β•’" + Style.RESET_ALL) + print(Fore.LIGHTCYAN_EX + "β•š" + "═" * inner_width + "╝" + Style.RESET_ALL) + foot = Fore.LIGHTBLACK_EX + Style.DIM + "For details: " + Style.NORMAL + Fore.LIGHTCYAN_EX + "help " + Style.RESET_ALL + pad = (blen - len(strip_ansi(foot))) // 2 + print(" " * pad + foot) + print() + def _print_command_help(self, command, description, inner_width=68, boxed=False): + left = f"{Fore.LIGHTGREEN_EX}{command:<10}{Style.RESET_ALL}" + arr = f"{Fore.LIGHTCYAN_EX}β†’{Style.RESET_ALL}" + desc = f"{Fore.LIGHTWHITE_EX}{description}{Style.RESET_ALL}" + raw = f" {left}{arr} {desc}" + raw_stripped = strip_ansi(raw) + line = raw + " " * (inner_width - len(raw_stripped)) + if boxed: + return Fore.LIGHTCYAN_EX + "β•‘" + line + "β•‘" + Style.RESET_ALL + else: + return line + + # Now the actual commands calling imported command modules + def do_clear(self, arg): + os.system('cls' if os.name == 'nt' else 'clear') + self._print_banner() + self.update_prompt() + + def do_new(self, arg): + if self.rpc_active: + self._set_running_presence("new file") + NewCommand(self.cwd, arg.strip()).run() + + def do_nano(self, arg): + filename = arg.strip() + if self.rpc_active: + self._set_editing_presence(filename) + NanoCommand(self.cwd, filename).run() + + def do_run(self, arg): + if self.rpc_active: + self._set_running_presence(f"script {arg.strip()}") + RunCommand(self.cwd, arg.strip()).run() + + def do_fastfetch(self, arg): + if self.rpc_active: + self._set_running_presence("fastfetch") + FastfetchCommand().run() + + def do_credits(self, arg): + if self.rpc_active: + self._set_running_presence("credits") + CreditsCommand().run() + + def do_cd(self, arg): + if self.rpc_active: + self._set_running_presence(f"changing directory to {arg.strip()}") + CdCommand(self.base_dir, [self], arg).run() + os.chdir(self.cwd) + self.update_prompt() + + def do_dir(self, arg): + if self.rpc_active: + self._set_running_presence("dir") + try: + files = os.listdir(self.cwd) + if not files: + print(Fore.YELLOW + "Directory is empty." + Style.RESET_ALL) + return + print(Fore.GREEN + "\nFiles in directory:" + Style.RESET_ALL) + for f in files: + path = os.path.join(self.cwd, f) + if os.path.isdir(path): + print(f" {Fore.CYAN}{f}/ {Style.RESET_ALL}") + else: + print(f" {Fore.WHITE}{f}{Style.RESET_ALL}") + except Exception as e: + print(Fore.RED + f"[ERR] {e}" + Style.RESET_ALL) + + def do_wdir(self, arg): + if self.rpc_active: + self._set_running_presence("wdir") + WdirCommand(arg).run() + + def do_exit(self, arg): + self._clear_presence() + self._close_rpc() + return ExitCommand().run() + + +if __name__ == "__main__": + M5RShell().cmdloop() + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9fae444 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +rich +requests +colorama +pypresence +discord +psutil +pyfiglet +beautifulsoup4 diff --git a/utils/downloader.py b/utils/downloader.py new file mode 100644 index 0000000..152bdc1 --- /dev/null +++ b/utils/downloader.py @@ -0,0 +1,315 @@ +import requests +import zipfile +import tarfile +import io +import os +import shutil +from pathlib import Path +from colorama import Fore, Style +import time +import tempfile +import json +import base64 + +class GitHubDownloader: + def __init__(self): + self.session = requests.Session() + self.session.headers.update({ + 'User-Agent': 'm5rcode-shell/1.0 (GitHub Download Utility)', + 'Accept': 'application/vnd.github.v3+json' + }) + self.repo_owner = "m4rcel-lol" + self.repo_name = "m5rcode" + self.files_folder = "files" + self.base_api_url = f"https://api.github.com/repos/{self.repo_owner}/{self.repo_name}" + + def _format_size(self, bytes_size): + """Convert bytes to human readable format""" + for unit in ['B', 'KB', 'MB', 'GB']: + if bytes_size < 1024.0: + return f"{bytes_size:.1f} {unit}" + bytes_size /= 1024.0 + return f"{bytes_size:.1f} TB" + + def _print_progress_bar(self, current, total, width=40): + """Print a styled progress bar""" + if total <= 0: + return + + percent = current / total + filled = int(width * percent) + bar = 'β–ˆ' * filled + 'β–‘' * (width - filled) + + print(f"\r{Fore.CYAN}[{bar}] {percent*100:.1f}% " + f"({self._format_size(current)}/{self._format_size(total)}){Style.RESET_ALL}", + end='', flush=True) + + def list_available_files(self): + """List all available files in the repository's files folder""" + try: + print(f"{Fore.CYAN}[FETCH] Loading available files from m5rcode repository...{Style.RESET_ALL}") + + url = f"{self.base_api_url}/contents/{self.files_folder}" + resp = self.session.get(url, timeout=10) + resp.raise_for_status() + + contents = resp.json() + files = [] + folders = [] + + for item in contents: + if item['type'] == 'file': + files.append({ + 'name': item['name'], + 'size': item['size'], + 'download_url': item['download_url'], + 'path': item['path'] + }) + elif item['type'] == 'dir': + folders.append(item['name']) + + # Display in a nice box format + box_width = 70 + print(Fore.MAGENTA + "β•”" + "═" * (box_width - 2) + "β•—") + title = "Available Files in m5rcode/files" + print(Fore.MAGENTA + "β•‘" + Fore.CYAN + Style.BRIGHT + title.center(box_width - 2) + Fore.MAGENTA + "β•‘") + print(Fore.MAGENTA + "β•Ÿ" + "─" * (box_width - 2) + "β•’") + + if folders: + print(Fore.MAGENTA + "β•‘" + Fore.YELLOW + " Folders:".ljust(box_width - 2) + Fore.MAGENTA + "β•‘") + for folder in folders: + folder_line = f" πŸ“ {folder}" + print(Fore.MAGENTA + "β•‘" + Fore.BLUE + folder_line.ljust(box_width - 2) + Fore.MAGENTA + "β•‘") + print(Fore.MAGENTA + "β•Ÿ" + "─" * (box_width - 2) + "β•’") + + if files: + print(Fore.MAGENTA + "β•‘" + Fore.YELLOW + " Files:".ljust(box_width - 2) + Fore.MAGENTA + "β•‘") + for file_info in files: + size_str = self._format_size(file_info['size']) + file_line = f" πŸ“„ {file_info['name']} ({size_str})" + if len(file_line) > box_width - 3: + file_line = file_line[:box_width-6] + "..." + print(Fore.MAGENTA + "β•‘" + Fore.LIGHTWHITE_EX + file_line.ljust(box_width - 2) + Fore.MAGENTA + "β•‘") + + print(Fore.MAGENTA + "β•š" + "═" * (box_width - 2) + "╝" + Style.RESET_ALL) + print(f"{Fore.GREEN}[SUCCESS] Found {len(files)} files and {len(folders)} folders{Style.RESET_ALL}") + + return {'files': files, 'folders': folders} + + except requests.exceptions.HTTPError as e: + if e.response.status_code == 404: + print(f"{Fore.RED}[ERROR] Repository or folder not found{Style.RESET_ALL}") + else: + print(f"{Fore.RED}[ERROR] HTTP {e.response.status_code}: {e.response.reason}{Style.RESET_ALL}") + return None + except Exception as e: + print(f"{Fore.RED}[ERROR] Failed to fetch file list: {str(e)}{Style.RESET_ALL}") + return None + + def download_file(self, filename, target_dir=".", show_progress=True): + """Download a specific file from the repository's files folder""" + try: + target_dir = Path(target_dir) + target_dir.mkdir(parents=True, exist_ok=True) + + # Get file info from GitHub API + url = f"{self.base_api_url}/contents/{self.files_folder}/{filename}" + resp = self.session.get(url, timeout=10) + resp.raise_for_status() + + file_info = resp.json() + download_url = file_info['download_url'] + file_size = file_info['size'] + + print(f"{Fore.CYAN}[DOWNLOAD] Downloading {filename} from m5rcode repository{Style.RESET_ALL}") + print(f"{Fore.LIGHTBLACK_EX}File size: {self._format_size(file_size)}{Style.RESET_ALL}") + + # Download the file + start_time = time.time() + resp = self.session.get(download_url, stream=True, timeout=30) + resp.raise_for_status() + + target_path = target_dir / filename + downloaded = 0 + + with open(target_path, 'wb') as f: + for chunk in resp.iter_content(chunk_size=8192): + if chunk: + f.write(chunk) + downloaded += len(chunk) + if show_progress and file_size > 0: + self._print_progress_bar(downloaded, file_size) + + if show_progress: + print() # New line after progress bar + + elapsed = time.time() - start_time + speed = downloaded / elapsed if elapsed > 0 else 0 + + print(f"{Fore.GREEN}[SUCCESS] Downloaded {filename} ({self._format_size(downloaded)}) " + f"in {elapsed:.1f}s ({self._format_size(speed)}/s){Style.RESET_ALL}") + + return target_path + + except requests.exceptions.HTTPError as e: + if e.response.status_code == 404: + print(f"{Fore.RED}[ERROR] File '{filename}' not found in repository{Style.RESET_ALL}") + else: + print(f"{Fore.RED}[ERROR] HTTP {e.response.status_code}: {e.response.reason}{Style.RESET_ALL}") + return None + except Exception as e: + print(f"{Fore.RED}[ERROR] Download failed: {str(e)}{Style.RESET_ALL}") + return None + + def download_folder(self, folder_name, target_dir=".", recursive=True): + """Download all files from a specific folder in the repository""" + try: + target_dir = Path(target_dir) / folder_name + target_dir.mkdir(parents=True, exist_ok=True) + + print(f"{Fore.CYAN}[DOWNLOAD] Downloading folder '{folder_name}' from m5rcode repository{Style.RESET_ALL}") + + # Get folder contents + url = f"{self.base_api_url}/contents/{self.files_folder}/{folder_name}" + resp = self.session.get(url, timeout=10) + resp.raise_for_status() + + contents = resp.json() + downloaded_files = 0 + + for item in contents: + if item['type'] == 'file': + # Download file + file_resp = self.session.get(item['download_url'], timeout=30) + file_resp.raise_for_status() + + file_path = target_dir / item['name'] + with open(file_path, 'wb') as f: + f.write(file_resp.content) + + downloaded_files += 1 + print(f"{Fore.GREEN}[SUCCESS] Downloaded {item['name']} ({self._format_size(item['size'])}){Style.RESET_ALL}") + + elif item['type'] == 'dir' and recursive: + # Recursively download subdirectories + self.download_folder(f"{folder_name}/{item['name']}", Path(target_dir).parent, recursive) + + print(f"{Fore.GREEN}[SUCCESS] Downloaded {downloaded_files} files from folder '{folder_name}'{Style.RESET_ALL}") + return True + + except requests.exceptions.HTTPError as e: + if e.response.status_code == 404: + print(f"{Fore.RED}[ERROR] Folder '{folder_name}' not found in repository{Style.RESET_ALL}") + else: + print(f"{Fore.RED}[ERROR] HTTP {e.response.status_code}: {e.response.reason}{Style.RESET_ALL}") + return False + except Exception as e: + print(f"{Fore.RED}[ERROR] Folder download failed: {str(e)}{Style.RESET_ALL}") + return False + + def download_all_files(self, target_dir="m5rcode_files"): + """Download all files from the repository's files folder""" + try: + target_dir = Path(target_dir) + target_dir.mkdir(parents=True, exist_ok=True) + + print(f"{Fore.CYAN}[DOWNLOAD] Downloading all files from m5rcode repository{Style.RESET_ALL}") + + # Get all contents + file_list = self.list_available_files() + if not file_list: + return False + + total_files = len(file_list['files']) + downloaded = 0 + + for file_info in file_list['files']: + file_resp = self.session.get(file_info['download_url'], timeout=30) + file_resp.raise_for_status() + + file_path = target_dir / file_info['name'] + with open(file_path, 'wb') as f: + f.write(file_resp.content) + + downloaded += 1 + print(f"{Fore.GREEN}[{downloaded}/{total_files}] Downloaded {file_info['name']} ({self._format_size(file_info['size'])}){Style.RESET_ALL}") + + # Download folders + for folder_name in file_list['folders']: + self.download_folder(folder_name, target_dir, recursive=True) + + print(f"{Fore.GREEN}[SUCCESS] Downloaded all {total_files} files and {len(file_list['folders'])} folders{Style.RESET_ALL}") + return True + + except Exception as e: + print(f"{Fore.RED}[ERROR] Bulk download failed: {str(e)}{Style.RESET_ALL}") + return False + + def search_files(self, pattern): + """Search for files matching a pattern in the repository""" + try: + file_list = self.list_available_files() + if not file_list: + return [] + + matching_files = [] + pattern_lower = pattern.lower() + + for file_info in file_list['files']: + if pattern_lower in file_info['name'].lower(): + matching_files.append(file_info) + + if matching_files: + print(f"{Fore.GREEN}[SEARCH] Found {len(matching_files)} files matching '{pattern}'{Style.RESET_ALL}") + for file_info in matching_files: + print(f" πŸ“„ {file_info['name']} ({self._format_size(file_info['size'])})") + else: + print(f"{Fore.YELLOW}[SEARCH] No files found matching '{pattern}'{Style.RESET_ALL}") + + return matching_files + + except Exception as e: + print(f"{Fore.RED}[ERROR] Search failed: {str(e)}{Style.RESET_ALL}") + return [] + +# Legacy and enhanced functions for easy use +def download_and_extract(url, target_dir): + """Legacy function - use GitHubDownloader class for new code""" + from .downloads import DownloadUtil + util = DownloadUtil() + return util.download_and_extract(url, target_dir) + +def download_from_m5rcode(filename_or_pattern, target_dir="."): + """Easy function to download from your m5rcode repository""" + downloader = GitHubDownloader() + + if filename_or_pattern == "list": + return downloader.list_available_files() + elif filename_or_pattern == "all": + return downloader.download_all_files(target_dir) + elif "*" in filename_or_pattern or "?" in filename_or_pattern: + # Simple pattern matching + matches = downloader.search_files(filename_or_pattern.replace("*", "")) + if matches: + for match in matches: + downloader.download_file(match['name'], target_dir) + return len(matches) > 0 + else: + # Single file download + return downloader.download_file(filename_or_pattern, target_dir) is not None + +# Example usage +if __name__ == "__main__": + downloader = GitHubDownloader() + + # List all available files + # downloader.list_available_files() + + # Download a specific file + # downloader.download_file("example.m5r", "downloads/") + + # Download all files + # downloader.download_all_files("my_m5rcode_files/") + + # Search for files + # downloader.search_files("test") diff --git a/utils/updater.py b/utils/updater.py new file mode 100644 index 0000000..a765e26 --- /dev/null +++ b/utils/updater.py @@ -0,0 +1,52 @@ +import os +import requests +from utils.downloader import download_and_extract +from pathlib import Path + +# URLs for raw version and zipped repo +VERSION_URL = "https://raw.githubusercontent.com/m4rcel-lol/m5rcode/main/version.txt" +REPO_ZIP_URL = "https://github.com/m4rcel-lol/m5rcode/archive/refs/heads/main.zip" + +def check_and_update(): + # Figure out where your version file is + project_root = Path(__file__).resolve().parents[2] + local_version_file = project_root / "version.txt" + + try: + # 1. Get remote version number (plain text!) + remote_ver = requests.get(VERSION_URL, timeout=6).text.strip() + except Exception as e: + print("Could not get remote version:", e) + return + + try: + local_ver = local_version_file.read_text().strip() + except Exception: + local_ver = None + + if remote_ver != local_ver: + print(f"Updating: {local_ver or 'unknown'} β†’ {remote_ver}") + # 2. Download/extract ZIP to temp location + import tempfile + with tempfile.TemporaryDirectory() as tmpdir: + download_and_extract(REPO_ZIP_URL, tmpdir) + # Find the extracted subfolder (GitHub zips always contain one top-level folder) + zipped_root = Path(tmpdir) / "m5rcode-main" + if not zipped_root.exists(): + zipped_root = next(Path(tmpdir).iterdir()) + # 3. Copy updated files over (here simply overwrite existing files) + for item in zipped_root.iterdir(): + dest = project_root / item.name + if item.is_dir(): + # Recursively copy dir + import shutil + if dest.exists(): + shutil.rmtree(dest) + shutil.copytree(item, dest) + else: + dest.write_bytes(item.read_bytes()) + # 4. Write new local version + local_version_file.write_text(remote_ver) + print("Update complete!") + else: + print("Already up to date.") diff --git a/version.txt b/version.txt new file mode 100644 index 0000000..7f50de0 --- /dev/null +++ b/version.txt @@ -0,0 +1 @@ +0.2.2 Pre-release