feat: Enhance navigation accessibility and update terminal commands

This commit is contained in:
2025-11-15 09:44:02 -05:00
parent 4cb5559b40
commit 2920a32abd
6 changed files with 201 additions and 116 deletions

Binary file not shown.

View File

@@ -13,12 +13,12 @@
<header class="hero" id="top"> <header class="hero" id="top">
<nav class="nav"> <nav class="nav">
<div class="brand">ZDTT</div> <div class="brand">ZDTT</div>
<button class="nav__toggle" aria-label="Toggle navigation"> <button class="nav__toggle" type="button" aria-label="Toggle navigation" aria-expanded="false" aria-controls="primary-nav">
<span></span> <span></span>
<span></span> <span></span>
<span></span> <span></span>
</button> </button>
<div class="nav__links"> <div class="nav__links" id="primary-nav">
<a href="#features">Features</a> <a href="#features">Features</a>
<a href="#install">Install</a> <a href="#install">Install</a>
<a href="#showcase">Showcase</a> <a href="#showcase">Showcase</a>

View File

@@ -8,28 +8,68 @@ document.addEventListener('DOMContentLoaded', () => {
} }
if (navToggle && navLinks) { if (navToggle && navLinks) {
const setExpanded = (isOpen) => {
navLinks.classList.toggle('is-open', isOpen);
navToggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
};
navToggle.addEventListener('click', () => { navToggle.addEventListener('click', () => {
navLinks.classList.toggle('is-open'); const nextState = !navLinks.classList.contains('is-open');
setExpanded(nextState);
});
navLinks.addEventListener('click', (event) => {
if (event.target.closest('a')) {
setExpanded(false);
}
});
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && navLinks.classList.contains('is-open')) {
setExpanded(false);
navToggle.focus();
}
}); });
} }
document.querySelectorAll('[data-copy]').forEach((button) => { document.querySelectorAll('[data-copy]').forEach((button) => {
const defaultLabel = button.textContent;
const setStatus = (label) => {
button.textContent = label;
setTimeout(() => {
button.textContent = defaultLabel;
}, 1800);
};
button.addEventListener('click', () => { button.addEventListener('click', () => {
const target = document.querySelector(button.dataset.copy); const target = document.querySelector(button.dataset.copy);
if (!target) { if (!target) {
return; return;
} }
navigator.clipboard?.writeText(target.textContent.trim()).then(() => { const textToCopy = target.textContent.trim();
button.textContent = 'Copied!'; const clipboard = navigator.clipboard;
setTimeout(() => {
button.textContent = 'Copy Install Command'; if (clipboard && typeof clipboard.writeText === 'function') {
}, 1800); clipboard.writeText(textToCopy).then(() => {
}).catch(() => { setStatus('Copied!');
button.textContent = 'Unable to copy'; }).catch(() => {
setTimeout(() => { setStatus('Copy manually');
button.textContent = 'Copy Install Command'; });
}, 1800); return;
}); }
try {
const range = document.createRange();
range.selectNodeContents(target);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
const successful = document.execCommand('copy');
selection.removeAllRanges();
setStatus(successful ? 'Copied!' : 'Copy manually');
} catch (error) {
setStatus('Copy manually');
}
}); });
}); });
}); });

View File

@@ -35,8 +35,20 @@ img {
} }
a { a {
color: inherit; color: var(--accent);
text-decoration: none; text-decoration: underline;
text-decoration-color: rgba(77, 213, 255, 0.6);
}
a:hover {
color: var(--text);
text-decoration-color: var(--text);
}
a:focus-visible,
button:focus-visible {
outline: 2px solid rgba(77, 213, 255, 0.8);
outline-offset: 3px;
} }
.hero { .hero {
@@ -68,6 +80,7 @@ a {
color: var(--muted); color: var(--muted);
font-weight: 500; font-weight: 500;
transition: color 0.2s ease; transition: color 0.2s ease;
text-decoration: none;
} }
.nav__links a:hover { .nav__links a:hover {
@@ -137,6 +150,16 @@ h3 {
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
text-decoration: none;
}
.btn:focus-visible {
outline: none;
box-shadow: 0 0 0 3px rgba(77, 213, 255, 0.35), 0 12px 30px rgba(109, 132, 255, 0.25);
}
.nav__toggle:focus-visible {
outline-offset: 6px;
} }
.btn.primary { .btn.primary {

View File

@@ -15,6 +15,7 @@ import atexit
import logging import logging
import threading import threading
import json import json
import shlex
from datetime import datetime from datetime import datetime
import urllib.request import urllib.request
import urllib.error import urllib.error
@@ -215,6 +216,8 @@ class ZDTTTerminal:
self.status_bar_thread = None self.status_bar_thread = None
self.status_bar_stop_event = threading.Event() self.status_bar_stop_event = threading.Event()
self.scroll_region_set = False self.scroll_region_set = False
self.plugin_command_names = set()
self.update_check_thread = None
# Setup logging for plugins # Setup logging for plugins
self.setup_logging() self.setup_logging()
@@ -250,13 +253,14 @@ class ZDTTTerminal:
'zps': self.cmd_zps, 'zps': self.cmd_zps,
'time': self.cmd_time, 'time': self.cmd_time,
'statusbar': self.cmd_statusbar, 'statusbar': self.cmd_statusbar,
'update': self.cmd_update,
# System commands # System commands
'ls': self.cmd_ls, 'ls': self.cmd_ls,
'pwd': self.cmd_pwd, 'pwd': self.cmd_pwd,
'cd': self.cmd_cd, 'cd': self.cmd_cd,
'cat': self.cmd_cat, 'cat': self.cmd_cat,
'nano': self.cmd_nano, 'nano': self.cmd_nano,
'fastfetch': self.cmd_fastfetch, 'sysfetch': self.cmd_sysfetch,
'mkdir': self.cmd_mkdir, 'mkdir': self.cmd_mkdir,
'touch': self.cmd_touch, 'touch': self.cmd_touch,
'rm': self.cmd_rm, 'rm': self.cmd_rm,
@@ -279,8 +283,8 @@ class ZDTTTerminal:
# Load plugins # Load plugins
self.load_plugins() self.load_plugins()
# Check for updates (non-blocking) # Kick off async update check
self.check_for_updates() self.start_update_check()
def setup_logging(self): def setup_logging(self):
"""Setup logging for plugin errors""" """Setup logging for plugin errors"""
@@ -338,8 +342,19 @@ class ZDTTTerminal:
with open(self.config_file, 'w') as f: with open(self.config_file, 'w') as f:
json.dump(data, f, indent=2) json.dump(data, f, indent=2)
def check_for_updates(self): def start_update_check(self):
"""Check if a new version is available""" """Start the background thread that checks for updates."""
if self.update_check_thread and self.update_check_thread.is_alive():
return
self.update_check_thread = threading.Thread(
target=self._check_for_updates,
name="ZDTTUpdateCheck",
daemon=True,
)
self.update_check_thread.start()
def _check_for_updates(self):
"""Background worker that checks if a new version is available."""
try: try:
# Get remote version # Get remote version
url = "https://zdtt-sources.zane.org/version.txt" url = "https://zdtt-sources.zane.org/version.txt"
@@ -350,7 +365,7 @@ class ZDTTTerminal:
if remote_version != self.version: if remote_version != self.version:
print() print()
print(f"🔔 Update available! Current: {self.version} → Latest: {remote_version}") print(f"🔔 Update available! Current: {self.version} → Latest: {remote_version}")
print(" Run 'zdtt update' to update") print(" Run 'zdtt update' from your shell to update")
print() print()
except Exception: except Exception:
# Silently fail if we can't check for updates # Silently fail if we can't check for updates
@@ -596,6 +611,7 @@ ZDTT Terminal v{self.version}
plugin_commands = plugin_namespace['register_commands']() plugin_commands = plugin_namespace['register_commands']()
if isinstance(plugin_commands, dict): if isinstance(plugin_commands, dict):
self.commands.update(plugin_commands) self.commands.update(plugin_commands)
self.plugin_command_names.update(plugin_commands.keys())
loaded_count += 1 loaded_count += 1
else: else:
raise ValueError("register_commands() must return a dictionary") raise ValueError("register_commands() must return a dictionary")
@@ -612,6 +628,12 @@ ZDTT Terminal v{self.version}
if failed_count > 0: if failed_count > 0:
print(f"{failed_count} plugin(s) failed to load. Check ~/.zdtt/plugin_errors.log") print(f"{failed_count} plugin(s) failed to load. Check ~/.zdtt/plugin_errors.log")
def unload_plugin_commands(self):
"""Remove commands that originated from plugins."""
for cmd_name in list(self.plugin_command_names):
self.commands.pop(cmd_name, None)
self.plugin_command_names.clear()
def load_aliases(self): def load_aliases(self):
"""Load user-defined aliases from file""" """Load user-defined aliases from file"""
if not os.path.exists(self.aliases_file): if not os.path.exists(self.aliases_file):
@@ -705,6 +727,7 @@ ZDTT Terminal v{self.version}
print(" zps install <url> - Install plugin from URL") print(" zps install <url> - Install plugin from URL")
print(" time [options] - Display date/time (MM/DD/YY 12h default)") print(" time [options] - Display date/time (MM/DD/YY 12h default)")
print(" statusbar color <name> - Change status bar highlight color") print(" statusbar color <name> - Change status bar highlight color")
print(" update - Run the ZDTT updater helper")
print(" exit - Exit ZDTT (return to shell)") print(" exit - Exit ZDTT (return to shell)")
print(" quit - Quit and close terminal window") print(" quit - Quit and close terminal window")
print() print()
@@ -725,7 +748,7 @@ ZDTT Terminal v{self.version}
print(" date - Display current date/time") print(" date - Display current date/time")
print(" uname [options] - Display system information") print(" uname [options] - Display system information")
print(" nano <file> - Edit file with nano") print(" nano <file> - Edit file with nano")
print(" fastfetch - Display system info (auto-installs)") print(" sysfetch - Display system info (prefers distro tools)")
print() print()
print("Python Commands:") print("Python Commands:")
print(" python [args] - Run Python interpreter") print(" python [args] - Run Python interpreter")
@@ -750,6 +773,7 @@ ZDTT Terminal v{self.version}
def cmd_exit(self, args): def cmd_exit(self, args):
"""Exit ZDTT Terminal (returns to parent shell)""" """Exit ZDTT Terminal (returns to parent shell)"""
print("Goodbye!") print("Goodbye!")
os.system('clear' if os.name != 'nt' else 'cls')
self.running = False self.running = False
def cmd_quit(self, args): def cmd_quit(self, args):
@@ -823,17 +847,8 @@ ZDTT Terminal v{self.version}
# Check for reload subcommand # Check for reload subcommand
if args and args[0] == 'reload': if args and args[0] == 'reload':
print("Reloading plugins...") print("Reloading plugins...")
# Remove plugin commands from command dict # Remove plugin commands and reload aliases to avoid conflicts
plugin_commands = [] self.unload_plugin_commands()
for cmd_name, cmd_func in list(self.commands.items()):
# Check if it's not a built-in command (hacky but works)
if hasattr(cmd_func, '__self__') and cmd_func.__self__ != self:
plugin_commands.append(cmd_name)
for cmd in plugin_commands:
del self.commands[cmd]
# Clear aliases to avoid conflicts
self.aliases.clear() self.aliases.clear()
self.load_aliases() self.load_aliases()
@@ -1213,7 +1228,9 @@ ZDTT Terminal v{self.version}
for path in paths: for path in paths:
try: try:
if os.path.isfile(path): if os.path.islink(path):
os.unlink(path)
elif os.path.isfile(path):
os.remove(path) os.remove(path)
elif os.path.isdir(path): elif os.path.isdir(path):
if recursive: if recursive:
@@ -1331,97 +1348,75 @@ ZDTT Terminal v{self.version}
subprocess.run(['nano'] + args) subprocess.run(['nano'] + args)
def cmd_fastfetch(self, args): def cmd_sysfetch(self, args):
"""Display system info with fastfetch (auto-installs if needed)""" """Display system info using distro-preferred fetch tool."""
def _find_fastfetch_binary(): def _find_tool_binary(tool_name):
"""Return absolute path to fastfetch if available.""" candidate = shutil.which(tool_name)
fastfetch_path = shutil.which('fastfetch') if candidate:
if fastfetch_path: return candidate
return fastfetch_path for path in (
f"/usr/bin/{tool_name}",
# Fallback search in common locations f"/usr/local/bin/{tool_name}",
common_paths = [ os.path.expanduser(f"~/.local/bin/{tool_name}"),
'/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): if os.path.isfile(path) and os.access(path, os.X_OK):
return path return path
return None return None
def _build_install_command(): def _build_install_command(tool_name):
"""Return (cmd_list, manual_hint) based on distro/privileges."""
manual_hint = None manual_hint = None
if self.is_debian: base_cmd = None
base_cmd = ['apt-get', 'install', '-y', 'fastfetch'] if tool_name == 'fastfetch' and self.is_arch:
manual_hint = "sudo apt-get install fastfetch"
elif self.is_arch:
base_cmd = ['pacman', '-S', '--noconfirm', 'fastfetch'] base_cmd = ['pacman', '-S', '--noconfirm', 'fastfetch']
manual_hint = "sudo pacman -S fastfetch" manual_hint = "sudo pacman -S fastfetch"
elif tool_name == 'neofetch' and self.is_debian:
base_cmd = ['apt-get', 'install', '-y', 'neofetch']
manual_hint = "sudo apt-get install neofetch"
else: else:
return None, None return None, manual_hint
# Determine if sudo is needed
geteuid = getattr(os, 'geteuid', None) geteuid = getattr(os, 'geteuid', None)
is_root = geteuid is not None and geteuid() == 0 is_root = geteuid is not None and geteuid() == 0
sudo_path = shutil.which('sudo') sudo_path = shutil.which('sudo')
if is_root: if is_root:
return base_cmd, manual_hint return base_cmd, manual_hint
if sudo_path: if sudo_path:
return [sudo_path] + base_cmd, manual_hint return [sudo_path] + base_cmd, manual_hint
# Cannot elevate automatically
return None, manual_hint return None, manual_hint
# Check if fastfetch is installed tool_name = 'fastfetch' if self.is_arch else 'neofetch' if self.is_debian else None
fastfetch_bin = _find_fastfetch_binary() if not tool_name:
print("sysfetch currently supports Debian-based or Arch-based systems only.")
return
if not fastfetch_bin: tool_bin = _find_tool_binary(tool_name)
if not self.is_supported: if not tool_bin:
print("fastfetch is not installed.") install_cmd, manual_hint = _build_install_command(tool_name)
print("Auto-install is only supported on Debian-based and Arch Linux systems.") if install_cmd:
print("Please install fastfetch manually using your package manager:") print(f"{tool_name} is not installed. Installing...")
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
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(install_cmd, check=True)
print() print()
print("fastfetch installed successfully!") try:
print() subprocess.run(install_cmd, check=True)
except subprocess.CalledProcessError: print()
print("Failed to install fastfetch") print(f"{tool_name} installed successfully!")
if manual_hint: print()
print(f"Try manually: {manual_hint}") except subprocess.CalledProcessError:
else: print(f"Failed to install {tool_name}")
print("Please install fastfetch via your package manager.") if manual_hint:
return print(f"Try manually: {manual_hint}")
else:
print("Please install the tool via your package manager.")
elif manual_hint:
print(manual_hint)
tool_bin = _find_tool_binary(tool_name)
fastfetch_bin = _find_fastfetch_binary() if not tool_bin:
if not fastfetch_bin: print(f"Unable to run {tool_name}. Install it manually and rerun sysfetch.")
print("fastfetch installation completed but binary was not found.") return
print("Ensure fastfetch is in your PATH and try again.")
return
subprocess.run([fastfetch_bin] + args) subprocess.run([tool_bin] + args)
print(f"\n(sysfetch used {tool_name})\n")
# Python Commands # Python Commands
@@ -1449,6 +1444,25 @@ ZDTT Terminal v{self.version}
print("pip3: command not found") print("pip3: command not found")
print("Try installing with: sudo apt-get install python3-pip") print("Try installing with: sudo apt-get install python3-pip")
def cmd_update(self, args):
"""Trigger the external updater shipping with ZDTT."""
zdtt_wrapper = shutil.which('zdtt')
installer_script = os.path.join(
os.path.expanduser("~/.local/share/zdtt"),
'install.sh'
)
if zdtt_wrapper:
subprocess.run([zdtt_wrapper, 'update'] + args)
return
if os.path.isfile(installer_script):
subprocess.run(['bash', installer_script, 'update'] + args)
return
print("Unable to locate the ZDTT updater.")
print("Re-run the installer script or use 'zdtt update' from your shell if available.")
def execute_command(self, command_line): def execute_command(self, command_line):
"""Parse and execute a command""" """Parse and execute a command"""
if not command_line.strip(): if not command_line.strip():
@@ -1473,7 +1487,15 @@ ZDTT Terminal v{self.version}
print("No command specified with -oszdtt flag") print("No command specified with -oszdtt flag")
return return
parts = command_line.strip().split() try:
parts = shlex.split(command_line)
except ValueError as exc:
print(f"parse error: {exc}")
return
if not parts:
return
cmd = parts[0].lower() cmd = parts[0].lower()
args = parts[1:] if len(parts) > 1 else [] args = parts[1:] if len(parts) > 1 else []

View File

@@ -1 +1 @@
0.1.2.a 0.1.2.a.2