Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/env python3
- import argparse
- import os
- import re
- import shutil
- import stat
- import subprocess
- import sys
- import tempfile
- import time
- # =============================================================
- # GENERAL SETTINGS
- # =============================================================
- # Initial xterm geometry before it gets repositioned/resized
- # into its grid cell. Doesn't need to be precise - it's just a
- # starting size.
- WINDOW_GEOMETRY = "80x24"
- FONT = "Monospace"
- FONT_SIZE = "11"
- # Delay between launching terminals.
- #
- # 0 = launch all immediately
- #
- LAUNCH_DELAY = 0.25
- # =============================================================
- # GRID / TILING SETTINGS
- # =============================================================
- # Rows x Cols must have at least as many cells as terminals (6).
- # 2x3 gives six equal-sized tiles.
- GRID_ROWS = 2
- GRID_COLS = 3
- # Pixel gap between tiles (also applied around window edges).
- GRID_GAP = 6
- # Reserve space (in pixels) at the top/bottom for taskbars,
- # panels, etc. Adjust to taste for your window manager.
- SCREEN_MARGIN_TOP = 0
- SCREEN_MARGIN_BOTTOM = 0
- # How long (seconds) and how often to poll while waiting for a
- # freshly-launched xterm's window to appear before positioning
- # it.
- POSITION_ATTEMPTS = 30
- POSITION_DELAY = 0.15
- # =============================================================
- # WPScan SETTINGS
- # =============================================================
- # The API token can be supplied through the environment:
- #
- # export WPSCAN_API_TOKEN="your-token"
- #
- # If the environment variable isn't present, the token below
- # will be used.
- #
- WPSCAN_API_TOKEN = os.environ.get(
- "WPSCAN_API_TOKEN",
- "APICODEHERE"
- )
- # =============================================================
- # =============================================================
- # TOOL INSTALLATION SETTINGS
- # =============================================================
- # Set to False if you only want the script to report missing tools.
- AUTO_INSTALL_TOOLS = True
- # apt packages used by this launcher.
- APT_PACKAGES = {
- "xterm": "xterm",
- "xdotool": "xdotool",
- "xrandr": "x11-xserver-utils",
- "curl": "curl",
- "unzip": "unzip",
- "sudo": "sudo",
- "wpscan": "wpscan",
- "sqlmap": "sqlmap",
- "nikto": "nikto",
- "nmap": "nmap",
- "python3": "python3",
- }
- # Nuclei is normally easiest to install from its official release
- # when it is not available through apt.
- NUCLEI_INSTALL_PATH = "/usr/local/bin/nuclei"
- # Findomain is installed from its GitHub release when absent.
- FINDOMAIN_INSTALL_PATH = "/usr/local/bin/findomain"
- # =============================================================
- # TERMINAL 1
- # =============================================================
- TERMINAL_1 = {
- "key": "wpscan",
- "title": "WPScan",
- "command": (
- "wpscan "
- "--proxy http://127.0.0.1:8080 "
- "--rua "
- "--api-token \"$WPSCAN_API_TOKEN\" "
- "--url {host}"
- ),
- "background": "#000000",
- "foreground": "#00ff00",
- }
- # =============================================================
- # TERMINAL 2
- # =============================================================
- TERMINAL_2 = {
- "key": "sqlmap",
- "title": "SQLMap",
- "command": (
- "python3 /media/veracrypt1/sqlmap/sqlmap.py "
- "--beep "
- "--eta "
- "--batch "
- "--random-agent "
- "--threads=5 "
- "--tamper=space2comment,randomcase,charencode "
- "--crawl=5 "
- "--risk=3 "
- "--level=5 "
- "--dump-all "
- "-o "
- "--forms "
- "--proxy=http://127.0.0.1:8080 "
- "-u {host}"
- ),
- "background": "#000000",
- "foreground": "#00ffff",
- }
- # =============================================================
- # TERMINAL 3
- # =============================================================
- TERMINAL_3 = {
- "key": "nikto",
- "title": "Nikto",
- "command": (
- "nikto "
- "-Tuning 1,2,3,4,5,7,8,9,0,a,b,c "
- "-output nikto.txt "
- "-mutate 2,3,4,5 "
- "-Display 1,2,3,4,V "
- "-ask auto "
- "-host {host}"
- ),
- "background": "#000000",
- "foreground": "#ffff00",
- }
- # =============================================================
- # TERMINAL 4
- # =============================================================
- TERMINAL_4 = {
- "key": "nmap",
- "title": "NMap",
- "command": (
- "nmap "
- "-sC "
- "-sV "
- "-oG \"nmap log\" "
- "{host}"
- ),
- "background": "#000000",
- "foreground": "#ff00ff",
- }
- # =============================================================
- # TERMINAL 5
- # =============================================================
- TERMINAL_5 = {
- "key": "nuclei",
- "title": "Nuclei",
- "command": (
- "nuclei "
- "-target {host}"
- ),
- "background": "#000000",
- "foreground": "#ffffff",
- }
- # =============================================================
- # TERMINAL 6
- # FINDOMAIN
- # =============================================================
- TERMINAL_6 = {
- "key": "findomain",
- "title": "Findomain",
- "command": (
- "findomain -t {host}"
- ),
- "background": "#000000",
- "foreground": "#ff6600",
- }
- # =============================================================
- # TERMINAL LIST
- # =============================================================
- TERMINALS = [
- TERMINAL_1,
- TERMINAL_2,
- TERMINAL_3,
- TERMINAL_4,
- TERMINAL_5,
- TERMINAL_6,
- ]
- # =============================================================
- # ARGUMENT PARSER
- # =============================================================
- def parse_arguments():
- parser = argparse.ArgumentParser(
- description=(
- "Check/install dependencies and launch six independent xterm windows "
- "using a supplied host."
- )
- )
- parser.add_argument(
- "host",
- nargs="?",
- default=None,
- help="Target hostname or URL."
- )
- parser.add_argument(
- "--monitor",
- default=None,
- help=(
- "Force a specific monitor instead of "
- "auto-detecting one. Accepts either the monitor's "
- "index (0-based, in the order `xrandr --query` "
- "lists connected monitors) or its xrandr name "
- "(e.g. HDMI-1, eDP-1, DP-2). Run with "
- "--list-monitors to see the available values."
- )
- )
- parser.add_argument(
- "--list-monitors",
- action="store_true",
- help=(
- "Print detected monitors (with the index/name "
- "you can pass to --monitor) and exit without "
- "launching anything."
- )
- )
- parser.add_argument(
- "--terminals",
- default=None,
- help=(
- "Comma-separated list of terminals to launch, "
- "instead of all six. Accepts either 1-based index "
- "(in the order below) or name, case-insensitive: "
- "1=wpscan, 2=sqlmap, 3=nikto, 4=nmap, 5=nuclei, "
- "6=findomain. Example: --terminals nmap,nuclei or "
- "--terminals 4,5. If omitted, all six run. Run "
- "with --list-terminals to see the available "
- "values."
- )
- )
- parser.add_argument(
- "--list-terminals",
- action="store_true",
- help=(
- "Print available terminals (with the index/name "
- "you can pass to --terminals) and exit without "
- "launching anything."
- )
- )
- args = parser.parse_args()
- skip_host_requirement = (
- args.list_monitors
- or args.list_terminals
- )
- if not skip_host_requirement and not args.host:
- parser.error(
- "the following arguments are required: host "
- "(unless --list-monitors or --list-terminals is "
- "given)"
- )
- return args
- # =============================================================
- # NORMALIZE HOST
- # =============================================================
- def normalize_host(host):
- """
- Remove http:// or https:// from the supplied host.
- Examples:
- https://example.com
- ->
- example.com
- http://example.com/test
- ->
- example.com/test
- example.com
- ->
- example.com
- """
- host = host.strip()
- host = re.sub(
- r"^https?://",
- "",
- host,
- flags=re.IGNORECASE
- )
- if host.startswith("//"):
- host = host[2:]
- host = host.rstrip("/")
- return host
- # =============================================================
- # CHECK COMMAND
- # =============================================================
- def command_exists(command):
- """
- Determine whether an executable is available.
- """
- # Handle absolute paths.
- if os.path.isabs(command):
- return (
- os.path.isfile(command)
- and os.access(command, os.X_OK)
- )
- return shutil.which(command) is not None
- # =============================================================
- # TOOL CHECK / INSTALL HELPERS
- # =============================================================
- def run_command(command, check=False, **kwargs):
- """Run a command while keeping output visible to the user."""
- return subprocess.run(command, check=check, **kwargs)
- def apt_update():
- """Refresh apt package indexes once."""
- print()
- print("[*] Updating apt package indexes...")
- try:
- run_command(["sudo", "apt-get", "update"], check=True)
- return True
- except (subprocess.CalledProcessError, FileNotFoundError) as error:
- print(f"[ERROR] apt update failed: {error}")
- return False
- def install_apt_packages(packages):
- """Install missing Debian/Ubuntu/Mint packages in one apt call."""
- if not packages:
- return True
- if not command_exists("sudo"):
- print("[ERROR] sudo is required for automatic package installation.")
- return False
- if not apt_update():
- return False
- print()
- print("[*] Installing missing apt packages:")
- print(" " + " ".join(packages))
- print()
- try:
- run_command(
- ["sudo", "apt-get", "install", "-y", *packages],
- check=True,
- )
- return True
- except subprocess.CalledProcessError as error:
- print(f"[ERROR] apt installation failed: {error}")
- return False
- def get_missing_commands(commands):
- """Return commands that are not currently available."""
- return [
- command
- for command in commands
- if not command_exists(command)
- ]
- def install_nuclei():
- """
- Install Nuclei from the latest ProjectDiscovery GitHub release.
- The release API is used to select the correct Linux architecture.
- """
- if command_exists("nuclei"):
- return True
- print()
- print("[*] Nuclei was not found - attempting automatic installation...")
- if not command_exists("curl"):
- print("[ERROR] curl is required before Nuclei can be installed.")
- return False
- machine = subprocess.check_output(
- ["uname", "-m"], text=True
- ).strip()
- architecture_map = {
- "x86_64": "amd64",
- "amd64": "amd64",
- "aarch64": "arm64",
- "arm64": "arm64",
- "armv7l": "armv7",
- }
- nuclei_arch = architecture_map.get(machine)
- if not nuclei_arch:
- print(f"[ERROR] Unsupported CPU architecture for Nuclei: {machine}")
- return False
- with tempfile.TemporaryDirectory(prefix="nuclei-install-") as tmp_dir:
- try:
- api_url = (
- "https://api.github.com/repos/projectdiscovery/"
- "nuclei/releases/latest"
- )
- metadata_path = os.path.join(tmp_dir, "release.json")
- run_command(
- ["curl", "-fsSL", api_url, "-o", metadata_path],
- check=True,
- )
- import json
- with open(metadata_path, "r", encoding="utf-8") as file:
- release = json.load(file)
- asset = None
- for candidate in release.get("assets", []):
- name = candidate.get("name", "")
- if (
- name.endswith(".zip")
- and "linux" in name.lower()
- and nuclei_arch in name.lower()
- ):
- asset = candidate
- break
- if not asset:
- print(
- "[ERROR] Could not find a matching Nuclei release "
- f"asset for Linux/{nuclei_arch}."
- )
- return False
- archive_name = asset["name"]
- archive_path = os.path.join(tmp_dir, archive_name)
- print(f"[*] Downloading Nuclei: {archive_name}")
- run_command(
- ["curl", "-fL", asset["browser_download_url"],
- "-o", archive_path],
- check=True,
- )
- run_command(
- ["unzip", "-o", archive_name],
- cwd=tmp_dir,
- check=True,
- )
- binary_path = os.path.join(tmp_dir, "nuclei")
- if not os.path.isfile(binary_path):
- print("[ERROR] Nuclei binary was not found after extraction.")
- return False
- os.chmod(
- binary_path,
- os.stat(binary_path).st_mode
- | stat.S_IXUSR
- | stat.S_IXGRP
- | stat.S_IXOTH,
- )
- print(f"[*] Installing Nuclei to {NUCLEI_INSTALL_PATH}")
- run_command(
- ["sudo", "install", "-m", "0755",
- binary_path, NUCLEI_INSTALL_PATH],
- check=True,
- )
- except (subprocess.CalledProcessError, OSError, ValueError) as error:
- print(f"[ERROR] Nuclei installation failed: {error}")
- return False
- if command_exists("nuclei"):
- print("[OK] Nuclei installed successfully.")
- return True
- print("[ERROR] Nuclei is still unavailable after installation.")
- return False
- def install_findomain():
- """
- Install Findomain from its latest GitHub release.
- The release API is used so the correct architecture is selected
- instead of assuming an i386 machine.
- """
- if command_exists("findomain"):
- return True
- print()
- print("[*] Findomain was not found - attempting automatic installation...")
- if not command_exists("curl"):
- print("[ERROR] curl is required before Findomain can be installed.")
- return False
- machine = subprocess.check_output(
- ["uname", "-m"], text=True
- ).strip()
- architecture_map = {
- "x86_64": "amd64",
- "amd64": "amd64",
- "aarch64": "arm64",
- "arm64": "arm64",
- "i686": "i386",
- "i386": "i386",
- }
- findomain_arch = architecture_map.get(machine)
- if not findomain_arch:
- print(
- "[ERROR] Unsupported CPU architecture for Findomain: "
- f"{machine}"
- )
- return False
- with tempfile.TemporaryDirectory(prefix="findomain-install-") as tmp_dir:
- try:
- api_url = (
- "https://api.github.com/repos/Findomain/Findomain/"
- "releases/latest"
- )
- metadata_path = os.path.join(tmp_dir, "release.json")
- run_command(
- ["curl", "-fsSL", api_url, "-o", metadata_path],
- check=True,
- )
- import json
- with open(metadata_path, "r", encoding="utf-8") as file:
- release = json.load(file)
- asset = None
- for candidate in release.get("assets", []):
- name = candidate.get("name", "").lower()
- if (
- "linux" in name
- and findomain_arch in name
- and name.endswith(".zip")
- ):
- asset = candidate
- break
- if not asset:
- print(
- "[ERROR] Could not find a matching Findomain release "
- f"asset for Linux/{findomain_arch}."
- )
- return False
- archive_name = asset["name"]
- archive_path = os.path.join(tmp_dir, archive_name)
- print(f"[*] Downloading Findomain: {archive_name}")
- run_command(
- ["curl", "-fL", asset["browser_download_url"],
- "-o", archive_path],
- check=True,
- )
- run_command(
- ["unzip", "-o", archive_name],
- cwd=tmp_dir,
- check=True,
- )
- binary_path = os.path.join(tmp_dir, "findomain")
- if not os.path.isfile(binary_path):
- print("[ERROR] Findomain binary was not found after extraction.")
- return False
- os.chmod(
- binary_path,
- os.stat(binary_path).st_mode
- | stat.S_IXUSR
- | stat.S_IXGRP
- | stat.S_IXOTH,
- )
- print(f"[*] Installing Findomain to {FINDOMAIN_INSTALL_PATH}")
- run_command(
- ["sudo", "install", "-m", "0755",
- binary_path, FINDOMAIN_INSTALL_PATH],
- check=True,
- )
- except (subprocess.CalledProcessError, OSError, ValueError) as error:
- print(f"[ERROR] Findomain installation failed: {error}")
- return False
- if command_exists("findomain"):
- print("[OK] Findomain installed successfully.")
- return True
- print("[ERROR] Findomain is still unavailable after installation.")
- return False
- def check_and_install_all_tools():
- """
- Check every external dependency used by this launcher.
- Missing apt-based tools are installed together. Nuclei and
- Findomain are installed from their current GitHub releases.
- """
- print()
- print("======================================================")
- print(" Checking required tools")
- print("======================================================")
- print()
- # First make sure sudo itself exists. We cannot auto-install
- # anything else without it.
- if not command_exists("sudo"):
- print("[ERROR] sudo is not available.")
- print("Install sudo manually, then run this script again.")
- return False
- apt_commands = [
- command for command in APT_PACKAGES
- if not command_exists(command)
- ]
- if apt_commands:
- apt_packages = [
- APT_PACKAGES[command]
- for command in apt_commands
- if command != "sudo"
- ]
- print("[*] Missing apt tools:")
- print(" " + ", ".join(apt_commands))
- if AUTO_INSTALL_TOOLS:
- if not install_apt_packages(apt_packages):
- return False
- else:
- print("[WARNING] Automatic installation is disabled.")
- else:
- print("[OK] All apt-based tools are already installed.")
- # Re-check after apt installation.
- missing_after_apt = get_missing_commands(
- list(APT_PACKAGES.keys())
- )
- if missing_after_apt:
- print()
- print("[ERROR] Still missing after apt installation:")
- print(" " + ", ".join(missing_after_apt))
- return False
- if command_exists("nuclei"):
- print("[OK] nuclei")
- elif AUTO_INSTALL_TOOLS:
- if not install_nuclei():
- return False
- else:
- print("[MISSING] nuclei")
- if command_exists("findomain"):
- print("[OK] findomain")
- elif AUTO_INSTALL_TOOLS:
- if not install_findomain():
- return False
- else:
- print("[MISSING] findomain")
- final_required = [
- "xterm",
- "xdotool",
- "xrandr",
- "curl",
- "unzip",
- "sudo",
- "python3",
- "wpscan",
- "sqlmap",
- "nikto",
- "nmap",
- "nuclei",
- "findomain",
- ]
- missing_final = get_missing_commands(final_required)
- print()
- if missing_final:
- print("======================================================")
- print(" TOOL CHECK FAILED")
- print("======================================================")
- print()
- print("Missing:")
- for tool in missing_final:
- print(f" - {tool}")
- print()
- return False
- print("======================================================")
- print(" All required tools are available")
- print("======================================================")
- print()
- return True
- # =============================================================
- # CHECK XTERM
- # =============================================================
- def check_xterm():
- if not command_exists("xterm"):
- print()
- print("ERROR: xterm was not found.")
- print()
- print("Install it with:")
- print()
- print(" sudo apt install xterm")
- print()
- sys.exit(1)
- # =============================================================
- # CHECK XDOTOOL
- # =============================================================
- def check_xdotool():
- """
- xdotool is used to move/resize windows into the grid after
- launch. Tiling still works without it if xrandr also isn't
- found, but windows will keep their default placement.
- """
- if not command_exists("xdotool"):
- print()
- print(
- "WARNING: xdotool was not found. Terminals will "
- "launch but will NOT be tiled into equal grid "
- "cells."
- )
- print()
- print("Install it with:")
- print()
- print(" sudo apt install xdotool")
- print()
- return False
- return True
- # =============================================================
- # ENUMERATE MONITORS
- # =============================================================
- def get_monitors():
- """
- Enumerate connected monitors using xrandr.
- Returns a list of dicts:
- {
- "name": "HDMI-1",
- "width": 1920,
- "height": 1080,
- "x": 0,
- "y": 0,
- "primary": True
- }
- Returns an empty list if xrandr isn't available or nothing
- could be parsed.
- """
- monitors = []
- if not command_exists("xrandr"):
- return monitors
- try:
- output = subprocess.check_output(
- ["xrandr", "--query"],
- text=True,
- stderr=subprocess.DEVNULL
- )
- except Exception:
- return monitors
- for line in output.splitlines():
- if " connected" not in line:
- continue
- match = re.search(
- r"(\d+)x(\d+)\+(\d+)\+(\d+)",
- line
- )
- if not match:
- # Connected but currently disabled/no geometry
- # (e.g. lid-closed laptop panel) - skip it.
- continue
- width, height, x, y = (
- int(value) for value in match.groups()
- )
- monitors.append(
- {
- "name": line.split()[0],
- "width": width,
- "height": height,
- "x": x,
- "y": y,
- "primary": " primary" in line,
- }
- )
- return monitors
- # =============================================================
- # GET ACTIVE WINDOW GEOMETRY
- # =============================================================
- def get_active_window_geometry():
- """
- Return (x, y, width, height) of the currently active window
- (in absolute screen pixels) using xdotool, or None if it
- can't be determined.
- Called early in main(), before any xterms are launched, so
- this should be the terminal the script was run from.
- """
- if not command_exists("xdotool"):
- return None
- try:
- result = subprocess.run(
- [
- "xdotool",
- "getactivewindow",
- "getwindowgeometry",
- "--shell"
- ],
- capture_output=True,
- text=True
- )
- except Exception:
- return None
- if result.returncode != 0:
- return None
- values = {}
- for line in result.stdout.splitlines():
- if "=" in line:
- key, _, value = line.partition("=")
- values[key.strip()] = value.strip()
- try:
- return (
- int(values["X"]),
- int(values["Y"]),
- int(values["WIDTH"]),
- int(values["HEIGHT"])
- )
- except (KeyError, ValueError):
- return None
- # =============================================================
- # GET MOUSE POSITION
- # =============================================================
- def get_mouse_position():
- """
- Return (x, y) of the current mouse pointer in absolute
- screen pixels using xdotool, or None if it can't be
- determined.
- """
- if not command_exists("xdotool"):
- return None
- try:
- result = subprocess.run(
- ["xdotool", "getmouselocation", "--shell"],
- capture_output=True,
- text=True
- )
- except Exception:
- return None
- if result.returncode != 0:
- return None
- values = {}
- for line in result.stdout.splitlines():
- if "=" in line:
- key, _, value = line.partition("=")
- values[key.strip()] = value.strip()
- try:
- return int(values["X"]), int(values["Y"])
- except (KeyError, ValueError):
- return None
- # =============================================================
- # FIND MONITOR CONTAINING A POINT
- # =============================================================
- def find_monitor_for_point(monitors, point_x, point_y):
- for monitor in monitors:
- if (
- monitor["x"] <= point_x < monitor["x"] + monitor["width"]
- and
- monitor["y"] <= point_y < monitor["y"] + monitor["height"]
- ):
- return monitor
- return None
- # =============================================================
- # DETERMINE TARGET MONITOR
- # =============================================================
- def determine_target_monitor(xdotool_available, verbose=True):
- """
- Work out which monitor to tile the six terminals on: the
- one the launcher itself is currently running on.
- Resolution order:
- 1. Active window's center point (the terminal running
- this script).
- 2. Current mouse position.
- 3. Monitor marked "primary" by xrandr.
- 4. First monitor xrandr reports.
- 5. Hardcoded 1920x1080 @ 0,0 if xrandr found nothing.
- When `verbose` is True, prints each step's raw result so
- you can see *why* a given monitor was picked - this is the
- thing to check if it picks the wrong one. If it's wrong,
- pass --monitor to force a specific one instead of relying
- on auto-detection.
- """
- def log(message):
- if verbose:
- print(f" [monitor-detect] {message}")
- monitors = get_monitors()
- if verbose:
- print("Detecting target monitor...")
- if not monitors:
- log("xrandr reported no usable monitors.")
- return {
- "name": "fallback",
- "width": 1920,
- "height": 1080,
- "x": 0,
- "y": 0,
- "primary": True,
- "detected_via": "hardcoded fallback",
- }
- log(f"xrandr monitors: {monitors}")
- if len(monitors) == 1:
- monitor = dict(monitors[0])
- monitor["detected_via"] = "only monitor"
- log("Only one monitor detected, using it.")
- return monitor
- if not xdotool_available:
- log(
- "xdotool unavailable - can't check active window "
- "or mouse position."
- )
- if xdotool_available:
- geometry = get_active_window_geometry()
- log(f"Active window geometry (x, y, w, h): {geometry}")
- if geometry:
- x, y, width, height = geometry
- center_x = x + width // 2
- center_y = y + height // 2
- log(
- f"Active window center point: "
- f"({center_x}, {center_y})"
- )
- match = find_monitor_for_point(
- monitors,
- center_x,
- center_y
- )
- if match:
- monitor = dict(match)
- monitor["detected_via"] = "active window"
- log(
- f"Center point falls on monitor "
- f"'{match['name']}' - using it."
- )
- return monitor
- log(
- "Active window center point didn't fall "
- "inside any reported monitor bounds."
- )
- else:
- log(
- "Couldn't read an active window (no window "
- "manager support for _NET_ACTIVE_WINDOW, or "
- "DISPLAY/XAUTHORITY not set correctly for "
- "this process)."
- )
- position = get_mouse_position()
- log(f"Mouse position (x, y): {position}")
- if position:
- match = find_monitor_for_point(monitors, *position)
- if match:
- monitor = dict(match)
- monitor["detected_via"] = "mouse position"
- log(
- f"Mouse position falls on monitor "
- f"'{match['name']}' - using it."
- )
- return monitor
- log(
- "Mouse position didn't fall inside any "
- "reported monitor bounds."
- )
- log(
- "Falling back to the primary/first monitor - this is "
- "likely NOT the monitor you're actually using. Pass "
- "--monitor to force the right one (see "
- "--list-monitors)."
- )
- for monitor in monitors:
- if monitor["primary"]:
- result = dict(monitor)
- result["detected_via"] = "primary flag"
- return result
- result = dict(monitors[0])
- result["detected_via"] = "first monitor"
- return result
- # =============================================================
- # RESOLVE MONITOR BY EXPLICIT SELECTOR (--monitor)
- # =============================================================
- def resolve_monitor_by_selector(selector, monitors):
- """
- Resolve a user-supplied --monitor value against the
- detected monitor list.
- Accepts either a 0-based index (in xrandr's listed order)
- or an xrandr output name (e.g. "HDMI-1"), matched case-
- insensitively.
- Returns the matching monitor dict (with "detected_via" set
- to "manual selection"), or None if it couldn't be resolved.
- """
- if not monitors:
- return None
- # Try as a numeric index first.
- try:
- index = int(selector)
- except ValueError:
- index = None
- if index is not None:
- if 0 <= index < len(monitors):
- monitor = dict(monitors[index])
- monitor["detected_via"] = "manual selection (index)"
- return monitor
- return None
- # Fall back to matching by name.
- for monitor in monitors:
- if monitor["name"].lower() == selector.lower():
- result = dict(monitor)
- result["detected_via"] = "manual selection (name)"
- return result
- return None
- # =============================================================
- # PRINT MONITOR LIST
- # =============================================================
- def print_monitor_list(monitors):
- if not monitors:
- print(
- "No monitors detected (xrandr missing, "
- "unavailable, or returned nothing usable)."
- )
- return
- print()
- print("Detected monitors:")
- print()
- for index, monitor in enumerate(monitors):
- primary_tag = " (primary)" if monitor["primary"] else ""
- print(
- f" [{index}] {monitor['name']}{primary_tag}: "
- f"{monitor['width']}x{monitor['height']}"
- f"+{monitor['x']}+{monitor['y']}"
- )
- print()
- print(
- "Pass either the index or the name to --monitor, e.g.:"
- )
- print()
- print(
- f" python3 {os.path.basename(sys.argv[0])} "
- f"--monitor {monitors[0]['name']} <host>"
- )
- print()
- # =============================================================
- # RESOLVE SELECTED TERMINALS (--terminals)
- # =============================================================
- def resolve_selected_terminals(selector_string, terminals):
- """
- Parse a comma-separated --terminals value into a list of
- terminal configs, preserving `terminals`' original order
- (not the order tokens were given in).
- Each token may be a 1-based index (in `terminals`' order)
- or a terminal's "key" (case-insensitive, e.g. "wpscan").
- Raises ValueError with a message listing any tokens that
- didn't match anything, for the caller to display alongside
- print_terminal_list().
- """
- tokens = [
- token.strip()
- for token in selector_string.split(",")
- if token.strip()
- ]
- if not tokens:
- raise ValueError("no terminals specified")
- keys = [config["key"] for config in terminals]
- selected_indices = set()
- unknown_tokens = []
- for token in tokens:
- if token.isdigit():
- index = int(token) - 1
- if 0 <= index < len(terminals):
- selected_indices.add(index)
- else:
- unknown_tokens.append(token)
- continue
- normalized = token.lower()
- if normalized in keys:
- selected_indices.add(keys.index(normalized))
- else:
- unknown_tokens.append(token)
- if unknown_tokens:
- raise ValueError(
- f"unrecognized terminal(s): {', '.join(unknown_tokens)}"
- )
- return [
- terminals[index]
- for index in sorted(selected_indices)
- ]
- # =============================================================
- # PRINT TERMINAL LIST
- # =============================================================
- def print_terminal_list(terminals):
- print()
- print("Available terminals:")
- print()
- for index, config in enumerate(terminals, start=1):
- print(
- f" [{index}] {config['key']} - {config['title']}"
- )
- print()
- print(
- "Pass a comma-separated list of indexes or names to "
- "--terminals, e.g.:"
- )
- print()
- print(
- f" python3 {os.path.basename(sys.argv[0])} "
- f"--terminals {terminals[0]['key']},{terminals[-1]['key']} "
- f"<host>"
- )
- print()
- # =============================================================
- # INTERACTIVE HOST PROMPT
- # =============================================================
- def prompt_for_host():
- """
- Interactively ask for a target host. Used when the script
- is run with no CLI arguments at all, since `host` is
- otherwise required.
- """
- while True:
- raw_host = input(
- "Target host (e.g. https://example.com): "
- ).strip()
- if raw_host:
- return raw_host
- print("Host cannot be empty. Try again.")
- # =============================================================
- # INTERACTIVE TERMINAL SELECTION MENU
- # =============================================================
- def prompt_for_terminal_selection(terminals):
- """
- Interactively ask which terminals to run. Used when the
- script is run with no CLI arguments at all.
- Returns a comma-separated selector string suitable for
- resolve_selected_terminals(), or None to mean "run all".
- """
- print()
- print("Which terminals do you want to run?")
- print()
- print(" [0] All terminals")
- for index, config in enumerate(terminals, start=1):
- print(
- f" [{index}] {config['key']} - {config['title']}"
- )
- print()
- print(
- "Enter comma-separated numbers or names (e.g. 1,3,6 "
- "or wpscan,nmap,findomain)."
- )
- print(
- "Press Enter or 0 to run all terminals."
- )
- print()
- while True:
- selection = input(
- "Terminals to run [0=all]: "
- ).strip()
- if not selection or selection == "0":
- return None
- try:
- resolve_selected_terminals(selection, terminals)
- except ValueError as error:
- print(f" {error} - try again.")
- continue
- return selection
- # =============================================================
- # INTERACTIVE MONITOR SELECTION MENU
- # =============================================================
- def prompt_for_monitor_selection(monitors):
- """
- Interactively ask which monitor to tile terminals on. Used
- when the script is run with no CLI arguments at all.
- Returns a --monitor-style selector string (a 0-based index
- into `monitors`, as a string) suitable for
- resolve_monitor_by_selector(), or None to mean
- "auto-detect".
- """
- if not monitors:
- return None
- print()
- print("Which monitor do you want to tile terminals on?")
- print()
- print(" [0] Auto-detect (recommended)")
- for index, monitor in enumerate(monitors):
- primary_tag = " (primary)" if monitor["primary"] else ""
- print(
- f" [{index + 1}] {monitor['name']}{primary_tag}: "
- f"{monitor['width']}x{monitor['height']}"
- f"+{monitor['x']}+{monitor['y']}"
- )
- print()
- while True:
- selection = input(
- "Monitor to use [0=auto-detect]: "
- ).strip()
- if not selection or selection == "0":
- return None
- try:
- menu_index = int(selection)
- except ValueError:
- print(
- " Enter a number from the list above - "
- "try again."
- )
- continue
- monitor_index = menu_index - 1
- if 0 <= monitor_index < len(monitors):
- return str(monitor_index)
- print(
- " Enter a number from the list above - try again."
- )
- # =============================================================
- # COMPUTE GRID CELL GEOMETRY
- # =============================================================
- def compute_cell_geometry(
- index,
- monitor_width,
- monitor_height,
- origin_x=0,
- origin_y=0,
- rows=GRID_ROWS,
- cols=GRID_COLS,
- gap=GRID_GAP
- ):
- """
- Compute the absolute pixel geometry (x, y, width, height)
- for cell `index` (0-based, filled left-to-right, top-to-
- bottom) in a rows x cols grid that evenly divides the given
- monitor.
- `origin_x`/`origin_y` are the monitor's top-left corner in
- the X server's combined virtual screen space (from xrandr),
- since xdotool's windowmove takes absolute coordinates, not
- coordinates relative to a single monitor.
- """
- usable_height = (
- monitor_height
- - SCREEN_MARGIN_TOP
- - SCREEN_MARGIN_BOTTOM
- )
- row = index // cols
- col = index % cols
- cell_width = monitor_width // cols
- cell_height = usable_height // rows
- x = origin_x + col * cell_width + gap // 2
- y = (
- origin_y
- + SCREEN_MARGIN_TOP
- + row * cell_height
- + gap // 2
- )
- width = cell_width - gap
- height = cell_height - gap
- return x, y, max(width, 1), max(height, 1)
- # =============================================================
- # POSITION WINDOW (MOVE + RESIZE VIA XDOTOOL)
- # =============================================================
- def position_window(
- pid,
- x,
- y,
- width,
- height,
- xdotool_available,
- attempts=POSITION_ATTEMPTS,
- delay=POSITION_DELAY
- ):
- """
- Find the xterm window belonging to `pid` and move/resize it
- to the given pixel geometry using xdotool.
- Retries repeatedly because the X window for a just-launched
- process may not exist/be mapped yet when Popen returns.
- """
- if not xdotool_available:
- return False
- window_id = None
- for _ in range(attempts):
- try:
- result = subprocess.run(
- ["xdotool", "search", "--pid", str(pid)],
- capture_output=True,
- text=True
- )
- ids = [
- window_id
- for window_id in result.stdout.split()
- if window_id
- ]
- if ids:
- window_id = ids[-1]
- break
- except Exception:
- pass
- time.sleep(delay)
- if window_id is None:
- return False
- try:
- subprocess.run(
- [
- "xdotool",
- "windowmove",
- window_id,
- str(x),
- str(y)
- ]
- )
- subprocess.run(
- [
- "xdotool",
- "windowsize",
- window_id,
- str(width),
- str(height)
- ]
- )
- except Exception:
- return False
- return True
- # =============================================================
- # BUILD COMMAND
- # =============================================================
- def build_command(command_template):
- """
- Replace {host} with the HOST environment variable.
- This allows the host to be supplied to each xterm through
- its environment.
- """
- return command_template.replace(
- "{host}",
- "$HOST"
- )
- # =============================================================
- # CREATE STANDARD TERMINAL SCRIPT
- # =============================================================
- def create_terminal_script(config):
- title = config["title"]
- command = build_command(
- config["command"]
- )
- script = f'''
- printf "\\033]0;{title} - $HOST\\007"
- printf "\\n"
- printf "\\033[1;36m==================================================\\033[0m\\n"
- printf "\\033[1;36m {title}\\033[0m\\n"
- printf "\\033[1;36m==================================================\\033[0m\\n"
- printf "\\n"
- printf "Host: %s\\n" "$HOST"
- printf "Command:\\n"
- printf " {config["command"]}\\n"
- printf "\\n"
- printf "\\033[1;36m--------------------------------------------------\\033[0m\\n"
- printf "\\n"
- {command}
- EXIT_CODE=$?
- printf "\\n"
- printf "\\033[1;36m==================================================\\033[0m\\n"
- printf "\\033[1;36m PROCESS COMPLETED\\033[0m\\n"
- printf "\\033[1;36m==================================================\\033[0m\\n"
- printf "\\n"
- printf "Terminal: %s\\n" "{title}"
- printf "Host: %s\\n" "$HOST"
- printf "Exit code: %s\\n" "$EXIT_CODE"
- printf "\\n"
- printf "The terminal will remain open.\\n"
- printf "Press CTRL+C to close this terminal.\\n"
- printf "\\n"
- trap 'exit 0' INT
- while true
- do
- sleep 1
- done
- '''
- return script
- # =============================================================
- # LAUNCH STANDARD XTERM
- # =============================================================
- def launch_terminal(config, host):
- title = config["title"]
- background = config["background"]
- foreground = config["foreground"]
- shell_script = create_terminal_script(
- config
- )
- environment = os.environ.copy()
- environment["HOST"] = host
- environment["WPSCAN_API_TOKEN"] = (
- WPSCAN_API_TOKEN
- )
- process = subprocess.Popen(
- [
- "xterm",
- "-title",
- title,
- "-geometry",
- WINDOW_GEOMETRY,
- "-bg",
- background,
- "-fg",
- foreground,
- "-fa",
- FONT,
- "-fs",
- FONT_SIZE,
- "-e",
- "bash",
- "-c",
- shell_script
- ],
- env=environment
- )
- return process
- # =============================================================
- # TERMINATE CHILD PROCESSES
- # =============================================================
- def terminate_processes(processes):
- for process in processes:
- if process.poll() is None:
- try:
- process.terminate()
- except ProcessLookupError:
- pass
- # =============================================================
- # MAIN
- # =============================================================
- def main():
- # =========================================================
- # Parse arguments (or fall back to an interactive prompt
- # if the script was run with no arguments at all)
- # =========================================================
- if len(sys.argv) == 1:
- print()
- print("======================================================")
- print(" Six XTerm Launcher - interactive setup")
- print("======================================================")
- print(
- "No arguments given - run with -h/--help to see "
- "available flags instead."
- )
- host_input = prompt_for_host()
- terminal_selector = prompt_for_terminal_selection(
- TERMINALS
- )
- monitor_selector = prompt_for_monitor_selection(
- get_monitors()
- )
- args = argparse.Namespace(
- host=host_input,
- monitor=monitor_selector,
- list_monitors=False,
- terminals=terminal_selector,
- list_terminals=False,
- )
- else:
- args = parse_arguments()
- # =========================================================
- # --list-monitors: show monitors and exit
- # =========================================================
- if args.list_monitors:
- print_monitor_list(get_monitors())
- sys.exit(0)
- # =========================================================
- # --list-terminals: show terminals and exit
- # =========================================================
- if args.list_terminals:
- print_terminal_list(TERMINALS)
- sys.exit(0)
- # =========================================================
- # Resolve which terminals to launch
- # =========================================================
- if args.terminals is not None:
- try:
- selected_terminals = resolve_selected_terminals(
- args.terminals,
- TERMINALS
- )
- except ValueError as error:
- print()
- print(f"ERROR: {error}")
- print_terminal_list(TERMINALS)
- sys.exit(1)
- else:
- selected_terminals = TERMINALS
- # =========================================================
- # Normalize host
- # =========================================================
- host = normalize_host(
- args.host
- )
- # =========================================================
- # Validate host
- # =========================================================
- if not host:
- print()
- print("ERROR: Host cannot be empty.")
- print()
- sys.exit(1)
- # =========================================================
- # Check / install ALL required tools before launching anything
- # =========================================================
- if not check_and_install_all_tools():
- sys.exit(1)
- # xdotool is guaranteed to be present after the tool check.
- xdotool_available = True
- # =========================================================
- # Determine which monitor to tile on
- # =========================================================
- #
- # Either a manual --monitor override, or auto-detection
- # done before launching any xterms, while this script's
- # own terminal should still be the active window.
- if args.monitor is not None:
- monitors = get_monitors()
- target_monitor = resolve_monitor_by_selector(
- args.monitor,
- monitors
- )
- if target_monitor is None:
- print()
- print(
- f"ERROR: --monitor '{args.monitor}' didn't "
- f"match any detected monitor."
- )
- print_monitor_list(monitors)
- sys.exit(1)
- else:
- target_monitor = determine_target_monitor(
- xdotool_available
- )
- cell_count = GRID_ROWS * GRID_COLS
- terminal_count = len(selected_terminals)
- # =========================================================
- # Display launcher information
- # =========================================================
- print()
- print("======================================================")
- print(" Six XTerm Launcher (equal-grid tiling)")
- print("======================================================")
- print()
- print(
- f"Original host: {args.host}"
- )
- print(
- f"Normalized host: {host}"
- )
- print()
- print(
- f"Target monitor: {target_monitor['name']} "
- f"{target_monitor['width']}x{target_monitor['height']}"
- f"+{target_monitor['x']}+{target_monitor['y']} "
- f"(detected via: {target_monitor['detected_via']})"
- )
- print(
- f"Grid: {GRID_ROWS} rows x {GRID_COLS} cols "
- f"({cell_count} cells available)"
- )
- print(
- f"Terminals selected ({terminal_count}/"
- f"{len(TERMINALS)}): "
- + ", ".join(
- config["key"] for config in selected_terminals
- )
- )
- print()
- print(
- f"Launching {terminal_count} independent "
- f"terminal{'s' if terminal_count != 1 else ''}..."
- )
- print()
- # =========================================================
- # Store child processes
- # =========================================================
- processes = []
- # =========================================================
- # Launch the selected terminals
- # =========================================================
- for number, config in enumerate(
- selected_terminals,
- start=1
- ):
- print(
- f"[{number}/{terminal_count}] "
- f"Launching {config['title']}"
- )
- print(
- f" Command: "
- f"{config['command']}"
- )
- print(
- f" Background: "
- f"{config['background']}"
- )
- print(
- f" Foreground: "
- f"{config['foreground']}"
- )
- try:
- process = launch_terminal(
- config,
- host
- )
- processes.append(
- process
- )
- print(
- f" PID: "
- f"{process.pid}"
- )
- x, y, width, height = compute_cell_geometry(
- number - 1,
- target_monitor["width"],
- target_monitor["height"],
- origin_x=target_monitor["x"],
- origin_y=target_monitor["y"]
- )
- print(
- f" Grid cell: "
- f"{width}x{height}+{x}+{y}"
- )
- placed = position_window(
- process.pid,
- x,
- y,
- width,
- height,
- xdotool_available
- )
- if xdotool_available and not placed:
- print(
- " WARNING: could not locate/position "
- "this window."
- )
- except Exception as error:
- print(
- f" ERROR: {error}"
- )
- print()
- if (
- LAUNCH_DELAY > 0
- and number < terminal_count
- ):
- time.sleep(
- LAUNCH_DELAY
- )
- # =========================================================
- # Finished launching
- # =========================================================
- print()
- print("======================================================")
- print(
- f" All {terminal_count} terminal"
- f"{'s' if terminal_count != 1 else ''} have been "
- f"launched and tiled."
- )
- print("======================================================")
- print()
- print(
- f"Target: {host}"
- )
- print()
- print(
- "Each terminal runs independently."
- )
- print()
- print(
- "Each terminal remains open after its command"
- )
- print(
- "finishes until CTRL+C is pressed inside that"
- )
- print(
- "individual terminal."
- )
- print()
- # =========================================================
- # Keep launcher alive
- # =========================================================
- try:
- while True:
- processes = [
- process
- for process in processes
- if process.poll() is None
- ]
- time.sleep(1)
- except KeyboardInterrupt:
- print()
- print(
- "Launcher interrupted."
- )
- print(
- "Closing remaining xterm processes..."
- )
- terminate_processes(
- processes
- )
- print(
- "Done."
- )
- sys.exit(0)
- # =============================================================
- # PROGRAM ENTRY POINT
- # =============================================================
- if __name__ == "__main__":
- main()
Advertisement
Add Comment
Please, Sign In to add comment