pentool 0.1.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 +16 -0
- pentool/api/__init__.py +41 -0
- pentool/api/base_api.py +48 -0
- pentool/api/comparer_api.py +19 -0
- pentool/api/decoder_api.py +24 -0
- pentool/api/intruder_api.py +171 -0
- pentool/api/proxy_api.py +210 -0
- pentool/api/repeater_api.py +57 -0
- pentool/api/scanner_api.py +266 -0
- pentool/api/sequencer_api.py +17 -0
- pentool/api/spider_api.py +106 -0
- pentool/api/target_api.py +87 -0
- pentool/cli/__init__.py +1 -0
- pentool/cli/main.py +98 -0
- pentool/cli/project.py +88 -0
- pentool/cli/proxy.py +173 -0
- pentool/cli/scan.py +115 -0
- pentool/core/__init__.py +1 -0
- pentool/core/config.py +171 -0
- pentool/core/crash_reporter.py +75 -0
- pentool/core/database.py +139 -0
- pentool/core/event_bus.py +207 -0
- pentool/core/events.py +160 -0
- pentool/core/features.py +373 -0
- pentool/core/license.py +274 -0
- pentool/core/logging.py +54 -0
- pentool/core/plugin_manager.py +312 -0
- pentool/core/project.py +31 -0
- pentool/core/storage_interface.py +307 -0
- pentool/core/updater.py +96 -0
- pentool/core/utils.py +36 -0
- pentool/modules/__init__.py +1 -0
- pentool/modules/comparer.py +177 -0
- pentool/modules/decoder.py +281 -0
- pentool/modules/intruder.py +428 -0
- pentool/modules/intruder_turbo.py +176 -0
- pentool/modules/match_replace.py +142 -0
- pentool/modules/proxy.py +797 -0
- pentool/modules/repeater.py +195 -0
- pentool/modules/sequencer.py +492 -0
- pentool/modules/spider.py +679 -0
- pentool/modules/target.py +185 -0
- pentool/modules/websocket_handler.py +249 -0
- pentool/plugins/__init__.py +1 -0
- pentool/plugins/builtin/__init__.py +1 -0
- pentool/plugins/builtin/payloads_pro.py +381 -0
- pentool/plugins/builtin/reports_pro.py +436 -0
- pentool/plugins/builtin/scanner_pro.py +291 -0
- pentool/plugins/example_plugin.py +39 -0
- pentool/services/__init__.py +1 -0
- pentool/services/base_service.py +60 -0
- pentool/services/intruder_service.py +94 -0
- pentool/services/proxy_service.py +178 -0
- pentool/services/repeater_service.py +85 -0
- pentool/services/scan_service.py +330 -0
- pentool/storage/__init__.py +0 -0
- pentool/storage/http_storage.py +506 -0
- pentool/storage/large_body_handler.py +50 -0
- pentool/storage/lru_cache.py +45 -0
- pentool/t.py +213 -0
- pentool/tui/__init__.py +1 -0
- pentool/tui/app.py +1429 -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 +226 -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 +224 -0
- pentool/tui/messages.py +103 -0
- pentool/tui/mixins/__init__.py +0 -0
- pentool/tui/mixins/app_mixin.py +59 -0
- pentool/tui/mixins/dialog_cancel.py +21 -0
- pentool/tui/mixins/request_context_menu.py +247 -0
- pentool/tui/mixins/tab_rename.py +89 -0
- pentool/tui/screens/__init__.py +31 -0
- pentool/tui/screens/comparer/__init__.py +3 -0
- pentool/tui/screens/comparer/screen.py +229 -0
- pentool/tui/screens/dashboard/__init__.py +2 -0
- pentool/tui/screens/dashboard/live_dashboard.py +730 -0
- pentool/tui/screens/dashboard/screen.py +771 -0
- pentool/tui/screens/decoder/__init__.py +3 -0
- pentool/tui/screens/decoder/screen.py +306 -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 +1356 -0
- pentool/tui/screens/proxy/__init__.py +3 -0
- pentool/tui/screens/proxy/screen.py +1554 -0
- pentool/tui/screens/repeater/__init__.py +3 -0
- pentool/tui/screens/repeater/screen.py +621 -0
- pentool/tui/screens/scanner/__init__.py +3 -0
- pentool/tui/screens/scanner/screen.py +1956 -0
- pentool/tui/screens/sequencer/__init__.py +3 -0
- pentool/tui/screens/sequencer/screen.py +516 -0
- pentool/tui/screens/settings/__init__.py +5 -0
- pentool/tui/screens/settings/hotkeys.py +134 -0
- pentool/tui/screens/settings/screen.py +540 -0
- pentool/tui/screens/spider/__init__.py +3 -0
- pentool/tui/screens/spider/screen.py +380 -0
- pentool/tui/screens/target/__init__.py +4 -0
- pentool/tui/screens/target/screen.py +358 -0
- pentool/tui/screens/terminal/__init__.py +5 -0
- pentool/tui/screens/terminal/screen.py +179 -0
- pentool/tui/widgets/__init__.py +1 -0
- pentool/tui/widgets/context_menu.py +148 -0
- pentool/tui/widgets/filter_bar.py +204 -0
- pentool/tui/widgets/inspector_panel.py +111 -0
- pentool/tui/widgets/menu.py +86 -0
- pentool/tui/widgets/menu_bar.py +238 -0
- pentool/tui/widgets/module_tabs.py +68 -0
- pentool/tui/widgets/payload_drop_zone.py +65 -0
- pentool/tui/widgets/request_editor.py +495 -0
- pentool/tui/widgets/resize_handle.py +116 -0
- pentool/tui/widgets/search_bar.py +112 -0
- pentool/tui/widgets/statusbar.py +96 -0
- pentool/tui/widgets/toolbar_button.py +68 -0
- pentool/utils/__init__.py +1 -0
- pentool/utils/cert.py +314 -0
- pentool/utils/coder.py +170 -0
- pentool/utils/copy_as.py +250 -0
- pentool/utils/diff.py +82 -0
- pentool/utils/http_client.py +145 -0
- pentool/utils/parser.py +227 -0
- pentool/utils/terminal_check.py +31 -0
- pentool-0.1.0.dist-info/METADATA +231 -0
- pentool-0.1.0.dist-info/RECORD +133 -0
- pentool-0.1.0.dist-info/WHEEL +5 -0
- pentool-0.1.0.dist-info/entry_points.txt +2 -0
- pentool-0.1.0.dist-info/licenses/LICENSE +34 -0
- pentool-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""ResizeHandle — перетаскиваемый разделитель между двумя панелями."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from textual.events import MouseDown, MouseMove, MouseUp
|
|
6
|
+
from textual.widget import Widget
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
_CSS = (Path(__file__).parent / "resize_handle.tcss").read_text(encoding="utf-8")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ResizeHandle(Widget):
|
|
13
|
+
"""Перетаскиваемый разделитель между двумя виджетами.
|
|
14
|
+
|
|
15
|
+
Механика (без пустот, без отставания):
|
|
16
|
+
- mouse_down: фиксируем screen_x/y, размер левой панели и суммарный
|
|
17
|
+
размер пары (left.size + right.size). Total кешируется и не меняется
|
|
18
|
+
во время drag — иначе 1fr плавает.
|
|
19
|
+
- mouse_move: new_left = start_left + (screen_x - start_x).
|
|
20
|
+
new_right = total - new_left. Обе панели в абсолютных единицах → нет пустот.
|
|
21
|
+
- mouse_up: release_mouse.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
DEFAULT_CSS = _CSS
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
left_id: str,
|
|
29
|
+
right_id: str,
|
|
30
|
+
vertical: bool = False,
|
|
31
|
+
min_left: int = 8,
|
|
32
|
+
min_right: int = 8,
|
|
33
|
+
**kwargs,
|
|
34
|
+
) -> None:
|
|
35
|
+
super().__init__(**kwargs)
|
|
36
|
+
self._left_id = left_id
|
|
37
|
+
self._right_id = right_id
|
|
38
|
+
self._vertical = vertical
|
|
39
|
+
self._min_left = min_left
|
|
40
|
+
self._min_right = min_right
|
|
41
|
+
self._dragging = False
|
|
42
|
+
self._drag_start_x: int = 0
|
|
43
|
+
self._drag_start_y: int = 0
|
|
44
|
+
self._start_size: int = 0
|
|
45
|
+
self._pair_total_cached: int = 0
|
|
46
|
+
|
|
47
|
+
self.tooltip = "Drag to resize"
|
|
48
|
+
if vertical:
|
|
49
|
+
self.add_class("-vertical")
|
|
50
|
+
|
|
51
|
+
def render(self) -> str:
|
|
52
|
+
"""Рендерим символ-разделитель вместо текста по умолчанию."""
|
|
53
|
+
if self._vertical:
|
|
54
|
+
return "─" * (self.size.width or 1)
|
|
55
|
+
return "│"
|
|
56
|
+
|
|
57
|
+
def on_mouse_down(self, event: MouseDown) -> None:
|
|
58
|
+
if event.button != 1:
|
|
59
|
+
return
|
|
60
|
+
try:
|
|
61
|
+
left = self.app.query_one(f"#{self._left_id}")
|
|
62
|
+
right = self.app.query_one(f"#{self._right_id}")
|
|
63
|
+
except Exception:
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
self._dragging = True
|
|
67
|
+
self._drag_start_x = event.screen_x
|
|
68
|
+
self._drag_start_y = event.screen_y
|
|
69
|
+
|
|
70
|
+
if self._vertical:
|
|
71
|
+
self._start_size = left.size.height
|
|
72
|
+
self._pair_total_cached = left.size.height + right.size.height
|
|
73
|
+
else:
|
|
74
|
+
self._start_size = left.size.width
|
|
75
|
+
self._pair_total_cached = left.size.width + right.size.width
|
|
76
|
+
|
|
77
|
+
self.add_class("-dragging")
|
|
78
|
+
self.capture_mouse()
|
|
79
|
+
event.stop()
|
|
80
|
+
|
|
81
|
+
def on_mouse_move(self, event: MouseMove) -> None:
|
|
82
|
+
if not self._dragging:
|
|
83
|
+
return
|
|
84
|
+
try:
|
|
85
|
+
left = self.app.query_one(f"#{self._left_id}")
|
|
86
|
+
right = self.app.query_one(f"#{self._right_id}")
|
|
87
|
+
except Exception:
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
delta = event.screen_y - self._drag_start_y if self._vertical else event.screen_x - self._drag_start_x
|
|
91
|
+
total = self._pair_total_cached
|
|
92
|
+
|
|
93
|
+
new_left = self._start_size + delta
|
|
94
|
+
new_left = max(self._min_left, min(new_left, total - self._min_right))
|
|
95
|
+
new_right = total - new_left
|
|
96
|
+
|
|
97
|
+
if self._vertical:
|
|
98
|
+
left.styles.height = new_left
|
|
99
|
+
right.styles.height = new_right
|
|
100
|
+
else:
|
|
101
|
+
left.styles.width = new_left
|
|
102
|
+
right.styles.width = new_right
|
|
103
|
+
|
|
104
|
+
# Принудительный немедленный перерасчёт layout
|
|
105
|
+
if self.parent is not None:
|
|
106
|
+
self.parent.refresh(layout=True)
|
|
107
|
+
|
|
108
|
+
event.stop()
|
|
109
|
+
|
|
110
|
+
def on_mouse_up(self, event: MouseUp) -> None:
|
|
111
|
+
if not self._dragging:
|
|
112
|
+
return
|
|
113
|
+
self._dragging = False
|
|
114
|
+
self.remove_class("-dragging")
|
|
115
|
+
self.release_mouse()
|
|
116
|
+
event.stop()
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""SearchBar — полоса поиска, появляющаяся по Ctrl+F."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from textual.app import ComposeResult
|
|
6
|
+
from textual.message import Message
|
|
7
|
+
from textual.widget import Widget
|
|
8
|
+
from textual.widgets import Button, Input, Label, Static
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
_CSS = (Path(__file__).parent / "search_bar.tcss").read_text(encoding="utf-8")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SearchBar(Widget):
|
|
15
|
+
"""Полоса поиска, появляющаяся по Ctrl+F поверх TextArea/RichLog.
|
|
16
|
+
|
|
17
|
+
Использование:
|
|
18
|
+
yield SearchBar(id="search-bar")
|
|
19
|
+
|
|
20
|
+
Открыть:
|
|
21
|
+
self.query_one(SearchBar).show()
|
|
22
|
+
|
|
23
|
+
Закрыть:
|
|
24
|
+
self.query_one(SearchBar).hide()
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
DEFAULT_CSS = _CSS
|
|
28
|
+
|
|
29
|
+
class Search(Message):
|
|
30
|
+
"""Пользователь ввёл поисковый запрос."""
|
|
31
|
+
def __init__(self, query: str, regex: bool = False, direction: int = 1) -> None:
|
|
32
|
+
super().__init__()
|
|
33
|
+
self.query = query
|
|
34
|
+
self.regex = regex
|
|
35
|
+
self.direction = direction # 1 = вперёд, -1 = назад
|
|
36
|
+
|
|
37
|
+
class Closed(Message):
|
|
38
|
+
"""Поисковая строка закрыта."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, **kwargs) -> None:
|
|
41
|
+
super().__init__(**kwargs)
|
|
42
|
+
self._regex_enabled = False
|
|
43
|
+
|
|
44
|
+
def compose(self) -> ComposeResult:
|
|
45
|
+
yield Label("Find:")
|
|
46
|
+
yield Input(placeholder="search...", id="search-input", compact=True)
|
|
47
|
+
yield Button("◀", id="btn-prev", variant="default")
|
|
48
|
+
yield Button("▶", id="btn-next", variant="default")
|
|
49
|
+
yield Static("Regex", id="search-regex-toggle")
|
|
50
|
+
yield Static("", id="search-count")
|
|
51
|
+
|
|
52
|
+
def show(self) -> None:
|
|
53
|
+
self.display = True
|
|
54
|
+
self.call_after_refresh(self._focus_input)
|
|
55
|
+
|
|
56
|
+
def hide(self) -> None:
|
|
57
|
+
self.display = False
|
|
58
|
+
self.post_message(self.Closed())
|
|
59
|
+
|
|
60
|
+
def _focus_input(self) -> None:
|
|
61
|
+
try:
|
|
62
|
+
self.query_one("#search-input", Input).focus()
|
|
63
|
+
except Exception:
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
def _fire_search(self, direction: int = 1) -> None:
|
|
67
|
+
try:
|
|
68
|
+
query = self.query_one("#search-input", Input).value
|
|
69
|
+
except Exception:
|
|
70
|
+
return
|
|
71
|
+
if query:
|
|
72
|
+
self.post_message(self.Search(query, self._regex_enabled, direction))
|
|
73
|
+
|
|
74
|
+
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
75
|
+
if event.input.id == "search-input":
|
|
76
|
+
self._fire_search(1)
|
|
77
|
+
|
|
78
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
79
|
+
if event.button.id == "btn-next":
|
|
80
|
+
self._fire_search(1)
|
|
81
|
+
elif event.button.id == "btn-prev":
|
|
82
|
+
self._fire_search(-1)
|
|
83
|
+
|
|
84
|
+
def on_static_click(self, event) -> None:
|
|
85
|
+
try:
|
|
86
|
+
widget = event.widget
|
|
87
|
+
if getattr(widget, "id", None) == "search-regex-toggle":
|
|
88
|
+
self._regex_enabled = not self._regex_enabled
|
|
89
|
+
if self._regex_enabled:
|
|
90
|
+
widget.add_class("-active")
|
|
91
|
+
else:
|
|
92
|
+
widget.remove_class("-active")
|
|
93
|
+
except Exception:
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
def on_key(self, event) -> None:
|
|
97
|
+
if event.key == "escape":
|
|
98
|
+
self.hide()
|
|
99
|
+
event.prevent_default()
|
|
100
|
+
elif event.key == "enter":
|
|
101
|
+
self._fire_search(1)
|
|
102
|
+
event.prevent_default()
|
|
103
|
+
|
|
104
|
+
def set_count(self, current: int, total: int) -> None:
|
|
105
|
+
try:
|
|
106
|
+
count_label = self.query_one("#search-count", Static)
|
|
107
|
+
if total == 0:
|
|
108
|
+
count_label.update("No matches")
|
|
109
|
+
else:
|
|
110
|
+
count_label.update(f"{current}/{total}")
|
|
111
|
+
except Exception:
|
|
112
|
+
pass
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Статусная строка: прокси, проект, время."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
from textual.app import ComposeResult
|
|
8
|
+
from textual.reactive import reactive
|
|
9
|
+
from textual.widget import Widget
|
|
10
|
+
from textual.widgets import Static
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
_CSS = (Path(__file__).parent / "statusbar.tcss").read_text(encoding="utf-8")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class StatusBar(Widget):
|
|
17
|
+
"""Нижняя строка с состоянием прокси, именем проекта и временем."""
|
|
18
|
+
|
|
19
|
+
DEFAULT_CSS = _CSS
|
|
20
|
+
|
|
21
|
+
proxy_running: reactive[bool] = reactive(False)
|
|
22
|
+
proxy_port: reactive[int] = reactive(8080)
|
|
23
|
+
project_name: reactive[str] = reactive("no project")
|
|
24
|
+
project_path: reactive[str] = reactive("")
|
|
25
|
+
project_saved: reactive[bool] = reactive(True)
|
|
26
|
+
current_time: reactive[str] = reactive("")
|
|
27
|
+
|
|
28
|
+
def compose(self) -> ComposeResult:
|
|
29
|
+
yield Static("", id="proxy-status", classes="status-proxy stopped")
|
|
30
|
+
yield Static("", id="project-name", classes="status-project")
|
|
31
|
+
yield Static("", id="saved-status", classes="status-saved")
|
|
32
|
+
yield Static("", id="current-time", classes="status-time")
|
|
33
|
+
|
|
34
|
+
def on_mount(self) -> None:
|
|
35
|
+
self._update_proxy()
|
|
36
|
+
self._update_project()
|
|
37
|
+
self._update_saved()
|
|
38
|
+
self._update_time()
|
|
39
|
+
self.set_interval(1.0, self._update_time)
|
|
40
|
+
|
|
41
|
+
def _update_proxy(self) -> None:
|
|
42
|
+
widget = self.query_one("#proxy-status", Static)
|
|
43
|
+
if self.proxy_running:
|
|
44
|
+
widget.update(f"[green]● Proxy :{self.proxy_port}[/green]")
|
|
45
|
+
widget.remove_class("stopped")
|
|
46
|
+
widget.add_class("running")
|
|
47
|
+
else:
|
|
48
|
+
widget.update("[red]○ Proxy stopped[/red]")
|
|
49
|
+
widget.remove_class("running")
|
|
50
|
+
widget.add_class("stopped")
|
|
51
|
+
|
|
52
|
+
def _update_project(self) -> None:
|
|
53
|
+
name = self.project_name
|
|
54
|
+
path = self.project_path
|
|
55
|
+
if name == "no project":
|
|
56
|
+
markup = "[dim]no project[/dim]"
|
|
57
|
+
elif path:
|
|
58
|
+
markup = f"[dim]project:[/dim] [bold cyan]{name}[/bold cyan] [dim]{path}[/dim]"
|
|
59
|
+
else:
|
|
60
|
+
markup = f"[dim]project:[/dim] [bold cyan]{name}[/bold cyan]"
|
|
61
|
+
self.query_one("#project-name", Static).update(markup)
|
|
62
|
+
|
|
63
|
+
def _update_saved(self) -> None:
|
|
64
|
+
widget = self.query_one("#saved-status", Static)
|
|
65
|
+
if self.project_name == "no project":
|
|
66
|
+
widget.update("")
|
|
67
|
+
elif self.project_saved:
|
|
68
|
+
widget.update("[dim green]● Saved[/dim green]")
|
|
69
|
+
else:
|
|
70
|
+
widget.update("[yellow]● Unsaved[/yellow]")
|
|
71
|
+
|
|
72
|
+
def _update_time(self) -> None:
|
|
73
|
+
self.query_one("#current-time", Static).update(
|
|
74
|
+
datetime.now().strftime("%H:%M:%S")
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def set_proxy_status(self, running: bool, port: int = 8080) -> None:
|
|
78
|
+
self.proxy_running = running
|
|
79
|
+
self.proxy_port = port
|
|
80
|
+
self._update_proxy()
|
|
81
|
+
|
|
82
|
+
def set_project_name(self, name: str) -> None:
|
|
83
|
+
self.project_name = name
|
|
84
|
+
self._update_project()
|
|
85
|
+
self._update_saved()
|
|
86
|
+
|
|
87
|
+
def set_project(self, name: str, path: str, saved: bool = True) -> None:
|
|
88
|
+
self.project_name = name
|
|
89
|
+
self.project_path = path
|
|
90
|
+
self.project_saved = saved
|
|
91
|
+
self._update_project()
|
|
92
|
+
self._update_saved()
|
|
93
|
+
|
|
94
|
+
def set_saved(self, saved: bool) -> None:
|
|
95
|
+
self.project_saved = saved
|
|
96
|
+
self._update_saved()
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""ToolbarButton — единая плоская кнопка для тулбаров всех экранов."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from textual.message import Message
|
|
6
|
+
from textual.widgets import Static
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
_CSS = (Path(__file__).parent / "toolbar_button.tcss").read_text(encoding="utf-8")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ToolbarButton(Static):
|
|
13
|
+
"""Плоская кнопка для тулбара — без рамки, высота 1.
|
|
14
|
+
|
|
15
|
+
CSS-классы:
|
|
16
|
+
.active — зелёный цвет (включено / активно)
|
|
17
|
+
.inactive — красный цвет (выключено)
|
|
18
|
+
.disabled — серый цвет, клик игнорируется
|
|
19
|
+
.warn — оранжевый цвет (предупреждение)
|
|
20
|
+
.sending — жёлтый цвет (ожидание ответа)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
DEFAULT_CSS = _CSS
|
|
24
|
+
|
|
25
|
+
class Pressed(Message):
|
|
26
|
+
"""Сообщение о нажатии кнопки."""
|
|
27
|
+
|
|
28
|
+
ALLOW_SELECTOR_MATCH = True
|
|
29
|
+
|
|
30
|
+
def __init__(self, button: "ToolbarButton") -> None:
|
|
31
|
+
super().__init__()
|
|
32
|
+
self.button = button
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def control(self) -> "ToolbarButton":
|
|
36
|
+
"""Позволяет @on(ToolbarButton.Pressed, "#btn-id") CSS-селектор."""
|
|
37
|
+
return self.button
|
|
38
|
+
|
|
39
|
+
def __init__(self, label: str, btn_id: str, classes: str = "") -> None:
|
|
40
|
+
super().__init__(label, id=btn_id, classes=classes)
|
|
41
|
+
self._label = label
|
|
42
|
+
self._disabled = "disabled" in classes.split()
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def label(self) -> str:
|
|
46
|
+
return self._label
|
|
47
|
+
|
|
48
|
+
@label.setter
|
|
49
|
+
def label(self, value: str) -> None:
|
|
50
|
+
self._label = value
|
|
51
|
+
self.update(value)
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def disabled(self) -> bool:
|
|
55
|
+
return self._disabled
|
|
56
|
+
|
|
57
|
+
@disabled.setter
|
|
58
|
+
def disabled(self, value: bool) -> None:
|
|
59
|
+
self._disabled = value
|
|
60
|
+
if value:
|
|
61
|
+
self.add_class("disabled")
|
|
62
|
+
else:
|
|
63
|
+
self.remove_class("disabled")
|
|
64
|
+
|
|
65
|
+
def on_click(self) -> None:
|
|
66
|
+
"""Постит Pressed только если кнопка не disabled."""
|
|
67
|
+
if not self._disabled:
|
|
68
|
+
self.post_message(self.Pressed(self))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Пакет utils: вспомогательные функции."""
|