pentool 1.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.
- pentool/__init__.py +4 -0
- pentool/__main__.py +17 -0
- pentool/api/__init__.py +45 -0
- pentool/api/base_api.py +52 -0
- pentool/api/comparer_api.py +23 -0
- pentool/api/decoder_api.py +28 -0
- pentool/api/intruder_api.py +209 -0
- pentool/api/proxy_api.py +319 -0
- pentool/api/repeater_api.py +118 -0
- pentool/api/scanner_api.py +336 -0
- pentool/api/sequencer_api.py +21 -0
- pentool/api/spider_api.py +126 -0
- pentool/api/target_api.py +121 -0
- pentool/cli/__init__.py +1 -0
- pentool/cli/main.py +100 -0
- pentool/cli/project.py +90 -0
- pentool/cli/proxy.py +185 -0
- pentool/cli/scan.py +118 -0
- pentool/core/__init__.py +1 -0
- pentool/core/config.py +184 -0
- pentool/core/crash_reporter.py +75 -0
- pentool/core/database.py +139 -0
- pentool/core/event_bus.py +232 -0
- pentool/core/events.py +164 -0
- pentool/core/features.py +410 -0
- pentool/core/license.py +294 -0
- pentool/core/logging.py +59 -0
- pentool/core/plugin_manager.py +332 -0
- pentool/core/project.py +37 -0
- pentool/core/storage_interface.py +311 -0
- pentool/core/updater.py +96 -0
- pentool/core/utils.py +61 -0
- pentool/modules/__init__.py +1 -0
- pentool/modules/comparer.py +177 -0
- pentool/modules/decoder.py +281 -0
- pentool/modules/intruder.py +432 -0
- pentool/modules/intruder_turbo.py +190 -0
- pentool/modules/match_replace.py +146 -0
- pentool/modules/proxy.py +803 -0
- pentool/modules/repeater.py +237 -0
- pentool/modules/scanner/__init__.py +7 -0
- pentool/modules/scanner/ai_analyzer.py +230 -0
- pentool/modules/scanner/base.py +173 -0
- pentool/modules/scanner/baseline.py +101 -0
- pentool/modules/scanner/checks/__init__.py +47 -0
- pentool/modules/scanner/checks/broken_auth.py +231 -0
- pentool/modules/scanner/checks/cors.py +209 -0
- pentool/modules/scanner/checks/dom_xss.py +114 -0
- pentool/modules/scanner/checks/graphql.py +110 -0
- pentool/modules/scanner/checks/header_injection.py +262 -0
- pentool/modules/scanner/checks/headers.py +75 -0
- pentool/modules/scanner/checks/helpers.py +358 -0
- pentool/modules/scanner/checks/info_leak.py +68 -0
- pentool/modules/scanner/checks/jwt_none.py +238 -0
- pentool/modules/scanner/checks/lfi.py +186 -0
- pentool/modules/scanner/checks/nosql_injection.py +92 -0
- pentool/modules/scanner/checks/oauth.py +123 -0
- pentool/modules/scanner/checks/open_redirect.py +168 -0
- pentool/modules/scanner/checks/path_traversal.py +229 -0
- pentool/modules/scanner/checks/prototype_pollution.py +88 -0
- pentool/modules/scanner/checks/rce.py +338 -0
- pentool/modules/scanner/checks/sensitive_data.py +74 -0
- pentool/modules/scanner/checks/sqli.py +546 -0
- pentool/modules/scanner/checks/ssrf.py +273 -0
- pentool/modules/scanner/checks/ssti.py +353 -0
- pentool/modules/scanner/checks/xss.py +622 -0
- pentool/modules/scanner/checks/xxe.py +240 -0
- pentool/modules/scanner/engine.py +407 -0
- pentool/modules/scanner/fingerprint.py +169 -0
- pentool/modules/scanner/helpers.py +94 -0
- pentool/modules/scanner/mutator.py +378 -0
- pentool/modules/scanner/oob.py +104 -0
- pentool/modules/scanner/passive.py +114 -0
- pentool/modules/scanner/payloads.py +224 -0
- pentool/modules/scanner/report.py +117 -0
- pentool/modules/sequencer.py +500 -0
- pentool/modules/spider.py +702 -0
- pentool/modules/target.py +190 -0
- pentool/modules/websocket_handler.py +259 -0
- pentool/plugins/__init__.py +7 -0
- pentool/plugins/builtin/__init__.py +1 -0
- pentool/plugins/builtin/payloads_pro.py +404 -0
- pentool/plugins/builtin/reports_pro.py +450 -0
- pentool/plugins/builtin/scanner_pro.py +308 -0
- pentool/plugins/example_plugin.py +75 -0
- pentool/project_to_txt.py +67 -0
- pentool/services/__init__.py +1 -0
- pentool/services/base_service.py +64 -0
- pentool/services/intruder_service.py +108 -0
- pentool/services/proxy_service.py +232 -0
- pentool/services/repeater_service.py +111 -0
- pentool/services/scan_service.py +340 -0
- pentool/storage/__init__.py +0 -0
- pentool/storage/http_storage.py +522 -0
- pentool/storage/large_body_handler.py +54 -0
- pentool/storage/lru_cache.py +49 -0
- pentool/t.py +213 -0
- pentool/tui/__init__.py +1 -0
- pentool/tui/app.py +1438 -0
- pentool/tui/constants.py +79 -0
- pentool/tui/dialogs/__init__.py +0 -0
- pentool/tui/dialogs/cert_dialog.py +81 -0
- pentool/tui/dialogs/file_selector.py +227 -0
- pentool/tui/dialogs/load_from_proxy.py +125 -0
- pentool/tui/dialogs/match_replace_dialog.py +229 -0
- pentool/tui/dialogs/project_dialog.py +82 -0
- pentool/tui/dialogs/scope_dialog.py +225 -0
- pentool/tui/messages.py +121 -0
- pentool/tui/mixins/__init__.py +0 -0
- pentool/tui/mixins/app_mixin.py +61 -0
- pentool/tui/mixins/dialog_cancel.py +25 -0
- pentool/tui/mixins/request_context_menu.py +285 -0
- pentool/tui/mixins/tab_rename.py +101 -0
- pentool/tui/screens/__init__.py +31 -0
- pentool/tui/screens/comparer/__init__.py +3 -0
- pentool/tui/screens/comparer/screen.py +233 -0
- pentool/tui/screens/dashboard/__init__.py +2 -0
- pentool/tui/screens/dashboard/live_dashboard.py +741 -0
- pentool/tui/screens/dashboard/screen.py +820 -0
- pentool/tui/screens/decoder/__init__.py +3 -0
- pentool/tui/screens/decoder/screen.py +311 -0
- pentool/tui/screens/extensions/__init__.py +3 -0
- pentool/tui/screens/extensions/screen.py +24 -0
- pentool/tui/screens/intruder/__init__.py +3 -0
- pentool/tui/screens/intruder/screen.py +1362 -0
- pentool/tui/screens/proxy/__init__.py +3 -0
- pentool/tui/screens/proxy/screen.py +1664 -0
- pentool/tui/screens/repeater/__init__.py +3 -0
- pentool/tui/screens/repeater/screen.py +646 -0
- pentool/tui/screens/scanner/__init__.py +3 -0
- pentool/tui/screens/scanner/screen.py +1984 -0
- pentool/tui/screens/sequencer/__init__.py +3 -0
- pentool/tui/screens/sequencer/screen.py +520 -0
- pentool/tui/screens/settings/__init__.py +5 -0
- pentool/tui/screens/settings/hotkeys.py +134 -0
- pentool/tui/screens/settings/screen.py +543 -0
- pentool/tui/screens/spider/__init__.py +3 -0
- pentool/tui/screens/spider/screen.py +385 -0
- pentool/tui/screens/target/__init__.py +4 -0
- pentool/tui/screens/target/screen.py +361 -0
- pentool/tui/screens/terminal/__init__.py +5 -0
- pentool/tui/screens/terminal/screen.py +181 -0
- pentool/tui/widgets/__init__.py +1 -0
- pentool/tui/widgets/context_menu.py +148 -0
- pentool/tui/widgets/filter_bar.py +206 -0
- pentool/tui/widgets/inspector_panel.py +112 -0
- pentool/tui/widgets/menu.py +86 -0
- pentool/tui/widgets/menu_bar.py +238 -0
- pentool/tui/widgets/module_tabs.py +73 -0
- pentool/tui/widgets/payload_drop_zone.py +66 -0
- pentool/tui/widgets/request_editor.py +504 -0
- pentool/tui/widgets/resize_handle.py +116 -0
- pentool/tui/widgets/search_bar.py +113 -0
- pentool/tui/widgets/statusbar.py +99 -0
- pentool/tui/widgets/toolbar_button.py +76 -0
- pentool/utils/__init__.py +1 -0
- pentool/utils/cert.py +361 -0
- pentool/utils/coder.py +170 -0
- pentool/utils/copy_as.py +251 -0
- pentool/utils/diff.py +91 -0
- pentool/utils/http_client.py +175 -0
- pentool/utils/parser.py +227 -0
- pentool/utils/terminal_check.py +32 -0
- pentool-1.0.0.dist-info/METADATA +155 -0
- pentool-1.0.0.dist-info/RECORD +169 -0
- pentool-1.0.0.dist-info/WHEEL +5 -0
- pentool-1.0.0.dist-info/entry_points.txt +2 -0
- pentool-1.0.0.dist-info/licenses/LICENSE +34 -0
- pentool-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"""Экран Decoder/Encoder — кодирование, декодирование, хэширование."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from textual import on
|
|
7
|
+
from textual.app import ComposeResult
|
|
8
|
+
from textual.binding import Binding
|
|
9
|
+
from textual.containers import Horizontal, Vertical
|
|
10
|
+
from textual.widget import Widget
|
|
11
|
+
from textual.widgets import Label, RichLog, Static, TextArea
|
|
12
|
+
|
|
13
|
+
from pentool.tui.widgets.toolbar_button import ToolbarButton
|
|
14
|
+
from pentool.tui.widgets.resize_handle import ResizeHandle
|
|
15
|
+
from pentool.core.logging import get_logger
|
|
16
|
+
|
|
17
|
+
logger = get_logger(__name__)
|
|
18
|
+
|
|
19
|
+
_CSS = (Path(__file__).parent / "screen.tcss").read_text(encoding="utf-8")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DecoderScreen(Widget):
|
|
23
|
+
"""Encoder / Decoder с цепочками операций."""
|
|
24
|
+
|
|
25
|
+
DEFAULT_CSS = _CSS
|
|
26
|
+
|
|
27
|
+
BINDINGS = [
|
|
28
|
+
Binding("ctrl+enter", "run_chain", "Run", show=True),
|
|
29
|
+
Binding("ctrl+l", "clear_all", "Clear", show=False),
|
|
30
|
+
Binding("ctrl+c", "copy_result","Copy", show=False),
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
def __init__(self, **kwargs) -> None:
|
|
34
|
+
super().__init__(**kwargs)
|
|
35
|
+
from pentool.api.decoder_api import OP_LABELS
|
|
36
|
+
self._op_labels: list[str] = OP_LABELS
|
|
37
|
+
self._selected_op: str = OP_LABELS[0] # текущая выбранная операция
|
|
38
|
+
self._chain: list[str] = [] # список операций в цепочке
|
|
39
|
+
|
|
40
|
+
def compose(self) -> ComposeResult:
|
|
41
|
+
from pentool.api.decoder_api import OP_LABELS
|
|
42
|
+
|
|
43
|
+
# ── Тулбар ─────────────────────────────────────────────────────────────
|
|
44
|
+
with Horizontal(id="dec-toolbar"):
|
|
45
|
+
yield ToolbarButton("▶ Run", "btn-dec-run")
|
|
46
|
+
yield Static(" │ ", classes="toolbar-sep")
|
|
47
|
+
yield ToolbarButton("+ Add Step", "btn-dec-add")
|
|
48
|
+
yield Static(" │ ", classes="toolbar-sep")
|
|
49
|
+
yield ToolbarButton("✗ Clear Chain","btn-dec-clrchain")
|
|
50
|
+
yield Static(" │ ", classes="toolbar-sep")
|
|
51
|
+
yield ToolbarButton("⇅ Swap I/O", "btn-dec-swap")
|
|
52
|
+
yield Static(" │ ", classes="toolbar-sep")
|
|
53
|
+
yield ToolbarButton("📋 Copy", "btn-dec-copy")
|
|
54
|
+
yield Static(" │ ", classes="toolbar-sep")
|
|
55
|
+
yield ToolbarButton("🔍 Smart", "btn-dec-smart")
|
|
56
|
+
|
|
57
|
+
# ── Operation selector (добавление шагов) ──────────────────────────────
|
|
58
|
+
with Horizontal(id="dec-op-row"):
|
|
59
|
+
yield Label("Operation:", id="dec-op-label")
|
|
60
|
+
yield ToolbarButton(f"{self._selected_op} ▼", "btn-dec-op-select")
|
|
61
|
+
yield Label(" Chain:", id="dec-chain-label")
|
|
62
|
+
yield Static("(empty)", id="dec-chain-display")
|
|
63
|
+
|
|
64
|
+
# ── Рабочая область ─────────────────────────────────────────────────────
|
|
65
|
+
with Horizontal(id="dec-work-area"):
|
|
66
|
+
# Ввод
|
|
67
|
+
with Vertical(id="dec-input-col"):
|
|
68
|
+
yield Static("Input", id="dec-input-label", classes="dec-col-label")
|
|
69
|
+
yield TextArea(id="dec-input", language=None)
|
|
70
|
+
|
|
71
|
+
yield ResizeHandle("dec-input-col", "dec-output-col", id="dec-resize-h")
|
|
72
|
+
|
|
73
|
+
# Вывод
|
|
74
|
+
with Vertical(id="dec-output-col"):
|
|
75
|
+
yield Static("Output", id="dec-output-label", classes="dec-col-label")
|
|
76
|
+
yield TextArea(id="dec-output", language=None)
|
|
77
|
+
|
|
78
|
+
yield ResizeHandle("dec-work-area", "dec-steps-area", vertical=True,
|
|
79
|
+
id="dec-resize-v")
|
|
80
|
+
|
|
81
|
+
# ── Лог шагов цепочки ─────────────────────────────────────────────────
|
|
82
|
+
with Vertical(id="dec-steps-area"):
|
|
83
|
+
yield Static("Chain steps", id="dec-steps-label", classes="dec-col-label")
|
|
84
|
+
yield RichLog(id="dec-steps-log", highlight=True, markup=True,
|
|
85
|
+
wrap=True, max_lines=200)
|
|
86
|
+
|
|
87
|
+
yield Static(
|
|
88
|
+
"Ctrl+Enter: Run │ + Add Step: add operation to chain │ ⇅ Swap: swap Input/Output"
|
|
89
|
+
" │ 📋 Copy: copy result │ 🔍 Smart: auto-detect encoding",
|
|
90
|
+
id="status-bar",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# ── Toolbar actions ────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
@on(ToolbarButton.Pressed, "#btn-dec-run")
|
|
96
|
+
def on_btn_dec_run(self, _: ToolbarButton.Pressed) -> None:
|
|
97
|
+
self.action_run_chain()
|
|
98
|
+
|
|
99
|
+
@on(ToolbarButton.Pressed, "#btn-dec-add")
|
|
100
|
+
def on_btn_dec_add(self, _: ToolbarButton.Pressed) -> None:
|
|
101
|
+
self._add_step()
|
|
102
|
+
|
|
103
|
+
@on(ToolbarButton.Pressed, "#btn-dec-op-select")
|
|
104
|
+
def on_btn_dec_op_select(self, event: ToolbarButton.Pressed) -> None:
|
|
105
|
+
self._open_op_menu(event.button)
|
|
106
|
+
|
|
107
|
+
@on(ToolbarButton.Pressed, "#btn-dec-clrchain")
|
|
108
|
+
def on_btn_dec_clrchain(self, _: ToolbarButton.Pressed) -> None:
|
|
109
|
+
self._clear_chain()
|
|
110
|
+
|
|
111
|
+
@on(ToolbarButton.Pressed, "#btn-dec-swap")
|
|
112
|
+
def on_btn_dec_swap(self, _: ToolbarButton.Pressed) -> None:
|
|
113
|
+
self._swap_io()
|
|
114
|
+
|
|
115
|
+
@on(ToolbarButton.Pressed, "#btn-dec-copy")
|
|
116
|
+
def on_btn_dec_copy(self, _: ToolbarButton.Pressed) -> None:
|
|
117
|
+
self.action_copy_result()
|
|
118
|
+
|
|
119
|
+
@on(ToolbarButton.Pressed, "#btn-dec-smart")
|
|
120
|
+
def on_btn_dec_smart(self, _: ToolbarButton.Pressed) -> None:
|
|
121
|
+
self._smart_decode()
|
|
122
|
+
|
|
123
|
+
def _open_op_menu(self, btn: ToolbarButton) -> None:
|
|
124
|
+
"""Открыть контекстное меню выбора операции."""
|
|
125
|
+
items = [
|
|
126
|
+
(op, ("✓ " if op == self._selected_op else " ") + op)
|
|
127
|
+
for op in self._op_labels
|
|
128
|
+
]
|
|
129
|
+
r = btn.region
|
|
130
|
+
self.app.show_context_menu(items, r.x, r.y + 1, callback=self._on_op_selected)
|
|
131
|
+
|
|
132
|
+
def _on_op_selected(self, op: str) -> None:
|
|
133
|
+
self._selected_op = op
|
|
134
|
+
try:
|
|
135
|
+
btn = self.query_one("#btn-dec-op-select", ToolbarButton)
|
|
136
|
+
btn.label = f"{op} ▼"
|
|
137
|
+
except Exception:
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
def _add_step(self) -> None:
|
|
141
|
+
"""Добавить выбранную операцию в цепочку."""
|
|
142
|
+
try:
|
|
143
|
+
op = self._selected_op
|
|
144
|
+
if op:
|
|
145
|
+
self._chain.append(op)
|
|
146
|
+
self._update_chain_display()
|
|
147
|
+
except Exception as exc:
|
|
148
|
+
logger.debug("_add_step: %s", exc)
|
|
149
|
+
|
|
150
|
+
def _clear_chain(self) -> None:
|
|
151
|
+
self._chain.clear()
|
|
152
|
+
self._update_chain_display()
|
|
153
|
+
try:
|
|
154
|
+
self.query_one("#dec-steps-log", RichLog).clear()
|
|
155
|
+
except Exception:
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
def _swap_io(self) -> None:
|
|
159
|
+
try:
|
|
160
|
+
inp = self.query_one("#dec-input", TextArea)
|
|
161
|
+
out = self.query_one("#dec-output", TextArea)
|
|
162
|
+
inp_text = inp.text
|
|
163
|
+
inp.load_text(out.text)
|
|
164
|
+
out.load_text(inp_text)
|
|
165
|
+
except Exception as exc:
|
|
166
|
+
logger.debug("_swap_io: %s", exc)
|
|
167
|
+
|
|
168
|
+
def _smart_decode(self) -> None:
|
|
169
|
+
"""Автоопределение и цепочное декодирование ввода."""
|
|
170
|
+
try:
|
|
171
|
+
from pentool.api.decoder_api import decode_smart
|
|
172
|
+
from pentool.modules.decoder import _detect_encoding, encode_op
|
|
173
|
+
inp = self.query_one("#dec-input", TextArea)
|
|
174
|
+
text = inp.text.strip()
|
|
175
|
+
if not text:
|
|
176
|
+
self.app.notify("Input is empty", severity="warning")
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
# Run chain and collect steps for visualization
|
|
180
|
+
current = text
|
|
181
|
+
chain: list[str] = []
|
|
182
|
+
steps: list[str] = [current]
|
|
183
|
+
for _ in range(8):
|
|
184
|
+
op = _detect_encoding(current)
|
|
185
|
+
if op is None:
|
|
186
|
+
break
|
|
187
|
+
try:
|
|
188
|
+
nxt = encode_op(op, current)
|
|
189
|
+
if nxt == current:
|
|
190
|
+
break
|
|
191
|
+
chain.append(op)
|
|
192
|
+
current = nxt
|
|
193
|
+
steps.append(current)
|
|
194
|
+
except Exception:
|
|
195
|
+
break
|
|
196
|
+
|
|
197
|
+
result = current
|
|
198
|
+
self.query_one("#dec-output", TextArea).load_text(result)
|
|
199
|
+
log = self.query_one("#dec-steps-log", RichLog)
|
|
200
|
+
log.clear()
|
|
201
|
+
if chain:
|
|
202
|
+
log.write(f"[bold cyan]Smart decode chain: {' → '.join(chain)}[/bold cyan]")
|
|
203
|
+
log.write("")
|
|
204
|
+
for i, (op, val) in enumerate(zip(["INPUT"] + chain, steps)):
|
|
205
|
+
color = "dim" if i == 0 else "green"
|
|
206
|
+
preview = val[:100].replace("\n", "↵")
|
|
207
|
+
label = f"Step {i}" if i > 0 else "Input"
|
|
208
|
+
log.write(f"[bold]{label}[/bold] [{color}]{op}[/{color}]")
|
|
209
|
+
log.write(f" [dim]{preview}[/dim] [dim]({len(val)} chars)[/dim]")
|
|
210
|
+
else:
|
|
211
|
+
log.write("[yellow]Smart decode: no known encoding detected[/yellow]")
|
|
212
|
+
log.write(f"[dim]Input ({len(text)} chars): {text[:80]}[/dim]")
|
|
213
|
+
except Exception as exc:
|
|
214
|
+
self.app.notify(f"Smart decode error: {exc}", severity="error")
|
|
215
|
+
|
|
216
|
+
def _update_chain_display(self) -> None:
|
|
217
|
+
try:
|
|
218
|
+
lbl = self.query_one("#dec-chain-display", Static)
|
|
219
|
+
if self._chain:
|
|
220
|
+
lbl.update(" → ".join(self._chain))
|
|
221
|
+
else:
|
|
222
|
+
lbl.update("[dim](empty)[/dim]")
|
|
223
|
+
except Exception:
|
|
224
|
+
pass
|
|
225
|
+
|
|
226
|
+
# ── Run ───────────────────────────────────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
def action_run_chain(self) -> None:
|
|
229
|
+
"""Запустить цепочку операций."""
|
|
230
|
+
try:
|
|
231
|
+
from pentool.api.decoder_api import run_chain, encode_op
|
|
232
|
+
inp_text = self.query_one("#dec-input", TextArea).text
|
|
233
|
+
if not inp_text:
|
|
234
|
+
self.app.notify("Input is empty", severity="warning")
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
if not self._chain:
|
|
238
|
+
# Одиночная операция — по выбранной кнопкой
|
|
239
|
+
op = self._selected_op
|
|
240
|
+
try:
|
|
241
|
+
result = encode_op(op, inp_text)
|
|
242
|
+
steps = [inp_text, result]
|
|
243
|
+
chain_used = [op]
|
|
244
|
+
except Exception as exc:
|
|
245
|
+
self.app.notify(f"Error: {exc}", severity="error")
|
|
246
|
+
return
|
|
247
|
+
else:
|
|
248
|
+
result, steps = run_chain(self._chain, inp_text)
|
|
249
|
+
chain_used = self._chain
|
|
250
|
+
|
|
251
|
+
self.query_one("#dec-output", TextArea).load_text(result)
|
|
252
|
+
self._render_steps(chain_used, steps)
|
|
253
|
+
|
|
254
|
+
except Exception as exc:
|
|
255
|
+
self.app.notify(f"Run error: {exc}", severity="error")
|
|
256
|
+
logger.debug("action_run_chain: %s", exc)
|
|
257
|
+
|
|
258
|
+
def _render_steps(self, chain: list[str], steps: list[str]) -> None:
|
|
259
|
+
"""Показать шаги цепочки в лог-панели."""
|
|
260
|
+
try:
|
|
261
|
+
log = self.query_one("#dec-steps-log", RichLog)
|
|
262
|
+
log.clear()
|
|
263
|
+
log.write(f"[bold cyan]Chain: {' → '.join(chain)}[/bold cyan]")
|
|
264
|
+
log.write("")
|
|
265
|
+
for i, (op, value) in enumerate(zip(["INPUT"] + chain, steps)):
|
|
266
|
+
step_label = f"Step {i}" if i > 0 else "Input"
|
|
267
|
+
color = "dim" if i == 0 else ("green" if not value.startswith("[error") else "red")
|
|
268
|
+
preview = value[:100].replace("\n", "↵") + ("…" if len(value) > 100 else "")
|
|
269
|
+
log.write(f"[bold]{step_label}[/bold] [{color}]{op}[/{color}]")
|
|
270
|
+
log.write(f" [dim]{preview}[/dim] [dim]({len(value)} chars)[/dim]")
|
|
271
|
+
except Exception as exc:
|
|
272
|
+
logger.debug("_render_steps: %s", exc)
|
|
273
|
+
|
|
274
|
+
# ── Copy / Clear ──────────────────────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
def action_copy_result(self) -> None:
|
|
277
|
+
try:
|
|
278
|
+
from pentool.utils.copy_as import copy_to_clipboard
|
|
279
|
+
text = self.query_one("#dec-output", TextArea).text
|
|
280
|
+
if text and copy_to_clipboard(text):
|
|
281
|
+
self.app.notify("Result copied", timeout=2)
|
|
282
|
+
else:
|
|
283
|
+
self.app.notify("Nothing to copy", severity="warning")
|
|
284
|
+
except Exception as exc:
|
|
285
|
+
self.app.notify(f"Copy failed: {exc}", severity="error")
|
|
286
|
+
|
|
287
|
+
def action_clear_all(self) -> None:
|
|
288
|
+
try:
|
|
289
|
+
self.query_one("#dec-input", TextArea).load_text("")
|
|
290
|
+
self.query_one("#dec-output", TextArea).load_text("")
|
|
291
|
+
self.query_one("#dec-steps-log", RichLog).clear()
|
|
292
|
+
self._chain.clear()
|
|
293
|
+
self._update_chain_display()
|
|
294
|
+
except Exception as exc:
|
|
295
|
+
logger.debug("action_clear_all: %s", exc)
|
|
296
|
+
|
|
297
|
+
# ── Public API (загрузка из других модулей) ───────────────────────────────
|
|
298
|
+
|
|
299
|
+
def load_text(self, text: str) -> None:
|
|
300
|
+
"""Загрузить текст в input-панель (вызов из Proxy/Repeater), очищает output."""
|
|
301
|
+
try:
|
|
302
|
+
self.query_one("#dec-input", TextArea).load_text(text)
|
|
303
|
+
self.query_one("#dec-output", TextArea).load_text("")
|
|
304
|
+
self.query_one("#dec-steps-log", RichLog).clear()
|
|
305
|
+
except Exception as exc:
|
|
306
|
+
logger.debug("load_text: %s", exc)
|
|
307
|
+
|
|
308
|
+
# ── Keyboard shortcut ─────────────────────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
def action_run_chain_binding(self) -> None:
|
|
311
|
+
self.action_run_chain()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Экран Extensions — управление плагинами."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from textual.app import ComposeResult
|
|
6
|
+
from textual.widget import Widget
|
|
7
|
+
from textual.widgets import Static
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
_CSS = (Path(__file__).parent / "screen.tcss").read_text(encoding="utf-8")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ExtensionsScreen(Widget):
|
|
14
|
+
"""Placeholder: Plugin manager screen."""
|
|
15
|
+
|
|
16
|
+
DEFAULT_CSS = _CSS
|
|
17
|
+
|
|
18
|
+
def compose(self) -> ComposeResult:
|
|
19
|
+
with Static(id="box"):
|
|
20
|
+
yield Static("Extensions", classes="title")
|
|
21
|
+
yield Static(
|
|
22
|
+
"Load Python plugins that add\nnew screens and CLI commands.",
|
|
23
|
+
classes="desc",
|
|
24
|
+
)
|