souleyez 2.43.29__py3-none-any.whl → 3.0.0__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.
- souleyez/__init__.py +1 -2
- souleyez/ai/__init__.py +21 -15
- souleyez/ai/action_mapper.py +249 -150
- souleyez/ai/chain_advisor.py +116 -100
- souleyez/ai/claude_provider.py +29 -28
- souleyez/ai/context_builder.py +80 -62
- souleyez/ai/executor.py +158 -117
- souleyez/ai/feedback_handler.py +136 -121
- souleyez/ai/llm_factory.py +27 -20
- souleyez/ai/llm_provider.py +4 -2
- souleyez/ai/ollama_provider.py +6 -9
- souleyez/ai/ollama_service.py +44 -37
- souleyez/ai/path_scorer.py +91 -76
- souleyez/ai/recommender.py +176 -144
- souleyez/ai/report_context.py +74 -73
- souleyez/ai/report_service.py +84 -66
- souleyez/ai/result_parser.py +222 -229
- souleyez/ai/safety.py +67 -44
- souleyez/auth/__init__.py +23 -22
- souleyez/auth/audit.py +36 -26
- souleyez/auth/engagement_access.py +65 -48
- souleyez/auth/permissions.py +14 -3
- souleyez/auth/session_manager.py +54 -37
- souleyez/auth/user_manager.py +109 -64
- souleyez/commands/audit.py +40 -43
- souleyez/commands/auth.py +35 -15
- souleyez/commands/deliverables.py +55 -50
- souleyez/commands/engagement.py +47 -28
- souleyez/commands/license.py +32 -23
- souleyez/commands/screenshots.py +36 -32
- souleyez/commands/user.py +82 -36
- souleyez/config.py +52 -44
- souleyez/core/credential_tester.py +87 -81
- souleyez/core/cve_mappings.py +179 -192
- souleyez/core/cve_matcher.py +162 -148
- souleyez/core/msf_auto_mapper.py +100 -83
- souleyez/core/msf_chain_engine.py +294 -256
- souleyez/core/msf_database.py +153 -70
- souleyez/core/msf_integration.py +679 -673
- souleyez/core/msf_rpc_client.py +40 -42
- souleyez/core/msf_rpc_manager.py +77 -79
- souleyez/core/msf_sync_manager.py +241 -181
- souleyez/core/network_utils.py +22 -15
- souleyez/core/parser_handler.py +34 -25
- souleyez/core/pending_chains.py +114 -63
- souleyez/core/templates.py +158 -107
- souleyez/core/tool_chaining.py +9564 -2881
- souleyez/core/version_utils.py +79 -94
- souleyez/core/vuln_correlation.py +136 -89
- souleyez/core/web_utils.py +33 -32
- souleyez/data/wordlists/ad_users.txt +378 -0
- souleyez/data/wordlists/api_endpoints_large.txt +769 -0
- souleyez/data/wordlists/home_dir_sensitive.txt +39 -0
- souleyez/data/wordlists/lfi_payloads.txt +82 -0
- souleyez/data/wordlists/passwords_brute.txt +1548 -0
- souleyez/data/wordlists/passwords_crack.txt +2479 -0
- souleyez/data/wordlists/passwords_spray.txt +386 -0
- souleyez/data/wordlists/subdomains_large.txt +5057 -0
- souleyez/data/wordlists/usernames_common.txt +694 -0
- souleyez/data/wordlists/web_dirs_large.txt +4769 -0
- souleyez/detection/__init__.py +1 -1
- souleyez/detection/attack_signatures.py +12 -17
- souleyez/detection/mitre_mappings.py +61 -55
- souleyez/detection/validator.py +97 -86
- souleyez/devtools.py +23 -10
- souleyez/docs/README.md +4 -4
- souleyez/docs/api-reference/cli-commands.md +2 -2
- souleyez/docs/developer-guide/adding-new-tools.md +562 -0
- souleyez/docs/user-guide/auto-chaining.md +30 -8
- souleyez/docs/user-guide/getting-started.md +1 -1
- souleyez/docs/user-guide/installation.md +26 -3
- souleyez/docs/user-guide/metasploit-integration.md +2 -2
- souleyez/docs/user-guide/rbac.md +1 -1
- souleyez/docs/user-guide/scope-management.md +1 -1
- souleyez/docs/user-guide/siem-integration.md +1 -1
- souleyez/docs/user-guide/tools-reference.md +1 -8
- souleyez/docs/user-guide/worker-management.md +1 -1
- souleyez/engine/background.py +1239 -535
- souleyez/engine/base.py +4 -1
- souleyez/engine/job_status.py +17 -49
- souleyez/engine/log_sanitizer.py +103 -77
- souleyez/engine/manager.py +38 -7
- souleyez/engine/result_handler.py +2200 -1550
- souleyez/engine/worker_manager.py +50 -41
- souleyez/export/evidence_bundle.py +72 -62
- souleyez/feature_flags/features.py +16 -20
- souleyez/feature_flags.py +5 -9
- souleyez/handlers/__init__.py +11 -0
- souleyez/handlers/base.py +188 -0
- souleyez/handlers/bash_handler.py +277 -0
- souleyez/handlers/bloodhound_handler.py +243 -0
- souleyez/handlers/certipy_handler.py +311 -0
- souleyez/handlers/crackmapexec_handler.py +486 -0
- souleyez/handlers/dnsrecon_handler.py +344 -0
- souleyez/handlers/enum4linux_handler.py +400 -0
- souleyez/handlers/evil_winrm_handler.py +493 -0
- souleyez/handlers/ffuf_handler.py +815 -0
- souleyez/handlers/gobuster_handler.py +1114 -0
- souleyez/handlers/gpp_extract_handler.py +334 -0
- souleyez/handlers/hashcat_handler.py +444 -0
- souleyez/handlers/hydra_handler.py +564 -0
- souleyez/handlers/impacket_getuserspns_handler.py +343 -0
- souleyez/handlers/impacket_psexec_handler.py +222 -0
- souleyez/handlers/impacket_secretsdump_handler.py +426 -0
- souleyez/handlers/john_handler.py +286 -0
- souleyez/handlers/katana_handler.py +425 -0
- souleyez/handlers/kerbrute_handler.py +298 -0
- souleyez/handlers/ldapsearch_handler.py +636 -0
- souleyez/handlers/lfi_extract_handler.py +464 -0
- souleyez/handlers/msf_auxiliary_handler.py +409 -0
- souleyez/handlers/msf_exploit_handler.py +380 -0
- souleyez/handlers/nikto_handler.py +413 -0
- souleyez/handlers/nmap_handler.py +821 -0
- souleyez/handlers/nuclei_handler.py +359 -0
- souleyez/handlers/nxc_handler.py +417 -0
- souleyez/handlers/rdp_sec_check_handler.py +353 -0
- souleyez/handlers/registry.py +292 -0
- souleyez/handlers/responder_handler.py +232 -0
- souleyez/handlers/service_explorer_handler.py +434 -0
- souleyez/handlers/smbclient_handler.py +344 -0
- souleyez/handlers/smbmap_handler.py +510 -0
- souleyez/handlers/smbpasswd_handler.py +296 -0
- souleyez/handlers/sqlmap_handler.py +1116 -0
- souleyez/handlers/theharvester_handler.py +601 -0
- souleyez/handlers/web_login_test_handler.py +327 -0
- souleyez/handlers/whois_handler.py +277 -0
- souleyez/handlers/wpscan_handler.py +554 -0
- souleyez/history.py +32 -16
- souleyez/importers/msf_importer.py +106 -75
- souleyez/importers/smart_importer.py +208 -147
- souleyez/integrations/siem/__init__.py +10 -10
- souleyez/integrations/siem/base.py +17 -18
- souleyez/integrations/siem/elastic.py +108 -122
- souleyez/integrations/siem/factory.py +207 -80
- souleyez/integrations/siem/googlesecops.py +146 -154
- souleyez/integrations/siem/rule_mappings/__init__.py +1 -1
- souleyez/integrations/siem/rule_mappings/wazuh_rules.py +8 -5
- souleyez/integrations/siem/sentinel.py +107 -109
- souleyez/integrations/siem/splunk.py +246 -212
- souleyez/integrations/siem/wazuh.py +65 -71
- souleyez/integrations/wazuh/__init__.py +5 -5
- souleyez/integrations/wazuh/client.py +70 -93
- souleyez/integrations/wazuh/config.py +85 -57
- souleyez/integrations/wazuh/host_mapper.py +28 -36
- souleyez/integrations/wazuh/sync.py +78 -68
- souleyez/intelligence/__init__.py +4 -5
- souleyez/intelligence/correlation_analyzer.py +309 -295
- souleyez/intelligence/exploit_knowledge.py +661 -623
- souleyez/intelligence/exploit_suggestions.py +159 -139
- souleyez/intelligence/gap_analyzer.py +132 -97
- souleyez/intelligence/gap_detector.py +251 -214
- souleyez/intelligence/sensitive_tables.py +266 -129
- souleyez/intelligence/service_parser.py +137 -123
- souleyez/intelligence/surface_analyzer.py +407 -268
- souleyez/intelligence/target_parser.py +159 -162
- souleyez/licensing/__init__.py +6 -6
- souleyez/licensing/validator.py +17 -19
- souleyez/log_config.py +79 -54
- souleyez/main.py +1505 -687
- souleyez/migrations/fix_job_counter.py +16 -14
- souleyez/parsers/bloodhound_parser.py +41 -39
- souleyez/parsers/crackmapexec_parser.py +178 -111
- souleyez/parsers/dalfox_parser.py +72 -77
- souleyez/parsers/dnsrecon_parser.py +103 -91
- souleyez/parsers/enum4linux_parser.py +183 -153
- souleyez/parsers/ffuf_parser.py +29 -25
- souleyez/parsers/gobuster_parser.py +301 -41
- souleyez/parsers/hashcat_parser.py +324 -79
- souleyez/parsers/http_fingerprint_parser.py +350 -103
- souleyez/parsers/hydra_parser.py +131 -111
- souleyez/parsers/impacket_parser.py +231 -178
- souleyez/parsers/john_parser.py +98 -86
- souleyez/parsers/katana_parser.py +316 -0
- souleyez/parsers/msf_parser.py +943 -498
- souleyez/parsers/nikto_parser.py +346 -65
- souleyez/parsers/nmap_parser.py +262 -174
- souleyez/parsers/nuclei_parser.py +40 -44
- souleyez/parsers/responder_parser.py +26 -26
- souleyez/parsers/searchsploit_parser.py +74 -74
- souleyez/parsers/service_explorer_parser.py +279 -0
- souleyez/parsers/smbmap_parser.py +180 -124
- souleyez/parsers/sqlmap_parser.py +434 -308
- souleyez/parsers/theharvester_parser.py +75 -57
- souleyez/parsers/whois_parser.py +135 -94
- souleyez/parsers/wpscan_parser.py +278 -190
- souleyez/plugins/afp.py +44 -36
- souleyez/plugins/afp_brute.py +114 -46
- souleyez/plugins/ard.py +48 -37
- souleyez/plugins/bloodhound.py +95 -61
- souleyez/plugins/certipy.py +303 -0
- souleyez/plugins/crackmapexec.py +186 -85
- souleyez/plugins/dalfox.py +120 -59
- souleyez/plugins/dns_hijack.py +146 -41
- souleyez/plugins/dnsrecon.py +97 -61
- souleyez/plugins/enum4linux.py +91 -66
- souleyez/plugins/evil_winrm.py +291 -0
- souleyez/plugins/ffuf.py +166 -90
- souleyez/plugins/firmware_extract.py +133 -29
- souleyez/plugins/gobuster.py +387 -190
- souleyez/plugins/gpp_extract.py +393 -0
- souleyez/plugins/hashcat.py +100 -73
- souleyez/plugins/http_fingerprint.py +913 -267
- souleyez/plugins/hydra.py +566 -200
- souleyez/plugins/impacket_getnpusers.py +117 -69
- souleyez/plugins/impacket_psexec.py +84 -64
- souleyez/plugins/impacket_secretsdump.py +103 -69
- souleyez/plugins/impacket_smbclient.py +89 -75
- souleyez/plugins/john.py +86 -69
- souleyez/plugins/katana.py +313 -0
- souleyez/plugins/kerbrute.py +237 -0
- souleyez/plugins/lfi_extract.py +541 -0
- souleyez/plugins/macos_ssh.py +117 -48
- souleyez/plugins/mdns.py +35 -30
- souleyez/plugins/msf_auxiliary.py +253 -130
- souleyez/plugins/msf_exploit.py +239 -161
- souleyez/plugins/nikto.py +134 -78
- souleyez/plugins/nmap.py +275 -91
- souleyez/plugins/nuclei.py +180 -89
- souleyez/plugins/nxc.py +285 -0
- souleyez/plugins/plugin_base.py +35 -36
- souleyez/plugins/plugin_template.py +13 -5
- souleyez/plugins/rdp_sec_check.py +130 -0
- souleyez/plugins/responder.py +112 -71
- souleyez/plugins/router_http_brute.py +76 -65
- souleyez/plugins/router_ssh_brute.py +118 -41
- souleyez/plugins/router_telnet_brute.py +124 -42
- souleyez/plugins/routersploit.py +91 -59
- souleyez/plugins/routersploit_exploit.py +77 -55
- souleyez/plugins/searchsploit.py +91 -77
- souleyez/plugins/service_explorer.py +1160 -0
- souleyez/plugins/smbmap.py +122 -72
- souleyez/plugins/smbpasswd.py +215 -0
- souleyez/plugins/sqlmap.py +301 -113
- souleyez/plugins/theharvester.py +127 -75
- souleyez/plugins/tr069.py +79 -57
- souleyez/plugins/upnp.py +65 -47
- souleyez/plugins/upnp_abuse.py +73 -55
- souleyez/plugins/vnc_access.py +129 -42
- souleyez/plugins/vnc_brute.py +109 -38
- souleyez/plugins/web_login_test.py +417 -0
- souleyez/plugins/whois.py +77 -58
- souleyez/plugins/wpscan.py +219 -69
- souleyez/reporting/__init__.py +2 -1
- souleyez/reporting/attack_chain.py +411 -346
- souleyez/reporting/charts.py +436 -501
- souleyez/reporting/compliance_mappings.py +334 -201
- souleyez/reporting/detection_report.py +126 -125
- souleyez/reporting/formatters.py +828 -591
- souleyez/reporting/generator.py +386 -302
- souleyez/reporting/metrics.py +72 -75
- souleyez/scanner.py +35 -29
- souleyez/security/__init__.py +37 -11
- souleyez/security/scope_validator.py +175 -106
- souleyez/security/validation.py +237 -149
- souleyez/security.py +22 -6
- souleyez/storage/credentials.py +247 -186
- souleyez/storage/crypto.py +296 -129
- souleyez/storage/database.py +73 -50
- souleyez/storage/db.py +58 -36
- souleyez/storage/deliverable_evidence.py +177 -128
- souleyez/storage/deliverable_exporter.py +282 -246
- souleyez/storage/deliverable_templates.py +134 -116
- souleyez/storage/deliverables.py +135 -130
- souleyez/storage/engagements.py +109 -56
- souleyez/storage/evidence.py +181 -152
- souleyez/storage/execution_log.py +31 -17
- souleyez/storage/exploit_attempts.py +93 -57
- souleyez/storage/exploits.py +67 -36
- souleyez/storage/findings.py +48 -61
- souleyez/storage/hosts.py +176 -144
- souleyez/storage/migrate_to_engagements.py +43 -19
- souleyez/storage/migrations/_001_add_credential_enhancements.py +22 -12
- souleyez/storage/migrations/_002_add_status_tracking.py +10 -7
- souleyez/storage/migrations/_003_add_execution_log.py +14 -8
- souleyez/storage/migrations/_005_screenshots.py +13 -5
- souleyez/storage/migrations/_006_deliverables.py +13 -5
- souleyez/storage/migrations/_007_deliverable_templates.py +12 -7
- souleyez/storage/migrations/_008_add_nuclei_table.py +10 -4
- souleyez/storage/migrations/_010_evidence_linking.py +17 -10
- souleyez/storage/migrations/_011_timeline_tracking.py +20 -13
- souleyez/storage/migrations/_012_team_collaboration.py +34 -21
- souleyez/storage/migrations/_013_add_host_tags.py +12 -6
- souleyez/storage/migrations/_014_exploit_attempts.py +22 -10
- souleyez/storage/migrations/_015_add_mac_os_fields.py +15 -7
- souleyez/storage/migrations/_016_add_domain_field.py +10 -4
- souleyez/storage/migrations/_017_msf_sessions.py +16 -8
- souleyez/storage/migrations/_018_add_osint_target.py +10 -6
- souleyez/storage/migrations/_019_add_engagement_type.py +10 -6
- souleyez/storage/migrations/_020_add_rbac.py +36 -15
- souleyez/storage/migrations/_021_wazuh_integration.py +20 -8
- souleyez/storage/migrations/_022_wazuh_indexer_columns.py +6 -4
- souleyez/storage/migrations/_023_fix_detection_results_fk.py +16 -6
- souleyez/storage/migrations/_024_wazuh_vulnerabilities.py +26 -10
- souleyez/storage/migrations/_025_multi_siem_support.py +3 -5
- souleyez/storage/migrations/_026_add_engagement_scope.py +31 -12
- souleyez/storage/migrations/_027_multi_siem_persistence.py +32 -15
- souleyez/storage/migrations/__init__.py +26 -26
- souleyez/storage/migrations/migration_manager.py +19 -19
- souleyez/storage/msf_sessions.py +100 -65
- souleyez/storage/osint.py +17 -24
- souleyez/storage/recommendation_engine.py +269 -235
- souleyez/storage/screenshots.py +33 -32
- souleyez/storage/smb_shares.py +136 -92
- souleyez/storage/sqlmap_data.py +183 -128
- souleyez/storage/team_collaboration.py +135 -141
- souleyez/storage/timeline_tracker.py +122 -94
- souleyez/storage/wazuh_vulns.py +64 -66
- souleyez/storage/web_paths.py +33 -37
- souleyez/testing/credential_tester.py +221 -205
- souleyez/ui/__init__.py +1 -1
- souleyez/ui/ai_quotes.py +12 -12
- souleyez/ui/attack_surface.py +2439 -1516
- souleyez/ui/chain_rules_view.py +914 -382
- souleyez/ui/correlation_view.py +312 -230
- souleyez/ui/dashboard.py +2382 -1130
- souleyez/ui/deliverables_view.py +148 -62
- souleyez/ui/design_system.py +13 -13
- souleyez/ui/errors.py +49 -49
- souleyez/ui/evidence_linking_view.py +284 -179
- souleyez/ui/evidence_vault.py +393 -285
- souleyez/ui/exploit_suggestions_view.py +555 -349
- souleyez/ui/export_view.py +100 -66
- souleyez/ui/gap_analysis_view.py +315 -171
- souleyez/ui/help_system.py +105 -97
- souleyez/ui/intelligence_view.py +436 -293
- souleyez/ui/interactive.py +23034 -10679
- souleyez/ui/interactive_selector.py +75 -68
- souleyez/ui/log_formatter.py +47 -39
- souleyez/ui/menu_components.py +22 -13
- souleyez/ui/msf_auxiliary_menu.py +184 -133
- souleyez/ui/pending_chains_view.py +336 -172
- souleyez/ui/progress_indicators.py +5 -3
- souleyez/ui/recommendations_view.py +195 -137
- souleyez/ui/rule_builder.py +343 -225
- souleyez/ui/setup_wizard.py +678 -284
- souleyez/ui/shortcuts.py +217 -165
- souleyez/ui/splunk_gap_analysis_view.py +452 -270
- souleyez/ui/splunk_vulns_view.py +139 -86
- souleyez/ui/team_dashboard.py +498 -335
- souleyez/ui/template_selector.py +196 -105
- souleyez/ui/terminal.py +6 -6
- souleyez/ui/timeline_view.py +198 -127
- souleyez/ui/tool_setup.py +264 -164
- souleyez/ui/tutorial.py +202 -72
- souleyez/ui/tutorial_state.py +40 -40
- souleyez/ui/wazuh_vulns_view.py +235 -141
- souleyez/ui/wordlist_browser.py +260 -107
- souleyez/ui.py +464 -312
- souleyez/utils/tool_checker.py +427 -367
- souleyez/utils.py +33 -29
- souleyez/wordlists.py +134 -167
- {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/METADATA +2 -2
- souleyez-3.0.0.dist-info/RECORD +443 -0
- {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/WHEEL +1 -1
- souleyez-2.43.29.dist-info/RECORD +0 -379
- {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/entry_points.txt +0 -0
- {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/licenses/LICENSE +0 -0
- {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/top_level.txt +0 -0
|
@@ -17,6 +17,7 @@ from souleyez.ui.design_system import DesignSystem
|
|
|
17
17
|
# Key codes - using readchar constants when available
|
|
18
18
|
try:
|
|
19
19
|
import readchar
|
|
20
|
+
|
|
20
21
|
KEY_UP = readchar.key.UP
|
|
21
22
|
KEY_DOWN = readchar.key.DOWN
|
|
22
23
|
KEY_LEFT = readchar.key.LEFT
|
|
@@ -28,15 +29,15 @@ try:
|
|
|
28
29
|
KEY_SPACE = readchar.key.SPACE
|
|
29
30
|
except (ImportError, AttributeError):
|
|
30
31
|
# Fallback to raw codes
|
|
31
|
-
KEY_UP =
|
|
32
|
-
KEY_DOWN =
|
|
33
|
-
KEY_LEFT =
|
|
34
|
-
KEY_RIGHT =
|
|
35
|
-
KEY_PAGE_UP =
|
|
36
|
-
KEY_PAGE_DOWN =
|
|
37
|
-
KEY_ESCAPE =
|
|
38
|
-
KEY_ENTER =
|
|
39
|
-
KEY_SPACE =
|
|
32
|
+
KEY_UP = "\x1b[A"
|
|
33
|
+
KEY_DOWN = "\x1b[B"
|
|
34
|
+
KEY_LEFT = "\x1b[D"
|
|
35
|
+
KEY_RIGHT = "\x1b[C"
|
|
36
|
+
KEY_PAGE_UP = "\x1b[5~"
|
|
37
|
+
KEY_PAGE_DOWN = "\x1b[6~"
|
|
38
|
+
KEY_ESCAPE = "\x1b"
|
|
39
|
+
KEY_ENTER = "\r"
|
|
40
|
+
KEY_SPACE = " "
|
|
40
41
|
|
|
41
42
|
|
|
42
43
|
def _get_key() -> str:
|
|
@@ -48,6 +49,7 @@ def _get_key() -> str:
|
|
|
48
49
|
"""
|
|
49
50
|
try:
|
|
50
51
|
import readchar
|
|
52
|
+
|
|
51
53
|
key = readchar.readkey()
|
|
52
54
|
return key
|
|
53
55
|
except ImportError:
|
|
@@ -58,44 +60,44 @@ def _get_key() -> str:
|
|
|
58
60
|
ch = click.getchar()
|
|
59
61
|
|
|
60
62
|
# Handle escape sequences (arrow keys send \x1b[A, \x1b[B, etc.)
|
|
61
|
-
if ch ==
|
|
63
|
+
if ch == "\x1b" or (len(ch) > 1 and ch.startswith("\x1b")):
|
|
62
64
|
# click.getchar() may return the full sequence or just escape
|
|
63
65
|
if len(ch) >= 3:
|
|
64
|
-
if ch ==
|
|
66
|
+
if ch == "\x1b[A":
|
|
65
67
|
return KEY_UP
|
|
66
|
-
elif ch ==
|
|
68
|
+
elif ch == "\x1b[B":
|
|
67
69
|
return KEY_DOWN
|
|
68
|
-
elif ch ==
|
|
70
|
+
elif ch == "\x1b[C":
|
|
69
71
|
return KEY_RIGHT
|
|
70
|
-
elif ch ==
|
|
72
|
+
elif ch == "\x1b[D":
|
|
71
73
|
return KEY_LEFT
|
|
72
|
-
elif ch in (
|
|
74
|
+
elif ch in ("\x1b[5~", "\x1b[5"):
|
|
73
75
|
return KEY_PAGE_UP
|
|
74
|
-
elif ch in (
|
|
76
|
+
elif ch in ("\x1b[6~", "\x1b[6"):
|
|
75
77
|
return KEY_PAGE_DOWN
|
|
76
|
-
elif ch ==
|
|
78
|
+
elif ch == "\x1b":
|
|
77
79
|
# Got just escape - try to read more (arrow key sequence)
|
|
78
80
|
try:
|
|
79
81
|
ch2 = click.getchar()
|
|
80
|
-
if ch2 ==
|
|
82
|
+
if ch2 == "[":
|
|
81
83
|
ch3 = click.getchar()
|
|
82
|
-
if ch3 ==
|
|
84
|
+
if ch3 == "A":
|
|
83
85
|
return KEY_UP
|
|
84
|
-
elif ch3 ==
|
|
86
|
+
elif ch3 == "B":
|
|
85
87
|
return KEY_DOWN
|
|
86
|
-
elif ch3 ==
|
|
88
|
+
elif ch3 == "C":
|
|
87
89
|
return KEY_RIGHT
|
|
88
|
-
elif ch3 ==
|
|
90
|
+
elif ch3 == "D":
|
|
89
91
|
return KEY_LEFT
|
|
90
|
-
elif ch3 in (
|
|
92
|
+
elif ch3 in ("5", "6"):
|
|
91
93
|
click.getchar() # consume ~
|
|
92
|
-
return KEY_PAGE_UP if ch3 ==
|
|
94
|
+
return KEY_PAGE_UP if ch3 == "5" else KEY_PAGE_DOWN
|
|
93
95
|
# Unknown sequence, ignore it
|
|
94
|
-
return
|
|
96
|
+
return "" # Return empty to ignore
|
|
95
97
|
except Exception:
|
|
96
98
|
return KEY_ESCAPE
|
|
97
99
|
# Unknown escape sequence, ignore
|
|
98
|
-
return
|
|
100
|
+
return ""
|
|
99
101
|
return ch
|
|
100
102
|
except Exception:
|
|
101
103
|
pass
|
|
@@ -113,29 +115,29 @@ def _get_key() -> str:
|
|
|
113
115
|
ch = sys.stdin.read(1)
|
|
114
116
|
|
|
115
117
|
# Handle escape sequences (arrow keys, etc.)
|
|
116
|
-
if ch ==
|
|
118
|
+
if ch == "\x1b":
|
|
117
119
|
# Check if more characters are available (escape sequence)
|
|
118
120
|
if select.select([sys.stdin], [], [], 0.05)[0]:
|
|
119
121
|
ch2 = sys.stdin.read(1)
|
|
120
|
-
if ch2 ==
|
|
122
|
+
if ch2 == "[":
|
|
121
123
|
# CSI sequence - read the final byte
|
|
122
124
|
if select.select([sys.stdin], [], [], 0.05)[0]:
|
|
123
125
|
ch3 = sys.stdin.read(1)
|
|
124
126
|
# Map to our key constants
|
|
125
|
-
if ch3 ==
|
|
127
|
+
if ch3 == "A":
|
|
126
128
|
return KEY_UP
|
|
127
|
-
elif ch3 ==
|
|
129
|
+
elif ch3 == "B":
|
|
128
130
|
return KEY_DOWN
|
|
129
|
-
elif ch3 ==
|
|
131
|
+
elif ch3 == "C":
|
|
130
132
|
return KEY_RIGHT
|
|
131
|
-
elif ch3 ==
|
|
133
|
+
elif ch3 == "D":
|
|
132
134
|
return KEY_LEFT
|
|
133
|
-
elif ch3 ==
|
|
135
|
+
elif ch3 == "5":
|
|
134
136
|
# Page Up - consume the ~
|
|
135
137
|
if select.select([sys.stdin], [], [], 0.05)[0]:
|
|
136
138
|
sys.stdin.read(1)
|
|
137
139
|
return KEY_PAGE_UP
|
|
138
|
-
elif ch3 ==
|
|
140
|
+
elif ch3 == "6":
|
|
139
141
|
# Page Down - consume the ~
|
|
140
142
|
if select.select([sys.stdin], [], [], 0.05)[0]:
|
|
141
143
|
sys.stdin.read(1)
|
|
@@ -181,12 +183,12 @@ class InteractiveSelector:
|
|
|
181
183
|
"""
|
|
182
184
|
|
|
183
185
|
# Checkbox characters (using larger circles for visibility)
|
|
184
|
-
CHECKBOX_EMPTY =
|
|
185
|
-
CHECKBOX_CHECKED =
|
|
186
|
+
CHECKBOX_EMPTY = "○"
|
|
187
|
+
CHECKBOX_CHECKED = "●"
|
|
186
188
|
|
|
187
189
|
# Cursor indicator
|
|
188
|
-
CURSOR =
|
|
189
|
-
NO_CURSOR =
|
|
190
|
+
CURSOR = "▶"
|
|
191
|
+
NO_CURSOR = " "
|
|
190
192
|
|
|
191
193
|
def __init__(
|
|
192
194
|
self,
|
|
@@ -194,11 +196,11 @@ class InteractiveSelector:
|
|
|
194
196
|
columns: List[Dict[str, Any]],
|
|
195
197
|
selected_ids: Set[Any],
|
|
196
198
|
get_id: Callable[[Dict], Any],
|
|
197
|
-
title: str =
|
|
199
|
+
title: str = "SELECT ITEMS",
|
|
198
200
|
page_size: int = 20,
|
|
199
201
|
format_cell: Optional[Callable[[Dict, str], str]] = None,
|
|
200
202
|
show_header_info: Optional[Callable[[], str]] = None,
|
|
201
|
-
extra_actions: Optional[Dict[str, str]] = None
|
|
203
|
+
extra_actions: Optional[Dict[str, str]] = None,
|
|
202
204
|
):
|
|
203
205
|
"""
|
|
204
206
|
Initialize the interactive selector.
|
|
@@ -244,7 +246,7 @@ class InteractiveSelector:
|
|
|
244
246
|
The modified selected_ids set
|
|
245
247
|
"""
|
|
246
248
|
if not self.items:
|
|
247
|
-
click.echo(click.style(" No items to select.", fg=
|
|
249
|
+
click.echo(click.style(" No items to select.", fg="yellow"))
|
|
248
250
|
click.pause()
|
|
249
251
|
return self.selected_ids
|
|
250
252
|
|
|
@@ -266,7 +268,11 @@ class InteractiveSelector:
|
|
|
266
268
|
# Title
|
|
267
269
|
click.echo()
|
|
268
270
|
click.echo("┌" + "─" * (width - 2) + "┐")
|
|
269
|
-
click.echo(
|
|
271
|
+
click.echo(
|
|
272
|
+
"│"
|
|
273
|
+
+ click.style(f" {self.title} ".center(width - 2), bold=True, fg="cyan")
|
|
274
|
+
+ "│"
|
|
275
|
+
)
|
|
270
276
|
click.echo("└" + "─" * (width - 2) + "┘")
|
|
271
277
|
click.echo()
|
|
272
278
|
|
|
@@ -280,13 +286,15 @@ class InteractiveSelector:
|
|
|
280
286
|
# Stats
|
|
281
287
|
total = len(self.items)
|
|
282
288
|
selected_count = len(self.selected_ids)
|
|
283
|
-
click.echo(
|
|
284
|
-
|
|
289
|
+
click.echo(
|
|
290
|
+
f" {click.style('Total:', bold=True)} {total} items | "
|
|
291
|
+
f"{click.style('Selected:', bold=True, fg='cyan')} {selected_count}"
|
|
292
|
+
)
|
|
285
293
|
click.echo()
|
|
286
294
|
|
|
287
295
|
# Calculate visible items
|
|
288
296
|
page_end = min(self.page_start + self.page_size, len(self.items))
|
|
289
|
-
visible_items = self.items[self.page_start:page_end]
|
|
297
|
+
visible_items = self.items[self.page_start : page_end]
|
|
290
298
|
|
|
291
299
|
# Create table
|
|
292
300
|
table = Table(
|
|
@@ -294,7 +302,7 @@ class InteractiveSelector:
|
|
|
294
302
|
header_style="bold cyan",
|
|
295
303
|
box=DesignSystem.TABLE_BOX,
|
|
296
304
|
padding=(0, 1),
|
|
297
|
-
expand=True
|
|
305
|
+
expand=True,
|
|
298
306
|
)
|
|
299
307
|
|
|
300
308
|
# Add cursor column
|
|
@@ -305,11 +313,11 @@ class InteractiveSelector:
|
|
|
305
313
|
# Add user-defined columns
|
|
306
314
|
for col in self.columns:
|
|
307
315
|
table.add_column(
|
|
308
|
-
col[
|
|
309
|
-
width=col.get(
|
|
310
|
-
justify=col.get(
|
|
311
|
-
no_wrap=col.get(
|
|
312
|
-
style=col.get(
|
|
316
|
+
col["name"],
|
|
317
|
+
width=col.get("width"),
|
|
318
|
+
justify=col.get("justify", "left"),
|
|
319
|
+
no_wrap=col.get("no_wrap", False),
|
|
320
|
+
style=col.get("style"),
|
|
313
321
|
)
|
|
314
322
|
|
|
315
323
|
# Add rows
|
|
@@ -318,7 +326,7 @@ class InteractiveSelector:
|
|
|
318
326
|
item_id = self.get_id(item)
|
|
319
327
|
|
|
320
328
|
# Cursor indicator
|
|
321
|
-
is_cursor =
|
|
329
|
+
is_cursor = absolute_idx == self.cursor_pos
|
|
322
330
|
cursor = self.CURSOR if is_cursor else self.NO_CURSOR
|
|
323
331
|
|
|
324
332
|
# Checkbox
|
|
@@ -329,13 +337,13 @@ class InteractiveSelector:
|
|
|
329
337
|
row_values = [cursor, checkbox]
|
|
330
338
|
|
|
331
339
|
for col in self.columns:
|
|
332
|
-
key = col[
|
|
340
|
+
key = col["key"]
|
|
333
341
|
if self.format_cell:
|
|
334
342
|
value = self.format_cell(item, key)
|
|
335
|
-
elif
|
|
336
|
-
value = col[
|
|
343
|
+
elif "format" in col:
|
|
344
|
+
value = col["format"](item.get(key, ""))
|
|
337
345
|
else:
|
|
338
|
-
value = str(item.get(key,
|
|
346
|
+
value = str(item.get(key, "-") or "-")
|
|
339
347
|
row_values.append(value)
|
|
340
348
|
|
|
341
349
|
# Apply highlight style for cursor row
|
|
@@ -350,8 +358,7 @@ class InteractiveSelector:
|
|
|
350
358
|
if len(self.items) > self.page_size:
|
|
351
359
|
page_num = (self.page_start // self.page_size) + 1
|
|
352
360
|
total_pages = (len(self.items) + self.page_size - 1) // self.page_size
|
|
353
|
-
click.echo(f"\n Page {page_num}/{total_pages} | "
|
|
354
|
-
f"p/n: Prev/Next page")
|
|
361
|
+
click.echo(f"\n Page {page_num}/{total_pages} | " f"p/n: Prev/Next page")
|
|
355
362
|
|
|
356
363
|
# Help bar
|
|
357
364
|
click.echo()
|
|
@@ -375,7 +382,7 @@ class InteractiveSelector:
|
|
|
375
382
|
def _handle_key(self, key: str):
|
|
376
383
|
"""Handle a keypress."""
|
|
377
384
|
# Navigation - Up
|
|
378
|
-
if key in (KEY_UP,
|
|
385
|
+
if key in (KEY_UP, "k"):
|
|
379
386
|
if self.cursor_pos > 0:
|
|
380
387
|
self.cursor_pos -= 1
|
|
381
388
|
# Scroll up if needed
|
|
@@ -383,7 +390,7 @@ class InteractiveSelector:
|
|
|
383
390
|
self.page_start = max(0, self.page_start - self.page_size)
|
|
384
391
|
|
|
385
392
|
# Navigation - Down
|
|
386
|
-
elif key in (KEY_DOWN,
|
|
393
|
+
elif key in (KEY_DOWN, "j"):
|
|
387
394
|
if self.cursor_pos < len(self.items) - 1:
|
|
388
395
|
self.cursor_pos += 1
|
|
389
396
|
# Scroll down if needed
|
|
@@ -391,14 +398,14 @@ class InteractiveSelector:
|
|
|
391
398
|
self.page_start += self.page_size
|
|
392
399
|
|
|
393
400
|
# Page Up / Previous page
|
|
394
|
-
elif key in (KEY_PAGE_UP,
|
|
401
|
+
elif key in (KEY_PAGE_UP, "p", "[", "<"):
|
|
395
402
|
current_page = self.page_start // self.page_size
|
|
396
403
|
if current_page > 0:
|
|
397
404
|
self.page_start = (current_page - 1) * self.page_size
|
|
398
405
|
self.cursor_pos = self.page_start
|
|
399
406
|
|
|
400
407
|
# Page Down / Next page
|
|
401
|
-
elif key in (KEY_PAGE_DOWN,
|
|
408
|
+
elif key in (KEY_PAGE_DOWN, "n", "]", ">"):
|
|
402
409
|
total_pages = (len(self.items) + self.page_size - 1) // self.page_size
|
|
403
410
|
current_page = self.page_start // self.page_size
|
|
404
411
|
if current_page < total_pages - 1:
|
|
@@ -420,18 +427,18 @@ class InteractiveSelector:
|
|
|
420
427
|
self.selected_ids.add(item_id)
|
|
421
428
|
|
|
422
429
|
# Select all
|
|
423
|
-
elif key ==
|
|
430
|
+
elif key == "a":
|
|
424
431
|
for item in self.items:
|
|
425
432
|
self.selected_ids.add(self.get_id(item))
|
|
426
433
|
|
|
427
434
|
# Select none / Unselect all
|
|
428
|
-
elif key ==
|
|
435
|
+
elif key == "u":
|
|
429
436
|
# Only clear items that are in current list
|
|
430
437
|
current_ids = {self.get_id(item) for item in self.items}
|
|
431
438
|
self.selected_ids -= current_ids
|
|
432
439
|
|
|
433
440
|
# Exit - q, Enter, or Escape
|
|
434
|
-
elif key in (KEY_ESCAPE, KEY_ENTER,
|
|
441
|
+
elif key in (KEY_ESCAPE, KEY_ENTER, "q", "\x03", "\r", "\n"): # \x03 is Ctrl+C
|
|
435
442
|
self.exit_key = key
|
|
436
443
|
self.running = False
|
|
437
444
|
|
|
@@ -446,8 +453,8 @@ def interactive_select(
|
|
|
446
453
|
columns: List[Dict[str, Any]],
|
|
447
454
|
selected_ids: Set[Any],
|
|
448
455
|
get_id: Callable[[Dict], Any],
|
|
449
|
-
title: str =
|
|
450
|
-
**kwargs
|
|
456
|
+
title: str = "SELECT ITEMS",
|
|
457
|
+
**kwargs,
|
|
451
458
|
) -> Set[Any]:
|
|
452
459
|
"""
|
|
453
460
|
Convenience function to run an interactive selector.
|
|
@@ -469,6 +476,6 @@ def interactive_select(
|
|
|
469
476
|
selected_ids=selected_ids,
|
|
470
477
|
get_id=get_id,
|
|
471
478
|
title=title,
|
|
472
|
-
**kwargs
|
|
479
|
+
**kwargs,
|
|
473
480
|
)
|
|
474
481
|
return selector.run()
|
souleyez/ui/log_formatter.py
CHANGED
|
@@ -11,72 +11,80 @@ from typing import List, Optional
|
|
|
11
11
|
def format_json_log_line(line: str) -> Optional[str]:
|
|
12
12
|
"""
|
|
13
13
|
Format a JSON log line into human-readable format.
|
|
14
|
-
|
|
14
|
+
|
|
15
15
|
Args:
|
|
16
16
|
line: Raw JSON log line
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
Returns:
|
|
19
19
|
Formatted string or None if not JSON
|
|
20
20
|
"""
|
|
21
21
|
try:
|
|
22
22
|
data = json.loads(line.strip())
|
|
23
|
-
|
|
23
|
+
|
|
24
24
|
# JSON can parse primitives (numbers, strings, booleans) - we only want objects
|
|
25
25
|
if not isinstance(data, dict):
|
|
26
26
|
return None
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
# Extract common fields
|
|
29
|
-
timestamp = data.get(
|
|
30
|
-
level = data.get(
|
|
31
|
-
name = data.get(
|
|
32
|
-
message = data.get(
|
|
33
|
-
|
|
29
|
+
timestamp = data.get("timestamp", "")
|
|
30
|
+
level = data.get("levelname", "INFO")
|
|
31
|
+
name = data.get("name", "")
|
|
32
|
+
message = data.get("message", "")
|
|
33
|
+
|
|
34
34
|
# Parse timestamp
|
|
35
35
|
if timestamp:
|
|
36
36
|
try:
|
|
37
|
-
dt = datetime.fromisoformat(timestamp.replace(
|
|
38
|
-
time_str = dt.strftime(
|
|
37
|
+
dt = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
|
|
38
|
+
time_str = dt.strftime("%H:%M:%S")
|
|
39
39
|
except:
|
|
40
40
|
time_str = timestamp[:8] if len(timestamp) > 8 else timestamp
|
|
41
41
|
else:
|
|
42
|
-
time_str =
|
|
43
|
-
|
|
42
|
+
time_str = "??:??:??"
|
|
43
|
+
|
|
44
44
|
# Color codes for log levels
|
|
45
45
|
level_colors = {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
"DEBUG": "\033[36m", # Cyan
|
|
47
|
+
"INFO": "\033[32m", # Green
|
|
48
|
+
"WARNING": "\033[33m", # Yellow
|
|
49
|
+
"ERROR": "\033[31m", # Red
|
|
50
|
+
"CRITICAL": "\033[35m", # Magenta
|
|
51
51
|
}
|
|
52
|
-
reset =
|
|
53
|
-
|
|
54
|
-
level_color = level_colors.get(level,
|
|
52
|
+
reset = "\033[0m"
|
|
53
|
+
|
|
54
|
+
level_color = level_colors.get(level, "")
|
|
55
55
|
level_short = level[0] # D, I, W, E, C
|
|
56
|
-
|
|
56
|
+
|
|
57
57
|
# Build formatted line
|
|
58
58
|
formatted = f"{time_str} {level_color}[{level_short}]{reset} "
|
|
59
|
-
|
|
59
|
+
|
|
60
60
|
# Add module name if not too long
|
|
61
61
|
if name and len(name) < 30:
|
|
62
|
-
module = name.split(
|
|
62
|
+
module = name.split(".")[-1] # Last part only
|
|
63
63
|
formatted += f"{module}: "
|
|
64
|
-
|
|
64
|
+
|
|
65
65
|
formatted += message
|
|
66
|
-
|
|
66
|
+
|
|
67
67
|
# Add interesting extra fields
|
|
68
68
|
extras = []
|
|
69
|
-
skip_fields = {
|
|
69
|
+
skip_fields = {
|
|
70
|
+
"timestamp",
|
|
71
|
+
"levelname",
|
|
72
|
+
"name",
|
|
73
|
+
"message",
|
|
74
|
+
"log_file",
|
|
75
|
+
"log_level",
|
|
76
|
+
"log_format",
|
|
77
|
+
}
|
|
70
78
|
for key, value in data.items():
|
|
71
79
|
if key not in skip_fields and value:
|
|
72
80
|
if isinstance(value, (str, int, float, bool)):
|
|
73
81
|
extras.append(f"{key}={value}")
|
|
74
|
-
|
|
82
|
+
|
|
75
83
|
if extras:
|
|
76
84
|
formatted += f" ({', '.join(extras[:3])})" # Limit to 3 extras
|
|
77
|
-
|
|
85
|
+
|
|
78
86
|
return formatted
|
|
79
|
-
|
|
87
|
+
|
|
80
88
|
except (json.JSONDecodeError, KeyError):
|
|
81
89
|
# Not JSON or malformed
|
|
82
90
|
return None
|
|
@@ -85,20 +93,20 @@ def format_json_log_line(line: str) -> Optional[str]:
|
|
|
85
93
|
def format_log_stream(lines: List[str], max_lines: int = 50) -> List[str]:
|
|
86
94
|
"""
|
|
87
95
|
Format a stream of log lines (mix of JSON and plain text).
|
|
88
|
-
|
|
96
|
+
|
|
89
97
|
Args:
|
|
90
98
|
lines: List of raw log lines
|
|
91
99
|
max_lines: Maximum lines to return
|
|
92
|
-
|
|
100
|
+
|
|
93
101
|
Returns:
|
|
94
102
|
List of formatted lines
|
|
95
103
|
"""
|
|
96
104
|
formatted = []
|
|
97
|
-
|
|
105
|
+
|
|
98
106
|
for line in lines[-max_lines:]:
|
|
99
107
|
if not line.strip():
|
|
100
108
|
continue
|
|
101
|
-
|
|
109
|
+
|
|
102
110
|
# Try JSON formatting first
|
|
103
111
|
json_formatted = format_json_log_line(line)
|
|
104
112
|
if json_formatted:
|
|
@@ -106,25 +114,25 @@ def format_log_stream(lines: List[str], max_lines: int = 50) -> List[str]:
|
|
|
106
114
|
else:
|
|
107
115
|
# Keep plain text as-is (tool output, etc.)
|
|
108
116
|
formatted.append(line.rstrip())
|
|
109
|
-
|
|
117
|
+
|
|
110
118
|
return formatted
|
|
111
119
|
|
|
112
120
|
|
|
113
121
|
def tail_and_format_log(log_path: str, num_lines: int = 50) -> List[str]:
|
|
114
122
|
"""
|
|
115
123
|
Read last N lines from log file and format them.
|
|
116
|
-
|
|
124
|
+
|
|
117
125
|
Args:
|
|
118
126
|
log_path: Path to log file
|
|
119
127
|
num_lines: Number of lines to read
|
|
120
|
-
|
|
128
|
+
|
|
121
129
|
Returns:
|
|
122
130
|
List of formatted lines
|
|
123
131
|
"""
|
|
124
132
|
try:
|
|
125
|
-
with open(log_path,
|
|
133
|
+
with open(log_path, "r", encoding="utf-8", errors="replace") as f:
|
|
126
134
|
lines = f.readlines()
|
|
127
|
-
|
|
135
|
+
|
|
128
136
|
return format_log_stream(lines, max_lines=num_lines)
|
|
129
137
|
except Exception as e:
|
|
130
138
|
return [f"Error reading log: {e}"]
|
souleyez/ui/menu_components.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Reusable menu UI components for consistent layouts.
|
|
3
3
|
"""
|
|
4
|
+
|
|
4
5
|
import click
|
|
5
6
|
from typing import List, Dict
|
|
6
7
|
from souleyez.ui.design_system import DesignSystem
|
|
@@ -10,7 +11,13 @@ class StandardMenu:
|
|
|
10
11
|
"""Standard menu renderer with consistent formatting."""
|
|
11
12
|
|
|
12
13
|
@staticmethod
|
|
13
|
-
def render(
|
|
14
|
+
def render(
|
|
15
|
+
options: List[Dict[str, str]],
|
|
16
|
+
show_back: bool = True,
|
|
17
|
+
shortcuts: Dict[str, int] = None,
|
|
18
|
+
show_shortcuts: bool = True,
|
|
19
|
+
tip: str = None,
|
|
20
|
+
):
|
|
14
21
|
"""
|
|
15
22
|
Render a standardized menu with single OPTIONS section.
|
|
16
23
|
|
|
@@ -29,14 +36,14 @@ class StandardMenu:
|
|
|
29
36
|
width = DesignSystem.get_terminal_width()
|
|
30
37
|
|
|
31
38
|
click.echo()
|
|
32
|
-
click.echo(click.style("⚙️ OPTIONS", bold=True, fg=
|
|
39
|
+
click.echo(click.style("⚙️ OPTIONS", bold=True, fg="cyan"))
|
|
33
40
|
click.echo("─" * width)
|
|
34
41
|
click.echo()
|
|
35
42
|
|
|
36
43
|
for opt in options:
|
|
37
|
-
number = opt[
|
|
38
|
-
label = opt[
|
|
39
|
-
desc = opt.get(
|
|
44
|
+
number = opt["number"]
|
|
45
|
+
label = opt["label"]
|
|
46
|
+
desc = opt.get("description", "")
|
|
40
47
|
|
|
41
48
|
if desc:
|
|
42
49
|
click.echo(f" [{number}] {label} - {desc}")
|
|
@@ -56,20 +63,20 @@ class StandardMenu:
|
|
|
56
63
|
shortcut_hints = []
|
|
57
64
|
for key, option_num in shortcuts.items():
|
|
58
65
|
# Map common shortcuts to their actions
|
|
59
|
-
if key ==
|
|
66
|
+
if key == "n":
|
|
60
67
|
shortcut_hints.append(f"'{key}' = Next Page")
|
|
61
|
-
elif key ==
|
|
68
|
+
elif key == "p":
|
|
62
69
|
shortcut_hints.append(f"'{key}' = Previous Page")
|
|
63
|
-
elif key ==
|
|
70
|
+
elif key == ">":
|
|
64
71
|
shortcut_hints.append("'>' = Next Page")
|
|
65
|
-
elif key ==
|
|
72
|
+
elif key == "<":
|
|
66
73
|
shortcut_hints.append("'<' = Previous Page")
|
|
67
|
-
elif key ==
|
|
74
|
+
elif key == "?":
|
|
68
75
|
shortcut_hints.append("'?' = Help")
|
|
69
76
|
else:
|
|
70
77
|
# For other shortcuts, try to find the label
|
|
71
78
|
for opt in options:
|
|
72
|
-
if opt[
|
|
79
|
+
if opt["number"] == option_num:
|
|
73
80
|
shortcut_hints.append(f"'{key}' = {opt['label']}")
|
|
74
81
|
break
|
|
75
82
|
if shortcut_hints:
|
|
@@ -78,12 +85,14 @@ class StandardMenu:
|
|
|
78
85
|
# Show tip if provided
|
|
79
86
|
if tip:
|
|
80
87
|
click.echo()
|
|
81
|
-
click.echo(" " + click.style("💡 TIP:", fg=
|
|
88
|
+
click.echo(" " + click.style("💡 TIP:", fg="cyan", bold=True) + " " + tip)
|
|
82
89
|
|
|
83
90
|
click.echo()
|
|
84
91
|
|
|
85
92
|
try:
|
|
86
|
-
choice_input = click.prompt(
|
|
93
|
+
choice_input = click.prompt(
|
|
94
|
+
"Select option", type=str, default="0", show_default=False
|
|
95
|
+
)
|
|
87
96
|
|
|
88
97
|
# Handle keyboard shortcuts
|
|
89
98
|
if shortcuts and choice_input.lower() in shortcuts:
|