eva-exploit 2.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- config.py +29 -0
- eva.py +166 -0
- eva_exploit-2.5.dist-info/METADATA +410 -0
- eva_exploit-2.5.dist-info/RECORD +17 -0
- eva_exploit-2.5.dist-info/WHEEL +5 -0
- eva_exploit-2.5.dist-info/entry_points.txt +2 -0
- eva_exploit-2.5.dist-info/top_level.txt +5 -0
- modules/__init__.py +0 -0
- modules/attack_map.py +467 -0
- modules/llm.py +599 -0
- modules/prompt_builder.py +56 -0
- modules/reporting.py +254 -0
- sessions/__init__.py +0 -0
- sessions/eva_session.py +457 -0
- utils/__init__.py +0 -0
- utils/system.py +264 -0
- utils/ui.py +191 -0
utils/system.py
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import socket
|
|
4
|
+
import signal
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
from importlib import metadata
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from urllib import error, request
|
|
11
|
+
import tomllib
|
|
12
|
+
|
|
13
|
+
from colorama import Fore
|
|
14
|
+
|
|
15
|
+
from config import API_ENDPOINT, APP_NAME, APP_VERSION, ENV_PATH, OLLAMA_MODEL, PYPI_PACKAGE
|
|
16
|
+
from utils.ui import clear, cyber
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ═══════════════════════════════════════════════════════════════
|
|
20
|
+
# :: Utilities
|
|
21
|
+
# Utility functions such as ApiKEY verifier and signal handler
|
|
22
|
+
# ═══════════════════════════════════════════════════════════════
|
|
23
|
+
def checkAPI():
|
|
24
|
+
if API_ENDPOINT == "NOT_SET":
|
|
25
|
+
print(Fore.RED + "\nNo custom API set. Please configure in source code at API_ENPOINT")
|
|
26
|
+
sys.exit(0)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def checkOpenAIKey():
|
|
30
|
+
key = os.getenv("OPENAI_API_KEY")
|
|
31
|
+
if key and key.strip():
|
|
32
|
+
return key.strip()
|
|
33
|
+
if ENV_PATH.exists():
|
|
34
|
+
for line in ENV_PATH.read_text().splitlines():
|
|
35
|
+
if line.startswith("OPENAI_API_KEY="):
|
|
36
|
+
key = line.split("=", 1)[1].strip()
|
|
37
|
+
if key:
|
|
38
|
+
os.environ["OPENAI_API_KEY"] = key
|
|
39
|
+
return key
|
|
40
|
+
os.system("clear")
|
|
41
|
+
cyber("OpenAI key not found! :: Please insert it below", color=Fore.RED)
|
|
42
|
+
print("\nYour OpenAI API key will be stored locally in .env\n")
|
|
43
|
+
key = input("#key > ").strip()
|
|
44
|
+
if not key:
|
|
45
|
+
print(Fore.RED + "\nNo key provided. Aborting.")
|
|
46
|
+
sys.exit(1)
|
|
47
|
+
with open(ENV_PATH, "a") as f:
|
|
48
|
+
f.write(f"\nOPENAI_API_KEY={key}\n")
|
|
49
|
+
os.environ["OPENAI_API_KEY"] = key
|
|
50
|
+
print(Fore.GREEN + "\n✔ OpenAI API key saved successfully.")
|
|
51
|
+
time.sleep(1)
|
|
52
|
+
return key
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def checkAnthropicKey():
|
|
56
|
+
key = os.getenv("ANTHROPIC_API_KEY")
|
|
57
|
+
if key and key.strip():
|
|
58
|
+
return key.strip()
|
|
59
|
+
if ENV_PATH.exists():
|
|
60
|
+
for line in ENV_PATH.read_text().splitlines():
|
|
61
|
+
if line.startswith("ANTHROPIC_API_KEY="):
|
|
62
|
+
key = line.split("=", 1)[1].strip()
|
|
63
|
+
if key:
|
|
64
|
+
os.environ["ANTHROPIC_API_KEY"] = key
|
|
65
|
+
return key
|
|
66
|
+
os.system("clear")
|
|
67
|
+
cyber("Anthropic key not found! :: Please insert it below", color=Fore.RED)
|
|
68
|
+
print("\nYour Anthropic API key will be stored locally in .env\n")
|
|
69
|
+
key = input("#key > ").strip()
|
|
70
|
+
if not key:
|
|
71
|
+
print(Fore.RED + "\nNo key provided. Aborting.")
|
|
72
|
+
sys.exit(1)
|
|
73
|
+
with open(ENV_PATH, "a") as f:
|
|
74
|
+
f.write(f"\nANTHROPIC_API_KEY={key}\n")
|
|
75
|
+
os.environ["ANTHROPIC_API_KEY"] = key
|
|
76
|
+
print(Fore.GREEN + "\n✔ Anthropic API key saved successfully.")
|
|
77
|
+
time.sleep(1)
|
|
78
|
+
return key
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def checkGeminiKey():
|
|
82
|
+
key = os.getenv("GEMINI_API_KEY")
|
|
83
|
+
if key and key.strip():
|
|
84
|
+
return key.strip()
|
|
85
|
+
if ENV_PATH.exists():
|
|
86
|
+
for line in ENV_PATH.read_text().splitlines():
|
|
87
|
+
if line.startswith("GEMINI_API_KEY="):
|
|
88
|
+
key = line.split("=", 1)[1].strip()
|
|
89
|
+
if key:
|
|
90
|
+
os.environ["GEMINI_API_KEY"] = key
|
|
91
|
+
return key
|
|
92
|
+
os.system("clear")
|
|
93
|
+
cyber("Gemini key not found! :: Please insert it below", color=Fore.RED)
|
|
94
|
+
print("\nYour Gemini API key will be stored locally in .env\n")
|
|
95
|
+
key = input("#key > ").strip()
|
|
96
|
+
if not key:
|
|
97
|
+
print(Fore.RED + "\nNo key provided. Aborting.")
|
|
98
|
+
sys.exit(1)
|
|
99
|
+
with open(ENV_PATH, "a") as f:
|
|
100
|
+
f.write(f"\nGEMINI_API_KEY={key}\n")
|
|
101
|
+
os.environ["GEMINI_API_KEY"] = key
|
|
102
|
+
print(Fore.GREEN + "\n✔ Gemini API key saved successfully.")
|
|
103
|
+
time.sleep(1)
|
|
104
|
+
return key
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def ctrl_c_handler(signum, frame):
|
|
108
|
+
raise KeyboardInterrupt
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def register_signal_handler():
|
|
112
|
+
signal.signal(signal.SIGINT, ctrl_c_handler)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def graceful_exit():
|
|
116
|
+
cyber("EVA OFFLINE :: SESSION IS SAVED", color=Fore.RED)
|
|
117
|
+
print(Fore.YELLOW + "🜂 E x i t i n g E V A ...")
|
|
118
|
+
time.sleep(2.5)
|
|
119
|
+
clear()
|
|
120
|
+
sys.exit(0)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _split_version(value):
|
|
124
|
+
clean = str(value).strip().lower().lstrip("v")
|
|
125
|
+
parts = []
|
|
126
|
+
for part in clean.split("."):
|
|
127
|
+
digits = ""
|
|
128
|
+
for char in part:
|
|
129
|
+
if char.isdigit():
|
|
130
|
+
digits += char
|
|
131
|
+
else:
|
|
132
|
+
break
|
|
133
|
+
parts.append(int(digits or 0))
|
|
134
|
+
while len(parts) < 3:
|
|
135
|
+
parts.append(0)
|
|
136
|
+
return tuple(parts[:3])
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _is_newer(latest, current):
|
|
140
|
+
return _split_version(latest) > _split_version(current)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def get_current_version():
|
|
144
|
+
try:
|
|
145
|
+
return metadata.version(PYPI_PACKAGE)
|
|
146
|
+
except metadata.PackageNotFoundError:
|
|
147
|
+
pyproject = Path("pyproject.toml")
|
|
148
|
+
if pyproject.exists():
|
|
149
|
+
try:
|
|
150
|
+
data = tomllib.loads(pyproject.read_text(encoding="utf-8"))
|
|
151
|
+
version = data.get("project", {}).get("version")
|
|
152
|
+
if version:
|
|
153
|
+
return version
|
|
154
|
+
except (tomllib.TOMLDecodeError, OSError):
|
|
155
|
+
pass
|
|
156
|
+
return APP_VERSION
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def fetch_latest_pypi_version():
|
|
160
|
+
url = f"https://pypi.org/pypi/{PYPI_PACKAGE}/json"
|
|
161
|
+
req = request.Request(url, headers={"Accept": "application/json", "User-Agent": "eva-update-check"})
|
|
162
|
+
try:
|
|
163
|
+
with request.urlopen(req, timeout=5) as response:
|
|
164
|
+
data = json.loads(response.read().decode("utf-8"))
|
|
165
|
+
return data.get("info", {}).get("version")
|
|
166
|
+
except (error.URLError, TimeoutError, json.JSONDecodeError):
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def checkupdts():
|
|
171
|
+
try:
|
|
172
|
+
socket.create_connection(("pypi.org", 443), timeout=2)
|
|
173
|
+
except OSError:
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
current = get_current_version()
|
|
177
|
+
latest = fetch_latest_pypi_version()
|
|
178
|
+
if not latest:
|
|
179
|
+
return
|
|
180
|
+
if _is_newer(latest, current):
|
|
181
|
+
print("\n" + Fore.CYAN + "=" * 40)
|
|
182
|
+
print(Fore.CYAN + f"🐱 Update available: {current} → {latest}")
|
|
183
|
+
print(Fore.GREEN + "Run: eva -u to update to the latest version")
|
|
184
|
+
print("=" * 40 + Fore.CYAN + "\n")
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def run_self_update():
|
|
188
|
+
print(Fore.CYAN + f"\nChecking updates for {APP_NAME}...\n")
|
|
189
|
+
updated = False
|
|
190
|
+
|
|
191
|
+
pip_result = subprocess.run(
|
|
192
|
+
[sys.executable, "-m", "pip", "install", "--upgrade", PYPI_PACKAGE],
|
|
193
|
+
text=True
|
|
194
|
+
)
|
|
195
|
+
if pip_result.returncode == 0:
|
|
196
|
+
updated = True
|
|
197
|
+
|
|
198
|
+
if Path(".git").exists() and command_exists("git"):
|
|
199
|
+
branch = "main"
|
|
200
|
+
branch_detect = subprocess.run(
|
|
201
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
202
|
+
capture_output=True,
|
|
203
|
+
text=True
|
|
204
|
+
)
|
|
205
|
+
if branch_detect.returncode == 0 and branch_detect.stdout.strip():
|
|
206
|
+
branch = branch_detect.stdout.strip()
|
|
207
|
+
|
|
208
|
+
pull_result = subprocess.run(["git", "pull", "--tags", "origin", branch], text=True)
|
|
209
|
+
if pull_result.returncode == 0:
|
|
210
|
+
updated = True
|
|
211
|
+
|
|
212
|
+
if updated:
|
|
213
|
+
print(Fore.GREEN + "\n✔ Update process finished. Restart EVA to use the latest version.")
|
|
214
|
+
return 0
|
|
215
|
+
|
|
216
|
+
print(Fore.RED + "\n[!] Could not auto-update EVA in this environment.")
|
|
217
|
+
print(Fore.YELLOW + f"Try manually: {sys.executable} -m pip install --upgrade {PYPI_PACKAGE}")
|
|
218
|
+
return 1
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
# ================= STARTUP OF EVA here =================
|
|
222
|
+
def command_exists(cmd):
|
|
223
|
+
return subprocess.call(
|
|
224
|
+
["which", cmd],
|
|
225
|
+
stdout=subprocess.DEVNULL,
|
|
226
|
+
stderr=subprocess.DEVNULL
|
|
227
|
+
) == 0
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def ollama_running():
|
|
231
|
+
try:
|
|
232
|
+
subprocess.check_output(['ollama', 'list'], stderr=subprocess.STDOUT, text=True)
|
|
233
|
+
return True
|
|
234
|
+
except subprocess.CalledProcessError as e:
|
|
235
|
+
if "server not responding" in e.output.lower():
|
|
236
|
+
return False
|
|
237
|
+
return False
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def start_ollama():
|
|
241
|
+
clear()
|
|
242
|
+
print("\n\n\n")
|
|
243
|
+
print(Fore.YELLOW + "🜂 OLLAMA NOT RUNNING :: Starting for you...\n\n")
|
|
244
|
+
|
|
245
|
+
with open(os.devnull, 'w') as DEVNULL:
|
|
246
|
+
subprocess.Popen(
|
|
247
|
+
['ollama', 'serve'],
|
|
248
|
+
stdout=DEVNULL,
|
|
249
|
+
stderr=DEVNULL,
|
|
250
|
+
stdin=DEVNULL,
|
|
251
|
+
close_fds=True,
|
|
252
|
+
start_new_session=True
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
time.sleep(3)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def model_exists():
|
|
259
|
+
r = subprocess.run(
|
|
260
|
+
["ollama", "list"],
|
|
261
|
+
capture_output=True,
|
|
262
|
+
text=True
|
|
263
|
+
)
|
|
264
|
+
return OLLAMA_MODEL in r.stdout
|
utils/ui.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import sys
|
|
4
|
+
import termios
|
|
5
|
+
import tty
|
|
6
|
+
import itertools
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
from colorama import Fore, Style, init
|
|
10
|
+
|
|
11
|
+
init(autoreset=True)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def clear():
|
|
15
|
+
os.system("clear")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ════════════════════
|
|
19
|
+
# :: UI FUNCTIONS
|
|
20
|
+
# ════════════════════
|
|
21
|
+
|
|
22
|
+
_spinner_frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
23
|
+
_spinner_text = " L o a d i n g "
|
|
24
|
+
_spinner_delay = 0.10
|
|
25
|
+
_spinner_running = False
|
|
26
|
+
_spinner_thread = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _spinner_animate():
|
|
30
|
+
for i, frame in enumerate(itertools.cycle(_spinner_frames)):
|
|
31
|
+
if not _spinner_running:
|
|
32
|
+
break
|
|
33
|
+
dot_count = (i // 3) % 4 + 1
|
|
34
|
+
dots = "." * dot_count
|
|
35
|
+
spaces = " " * (4 - dot_count)
|
|
36
|
+
sys.stdout.write(f"\r {frame} {_spinner_text}{dots}{spaces}")
|
|
37
|
+
sys.stdout.flush()
|
|
38
|
+
time.sleep(_spinner_delay)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def spinner_start():
|
|
42
|
+
global _spinner_running, _spinner_thread
|
|
43
|
+
if _spinner_running:
|
|
44
|
+
return
|
|
45
|
+
_spinner_running = True
|
|
46
|
+
sys.stdout.write(f"\n\n")
|
|
47
|
+
_spinner_thread = threading.Thread(target=_spinner_animate)
|
|
48
|
+
_spinner_thread.daemon = True
|
|
49
|
+
_spinner_thread.start()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def spinner_stop():
|
|
53
|
+
global _spinner_running
|
|
54
|
+
_spinner_running = False
|
|
55
|
+
if _spinner_thread:
|
|
56
|
+
_spinner_thread.join()
|
|
57
|
+
sys.stdout.write("\r" + " " * (len(_spinner_text) + 4) + "\r")
|
|
58
|
+
sys.stdout.flush()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def cyber(msg="", width=50, color=Fore.GREEN):
|
|
62
|
+
"""
|
|
63
|
+
Stylized output in a centered scifi box.
|
|
64
|
+
:param msg: message
|
|
65
|
+
:param width: width
|
|
66
|
+
:param color: Ccolor
|
|
67
|
+
"""
|
|
68
|
+
width = int(width)
|
|
69
|
+
ansi_re = re.compile(r"\x1b\[[0-9;]*m")
|
|
70
|
+
|
|
71
|
+
def visible_len(text):
|
|
72
|
+
return len(ansi_re.sub("", text))
|
|
73
|
+
|
|
74
|
+
lines = str(msg).splitlines() if msg else []
|
|
75
|
+
longest = max((visible_len(line) for line in lines), default=0)
|
|
76
|
+
width = max(width, longest + 4)
|
|
77
|
+
|
|
78
|
+
print(color + "╔" + "═" * width + "╗")
|
|
79
|
+
if lines:
|
|
80
|
+
for line in lines:
|
|
81
|
+
line_len = visible_len(line)
|
|
82
|
+
content_len = line_len + 2
|
|
83
|
+
padding = max(0, width - content_len)
|
|
84
|
+
left_pad = padding // 2
|
|
85
|
+
right_pad = padding - left_pad
|
|
86
|
+
print(color + "║" + " " * left_pad + " " + line + " " + " " * right_pad + "║")
|
|
87
|
+
print(color + "╚" + "═" * width + "╝")
|
|
88
|
+
print(Style.RESET_ALL)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def banner(version=None):
|
|
93
|
+
clear()
|
|
94
|
+
print(Style.BRIGHT + Fore.CYAN + rf"""
|
|
95
|
+
╔═════════════════════════════════════════════╗
|
|
96
|
+
║ ║
|
|
97
|
+
║ ░▒▓████████▓▒░▒▓█▓▒░░▒▓█▓▒░░▒▓██████▓▒░ ║
|
|
98
|
+
║ ░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ║
|
|
99
|
+
║ ░▒▓█▓▒░ ░▒▓█▓▒▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░ ║
|
|
100
|
+
║ ░▒▓██████▓▒░ ░▒▓█▓▒▒▓█▓▒░░▒▓████████▓▒░ ║
|
|
101
|
+
║ ░▒▓█▓▒░ ░▒▓█▓▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ║
|
|
102
|
+
║ ░▒▓█▓▒░ ░▒▓█▓▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ║
|
|
103
|
+
║ ░▒▓████████▓▒░ ░▒▓██▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ║
|
|
104
|
+
║ Exploit Vector Agent ║
|
|
105
|
+
║ ║
|
|
106
|
+
║ ᴍᴀᴅᴇ ʙʏ: 𝝺𝗿𝗰𝗮𝗻𝗴𝗲𝗹𝗼 ║
|
|
107
|
+
╚═════════════════════════════════════════════╝
|
|
108
|
+
{Style.BRIGHT} {Fore.MAGENTA} version: {version} {Style.RESET_ALL} """)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ╔═════════░░░═════════╗
|
|
112
|
+
# ░░░ Query input ░░░
|
|
113
|
+
# ╚═════════░░░═════════╝
|
|
114
|
+
def raw_input(prompt=""):
|
|
115
|
+
fd = sys.stdin.fileno()
|
|
116
|
+
old = termios.tcgetattr(fd)
|
|
117
|
+
buf = ""
|
|
118
|
+
try:
|
|
119
|
+
tty.setcbreak(fd)
|
|
120
|
+
print(prompt, end="", flush=True)
|
|
121
|
+
while True:
|
|
122
|
+
ch = sys.stdin.read(1)
|
|
123
|
+
if ch in ("\n", "\r"):
|
|
124
|
+
print()
|
|
125
|
+
return buf
|
|
126
|
+
elif ch in ("\x7f", "\x08"):
|
|
127
|
+
if buf:
|
|
128
|
+
buf = buf[:-1]
|
|
129
|
+
print("\b \b", end="", flush=True)
|
|
130
|
+
elif ch == "\x03":
|
|
131
|
+
raise KeyboardInterrupt
|
|
132
|
+
else:
|
|
133
|
+
buf += ch
|
|
134
|
+
print(ch, end="", flush=True)
|
|
135
|
+
finally:
|
|
136
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# ╔═════════░░░═════════╗
|
|
140
|
+
# ░░░ Control utilies ░░░
|
|
141
|
+
# ╚═════════░░░═════════╝
|
|
142
|
+
|
|
143
|
+
def menu(title, options):
|
|
144
|
+
idx = 0
|
|
145
|
+
fd = sys.stdin.fileno()
|
|
146
|
+
old = termios.tcgetattr(fd)
|
|
147
|
+
try:
|
|
148
|
+
tty.setcbreak(fd)
|
|
149
|
+
while True:
|
|
150
|
+
os.system("clear")
|
|
151
|
+
cyber(title)
|
|
152
|
+
for i, opt in enumerate(options):
|
|
153
|
+
prefix = "→ " if i == idx else " "
|
|
154
|
+
print(prefix + opt)
|
|
155
|
+
ch = sys.stdin.read(1)
|
|
156
|
+
if ch == "\x1b":
|
|
157
|
+
sys.stdin.read(1)
|
|
158
|
+
arrow = sys.stdin.read(1)
|
|
159
|
+
if arrow == "A":
|
|
160
|
+
idx = (idx - 1) % len(options)
|
|
161
|
+
elif arrow == "B":
|
|
162
|
+
idx = (idx + 1) % len(options)
|
|
163
|
+
elif ch in ("\n", "\r"):
|
|
164
|
+
return idx
|
|
165
|
+
finally:
|
|
166
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
|
167
|
+
def get_sessions(title, options, version=None):
|
|
168
|
+
idx = 0
|
|
169
|
+
fd = sys.stdin.fileno()
|
|
170
|
+
old = termios.tcgetattr(fd)
|
|
171
|
+
try:
|
|
172
|
+
tty.setcbreak(fd)
|
|
173
|
+
while True:
|
|
174
|
+
banner(version)
|
|
175
|
+
print("\n")
|
|
176
|
+
cyber(title)
|
|
177
|
+
for i, opt in enumerate(options):
|
|
178
|
+
prefix = "→ " if i == idx else " "
|
|
179
|
+
print(prefix + opt)
|
|
180
|
+
ch = sys.stdin.read(1)
|
|
181
|
+
if ch == "\x1b":
|
|
182
|
+
sys.stdin.read(1)
|
|
183
|
+
arrow = sys.stdin.read(1)
|
|
184
|
+
if arrow == "A":
|
|
185
|
+
idx = (idx - 1) % len(options)
|
|
186
|
+
elif arrow == "B":
|
|
187
|
+
idx = (idx + 1) % len(options)
|
|
188
|
+
elif ch in ("\n", "\r"):
|
|
189
|
+
return idx
|
|
190
|
+
finally:
|
|
191
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|