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.
Files changed (43) hide show
  1. main.py +112 -0
  2. mcpower_proxy-0.0.58.dist-info/METADATA +250 -0
  3. mcpower_proxy-0.0.58.dist-info/RECORD +43 -0
  4. mcpower_proxy-0.0.58.dist-info/WHEEL +5 -0
  5. mcpower_proxy-0.0.58.dist-info/entry_points.txt +2 -0
  6. mcpower_proxy-0.0.58.dist-info/licenses/LICENSE +201 -0
  7. mcpower_proxy-0.0.58.dist-info/top_level.txt +3 -0
  8. modules/__init__.py +1 -0
  9. modules/apis/__init__.py +1 -0
  10. modules/apis/security_policy.py +322 -0
  11. modules/logs/__init__.py +1 -0
  12. modules/logs/audit_trail.py +162 -0
  13. modules/logs/logger.py +128 -0
  14. modules/redaction/__init__.py +13 -0
  15. modules/redaction/constants.py +38 -0
  16. modules/redaction/gitleaks_rules.py +1268 -0
  17. modules/redaction/pii_rules.py +271 -0
  18. modules/redaction/redactor.py +599 -0
  19. modules/ui/__init__.py +1 -0
  20. modules/ui/classes.py +48 -0
  21. modules/ui/confirmation.py +200 -0
  22. modules/ui/simple_dialog.py +104 -0
  23. modules/ui/xdialog/__init__.py +249 -0
  24. modules/ui/xdialog/constants.py +13 -0
  25. modules/ui/xdialog/mac_dialogs.py +190 -0
  26. modules/ui/xdialog/tk_dialogs.py +78 -0
  27. modules/ui/xdialog/windows_custom_dialog.py +426 -0
  28. modules/ui/xdialog/windows_dialogs.py +250 -0
  29. modules/ui/xdialog/windows_structs.py +183 -0
  30. modules/ui/xdialog/yad_dialogs.py +236 -0
  31. modules/ui/xdialog/zenity_dialogs.py +156 -0
  32. modules/utils/__init__.py +1 -0
  33. modules/utils/cli.py +46 -0
  34. modules/utils/config.py +193 -0
  35. modules/utils/copy.py +36 -0
  36. modules/utils/ids.py +160 -0
  37. modules/utils/json.py +120 -0
  38. modules/utils/mcp_configs.py +48 -0
  39. wrapper/__init__.py +1 -0
  40. wrapper/__version__.py +6 -0
  41. wrapper/middleware.py +750 -0
  42. wrapper/schema.py +227 -0
  43. wrapper/server.py +78 -0
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import subprocess
4
+
5
+ from .constants import *
6
+
7
+ def osascript(*code: str):
8
+ proc = subprocess.Popen(
9
+ ["osascript", "-e", " ".join(code)],
10
+ stdout=subprocess.PIPE,
11
+ stderr=subprocess.PIPE,
12
+ shell=False
13
+ )
14
+ stdout, stderr = proc.communicate()
15
+
16
+ return (proc.returncode, stdout.decode('utf-8'), stderr.decode('utf-8'))
17
+
18
+ def quote(text: str):
19
+ return '"' + text.replace("\\", "\\\\").replace('"', '\\"') + '"'
20
+
21
+ def dialog(title, message, icon, buttons=["OK"], default_button=None):
22
+ script = [
23
+ 'display dialog', quote(message),
24
+ 'with icon', icon,
25
+ 'buttons', "{" + ",".join(quote(btn) for btn in buttons) + "}",
26
+ ]
27
+ if title: script.append('with title ' + quote(title))
28
+
29
+ # Set default button if specified
30
+ if default_button is not None and 0 <= default_button < len(buttons):
31
+ script.append('default button ' + quote(buttons[default_button]))
32
+
33
+ code, out, err = osascript(*script)
34
+ if code: return ''
35
+ else: return out[out.index(":")+1:].strip("\r\n")
36
+
37
+ def open_file(title, filetypes, multiple=False):
38
+ script = ['choose file']
39
+ if title: script.append('with prompt ' + quote(title))
40
+ if filetypes:
41
+ oftype = []
42
+ for _, exts in filetypes:
43
+ for ext in exts.split():
44
+ if ext == "*": break
45
+ if ext[:2] == "*.": oftype.append(quote(ext[2:]))
46
+ else:
47
+ if oftype: script.append("of type {" + ",".join(oftype) + "}")
48
+
49
+ if multiple:
50
+ script.append("multiple selections allowed true")
51
+ code, out, err = osascript(f'set ps to ({" ".join(script)})\rrepeat with p in ps\r log (POSIX path of p)\rend repeat')
52
+ if code: return []
53
+
54
+ return err.strip("\r\n").splitlines()
55
+ else:
56
+ code, out, err = osascript(f'POSIX path of ({" ".join(script)})')
57
+ if code: return ''
58
+
59
+ return out.strip("\r\n")
60
+
61
+ def save_file(title, filetypes):
62
+ script = ['choose file name']
63
+ if title: script.append('with prompt ' + quote(title))
64
+ if filetypes:
65
+ for filetype, exts in filetypes:
66
+ for ext in exts.split():
67
+ if ext == "*": continue
68
+ if ext[:2] == "*.":
69
+ script.append(f'default name "{filetype}.{ext[2:]}"')
70
+ break
71
+
72
+ code, out, err = osascript(f'POSIX path of ({" ".join(script)})')
73
+ if code: return ''
74
+
75
+ return out.strip("\r\n")
76
+
77
+ def directory(title):
78
+ script = ['choose folder']
79
+ if title: script.append('with prompt ' + quote(title))
80
+
81
+ code, out, err = osascript(f'POSIX path of ({" ".join(script)})')
82
+ if code: return ''
83
+
84
+ return out.strip("\r\n")
85
+
86
+ def info(title, message):
87
+ dialog(title, message, "note")
88
+
89
+ def warning(title, message):
90
+ dialog(title, message, "caution")
91
+
92
+ def error(title, message):
93
+ dialog(title, message, "stop")
94
+
95
+ def yesno(title, message):
96
+ out = dialog(title, message, "caution", ["No", "Yes"])
97
+ if not out or out == "No": return NO
98
+ elif out == "Yes": return YES
99
+
100
+ def yesno_always(title, message, yes_always=False, no_always=False):
101
+ """
102
+ Enhanced yes/no dialog with optional always buttons
103
+ Button order: No, No Always (if enabled), Yes, Yes Always (if enabled)
104
+ """
105
+ buttons = []
106
+
107
+ # Build button list in order: No, No Always, Yes, Yes Always
108
+ buttons.append("No")
109
+ if no_always:
110
+ buttons.append("No (Always)")
111
+ buttons.append("Yes")
112
+ if yes_always:
113
+ buttons.append("Yes (Always)")
114
+
115
+ out = dialog(title, message, "caution", buttons)
116
+
117
+ if not out or out == "No":
118
+ return NO
119
+ elif out == "No (Always)":
120
+ return NO_ALWAYS
121
+ elif out == "Yes":
122
+ return YES
123
+ elif out == "Yes (Always)":
124
+ return YES_ALWAYS
125
+ else:
126
+ return NO # Fallback
127
+
128
+ def yesnocancel(title, message):
129
+ out = dialog(title, message, "note", ["Cancel", "No", "Yes"])
130
+ if not out or out == "Cancel": return CANCEL
131
+ elif out == "No": return NO
132
+ elif out == "Yes": return YES
133
+
134
+ def retrycancel(title, message):
135
+ out = dialog(title, message, "note", ["Cancel", "Retry"])
136
+ if not out or out == "Cancel": return CANCEL
137
+ elif out == "Retry": return RETRY
138
+
139
+ def okcancel(title, message):
140
+ out = dialog(title, message, "note", ["Cancel", "OK"])
141
+ if not out or out == "Cancel": return CANCEL
142
+ elif out == "OK": return OK
143
+
144
+ def generic_dialog(title, message, buttons, default_button, icon):
145
+ """
146
+ Generic dialog with custom buttons and icon
147
+
148
+ Args:
149
+ title (str): Dialog title
150
+ message (str): Dialog message
151
+ buttons (list): List of button text strings
152
+ default_button (int): Index of default button (0-based)
153
+ icon (str): Icon type (ICON_QUESTION, ICON_WARNING, ICON_ERROR, ICON_INFO)
154
+
155
+ Returns:
156
+ int: Index of clicked button (0-based)
157
+ """
158
+ from .constants import ICON_QUESTION, ICON_WARNING, ICON_ERROR, ICON_INFO
159
+
160
+ # Map icon constants to AppleScript icons
161
+ icon_map = {
162
+ ICON_QUESTION: "caution",
163
+ ICON_WARNING: "caution",
164
+ ICON_ERROR: "stop",
165
+ ICON_INFO: "note"
166
+ }
167
+
168
+ if icon not in icon_map:
169
+ raise ValueError(f"Unsupported icon: {icon}")
170
+
171
+ if not isinstance(buttons, list) or len(buttons) == 0:
172
+ raise ValueError("buttons must be a non-empty list")
173
+
174
+ if not isinstance(default_button, int) or default_button < 0 or default_button >= len(buttons):
175
+ raise ValueError(f"default_button must be a valid index (0-{len(buttons)-1})")
176
+
177
+ # Call AppleScript dialog with default button
178
+ out = dialog(title, message, icon_map[icon], buttons, default_button)
179
+
180
+ # Map response back to button index
181
+ if not out:
182
+ # Dialog was dismissed/cancelled - return default button index
183
+ return default_button
184
+
185
+ # Find the button that was clicked
186
+ try:
187
+ return buttons.index(out)
188
+ except ValueError:
189
+ # This should not happen unless AppleScript returns unexpected text
190
+ raise RuntimeError(f"Unexpected dialog result: {out}, expected one of {buttons}")
@@ -0,0 +1,78 @@
1
+ from .constants import *
2
+
3
+ from tkinter import messagebox
4
+ from tkinter import filedialog
5
+
6
+ def open_file(title, filetypes, multiple=False):
7
+ if multiple:
8
+ return filedialog.askopenfilenames(title=title, filetypes=filetypes)
9
+ else:
10
+ return filedialog.askopenfilename(title=title) or ''
11
+
12
+ def save_file(title, filetypes):
13
+ return filedialog.asksaveasfilename(title=title, filetypes=filetypes) or ''
14
+
15
+ def directory(title):
16
+ return filedialog.askdirectory(mustexist=True) or ''
17
+
18
+ info = messagebox.showinfo
19
+ warning = messagebox.showwarning
20
+ error = messagebox.showerror
21
+
22
+ def yesno(title, message):
23
+ if messagebox.askyesno(title, message):
24
+ return YES
25
+ else:
26
+ return NO
27
+
28
+ def yesno_always(title, message, yes_always=False, no_always=False):
29
+ """
30
+ Enhanced yes/no dialog with optional always buttons
31
+ Button order: No, No Always (if enabled), Yes, Yes Always (if enabled)
32
+
33
+ Note: Tkinter messagebox doesn't support custom buttons, so we fall back to yesno
34
+ for now. Full implementation would require custom Tkinter dialog creation.
35
+ """
36
+ # TODO: Implement custom Tkinter dialog with proper button layout
37
+ # For now, fall back to standard yesno dialog
38
+ return yesno(title, message)
39
+
40
+ def yesnocancel(title, message):
41
+ r = messagebox.askyesnocancel(title, message)
42
+ if r is None:
43
+ return CANCEL
44
+ elif r:
45
+ return YES
46
+ else:
47
+ return NO
48
+
49
+ def retrycancel(title, message):
50
+ if messagebox.askretrycancel(title, message):
51
+ return RETRY
52
+ else:
53
+ return CANCEL
54
+
55
+ def okcancel(title, message):
56
+ if messagebox.askokcancel(title, message):
57
+ return OK
58
+ else:
59
+ return CANCEL
60
+
61
+ def generic_dialog(title, message, buttons, default_button, icon):
62
+ """
63
+ Generic dialog with custom buttons and icon
64
+
65
+ Args:
66
+ title (str): Dialog title
67
+ message (str): Dialog message
68
+ buttons (list): List of button text strings
69
+ default_button (int): Index of default button (0-based)
70
+ icon (str): Icon type (ICON_QUESTION, ICON_WARNING, ICON_ERROR, ICON_INFO)
71
+
72
+ Returns:
73
+ int: Index of clicked button (0-based)
74
+
75
+ Raises:
76
+ NotImplementedError: Tkinter messagebox doesn't support custom buttons
77
+ """
78
+ raise NotImplementedError("generic_dialog is not supported on Tkinter - custom buttons require custom dialog implementation")
@@ -0,0 +1,426 @@
1
+ """
2
+ Windows Custom Dialog Module for MCPower
3
+ A module to display Windows dialogs with 4 custom buttons:
4
+ No, No (Always), Yes, Yes (Always)
5
+
6
+ This module is specifically designed for MCPower's
7
+ user confirmation dialogs on Windows platforms.
8
+ """
9
+
10
+ import ctypes
11
+ from ctypes import wintypes
12
+ import sys
13
+ from .constants import NO, NO_ALWAYS, YES, YES_ALWAYS
14
+
15
+ # Custom button IDs (internal use)
16
+ _BUTTON_NO = 100
17
+ _BUTTON_NO_ALWAYS = 101
18
+ _BUTTON_YES = 102
19
+ _BUTTON_YES_ALWAYS = 103
20
+
21
+ # Global result storage (used internally)
22
+ _dialog_result = None
23
+
24
+ # Define WNDCLASSW structure
25
+ class _WNDCLASSW(ctypes.Structure):
26
+ """Windows WNDCLASSW structure for window class registration"""
27
+ _fields_ = [
28
+ ("style", ctypes.c_uint),
29
+ ("lpfnWndProc", ctypes.c_void_p),
30
+ ("cbClsExtra", ctypes.c_int),
31
+ ("cbWndExtra", ctypes.c_int),
32
+ ("hInstance", wintypes.HANDLE),
33
+ ("hIcon", wintypes.HANDLE),
34
+ ("hCursor", wintypes.HANDLE),
35
+ ("hbrBackground", wintypes.HANDLE),
36
+ ("lpszMenuName", ctypes.c_wchar_p),
37
+ ("lpszClassName", ctypes.c_wchar_p)
38
+ ]
39
+
40
+ def _create_dialog_window(title, main_text, buttons_config, default_button_index=0):
41
+ """
42
+ Internal function to create the Windows dialog with native styling
43
+
44
+ Args:
45
+ title (str): Window title
46
+ main_text (str): Main message text
47
+ buttons_config (list): List of (text, button_id) tuples for buttons to show
48
+ default_button_index (int): Index of default button (0-based)
49
+
50
+ Returns:
51
+ int: Button result mapped to xdialog constants
52
+ """
53
+ global _dialog_result
54
+ _dialog_result = None
55
+
56
+ user32 = ctypes.windll.user32
57
+ kernel32 = ctypes.windll.kernel32
58
+ gdi32 = ctypes.windll.gdi32
59
+
60
+ # Set proper argument types and return types for Windows API functions
61
+ user32.DefWindowProcW.argtypes = [wintypes.HWND, ctypes.c_uint, wintypes.WPARAM, wintypes.LPARAM]
62
+ user32.DefWindowProcW.restype = wintypes.LPARAM
63
+
64
+ user32.CreateWindowExW.argtypes = [wintypes.DWORD, ctypes.c_wchar_p, ctypes.c_wchar_p, wintypes.DWORD,
65
+ ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_int,
66
+ wintypes.HWND, wintypes.HMENU, wintypes.HINSTANCE, wintypes.LPVOID]
67
+ user32.CreateWindowExW.restype = wintypes.HWND
68
+
69
+ user32.RegisterClassW.argtypes = [ctypes.POINTER(_WNDCLASSW)]
70
+ user32.RegisterClassW.restype = wintypes.ATOM
71
+
72
+ user32.GetMessageW.argtypes = [ctypes.POINTER(wintypes.MSG), wintypes.HWND, ctypes.c_uint, ctypes.c_uint]
73
+ user32.GetMessageW.restype = ctypes.c_int
74
+
75
+ kernel32.GetModuleHandleW.argtypes = [ctypes.c_wchar_p]
76
+ kernel32.GetModuleHandleW.restype = wintypes.HMODULE
77
+
78
+ # Get system fonts for native appearance
79
+ def get_message_font():
80
+ """Get the system font used for dialogs/message boxes"""
81
+ try:
82
+ # Get the font used by the system for message boxes
83
+ ncm_size = 504 if sys.getwindowsversion().major >= 6 else 440
84
+ ncm = ctypes.create_string_buffer(ncm_size)
85
+ ctypes.windll.user32.SystemParametersInfoW(0x0029, ncm_size, ncm, 0) # SPI_GETNONCLIENTMETRICS
86
+ # Extract font info from NONCLIENTMETRICS
87
+ return gdi32.CreateFontW(
88
+ -14, # Height (negative for character height) - increased from -11
89
+ 0, # Width
90
+ 0, # Escapement
91
+ 0, # Orientation
92
+ 400, # Weight (FW_NORMAL)
93
+ 0, # Italic
94
+ 0, # Underline
95
+ 0, # StrikeOut
96
+ 1, # CharSet (DEFAULT_CHARSET)
97
+ 0, # OutPrecision
98
+ 0, # ClipPrecision
99
+ 5, # Quality (CLEARTYPE_QUALITY)
100
+ 0, # PitchAndFamily
101
+ "Segoe UI" # Face name
102
+ )
103
+ except:
104
+ # Fallback to default GUI font
105
+ return gdi32.GetStockObject(17) # DEFAULT_GUI_FONT
106
+
107
+ system_font = get_message_font()
108
+
109
+ # Window procedure
110
+ def WndProc(hwnd, msg, wparam, lparam):
111
+ global _dialog_result
112
+
113
+ WM_COMMAND = 0x0111
114
+ WM_CLOSE = 0x0010
115
+ WM_DESTROY = 0x0002
116
+
117
+ if msg == WM_COMMAND:
118
+ button_id = wparam & 0xFFFF
119
+ if button_id in [_BUTTON_NO, _BUTTON_NO_ALWAYS, _BUTTON_YES, _BUTTON_YES_ALWAYS]:
120
+ _dialog_result = button_id
121
+ user32.DestroyWindow(hwnd)
122
+ return 0
123
+
124
+ elif msg == WM_CLOSE:
125
+ _dialog_result = _BUTTON_NO # Default to No on close
126
+ user32.DestroyWindow(hwnd)
127
+ return 0
128
+
129
+ elif msg == WM_DESTROY:
130
+ user32.PostQuitMessage(0)
131
+ return 0
132
+
133
+ # Let Windows handle the default background and colors
134
+
135
+ return user32.DefWindowProcW(hwnd, msg, wparam, lparam)
136
+
137
+ # Window procedure callback
138
+ WNDPROC = ctypes.WINFUNCTYPE(wintypes.LPARAM, wintypes.HWND, ctypes.c_uint, wintypes.WPARAM, wintypes.LPARAM)
139
+ wndproc_callback = WNDPROC(WndProc)
140
+
141
+ # Get instance handle
142
+ hInstance = kernel32.GetModuleHandleW(None)
143
+ className = "MCPSecurityDialog"
144
+
145
+ # Register class with proper styling
146
+ wc = _WNDCLASSW()
147
+ wc.style = 0x0008 | 0x0020 # CS_DBLCLKS | CS_CLASSDC
148
+ wc.lpfnWndProc = ctypes.cast(wndproc_callback, ctypes.c_void_p)
149
+ wc.cbClsExtra = 0
150
+ wc.cbWndExtra = 0
151
+ wc.hInstance = hInstance
152
+ wc.hIcon = user32.LoadIconW(None, 32514) # IDI_QUESTION
153
+ wc.hCursor = user32.LoadCursorW(None, 32512) # IDC_ARROW
154
+ wc.hbrBackground = user32.GetSysColorBrush(15) # COLOR_3DFACE
155
+ wc.lpszMenuName = None
156
+ wc.lpszClassName = className
157
+
158
+ atom = user32.RegisterClassW(ctypes.byref(wc))
159
+ if not atom:
160
+ return None
161
+
162
+ # Calculate proper dialog dimensions with padding
163
+ padding = 25
164
+ text_padding = 20
165
+ button_padding = 20
166
+
167
+ # Estimate text dimensions (more generous calculation)
168
+ text_lines = main_text.count('\n') + 1
169
+ # Use a more generous calculation for text height
170
+ char_height = 18 # Larger character height
171
+ line_height = 24 # More generous line spacing
172
+ text_height = max(text_lines * line_height + text_padding * 2, 120)
173
+
174
+ # Calculate button area
175
+ button_count = len(buttons_config)
176
+ btn_width = 100 # Wider buttons to fit text like "Yes (Always)"
177
+ btn_height = 26 # Slightly taller buttons
178
+ btn_spacing = 8 # More space between buttons
179
+
180
+ total_btn_width = button_count * btn_width + (button_count - 1) * btn_spacing
181
+ button_area_height = btn_height + button_padding * 2
182
+
183
+ # Calculate window dimensions
184
+ min_width = max(total_btn_width + padding * 2, 550) # Wider minimum dialog width
185
+ content_width = min_width - padding * 2
186
+ window_width = min_width
187
+ window_height = text_height + button_area_height + padding + 40 # Extra space for title bar and margins
188
+
189
+ # Calculate center position
190
+ screen_width = user32.GetSystemMetrics(0)
191
+ screen_height = user32.GetSystemMetrics(1)
192
+ x = (screen_width - window_width) // 2
193
+ y = (screen_height - window_height) // 2
194
+
195
+ # Window styles for a proper dialog
196
+ WS_POPUP = 0x80000000
197
+ WS_VISIBLE = 0x10000000
198
+ WS_CAPTION = 0x00C00000
199
+ WS_SYSMENU = 0x00080000
200
+ WS_DLGFRAME = 0x00400000
201
+ WS_EX_DLGMODALFRAME = 0x00000001
202
+ WS_EX_TOPMOST = 0x00000008
203
+
204
+ # Create main window with dialog styling
205
+ hwnd = user32.CreateWindowExW(
206
+ WS_EX_DLGMODALFRAME | WS_EX_TOPMOST,
207
+ className,
208
+ title,
209
+ WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_DLGFRAME | WS_VISIBLE,
210
+ x, y, window_width, window_height,
211
+ None, # Parent window
212
+ None, # Menu
213
+ hInstance,
214
+ None # Additional data
215
+ )
216
+
217
+ if not hwnd:
218
+ try:
219
+ user32.UnregisterClassW(ctypes.c_wchar_p(className), wintypes.HINSTANCE(hInstance))
220
+ except:
221
+ pass
222
+ return None
223
+
224
+ # Create controls with proper styling
225
+ WS_CHILD = 0x40000000
226
+ WS_VISIBLE = 0x10000000
227
+ WS_TABSTOP = 0x00010000
228
+ SS_CENTER = 0x00000001
229
+ SS_CENTERIMAGE = 0x00000200
230
+ BS_PUSHBUTTON = 0x00000000
231
+ BS_DEFPUSHBUTTON = 0x00000001
232
+
233
+ # Create static text with better positioning
234
+ text_x = padding
235
+ text_y = padding
236
+ text_w = content_width
237
+ text_h = text_height - padding
238
+
239
+ # Create static text with left alignment instead of center
240
+ SS_LEFT = 0x00000000 # Left-aligned text
241
+ text_hwnd = user32.CreateWindowExW(
242
+ 0, "STATIC", main_text,
243
+ WS_VISIBLE | WS_CHILD | SS_LEFT,
244
+ text_x, text_y, text_w, text_h,
245
+ hwnd, None, hInstance, None
246
+ )
247
+
248
+ # Set font for text
249
+ if text_hwnd and system_font:
250
+ user32.SendMessageW(text_hwnd, 0x0030, system_font, 1) # WM_SETFONT
251
+
252
+ # Create buttons with proper spacing and styling
253
+ btn_y = text_height + button_padding + 10 # Extra margin from text
254
+ start_x = (window_width - total_btn_width) // 2
255
+
256
+ button_hwnds = []
257
+ for i, (btn_text, btn_id) in enumerate(buttons_config):
258
+ btn_x = start_x + i * (btn_width + btn_spacing)
259
+
260
+ # Use default button style for the specified default button
261
+ is_default = (i == default_button_index)
262
+ btn_style = BS_DEFPUSHBUTTON if is_default else BS_PUSHBUTTON
263
+
264
+ btn_hwnd = user32.CreateWindowExW(
265
+ 0, "BUTTON", btn_text,
266
+ WS_VISIBLE | WS_CHILD | WS_TABSTOP | btn_style,
267
+ btn_x, btn_y, btn_width, btn_height,
268
+ hwnd, btn_id, hInstance, None
269
+ )
270
+
271
+ # Set font for button
272
+ if btn_hwnd and system_font:
273
+ user32.SendMessageW(btn_hwnd, 0x0030, system_font, 0) # WM_SETFONT
274
+
275
+ button_hwnds.append(btn_hwnd)
276
+
277
+ # Set focus to default button
278
+ if is_default and btn_hwnd:
279
+ user32.SetFocus(btn_hwnd)
280
+
281
+ # Make dialog modal and bring to front
282
+ user32.SetWindowPos(hwnd, -1, 0, 0, 0, 0, 0x0010 | 0x0002 | 0x0001) # HWND_TOPMOST | SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE
283
+ user32.SetForegroundWindow(hwnd)
284
+ user32.BringWindowToTop(hwnd)
285
+
286
+ # Enable the parent window to be disabled (modal behavior)
287
+ parent = user32.GetWindow(hwnd, 4) # GW_OWNER
288
+ if parent:
289
+ user32.EnableWindow(parent, False)
290
+
291
+ # Simple message loop
292
+ msg = wintypes.MSG()
293
+ while True:
294
+ bRet = user32.GetMessageW(ctypes.byref(msg), None, 0, 0)
295
+ if bRet == 0: # WM_QUIT
296
+ break
297
+ elif bRet == -1: # Error
298
+ break
299
+ else:
300
+ user32.TranslateMessage(ctypes.byref(msg))
301
+ user32.DispatchMessageW(ctypes.byref(msg))
302
+
303
+ # Re-enable parent window
304
+ if parent:
305
+ user32.EnableWindow(parent, True)
306
+ user32.SetForegroundWindow(parent)
307
+
308
+ # Cleanup
309
+ if system_font:
310
+ gdi32.DeleteObject(system_font)
311
+ try:
312
+ user32.UnregisterClassW(ctypes.c_wchar_p(className), wintypes.HINSTANCE(hInstance))
313
+ except:
314
+ pass # Ignore cleanup errors
315
+
316
+ return _dialog_result
317
+
318
+ def show_confirmation_dialog(title, message, yes_always=False, no_always=False):
319
+ """
320
+ Show a Windows confirmation dialog with configurable buttons.
321
+
322
+ Args:
323
+ title (str): Window title
324
+ message (str): Main message text
325
+ yes_always (bool): Whether to show "Yes (Always)" button
326
+ no_always (bool): Whether to show "No (Always)" button
327
+
328
+ Returns:
329
+ int: One of the xdialog constants (NO, NO_ALWAYS, YES, YES_ALWAYS)
330
+ NO: If dialog was cancelled or closed
331
+ """
332
+ # Check if we're on Windows
333
+ if sys.platform != "win32":
334
+ raise OSError("This module only works on Windows")
335
+
336
+ # Build button configuration based on parameters
337
+ # Order: No, No (Always), Yes, Yes (Always)
338
+ buttons_config = []
339
+
340
+ buttons_config.append(("No", _BUTTON_NO))
341
+ if no_always:
342
+ buttons_config.append(("No (Always)", _BUTTON_NO_ALWAYS))
343
+ buttons_config.append(("Yes", _BUTTON_YES))
344
+ if yes_always:
345
+ buttons_config.append(("Yes (Always)", _BUTTON_YES_ALWAYS))
346
+
347
+ # Try to create the dialog
348
+ try:
349
+ result = _create_dialog_window(title, message, buttons_config, 2) # Default to "Yes" button
350
+
351
+ # Map internal button IDs to xdialog constants
352
+ mapping = {
353
+ _BUTTON_NO: NO,
354
+ _BUTTON_NO_ALWAYS: NO_ALWAYS,
355
+ _BUTTON_YES: YES,
356
+ _BUTTON_YES_ALWAYS: YES_ALWAYS
357
+ }
358
+
359
+ return mapping.get(result, NO) # Default to NO if unknown result
360
+
361
+ except Exception:
362
+ # Fallback to NO on any error
363
+ return NO
364
+
365
+ def is_available():
366
+ """
367
+ Check if the custom dialog is available on this platform.
368
+
369
+ Returns:
370
+ bool: True if custom dialogs can be used
371
+ """
372
+ return sys.platform == "win32"
373
+
374
+ def show_generic_dialog(title, message, buttons, default_button, icon):
375
+ """
376
+ Show a Windows generic dialog with custom buttons and icon.
377
+
378
+ Args:
379
+ title (str): Window title
380
+ message (str): Main message text
381
+ buttons (list): List of button text strings
382
+ default_button (int): Index of default button (0-based)
383
+ icon (str): Icon type (ICON_QUESTION, ICON_WARNING, ICON_ERROR, ICON_INFO)
384
+
385
+ Returns:
386
+ int: Index of clicked button (0-based), or default_button if dismissed
387
+ """
388
+ from .constants import ICON_QUESTION, ICON_WARNING, ICON_ERROR, ICON_INFO
389
+
390
+ # Check if we're on Windows
391
+ if sys.platform != "win32":
392
+ raise OSError("This module only works on Windows")
393
+
394
+ # Map icon constants (currently not used in UI, but validated)
395
+ icon_map = {
396
+ ICON_QUESTION: "question",
397
+ ICON_WARNING: "warning",
398
+ ICON_ERROR: "error",
399
+ ICON_INFO: "info"
400
+ }
401
+
402
+ if icon not in icon_map:
403
+ raise ValueError(f"Unsupported icon: {icon}")
404
+
405
+ # Build button configuration with custom button IDs starting from 100
406
+ buttons_config = []
407
+ for i, button_text in enumerate(buttons):
408
+ buttons_config.append((button_text, 100 + i))
409
+
410
+ # Try to create the dialog
411
+ try:
412
+ result = _create_dialog_window(title, message, buttons_config, default_button)
413
+
414
+ # Map internal button IDs back to indices
415
+ if result is not None and result >= 100:
416
+ button_index = result - 100
417
+ if 0 <= button_index < len(buttons):
418
+ return button_index
419
+
420
+ # Dialog was dismissed or unexpected result - return default button
421
+ return default_button
422
+
423
+ except Exception:
424
+ # Return default button on any error
425
+ return default_button
426
+