-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathPyDisplay.pyw
More file actions
8588 lines (7701 loc) · 399 KB
/
Copy pathPyDisplay.pyw
File metadata and controls
8588 lines (7701 loc) · 399 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import tkinter as tk
import tkinter.filedialog as _fd
import threading
import time
import json
import os
import datetime
import importlib
import importlib.metadata as _importlib_metadata
import subprocess
import sys
import traceback
import urllib.request
import socket
import ctypes
import glob
# ── distutils.version shim (removed in Python 3.12+, needed by GPUtil) ───────
# Inject before anything else so GPUtil imports succeed in frozen EXEs too.
def _inject_distutils_shim():
if "distutils.version" not in sys.modules:
import types as _t
_injected = False
try:
import setuptools # noqa — activates shim on 3.12+
import distutils.version
_injected = True
except Exception:
pass
if not _injected:
if "distutils" not in sys.modules:
sys.modules["distutils"] = _t.ModuleType("distutils")
# distutils.version
_dv = _t.ModuleType("distutils.version")
try:
from packaging.version import Version
_dv.LooseVersion = Version
_dv.StrictVersion = Version
except Exception:
class _FallbackVersion:
def __init__(self, v=""): self.vstring = str(v)
def __str__(self): return self.vstring
def __repr__(self): return f"Version('{self.vstring}')"
def __lt__(self, o): return self.vstring < str(o)
def __le__(self, o): return self.vstring <= str(o)
def __eq__(self, o): return self.vstring == str(o)
def __ge__(self, o): return self.vstring >= str(o)
def __gt__(self, o): return self.vstring > str(o)
_dv.LooseVersion = _FallbackVersion
_dv.StrictVersion = _FallbackVersion
sys.modules["distutils.version"] = _dv
sys.modules["distutils"].version = _dv
# distutils.spawn
_ds = _t.ModuleType("distutils.spawn")
import shutil as _shutil
_ds.find_executable = _shutil.which
_ds.spawn = lambda cmd, **kw: None
sys.modules["distutils.spawn"] = _ds
sys.modules["distutils"].spawn = _ds
# distutils.util
_du = _t.ModuleType("distutils.util")
_du.strtobool = lambda v: 1 if str(v).lower() in ("y","yes","t","true","on","1") else 0
sys.modules["distutils.util"] = _du
sys.modules["distutils"].util = _du
# distutils.errors
_de = _t.ModuleType("distutils.errors")
for _en in ("DistutilsError","DistutilsModuleError","DistutilsClassError",
"DistutilsGetoptError","DistutilsArgError","DistutilsFileError",
"DistutilsOptionError","DistutilsSetupError","DistutilsPlatformError",
"DistutilsExecError","DistutilsInternalError","DistutilsTemplateError",
"DistutilsByteCompileError","CCompilerError","CompileError",
"PreprocessError","LinkError","LibError","UnknownFileError"):
setattr(_de, _en, type(_en, (Exception,), {}))
sys.modules["distutils.errors"] = _de
sys.modules["distutils"].errors = _de
_inject_distutils_shim()
# ─────────────────────────────────────────────────────────────────────────────
# ── App-wide constants ────────────────────────────────────────────────────────
# Suppress console windows spawned by subprocess calls on Windows
_NO_WIN = subprocess.CREATE_NO_WINDOW if hasattr(subprocess, "CREATE_NO_WINDOW") else 0x08000000
# ── Portable Mode Detection ───────────────────────────────────────────────────
# Check if we're running from inside a folder named "PyDisplay" (portable mode)
def _detect_portable_mode():
"""Return True if the script is in a folder named 'PyDisplay', False otherwise."""
try:
script_dir = os.path.dirname(os.path.abspath(__file__))
folder_name = os.path.basename(script_dir)
return folder_name.lower() == "pydisplay"
except Exception:
return False
_portable_mode_detected = _detect_portable_mode()
# Config / data paths — single source of truth used everywhere
# If portable mode detected, use script dir; otherwise use APPDATA
if _portable_mode_detected:
_APP_DIR = os.path.dirname(os.path.abspath(__file__))
else:
_APP_DIR = os.path.join(os.environ.get("APPDATA", ""), "PyDisplay")
_CFG_PATH = os.path.join(_APP_DIR, "PyDisplay_pos.json")
_LOG_PATH = os.path.join(_APP_DIR, "PyDisplay_error.log")
_DATA_LOG_PATH = os.path.join(_APP_DIR, "PyDisplay_log.txt")
_DEFAULT_THEME_PATH = os.path.join(_APP_DIR, "PyDisplay_theme_Default.json")
_THEME_DIR = _APP_DIR # themes live alongside the config
_FONT = "Courier New" # single source of truth for the app font
_BASE_FONT_SIZE = 9 # default font size; all remap logic is relative to this
_CONFIG_VERSION = 1 # increment when config schema changes; triggers migration
_APP_VERSION = "1.1.1" # increment on each release; checked against GitHub latest tag
_GITHUB_REPO = "VisaHolder/PyDisplay" # account renamed from reaprrr 2026-06-05
# Default display section order — defined once, referenced everywhere
_DEFAULT_SECTION_ORDER = ["gpu", "cpu", "mem", "net", "disk", "storage"]
# Win32 constants used by click-through and popup z-order pinning
_HWND_TOPMOST = -1
_SWP_NOSIZE = 0x0001
_SWP_NOMOVE = 0x0002
_SWP_NOACTIVATE = 0x0010
_GWL_EXSTYLE = -20
_WS_EX_LAYERED = 0x80000
_WS_EX_TRANSPARENT = 0x20
_WS_EX_TOOLWINDOW = 0x80
_WS_EX_APPWINDOW = 0x40000
_LWA_ALPHA = 0x2
_user32 = ctypes.windll.user32
# ── Memory Cleaner — Win32 constants & helpers ────────────────────────────────
_MC_TOKEN_ADJUST_PRIVILEGES = 0x0020
_MC_TOKEN_QUERY = 0x0008
_MC_SE_PRIVILEGE_ENABLED = 0x00000002
_MC_PROCESS_QUERY_INFO = 0x0400
_MC_PROCESS_SET_QUOTA = 0x0100
_MC_SystemFileCacheInfo = 0x15
_MC_SystemMemListInfo = 0x50
_MC_SystemCombinePhysMem = 0x82
_MC_SystemRegistryRecon = 0x9E
_MC_MemEmptyWorkingSet = 2
_MC_MemFlushModified = 3
_MC_MemPurgeStandby = 4
_MC_MemPurgeLowStandby = 5
class _MC_LUID(ctypes.Structure):
_fields_ = [("LowPart", ctypes.c_ulong), ("HighPart", ctypes.c_long)]
class _MC_LUID_ATTR(ctypes.Structure):
_fields_ = [("Luid", _MC_LUID), ("Attributes", ctypes.c_ulong)]
class _MC_TOKEN_PRIVS(ctypes.Structure):
_fields_ = [("PrivilegeCount", ctypes.c_ulong), ("Privileges", _MC_LUID_ATTR * 1)]
class _MC_FILECACHE_INFO(ctypes.Structure):
_fields_ = [("CurrentSize", ctypes.c_size_t), ("PeakSize", ctypes.c_size_t),
("PageFaultCount", ctypes.c_ulong), ("MinimumWorkingSet", ctypes.c_size_t),
("MaximumWorkingSet", ctypes.c_size_t), ("Flags", ctypes.c_ulong)]
class _MC_COMBINE_INFO(ctypes.Structure):
_fields_ = [("Handle", ctypes.c_void_p), ("PagesCombined", ctypes.c_ulonglong),
("Flags", ctypes.c_ulong)]
def _mc_enable_privilege(name):
advapi32 = ctypes.windll.advapi32
kernel32 = ctypes.windll.kernel32
advapi32.OpenProcessToken.argtypes = [ctypes.c_void_p, ctypes.c_ulong, ctypes.POINTER(ctypes.c_void_p)]
advapi32.OpenProcessToken.restype = ctypes.c_int
advapi32.LookupPrivilegeValueW.argtypes = [ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.POINTER(_MC_LUID)]
advapi32.LookupPrivilegeValueW.restype = ctypes.c_int
advapi32.AdjustTokenPrivileges.restype = ctypes.c_int
h = ctypes.c_void_p()
if not advapi32.OpenProcessToken(kernel32.GetCurrentProcess(),
_MC_TOKEN_ADJUST_PRIVILEGES | _MC_TOKEN_QUERY,
ctypes.byref(h)):
return False
luid = _MC_LUID()
if not advapi32.LookupPrivilegeValueW(None, name, ctypes.byref(luid)):
kernel32.CloseHandle(h); return False
tp = _MC_TOKEN_PRIVS()
tp.PrivilegeCount = 1; tp.Privileges[0].Luid = luid
tp.Privileges[0].Attributes = _MC_SE_PRIVILEGE_ENABLED
advapi32.AdjustTokenPrivileges(h, False, ctypes.byref(tp), ctypes.sizeof(tp), None, None)
ok = kernel32.GetLastError() == 0
kernel32.CloseHandle(h)
return ok
def _mc_set_mem_list(cmd_val):
cmd = ctypes.c_int(cmd_val)
return ctypes.windll.ntdll.NtSetSystemInformation(
_MC_SystemMemListInfo, ctypes.byref(cmd), ctypes.sizeof(cmd)) == 0
def _mc_get_ram_mb():
"""Return (total_mb, used_mb, free_mb)."""
class _MEMSTATUS(ctypes.Structure):
_fields_ = [("dwLength", ctypes.c_ulong), ("dwMemoryLoad", ctypes.c_ulong),
("ullTotalPhys", ctypes.c_ulonglong), ("ullAvailPhys", ctypes.c_ulonglong),
("ullTotalPageFile", ctypes.c_ulonglong), ("ullAvailPageFile", ctypes.c_ulonglong),
("ullTotalVirtual", ctypes.c_ulonglong), ("ullAvailVirtual", ctypes.c_ulonglong),
("ullAvailExtVirtual", ctypes.c_ulonglong)]
ms = _MEMSTATUS(); ms.dwLength = ctypes.sizeof(ms)
ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(ms))
total = ms.ullTotalPhys / (1024**2)
free = ms.ullAvailPhys / (1024**2)
return total, total - free, free
def _mc_run(aggressive=False, log_cb=None, clear_clipboard=True):
"""
Run the full memory cleaning sequence in a background thread.
log_cb(msg) is called with status lines for the UI to display.
clear_clipboard=False is used by the timed auto-cleaner — silently
wiping the clipboard on a timer would be data loss, not cleaning.
Returns MB freed (float).
"""
def _log(msg):
if log_cb: log_cb(msg)
ntdll = ctypes.windll.ntdll
kernel32 = ctypes.windll.kernel32
psapi = ctypes.windll.psapi
_, used_before, _ = _mc_get_ram_mb()
# 1. Privileges
for priv in ("SeDebugPrivilege", "SeIncreaseQuotaPrivilege",
"SeMaintainVolumePrivilege", "SeProfileSingleProcessPrivilege"):
_mc_enable_privilege(priv)
def _trim_all_working_sets():
"""EmptyWorkingSet + min-size every accessible process. Returns count trimmed."""
pid_arr = (ctypes.c_ulong * 8192)()
bytes_ret = ctypes.c_ulong()
psapi.EnumProcesses(ctypes.byref(pid_arr), ctypes.sizeof(pid_arr), ctypes.byref(bytes_ret))
n_pids = bytes_ret.value // ctypes.sizeof(ctypes.c_ulong)
ok = 0
for i in range(n_pids):
pid = pid_arr[i]
if not pid: continue
h = kernel32.OpenProcess(_MC_PROCESS_QUERY_INFO | _MC_PROCESS_SET_QUOTA, False, pid)
if h:
psapi.EmptyWorkingSet(h)
kernel32.SetProcessWorkingSetSizeEx(h, ctypes.c_size_t(-1), ctypes.c_size_t(-1), 0)
kernel32.CloseHandle(h); ok += 1
return ok
# 2. Flush process working sets
_log("Flushing process working sets…")
_log(f" Trimmed {_trim_all_working_sets()} processes")
# 3. System working set
_log("Flushing system working set…")
_mc_set_mem_list(_MC_MemEmptyWorkingSet)
# 4. Modified page list
_log("Flushing modified page list…")
_mc_set_mem_list(_MC_MemFlushModified)
# 5. File cache
_log("Clearing file system cache…")
info = _MC_FILECACHE_INFO()
ret_len = ctypes.c_ulong(0)
st = ntdll.NtQuerySystemInformation(_MC_SystemFileCacheInfo, ctypes.byref(info),
ctypes.sizeof(info), ctypes.byref(ret_len))
if st == 0:
info.MinimumWorkingSet = ctypes.c_size_t(-1).value
info.MaximumWorkingSet = ctypes.c_size_t(-1).value
ntdll.NtSetSystemInformation(_MC_SystemFileCacheInfo, ctypes.byref(info), ctypes.sizeof(info))
# 6. Registry cache
_log("Flushing registry cache…")
ntdll.NtSetSystemInformation(_MC_SystemRegistryRecon, None, 0)
# 7. Combine duplicate pages
_log("Combining duplicate memory pages…")
ci = _MC_COMBINE_INFO()
st = ntdll.NtSetSystemInformation(_MC_SystemCombinePhysMem, ctypes.byref(ci), ctypes.sizeof(ci))
if st == 0 and ci.PagesCombined:
_log(f" Combined {ci.PagesCombined:,} pages ({ci.PagesCombined*4//1024} MB)")
# 8. Low-memory notification
_log("Signalling low-memory event…")
cmd = ctypes.c_int(1)
ntdll.NtSetSystemInformation(0x4A, ctypes.byref(cmd), ctypes.sizeof(cmd))
# 9. Heap compact
_log("Compacting heaps…")
kernel32.HeapCompact.restype = ctypes.c_size_t
kernel32.HeapCompact.argtypes = [ctypes.c_void_p, ctypes.c_ulong]
heap_arr = (ctypes.c_void_p * 64)()
n = kernel32.GetProcessHeaps(64, ctypes.byref(heap_arr))
for i in range(min(n, 64)):
if heap_arr[i]: kernel32.HeapCompact(heap_arr[i], 0)
# 10. DNS cache
_log("Flushing DNS cache…")
try: ctypes.windll.dnsapi.DnsFlushResolverCache()
except Exception: pass
# 11. Clipboard (manual cleans only — see clear_clipboard docstring note)
if clear_clipboard:
_log("Clearing clipboard…")
u32 = ctypes.windll.user32
if u32.OpenClipboard(None): u32.EmptyClipboard(); u32.CloseClipboard()
# 12+13. Aggressive: standby lists
if aggressive:
_log("Purging low-priority standby list…")
_mc_set_mem_list(_MC_MemPurgeLowStandby)
_log("Purging full standby list…")
_mc_set_mem_list(_MC_MemPurgeStandby)
# 14. Second working-set sweep — purging the standby list faults pages
# back into other processes' working sets; re-trimming reclaims them.
_log("Re-trimming working sets (post-standby)…")
_log(f" Re-trimmed {_trim_all_working_sets()} processes")
# 15. Second modified-page flush — standby purge can dirty pages
_log("Re-flushing modified pages (post-standby)…")
_mc_set_mem_list(_MC_MemFlushModified)
# 15. Second combine pass — newly freed pages are now combinable
_log("Second combine pass…")
ci2 = _MC_COMBINE_INFO()
st2 = ntdll.NtSetSystemInformation(_MC_SystemCombinePhysMem, ctypes.byref(ci2), ctypes.sizeof(ci2))
if st2 == 0 and ci2.PagesCombined:
_log(f" Combined {ci2.PagesCombined:,} more pages ({ci2.PagesCombined*4//1024} MB)")
# 16. Force Python garbage collection
_log("Collecting Python garbage…")
import gc as _gc
_gc.collect(2)
# 17. Trim our own process working set to minimum
_log("Trimming own process…")
self_h = kernel32.OpenProcess(_MC_PROCESS_QUERY_INFO | _MC_PROCESS_SET_QUOTA,
False, kernel32.GetCurrentProcessId())
if self_h:
psapi.EmptyWorkingSet(self_h)
kernel32.SetProcessWorkingSetSizeEx(self_h, ctypes.c_size_t(-1), ctypes.c_size_t(-1), 0)
kernel32.CloseHandle(self_h)
# 18. Compact all heaps a second time (aggressive heap trim)
_log("Final heap compaction…")
n = kernel32.GetProcessHeaps(64, ctypes.byref(heap_arr))
for i in range(min(n, 64)):
if heap_arr[i]: kernel32.HeapCompact(heap_arr[i], 0)
_, used_after, _ = _mc_get_ram_mb()
freed = used_before - used_after
return freed
# ── Config helpers ────────────────────────────────────────────────────────────
def _read_config():
"""Read and return the config dict from disk. Returns {} on any failure."""
try:
with open(_CFG_PATH) as _f:
return json.load(_f)
except Exception:
return {}
def _write_config(data):
"""Merge data into the existing config and write it back to disk.
Atomic: write to a temp file in the same dir, then os.replace() it over the
real config. A crash or power loss mid-write can never truncate/corrupt the
live config (which previously caused _read_config to return {} and reset
every setting to defaults)."""
try:
os.makedirs(_APP_DIR, exist_ok=True)
existing = _read_config()
existing.update(data)
_tmp = _CFG_PATH + ".tmp"
with open(_tmp, "w") as _f:
json.dump(existing, _f, indent=2)
_f.flush()
os.fsync(_f.fileno())
os.replace(_tmp, _CFG_PATH)
except Exception as e:
_log_error("_write_config", e)
def _append_to_log(text):
"""Append raw text to the error log file. Creates the file/dir if needed."""
try:
os.makedirs(_APP_DIR, exist_ok=True)
with open(_LOG_PATH, "a", encoding="utf-8") as f:
f.write(text)
except Exception:
pass
def _write_crash_log(exc_text):
"""Write an unhandled exception to PyDisplay_error.log."""
ts = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
_append_to_log(f"\n{'─'*72}\n CRASH — {ts}\n{'─'*72}\n{exc_text}\n")
def _log_error(context, exc):
"""Append a non-fatal error to the error log with context label."""
ts = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
_append_to_log(f"\n [{ts}] {context}: {exc}\n")
def _can_import(imp_name):
"""Check if a package is importable. imp_name may be 'A|B' to try alternatives.
Purges sys.modules before checking so post-uninstall state is always accurate."""
for name in imp_name.split("|"):
name = name.strip()
# Remove any cached entry so we always hit the filesystem
for key in list(sys.modules.keys()):
if key == name or key.startswith(name + "."):
sys.modules.pop(key, None)
importlib.invalidate_caches()
# GPUtil uses distutils.version which was removed in Python 3.12+.
# Inject a manual shim so the import check succeeds in both live and frozen EXE contexts.
if name == "GPUtil":
for key in list(sys.modules.keys()):
if key == "distutils" or key.startswith("distutils."):
sys.modules.pop(key, None)
importlib.invalidate_caches()
try:
import setuptools # noqa — activates distutils shim
import distutils.version
except Exception:
pass
# If distutils.version is still missing (frozen EXE / Python 3.12+),
# inject a minimal shim so GPUtil's top-level import doesn't crash.
if "distutils" not in sys.modules or "distutils.version" not in sys.modules:
import types
if "distutils" not in sys.modules:
sys.modules["distutils"] = types.ModuleType("distutils")
# distutils.version shim
_dv = types.ModuleType("distutils.version")
try:
from packaging.version import Version
_dv.LooseVersion = Version
_dv.StrictVersion = Version
except Exception:
class _FallbackVersion:
def __init__(self, v=""): self.vstring = str(v)
def __str__(self): return self.vstring
def __repr__(self): return f"Version('{self.vstring}')"
def __lt__(self, o): return self.vstring < str(o)
def __le__(self, o): return self.vstring <= str(o)
def __eq__(self, o): return self.vstring == str(o)
def __ge__(self, o): return self.vstring >= str(o)
def __gt__(self, o): return self.vstring > str(o)
_dv.LooseVersion = _FallbackVersion
_dv.StrictVersion = _FallbackVersion
sys.modules["distutils.version"] = _dv
sys.modules["distutils"].version = _dv
# distutils.spawn shim
_ds = types.ModuleType("distutils.spawn")
import shutil as _shutil
_ds.find_executable = _shutil.which
_ds.spawn = lambda cmd, **kw: None
sys.modules["distutils.spawn"] = _ds
sys.modules["distutils"].spawn = _ds
# distutils.util shim
_du = types.ModuleType("distutils.util")
_du.strtobool = lambda v: 1 if str(v).lower() in ("y","yes","t","true","on","1") else 0
sys.modules["distutils.util"] = _du
sys.modules["distutils"].util = _du
# distutils.errors shim
_de = types.ModuleType("distutils.errors")
for _en in ("DistutilsError","DistutilsModuleError","DistutilsClassError",
"DistutilsGetoptError","DistutilsArgError","DistutilsFileError",
"DistutilsOptionError","DistutilsSetupError","DistutilsPlatformError",
"DistutilsExecError","DistutilsInternalError","DistutilsTemplateError",
"DistutilsByteCompileError","CCompilerError","CompileError",
"PreprocessError","LinkError","LibError","UnknownFileError"):
setattr(_de, _en, type(_en, (Exception,), {}))
sys.modules["distutils.errors"] = _de
sys.modules["distutils"].errors = _de
try:
importlib.import_module(name)
return True
except Exception as _e:
_log_error(f"_can_import({name})", _e)
continue
return False
# ── Dependency checker — runs before anything else ───────────────────────────
def _make_titlebar(window, bg, border, subtext, red, on_close=None,
title_text=None, title_fg=None, title_bg=None,
separator_color=None):
"""
Build a standard draggable title bar on *window* and return the bar frame.
Adds a close button (✕) on the left that calls on_close (defaults to
window.destroy). If title_text is given, a centred label is placed as the
drag handle; otherwise an invisible spacer fills that role.
A 1-px separator is packed below the bar automatically.
"""
drag = {"x": 0, "y": 0}
def _drag_start(e): drag["x"] = e.x_root; drag["y"] = e.y_root
def _drag_move(e):
dx = e.x_root - drag["x"]; dy = e.y_root - drag["y"]
window.geometry(f"+{window.winfo_x()+dx}+{window.winfo_y()+dy}")
drag["x"] = e.x_root; drag["y"] = e.y_root
tb = tk.Frame(window, bg=bg, height=28)
tb.pack(fill="x")
tb.pack_propagate(False)
x_btn = tk.Label(tb, text=" ✕ ", bg=border, fg=subtext,
font=(_FONT, _BASE_FONT_SIZE, "bold"), cursor="hand2", padx=2, pady=2)
x_btn.pack(side="left", padx=(4, 0))
_close = on_close if on_close else window.destroy
x_btn.bind("<Button-1>", lambda e: _close())
x_btn.bind("<Enter>", lambda e: x_btn.config(fg=red))
x_btn.bind("<Leave>", lambda e: x_btn.config(fg=subtext))
if title_text:
handle = tk.Label(tb, text=title_text, bg=title_bg or bg,
fg=title_fg or subtext,
font=(_FONT, _BASE_FONT_SIZE, "bold"), cursor="fleur")
handle.pack(side="left", fill="both", expand=True)
else:
handle = tk.Label(tb, text="", bg=bg, cursor="fleur")
handle.pack(side="left", fill="both", expand=True)
for w in (tb, handle):
w.bind("<ButtonPress-1>", _drag_start)
w.bind("<B1-Motion>", _drag_move)
sep_color = separator_color or border
tk.Frame(window, bg=sep_color, height=1).pack(fill="x")
return tb
def _has_nvidia_gpu():
"""Return True if an NVIDIA GPU is detected on this system."""
# Method 1: check for nvidia-smi or NVIDIA driver DLL in system dirs
_system32 = os.path.join(os.environ.get("SystemRoot", r"C:\Windows"), "System32")
if os.path.exists(os.path.join(_system32, "nvapi64.dll")) or os.path.exists(os.path.join(_system32, "nvcuda.dll")):
return True
# Method 2: check Windows registry for NVIDIA display adapter
try:
import winreg
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE,
r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\OpenGLDrivers") as _k:
i = 0
while True:
try:
name, _, _ = winreg.EnumValue(_k, i)
if "nvidia" in name.lower() or "nvoglv" in name.lower():
return True
i += 1
except OSError:
break
except Exception:
pass
# Method 3: scan device manager via registry
try:
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE,
r"SYSTEM\CurrentControlSet\Enum\PCI") as _k:
i = 0
while True:
try:
sub = winreg.EnumKey(_k, i)
# NVIDIA PCI vendor ID is 10DE
if "VEN_10DE" in sub.upper():
return True
i += 1
except OSError:
break
except Exception:
pass
return False
# ── App-wide colour palette — defined here so _run_dependency_check can use ──
# them before psutil is imported. _load_position() overwrites these at startup
# with the saved theme values.
BG = "#0a0a0f"
PANEL = "#111118"
BORDER = "#1e1e2e"
ACCENT1 = "#00ffe5"
ACCENT2 = "#ff6b35"
ACCENT3 = "#7b30d1"
DIM = "#3a3a5c"
TEXT = "#e0e0f0"
SUBTEXT = "#6868a0"
RED = "#ff3860"
GREEN = "#39ff7f"
YELLOW = "#ffcc00"
def _pip_uninstall_pkg(pip_name, imp_name):
"""
Uninstall *pip_name* and verify the import is gone.
Handles pynvml / GPUtil alias candidates and falls back to manual
file removal when pip has no dist-info record.
Raises RuntimeError if the package is still importable afterwards.
"""
_pip_candidates = (["pynvml", "nvidia-ml-py"] if pip_name == "pynvml"
else ["GPUtil", "gputil"] if pip_name == "GPUtil"
else [pip_name])
for _cand in _pip_candidates:
subprocess.run(
[sys.executable, "-m", "pip", "uninstall", _cand, "-y",
"--disable-pip-version-check"],
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, creationflags=_NO_WIN)
importlib.invalidate_caches()
for _mod in list(sys.modules.keys()):
if _mod == imp_name or _mod.startswith(imp_name + "."):
sys.modules.pop(_mod, None)
# Manual file removal if pip didn't register it (no .dist-info)
if _can_import(imp_name):
import importlib.util as _ilu, shutil as _sh
_spec = _ilu.find_spec(imp_name)
if _spec and _spec.origin:
_pkg_dir = os.path.dirname(_spec.origin)
if os.path.basename(_pkg_dir).lower() == imp_name.lower():
_sh.rmtree(_pkg_dir, ignore_errors=True)
else:
os.remove(_spec.origin)
_sp = os.path.dirname(_pkg_dir)
for _e in os.listdir(_sp):
if _e.lower().startswith(imp_name.lower()) and (
_e.endswith(".dist-info") or _e.endswith(".egg-info")):
_sh.rmtree(os.path.join(_sp, _e), ignore_errors=True)
for _mod in list(sys.modules.keys()):
if _mod == imp_name or _mod.startswith(imp_name + "."):
sys.modules.pop(_mod, None)
importlib.invalidate_caches()
if _can_import(imp_name):
raise RuntimeError(
f"{imp_name!r} still importable — may need manual removal "
f"from site-packages.")
def _pip_install_setuptools_for_gputil():
"""Install setuptools before GPUtil so distutils is available on Python 3.12+."""
subprocess.run(
[sys.executable, "-m", "pip", "install", "setuptools",
"--disable-pip-version-check", "--no-cache-dir"],
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, creationflags=_NO_WIN)
def _run_dependency_check():
"""Pre-launch dependency manager. Returns dict with launch/skip_dep_check keys."""
_nvidia = _has_nvidia_gpu()
PACKAGES = [
# (display_name, pip_name, import_name, required, description)
# ── Required ──────────────────────────────────────────────────────────
("psutil", "psutil", "psutil", True, "Core system stats — CPU, RAM, disk & network usage"),
("pynvml", "pynvml", "pynvml", _nvidia, "Reads live data from your NVIDIA GPU (temp, usage, VRAM)"),
# ── Optional ──────────────────────────────────────────────────────────
("pywin32", "pywin32", "pythoncom", False, "Enables CPU clock speed & AMD/Intel GPU stats on Windows"),
("pystray", "pystray", "pystray", False, "Adds a system tray icon so you can minimize to the taskbar"),
("GPUtil", "GPUtil", "GPUtil", False, "Backup GPU reader if pynvml & pywin32 both can't detect your GPU"),
]
ACCENT = ACCENT1 # convenience alias for the dep-checker UI
result = {"launch": False, "skip_dep_check": False, "reopen_placement": False}
root = tk.Tk()
root.title("PyDisplay — Dependencies")
root.configure(bg=BG)
root.resizable(False, False)
root.wm_attributes("-topmost", True)
root.overrideredirect(True)
root.minsize(460, 0)
# ── Custom title bar with X ───────────────────────────────────────────────
_make_titlebar(root, PANEL, BORDER, SUBTEXT, RED, on_close=root.destroy)
# ── Header ────────────────────────────────────────────────────────────────
hdr_frame = tk.Frame(root, bg=BG)
hdr_frame.pack(fill="x", padx=16, pady=(12, 0))
_ver_frame = tk.Frame(hdr_frame, bg=BG)
_ver_frame.pack(side="right")
tk.Label(_ver_frame, text="PyDisplay", bg=BG, fg=ACCENT,
font=(_FONT, _BASE_FONT_SIZE, "bold")).pack(side="left")
tk.Label(_ver_frame, text=f" v{_APP_VERSION}", bg=BG, fg=SUBTEXT,
font=(_FONT, _BASE_FONT_SIZE - 1)).pack(side="left")
_app_upd_btn = tk.Label(_ver_frame, text="⌕ Update",
bg=BORDER, fg=ACCENT,
font=(_FONT, _BASE_FONT_SIZE - 2, "bold"), cursor="hand2",
padx=6, pady=2)
_app_upd_btn.pack(side="left", padx=(10, 0))
tk.Label(hdr_frame, text=" · dependency setup", bg=BG, fg=TEXT,
font=(_FONT, _BASE_FONT_SIZE, "bold")).pack(side="left")
tk.Frame(root, bg=BORDER, height=1).pack(fill="x", padx=12, pady=(8, 0))
# ── Helper ────────────────────────────────────────────────────────────────
def _get_version(pip_name):
try:
return _importlib_metadata.version(pip_name)
except Exception:
return ""
_install_log = os.path.join(_APP_DIR, "PyDisplay_install.log")
# row_data: list of dicts with keys: disp, pip_name, imp_name, required,
# installed (live), action_var ("install"|"keep"), widgets
row_data = []
_row_errors = {} # pip_name → error string, for clickable "? Failed" status
def _set_failed(status_lbl, pip_name, err_str):
"""Mark a status label as failed and make it clickable to show the error."""
_row_errors[pip_name] = err_str
status_lbl.config(text="? Failed", fg=YELLOW, cursor="hand2")
def _show_err(e, pn=pip_name, sl=status_lbl):
err = _row_errors.get(pn, "No error details available.")
dlg = tk.Toplevel(root)
dlg.configure(bg=PANEL)
dlg.overrideredirect(True)
dlg.attributes("-topmost", True)
dlg.lift()
dlg.focus_force()
dlg.grab_set()
_make_titlebar(dlg, PANEL, BORDER, SUBTEXT, RED,
on_close=dlg.destroy,
title_text=f"PyDisplay · {pn} error", title_fg=TEXT, title_bg=PANEL)
df = tk.Frame(dlg, bg=PANEL, padx=16, pady=12)
df.pack(fill="both")
tk.Label(df, text=f"Install error — {pn}", bg=PANEL, fg=RED,
font=(_FONT, _BASE_FONT_SIZE, "bold")).pack(anchor="w")
tk.Frame(df, bg=BORDER, height=1).pack(fill="x", pady=(4, 8))
tk.Label(df, text=err, bg=BORDER, fg=TEXT,
font=(_FONT, _BASE_FONT_SIZE - 2), anchor="w", justify="left",
padx=8, pady=8, wraplength=380).pack(fill="x")
tk.Frame(df, bg=PANEL, height=6).pack()
close_btn = tk.Label(df, text="Close", bg=BORDER, fg=SUBTEXT,
font=(_FONT, _BASE_FONT_SIZE, "bold"), cursor="hand2",
padx=12, pady=4)
close_btn.pack(anchor="e")
close_btn.bind("<Button-1>", lambda e: dlg.destroy())
close_btn.bind("<Enter>", lambda e: close_btn.config(fg=TEXT))
close_btn.bind("<Leave>", lambda e: close_btn.config(fg=SUBTEXT))
dlg.update_idletasks()
cx = root.winfo_x() + (root.winfo_width() - dlg.winfo_reqwidth()) // 2
cy = root.winfo_y() + (root.winfo_height() - dlg.winfo_reqheight()) // 2
dlg.geometry(f"+{cx}+{cy}")
status_lbl.bind("<Button-1>", _show_err)
status_lbl.bind("<Enter>", lambda e: status_lbl.config(fg=GREEN))
status_lbl.bind("<Leave>", lambda e: status_lbl.config(fg=YELLOW))
# ── Package list ──────────────────────────────────────────────────────────
# Single-button design: shows "Install" when missing, "Delete" when installed.
# Clicking toggles the queued action; button text/colour always reflects
# what WILL HAPPEN when you hit Launch — no separate Remove column needed.
list_frame = tk.Frame(root, bg=BG)
list_frame.pack(fill="x", padx=12, pady=(10, 4))
list_frame.columnconfigure(0, minsize=76) # name
list_frame.columnconfigure(1, minsize=130) # status
list_frame.columnconfigure(2, weight=1) # description
list_frame.columnconfigure(3, minsize=100) # action btn
# Column headers
for col, hdr in enumerate(["Package", "Status", "Description", "Action"]):
tk.Label(list_frame, text=hdr, bg=BG, fg=SUBTEXT,
font=(_FONT, _BASE_FONT_SIZE, "bold"), anchor="w",
padx=6).grid(row=0, column=col, sticky="ew", pady=(0, 2))
tk.Frame(root, bg=BORDER, height=1).pack(fill="x", padx=12, pady=(0, 2))
data_frame = tk.Frame(root, bg=BG)
data_frame.pack(fill="x", padx=12)
data_frame.columnconfigure(0, minsize=76)
data_frame.columnconfigure(1, minsize=130)
data_frame.columnconfigure(2, weight=1)
data_frame.columnconfigure(3, minsize=100)
for i, (disp, pip_name, imp_name, required, desc) in enumerate(PACKAGES):
installed = _can_import(imp_name)
version = _get_version(pip_name) if installed else ""
row_bg = PANEL if i % 2 == 0 else BG
# Default queued action: keep — user must explicitly click Install
action_var = tk.StringVar(value="keep")
# Col 0 — package name + required marker
req_tag = " *" if required else ""
tk.Label(data_frame, text=disp + req_tag, bg=row_bg, fg=TEXT,
font=(_FONT, _BASE_FONT_SIZE, "bold"), anchor="w",
padx=6, pady=8).grid(row=i, column=0, sticky="ew")
# Col 1 — live status label
if installed:
st_text = f"✔ v{version}" if version else "✔ installed"
st_color = GREEN
else:
st_text = "✘ missing"
st_color = RED
status_lbl = tk.Label(data_frame, text=st_text, bg=row_bg, fg=st_color,
font=(_FONT, _BASE_FONT_SIZE), anchor="w", padx=6, pady=8)
status_lbl.grid(row=i, column=1, sticky="ew")
# Col 2 — description
tk.Label(data_frame, text=desc, bg=row_bg, fg=SUBTEXT,
font=(_FONT, _BASE_FONT_SIZE - 2), anchor="w",
padx=6, pady=8, justify="left").grid(row=i, column=2, sticky="ew")
# Col 3 — single smart action button
# not installed → "+ Install" (click → immediately pip install)
# installed → "✕ Delete" (click → immediately pip uninstall)
# while running → " …" (disabled until done)
action_btn = tk.Label(data_frame, bg=row_bg,
font=(_FONT, _BASE_FONT_SIZE, "bold"), cursor="hand2",
padx=8, pady=5, anchor="center")
action_btn.grid(row=i, column=3, sticky="ew", padx=(4, 6), pady=3)
_inst_ref = [installed] # mutable — updated after each op
_busy_ref = [False] # True while a pip op is running for this row
def _refresh_action_btn(btn=action_btn, inst_ref=_inst_ref, busy_ref=_busy_ref, rb=row_bg):
if busy_ref[0]:
btn.config(text=" … ", fg=SUBTEXT, bg=rb, cursor="arrow")
return
if inst_ref[0]:
btn.config(text="✕ Delete ", fg=RED, bg=BORDER, cursor="hand2")
else:
btn.config(text="+ Install", fg=GREEN, bg=BORDER, cursor="hand2")
_rd_ref = [None]
def _click_action(e, rd_ref=_rd_ref,
inst_ref=_inst_ref, busy_ref=_busy_ref,
disp_s=disp, pip_name_s=pip_name, imp_name_s=imp_name):
if busy_ref[0]:
return # already running
# Lock the button immediately
busy_ref[0] = True
root.after(0, rd_ref[0]["refresh_btn"])
def _worker_single():
if inst_ref[0]:
# ── UNINSTALL ──────────────────────────────────────────────
root.after(0, lambda: log_var.set(f"Removing {disp_s}…"))
root.after(0, lambda: rd_ref[0]["status_lbl"].config(text="⏳ Removing…", fg=YELLOW))
root.after(0, lambda: _set_progress(0.1))
try:
_pip_uninstall_pkg(pip_name_s, imp_name_s)
inst_ref[0] = False
root.after(0, lambda: rd_ref[0]["status_lbl"].config(
text="✘ removed", fg=SUBTEXT))
root.after(0, lambda: log_var.set(f"✔ {disp_s} removed."))
root.after(0, lambda: _set_progress(1.0))
root.after(200, lambda: _set_progress(None))
except Exception as exc:
_exc_str = str(exc)
root.after(0, lambda s=_exc_str: _set_failed(rd_ref[0]["status_lbl"], pip_name_s, s))
root.after(0, lambda s=_exc_str: log_var.set(f"✘ {disp_s}: {s}"))
root.after(0, lambda: _set_progress(None))
else:
# ── INSTALL ────────────────────────────────────────────────
_msg = f"Installing {disp_s}…" if pip_name_s != "pywin32" else "Installing pywin32 + wmi…"
root.after(0, lambda: log_var.set(_msg))
root.after(0, lambda: rd_ref[0]["status_lbl"].config(text="⏳ Installing…", fg=YELLOW))
root.after(0, lambda: _set_progress(0.1))
try:
# GPUtil requires distutils which was removed in Python 3.12+;
# install setuptools first to provide it.
if pip_name_s == "GPUtil":
root.after(0, lambda: log_var.set("Installing setuptools for GPUtil…"))
_pip_install_setuptools_for_gputil()
root.after(0, lambda: log_var.set("Installing GPUtil…"))
proc = subprocess.run(
[sys.executable, "-m", "pip", "install", pip_name_s,
"--disable-pip-version-check", "--no-cache-dir"],
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, creationflags=_NO_WIN)
if proc.returncode != 0:
_err = (proc.stderr or "").strip().splitlines()
raise RuntimeError(_err[-1] if _err else f"exit {proc.returncode}")
if pip_name_s == "pywin32":
# Skip post-install — it triggers a Windows DLL dialog if
# pythoncom is locked by another process. pywin32 works fully
# after a restart without needing the post-install in modern pip.
root.after(0, lambda: _set_progress(0.75))
# Still install wmi so it's ready after restart
root.after(0, lambda: log_var.set("Installing wmi…"))
subprocess.run(
[sys.executable, "-m", "pip", "install", "wmi",
"--disable-pip-version-check", "--no-cache-dir"],
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, creationflags=_NO_WIN)
# wmi may fail in current process — non-fatal, works after restart
importlib.invalidate_caches()
for _mod in list(sys.modules.keys()):
if _mod == imp_name_s or _mod.startswith(imp_name_s + "."):
sys.modules.pop(_mod, None)
if pip_name_s in ("pywin32", "wmi"):
try:
os.add_dll_directory(os.path.dirname(sys.executable))
except (AttributeError, OSError):
pass
try:
import site as _site
for _sp in _site.getsitepackages():
for _sub in ("", "win32",
os.path.join("win32", "lib"),
"win32com", "win32comext"):
_d = os.path.join(_sp, _sub)
if os.path.isdir(_d) and _d not in sys.path:
sys.path.insert(0, _d)
try:
os.add_dll_directory(_sp)
except (AttributeError, OSError):
pass
except Exception:
pass
importlib.invalidate_caches()
# GPUtil uses distutils.version which was removed in Python 3.12+.
# setuptools re-provides it but the current process won't see it
# until we purge the stale cache and inject it into sys.modules.
if pip_name_s == "GPUtil":
for _mod in list(sys.modules.keys()):
if _mod == "distutils" or _mod.startswith("distutils."):
sys.modules.pop(_mod, None)
importlib.invalidate_caches()
try:
import setuptools # noqa — triggers distutils shim
import distutils.version # should now resolve via setuptools
except Exception:
pass
if "distutils.version" not in sys.modules:
import types as _types
if "distutils" not in sys.modules:
sys.modules["distutils"] = _types.ModuleType("distutils")
_dv2 = _types.ModuleType("distutils.version")
try:
from packaging.version import Version as _V
_dv2.LooseVersion = _V; _dv2.StrictVersion = _V
except Exception:
class _FV:
def __init__(self, v=""): self.vstring = str(v)
def __str__(self): return self.vstring
_dv2.LooseVersion = _FV; _dv2.StrictVersion = _FV
sys.modules["distutils.version"] = _dv2
sys.modules["distutils"].version = _dv2
# pywin32 DLLs can't be loaded in the current process after
# install — verify via metadata only, skip live import check.
if pip_name_s == "pywin32":
try:
ver_str = f"✔ v{_importlib_metadata.version(pip_name_s)}"
except Exception:
ver_str = "✔ installed"
inst_ref[0] = True
root.after(0, lambda vs=ver_str: rd_ref[0]["status_lbl"].config(
text=vs, fg=GREEN))
root.after(0, lambda: log_var.set("✔ pywin32 installed. Restart PyDisplay to activate."))
root.after(0, lambda: _set_progress(1.0))
root.after(200, lambda: _set_progress(None))
else:
try:
importlib.import_module(imp_name_s)
except Exception as _ie:
raise RuntimeError(f"installed but import failed: {_ie}")
try:
ver_str = f"✔ v{_importlib_metadata.version(pip_name_s)}"
except Exception:
ver_str = "✔ installed"
inst_ref[0] = True
root.after(0, lambda vs=ver_str: rd_ref[0]["status_lbl"].config(
text=vs, fg=GREEN))
root.after(0, lambda: log_var.set(f"✔ {disp_s} installed."))
root.after(0, lambda: _set_progress(1.0))
root.after(200, lambda: _set_progress(None))
except Exception as exc:
_exc_str = str(exc)
root.after(0, lambda s=_exc_str: _set_failed(rd_ref[0]["status_lbl"], pip_name_s, s))
root.after(0, lambda s=_exc_str: log_var.set(f"✘ {disp_s}: {s}"))
root.after(0, lambda: _set_progress(None))
# Always unlock button and refresh its appearance when done
busy_ref[0] = False
root.after(0, rd_ref[0]["refresh_btn"])
threading.Thread(target=_worker_single, daemon=True).start()
action_btn.bind("<Button-1>", _click_action)
action_btn.bind("<Enter>", lambda e, btn=action_btn, ir=_inst_ref, br=_busy_ref:
btn.config(fg=RED if ir[0] else GREEN) if not br[0] else None)
action_btn.bind("<Leave>", lambda e, r=_refresh_action_btn: r())
_refresh_action_btn() # seed initial appearance
_rd = {
"disp": disp, "pip_name": pip_name, "imp_name": imp_name,
"required": required,
"action_var": action_var,
"status_lbl": status_lbl,
"action_btn": action_btn,
"refresh_btn": _refresh_action_btn,
"inst_ref": _inst_ref,