From a9d4d98c4c401131153674dfc9d3683b01bdb3bf Mon Sep 17 00:00:00 2001 From: Zane V Date: Sun, 16 Nov 2025 10:47:24 -0500 Subject: [PATCH] Refactor terminal.py to enhance plugin management and system compatibility - Moved various functions related to plugin validation and quarantine to the zdtt.plugins module for better organization and reusability. - Updated the handling of protected commands to source from the plugins module. - Refactored status bar management functions to utilize the zdtt.status_bar module, improving code clarity and separation of concerns. - Simplified the display banner and compatibility warning functions by delegating to the zdtt.ui module. - Enhanced the command execution process by integrating shell command execution from the zdtt.shell module. --- terminal.py | 687 ++------------------ zdtt/__init__.py | 4 + zdtt/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 129 bytes zdtt/__pycache__/config.cpython-312.pyc | Bin 0 -> 8016 bytes zdtt/__pycache__/plugins.cpython-312.pyc | Bin 0 -> 5103 bytes zdtt/__pycache__/shell.cpython-312.pyc | Bin 0 -> 3707 bytes zdtt/__pycache__/status_bar.cpython-312.pyc | Bin 0 -> 8973 bytes zdtt/__pycache__/ui.cpython-312.pyc | Bin 0 -> 5415 bytes zdtt/config.py | 166 +++++ zdtt/plugins.py | 106 +++ zdtt/shell.py | 82 +++ zdtt/status_bar.py | 154 +++++ zdtt/ui.py | 85 +++ 13 files changed, 644 insertions(+), 640 deletions(-) create mode 100644 zdtt/__init__.py create mode 100644 zdtt/__pycache__/__init__.cpython-312.pyc create mode 100644 zdtt/__pycache__/config.cpython-312.pyc create mode 100644 zdtt/__pycache__/plugins.cpython-312.pyc create mode 100644 zdtt/__pycache__/shell.cpython-312.pyc create mode 100644 zdtt/__pycache__/status_bar.cpython-312.pyc create mode 100644 zdtt/__pycache__/ui.cpython-312.pyc create mode 100644 zdtt/config.py create mode 100644 zdtt/plugins.py create mode 100644 zdtt/shell.py create mode 100644 zdtt/status_bar.py create mode 100644 zdtt/ui.py diff --git a/terminal.py b/terminal.py index cc867cf..6a94d71 100644 --- a/terminal.py +++ b/terminal.py @@ -23,39 +23,18 @@ import urllib.request import urllib.error import time as time_module - -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', -} +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 STATUS_BAR_COLORS = { 'blue': ('44', '97'), @@ -68,182 +47,15 @@ STATUS_BAR_COLORS = { 'black': ('40', '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' -} +# 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')) -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 +## moved to zdtt.config class ZDTTTerminal: @@ -273,6 +85,8 @@ 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() @@ -475,272 +289,49 @@ class ZDTTTerminal: def display_banner(self): """Display the ZDTT ASCII art banner (or custom banner if available)""" - 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() + ui_display_banner(self) def _show_compatibility_warning(self): """Show compatibility warning for unsupported systems""" - if self.is_supported: - return - - print() - print("⚠️ Running on unsupported system - limited support") - print(" Tested on Debian-based and Arch Linux distributions.") - print() + from zdtt.ui import _show_compatibility_warning as ui_warn + ui_warn(self) def initialize_status_bar(self): """Reserve the first terminal row and start the status bar thread.""" - self._set_scroll_region() - self._start_status_bar_thread() - self._render_status_bar() + sb.initialize_status_bar(self) def shutdown_status_bar(self): """Stop the status bar thread and release terminal state.""" - 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() + sb.shutdown_status_bar(self) def _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() + sb.start_status_bar_thread(self) def _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 + sb.status_bar_loop(self) def _render_status_bar(self): """Render a single-line status bar with branding and time.""" - 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 + sb.render_status_bar(self) def _build_status_bar_text(self): """Render a single-line status bar with enhanced branding and time.""" - 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 + return sb.build_status_bar_text(self) def _set_scroll_region(self): """Reserve the top row for the status bar.""" - 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 + sb.set_scroll_region(self) def _reset_scroll_region(self): """Restore default scrolling behavior.""" - if not self.scroll_region_set: - return - sys.stdout.write("\033[r") - sys.stdout.flush() - self.scroll_region_set = False + sb.reset_scroll_region(self) def _handle_resize(self, signum=None, frame=None): """Handle terminal resize event (SIGWINCH).""" - # 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() + sb.handle_resize(self, signum, frame) + + def _spawn_thread(self, target, name): + return threading.Thread(target=target, name=name, daemon=True) def setup_readline(self): """Setup readline for history and tab completion""" @@ -800,92 +391,19 @@ ZDTT Terminal v{self.version} return None def _validate_plugin_ast(self, 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 + # Delegate to plugins module + return plugins_validate_ast(plugin_code, plugin_name) def _move_to_quarantine(self, plugin_file, reason): - """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 + # Delegate to plugins module + path = plugins_move_to_quarantine(plugin_file, self.quarantine_dir, logging) + if path: + logging.warning(f"Reason: {reason}") + return path def _validate_plugin_commands(self, plugin_commands, plugin_name): - """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 + # Delegate to plugins module + return plugins_validate_commands(plugin_commands, plugin_name, PROTECTED_COMMANDS) def load_plugins(self): """Load plugin commands from the plugins directory with security validation.""" @@ -1128,32 +646,7 @@ ZDTT Terminal v{self.version} def get_prompt(self): """Return the custom prompt string with enhanced colors""" - # 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 + return ui_get_prompt(self) def cmd_help(self, args): """Display available commands with enhanced formatting""" @@ -1949,93 +1442,7 @@ ZDTT Terminal v{self.version} def _execute_system_command(self, command): """Execute a system command with real-time I/O streaming.""" - # 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() + shell_execute(self, command) def execute_command(self, command_line): """Parse and execute a command""" diff --git a/zdtt/__init__.py b/zdtt/__init__.py new file mode 100644 index 0000000..dc54eac --- /dev/null +++ b/zdtt/__init__.py @@ -0,0 +1,4 @@ +# ZDTT package + + + diff --git a/zdtt/__pycache__/__init__.cpython-312.pyc b/zdtt/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a5d8625115dab3c8105aa8b774971d64b31bf1fa GIT binary patch literal 129 zcmX@j%ge<81aF-rGX;V4V-N=&d}aZPOlPQM&}8&m$xy@ugjEsy$%s>_Z DO&uFC literal 0 HcmV?d00001 diff --git a/zdtt/__pycache__/config.cpython-312.pyc b/zdtt/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d8701e9297ab9889fe65edbefc979880e8d346af GIT binary patch literal 8016 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 literal 0 HcmV?d00001 diff --git a/zdtt/__pycache__/plugins.cpython-312.pyc b/zdtt/__pycache__/plugins.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c60fabc369bec6d501290adbaf79f3045be0080c GIT binary patch literal 5103 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 DP9)q% literal 0 HcmV?d00001 diff --git a/zdtt/__pycache__/ui.cpython-312.pyc b/zdtt/__pycache__/ui.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5242472f83be3cef2438606db331d3c4faa0b69b GIT binary patch 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 new file mode 100644 index 0000000..f354f06 --- /dev/null +++ b/zdtt/shell.py @@ -0,0 +1,82 @@ +""" +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 new file mode 100644 index 0000000..11625b5 --- /dev/null +++ b/zdtt/status_bar.py @@ -0,0 +1,154 @@ +""" +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 new file mode 100644 index 0000000..5eb1bb5 --- /dev/null +++ b/zdtt/ui.py @@ -0,0 +1,85 @@ +""" +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 + + +