hackingtool/core.py
Hardik Zinzuvadiya 2834bd6964 Restore Description column and 1-based numbering in collection menus
HackingToolsCollection.show_options():
- Add Description column back (was dropped during restructure)
- Show first line of description; '—' for tools with no description
- Switch to box.SIMPLE_HEAD with show_lines=True for row separators
- Numbering changed from 0-based to 1-based (index 1..N)
- Input validation updated: 1 <= choice <= len(active) with active[choice-1]
2026-03-15 16:46:39 +05:30

302 lines
10 KiB
Python

import os
import sys
import webbrowser
from collections.abc import Callable
from platform import system
from rich import box
from rich.console import Console
from rich.panel import Panel
from rich.prompt import Prompt
from rich.table import Table
from rich.theme import Theme
from rich.traceback import install
from constants import (
THEME_PRIMARY, THEME_BORDER, THEME_ACCENT,
THEME_SUCCESS, THEME_ERROR, THEME_WARNING,
THEME_DIM, THEME_ARCHIVED, THEME_URL,
)
# Enable rich tracebacks globally
install()
_theme = Theme({
"purple": "#7B61FF",
"success": THEME_SUCCESS,
"error": THEME_ERROR,
"warning": THEME_WARNING,
"archived": THEME_ARCHIVED,
"url": THEME_URL,
"dim": THEME_DIM,
})
# Single shared console — all tool files do: from core import console
console = Console(theme=_theme)
def clear_screen():
os.system("cls" if system() == "Windows" else "clear")
def validate_input(ip, val_range: list) -> int | None:
"""Return the integer if it is in val_range, else None."""
if not val_range:
return None
try:
ip = int(ip)
if ip in val_range:
return ip
except (TypeError, ValueError):
pass
return None
class HackingTool:
TITLE: str = ""
DESCRIPTION: str = ""
INSTALL_COMMANDS: list[str] = []
UNINSTALL_COMMANDS: list[str] = []
RUN_COMMANDS: list[str] = []
OPTIONS: list[tuple[str, Callable]] = []
PROJECT_URL: str = ""
# OS / capability metadata
SUPPORTED_OS: list[str] = ["linux", "macos"]
REQUIRES_ROOT: bool = False
REQUIRES_WIFI: bool = False
REQUIRES_GO: bool = False
REQUIRES_RUBY: bool = False
REQUIRES_JAVA: bool = False
REQUIRES_DOCKER: bool = False
# Archived tool flags
ARCHIVED: bool = False
ARCHIVED_REASON: str = ""
def __init__(self, options=None, installable=True, runnable=True):
options = options or []
if not isinstance(options, list):
raise TypeError("options must be a list of (option_name, option_fn) tuples")
self.OPTIONS = []
if installable:
self.OPTIONS.append(("Install", self.install))
if runnable:
self.OPTIONS.append(("Run", self.run))
self.OPTIONS.extend(options)
def show_info(self):
desc = f"[cyan]{self.DESCRIPTION}[/cyan]"
if self.PROJECT_URL:
desc += f"\n[url]🔗 {self.PROJECT_URL}[/url]"
if self.ARCHIVED:
desc += f"\n[archived]⚠ ARCHIVED: {self.ARCHIVED_REASON}[/archived]"
console.print(Panel(
desc,
title=f"[{THEME_PRIMARY}]{self.TITLE}[/{THEME_PRIMARY}]",
border_style="purple",
box=box.DOUBLE,
))
def show_options(self, parent=None):
"""Iterative menu loop — no recursion, no stack growth."""
while True:
clear_screen()
self.show_info()
table = Table(title="Options", box=box.SIMPLE_HEAVY)
table.add_column("No.", style="bold cyan", justify="center")
table.add_column("Action", style="bold yellow")
for index, option in enumerate(self.OPTIONS):
table.add_row(str(index + 1), option[0])
if self.PROJECT_URL:
table.add_row("98", "Open Project Page")
table.add_row("99", f"Back to {parent.TITLE if parent else 'Main Menu'}")
console.print(table)
raw = Prompt.ask("\n[bold cyan][?] Select an option[/bold cyan]", default="")
if not raw.strip():
continue
try:
choice = int(raw)
except ValueError:
console.print("[error]⚠ Please enter a number.[/error]")
Prompt.ask("[dim]Press Enter to continue[/dim]", default="")
continue
if choice == 99:
return
elif choice == 98 and self.PROJECT_URL:
self.show_project_page()
elif 1 <= choice <= len(self.OPTIONS):
try:
self.OPTIONS[choice - 1][1]()
except Exception:
console.print_exception(show_locals=True)
Prompt.ask("[dim]Press Enter to continue[/dim]", default="")
else:
console.print("[error]⚠ Invalid option.[/error]")
def before_install(self): pass
def install(self):
self.before_install()
if isinstance(self.INSTALL_COMMANDS, (list, tuple)):
for cmd in self.INSTALL_COMMANDS:
console.print(f"[warning]→ {cmd}[/warning]")
os.system(cmd)
self.after_install()
def after_install(self):
console.print("[success]✔ Successfully installed![/success]")
def before_uninstall(self) -> bool:
return True
def uninstall(self):
if self.before_uninstall():
if isinstance(self.UNINSTALL_COMMANDS, (list, tuple)):
for cmd in self.UNINSTALL_COMMANDS:
console.print(f"[error]→ {cmd}[/error]")
os.system(cmd)
self.after_uninstall()
def after_uninstall(self): pass
def before_run(self): pass
def run(self):
self.before_run()
if isinstance(self.RUN_COMMANDS, (list, tuple)):
for cmd in self.RUN_COMMANDS:
console.print(f"[cyan]⚙ Running:[/cyan] [bold]{cmd}[/bold]")
os.system(cmd)
self.after_run()
def after_run(self): pass
def show_project_page(self):
console.print(f"[url]🌐 Opening: {self.PROJECT_URL}[/url]")
webbrowser.open_new_tab(self.PROJECT_URL)
class HackingToolsCollection:
TITLE: str = ""
DESCRIPTION: str = ""
TOOLS: list = []
def __init__(self):
pass
def show_info(self):
console.rule(f"[{THEME_PRIMARY}]{self.TITLE}[/{THEME_PRIMARY}]", style="purple")
if self.DESCRIPTION:
console.print(f"[italic cyan]{self.DESCRIPTION}[/italic cyan]\n")
def _active_tools(self) -> list:
"""Return tools that are not archived and are OS-compatible."""
from os_detect import CURRENT_OS
return [
t for t in self.TOOLS
if not getattr(t, "ARCHIVED", False)
and CURRENT_OS.system in getattr(t, "SUPPORTED_OS", ["linux", "macos"])
]
def _archived_tools(self) -> list:
return [t for t in self.TOOLS if getattr(t, "ARCHIVED", False)]
def _incompatible_tools(self) -> list:
from os_detect import CURRENT_OS
return [
t for t in self.TOOLS
if not getattr(t, "ARCHIVED", False)
and CURRENT_OS.system not in getattr(t, "SUPPORTED_OS", ["linux", "macos"])
]
def _show_archived_tools(self):
"""Show archived tools sub-menu (option 98)."""
archived = self._archived_tools()
if not archived:
console.print("[dim]No archived tools in this category.[/dim]")
Prompt.ask("[dim]Press Enter to return[/dim]", default="")
return
while True:
clear_screen()
console.rule(f"[archived]Archived Tools — {self.TITLE}[/archived]", style="yellow")
table = Table(box=box.MINIMAL_DOUBLE_HEAD, show_lines=True)
table.add_column("No.", justify="center", style="bold yellow")
table.add_column("Tool", style="dim yellow")
table.add_column("Reason", style="dim white")
for i, tool in enumerate(archived):
reason = getattr(tool, "ARCHIVED_REASON", "No reason given")
table.add_row(str(i + 1), tool.TITLE, reason)
table.add_row("99", "Back", "")
console.print(table)
raw = Prompt.ask("[bold yellow][?] Select[/bold yellow]", default="99")
try:
choice = int(raw)
except ValueError:
continue
if choice == 99:
return
elif 1 <= choice <= len(archived):
archived[choice - 1].show_options(parent=self)
def show_options(self, parent=None):
"""Iterative menu loop — no recursion, no stack growth."""
while True:
clear_screen()
self.show_info()
active = self._active_tools()
incompatible = self._incompatible_tools()
archived = self._archived_tools()
table = Table(title="Available Tools", box=box.SIMPLE_HEAD, show_lines=True)
table.add_column("No.", justify="center", style="bold cyan", width=6)
table.add_column("Tool", style="bold yellow", min_width=24)
table.add_column("Description", style="white", overflow="fold")
for index, tool in enumerate(active, start=1):
desc = getattr(tool, "DESCRIPTION", "") or ""
# Show only first line of description to keep rows compact
desc = desc.splitlines()[0] if desc != "" else ""
table.add_row(str(index), tool.TITLE, desc)
if archived:
table.add_row("[dim]98[/dim]", f"[archived]Archived tools ({len(archived)})[/archived]", "")
if incompatible:
console.print(f"[dim]({len(incompatible)} tools hidden — not supported on current OS)[/dim]")
table.add_row("99", f"Back to {parent.TITLE if parent else 'Main Menu'}", "")
console.print(table)
raw = Prompt.ask("\n[bold cyan][?] Choose a tool[/bold cyan]", default="")
if not raw.strip():
continue
try:
choice = int(raw)
except ValueError:
console.print("[error]⚠ Please enter a number.[/error]")
continue
if choice == 99:
return
elif choice == 98 and archived:
self._show_archived_tools()
elif 1 <= choice <= len(active):
try:
active[choice - 1].show_options(parent=self)
except Exception:
console.print_exception(show_locals=True)
Prompt.ask("[dim]Press Enter to continue[/dim]", default="")
else:
console.print("[error]⚠ Invalid option.[/error]")