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,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("&", "&")\
|
|
15
|
+
.replace("|", "|")\
|
|
16
|
+
.replace("<", "<")\
|
|
17
|
+
.replace(">", ">")\
|
|
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
|