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,250 @@
1
+ import ctypes
2
+ import atexit
3
+ from ctypes import wintypes
4
+
5
+ from .constants import *
6
+ from .windows_structs import *
7
+
8
+ # dlls
9
+ user32 = ctypes.windll.user32
10
+ comdlg32 = ctypes.windll.comdlg32
11
+ shell32 = ctypes.windll.shell32
12
+ ole32 = ctypes.oledll.ole32
13
+
14
+ BUFFER_SIZE = 8192
15
+
16
+
17
+ def split_null_list(strp):
18
+ p = ctypes.cast(strp, ctypes.c_wchar_p)
19
+ v = p.value
20
+ while v:
21
+ yield v
22
+ loc = ctypes.cast(p, ctypes.c_void_p).value + (len(v)*2+2)
23
+ p = ctypes.cast(loc, ctypes.c_wchar_p)
24
+ v = p.value
25
+
26
+
27
+ def open_file(title, filetypes, multiple=False):
28
+ file = ctypes.create_unicode_buffer(BUFFER_SIZE)
29
+ pfile = ctypes.cast(file, ctypes.c_wchar_p)
30
+
31
+ # Default options
32
+ opts = tagOFNW(
33
+ lStructSize=ctypes.sizeof(tagOFNW),
34
+
35
+ lpstrFile=pfile,
36
+ nMaxFile=BUFFER_SIZE,
37
+
38
+ lpstrTitle=title,
39
+ Flags=0x00081808 + (0x200 if multiple else 0)
40
+ )
41
+
42
+ # Filetypes
43
+ if filetypes:
44
+ out = []
45
+ for s, t in filetypes:
46
+ out.append(f'{s} ({t})\0{";".join(t.split())}\0')
47
+
48
+ buf = ctypes.create_unicode_buffer(''.join(out)+'\0')
49
+
50
+ opts.lpstrFilter = LPCWSTR(ctypes.addressof(buf))
51
+ opts.lpstrDefExt = LPCWSTR(ctypes.addressof(buf))
52
+
53
+ # Call file dialog
54
+ ok = comdlg32.GetOpenFileNameW(ctypes.byref(opts))
55
+
56
+ # Return data
57
+ if multiple:
58
+ if ok:
59
+ # Windows splits the parent folder, followed by files, by null characters.
60
+ gen = split_null_list(pfile)
61
+ parent = next(gen)
62
+ return [parent + "\\" + f for f in gen] or [parent]
63
+ else:
64
+ return []
65
+ else:
66
+ if ok:
67
+ return file.value
68
+ else:
69
+ return ''
70
+
71
+ def save_file(title, filetypes):
72
+ file = ctypes.create_unicode_buffer(BUFFER_SIZE)
73
+ pfile = ctypes.cast(file, ctypes.c_wchar_p)
74
+
75
+ # Default options
76
+ opts = tagOFNW(
77
+ lStructSize=ctypes.sizeof(tagOFNW),
78
+
79
+ lpstrFile=pfile,
80
+ nMaxFile=BUFFER_SIZE,
81
+
82
+ lpstrTitle=title,
83
+ Flags=0x0008000A
84
+ )
85
+
86
+ # Filetypes
87
+ if filetypes:
88
+ out = []
89
+ for s, t in filetypes:
90
+ out.append(f'{s} ({t})\0{";".join(t.split())}\0')
91
+
92
+ buf = ctypes.create_unicode_buffer(''.join(out)+'\0')
93
+
94
+ opts.lpstrFilter = LPCWSTR(ctypes.addressof(buf))
95
+ opts.lpstrDefExt = LPCWSTR(ctypes.addressof(buf))
96
+
97
+ # Call file dialog
98
+ ok = comdlg32.GetSaveFileNameW(ctypes.byref(opts))
99
+
100
+ # Return data
101
+ if ok:
102
+ return file.value
103
+ else:
104
+ return ''
105
+
106
+ # Code simplified and turned into python bindings from the tk8.6.12/win/tkWinDialog.c file.
107
+ # tk is licensed here: https://www.tcl.tk/software/tcltk/license.html
108
+ def directory(title):
109
+ # Create dialog
110
+ ifd = ctypes.POINTER(IFileOpenDialog)()
111
+
112
+ ole32.OleInitialize(None)
113
+ try:
114
+ hr = ole32.CoCreateInstance(
115
+ ctypes.byref(ClsidFileOpenDialog),
116
+ None,
117
+ 1,
118
+ ctypes.byref(IIDIFileOpenDialog),
119
+ ctypes.byref(ifd)
120
+ )
121
+ if hr < 0: raise OSError("Failed to create dialog")
122
+
123
+ # Set options
124
+ flags = UINT(0)
125
+ hr = ifd.contents.lpVtbl.contents.GetOptions(ifd, ctypes.byref(flags))
126
+ if hr < 0: raise OSError("Failed to get options")
127
+
128
+ flags = UINT(flags.value | 0x1020)
129
+ hr = ifd.contents.lpVtbl.contents.SetOptions(ifd, flags)
130
+
131
+ # Set title
132
+ if title is not None: ifd.contents.lpVtbl.contents.SetTitle(ifd, title)
133
+
134
+ try:
135
+ hr = ifd.contents.lpVtbl.contents.Show(ifd, None)
136
+ except OSError:
137
+ return ''
138
+
139
+ # Acquire selection result
140
+ resultIf = LPIShellItem()
141
+ try:
142
+ hr = ifd.contents.lpVtbl.contents.GetResult(ifd, ctypes.byref(resultIf))
143
+ if hr < 0: raise OSError("Failed to get result of directory selection")
144
+ wstr = LPWSTR()
145
+ hr = resultIf.contents.lpVtbl.contents.GetDisplayName(resultIf, 0x80058000, ctypes.byref(wstr))
146
+ if hr < 0: raise OSError("Failed to get display name from shell item")
147
+ val = wstr.value
148
+ ole32.CoTaskMemFree(wstr)
149
+ return val
150
+ finally:
151
+ resultIf.contents.lpVtbl.contents.Release(resultIf)
152
+ finally:
153
+ ole32.OleUninitialize()
154
+
155
+
156
+ # For where the magic numbers come from, see https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-messageboxw
157
+
158
+ def info(title, message):
159
+ user32.MessageBoxW(None, message or "", title or "Message", 0x00000040)
160
+
161
+ def warning(title, message):
162
+ user32.MessageBoxW(None, message or "", title or "Warning", 0x00000030)
163
+
164
+ def error(title, message):
165
+ user32.MessageBoxW(None, message or "", title or "Error", 0x00000010)
166
+
167
+ def yesno(title, message):
168
+ if user32.MessageBoxW(None, message or "", title or "", 0x00000030) == 6:
169
+ return YES
170
+ else:
171
+ return NO
172
+
173
+ def yesnocancel(title, message):
174
+ r = user32.MessageBoxW(None, message or "", title or "", 0x00000023)
175
+
176
+ if r == 2:
177
+ return CANCEL
178
+ elif r == 6:
179
+ return YES
180
+ else:
181
+ return NO
182
+
183
+ def yesno_always(title, message, yes_always=False, no_always=False):
184
+ """
185
+ Enhanced yes/no dialog with optional always buttons
186
+ Button order: No, No Always (if enabled), Yes, Yes Always (if enabled)
187
+
188
+ Uses custom Windows dialog implementation with proper 4-button support.
189
+ Falls back to standard yesno dialog if custom dialog fails.
190
+ """
191
+ try:
192
+ from .windows_custom_dialog import show_confirmation_dialog, is_available
193
+
194
+ # Use custom dialog if available
195
+ if is_available():
196
+ return show_confirmation_dialog(title, message, yes_always, no_always)
197
+ except Exception:
198
+ pass
199
+
200
+ # Fallback to standard yesno dialog
201
+ return yesno(title, message)
202
+
203
+ def retrycancel(title, message):
204
+ if user32.MessageBoxW(None, message or "", title or "", 0x00000025) == 4:
205
+ return RETRY
206
+ else:
207
+ return CANCEL
208
+
209
+ def okcancel(title, message):
210
+ if user32.MessageBoxW(None, message or "", title or "", 0x00000021) == 1:
211
+ return OK
212
+ else:
213
+ return CANCEL
214
+
215
+ def generic_dialog(title, message, buttons, default_button, icon):
216
+ """
217
+ Generic dialog with custom buttons and icon
218
+
219
+ Args:
220
+ title (str): Dialog title
221
+ message (str): Dialog message
222
+ buttons (list): List of button text strings
223
+ default_button (int): Index of default button (0-based)
224
+ icon (str): Icon type (ICON_QUESTION, ICON_WARNING, ICON_ERROR, ICON_INFO)
225
+
226
+ Returns:
227
+ int: Index of clicked button (0-based)
228
+ """
229
+ from .constants import ICON_QUESTION, ICON_WARNING, ICON_ERROR, ICON_INFO
230
+
231
+ if icon not in [ICON_QUESTION, ICON_WARNING, ICON_ERROR, ICON_INFO]:
232
+ raise ValueError(f"Unsupported icon: {icon}")
233
+
234
+ if not isinstance(buttons, list) or len(buttons) == 0:
235
+ raise ValueError("buttons must be a non-empty list")
236
+
237
+ if not isinstance(default_button, int) or default_button < 0 or default_button >= len(buttons):
238
+ raise ValueError(f"default_button must be a valid index (0-{len(buttons)-1})")
239
+
240
+ try:
241
+ from .windows_custom_dialog import show_generic_dialog, is_available
242
+
243
+ # Use custom dialog implementation
244
+ if is_available():
245
+ return show_generic_dialog(title, message, buttons, default_button, icon)
246
+ except Exception:
247
+ pass
248
+
249
+ # This should not happen since we require custom dialog support
250
+ raise RuntimeError("Windows custom dialog implementation not available")
@@ -0,0 +1,183 @@
1
+ # Code simplified and turned into python bindings from the tk8.6.12/win/tkWinDialog.c file.
2
+ # tk is licensed here: https://www.tcl.tk/software/tcltk/license.html
3
+
4
+ import ctypes
5
+ from ctypes.wintypes import *
6
+ from typing import Tuple
7
+
8
+ LPLPWSTR = ctypes.POINTER(LPWSTR)
9
+ LPVOIDP = ctypes.POINTER(ctypes.c_void_p)
10
+
11
+ LPOFNHOOKPROC = ctypes.WINFUNCTYPE(ctypes.POINTER(UINT), HWND, UINT, WPARAM, LPARAM)
12
+
13
+
14
+ class tagOFNW(ctypes.Structure):
15
+ _fields_ = [
16
+ ("lStructSize", DWORD),
17
+ ("hwndOwner", HWND),
18
+ ("hInstance", HINSTANCE),
19
+ ("lpstrFilter", LPCWSTR),
20
+ ("lpstrCustomFilter", LPWSTR),
21
+ ("nMaxCustFilter", DWORD),
22
+ ("nFilterIndex", DWORD),
23
+ ("lpstrFile", LPWSTR),
24
+ ("nMaxFile", DWORD),
25
+ ("lpstrFileTitle", LPWSTR),
26
+ ("nMaxFileTitle", DWORD),
27
+ ("lpstrInitialDir", LPCWSTR),
28
+ ("lpstrTitle", LPCWSTR),
29
+ ("Flags", DWORD),
30
+ ("nFileOffset", WORD),
31
+ ("nFileExtension", WORD),
32
+ ("lpstrDefExt", LPCWSTR),
33
+ ("lCustData", LPARAM),
34
+ ("lpfnHook", LPOFNHOOKPROC),
35
+ ("lpTemplateName", LPCWSTR),
36
+ ("pvReserved", ctypes.c_void_p),
37
+ ("dwReserved", DWORD),
38
+ ("FlagsEx", DWORD)
39
+ ]
40
+
41
+ def __init__(self, *args, **kwargs):
42
+ ctypes.memset(ctypes.pointer(self), 0, ctypes.sizeof(tagOFNW))
43
+ super().__init__(*args, **kwargs)
44
+
45
+
46
+ class GUID(ctypes.Structure):
47
+ _fields_ = [
48
+ ("Data1", ctypes.c_ulong),
49
+ ("Data2", ctypes.c_ushort),
50
+ ("Data3", ctypes.c_ushort),
51
+ ("Data4", ctypes.c_ubyte * 8)
52
+ ]
53
+
54
+ def __init__(self, data1: int, data2: int, data3: int, data4: Tuple[int], *args, **kwargs):
55
+ super().__init__(*args, **kwargs)
56
+
57
+ self.Data1 = data1
58
+ self.Data2 = data2
59
+ self.Data3 = data3
60
+
61
+ arr = self.Data4
62
+ for i in range(8):
63
+ arr[i] = data4[i]
64
+
65
+ CLSID = GUID
66
+ IID = GUID
67
+ REFGUID = ctypes.POINTER(GUID)
68
+ REFIID = ctypes.POINTER(GUID)
69
+
70
+ ClsidFileOpenDialog = CLSID(
71
+ 0xDC1C5A9C, 0xE88A, 0X4DDE, (0xA5, 0xA1, 0x60, 0xF8, 0x2A, 0x20, 0xAE, 0xF7)
72
+ )
73
+ IIDIFileOpenDialog = IID(
74
+ 0xD57C7288, 0xD4AD, 0x4768, (0xBE, 0x02, 0x9D, 0x96, 0x95, 0x32, 0xD9, 0x60)
75
+ )
76
+
77
+
78
+
79
+ SFGAOF = ULONG
80
+ SICHINTF = DWORD
81
+ LPSFGAOF = ctypes.POINTER(SFGAOF)
82
+ LPLPOLESTR = ctypes.POINTER(LPOLESTR)
83
+ FILEOPENDIALOGOPTIONS = DWORD
84
+ LPFILEOPENDIALOGOPTIONS = ctypes.POINTER(FILEOPENDIALOGOPTIONS)
85
+
86
+ # System dependent enums, could be wrong (I believe this is c_uint)
87
+ FDAP = ctypes.c_uint
88
+ SIGDN = ctypes.c_uint
89
+ SIATTRIBFLAGS = ctypes.c_uint
90
+
91
+ # Not useful for now
92
+ IBindCtx = ctypes.c_void_p
93
+ LPIBindCtx = LPVOIDP
94
+
95
+
96
+
97
+ # Single shell item class
98
+ class IShellItem(ctypes.Structure): pass
99
+
100
+ LPIShellItem = ctypes.POINTER(IShellItem)
101
+ LPLPIShellItem = ctypes.POINTER(ctypes.POINTER(IShellItem))
102
+
103
+ class IShellItemVtbl(ctypes.Structure):
104
+ _fields_ = [
105
+ ('QueryInterface', ctypes.WINFUNCTYPE(ctypes.HRESULT, LPIShellItem, REFIID, LPVOIDP)),
106
+ ('AddRef', ctypes.WINFUNCTYPE(ULONG, LPIShellItem)),
107
+ ('Release', ctypes.WINFUNCTYPE(ULONG, LPIShellItem)),
108
+ ('BindToHandler', ctypes.WINFUNCTYPE(ctypes.HRESULT, LPIShellItem, LPIBindCtx, REFGUID, REFIID, LPVOIDP)),
109
+ ('GetParent', ctypes.WINFUNCTYPE(ctypes.HRESULT, LPIShellItem, LPLPIShellItem)),
110
+ ('GetDisplayName', ctypes.WINFUNCTYPE(ctypes.HRESULT, LPIShellItem, SIGDN, LPLPOLESTR)),
111
+ ('GetAttributes', ctypes.WINFUNCTYPE(ctypes.HRESULT, LPIShellItem, SFGAOF, LPSFGAOF)),
112
+ ('Compare', ctypes.WINFUNCTYPE(ctypes.HRESULT, LPIShellItem, LPIShellItem, SICHINTF, ctypes.POINTER(ctypes.c_int))),
113
+ ]
114
+
115
+ IShellItem.\
116
+ _fields_ = [("lpVtbl", ctypes.POINTER(IShellItemVtbl))]
117
+
118
+
119
+ # Multiple shell item class
120
+ class IShellItemArray(ctypes.Structure): pass
121
+
122
+ LPIShellItemArray = ctypes.POINTER(IShellItem)
123
+ LPLPIShellItemArray = ctypes.POINTER(ctypes.POINTER(IShellItem))
124
+
125
+ class IShellItemArrayVtbl(ctypes.Structure):
126
+ _fields_ = [
127
+ ('QueryInterface', ctypes.WINFUNCTYPE(ctypes.HRESULT, LPIShellItemArray, REFIID, LPVOIDP)),
128
+ ('AddRef', ctypes.WINFUNCTYPE(ULONG, LPIShellItemArray)),
129
+ ('Release', ctypes.WINFUNCTYPE(ULONG, LPIShellItemArray)),
130
+ ('BindToHandler', ctypes.WINFUNCTYPE(ctypes.HRESULT, LPIShellItemArray, LPIBindCtx, REFGUID, REFIID, LPVOIDP)),
131
+ ('GetPropertyStore', ctypes.WINFUNCTYPE(ctypes.HRESULT, LPIShellItemArray, ctypes.c_int, REFIID, LPVOIDP)),
132
+ ('GetPropertyDescriptionList', ctypes.WINFUNCTYPE(ctypes.HRESULT, LPIShellItemArray, ctypes.c_void_p, REFIID, LPVOIDP)),
133
+ ('GetAttributes', ctypes.WINFUNCTYPE(ctypes.HRESULT, LPIShellItemArray, SIATTRIBFLAGS, SFGAOF, LPSFGAOF)),
134
+ ('GetCount', ctypes.WINFUNCTYPE(ctypes.HRESULT, LPIShellItemArray, LPDWORD)),
135
+ ('GetItemAt', ctypes.WINFUNCTYPE(ctypes.HRESULT, LPIShellItemArray, DWORD, LPLPIShellItem)),
136
+ ('EnumItems', ctypes.WINFUNCTYPE(ctypes.HRESULT, LPIShellItemArray, LPVOIDP)),
137
+ ]
138
+
139
+ IShellItemArray.\
140
+ _fields_ = [("lpVtbl", ctypes.POINTER(IShellItemArrayVtbl))]
141
+
142
+
143
+ # file open dialog
144
+ class IFileOpenDialog(ctypes.Structure): pass
145
+
146
+ LPIFileOpenDialog = ctypes.POINTER(IFileOpenDialog)
147
+ LPTCLCOMDLG_FILTERSPEC = ctypes.c_void_p
148
+
149
+ class IFileOpenDialogVtbl(ctypes.Structure):
150
+ _fields_ = [
151
+ ('QueryInterface', ctypes.WINFUNCTYPE(ctypes.HRESULT, LPIFileOpenDialog, REFIID, LPVOIDP)),
152
+ ('AddRef', ctypes.WINFUNCTYPE(ULONG, LPIFileOpenDialog)),
153
+ ('Release', ctypes.WINFUNCTYPE(ULONG, LPIFileOpenDialog)),
154
+ ('Show', ctypes.WINFUNCTYPE(ctypes.HRESULT, LPIFileOpenDialog, HWND)),
155
+ ('SetFileTypes', ctypes.WINFUNCTYPE(ctypes.HRESULT, LPIFileOpenDialog, UINT, LPTCLCOMDLG_FILTERSPEC)),
156
+ ('SetFileTypeIndex', ctypes.WINFUNCTYPE(ctypes.HRESULT, LPIFileOpenDialog, UINT)),
157
+ ('GetFileTypeIndex', ctypes.WINFUNCTYPE(ctypes.HRESULT, LPIFileOpenDialog, LPUINT)),
158
+ ('Advise', ctypes.WINFUNCTYPE(ctypes.HRESULT, LPIFileOpenDialog, ctypes.c_void_p, LPDWORD)),
159
+ ('Unadvise', ctypes.WINFUNCTYPE(ctypes.HRESULT, LPIFileOpenDialog, DWORD)),
160
+ ('SetOptions', ctypes.WINFUNCTYPE(ctypes.HRESULT, LPIFileOpenDialog, FILEOPENDIALOGOPTIONS)),
161
+ ('GetOptions', ctypes.WINFUNCTYPE(ctypes.HRESULT, LPIFileOpenDialog, LPFILEOPENDIALOGOPTIONS)),
162
+ ('SetDefaultFolder', ctypes.WINFUNCTYPE(ctypes.HRESULT, LPIFileOpenDialog, LPIShellItem)),
163
+ ('SetFolder', ctypes.WINFUNCTYPE(ctypes.HRESULT, LPIFileOpenDialog, LPIShellItem)),
164
+ ('GetFolder', ctypes.WINFUNCTYPE(ctypes.HRESULT, LPIFileOpenDialog, LPLPIShellItem)),
165
+ ('GetCurrentSelection', ctypes.WINFUNCTYPE(ctypes.HRESULT, LPIFileOpenDialog, LPLPIShellItem)),
166
+ ('SetFileName', ctypes.WINFUNCTYPE(ctypes.HRESULT, LPIFileOpenDialog, LPCWSTR)),
167
+ ('GetFileName', ctypes.WINFUNCTYPE(ctypes.HRESULT, LPIFileOpenDialog, LPLPWSTR)),
168
+ ('SetTitle', ctypes.WINFUNCTYPE(ctypes.HRESULT, LPIFileOpenDialog, LPCWSTR)),
169
+ ('SetOkButtonLabel', ctypes.WINFUNCTYPE(ctypes.HRESULT, LPIFileOpenDialog, LPCWSTR)),
170
+ ('SetFileNameLabel', ctypes.WINFUNCTYPE(ctypes.HRESULT, LPIFileOpenDialog, LPCWSTR)),
171
+ ('GetResult', ctypes.WINFUNCTYPE(ctypes.HRESULT, LPIFileOpenDialog, LPLPIShellItem)),
172
+ ('AddPlace', ctypes.WINFUNCTYPE(ctypes.HRESULT, LPIFileOpenDialog, LPIShellItem, FDAP)),
173
+ ('SetDefaultExtension', ctypes.WINFUNCTYPE(ctypes.HRESULT, LPIFileOpenDialog, LPCWSTR)),
174
+ ('Close', ctypes.WINFUNCTYPE(ctypes.HRESULT, LPIFileOpenDialog, ctypes.HRESULT)),
175
+ ('SetClientGuid', ctypes.WINFUNCTYPE(ctypes.HRESULT, LPIFileOpenDialog, REFGUID)),
176
+ ('ClearClientData', ctypes.WINFUNCTYPE(ctypes.HRESULT, LPIFileOpenDialog)),
177
+ ('SetFilter', ctypes.WINFUNCTYPE(ctypes.HRESULT, LPIFileOpenDialog, ctypes.c_void_p)),
178
+ ('GetResults', ctypes.WINFUNCTYPE(ctypes.HRESULT, LPIFileOpenDialog, LPLPIShellItemArray)),
179
+ ('GetSelectedItems', ctypes.WINFUNCTYPE(ctypes.HRESULT, LPIFileOpenDialog, LPLPIShellItemArray)),
180
+ ]
181
+
182
+ IFileOpenDialog.\
183
+ _fields_ = [("lpVtbl", ctypes.POINTER(IFileOpenDialogVtbl))]
@@ -0,0 +1,236 @@
1
+ import subprocess
2
+ from os.path import isfile
3
+ from typing import Tuple
4
+
5
+ from .constants import *
6
+
7
+ def clean(txt: str):
8
+ return txt\
9
+ .replace("\\", "\\\\")\
10
+ .replace("$", "\\$")\
11
+ .replace("!", "\\!")\
12
+ .replace("*", "\\*")\
13
+ .replace("?", "\\?")\
14
+ .replace("&", "&amp;")\
15
+ .replace("|", "&#124;")\
16
+ .replace("<", "&lt;")\
17
+ .replace(">", "&gt;")\
18
+ .replace("(", "\\(")\
19
+ .replace(")", "\\)")\
20
+ .replace("[", "\\[")\
21
+ .replace("]", "\\]")\
22
+ .replace("{", "\\{")\
23
+ .replace("}", "\\}")\
24
+
25
+ def yad(typ, filetypes=None, **kwargs) -> Tuple[int, str]:
26
+ # Build args based on keywords
27
+ args = ['yad', '--'+typ]
28
+ for k, v in kwargs.items():
29
+ vv = v
30
+ if not isinstance(v, list):
31
+ vv = [v]
32
+
33
+ for vvv in vv:
34
+ if vvv is True:
35
+ args.append(f'--{k.replace("_", "-").strip("-")}')
36
+ elif isinstance(vvv, str):
37
+ cv = clean(vvv) if k != "title" else vvv
38
+ args.append(f'--{k.replace("_", "-").strip("-")}={cv}')
39
+
40
+ # Build filetypes specially if specified
41
+ if filetypes:
42
+ for name, globs in filetypes:
43
+ if name:
44
+ globlist = globs.split()
45
+ args.append(f'--file-filter={name.replace("|", "")} ({", ".join(t for t in globlist)})|{globs}')
46
+
47
+ proc = subprocess.Popen(
48
+ args,
49
+ stdout=subprocess.PIPE,
50
+ stderr=subprocess.DEVNULL,
51
+ shell=False
52
+ )
53
+ stdout, _ = proc.communicate()
54
+
55
+ return (proc.returncode, stdout.decode('utf-8').strip())
56
+
57
+
58
+ def open_file(title, filetypes, multiple=False):
59
+ # Yad is strange and will let you select folders for some reason in some cases. So we filter those out.
60
+ if multiple:
61
+ files = yad('file', title=(title or ""), filetypes=filetypes, multiple=True, separator="\n", width="800", height="600")[1].splitlines()
62
+ return list(filter(isfile, files))
63
+ else:
64
+ file = yad('file', title=(title or ""), filetypes=filetypes, width="800", height="600")[1]
65
+ if file and isfile(file):
66
+ return file
67
+ else:
68
+ return ''
69
+
70
+ def save_file(title, filetypes):
71
+ return yad('file', title=(title or ""), filetypes=filetypes, save=True, width="800", height="600")[1]
72
+
73
+ def directory(title):
74
+ return yad("file", title=(title or ""), directory=True, width="800", height="600")[1]
75
+
76
+ def info(title, message):
77
+ yad(
78
+ "info",
79
+ title=(title or ""),
80
+ text=message,
81
+ image="dialog-information",
82
+ window_icon="dialog-information",
83
+ width="350",
84
+ button=f"OK:{OK}"
85
+ )
86
+
87
+ def warning(title, message):
88
+ yad(
89
+ "warning",
90
+ title=(title or ""),
91
+ text=message,
92
+ image="dialog-warning",
93
+ window_icon="dialog-warning",
94
+ width="350",
95
+ button=f"OK:{OK}"
96
+ )
97
+
98
+ def error(title, message):
99
+ yad(
100
+ "error",
101
+ title=(title or ""),
102
+ text=message,
103
+ image="dialog-error",
104
+ window_icon="dialog-error",
105
+ width="350",
106
+ button=f"OK:{OK}"
107
+ )
108
+
109
+ def yesno(title, message):
110
+ r = yad(
111
+ "question",
112
+ title=(title or ""),
113
+ text=message,
114
+ image="dialog-question",
115
+ window_icon="dialog-question",
116
+ width="350",
117
+ button=[f"No:{NO}", f"Yes:{YES}"]
118
+ )[0]
119
+ return NO if r > 128 or r < 0 else r
120
+
121
+ def yesno_always(title, message, yes_always=False, no_always=False):
122
+ """
123
+ Enhanced yes/no dialog with optional always buttons
124
+ Button order: No, No Always (if enabled), Yes, Yes Always (if enabled)
125
+ """
126
+ buttons = []
127
+
128
+ # Build button list in order: No, No Always, Yes, Yes Always
129
+ buttons.append(f"No:{NO}")
130
+ if no_always:
131
+ buttons.append(f"No (Always):{NO_ALWAYS}")
132
+ buttons.append(f"Yes:{YES}")
133
+ if yes_always:
134
+ buttons.append(f"Yes (Always):{YES_ALWAYS}")
135
+
136
+ r = yad(
137
+ "question",
138
+ title=(title or ""),
139
+ text=message,
140
+ image="dialog-question",
141
+ window_icon="dialog-question",
142
+ width="350",
143
+ button=buttons
144
+ )[0]
145
+ return NO if r > 128 or r < 0 else r
146
+
147
+ def yesnocancel(title, message):
148
+ r = yad(
149
+ "question",
150
+ title=(title or ""),
151
+ text=message,
152
+ image="dialog-question",
153
+ window_icon="dialog-question",
154
+ width="350",
155
+ button=[f"Cancel:{CANCEL}", f"No:{NO}", f"Yes:{YES}"]
156
+ )[0]
157
+ return CANCEL if r > 128 or r < 0 else r
158
+
159
+ def retrycancel(title, message):
160
+ r = yad(
161
+ "question",
162
+ title=(title or ""),
163
+ text=message,
164
+ image="dialog-question",
165
+ window_icon="dialog-question",
166
+ width="350",
167
+ button=[f"Cancel:{CANCEL}", f"Retry:{RETRY}"]
168
+ )[0]
169
+ return CANCEL if r > 128 or r < 0 else r
170
+
171
+ def okcancel(title, message):
172
+ r = yad(
173
+ "question",
174
+ title=(title or ""),
175
+ text=message,
176
+ image="dialog-question",
177
+ window_icon="dialog-question",
178
+ width="350",
179
+ button=[f"Cancel:{CANCEL}", f"OK:{OK}"]
180
+ )[0]
181
+ return CANCEL if r > 128 or r < 0 else r
182
+
183
+ def generic_dialog(title, message, buttons, default_button, icon):
184
+ """
185
+ Generic dialog with custom buttons and icon
186
+
187
+ Args:
188
+ title (str): Dialog title
189
+ message (str): Dialog message
190
+ buttons (list): List of button text strings
191
+ default_button (int): Index of default button (0-based)
192
+ icon (str): Icon type (ICON_QUESTION, ICON_WARNING, ICON_ERROR, ICON_INFO)
193
+
194
+ Returns:
195
+ int: Index of clicked button (0-based)
196
+ """
197
+ from .constants import ICON_QUESTION, ICON_WARNING, ICON_ERROR, ICON_INFO
198
+
199
+ # Map icon constants to YAD icons
200
+ icon_map = {
201
+ ICON_QUESTION: "dialog-question",
202
+ ICON_WARNING: "dialog-warning",
203
+ ICON_ERROR: "dialog-error",
204
+ ICON_INFO: "dialog-information"
205
+ }
206
+
207
+ if icon not in icon_map:
208
+ raise ValueError(f"Unsupported icon: {icon}")
209
+
210
+ if not isinstance(buttons, list) or len(buttons) == 0:
211
+ raise ValueError("buttons must be a non-empty list")
212
+
213
+ if not isinstance(default_button, int) or default_button < 0 or default_button >= len(buttons):
214
+ raise ValueError(f"default_button must be a valid index (0-{len(buttons)-1})")
215
+
216
+ # Build button list with custom text and return codes (button index)
217
+ button_args = []
218
+ for i, button_text in enumerate(buttons):
219
+ button_args.append(f"{button_text}:{i}")
220
+
221
+ r = yad(
222
+ "question",
223
+ title=(title or ""),
224
+ text=message,
225
+ image=icon_map[icon],
226
+ window_icon=icon_map[icon],
227
+ width="350",
228
+ button=button_args
229
+ )[0]
230
+
231
+ # Handle cancellation/dismissal - return default button index
232
+ if r > 128 or r < 0:
233
+ return default_button
234
+
235
+ # YAD returns the button index we specified
236
+ return r