mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-06-27 20:11:02 +00:00
Improving --gui and --tui
This commit is contained in:
parent
cef105f322
commit
0430f780ab
5 changed files with 1219 additions and 496 deletions
|
|
@ -189,7 +189,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch
|
|||
9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py
|
||||
0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py
|
||||
888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py
|
||||
c7a6dd94cf738716cc48f1daacdd402ddb0e78a6c9260233e319cde4f9054a60 lib/core/settings.py
|
||||
a6e15ece62113241870feacc9cda691c64be9b849ce2df169b35ee695a517165 lib/core/settings.py
|
||||
c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py
|
||||
a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py
|
||||
19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py
|
||||
|
|
@ -200,7 +200,7 @@ b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unesc
|
|||
2400e465fa4d13e4c32795910878c71ff212e4361b46428d57ce43983f5e997c lib/core/wordlist.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/__init__.py
|
||||
54bfd31ebded3ffa5848df1c644f196eb704116517c7a3d860b5d081e984d821 lib/parse/banner.py
|
||||
386065c4c40e07a10875d0b73b4ca2fb682c598e8d52b41d0b6b08d5c2c7b3c1 lib/parse/cmdline.py
|
||||
6060d2d11fab39796b87ace30a872302f365dea3b14d24670915fdb9edc86011 lib/parse/cmdline.py
|
||||
02d82e4069bd98c52755417f8b8e306d79945672656ac24f1a45e7a6eff4b158 lib/parse/configfile.py
|
||||
c5b258be7485089fac9d9cd179960e774fbd85e62836dc67cce76cc028bb6aeb lib/parse/handler.py
|
||||
5c9a9caee948843d5537745640cc7b98d70a0412cc0949f59d4ebe8b2907c06c lib/parse/headers.py
|
||||
|
|
@ -249,7 +249,7 @@ da5bcbcda3f667582adf5db8c1b5d511b469ac61b55d387cec66de35720ed718 lib/utils/craw
|
|||
a94958be0ec3e9d28d8171813a6a90655a9ad7e6aa33c661e8d8ebbfcf208dbb lib/utils/deps.py
|
||||
b0d8ae8513c1f5ffcaa4bf0398790f26bc2180a6acf07bf5b2c86555bf9113f6 lib/utils/dialect.py
|
||||
51cfab194cd5b6b24d62706fb79db86c852b9e593f4c55c15b35f175e70c9d75 lib/utils/getch.py
|
||||
853c3595e1d2efc54b8bfb6ab12c55d1efc1603be266978e3a7d96d553d91a52 lib/utils/gui.py
|
||||
417029b70afe672f3121746a7909887aa996766c92547b4566c50343fff76131 lib/utils/gui.py
|
||||
972c5db9c9e30ac0f91c0f8d4df4531d0304e151dac99f1399c37c952ba9f935 lib/utils/har.py
|
||||
0cd3860c03e39bacd1d0fe4cf1a0c605de48ff82f70441319f21d47e38e7e3a9 lib/utils/hashdb.py
|
||||
71a66ff766a2921106770b26acff380de469222dc893816a7b970b384c927666 lib/utils/hash.py
|
||||
|
|
@ -264,7 +264,7 @@ de4be7e291db0962cd59f9c04b3f7259f846e315df1fd9b323954f89fae0b2db lib/utils/sear
|
|||
8258d0f54ad94e6101934971af4e55d5540f217c40ddcc594e2fba837b856d35 lib/utils/sgmllib.py
|
||||
2760c4b82382e501f16bb98edec9531f46e5b286fbf004b346545b9b62f84824 lib/utils/sqlalchemy.py
|
||||
f0e5525a92fe971defc8f74c27942ff9138b1e8251f2e0d9a8bd59285b656084 lib/utils/timeout.py
|
||||
f821dc39a75ea48dccfa758788de15d38b9ca6a780a98f59935fb6610f75508c lib/utils/tui.py
|
||||
f19a6761284e689fca7d2e07120193f7b9c4f9c506ecaf87e82c2e97cca4db63 lib/utils/tui.py
|
||||
e430db49aa768ff2cdba76932e30871c366054599c44d91580dde459ab9b6fef lib/utils/versioncheck.py
|
||||
b3c5109394f6c3cdd73a524a737b36cca7ecc56619f2a5f801eb1e7f1bfdb78b lib/utils/wafbypass.py
|
||||
1b439fc59fd202c21c74978ed9f36d1c309533226c77907eae159461525f9fef lib/utils/xrange.py
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ from lib.core.enums import OS
|
|||
from thirdparty import six
|
||||
|
||||
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
|
||||
VERSION = "1.10.6.157"
|
||||
VERSION = "1.10.6.158"
|
||||
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
|
||||
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34}
|
||||
VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE)
|
||||
|
|
|
|||
|
|
@ -443,7 +443,7 @@ def cmdLineParser(argv=None):
|
|||
help="Load second-order HTTP request from file")
|
||||
|
||||
# Fingerprint options
|
||||
fingerprint = parser.add_argument_group("Fingerprint")
|
||||
fingerprint = parser.add_argument_group("Fingerprint", "These options can be used to perform a back-end database management system version fingerprint")
|
||||
|
||||
fingerprint.add_argument("-f", "--fingerprint", dest="extensiveFp", action="store_true",
|
||||
help="Perform an extensive DBMS version fingerprint")
|
||||
|
|
@ -789,7 +789,7 @@ def cmdLineParser(argv=None):
|
|||
help="Disable hash analysis on table dumps")
|
||||
|
||||
miscellaneous.add_argument("--gui", dest="gui", action="store_true",
|
||||
help="Experimental Tkinter GUI")
|
||||
help="Graphical user interface (Tkinter)")
|
||||
|
||||
miscellaneous.add_argument("--list-tampers", dest="listTampers", action="store_true",
|
||||
help="Display list of available tamper scripts")
|
||||
|
|
@ -816,7 +816,7 @@ def cmdLineParser(argv=None):
|
|||
help="Local directory for storing temporary files")
|
||||
|
||||
miscellaneous.add_argument("--tui", dest="tui", action="store_true",
|
||||
help="Experimental ncurses TUI")
|
||||
help="Textual user interface (ncurses)")
|
||||
|
||||
miscellaneous.add_argument("--unstable", dest="unstable", action="store_true",
|
||||
help="Adjust options for unstable connections")
|
||||
|
|
|
|||
1368
lib/utils/gui.py
1368
lib/utils/gui.py
File diff suppressed because it is too large
Load diff
331
lib/utils/tui.py
331
lib/utils/tui.py
|
|
@ -25,6 +25,75 @@ from lib.core.exception import SqlmapSystemException
|
|||
from lib.core.settings import IS_WIN
|
||||
from thirdparty.six.moves import configparser as _configparser
|
||||
|
||||
# Options surfaced on the curated "Quick start" tab (by destination), in display order
|
||||
QUICK_START_DESTS = (
|
||||
"url", "data", "cookie", "dbms", "level", "risk", "technique",
|
||||
"getCurrentUser", "getCurrentDb", "getBanner", "isDba",
|
||||
"getDbs", "getTables", "getColumns", "getPasswordHashes", "dumpTable",
|
||||
"batch", "threads", "proxy", "tor",
|
||||
)
|
||||
|
||||
# Short tab labels so the (sometimes verbose) option-group titles fit the top bar
|
||||
TAB_ALIASES = {
|
||||
"Optimization": "Optimize",
|
||||
"Enumeration": "Enumerate",
|
||||
"Brute force": "Brute",
|
||||
"User-defined function injection": "UDF",
|
||||
"File system access": "Files",
|
||||
"Operating system access": "OS",
|
||||
"Windows registry access": "Registry",
|
||||
"Miscellaneous": "Misc",
|
||||
}
|
||||
|
||||
# --- parser-backend compatibility (works for both optparse and argparse objects) ---
|
||||
|
||||
def _parserGroups(parser):
|
||||
groups = getattr(parser, "option_groups", None)
|
||||
if groups is None:
|
||||
groups = [_ for _ in getattr(parser, "_action_groups", []) if getattr(_, "title", None) not in (None, "positional arguments", "optional arguments", "options")]
|
||||
return groups or []
|
||||
|
||||
def _groupOptions(group):
|
||||
for attr in ("option_list", "_group_actions"):
|
||||
if hasattr(group, attr):
|
||||
return getattr(group, attr)
|
||||
return []
|
||||
|
||||
def _groupTitle(group):
|
||||
return getattr(group, "title", "") or ""
|
||||
|
||||
def _groupDescription(group):
|
||||
if hasattr(group, "get_description"):
|
||||
return group.get_description() or ""
|
||||
return getattr(group, "description", "") or ""
|
||||
|
||||
def _optStrings(option):
|
||||
if hasattr(option, "option_strings"):
|
||||
return list(option.option_strings)
|
||||
return list(getattr(option, "_short_opts", None) or []) + list(getattr(option, "_long_opts", None) or [])
|
||||
|
||||
def _optDest(option):
|
||||
return getattr(option, "dest", None)
|
||||
|
||||
def _optHelp(option):
|
||||
return getattr(option, "help", "") or ""
|
||||
|
||||
def _optTakesValue(option):
|
||||
if hasattr(option, "takes_value"):
|
||||
try:
|
||||
return option.takes_value()
|
||||
except Exception:
|
||||
pass
|
||||
return getattr(option, "nargs", 1) != 0
|
||||
|
||||
def _optValueType(option):
|
||||
kind = getattr(option, "type", None)
|
||||
if kind in ("int", int):
|
||||
return "int"
|
||||
if kind in ("float", float):
|
||||
return "float"
|
||||
return "string"
|
||||
|
||||
class NcursesUI:
|
||||
def __init__(self, stdscr, parser):
|
||||
self.stdscr = stdscr
|
||||
|
|
@ -38,61 +107,110 @@ class NcursesUI:
|
|||
self.process = None
|
||||
|
||||
# Initialize colors
|
||||
curses.start_color()
|
||||
curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_CYAN) # Header
|
||||
curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_BLUE) # Active tab
|
||||
curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_WHITE) # Inactive tab
|
||||
curses.init_pair(4, curses.COLOR_YELLOW, curses.COLOR_BLACK) # Selected field
|
||||
curses.init_pair(5, curses.COLOR_GREEN, curses.COLOR_BLACK) # Help text
|
||||
curses.init_pair(6, curses.COLOR_RED, curses.COLOR_BLACK) # Error/Important
|
||||
curses.init_pair(7, curses.COLOR_CYAN, curses.COLOR_BLACK) # Label
|
||||
self._init_colors()
|
||||
|
||||
# Setup curses
|
||||
curses.curs_set(1)
|
||||
curses.curs_set(0)
|
||||
self.stdscr.keypad(1)
|
||||
|
||||
# Parse option groups
|
||||
self._parse_options()
|
||||
|
||||
def _init_colors(self):
|
||||
"""Cohesive palette: a flat 256-color scheme with a graceful 8-color fallback"""
|
||||
curses.start_color()
|
||||
try:
|
||||
curses.use_default_colors()
|
||||
default_bg = -1
|
||||
except curses.error:
|
||||
default_bg = curses.COLOR_BLACK
|
||||
|
||||
if curses.COLORS >= 256:
|
||||
accent, accent_fg, sel_bg = 75, 234, 237
|
||||
text, muted, green, red = 252, 245, 114, 210
|
||||
curses.init_pair(1, accent_fg, accent) # header / footer bar
|
||||
curses.init_pair(2, accent_fg, accent) # active tab
|
||||
curses.init_pair(3, muted, 236) # inactive tab
|
||||
curses.init_pair(4, accent, sel_bg) # selected field row
|
||||
curses.init_pair(5, muted, default_bg) # help / description
|
||||
curses.init_pair(6, red, default_bg) # error / important
|
||||
curses.init_pair(7, text, default_bg) # label / value
|
||||
curses.init_pair(8, green, default_bg) # value that has been set
|
||||
curses.init_pair(9, muted, sel_bg) # help text on the highlighted row
|
||||
else:
|
||||
curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_CYAN)
|
||||
curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_CYAN)
|
||||
curses.init_pair(3, curses.COLOR_WHITE, curses.COLOR_BLUE)
|
||||
curses.init_pair(4, curses.COLOR_BLACK, curses.COLOR_CYAN)
|
||||
curses.init_pair(5, curses.COLOR_GREEN, default_bg)
|
||||
curses.init_pair(6, curses.COLOR_RED, default_bg)
|
||||
curses.init_pair(7, curses.COLOR_WHITE, default_bg)
|
||||
curses.init_pair(8, curses.COLOR_GREEN, default_bg)
|
||||
curses.init_pair(9, curses.COLOR_BLACK, curses.COLOR_CYAN)
|
||||
|
||||
def _parse_options(self):
|
||||
"""Parse command line options into tabs and fields"""
|
||||
for group in self.parser.option_groups:
|
||||
self.all_options = []
|
||||
for group in _parserGroups(self.parser):
|
||||
title = _groupTitle(group)
|
||||
tab_data = {
|
||||
'title': group.title,
|
||||
'description': group.get_description() if hasattr(group, 'get_description') and group.get_description() else "",
|
||||
'title': title,
|
||||
'description': _groupDescription(group),
|
||||
'options': []
|
||||
}
|
||||
|
||||
for option in group.option_list:
|
||||
for option in _groupOptions(group):
|
||||
dest = _optDest(option)
|
||||
if not dest:
|
||||
continue
|
||||
field_data = {
|
||||
'dest': option.dest,
|
||||
'dest': dest,
|
||||
'label': self._format_option_strings(option),
|
||||
'help': option.help if option.help else "",
|
||||
'type': option.type if hasattr(option, 'type') and option.type else 'bool',
|
||||
'help': _optHelp(option),
|
||||
'type': _optValueType(option) if _optTakesValue(option) else 'bool',
|
||||
'value': '',
|
||||
'default': defaults.get(option.dest) if defaults.get(option.dest) else None
|
||||
'default': defaults.get(dest) if defaults.get(dest) else None
|
||||
}
|
||||
tab_data['options'].append(field_data)
|
||||
self.fields[(group.title, option.dest)] = field_data
|
||||
self.fields[(title, dest)] = field_data
|
||||
self.all_options.append(field_data)
|
||||
|
||||
self.tabs.append(tab_data)
|
||||
|
||||
# curated "Quick start" tab; references the same field objects as the group tabs,
|
||||
# so a value edited in either place stays in sync
|
||||
seen = {}
|
||||
for tab in self.tabs:
|
||||
for option in tab['options']:
|
||||
seen.setdefault(option['dest'], option)
|
||||
quick = {
|
||||
'title': 'Quick start',
|
||||
'description': "The options people reach for most. Fill these in, then press F2 to run.",
|
||||
'options': [seen[dest] for dest in QUICK_START_DESTS if dest in seen],
|
||||
}
|
||||
if quick['options']:
|
||||
self.tabs.insert(0, quick)
|
||||
|
||||
def _format_option_strings(self, option):
|
||||
"""Format option strings for display"""
|
||||
parts = []
|
||||
if hasattr(option, '_short_opts') and option._short_opts:
|
||||
parts.extend(option._short_opts)
|
||||
if hasattr(option, '_long_opts') and option._long_opts:
|
||||
parts.extend(option._long_opts)
|
||||
return ', '.join(parts)
|
||||
return ', '.join(_optStrings(option))
|
||||
|
||||
def _tab_title(self, tab):
|
||||
return TAB_ALIASES.get(tab['title'], tab['title'])
|
||||
|
||||
def _draw_header(self):
|
||||
"""Draw the header bar"""
|
||||
height, width = self.stdscr.getmaxyx()
|
||||
header = " sqlmap - ncurses TUI "
|
||||
self.stdscr.attron(curses.color_pair(1) | curses.A_BOLD)
|
||||
self.stdscr.addstr(0, 0, header.center(width))
|
||||
self.stdscr.attroff(curses.color_pair(1) | curses.A_BOLD)
|
||||
self.stdscr.addstr(0, 0, " " * width)
|
||||
self.stdscr.addstr(0, 1, "sqlmap")
|
||||
self.stdscr.attroff(curses.A_BOLD)
|
||||
right = "F2 Run - F10 Quit "
|
||||
try:
|
||||
self.stdscr.addstr(0, max(8, width - len(right)), right)
|
||||
except:
|
||||
pass
|
||||
self.stdscr.attroff(curses.color_pair(1))
|
||||
|
||||
def _get_tab_bar_height(self):
|
||||
"""Calculate how many rows the tab bar uses"""
|
||||
|
|
@ -101,16 +219,12 @@ class NcursesUI:
|
|||
x = 0
|
||||
|
||||
for i, tab in enumerate(self.tabs):
|
||||
tab_text = " %s " % tab['title']
|
||||
|
||||
# Check if tab exceeds width, wrap to next line
|
||||
tab_text = " %s " % self._tab_title(tab)
|
||||
if x + len(tab_text) >= width:
|
||||
y += 1
|
||||
x = 0
|
||||
# Stop if we've used too many lines
|
||||
if y >= 3:
|
||||
if y >= 4:
|
||||
break
|
||||
|
||||
x += len(tab_text) + 1
|
||||
|
||||
return y
|
||||
|
|
@ -122,14 +236,11 @@ class NcursesUI:
|
|||
x = 0
|
||||
|
||||
for i, tab in enumerate(self.tabs):
|
||||
tab_text = " %s " % tab['title']
|
||||
|
||||
# Check if tab exceeds width, wrap to next line
|
||||
tab_text = " %s " % self._tab_title(tab)
|
||||
if x + len(tab_text) >= width:
|
||||
y += 1
|
||||
x = 0
|
||||
# Stop if we've used too many lines
|
||||
if y >= 3:
|
||||
if y >= 4:
|
||||
break
|
||||
|
||||
if i == self.current_tab:
|
||||
|
|
@ -152,11 +263,11 @@ class NcursesUI:
|
|||
def _draw_footer(self):
|
||||
"""Draw the footer with help text"""
|
||||
height, width = self.stdscr.getmaxyx()
|
||||
footer = " [Tab] Next | [Arrows] Navigate | [Enter] Edit | [F2] Run | [F3] Export | [F4] Import | [F10] Quit "
|
||||
footer = " Tab/<-/-> Section Up/Down Field Enter/Space Edit F2 Run F3 Export F4 Import F10 Quit "
|
||||
|
||||
try:
|
||||
self.stdscr.attron(curses.color_pair(1))
|
||||
self.stdscr.addstr(height - 1, 0, footer.ljust(width))
|
||||
self.stdscr.addstr(height - 1, 0, footer.ljust(width)[:width - 1])
|
||||
self.stdscr.attroff(curses.color_pair(1))
|
||||
except:
|
||||
pass
|
||||
|
|
@ -202,51 +313,58 @@ class NcursesUI:
|
|||
|
||||
is_selected = (i == self.current_field)
|
||||
|
||||
# Draw label
|
||||
# full-width highlight bar for the selected row
|
||||
if is_selected:
|
||||
try:
|
||||
self.stdscr.attron(curses.color_pair(4))
|
||||
self.stdscr.addstr(y, 0, " " * (width - 1))
|
||||
self.stdscr.attroff(curses.color_pair(4))
|
||||
except:
|
||||
pass
|
||||
|
||||
# label
|
||||
label = option['label'][:25].ljust(25)
|
||||
label_attr = curses.color_pair(4) | curses.A_BOLD if is_selected else curses.color_pair(7)
|
||||
try:
|
||||
if is_selected:
|
||||
self.stdscr.attron(curses.color_pair(4) | curses.A_BOLD)
|
||||
else:
|
||||
self.stdscr.attron(curses.color_pair(7))
|
||||
|
||||
self.stdscr.attron(label_attr)
|
||||
self.stdscr.addstr(y, 2, label)
|
||||
|
||||
if is_selected:
|
||||
self.stdscr.attroff(curses.color_pair(4) | curses.A_BOLD)
|
||||
else:
|
||||
self.stdscr.attroff(curses.color_pair(7))
|
||||
self.stdscr.attroff(label_attr)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Draw value
|
||||
value_str = ""
|
||||
# value (green once the user has set one, muted "(default)" otherwise)
|
||||
has_value = option['value'] not in (None, "", False)
|
||||
if option['type'] == 'bool':
|
||||
value = option['value'] if option['value'] is not None else option.get('default')
|
||||
value_str = "[X]" if value else "[ ]"
|
||||
value_str = "[x]" if value else "[ ]"
|
||||
value_attr = curses.color_pair(8) if value else curses.color_pair(5)
|
||||
elif has_value:
|
||||
value_str = str(option['value'])
|
||||
value_attr = curses.color_pair(8)
|
||||
elif option['default'] not in (None, False):
|
||||
value_str = "(%s)" % str(option['default'])
|
||||
value_attr = curses.color_pair(5)
|
||||
else:
|
||||
value_str = str(option['value']) if option['value'] else ""
|
||||
if option['default'] and not option['value']:
|
||||
value_str = "(%s)" % str(option['default'])
|
||||
|
||||
value_str = value_str[:30]
|
||||
value_str = ""
|
||||
value_attr = curses.color_pair(5)
|
||||
|
||||
if is_selected:
|
||||
value_attr = curses.color_pair(4) | curses.A_BOLD
|
||||
try:
|
||||
if is_selected:
|
||||
self.stdscr.attron(curses.color_pair(4) | curses.A_BOLD)
|
||||
self.stdscr.addstr(y, 28, value_str)
|
||||
if is_selected:
|
||||
self.stdscr.attroff(curses.color_pair(4) | curses.A_BOLD)
|
||||
self.stdscr.attron(value_attr)
|
||||
self.stdscr.addstr(y, 28, value_str[:30])
|
||||
self.stdscr.attroff(value_attr)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Draw help text
|
||||
# help text (always shown, including on the highlighted row so it stays readable)
|
||||
if width > 65:
|
||||
help_text = option['help'][:width-62] if option['help'] else ""
|
||||
help_text = option['help'][:width - 62] if option['help'] else ""
|
||||
help_attr = curses.color_pair(9) if is_selected else curses.color_pair(5)
|
||||
try:
|
||||
self.stdscr.attron(curses.color_pair(5))
|
||||
self.stdscr.addstr(y, 60, help_text)
|
||||
self.stdscr.attroff(curses.color_pair(5))
|
||||
self.stdscr.attron(help_attr)
|
||||
self.stdscr.addstr(y, 60, help_text.ljust(width - 61)[:width - 61])
|
||||
self.stdscr.attroff(help_attr)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
|
@ -292,50 +410,57 @@ class NcursesUI:
|
|||
# Toggle boolean
|
||||
option['value'] = not option['value']
|
||||
else:
|
||||
# Text input
|
||||
# Text input (manual key loop so Esc can cancel and Enter can save)
|
||||
height, width = self.stdscr.getmaxyx()
|
||||
|
||||
# Create input window
|
||||
input_win = curses.newwin(5, width - 20, height // 2 - 2, 10)
|
||||
input_win.keypad(True)
|
||||
input_win.box()
|
||||
input_win.attron(curses.color_pair(2))
|
||||
input_win.addstr(0, 2, " Edit %s " % option['label'][:20])
|
||||
input_win.attroff(curses.color_pair(2))
|
||||
input_win.addstr(2, 2, "Value:")
|
||||
input_win.refresh()
|
||||
input_win.attron(curses.color_pair(5))
|
||||
input_win.addstr(3, 2, "[Enter] save [Esc] cancel")
|
||||
input_win.attroff(curses.color_pair(5))
|
||||
|
||||
# Get input
|
||||
curses.echo()
|
||||
buffer = str(option['value']) if option['value'] not in (None, "") else ""
|
||||
max_len = max(1, width - 34)
|
||||
curses.noecho()
|
||||
curses.curs_set(1)
|
||||
|
||||
# Pre-fill with existing value
|
||||
current_value = str(option['value']) if option['value'] else ""
|
||||
input_win.addstr(2, 9, current_value)
|
||||
input_win.move(2, 9)
|
||||
while True:
|
||||
shown = buffer[-max_len:]
|
||||
input_win.addstr(2, 2, "Value: ")
|
||||
input_win.addstr(2, 9, shown.ljust(max_len)[:max_len])
|
||||
input_win.move(2, 9 + len(shown))
|
||||
input_win.refresh()
|
||||
|
||||
try:
|
||||
new_value = input_win.getstr(2, 9, width - 32).decode('utf-8')
|
||||
ch = input_win.getch()
|
||||
if ch == 27: # Esc -> cancel, keep old value
|
||||
buffer = None
|
||||
break
|
||||
elif ch in (curses.KEY_ENTER, 10, 13): # Enter -> commit
|
||||
break
|
||||
elif ch in (curses.KEY_BACKSPACE, 127, 8):
|
||||
buffer = buffer[:-1]
|
||||
elif 32 <= ch <= 126:
|
||||
buffer += chr(ch)
|
||||
|
||||
# Validate and convert based on type
|
||||
curses.curs_set(0)
|
||||
|
||||
if buffer is not None:
|
||||
if option['type'] == 'int':
|
||||
try:
|
||||
option['value'] = int(new_value) if new_value else None
|
||||
option['value'] = int(buffer) if buffer else None
|
||||
except ValueError:
|
||||
option['value'] = None
|
||||
elif option['type'] == 'float':
|
||||
try:
|
||||
option['value'] = float(new_value) if new_value else None
|
||||
option['value'] = float(buffer) if buffer else None
|
||||
except ValueError:
|
||||
option['value'] = None
|
||||
else:
|
||||
option['value'] = new_value if new_value else None
|
||||
except:
|
||||
pass
|
||||
option['value'] = buffer if buffer else None
|
||||
|
||||
curses.noecho()
|
||||
curses.curs_set(0)
|
||||
|
||||
# Clear input window
|
||||
input_win.clear()
|
||||
input_win.refresh()
|
||||
del input_win
|
||||
|
|
@ -378,9 +503,9 @@ class NcursesUI:
|
|||
config[dest] = value
|
||||
|
||||
# Set defaults for unset options
|
||||
for option in self.parser.option_list:
|
||||
if option.dest not in config or config[option.dest] is None:
|
||||
config[option.dest] = defaults.get(option.dest, None)
|
||||
for field in self.all_options:
|
||||
if field['dest'] not in config or config[field['dest']] is None:
|
||||
config[field['dest']] = defaults.get(field['dest'], None)
|
||||
|
||||
# Save config
|
||||
try:
|
||||
|
|
@ -537,9 +662,9 @@ class NcursesUI:
|
|||
config[dest] = value
|
||||
|
||||
# Set defaults for unset options
|
||||
for option in self.parser.option_list:
|
||||
if option.dest not in config or config[option.dest] is None:
|
||||
config[option.dest] = defaults.get(option.dest, None)
|
||||
for field in self.all_options:
|
||||
if field['dest'] not in config or config[field['dest']] is None:
|
||||
config[field['dest']] = defaults.get(field['dest'], None)
|
||||
|
||||
# Create temp config file
|
||||
handle, configFile = tempfile.mkstemp(prefix=MKSTEMP_PREFIX.CONFIG, text=True)
|
||||
|
|
@ -713,7 +838,7 @@ class NcursesUI:
|
|||
tab = self.tabs[self.current_tab]
|
||||
|
||||
# Handle input
|
||||
if key == curses.KEY_F10 or key == 27: # F10 or ESC
|
||||
if key == curses.KEY_F10: # F10 quits; Esc intentionally does NOT (it only cancels field edits)
|
||||
break
|
||||
elif key == ord('\t') or key == curses.KEY_RIGHT: # Tab or Right arrow
|
||||
self.current_tab = (self.current_tab + 1) % len(self.tabs)
|
||||
|
|
@ -755,9 +880,17 @@ def runTui(parser):
|
|||
# Check if ncurses is available
|
||||
if curses is None:
|
||||
raise SqlmapMissingDependence("missing 'curses' module (optional Python module). Use a Python build that includes curses/ncurses, or install the platform-provided equivalent (e.g. for Windows: pip install windows-curses)")
|
||||
# ncurses waits ESCDELAY ms (default 1000) after Esc to disambiguate escape sequences, which
|
||||
# makes Esc feel like it hangs for ~1s; shrink it so Esc reacts immediately
|
||||
os.environ.setdefault("ESCDELAY", "25")
|
||||
try:
|
||||
# Initialize and run
|
||||
def main(stdscr):
|
||||
if hasattr(curses, "set_escdelay"):
|
||||
try:
|
||||
curses.set_escdelay(25)
|
||||
except curses.error:
|
||||
pass
|
||||
ui = NcursesUI(stdscr, parser)
|
||||
ui.run()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue