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.
This commit is contained in:
2025-11-16 10:47:24 -05:00
parent 1af1d5338b
commit a9d4d98c4c
13 changed files with 644 additions and 640 deletions

4
zdtt/__init__.py Normal file
View File

@@ -0,0 +1,4 @@
# ZDTT package

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

166
zdtt/config.py Normal file
View File

@@ -0,0 +1,166 @@
"""
System detection and persistent configuration helpers for ZDTT.
"""
import os
import sys
import shutil
import json
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',
}
def _parse_os_release():
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):
tokens = set()
for value in values:
if not value:
continue
normalized = value.replace('"', '').strip().lower()
if not normalized:
continue
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():
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'
if shutil.which('apt-get'):
return 'debian'
if shutil.which('pacman'):
return 'arch'
return 'other'
def _prompt_distro_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():
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: str):
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. Returns 'debian' | 'arch' | 'other'."""
saved_distro = _load_saved_distro()
if saved_distro:
return saved_distro
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
distro = _detect_supported_distro()
if distro not in ('debian', 'arch'):
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)
distro = _prompt_distro_override(distro)
_save_distro_preference(distro)
return distro

106
zdtt/plugins.py Normal file
View File

@@ -0,0 +1,106 @@
"""
Plugin utilities for ZDTT: AST validation, quarantine, and command validation.
"""
import os
import shutil
import ast
from typing import Dict, Callable, Iterable, Optional
# 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'
}
def validate_plugin_ast(plugin_code: str, plugin_name: str) -> 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

82
zdtt/shell.py Normal file
View File

@@ -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()

154
zdtt/status_bar.py Normal file
View File

@@ -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()

85
zdtt/ui.py Normal file
View File

@@ -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