From 2e67031175460a753e3c0ad1a096a7954c2ea08a Mon Sep 17 00:00:00 2001 From: ZaneDev Date: Wed, 12 Nov 2025 21:59:23 -0500 Subject: [PATCH] feat: Enhance terminal compatibility and add Arch Linux support - Updated terminal.py to support Arch Linux alongside Debian-based systems. - Implemented distro detection and user override for unsupported distributions. - Added a status bar feature with customizable colors and live updates. - Introduced JSON-based user preferences for status bar color. - Replaced neofetch with fastfetch for system info display. - Updated version to 0.1.2.a in version.txt. - Created a new website with installation instructions, features, and community engagement. - Added JavaScript for interactive elements and CSS for styling the website. --- __pycache__/terminal.cpython-313.pyc | Bin 0 -> 70982 bytes install.sh | 95 +++++- terminal.py | 481 +++++++++++++++++++++++---- version.txt | 2 +- website/index.html | 173 ++++++++++ website/script.js | 35 ++ website/styles.css | 350 +++++++++++++++++++ 7 files changed, 1069 insertions(+), 67 deletions(-) create mode 100644 __pycache__/terminal.cpython-313.pyc mode change 100644 => 100755 install.sh create mode 100644 website/index.html create mode 100644 website/script.js create mode 100644 website/styles.css diff --git a/__pycache__/terminal.cpython-313.pyc b/__pycache__/terminal.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b603cdfa44682e8a3c63070de1884c9f8f809612 GIT binary patch literal 70982 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

uO*Lke}0}83tqwN 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 J8Abaw{|mze!=wNJ literal 0 HcmV?d00001 diff --git a/install.sh b/install.sh old mode 100644 new mode 100755 index 765e278..a8478af --- a/install.sh +++ b/install.sh @@ -72,20 +72,52 @@ if [ -f "$BIN_DIR/zdtt" ] || [ -d "$INSTALL_DIR" ]; then esac fi -# Check if running on Debian-based Linux +# Check if running on a supported Linux distribution IS_DEBIAN=false +IS_ARCH=false +DETECTED_DISTRO="other" + +OS_ID="" +OS_LIKE="" +if [ -f /etc/os-release ]; then + # shellcheck disable=SC1091 + . /etc/os-release + OS_ID=$(echo "${ID:-}" | tr '[:upper:]' '[:lower:]') + OS_LIKE=$(echo "${ID_LIKE:-}" | tr '[:upper:]' '[:lower:]') +fi + if [ -f /etc/debian_version ]; then IS_DEBIAN=true + DETECTED_DISTRO="debian" echo -e "${GREEN}✓${NC} Debian-based Linux detected" +elif [ -f /etc/arch-release ] || [ -f /etc/artix-release ]; then + IS_ARCH=true + DETECTED_DISTRO="arch" + echo -e "${GREEN}✓${NC} Arch Linux detected" +elif [[ "$OS_ID" == "debian" || "$OS_ID" == "ubuntu" || "$OS_ID" == "linuxmint" || "$OS_ID" == "pop" || "$OS_ID" == "pop-os" || "$OS_ID" == "pop_os" || "$OS_ID" == "elementary" || "$OS_LIKE" == *"debian"* || "$OS_LIKE" == *"ubuntu"* ]]; then + IS_DEBIAN=true + DETECTED_DISTRO="debian" + echo -e "${GREEN}✓${NC} Debian-based Linux detected (via os-release)" +elif [[ "$OS_ID" == "arch" || "$OS_ID" == "archlinux" || "$OS_ID" == "manjaro" || "$OS_ID" == "endeavouros" || "$OS_ID" == "endeavour" || "$OS_ID" == "arcolinux" || "$OS_ID" == "garuda" || "$OS_ID" == "artix" || "$OS_ID" == "blackarch" || "$OS_LIKE" == *"arch"* ]]; then + IS_ARCH=true + DETECTED_DISTRO="arch" + echo -e "${GREEN}✓${NC} Arch-based Linux detected (via os-release)" +elif command -v apt-get &>/dev/null; then + IS_DEBIAN=true + DETECTED_DISTRO="debian" + echo -e "${GREEN}✓${NC} Debian-based Linux detected (via package manager)" +elif command -v pacman &>/dev/null; then + IS_ARCH=true + DETECTED_DISTRO="arch" + echo -e "${GREEN}✓${NC} Arch-based Linux detected (via package manager)" else - echo -e "${YELLOW}⚠${NC} Non-Debian distribution detected" + echo -e "${YELLOW}⚠${NC} Unsupported distribution detected" echo "" - echo "ZDTT Terminal is optimized for Debian-based systems." - echo "(Debian, Ubuntu, Linux Mint, Pop!_OS, etc.)" + echo "ZDTT Terminal is optimized for Debian-based and Arch Linux systems." echo "" - echo "Running on a non-Debian system may result in:" + echo "Running on an unsupported system may result in:" echo " • Some commands may not work as expected" - echo " • Auto-install features (like neofetch) will not work" + echo " • Auto-install features (like fastfetch) will not work" echo " • Reduced plugin compatibility" echo " • Package management commands unavailable" echo "" @@ -100,6 +132,37 @@ else fi fi +echo "Detected distribution: ${DETECTED_DISTRO}" +read -p "Override detection? (debian/arch/other, Enter to keep): " -r USER_OVERRIDE +USER_OVERRIDE=$(echo "$USER_OVERRIDE" | tr '[:upper:]' '[:lower:]') + +case "$USER_OVERRIDE" in + debian) + IS_DEBIAN=true + IS_ARCH=false + DETECTED_DISTRO="debian" + echo "Override applied: Debian-based system selected." + ;; + arch) + IS_DEBIAN=false + IS_ARCH=true + DETECTED_DISTRO="arch" + echo "Override applied: Arch-based system selected." + ;; + other) + IS_DEBIAN=false + IS_ARCH=false + DETECTED_DISTRO="other" + echo "Override applied: Unsupported/Other selected." + ;; + "") + echo "Keeping detected distribution." + ;; + *) + echo "Unknown override '$USER_OVERRIDE'. Keeping detected distribution." + ;; +esac + # Check if Python 3 is installed if ! command -v python3 &> /dev/null; then echo -e "${RED}✗${NC} Python 3 is not installed" @@ -121,11 +184,28 @@ if ! command -v python3 &> /dev/null; then exit 1 fi + echo -e "${GREEN}✓${NC} Python 3 installed successfully" + elif [ "$IS_ARCH" = true ]; then + echo "Installing Python 3..." + + # Sync package databases and install Python + sudo pacman -Sy --noconfirm python + + if [ $? -ne 0 ]; then + echo -e "${RED}Failed to install Python 3${NC}" + echo "Please install Python 3 manually: sudo pacman -S python" + echo "" + echo "Press any key to exit..." + read -n 1 -s -r + exit 1 + fi + echo -e "${GREEN}✓${NC} Python 3 installed successfully" else - echo -e "${RED}Python 3 is required but auto-install is not supported on non-Debian systems.${NC}" + echo -e "${RED}Python 3 is required but auto-install is not supported on this distribution.${NC}" echo "" echo "Please install Python 3 manually using your package manager:" + echo " • Debian/Ubuntu: sudo apt-get install python3" echo " • Arch/Manjaro: sudo pacman -S python" echo " • Fedora: sudo dnf install python3" echo " • openSUSE: sudo zypper install python3" @@ -435,4 +515,3 @@ echo " zdtt start" echo "" echo "Press any key to exit..." read -n 1 -s -r - diff --git a/terminal.py b/terminal.py index ea00c39..88897e6 100644 --- a/terminal.py +++ b/terminal.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """ ZDTT Terminal - A custom terminal interface -Only works on Debian-based Linux systems +Only works on Debian-based or Arch Linux systems """ import os @@ -13,14 +13,147 @@ import readline import glob import atexit import logging +import threading +import json from datetime import datetime 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', +} + +STATUS_BAR_COLORS = { + 'blue': ('44', '97'), + 'red': ('41', '97'), + 'green': ('42', '30'), + 'cyan': ('46', '30'), + 'magenta': ('45', '97'), + 'yellow': ('43', '30'), + 'white': ('47', '30'), + 'black': ('40', '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 check_system_compatibility(): - """Check system compatibility and warn if not Debian-based""" + """Detect supported distributions and warn when unsupported""" # Check if running on Linux if sys.platform != 'linux': print("=" * 60) @@ -34,19 +167,21 @@ def check_system_compatibility(): if response != 'yes': print("Installation cancelled.") sys.exit(0) - return False + return 'other' - # Check for Debian-specific file - if not os.path.exists('/etc/debian_version'): + # Detect supported distributions + distro = _detect_supported_distro() + + if distro not in ('debian', 'arch'): + # Unsupported distribution print("=" * 60) - print("⚠️ WARNING: Non-Debian Distribution Detected") + print("⚠️ WARNING: Unsupported Distribution Detected") print("=" * 60) - print("ZDTT Terminal is optimized for Debian-based systems.") - print("(Debian, Ubuntu, Linux Mint, Pop!_OS, etc.)") + print("ZDTT Terminal is optimized for Debian-based and Arch Linux systems.") print() - print("Running on a non-Debian system may result in:") + print("Running on your current system may result in:") print(" • Some commands may not work as expected") - print(" • Auto-install features (like neofetch) may fail") + print(" • Auto-install features may fail") print(" • Reduced plugin compatibility") print(" • Package management commands unavailable") print() @@ -54,24 +189,32 @@ def check_system_compatibility(): if response != 'yes': print("Installation cancelled.") sys.exit(0) - return False - # It's a Debian-based system - return True + # Offer override regardless of detection + distro = _prompt_distro_override(distro) + return distro class ZDTTTerminal: - def __init__(self, is_debian=True): + def __init__(self, distro='debian'): self.username = getpass.getuser() self.running = True self.current_dir = os.getcwd() - self.is_debian = is_debian + self.distro = distro + self.is_debian = distro == 'debian' + self.is_arch = distro == 'arch' + self.is_supported = self.is_debian or self.is_arch self.zdtt_dir = os.path.expanduser("~/.zdtt") self.history_file = os.path.expanduser("~/.zdtt_history") self.plugin_dir = os.path.join(self.zdtt_dir, "plugins") self.log_file = os.path.join(self.zdtt_dir, "plugin_errors.log") self.banner_file = os.path.join(self.zdtt_dir, "banner.txt") self.aliases_file = os.path.join(self.zdtt_dir, "aliases") + self.config_file = os.path.join(self.zdtt_dir, "config.json") + self.status_bar_color = 'blue' + self.status_bar_thread = None + self.status_bar_stop_event = threading.Event() + self.scroll_region_set = False # Setup logging for plugins self.setup_logging() @@ -83,6 +226,9 @@ class ZDTTTerminal: # Read version from version.txt self.version = self.read_version() + # Load user preferences (status bar color, etc.) + self.load_preferences() + # ANSI color codes self.COLOR_RESET = '\033[0m' self.COLOR_GREEN = '\033[92m' @@ -103,13 +249,14 @@ class ZDTTTerminal: 'unalias': self.cmd_unalias, 'zps': self.cmd_zps, 'time': self.cmd_time, + 'statusbar': self.cmd_statusbar, # System commands 'ls': self.cmd_ls, 'pwd': self.cmd_pwd, 'cd': self.cmd_cd, 'cat': self.cmd_cat, 'nano': self.cmd_nano, - 'neofetch': self.cmd_neofetch, + 'fastfetch': self.cmd_fastfetch, 'mkdir': self.cmd_mkdir, 'touch': self.cmd_touch, 'rm': self.cmd_rm, @@ -166,6 +313,31 @@ class ZDTTTerminal: # Fallback version if file not found return "0.0.1.a" + def load_preferences(self): + """Load user preferences such as status bar color.""" + try: + with open(self.config_file, 'r') as f: + data = json.load(f) + self.status_bar_color = data.get('status_bar_color', self.status_bar_color) + except FileNotFoundError: + pass + except json.JSONDecodeError: + logging.warning("Preferences file is corrupted; using defaults.") + + def save_preferences(self): + """Persist user preferences.""" + data = {} + try: + with open(self.config_file, 'r') as f: + data = json.load(f) + except (FileNotFoundError, json.JSONDecodeError): + data = {} + + data['status_bar_color'] = self.status_bar_color + + with open(self.config_file, 'w') as f: + json.dump(data, f, indent=2) + def check_for_updates(self): """Check if a new version is available""" try: @@ -186,19 +358,21 @@ 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 non-Debian warning if needed - min_height = 13 if not self.is_debian else 11 + # 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_debian: - print("⚠️ Non-Debian system - limited support") + if not self.is_supported: + print("⚠️ Unsupported system - limited support") print() return except Exception: @@ -214,8 +388,8 @@ class ZDTTTerminal: if '{version}' in custom_banner: custom_banner = custom_banner.replace('{version}', self.version) print(custom_banner) - # Show non-Debian warning - if not self.is_debian: + # Show warning for unsupported systems + if not self.is_supported: self._show_compatibility_warning() return except Exception as e: @@ -237,16 +411,105 @@ ZDTT Terminal v{self.version} """ print(banner) - # Show non-Debian warning after banner - if not self.is_debian: + # 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 non-Debian systems""" + """Show compatibility warning for unsupported systems""" + if self.is_supported: + return + print() - print("⚠️ Running on non-Debian system - limited support") + 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.""" + 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.""" + 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): + 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): + 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.""" + bar_text = self._build_status_bar_text() + try: + sys.stdout.write("\033[s") # Save cursor position + sys.stdout.write("\033[1;1H") # Move to first row + sys.stdout.write("\033[2K") # Clear the line + sys.stdout.write(bar_text) + sys.stdout.write(self.COLOR_RESET) + sys.stdout.write("\033[u") # Restore cursor + sys.stdout.flush() + except Exception: + print(bar_text) + + def _build_status_bar_text(self): + left_text = "ZDTT by ZaneDev" + time_str = datetime.now().strftime("%I:%M:%p").lower() + try: + term_size = shutil.get_terminal_size() + width = max(term_size.columns, len(left_text) + len(time_str) + 4) + except Exception: + width = len(left_text) + len(time_str) + 4 + + padding = max(width - len(left_text) - len(time_str) - 2, 1) + bar_plain = f" {left_text}{' ' * padding}{time_str} " + if len(bar_plain) < width: + bar_plain = bar_plain.ljust(width) + else: + bar_plain = bar_plain[:width] + bg_code, fg_code = STATUS_BAR_COLORS.get(self.status_bar_color, ('44', '97')) + return f"\033[{bg_code}m\033[{fg_code}m{bar_plain}" + + 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 + + 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 + def setup_readline(self): """Setup readline for history and tab completion""" # Setup history @@ -441,6 +704,7 @@ ZDTT Terminal v{self.version} print(" unalias - Remove an alias") print(" zps install - Install plugin from URL") print(" time [options] - Display date/time (MM/DD/YY 12h default)") + print(" statusbar color - Change status bar highlight color") print(" exit - Exit ZDTT (return to shell)") print(" quit - Quit and close terminal window") print() @@ -461,7 +725,7 @@ ZDTT Terminal v{self.version} print(" date - Display current date/time") print(" uname [options] - Display system information") print(" nano - Edit file with nano") - print(" neofetch - Display system info (auto-installs)") + print(" fastfetch - Display system info (auto-installs)") print() print("Python Commands:") print(" python [args] - Run Python interpreter") @@ -479,6 +743,8 @@ ZDTT Terminal v{self.version} def cmd_clear(self, args): """Clear the terminal screen""" os.system('clear' if os.name != 'nt' else 'cls') + self._set_scroll_region() + self._render_status_bar() self.display_banner() def cmd_exit(self, args): @@ -496,13 +762,15 @@ ZDTT Terminal v{self.version} def cmd_about(self, args): """Display information about ZDTT Terminal""" print(f"\nZDTT Terminal v{self.version}") - print("A custom terminal interface for Debian-based Linux") + print("A custom terminal interface for Debian-based and Arch Linux systems") # Show distribution status if self.is_debian: print("Running on: Debian-based system (fully supported)") + elif self.is_arch: + print("Running on: Arch Linux (fully supported)") else: - print("Running on: Non-Debian system (limited support)") + print("Running on: Unsupported system (limited support)") print() print("Features:") @@ -815,6 +1083,35 @@ ZDTT Terminal v{self.version} print(f"{date_str} {time_str}") + def cmd_statusbar(self, args): + """Configure the status bar appearance.""" + if not args: + print(f"Status bar color: {self.status_bar_color}") + print("Usage: statusbar color ") + print(f"Available colors: {', '.join(sorted(STATUS_BAR_COLORS.keys()))}") + return + + subcommand = args[0].lower() + if subcommand != 'color': + print("Unknown statusbar option. Usage: statusbar color ") + return + + if len(args) < 2: + print("Missing color. Usage: statusbar color ") + print(f"Available colors: {', '.join(sorted(STATUS_BAR_COLORS.keys()))}") + return + + color = args[1].lower() + if color not in STATUS_BAR_COLORS: + print(f"Unsupported color '{color}'.") + print(f"Available colors: {', '.join(sorted(STATUS_BAR_COLORS.keys()))}") + return + + self.status_bar_color = color + self.save_preferences() + self._render_status_bar() + print(f"Status bar color updated to {color}.") + def cmd_echo(self, args): """Echo the provided arguments""" if args: @@ -1034,32 +1331,97 @@ ZDTT Terminal v{self.version} subprocess.run(['nano'] + args) - def cmd_neofetch(self, args): - """Display system info with neofetch (auto-installs if needed)""" - # Check if neofetch is installed - if not shutil.which('neofetch'): - if not self.is_debian: - print("neofetch is not installed.") - print("Auto-install is only supported on Debian-based systems.") - print("Please install neofetch manually using your package manager:") - print(" • Arch/Manjaro: sudo pacman -S neofetch") - print(" • Fedora: sudo dnf install neofetch") - print(" • openSUSE: sudo zypper install neofetch") + def cmd_fastfetch(self, args): + """Display system info with fastfetch (auto-installs if needed)""" + def _find_fastfetch_binary(): + """Return absolute path to fastfetch if available.""" + fastfetch_path = shutil.which('fastfetch') + if fastfetch_path: + return fastfetch_path + + # Fallback search in common locations + common_paths = [ + '/usr/bin/fastfetch', + '/usr/local/bin/fastfetch', + os.path.expanduser('~/.local/bin/fastfetch'), + ] + for path in common_paths: + if os.path.isfile(path) and os.access(path, os.X_OK): + return path + return None + + def _build_install_command(): + """Return (cmd_list, manual_hint) based on distro/privileges.""" + manual_hint = None + if self.is_debian: + base_cmd = ['apt-get', 'install', '-y', 'fastfetch'] + manual_hint = "sudo apt-get install fastfetch" + elif self.is_arch: + base_cmd = ['pacman', '-S', '--noconfirm', 'fastfetch'] + manual_hint = "sudo pacman -S fastfetch" + else: + return None, None + + # Determine if sudo is needed + geteuid = getattr(os, 'geteuid', None) + is_root = geteuid is not None and geteuid() == 0 + sudo_path = shutil.which('sudo') + + if is_root: + return base_cmd, manual_hint + + if sudo_path: + return [sudo_path] + base_cmd, manual_hint + + # Cannot elevate automatically + return None, manual_hint + + # Check if fastfetch is installed + fastfetch_bin = _find_fastfetch_binary() + + if not fastfetch_bin: + if not self.is_supported: + print("fastfetch is not installed.") + print("Auto-install is only supported on Debian-based and Arch Linux systems.") + print("Please install fastfetch manually using your package manager:") + print(" • Debian/Ubuntu: sudo apt-get install fastfetch") + print(" • Arch/Manjaro: sudo pacman -S fastfetch") + print(" • Fedora: sudo dnf install fastfetch") + print(" • openSUSE: sudo zypper install fastfetch") return - print("neofetch is not installed. Installing...") + install_cmd, manual_hint = _build_install_command() + if not install_cmd: + print("fastfetch is not installed and cannot be auto-installed because elevated privileges") + print("are required but 'sudo' was not found (or you're not running as root).") + if manual_hint: + print(f"Try manually: {manual_hint}") + else: + print("Please install fastfetch via your package manager.") + return + + print("fastfetch is not installed. Installing...") print() try: - subprocess.run(['sudo', 'apt-get', 'install', '-y', 'neofetch'], check=True) + subprocess.run(install_cmd, check=True) print() - print("neofetch installed successfully!") + print("fastfetch installed successfully!") print() except subprocess.CalledProcessError: - print("Failed to install neofetch") - print("Try manually: sudo apt-get install neofetch") + print("Failed to install fastfetch") + if manual_hint: + print(f"Try manually: {manual_hint}") + else: + print("Please install fastfetch via your package manager.") return - subprocess.run(['neofetch'] + args) + fastfetch_bin = _find_fastfetch_binary() + if not fastfetch_bin: + print("fastfetch installation completed but binary was not found.") + print("Ensure fastfetch is in your PATH and try again.") + return + + subprocess.run([fastfetch_bin] + args) # Python Commands @@ -1126,28 +1488,31 @@ ZDTT Terminal v{self.version} """Main terminal loop""" # Clear screen and display banner os.system('clear' if os.name != 'nt' else 'cls') + self.initialize_status_bar() self.display_banner() # Main command loop - while self.running: - try: - command = input(self.get_prompt()) - self.execute_command(command) - except KeyboardInterrupt: - print("\nUse 'exit' to return to shell, or 'quit' to close the window.") - except EOFError: - print("\nGoodbye!") - break + try: + while self.running: + try: + command = input(self.get_prompt()) + self.execute_command(command) + except KeyboardInterrupt: + print("\nUse 'exit' to return to shell, or 'quit' to close the window.") + except EOFError: + print("\nGoodbye!") + break + finally: + self.shutdown_status_bar() def main(): # Check system compatibility - is_debian = check_system_compatibility() + distro = check_system_compatibility() - terminal = ZDTTTerminal(is_debian=is_debian) + terminal = ZDTTTerminal(distro=distro) terminal.run() if __name__ == "__main__": main() - diff --git a/version.txt b/version.txt index 5a569b8..ae3fbae 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.1.1.r +0.1.2.a diff --git a/website/index.html b/website/index.html new file mode 100644 index 0000000..7c2bdac --- /dev/null +++ b/website/index.html @@ -0,0 +1,173 @@ + + + + + + ZDTT Terminal + + + + + + +

+ + +
+

Linux-first terminal

+

ZDTT Terminal

+

+ A vibe-coded, distro-aware terminal experience for Debian and Arch power users. + Smart onboarding, plugin support, custom aliases, and a living status bar—no fluff. +

+
+ + Explore Features +
+ + curl -O https://zdtt-sources.zane.org/install.sh && chmod +x install.sh && ./install.sh + +
+ +
+
+

Current release

+

v0.1.2.a

+
+
+

Supported families

+

Debian · Arch

+
+
+

Extension slots

+

Plugins + Aliases

+
+
+
+ +
+
+
+

Why ZDTT

+

Built for Linux hackers, tuned for comfort.

+

ZDTT pairs a friendly onboarding story with serious tooling. No learning cliff, no silent failure when you hop distros.

+
+
+
+

Smart distro detection

+

Installer inspects /etc/os-release, package managers, and user overrides to keep Debian and Arch flows tight.

+
+
+

Always-on status bar

+

Background thread renders a live status bar with branding, time, and color themes you can swap on the fly.

+
+
+

Plugin-friendly core

+

Drop Python hooks inside ~/.zdtt/plugins, log issues automatically, and iterate without touching core files.

+
+
+

Aliases that stick

+

Readable aliases file, inline history, and tab-completion support so your favorite shortcuts load every launch.

+
+
+

Command batteries included

+

ls, grep, fastfetch, Python runners, package helpers, and more are wired in.

+
+
+

Graceful fallbacks

+

Unsupported systems get friendly warnings, opt-in installs, and clear messaging so nothing breaks silently.

+
+
+
+ +
+
+

3-step install

+

From curl to custom shell in minutes.

+
+
    +
  1. +

    Fetch the installer

    +

    Use the one-liner above or download install.sh directly if you want to inspect the script first.

    +
  2. +
  3. +

    Follow the prompts

    +

    The script checks your distro, offers overrides, and handles both Debian (apt) and Arch (pacman) flows.

    +
  4. +
  5. +

    Launch zdtt

    +

    Run zdtt from any shell. Tweak banner art, status-bar colors, and drop plugins into ~/.zdtt.

    +
  6. +
+
+

Heads up: ZDTT is Linux-only. Non-Debian/Arch installs can continue at your own risk—warnings are built in.

+
+
+ +
+
+

Showcase

+

Personality baked in.

+
+
+
+
Custom ASCII banner system
+
+░█████████ ░███████   ░██████████░██████████
+      ░██  ░██   ░██      ░██        ░██
+     ░██   ░██    ░██     ░██        ░██
+   ░███    ░██    ░██     ░██        ░██
+  ░██      ░██    ░██     ░██        ░██
+ ░██       ░██   ░██      ░██        ░██
+░█████████ ░███████       ░██        ░██
+          
+
+
+
Live status bar with themed colors
+
ZDTT by ZaneDev10:24pm
+
+
+
Plugin logging with guard rails
+
+2024-01-02 21:41:07 ERROR my_plugin
+Traceback (most recent call last):
+  ...
+          
+
+
+
+ +
+
+

Community

+

Bring your tweaks, aliases, and plugins.

+

ZDTT thrives on experimentation. Fork it, vibe with it, file PRs, or just send screenshots.

+
+ +
+
+ + + + + + diff --git a/website/script.js b/website/script.js new file mode 100644 index 0000000..f7e1dd1 --- /dev/null +++ b/website/script.js @@ -0,0 +1,35 @@ +document.addEventListener('DOMContentLoaded', () => { + const navToggle = document.querySelector('.nav__toggle'); + const navLinks = document.querySelector('.nav__links'); + const yearEl = document.getElementById('year'); + + if (yearEl) { + yearEl.textContent = new Date().getFullYear(); + } + + if (navToggle && navLinks) { + navToggle.addEventListener('click', () => { + navLinks.classList.toggle('is-open'); + }); + } + + document.querySelectorAll('[data-copy]').forEach((button) => { + button.addEventListener('click', () => { + const target = document.querySelector(button.dataset.copy); + if (!target) { + return; + } + navigator.clipboard?.writeText(target.textContent.trim()).then(() => { + button.textContent = 'Copied!'; + setTimeout(() => { + button.textContent = 'Copy Install Command'; + }, 1800); + }).catch(() => { + button.textContent = 'Unable to copy'; + setTimeout(() => { + button.textContent = 'Copy Install Command'; + }, 1800); + }); + }); + }); +}); diff --git a/website/styles.css b/website/styles.css new file mode 100644 index 0000000..465e767 --- /dev/null +++ b/website/styles.css @@ -0,0 +1,350 @@ +:root { + color-scheme: dark; + font-family: 'Space Grotesk', system-ui, -apple-system, BlinkMacSystemFont, sans-serif; + --bg: #05060a; + --bg-alt: #0e1018; + --card: #121422; + --card-border: rgba(255, 255, 255, 0.08); + --text: #f5f6fd; + --muted: #9aa2c4; + --accent: #4dd5ff; + --accent-strong: #6d84ff; + --shadow: 0 18px 45px rgba(4, 6, 11, 0.6); +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +body { + margin: 0; + min-height: 100vh; + background: radial-gradient(circle at top, rgba(77, 213, 255, 0.12), transparent 55%), + radial-gradient(circle at 20% 20%, rgba(109, 132, 255, 0.25), transparent 35%), + var(--bg); + color: var(--text); + font-size: 1rem; + line-height: 1.6; +} + +img { + max-width: 100%; + height: auto; +} + +a { + color: inherit; + text-decoration: none; +} + +.hero { + padding: 2.5rem clamp(1.5rem, 5vw, 5rem) 5rem; + display: flex; + flex-direction: column; + gap: 2rem; +} + +.nav { + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; +} + +.brand { + font-weight: 700; + letter-spacing: 0.08em; +} + +.nav__links { + display: flex; + align-items: center; + gap: 1.5rem; +} + +.nav__links a { + color: var(--muted); + font-weight: 500; + transition: color 0.2s ease; +} + +.nav__links a:hover { + color: var(--text); +} + +.nav__toggle { + display: none; + background: none; + border: none; + cursor: pointer; + gap: 0.4rem; + flex-direction: column; +} + +.nav__toggle span { + display: block; + width: 1.5rem; + height: 2px; + background: var(--text); +} + +.hero__content { + max-width: 48rem; +} + +.eyebrow { + text-transform: uppercase; + letter-spacing: 0.3em; + font-size: 0.75rem; + color: var(--accent); + margin-bottom: 1rem; +} + +h1 { + font-size: clamp(2.7rem, 5vw, 4.8rem); + margin: 0 0 1rem; + line-height: 1.05; +} + +h2 { + font-size: clamp(2rem, 3vw, 3rem); + margin-bottom: 0.75rem; +} + +h3 { + margin-bottom: 0.35rem; +} + +.lead { + color: var(--muted); + font-size: 1.15rem; + margin-bottom: 1.5rem; +} + +.hero__actions { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + margin-bottom: 1rem; +} + +.btn { + border: 1px solid transparent; + padding: 0.85rem 1.4rem; + border-radius: 999px; + font-weight: 600; + cursor: pointer; + transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; +} + +.btn.primary { + background: linear-gradient(90deg, var(--accent-strong), var(--accent)); + color: #05060a; + border: none; + box-shadow: 0 15px 40px rgba(77, 213, 255, 0.3); +} + +.btn.ghost { + border-color: rgba(255, 255, 255, 0.2); + color: var(--text); + background: transparent; +} + +.btn:hover { + transform: translateY(-1px); + box-shadow: 0 12px 30px rgba(109, 132, 255, 0.25); +} + +.install-command { + display: block; + padding: 1rem 1.25rem; + border-radius: 0.75rem; + background: rgba(18, 20, 34, 0.7); + border: 1px solid var(--card-border); + font-family: 'JetBrains Mono', 'Space Grotesk', monospace; + font-size: 0.95rem; + overflow-x: auto; +} + +.hero__metrics { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); + gap: 1rem; +} + +.metric { + padding: 1.25rem; + border-radius: 1rem; + border: 1px solid var(--card-border); + background: rgba(5, 6, 10, 0.55); +} + +.metric__label { + text-transform: uppercase; + font-size: 0.75rem; + letter-spacing: 0.25em; + color: var(--muted); +} + +.metric__value { + font-size: 1.2rem; + margin: 0.35rem 0 0; + font-weight: 600; +} + +.section { + padding: clamp(3rem, 8vw, 6rem) clamp(1.5rem, 5vw, 5rem); +} + +.section__intro { + max-width: 42rem; + margin-bottom: 2.5rem; +} + +.feature-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); + gap: 1.25rem; +} + +.card { + padding: 1.5rem; + border-radius: 1.2rem; + border: 1px solid var(--card-border); + background: var(--card); + box-shadow: var(--shadow); + min-height: 10rem; +} + +.card p { + color: var(--muted); + margin: 0; +} + +.install { + background: var(--bg-alt); +} + +.install-steps { + list-style: none; + padding: 0; + margin: 0; + display: grid; + gap: 1.25rem; +} + +.install-steps li { + padding: 1.5rem; + border-radius: 1rem; + border: 1px solid var(--card-border); + background: rgba(5, 6, 10, 0.5); + counter-increment: install-step; + position: relative; + padding-left: 4.5rem; +} + +.install-steps li::before { + content: counter(install-step); + position: absolute; + left: 1.5rem; + top: 1.5rem; + width: 2rem; + height: 2rem; + border-radius: 50%; + background: rgba(77, 213, 255, 0.2); + border: 1px solid var(--accent); + display: grid; + place-items: center; + font-weight: 600; +} + +.callout { + margin-top: 2rem; + padding: 1.25rem 1.5rem; + border-radius: 1rem; + background: rgba(255, 196, 87, 0.12); + border: 1px solid rgba(255, 196, 87, 0.35); +} + +.showcase__grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); + gap: 1.35rem; +} + +figure { + margin: 0; + border: 1px solid var(--card-border); + border-radius: 1rem; + padding: 1rem; + background: rgba(9, 10, 18, 0.8); + min-height: 15rem; +} + +figcaption { + font-weight: 600; + margin-bottom: 0.75rem; + color: var(--accent); +} + +pre { + background: rgba(5, 6, 10, 0.65); + padding: 1rem; + border-radius: 0.75rem; + overflow: auto; + font-size: 0.85rem; + line-height: 1.3; +} + +.statusbar-demo { + display: flex; + justify-content: space-between; + background: linear-gradient(90deg, var(--accent-strong), var(--accent)); + color: #05060a; + padding: 0.65rem 1rem; + border-radius: 0.5rem; + font-weight: 600; +} + +.community__actions { + display: flex; + gap: 0.75rem; + flex-wrap: wrap; + margin-top: 1rem; +} + +.footer { + padding: 2rem clamp(1.5rem, 5vw, 5rem); + border-top: 1px solid rgba(255, 255, 255, 0.08); + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 1rem; + font-size: 0.9rem; + color: var(--muted); +} + +@media (max-width: 720px) { + .nav { + flex-wrap: wrap; + } + + .nav__toggle { + display: inline-flex; + } + + .nav__links { + display: none; + width: 100%; + flex-direction: column; + padding: 1rem 0 0; + gap: 0.75rem; + } + + .nav__links.is-open { + display: flex; + } +}