From c9b2be17240672962394a4987945e70fe5c28ac2 Mon Sep 17 00:00:00 2001 From: Zane V Date: Sun, 16 Nov 2025 11:11:18 -0500 Subject: [PATCH] Update release version to v0.1.2.b and refactor terminal.py for improved functionality - Updated the release version in index.html and version.txt to v0.1.2.b. - Refactored terminal.py to consolidate system detection and plugin management functionalities, enhancing code organization and maintainability. - Removed deprecated modules and streamlined the command execution process for better performance. --- index.html | 2 +- terminal.py | 706 ++++++++++++++++++-- version.txt | 2 +- zdtt/__init__.py | 4 - zdtt/__pycache__/__init__.cpython-312.pyc | Bin 129 -> 0 bytes zdtt/__pycache__/config.cpython-312.pyc | Bin 8016 -> 0 bytes zdtt/__pycache__/plugins.cpython-312.pyc | Bin 5103 -> 0 bytes zdtt/__pycache__/shell.cpython-312.pyc | Bin 3707 -> 0 bytes zdtt/__pycache__/status_bar.cpython-312.pyc | Bin 8973 -> 0 bytes zdtt/__pycache__/ui.cpython-312.pyc | Bin 5415 -> 0 bytes zdtt/config.py | 166 ----- zdtt/plugins.py | 106 --- zdtt/shell.py | 82 --- zdtt/status_bar.py | 154 ----- zdtt/ui.py | 85 --- 15 files changed, 641 insertions(+), 666 deletions(-) delete mode 100644 zdtt/__init__.py delete mode 100644 zdtt/__pycache__/__init__.cpython-312.pyc delete mode 100644 zdtt/__pycache__/config.cpython-312.pyc delete mode 100644 zdtt/__pycache__/plugins.cpython-312.pyc delete mode 100644 zdtt/__pycache__/shell.cpython-312.pyc delete mode 100644 zdtt/__pycache__/status_bar.cpython-312.pyc delete mode 100644 zdtt/__pycache__/ui.cpython-312.pyc delete mode 100644 zdtt/config.py delete mode 100644 zdtt/plugins.py delete mode 100644 zdtt/shell.py delete mode 100644 zdtt/status_bar.py delete mode 100644 zdtt/ui.py diff --git a/index.html b/index.html index 5139e23..0c9af29 100644 --- a/index.html +++ b/index.html @@ -46,7 +46,7 @@

Current release

-

v0.1.2.a.9

+

v0.1.2.b

Supported families

diff --git a/terminal.py b/terminal.py index 158557e..cc867cf 100644 --- a/terminal.py +++ b/terminal.py @@ -23,39 +23,39 @@ import urllib.request import urllib.error import time as time_module -# Ensure local 'zdtt' package is importable both from repo and installed locations -try: - # Probe a quick import to determine availability - import zdtt # type: ignore -except Exception: - script_dir = os.path.dirname(os.path.abspath(__file__)) - candidates = [ - script_dir, - os.path.abspath(os.path.join(script_dir, '..')), - os.path.join(script_dir, 'zdtt'), - '/home/zane/ZDTT', - ] - for path_candidate in candidates: - if path_candidate and os.path.isdir(path_candidate) and path_candidate not in sys.path: - sys.path.insert(0, path_candidate) - # Best-effort: ignore failure here; regular imports will raise if still missing - try: - import zdtt # type: ignore - except Exception: - pass -from zdtt.plugins import ( - PROTECTED_COMMANDS as PLUGIN_PROTECTED_COMMANDS, - validate_plugin_ast as plugins_validate_ast, - validate_plugin_commands as plugins_validate_commands, - move_to_quarantine as plugins_move_to_quarantine, -) -from zdtt.config import ( - check_system_compatibility, -) -from zdtt import status_bar as sb -from zdtt.shell import execute_system_command as shell_execute -from zdtt.ui import display_banner as ui_display_banner, get_prompt as ui_get_prompt +SUPPORTED_DEBIAN_IDS = { + 'debian', + 'ubuntu', + 'linuxmint', + 'mint', + 'pop', + 'pop-os', + 'pop_os', + 'elementary', + 'zorin', + 'kali', + 'parrot', + 'mx', + 'mx-linux', + 'deepin', + 'peppermint', + 'raspbian', + 'neon', +} + +SUPPORTED_ARCH_IDS = { + 'arch', + 'archlinux', + 'manjaro', + 'endeavouros', + 'endeavour', + 'arcolinux', + 'garuda', + 'artix', + 'blackarch', + 'chakra', +} STATUS_BAR_COLORS = { 'blue': ('44', '97'), @@ -68,15 +68,182 @@ STATUS_BAR_COLORS = { 'black': ('40', '97'), } -# Protected command names that plugins cannot override (sourced from plugins module) -PROTECTED_COMMANDS = PLUGIN_PROTECTED_COMMANDS - -# Helper for status bar module to get current color codes -def _STATUS_BAR_COLORS_LOOKUP(self): - return STATUS_BAR_COLORS.get(self.status_bar_color, ('44', '97')) +# Protected command names that plugins cannot override +PROTECTED_COMMANDS = { + 'ssh', 'sudo', 'su', 'cp', 'mv', 'rm', 'ls', 'cat', 'chmod', 'chown', + 'history', 'zps', 'zdtt', 'pip', 'python', 'python3', 'curl', 'wget' +} -## moved to zdtt.config +def _parse_os_release(): + """Return a dict of fields from /etc/os-release if available""" + data = {} + try: + with open('/etc/os-release', 'r') as f: + for line in f: + line = line.strip() + if not line or line.startswith('#') or '=' not in line: + continue + key, value = line.split('=', 1) + value = value.strip().strip('"') + data[key] = value + except FileNotFoundError: + pass + return data + + +def _collect_tokens(*values): + """Normalize distro identifiers for comparison""" + tokens = set() + for value in values: + if not value: + continue + normalized = value.replace('"', '').strip().lower() + if not normalized: + continue + # Keep the raw normalized value plus its dashed/underscored variants split + tokens.add(normalized) + delimiters_replaced = normalized.replace('-', ' ').replace('_', ' ') + for part in delimiters_replaced.split(): + if part: + tokens.add(part) + return tokens + + +def _detect_supported_distro(): + """Return distro identifier: 'debian', 'arch', or 'other'""" + if os.path.exists('/etc/debian_version'): + return 'debian' + + arch_markers = ( + '/etc/arch-release', + '/etc/artix-release', + ) + if any(os.path.exists(path) for path in arch_markers): + return 'arch' + + os_release = _parse_os_release() + tokens = _collect_tokens(os_release.get('ID'), os_release.get('ID_LIKE')) + + if tokens & SUPPORTED_DEBIAN_IDS: + return 'debian' + if tokens & SUPPORTED_ARCH_IDS: + return 'arch' + + # Fallback to package manager detection + if shutil.which('apt-get'): + return 'debian' + if shutil.which('pacman'): + return 'arch' + + return 'other' + + +def _prompt_distro_override(detected_distro): + """Allow user to override detected distro.""" + label_map = { + 'debian': "Debian-based", + 'arch': "Arch-based", + 'other': "Unsupported/Other", + } + print("=" * 60) + print(f"Detected distribution: {label_map.get(detected_distro, 'Unknown')}") + print("If this is incorrect, enter one of: debian / arch / other.") + print("Press Enter to accept the detected value.") + override = input("Override distribution (leave blank to keep): ").strip().lower() + + if override in ('debian', 'arch', 'other'): + return override + + if override: + print(f"Unknown override '{override}'. Using detected value.") + + return detected_distro + + +def _load_saved_distro(): + """Load saved distro preference from config file.""" + config_file = os.path.expanduser("~/.zdtt/config.json") + try: + with open(config_file, 'r') as f: + data = json.load(f) + saved_distro = data.get('distro') + if saved_distro in ('debian', 'arch', 'other'): + return saved_distro + except (FileNotFoundError, json.JSONDecodeError, KeyError): + pass + return None + +def _save_distro_preference(distro): + """Save distro preference to config file.""" + config_file = os.path.expanduser("~/.zdtt/config.json") + os.makedirs(os.path.dirname(config_file), exist_ok=True) + + data = {} + try: + with open(config_file, 'r') as f: + data = json.load(f) + except (FileNotFoundError, json.JSONDecodeError): + data = {} + + data['distro'] = distro + + with open(config_file, 'w') as f: + json.dump(data, f, indent=2) + +def check_system_compatibility(): + """Detect supported distributions and warn when unsupported""" + # Check for saved distro preference first + saved_distro = _load_saved_distro() + if saved_distro: + # Use saved preference, skip prompts + return saved_distro + + # Check if running on Linux + if sys.platform != 'linux': + print("=" * 60) + print("⚠️ WARNING: ZDTT Terminal is designed for Linux systems") + print(f" Detected platform: {sys.platform}") + print("=" * 60) + print("ZDTT may not work correctly on your system.") + print("Some features may be unavailable or broken.") + print() + response = input("Continue anyway? (yes/no): ").strip().lower() + if response != 'yes': + print("Installation cancelled.") + sys.exit(0) + distro = 'other' + _save_distro_preference(distro) + return distro + + # Detect supported distributions + distro = _detect_supported_distro() + + if distro not in ('debian', 'arch'): + # Unsupported distribution + print("=" * 60) + print("⚠️ WARNING: Unsupported Distribution Detected") + print("=" * 60) + print("ZDTT Terminal is optimized for Debian-based and Arch Linux systems.") + print() + print("Running on your current system may result in:") + print(" • Some commands may not work as expected") + print(" • Auto-install features may fail") + print(" • Reduced plugin compatibility") + print(" • Package management commands unavailable") + print() + response = input("Continue installation? (yes/no): ").strip().lower() + if response != 'yes': + print("Installation cancelled.") + sys.exit(0) + + # Offer override regardless of detection + distro = _prompt_distro_override(distro) + + # Save the selected distro preference + _save_distro_preference(distro) + + return distro class ZDTTTerminal: @@ -106,8 +273,6 @@ class ZDTTTerminal: self.safe_mode = False # Safe mode flag (no plugins loaded) self.quarantine_warnings = [] # Store warnings for plugins quarantined at startup self.trusted_plugins = set() # Plugins allowed to use imports - # Expose STATUS_BAR_COLORS lookup to status bar module - self.STATUS_BAR_COLORS_LOOKUP = lambda: _STATUS_BAR_COLORS_LOOKUP(self) # Setup logging for plugins self.setup_logging() @@ -310,49 +475,272 @@ class ZDTTTerminal: def display_banner(self): """Display the ZDTT ASCII art banner (or custom banner if available)""" - ui_display_banner(self) + print() + + # Check terminal size to see if banner will fit + try: + term_size = shutil.get_terminal_size() + # Banner is 44 chars wide and 11 lines tall (including version) + # Add extra space for compatibility warning if needed + min_height = 13 if not self.is_supported else 11 + min_width = 44 + + if term_size.columns < min_width or term_size.lines < min_height: + # Terminal too small, skip banner and just show minimal header + print(f"ZDTT Terminal v{self.version}") + if not self.is_supported: + print("⚠️ Unsupported system - limited support") + print() + return + except Exception: + # If we can't get terminal size, display the banner anyway + pass + + # Check for custom banner + if os.path.exists(self.banner_file): + try: + with open(self.banner_file, 'r') as f: + custom_banner = f.read() + # Add version at the bottom if not already present + if '{version}' in custom_banner: + custom_banner = custom_banner.replace('{version}', self.version) + print(custom_banner) + # Show warning for unsupported systems + if not self.is_supported: + self._show_compatibility_warning() + return + except Exception as e: + logging.error(f"Failed to load custom banner: {e}") + # Fall through to default banner + + # Default banner + banner = f""" +░█████████ ░███████ ░██████████░██████████ + ░██ ░██ ░██ ░██ ░██ + ░██ ░██ ░██ ░██ ░██ + ░███ ░██ ░██ ░██ ░██ + ░██ ░██ ░██ ░██ ░██ + ░██ ░██ ░██ ░██ ░██ +░█████████ ░███████ ░██ ░██ + + +ZDTT Terminal v{self.version} +""" + print(banner) + + # Show warning for unsupported systems after banner + if not self.is_supported: + self._show_compatibility_warning() def _show_compatibility_warning(self): """Show compatibility warning for unsupported systems""" - from zdtt.ui import _show_compatibility_warning as ui_warn - ui_warn(self) + if self.is_supported: + return + + print() + print("⚠️ Running on unsupported system - limited support") + print(" Tested on Debian-based and Arch Linux distributions.") + print() def initialize_status_bar(self): """Reserve the first terminal row and start the status bar thread.""" - sb.initialize_status_bar(self) + self._set_scroll_region() + self._start_status_bar_thread() + self._render_status_bar() def shutdown_status_bar(self): """Stop the status bar thread and release terminal state.""" - sb.shutdown_status_bar(self) + self.status_bar_stop_event.set() + if self.status_bar_thread and self.status_bar_thread.is_alive(): + self.status_bar_thread.join(timeout=0.5) + self.status_bar_thread = None + self._reset_scroll_region() def _start_status_bar_thread(self): - sb.start_status_bar_thread(self) + if self.status_bar_thread and self.status_bar_thread.is_alive(): + return + self.status_bar_stop_event.clear() + self.status_bar_thread = threading.Thread( + target=self._status_bar_loop, + name="ZDTTStatusBar", + daemon=True, + ) + self.status_bar_thread.start() def _status_bar_loop(self): - sb.status_bar_loop(self) + while not self.status_bar_stop_event.is_set(): + self._render_status_bar() + if self.status_bar_stop_event.wait(2): + break def _render_status_bar(self): """Render a single-line status bar with branding and time.""" - sb.render_status_bar(self) + try: + # Get terminal size first to ensure we don't write beyond bounds + try: + term_size = shutil.get_terminal_size() + max_width = term_size.columns + except Exception: + max_width = 80 # Fallback + + bar_text = self._build_status_bar_text() + + # Ensure bar_text doesn't exceed terminal width (safety check) + # Count visible characters (approximate - ANSI codes don't count) + # This is a rough check, but better than nothing + if len(bar_text) > max_width * 3: # Allow for ANSI codes (rough estimate) + # Rebuild with safer width + bar_text = self._build_status_bar_text() + + sys.stdout.write("\033[s") # Save cursor position + sys.stdout.write("\033[1;1H") # Move to first row, first column + sys.stdout.write("\033[2K") # Clear the entire line + sys.stdout.write("\033[0m") # Reset all attributes + sys.stdout.write(bar_text) + sys.stdout.write("\033[0m") # Ensure reset at end + # Move cursor to end of line to prevent wrapping issues + sys.stdout.write(f"\033[{max_width}G") # Move to column max_width + sys.stdout.write("\033[u") # Restore cursor + sys.stdout.flush() + except Exception: + # Fallback: just skip rendering if there's an error + pass def _build_status_bar_text(self): """Render a single-line status bar with enhanced branding and time.""" - return sb.build_status_bar_text(self) + left_text = f"{self.COLOR_BOLD}ZDTT{self.COLOR_RESET} by {self.COLOR_BOLD}ZaneDev{self.COLOR_RESET}" + time_str = datetime.now().strftime("%I:%M %p") + plain_left = "ZDTT by ZaneDev" + plain_time = time_str + + try: + # Always get fresh terminal size to handle resizes + term_size = shutil.get_terminal_size() + width = term_size.columns + # Safety: ensure width is at least 1 + width = max(1, width) + except Exception: + # Fallback to minimum width if we can't get terminal size + width = max(1, len(plain_left) + len(plain_time) + 6) + + # Calculate the minimum content width (plain text only, no ANSI codes) + # Format: " ZDTT by ZaneDev | TIME " + min_content_width = len(plain_left) + len(plain_time) + 5 # 5 = spaces + separator + + # Calculate padding to fill the line + if width < min_content_width: + # Terminal too narrow, use minimum padding + padding = 0 + else: + padding = width - min_content_width + + # Build the content (plain text calculation) + separator = f"{self.COLOR_DIM}│{self.COLOR_RESET}" + bar_content = f" {left_text} {' ' * padding}{separator} {self.COLOR_BRIGHT_WHITE}{time_str}{self.COLOR_RESET} " + + # Calculate actual display length (plain text only) + actual_display_len = len(plain_left) + len(plain_time) + padding + 5 + + # Ensure we fill exactly to terminal width (but never exceed it) + if actual_display_len < width: + # Add trailing spaces to fill exactly to width + trailing_spaces = width - actual_display_len + bar_content = bar_content.rstrip() + ' ' * trailing_spaces + elif actual_display_len > width: + # We exceeded width, recalculate with less padding + padding = max(0, width - min_content_width) + bar_content = f" {left_text} {' ' * padding}{separator} {self.COLOR_BRIGHT_WHITE}{time_str}{self.COLOR_RESET} " + actual_display_len = len(plain_left) + len(plain_time) + padding + 5 + if actual_display_len < width: + trailing_spaces = width - actual_display_len + bar_content = bar_content.rstrip() + ' ' * trailing_spaces + else: + # Still too wide, trim the time if necessary + if width < len(plain_left) + 10: + # Very narrow terminal, just show minimal content + bar_content = f" {left_text} {separator} {self.COLOR_BRIGHT_WHITE}{time_str[:8]}{self.COLOR_RESET} " + bar_content = bar_content[:width] if len(bar_content) > width else bar_content + + # Final safety check: ensure we never exceed terminal width + # This is approximate since ANSI codes don't count, but better than nothing + bg_code, fg_code = STATUS_BAR_COLORS.get(self.status_bar_color, ('44', '97')) + result = f"\033[{bg_code}m\033[{fg_code}m{bar_content}\033[0m" + + # If the result is suspiciously long, truncate it + # (rough heuristic: ANSI codes add ~30-50 chars, so if result > width*2, it's probably wrong) + if len(result) > width * 2: + # Emergency fallback: simple status bar + simple_bar = f" ZDTT by ZaneDev | {time_str} " + simple_bar = simple_bar[:width] if len(simple_bar) > width else simple_bar.ljust(width) + result = f"\033[{bg_code}m\033[{fg_code}m{simple_bar}\033[0m" + + return result def _set_scroll_region(self): """Reserve the top row for the status bar.""" - sb.set_scroll_region(self) + try: + rows = shutil.get_terminal_size().lines + rows = max(rows, 2) + sys.stdout.write(f"\033[2;{rows}r") + sys.stdout.write("\033[1;1H") + sys.stdout.write("\033[2K") + sys.stdout.write("\033[2;1H") + sys.stdout.flush() + self.scroll_region_set = True + except Exception: + self.scroll_region_set = False def _reset_scroll_region(self): """Restore default scrolling behavior.""" - sb.reset_scroll_region(self) + if not self.scroll_region_set: + return + sys.stdout.write("\033[r") + sys.stdout.flush() + self.scroll_region_set = False def _handle_resize(self, signum=None, frame=None): """Handle terminal resize event (SIGWINCH).""" - sb.handle_resize(self, signum, frame) - - def _spawn_thread(self, target, name): - return threading.Thread(target=target, name=name, daemon=True) + # Use a lock to prevent race conditions + if not self.resize_lock.acquire(blocking=False): + # If we can't acquire the lock immediately, skip this resize + # (another resize is already being handled) + return + + try: + # Small delay to let terminal settle after resize + time_module.sleep(0.05) + + # Reset scroll region first to clear any corrupted state + self._reset_scroll_region() + + # Update scroll region with new terminal size + self._set_scroll_region() + + # Clear the status bar line completely before redrawing + try: + sys.stdout.write("\033[1;1H") # Move to first row + sys.stdout.write("\033[2K") # Clear the entire line + sys.stdout.write("\033[0m") # Reset attributes + sys.stdout.flush() + except Exception: + pass + + # Force immediate status bar refresh + self._render_status_bar() + + # Ensure cursor is in a safe position + try: + term_size = shutil.get_terminal_size() + sys.stdout.write(f"\033[{term_size.lines};1H") # Move to last line, first column + sys.stdout.flush() + except Exception: + pass + + except Exception: + # Silently fail if resize handling fails + pass + finally: + self.resize_lock.release() def setup_readline(self): """Setup readline for history and tab completion""" @@ -412,19 +800,92 @@ class ZDTTTerminal: return None def _validate_plugin_ast(self, plugin_code, plugin_name): - # Delegate to plugins module - return plugins_validate_ast(plugin_code, plugin_name) + """ + Validate plugin AST to ensure no top-level code execution. + Only allows: imports, function definitions, class definitions, and docstrings. + """ + try: + tree = ast.parse(plugin_code) + except SyntaxError as e: + raise ValueError(f"Plugin has syntax errors: {e}") + + # Check module body directly - this is the most reliable way + if not isinstance(tree, ast.Module): + raise ValueError("Plugin must be a valid Python module") + + for stmt in tree.body: + # Allow imports + if isinstance(stmt, (ast.Import, ast.ImportFrom)): + continue + # Allow function definitions + if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef)): + continue + # Allow class definitions + if isinstance(stmt, ast.ClassDef): + continue + # Allow docstrings (Expr with string constant) + if isinstance(stmt, ast.Expr): + # Check if it's a string literal (docstring) + if isinstance(stmt.value, (ast.Constant, ast.Str)): + # For Python 3.8+, use ast.Constant; for older versions, use ast.Str + if isinstance(stmt.value, ast.Constant): + if isinstance(stmt.value.value, str): + continue + elif isinstance(stmt.value, ast.Str): + continue + # Anything else is forbidden (assignments, function calls, loops, etc.) + raise ValueError( + f"Plugin contains forbidden top-level statement: {stmt.__class__.__name__}. " + "Plugins can only contain imports, functions, classes, and docstrings. " + "No top-level code execution is allowed." + ) + + return True def _move_to_quarantine(self, plugin_file, reason): - # Delegate to plugins module - path = plugins_move_to_quarantine(plugin_file, self.quarantine_dir, logging) - if path: - logging.warning(f"Reason: {reason}") - return path + """Move a plugin file to quarantine directory and log the reason.""" + plugin_name = os.path.basename(plugin_file) + os.makedirs(self.quarantine_dir, exist_ok=True) + + # Create unique filename if already exists + quarantine_path = os.path.join(self.quarantine_dir, plugin_name) + counter = 1 + while os.path.exists(quarantine_path): + name, ext = os.path.splitext(plugin_name) + quarantine_path = os.path.join(self.quarantine_dir, f"{name}_{counter}{ext}") + counter += 1 + + try: + shutil.move(plugin_file, quarantine_path) + logging.warning(f"Plugin '{plugin_name}' quarantined: {reason}") + logging.warning(f"Moved to: {quarantine_path}") + return quarantine_path + except Exception as e: + logging.error(f"Failed to quarantine plugin '{plugin_name}': {e}") + return None def _validate_plugin_commands(self, plugin_commands, plugin_name): - # Delegate to plugins module - return plugins_validate_commands(plugin_commands, plugin_name, PROTECTED_COMMANDS) + """Validate that plugin commands don't override protected commands.""" + violations = [] + for cmd_name in plugin_commands.keys(): + if cmd_name in PROTECTED_COMMANDS: + violations.append(cmd_name) + + if violations: + raise ValueError( + f"Plugin attempted to override protected commands: {', '.join(violations)}. " + "This is a security violation and the plugin has been quarantined." + ) + + # Validate that all values are callable + for cmd_name, cmd_func in plugin_commands.items(): + if not callable(cmd_func): + raise ValueError( + f"Plugin command '{cmd_name}' is not callable. " + "All commands must be functions." + ) + + return True def load_plugins(self): """Load plugin commands from the plugins directory with security validation.""" @@ -667,7 +1128,32 @@ class ZDTTTerminal: def get_prompt(self): """Return the custom prompt string with enhanced colors""" - return ui_get_prompt(self) + # Show current directory in prompt + cwd = os.getcwd() + # Show ~ for home directory + home = os.path.expanduser("~") + if cwd.startswith(home): + display_path = "~" + cwd[len(home):] + else: + display_path = cwd + + # Wrap ANSI codes in \001 and \002 so readline knows they're non-printable + # This fixes line wrapping issues with long commands + RL_PROMPT_START = '\001' + RL_PROMPT_END = '\002' + + # Create enhanced colorized prompt with gradient-like effect + # [username in bright green @ ZDTT in bright cyan path in bright blue]=> + prompt = (f"{RL_PROMPT_START}{self.COLOR_BRIGHT_CYAN}{RL_PROMPT_END}┌─{RL_PROMPT_START}{self.COLOR_RESET}{RL_PROMPT_END}" + f"[{RL_PROMPT_START}{self.COLOR_BRIGHT_GREEN}{RL_PROMPT_END}{self.username}" + f"{RL_PROMPT_START}{self.COLOR_RESET}{RL_PROMPT_END}" + f"{RL_PROMPT_START}{self.COLOR_BRIGHT_WHITE}{RL_PROMPT_END}@{RL_PROMPT_START}{self.COLOR_RESET}{RL_PROMPT_END}" + f"{RL_PROMPT_START}{self.COLOR_BRIGHT_CYAN}{RL_PROMPT_END}ZDTT{RL_PROMPT_START}{self.COLOR_RESET}{RL_PROMPT_END} " + f"{RL_PROMPT_START}{self.COLOR_BRIGHT_BLUE}{RL_PROMPT_END}{display_path}" + f"{RL_PROMPT_START}{self.COLOR_RESET}{RL_PROMPT_END}]" + f"{RL_PROMPT_START}{self.COLOR_BRIGHT_CYAN}{RL_PROMPT_END}─{RL_PROMPT_START}{self.COLOR_RESET}{RL_PROMPT_END}\n" + f"{RL_PROMPT_START}{self.COLOR_BRIGHT_CYAN}{RL_PROMPT_END}└─{RL_PROMPT_START}{self.COLOR_BRIGHT_MAGENTA}{RL_PROMPT_END}➜{RL_PROMPT_START}{self.COLOR_RESET}{RL_PROMPT_END} ") + return prompt def cmd_help(self, args): """Display available commands with enhanced formatting""" @@ -1463,7 +1949,93 @@ class ZDTTTerminal: def _execute_system_command(self, command): """Execute a system command with real-time I/O streaming.""" - shell_execute(self, command) + # Temporarily disable status bar updates during command execution + status_bar_was_running = self.status_bar_thread and self.status_bar_thread.is_alive() + + try: + # Start the process with direct stdin/stdout/stderr + process = subprocess.Popen( + command, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, # Merge stderr into stdout + stdin=sys.stdin, # Direct stdin passthrough + bufsize=1, # Line buffered + text=True, + cwd=self.current_dir + ) + + # Buffer for early output detection + early_output = [] + start_time = time_module.time() + check_timeout = 0.1 # 0.1 seconds + hide_output = False + output_buffer = [] + + # Read output in real-time + try: + while True: + # Read character by character for early detection + char = process.stdout.read(1) + if not char: + if process.poll() is not None: + break + time_module.sleep(0.01) + continue + + # Check for "command not found" in first 0.1 seconds + if time_module.time() - start_time < check_timeout: + early_output.append(char) + combined = ''.join(early_output).lower() + if 'command not found' in combined or 'not found:' in combined: + hide_output = True + # Consume remaining output silently + while process.poll() is None: + process.stdout.read(1) + break + + # Buffer output + output_buffer.append(char) + + # If we have a complete line or enough chars, flush + if char == '\n' or len(output_buffer) >= 1024: + if not hide_output: + sys.stdout.write(''.join(output_buffer)) + sys.stdout.flush() + output_buffer.clear() + + # Flush remaining buffer + if output_buffer and not hide_output: + sys.stdout.write(''.join(output_buffer)) + sys.stdout.flush() + + # Wait for process to finish + process.wait() + + except BrokenPipeError: + # Process closed stdout + pass + + except KeyboardInterrupt: + # Handle Ctrl+C + try: + if 'process' in locals(): + process.terminate() + process.wait(timeout=1) + except Exception: + try: + if 'process' in locals(): + process.kill() + except Exception: + pass + print("\n^C") + except Exception as e: + if not hide_output: + print(f"{self.COLOR_ERROR}Error executing command: {e}{self.COLOR_RESET}") + finally: + # Restore status bar if it was running + if status_bar_was_running: + self._render_status_bar() def execute_command(self, command_line): """Parse and execute a command""" diff --git a/version.txt b/version.txt index 16216ac..c44b9ff 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.1.2.a.9 \ No newline at end of file +0.1.2.b \ No newline at end of file diff --git a/zdtt/__init__.py b/zdtt/__init__.py deleted file mode 100644 index dc54eac..0000000 --- a/zdtt/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# ZDTT package - - - diff --git a/zdtt/__pycache__/__init__.cpython-312.pyc b/zdtt/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index a5d8625115dab3c8105aa8b774971d64b31bf1fa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 129 zcmX@j%ge<81aF-rGX;V4V-N=&d}aZPOlPQM&}8&m$xy@ugjEsy$%s>_Z DO&uFC diff --git a/zdtt/__pycache__/config.cpython-312.pyc b/zdtt/__pycache__/config.cpython-312.pyc deleted file mode 100644 index d8701e9297ab9889fe65edbefc979880e8d346af..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8016 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 diff --git a/zdtt/__pycache__/plugins.cpython-312.pyc b/zdtt/__pycache__/plugins.cpython-312.pyc deleted file mode 100644 index c60fabc369bec6d501290adbaf79f3045be0080c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5103 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 DP9)q% diff --git a/zdtt/__pycache__/ui.cpython-312.pyc b/zdtt/__pycache__/ui.cpython-312.pyc deleted file mode 100644 index 5242472f83be3cef2438606db331d3c4faa0b69b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5415 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 bool: - """ - Validate plugin AST to ensure no top-level code execution. - Only allows: imports, function definitions, class definitions, and docstrings. - Raises ValueError on violation. - """ - try: - tree = ast.parse(plugin_code) - except SyntaxError as e: - raise ValueError(f"Plugin has syntax errors: {e}") - - if not isinstance(tree, ast.Module): - raise ValueError("Plugin must be a valid Python module") - - for stmt in tree.body: - if isinstance(stmt, (ast.Import, ast.ImportFrom)): - continue - if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef)): - continue - if isinstance(stmt, ast.ClassDef): - continue - if isinstance(stmt, ast.Expr): - # Allow docstring literals - if isinstance(stmt.value, (ast.Constant, ast.Str)): - if isinstance(stmt.value, ast.Constant): - if isinstance(stmt.value.value, str): - continue - else: - # ast.Str case (older Python) - continue - raise ValueError( - f"Plugin contains forbidden top-level statement: {stmt.__class__.__name__}. " - "Plugins can only contain imports, functions, classes, and docstrings. " - "No top-level code execution is allowed." - ) - - return True - - -def move_to_quarantine(plugin_file: str, quarantine_dir: str, logger) -> Optional[str]: - """ - Move a plugin file to quarantine directory and log the reason via caller. - Returns the final quarantine path or None on failure. - """ - plugin_name = os.path.basename(plugin_file) - os.makedirs(quarantine_dir, exist_ok=True) - - quarantine_path = os.path.join(quarantine_dir, plugin_name) - counter = 1 - while os.path.exists(quarantine_path): - name, ext = os.path.splitext(plugin_name) - quarantine_path = os.path.join(quarantine_dir, f"{name}_{counter}{ext}") - counter += 1 - - try: - shutil.move(plugin_file, quarantine_path) - logger.warning(f"Plugin '{plugin_name}' quarantined") - logger.warning(f"Moved to: {quarantine_path}") - return quarantine_path - except Exception as e: - logger.error(f"Failed to quarantine plugin '{plugin_name}': {e}") - return None - - -def validate_plugin_commands( - plugin_commands: Dict[str, Callable], - plugin_name: str, - protected_commands: Iterable[str] = PROTECTED_COMMANDS, -) -> bool: - """ - Ensure plugins do not override protected commands and values are callable. - Raises ValueError on violation. - """ - violations = [cmd for cmd in plugin_commands.keys() if cmd in protected_commands] - if violations: - raise ValueError( - f"Plugin attempted to override protected commands: {', '.join(violations)}. " - "This is a security violation and the plugin has been quarantined." - ) - - for cmd_name, cmd_func in plugin_commands.items(): - if not callable(cmd_func): - raise ValueError( - f"Plugin command '{cmd_name}' is not callable. All commands must be functions." - ) - - return True - - - diff --git a/zdtt/shell.py b/zdtt/shell.py deleted file mode 100644 index f354f06..0000000 --- a/zdtt/shell.py +++ /dev/null @@ -1,82 +0,0 @@ -""" -System shell command execution utilities for ZDTT. -""" -import sys -import subprocess -import time as time_module - - -def execute_system_command(terminal, command: str): - """Execute a system command with real-time I/O streaming.""" - status_bar_was_running = terminal.status_bar_thread and terminal.status_bar_thread.is_alive() - try: - process = subprocess.Popen( - command, - shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - stdin=sys.stdin, - bufsize=1, - text=True, - cwd=terminal.current_dir - ) - - early_output = [] - start_time = time_module.time() - check_timeout = 0.1 - hide_output = False - output_buffer = [] - - try: - while True: - char = process.stdout.read(1) - if not char: - if process.poll() is not None: - break - time_module.sleep(0.01) - continue - - if time_module.time() - start_time < check_timeout: - early_output.append(char) - combined = ''.join(early_output).lower() - if 'command not found' in combined or 'not found:' in combined: - hide_output = True - while process.poll() is None: - process.stdout.read(1) - break - - output_buffer.append(char) - if char == '\n' or len(output_buffer) >= 1024: - if not hide_output: - sys.stdout.write(''.join(output_buffer)) - sys.stdout.flush() - output_buffer.clear() - - if output_buffer and not hide_output: - sys.stdout.write(''.join(output_buffer)) - sys.stdout.flush() - - process.wait() - except BrokenPipeError: - pass - except KeyboardInterrupt: - try: - if 'process' in locals(): - process.terminate() - process.wait(timeout=1) - except Exception: - try: - if 'process' in locals(): - process.kill() - except Exception: - pass - print("\n^C") - except Exception as e: - if not locals().get('hide_output', False): - print(f"{terminal.COLOR_ERROR}Error executing command: {e}{terminal.COLOR_RESET}") - finally: - if status_bar_was_running: - terminal._render_status_bar() - - - diff --git a/zdtt/status_bar.py b/zdtt/status_bar.py deleted file mode 100644 index 11625b5..0000000 --- a/zdtt/status_bar.py +++ /dev/null @@ -1,154 +0,0 @@ -""" -Status bar, scroll region, and resize handling utilities for ZDTT. -All functions operate on the provided terminal instance. -""" -import sys -import shutil -from datetime import datetime - - -def set_scroll_region(terminal): - try: - rows = shutil.get_terminal_size().lines - rows = max(rows, 2) - sys.stdout.write(f"\033[2;{rows}r") - sys.stdout.write("\033[1;1H") - sys.stdout.write("\033[2K") - sys.stdout.write("\033[2;1H") - sys.stdout.flush() - terminal.scroll_region_set = True - except Exception: - terminal.scroll_region_set = False - - -def reset_scroll_region(terminal): - if not terminal.scroll_region_set: - return - sys.stdout.write("\033[r") - sys.stdout.flush() - terminal.scroll_region_set = False - - -def build_status_bar_text(terminal): - left_text = f"{terminal.COLOR_BOLD}ZDTT{terminal.COLOR_RESET} by {terminal.COLOR_BOLD}ZaneDev{terminal.COLOR_RESET}" - time_str = datetime.now().strftime("%I:%M %p") - plain_left = "ZDTT by ZaneDev" - plain_time = time_str - - try: - term_size = shutil.get_terminal_size() - width = max(1, term_size.columns) - except Exception: - width = max(1, len(plain_left) + len(plain_time) + 6) - - min_content_width = len(plain_left) + len(plain_time) + 5 - padding = 0 if width < min_content_width else width - min_content_width - separator = f"{terminal.COLOR_DIM}│{terminal.COLOR_RESET}" - bar_content = f" {left_text} {' ' * padding}{separator} {terminal.COLOR_BRIGHT_WHITE}{time_str}{terminal.COLOR_RESET} " - actual_display_len = len(plain_left) + len(plain_time) + padding + 5 - - if actual_display_len < width: - trailing_spaces = width - actual_display_len - bar_content = bar_content.rstrip() + ' ' * trailing_spaces - elif actual_display_len > width: - padding = max(0, width - min_content_width) - bar_content = f" {left_text} {' ' * padding}{separator} {terminal.COLOR_BRIGHT_WHITE}{time_str}{terminal.COLOR_RESET} " - actual_display_len = len(plain_left) + len(plain_time) + padding + 5 - if actual_display_len < width: - trailing_spaces = width - actual_display_len - bar_content = bar_content.rstrip() + ' ' * trailing_spaces - else: - if width < len(plain_left) + 10: - bar_content = f" {left_text} {separator} {terminal.COLOR_BRIGHT_WHITE}{time_str[:8]}{terminal.COLOR_RESET} " - bar_content = bar_content[:width] if len(bar_content) > width else bar_content - - bg_code, fg_code = terminal.STATUS_BAR_COLORS_LOOKUP() - result = f"\033[{bg_code}m\033[{fg_code}m{bar_content}\033[0m" - if len(result) > width * 2: - simple_bar = f" ZDTT by ZaneDev | {time_str} " - simple_bar = simple_bar[:width] if len(simple_bar) > width else simple_bar.ljust(width) - result = f"\033[{bg_code}m\033[{fg_code}m{simple_bar}\033[0m" - return result - - -def render_status_bar(terminal): - try: - try: - term_size = shutil.get_terminal_size() - max_width = term_size.columns - except Exception: - max_width = 80 - bar_text = build_status_bar_text(terminal) - if len(bar_text) > max_width * 3: - bar_text = build_status_bar_text(terminal) - sys.stdout.write("\033[s") - sys.stdout.write("\033[1;1H") - sys.stdout.write("\033[2K") - sys.stdout.write("\033[0m") - sys.stdout.write(bar_text) - sys.stdout.write("\033[0m") - sys.stdout.write(f"\033[{max_width}G") - sys.stdout.write("\033[u") - sys.stdout.flush() - except Exception: - pass - - -def status_bar_loop(terminal): - while not terminal.status_bar_stop_event.is_set(): - render_status_bar(terminal) - if terminal.status_bar_stop_event.wait(2): - break - - -def start_status_bar_thread(terminal): - if terminal.status_bar_thread and terminal.status_bar_thread.is_alive(): - return - terminal.status_bar_stop_event.clear() - terminal.status_bar_thread = terminal._spawn_thread(target=lambda: status_bar_loop(terminal), name="ZDTTStatusBar") - terminal.status_bar_thread.start() - - -def initialize_status_bar(terminal): - set_scroll_region(terminal) - start_status_bar_thread(terminal) - render_status_bar(terminal) - - -def shutdown_status_bar(terminal): - terminal.status_bar_stop_event.set() - if terminal.status_bar_thread and terminal.status_bar_thread.is_alive(): - terminal.status_bar_thread.join(timeout=0.5) - terminal.status_bar_thread = None - reset_scroll_region(terminal) - - -def handle_resize(terminal, signum=None, frame=None): - if not terminal.resize_lock.acquire(blocking=False): - return - try: - import time as time_module - time_module.sleep(0.05) - reset_scroll_region(terminal) - set_scroll_region(terminal) - try: - sys.stdout.write("\033[1;1H") - sys.stdout.write("\033[2K") - sys.stdout.write("\033[0m") - sys.stdout.flush() - except Exception: - pass - render_status_bar(terminal) - try: - term_size = shutil.get_terminal_size() - sys.stdout.write(f"\033[{term_size.lines};1H") - sys.stdout.flush() - except Exception: - pass - except Exception: - pass - finally: - terminal.resize_lock.release() - - - diff --git a/zdtt/ui.py b/zdtt/ui.py deleted file mode 100644 index 5eb1bb5..0000000 --- a/zdtt/ui.py +++ /dev/null @@ -1,85 +0,0 @@ -""" -UI helpers for ZDTT: banner, compatibility warning, and prompt. -""" -import os -import shutil - - -def display_banner(terminal): - print() - try: - term_size = shutil.get_terminal_size() - min_height = 13 if not terminal.is_supported else 11 - min_width = 44 - if term_size.columns < min_width or term_size.lines < min_height: - print(f"ZDTT Terminal v{terminal.version}") - if not terminal.is_supported: - _show_compatibility_warning(terminal) - print() - return - except Exception: - pass - - if os.path.exists(terminal.banner_file): - try: - with open(terminal.banner_file, 'r') as f: - custom_banner = f.read() - if '{version}' in custom_banner: - custom_banner = custom_banner.replace('{version}', terminal.version) - print(custom_banner) - if not terminal.is_supported: - _show_compatibility_warning(terminal) - return - except Exception as e: - import logging - logging.error(f"Failed to load custom banner: {e}") - - banner = f""" -░█████████ ░███████ ░██████████░██████████ - ░██ ░██ ░██ ░██ ░██ - ░██ ░██ ░██ ░██ ░██ - ░███ ░██ ░██ ░██ ░██ - ░██ ░██ ░██ ░██ ░██ - ░██ ░██ ░██ ░██ ░██ -░█████████ ░███████ ░██ ░██ - - -ZDTT Terminal v{terminal.version} -""" - print(banner) - if not terminal.is_supported: - _show_compatibility_warning(terminal) - - -def _show_compatibility_warning(terminal): - if terminal.is_supported: - return - print() - print("⚠️ Running on unsupported system - limited support") - print(" Tested on Debian-based and Arch Linux distributions.") - print() - - -def get_prompt(terminal): - cwd = os.getcwd() - home = os.path.expanduser("~") - if cwd.startswith(home): - display_path = "~" + cwd[len(home):] - else: - display_path = cwd - - RL_PROMPT_START = '\001' - RL_PROMPT_END = '\002' - prompt = (f"{RL_PROMPT_START}{terminal.COLOR_BRIGHT_CYAN}{RL_PROMPT_END}┌─{RL_PROMPT_START}{terminal.COLOR_RESET}{RL_PROMPT_END}" - f"[{RL_PROMPT_START}{terminal.COLOR_BRIGHT_GREEN}{RL_PROMPT_END}{terminal.username}" - f"{RL_PROMPT_START}{terminal.COLOR_RESET}{RL_PROMPT_END}" - f"{RL_PROMPT_START}{terminal.COLOR_BRIGHT_WHITE}{RL_PROMPT_END}@{RL_PROMPT_START}{terminal.COLOR_RESET}{RL_PROMPT_END}" - f"{RL_PROMPT_START}{terminal.COLOR_BRIGHT_CYAN}{RL_PROMPT_END}ZDTT{RL_PROMPT_START}{terminal.COLOR_RESET}{RL_PROMPT_END} " - f"{RL_PROMPT_START}{terminal.COLOR_BRIGHT_BLUE}{RL_PROMPT_END}{display_path}" - f"{RL_PROMPT_START}{terminal.COLOR_RESET}{RL_PROMPT_END}]" - f"{RL_PROMPT_START}{terminal.COLOR_BRIGHT_CYAN}{RL_PROMPT_END}─{RL_PROMPT_START}{terminal.COLOR_RESET}{RL_PROMPT_END}\n" - f"{RL_PROMPT_START}{terminal.COLOR_BRIGHT_CYAN}{RL_PROMPT_END}└─{RL_PROMPT_START}{terminal.COLOR_BRIGHT_MAGENTA}{RL_PROMPT_END}➜{RL_PROMPT_START}{terminal.COLOR_RESET}{RL_PROMPT_END} ") - return prompt - - -