mcpower-proxy 0.0.58__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.
- main.py +112 -0
- mcpower_proxy-0.0.58.dist-info/METADATA +250 -0
- mcpower_proxy-0.0.58.dist-info/RECORD +43 -0
- mcpower_proxy-0.0.58.dist-info/WHEEL +5 -0
- mcpower_proxy-0.0.58.dist-info/entry_points.txt +2 -0
- mcpower_proxy-0.0.58.dist-info/licenses/LICENSE +201 -0
- mcpower_proxy-0.0.58.dist-info/top_level.txt +3 -0
- modules/__init__.py +1 -0
- modules/apis/__init__.py +1 -0
- modules/apis/security_policy.py +322 -0
- modules/logs/__init__.py +1 -0
- modules/logs/audit_trail.py +162 -0
- modules/logs/logger.py +128 -0
- modules/redaction/__init__.py +13 -0
- modules/redaction/constants.py +38 -0
- modules/redaction/gitleaks_rules.py +1268 -0
- modules/redaction/pii_rules.py +271 -0
- modules/redaction/redactor.py +599 -0
- modules/ui/__init__.py +1 -0
- modules/ui/classes.py +48 -0
- modules/ui/confirmation.py +200 -0
- modules/ui/simple_dialog.py +104 -0
- modules/ui/xdialog/__init__.py +249 -0
- modules/ui/xdialog/constants.py +13 -0
- modules/ui/xdialog/mac_dialogs.py +190 -0
- modules/ui/xdialog/tk_dialogs.py +78 -0
- modules/ui/xdialog/windows_custom_dialog.py +426 -0
- modules/ui/xdialog/windows_dialogs.py +250 -0
- modules/ui/xdialog/windows_structs.py +183 -0
- modules/ui/xdialog/yad_dialogs.py +236 -0
- modules/ui/xdialog/zenity_dialogs.py +156 -0
- modules/utils/__init__.py +1 -0
- modules/utils/cli.py +46 -0
- modules/utils/config.py +193 -0
- modules/utils/copy.py +36 -0
- modules/utils/ids.py +160 -0
- modules/utils/json.py +120 -0
- modules/utils/mcp_configs.py +48 -0
- wrapper/__init__.py +1 -0
- wrapper/__version__.py +6 -0
- wrapper/middleware.py +750 -0
- wrapper/schema.py +227 -0
- wrapper/server.py +78 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""
|
|
2
|
+
User Confirmation Dialog for Security Policy Enforcement
|
|
3
|
+
|
|
4
|
+
Provides simple, lightweight modal dialogs for explicit user confirmation
|
|
5
|
+
when security policies require user approval for MCP operations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from mcpower_shared.mcp_types import UserDecision
|
|
12
|
+
|
|
13
|
+
from modules.logs.audit_trail import AuditTrailLogger
|
|
14
|
+
from modules.logs.logger import MCPLogger
|
|
15
|
+
from . import xdialog
|
|
16
|
+
from .classes import ConfirmationRequest, ConfirmationResponse, UserConfirmationError, DialogOptions
|
|
17
|
+
from .simple_dialog import show_explicit_user_confirmation_dialog, show_blocking_dialog
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# noinspection PyMethodMayBeStatic
|
|
21
|
+
class UserConfirmationDialog:
|
|
22
|
+
"""
|
|
23
|
+
Simple user confirmation dialog using tkinter messagebox
|
|
24
|
+
|
|
25
|
+
Provides lightweight modal dialogs for explicit user approval of MCP operations
|
|
26
|
+
when security policies require confirmation at request or response stages.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, logger: MCPLogger, audit_logger: AuditTrailLogger):
|
|
30
|
+
self.logger = logger
|
|
31
|
+
self.audit_logger = audit_logger
|
|
32
|
+
|
|
33
|
+
def request_confirmation(self, request: ConfirmationRequest, prompt_id: str,
|
|
34
|
+
call_type: Optional[str] = None, options: DialogOptions = None) -> ConfirmationResponse:
|
|
35
|
+
"""
|
|
36
|
+
Display a confirmation dialog and wait for the user decision
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
request: Confirmation request with all necessary context
|
|
40
|
+
call_type: Optional call type from inspect decision ("read", "write")
|
|
41
|
+
options: Optional dialog options for controlling button visibility
|
|
42
|
+
prompt_id: Prompt ID for audit trail grouping (mandatory for tool calls)
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
ConfirmationResponse with user decision
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
UserConfirmationError: If a user denies or the dialog fails
|
|
49
|
+
"""
|
|
50
|
+
if options is None:
|
|
51
|
+
options = DialogOptions()
|
|
52
|
+
self.logger.info(f"Requesting user confirmation for "
|
|
53
|
+
f"{'request' if request.is_request else 'response'} "
|
|
54
|
+
f"operation on tool '{request.tool_name}' (event: {request.event_id})")
|
|
55
|
+
|
|
56
|
+
# AUDIT: Log user interaction
|
|
57
|
+
self.audit_logger.log_event(
|
|
58
|
+
"user_interaction",
|
|
59
|
+
{
|
|
60
|
+
"type": "dialog",
|
|
61
|
+
"interaction": "explicit user confirmation"
|
|
62
|
+
},
|
|
63
|
+
event_id=request.event_id,
|
|
64
|
+
prompt_id=prompt_id
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
result = show_explicit_user_confirmation_dialog(request, options, self.logger)
|
|
68
|
+
direction = "request" if request.is_request else "response"
|
|
69
|
+
|
|
70
|
+
# Convert dialog result to UserDecision enum
|
|
71
|
+
match result:
|
|
72
|
+
case xdialog.YES:
|
|
73
|
+
user_decision = UserDecision.ALLOW
|
|
74
|
+
case xdialog.YES_ALWAYS:
|
|
75
|
+
user_decision = UserDecision.ALWAYS_ALLOW
|
|
76
|
+
case xdialog.NO_ALWAYS:
|
|
77
|
+
user_decision = UserDecision.ALWAYS_BLOCK
|
|
78
|
+
case _: # NO or any other result
|
|
79
|
+
user_decision = UserDecision.BLOCK
|
|
80
|
+
|
|
81
|
+
response = ConfirmationResponse(
|
|
82
|
+
user_decision=user_decision,
|
|
83
|
+
timestamp=datetime.now(timezone.utc),
|
|
84
|
+
event_id=request.event_id,
|
|
85
|
+
direction=direction,
|
|
86
|
+
call_type=call_type
|
|
87
|
+
)
|
|
88
|
+
self._log_confirmation_decision(request, response)
|
|
89
|
+
|
|
90
|
+
# AUDIT: Log user interaction result
|
|
91
|
+
self.audit_logger.log_event(
|
|
92
|
+
"user_interaction_result",
|
|
93
|
+
{
|
|
94
|
+
"decision": user_decision.value
|
|
95
|
+
},
|
|
96
|
+
event_id=request.event_id,
|
|
97
|
+
prompt_id=prompt_id
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Process user decision
|
|
101
|
+
if user_decision in (UserDecision.BLOCK, UserDecision.ALWAYS_BLOCK):
|
|
102
|
+
# User denied confirmation
|
|
103
|
+
raise UserConfirmationError(
|
|
104
|
+
f"User denied {'request' if request.is_request else 'response'} operation "
|
|
105
|
+
f"for tool '{request.tool_name}'",
|
|
106
|
+
event_id=request.event_id,
|
|
107
|
+
is_request=request.is_request,
|
|
108
|
+
tool_name=request.tool_name
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# User approved
|
|
112
|
+
return response
|
|
113
|
+
|
|
114
|
+
def request_blocking_confirmation(self, request: ConfirmationRequest, prompt_id: str,
|
|
115
|
+
call_type: Optional[str] = None) -> ConfirmationResponse:
|
|
116
|
+
"""
|
|
117
|
+
Display a blocking dialog and wait for the user decision
|
|
118
|
+
Shows "Block" vs "Allow Anyway" options for policy-blocked requests
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
request: Confirmation request with all necessary context
|
|
122
|
+
prompt_id: Prompt ID for audit trail grouping (mandatory for tool calls)
|
|
123
|
+
call_type: Optional call type from inspect decision ("read", "write")
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
ConfirmationResponse with user decision
|
|
127
|
+
|
|
128
|
+
Raises:
|
|
129
|
+
UserConfirmationError: If user chooses to block or dialog fails
|
|
130
|
+
"""
|
|
131
|
+
self.logger.info(f"Requesting user blocking confirmation for "
|
|
132
|
+
f"{'request' if request.is_request else 'response'} "
|
|
133
|
+
f"operation on tool '{request.tool_name}' (event: {request.event_id})")
|
|
134
|
+
|
|
135
|
+
# AUDIT: Log user interaction
|
|
136
|
+
self.audit_logger.log_event(
|
|
137
|
+
"user_interaction",
|
|
138
|
+
{
|
|
139
|
+
"type": "dialog",
|
|
140
|
+
"interaction": "block recommendation"
|
|
141
|
+
},
|
|
142
|
+
event_id=request.event_id,
|
|
143
|
+
prompt_id=prompt_id
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
result = show_blocking_dialog(request, self.logger)
|
|
147
|
+
direction = "request" if request.is_request else "response"
|
|
148
|
+
|
|
149
|
+
# Convert dialog result to UserDecision enum
|
|
150
|
+
match result:
|
|
151
|
+
case xdialog.YES: # Allow Anyway
|
|
152
|
+
user_decision = UserDecision.ALLOW
|
|
153
|
+
case _: # NO (Block) or any other result
|
|
154
|
+
user_decision = UserDecision.BLOCK
|
|
155
|
+
|
|
156
|
+
response = ConfirmationResponse(
|
|
157
|
+
user_decision=user_decision,
|
|
158
|
+
timestamp=datetime.now(timezone.utc),
|
|
159
|
+
event_id=request.event_id,
|
|
160
|
+
direction=direction,
|
|
161
|
+
call_type=call_type
|
|
162
|
+
)
|
|
163
|
+
self._log_confirmation_decision(request, response)
|
|
164
|
+
|
|
165
|
+
# AUDIT: Log user interaction result
|
|
166
|
+
self.audit_logger.log_event(
|
|
167
|
+
"user_interaction_result",
|
|
168
|
+
{
|
|
169
|
+
"decision": user_decision.value
|
|
170
|
+
},
|
|
171
|
+
event_id=request.event_id,
|
|
172
|
+
prompt_id=prompt_id
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Process user decision - only allow if user explicitly chose "Allow Anyway"
|
|
176
|
+
if user_decision == UserDecision.BLOCK:
|
|
177
|
+
# User chose to block
|
|
178
|
+
raise UserConfirmationError(
|
|
179
|
+
f"User blocked {'request' if request.is_request else 'response'} operation "
|
|
180
|
+
f"for tool '{request.tool_name}'",
|
|
181
|
+
event_id=request.event_id,
|
|
182
|
+
is_request=request.is_request,
|
|
183
|
+
tool_name=request.tool_name
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# User chose "Allow Anyway"
|
|
187
|
+
return response
|
|
188
|
+
|
|
189
|
+
def _log_confirmation_decision(self, request: ConfirmationRequest, response: ConfirmationResponse):
|
|
190
|
+
"""Log user confirmation decision for audit trail"""
|
|
191
|
+
self.logger.debug(
|
|
192
|
+
f"User confirmation decision: "
|
|
193
|
+
f"event_id={response.event_id}, "
|
|
194
|
+
f"direction={response.direction}, "
|
|
195
|
+
f"tool={request.tool_name}, "
|
|
196
|
+
f"user_decision={response.user_decision.value}, "
|
|
197
|
+
f"call_type={response.call_type}, "
|
|
198
|
+
f"timed_out={response.timed_out}, "
|
|
199
|
+
f"timestamp={response.timestamp.isoformat()}"
|
|
200
|
+
)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Simple, lightweight confirmation dialog using native OS dialogs
|
|
3
|
+
|
|
4
|
+
This provides cross-platform native dialogs
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from modules.logs.logger import MCPLogger
|
|
8
|
+
|
|
9
|
+
from . import xdialog
|
|
10
|
+
from .classes import ConfirmationRequest, DialogOptions
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def show_explicit_user_confirmation_dialog(request: ConfirmationRequest, options: DialogOptions,
|
|
14
|
+
logger: MCPLogger) -> int:
|
|
15
|
+
"""
|
|
16
|
+
Show a native OS confirmation dialog using xdialog
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
int: xdialog constant (YES, NO, YES_ALWAYS, NO_ALWAYS)
|
|
20
|
+
"""
|
|
21
|
+
message = (f"Server: {request.server_name}"
|
|
22
|
+
f"\nTool: {request.tool_name}"
|
|
23
|
+
f"\n\nPolicy Alert ({request.severity.title()}):"
|
|
24
|
+
f"\n{request.policy_reasons[0]}")
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
# Build custom button array: Block, Always Block (optional), Allow, Always Allow (optional)
|
|
28
|
+
buttons = ["Block"]
|
|
29
|
+
|
|
30
|
+
if options.show_always_block:
|
|
31
|
+
buttons.append("Always Block")
|
|
32
|
+
|
|
33
|
+
buttons.append("Allow")
|
|
34
|
+
|
|
35
|
+
if options.show_always_allow:
|
|
36
|
+
buttons.append("Always Allow")
|
|
37
|
+
|
|
38
|
+
# Use generic dialog with Block/Allow buttons
|
|
39
|
+
result_index = xdialog.generic_dialog(
|
|
40
|
+
title="MCPower Security Confirmation Required",
|
|
41
|
+
message=message,
|
|
42
|
+
buttons=buttons,
|
|
43
|
+
default_button=buttons.index("Allow"), # Default to "Allow"
|
|
44
|
+
icon=xdialog.ICON_WARNING
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Map button indices back to xdialog constants for compatibility
|
|
48
|
+
# Button order: Block, [Always Block], Allow, [Always Allow]
|
|
49
|
+
if result_index == 0:
|
|
50
|
+
return xdialog.NO # Block -> NO
|
|
51
|
+
elif options.show_always_block and result_index == 1:
|
|
52
|
+
return xdialog.NO_ALWAYS # Always Block -> NO_ALWAYS
|
|
53
|
+
elif not options.show_always_block and result_index == 1:
|
|
54
|
+
return xdialog.YES # Allow -> YES (when no Always Block)
|
|
55
|
+
elif options.show_always_block and result_index == 2:
|
|
56
|
+
return xdialog.YES # Allow -> YES (when Always Block is present)
|
|
57
|
+
elif not options.show_always_block and result_index == 2:
|
|
58
|
+
return xdialog.YES_ALWAYS # Always Allow -> YES_ALWAYS (when no Always Block)
|
|
59
|
+
elif options.show_always_block and result_index == 3:
|
|
60
|
+
return xdialog.YES_ALWAYS # Always Allow -> YES_ALWAYS (when Always Block is present)
|
|
61
|
+
else:
|
|
62
|
+
raise Exception(f"Unexpected result index {result_index}")
|
|
63
|
+
|
|
64
|
+
except Exception as e:
|
|
65
|
+
logger.error(f"Native dialog error: {e}")
|
|
66
|
+
raise e
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def show_blocking_dialog(request: ConfirmationRequest, logger: MCPLogger) -> int:
|
|
70
|
+
"""
|
|
71
|
+
Show a blocking dialog with red error styling and "Block"/"Allow Anyway" buttons
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
int: xdialog constant (YES=Allow Anyway, NO=Block)
|
|
75
|
+
"""
|
|
76
|
+
message = (f"Server: {request.server_name}"
|
|
77
|
+
f"\nTool: {request.tool_name}"
|
|
78
|
+
f"\n\nPolicy Alert ({request.severity.title()}):"
|
|
79
|
+
f"\n{request.policy_reasons[0]}")
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
# Build button array: Block (default), Allow Anyway
|
|
83
|
+
buttons = ["Block", "Allow Anyway"]
|
|
84
|
+
|
|
85
|
+
# Use generic dialog with error icon and Block as default
|
|
86
|
+
result_index = xdialog.generic_dialog(
|
|
87
|
+
title="MCPower Security Request Blocked",
|
|
88
|
+
message=message,
|
|
89
|
+
buttons=buttons,
|
|
90
|
+
default_button=0, # Default to "Block" button (first button)
|
|
91
|
+
icon=xdialog.ICON_ERROR # Red error icon
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Map button indices to xdialog constants
|
|
95
|
+
if result_index == 0:
|
|
96
|
+
return xdialog.NO # Block -> NO
|
|
97
|
+
elif result_index == 1:
|
|
98
|
+
return xdialog.YES # Allow Anyway -> YES
|
|
99
|
+
else:
|
|
100
|
+
raise Exception(f"Unexpected result index {result_index}")
|
|
101
|
+
|
|
102
|
+
except Exception as e:
|
|
103
|
+
logger.error(f"Blocking dialog error: {e}")
|
|
104
|
+
raise e
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import platform
|
|
2
|
+
import subprocess
|
|
3
|
+
|
|
4
|
+
from .constants import *
|
|
5
|
+
from typing import Iterable, Union, Tuple, Optional, Literal, overload
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"open_file", "save_file", "directory",
|
|
9
|
+
"info", "warning", "error",
|
|
10
|
+
"yesno", "yesno_always", "yesnocancel", "retrycancel", "okcancel",
|
|
11
|
+
"generic_dialog",
|
|
12
|
+
"YES", "NO", "CANCEL", "RETRY", "OK", "YES_ALWAYS", "NO_ALWAYS",
|
|
13
|
+
"ICON_QUESTION", "ICON_WARNING", "ICON_ERROR", "ICON_INFO"
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
SYSTEM = platform.system()
|
|
17
|
+
|
|
18
|
+
# Find the best dialog for this platform. Default to tkinter.
|
|
19
|
+
# Using import keyword instead of __import__ for extra compatibility.
|
|
20
|
+
def get_dialogs():
|
|
21
|
+
if SYSTEM == 'Windows':
|
|
22
|
+
from . import windows_dialogs
|
|
23
|
+
return windows_dialogs
|
|
24
|
+
elif SYSTEM == "Darwin":
|
|
25
|
+
from . import mac_dialogs
|
|
26
|
+
return mac_dialogs
|
|
27
|
+
else:
|
|
28
|
+
def cmd_exists(cmd):
|
|
29
|
+
proc = subprocess.Popen(('which', cmd), stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=False)
|
|
30
|
+
proc.communicate()
|
|
31
|
+
return not proc.returncode
|
|
32
|
+
|
|
33
|
+
if cmd_exists('yad'):
|
|
34
|
+
from . import yad_dialogs
|
|
35
|
+
return yad_dialogs
|
|
36
|
+
if cmd_exists('zenity'):
|
|
37
|
+
from . import zenity_dialogs
|
|
38
|
+
return zenity_dialogs
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
from . import tk_dialogs
|
|
42
|
+
return tk_dialogs
|
|
43
|
+
except ModuleNotFoundError: pass
|
|
44
|
+
|
|
45
|
+
raise ModuleNotFoundError('No dialog type is supported on this machine. Install tkinter to guarantee dialogs.')
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
dialogs = get_dialogs()
|
|
50
|
+
|
|
51
|
+
@overload
|
|
52
|
+
def open_file(title: Optional[str] = None, filetypes: Iterable[Tuple[str, str]] = [("All Files", "*")], multiple: Literal[False] = False) -> str: ...
|
|
53
|
+
@overload
|
|
54
|
+
def open_file(title: Optional[str] = None, filetypes: Iterable[Tuple[str, str]] = [("All Files", "*")], multiple: Literal[True] = True) -> Iterable[str]: ...
|
|
55
|
+
|
|
56
|
+
def open_file(title: Optional[str] = None, filetypes: Iterable[Tuple[str, str]] = [("All Files", "*")], multiple: bool = False) -> Union[str, Iterable[str]]:
|
|
57
|
+
'''Shows a dialog box for selecting one or more files to be opened.
|
|
58
|
+
|
|
59
|
+
Arguments:
|
|
60
|
+
title: Text to show on the header of the dialog box.
|
|
61
|
+
Omitting it has system-dependent results.
|
|
62
|
+
|
|
63
|
+
filetypes: A list of tuples specifying which filetypes to show.
|
|
64
|
+
The first string is a readable name of that filetype, and
|
|
65
|
+
the second string is one or more glob (e.g., * or *.txt) extension.
|
|
66
|
+
|
|
67
|
+
Each glob in the second string is separated by spaces.
|
|
68
|
+
|
|
69
|
+
Each tuple will normally appear in a dropdown of file types to select from.
|
|
70
|
+
If this argument is not specified, all file types are visible.
|
|
71
|
+
|
|
72
|
+
MacOS will ignore the first string in each tuple (as it doesn't
|
|
73
|
+
display it anywhere), and will instead enable selection of all
|
|
74
|
+
file extensions provided.
|
|
75
|
+
|
|
76
|
+
multiple: If False (default), only one file may be selected. If True, multiple files may be selected.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
If `multiple` is True, an iterable of selected files or an empty iterable.
|
|
80
|
+
If `multiple` is False, the file that was selected or an empty string.
|
|
81
|
+
'''
|
|
82
|
+
return dialogs.open_file(title, filetypes, multiple)
|
|
83
|
+
|
|
84
|
+
def save_file(title: Optional[str] = None, filetypes: Iterable[Tuple[str, str]] = [("All Files", "*")]) -> str:
|
|
85
|
+
'''Shows a dialog box for selecting one or more files to be opened.
|
|
86
|
+
|
|
87
|
+
Arguments:
|
|
88
|
+
title: Text to show on the header of the dialog box.
|
|
89
|
+
Omitting it has system-dependent results.
|
|
90
|
+
|
|
91
|
+
filetypes: A list of tuples specifying which filetypes to show.
|
|
92
|
+
The first string is a readable name of that filetype, and
|
|
93
|
+
the second string is one or more glob (e.g., * or *.txt) expression.
|
|
94
|
+
|
|
95
|
+
Each glob in the second string is separated by spaces.
|
|
96
|
+
|
|
97
|
+
Each tuple will appear in a dropdown of file types to select from.
|
|
98
|
+
If this argument is not specified, all file types are visible.
|
|
99
|
+
|
|
100
|
+
MacOS does not support this option, but will instead use the first
|
|
101
|
+
tuple's glob to populate the default name.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
The file that was selected or an empty string.
|
|
105
|
+
'''
|
|
106
|
+
return dialogs.save_file(title, filetypes)
|
|
107
|
+
|
|
108
|
+
def directory(title: Optional[str] = None) -> str:
|
|
109
|
+
'''Shows a dialog box for selecting a directory. The directory must exist to be selected.
|
|
110
|
+
|
|
111
|
+
Arguments:
|
|
112
|
+
title: Text to show on the header of the dialog box.
|
|
113
|
+
Omitting it has system-dependent results.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
The directory that was selected or an empty string.
|
|
117
|
+
'''
|
|
118
|
+
return dialogs.directory(title)
|
|
119
|
+
|
|
120
|
+
def info(title: Optional[str] = None, message: str = '') -> None:
|
|
121
|
+
'''Shows an info dialog box.
|
|
122
|
+
|
|
123
|
+
Arguments:
|
|
124
|
+
title: Text to show on the header of the dialog box.
|
|
125
|
+
Omitting it has system-dependent results.
|
|
126
|
+
|
|
127
|
+
message: Text to show in the middle of the dialog box.
|
|
128
|
+
'''
|
|
129
|
+
dialogs.info(title, message)
|
|
130
|
+
|
|
131
|
+
def warning(title: Optional[str] = None, message: str = '') -> None:
|
|
132
|
+
'''Shows a warning dialog box.
|
|
133
|
+
|
|
134
|
+
Arguments:
|
|
135
|
+
title: Text to show on the header of the dialog box.
|
|
136
|
+
Omitting it has system-dependent results.
|
|
137
|
+
|
|
138
|
+
message: Text to show in the middle of the dialog box.
|
|
139
|
+
'''
|
|
140
|
+
dialogs.warning(title, message)
|
|
141
|
+
|
|
142
|
+
def error(title: Optional[str] = None, message: str = '') -> None:
|
|
143
|
+
'''Shows an error dialog box.
|
|
144
|
+
|
|
145
|
+
Arguments:
|
|
146
|
+
title: Text to show on the header of the dialog box.
|
|
147
|
+
Omitting it has system-dependent results.
|
|
148
|
+
|
|
149
|
+
message: Text to show in the middle of the dialog box.
|
|
150
|
+
'''
|
|
151
|
+
dialogs.error(title, message)
|
|
152
|
+
|
|
153
|
+
def yesno(title: Optional[str] = None, message: str = '') -> int:
|
|
154
|
+
'''Shows a question dialog box with the buttons "Yes" and "No".
|
|
155
|
+
|
|
156
|
+
Arguments:
|
|
157
|
+
title: Text to show on the header of the dialog box.
|
|
158
|
+
Omitting it has system-dependent results.
|
|
159
|
+
|
|
160
|
+
message: Text to show in the middle of the dialog box.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
`xdialog.YES` or `xdialog.NO`. Closing the box results in `xdialog.NO`.
|
|
164
|
+
'''
|
|
165
|
+
return dialogs.yesno(title, message)
|
|
166
|
+
|
|
167
|
+
def yesno_always(title: Optional[str] = None, message: str = '', yes_always: bool = False, no_always: bool = False) -> int:
|
|
168
|
+
'''Shows a question dialog box with Yes/No and optional Always buttons.
|
|
169
|
+
|
|
170
|
+
Arguments:
|
|
171
|
+
title: Text to show on the header of the dialog box.
|
|
172
|
+
Omitting it has system-dependent results.
|
|
173
|
+
|
|
174
|
+
message: Text to show in the middle of the dialog box.
|
|
175
|
+
|
|
176
|
+
yes_always: If True, shows "Yes Always" button.
|
|
177
|
+
|
|
178
|
+
no_always: If True, shows "No Always" button.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
`xdialog.YES`, `xdialog.NO`, `xdialog.YES_ALWAYS`, or `xdialog.NO_ALWAYS`.
|
|
182
|
+
Button order: No, No Always (if enabled), Yes, Yes Always (if enabled).
|
|
183
|
+
'''
|
|
184
|
+
return dialogs.yesno_always(title, message, yes_always, no_always)
|
|
185
|
+
|
|
186
|
+
def yesnocancel(title: Optional[str] = None, message: str = '') -> int:
|
|
187
|
+
'''Shows a question dialog box with the buttons "Yes", "No", and "Cancel".
|
|
188
|
+
|
|
189
|
+
Arguments:
|
|
190
|
+
title: Text to show on the header of the dialog box.
|
|
191
|
+
Omitting it has system-dependent results.
|
|
192
|
+
|
|
193
|
+
message: Text to show in the middle of the dialog box.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
`xdialog.YES`, `xdialog.NO`, or `xdialog.CANCEL`. Closing the box results in `xdialog.CANCEL`.
|
|
197
|
+
'''
|
|
198
|
+
return dialogs.yesnocancel(title, message)
|
|
199
|
+
|
|
200
|
+
def retrycancel(title: Optional[str] = None, message: str = '') -> int:
|
|
201
|
+
'''Shows a question dialog box with the buttons "Retry" and "Cancel".
|
|
202
|
+
|
|
203
|
+
Arguments:
|
|
204
|
+
title: Text to show on the header of the dialog box.
|
|
205
|
+
Omitting it has system-dependent results.
|
|
206
|
+
|
|
207
|
+
message: Text to show in the middle of the dialog box.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
`xdialog.RETRY` or `xdialog.CANCEL`. Closing the box results in `xdialog.CANCEL`.
|
|
211
|
+
'''
|
|
212
|
+
return dialogs.retrycancel(title, message)
|
|
213
|
+
|
|
214
|
+
def okcancel(title: Optional[str] = None, message: str = '') -> int:
|
|
215
|
+
'''Shows a question dialog box with the buttons "Ok" and "Cancel".
|
|
216
|
+
|
|
217
|
+
Arguments:
|
|
218
|
+
title: Text to show on the header of the dialog box.
|
|
219
|
+
Omitting it has system-dependent results.
|
|
220
|
+
|
|
221
|
+
message: Text to show in the middle of the dialog box.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
`xdialog.OK` or `xdialog.CANCEL`. Closing the box results in `xdialog.CANCEL`.
|
|
225
|
+
'''
|
|
226
|
+
return dialogs.okcancel(title, message)
|
|
227
|
+
|
|
228
|
+
def generic_dialog(title: str, message: str, buttons: list[str], default_button: int, icon: str) -> int:
|
|
229
|
+
'''Shows a generic dialog box with custom buttons and icon.
|
|
230
|
+
|
|
231
|
+
Arguments:
|
|
232
|
+
title: Text to show on the header of the dialog box.
|
|
233
|
+
|
|
234
|
+
message: Text to show in the middle of the dialog box.
|
|
235
|
+
|
|
236
|
+
buttons: List of button text strings to display.
|
|
237
|
+
|
|
238
|
+
default_button: Index of default button (0-based). Used when dialog is dismissed.
|
|
239
|
+
|
|
240
|
+
icon: Icon type to display. Use ICON_QUESTION, ICON_WARNING, ICON_ERROR, or ICON_INFO.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
Index of clicked button (0-based). If dialog is dismissed, returns default_button.
|
|
244
|
+
|
|
245
|
+
Raises:
|
|
246
|
+
NotImplementedError: On platforms that don't support custom buttons (Zenity, Tkinter).
|
|
247
|
+
ValueError: If parameters are invalid (icon not supported, buttons empty, default_button out of range).
|
|
248
|
+
'''
|
|
249
|
+
return dialogs.generic_dialog(title, message, buttons, default_button, icon)
|