CTkFileDialog-plus 2.1.1__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.
- CTkFileDialog/Constants/__init__.py +45 -0
- CTkFileDialog/Constants/constants.py +53 -0
- CTkFileDialog/Dialog.py +25 -0
- CTkFileDialog/__init__.py +36 -0
- CTkFileDialog/_functions.py +521 -0
- CTkFileDialog/_system.py +5 -0
- CTkFileDialog/core/__init__.py +13 -0
- CTkFileDialog/core/filesystem.py +97 -0
- CTkFileDialog/core/search.py +13 -0
- CTkFileDialog/core/sorting.py +41 -0
- CTkFileDialog/preview/__init__.py +4 -0
- CTkFileDialog/preview/media.py +126 -0
- CTkFileDialog/resources/__init__.py +14 -0
- CTkFileDialog/resources/icons.py +89 -0
- CTkFileDialog/system/__init__.py +4 -0
- CTkFileDialog/system/platform.py +88 -0
- CTkFileDialog/ui/__init__.py +5 -0
- CTkFileDialog/ui/default_dialog.py +1013 -0
- CTkFileDialog/ui/mini_dialog.py +454 -0
- CTkFileDialog/ui/tooltip.py +25 -0
- CTkFileDialog/utils/__init__.py +9 -0
- CTkFileDialog/utils/helpers.py +61 -0
- ctkfiledialog_plus-2.1.1.dist-info/METADATA +592 -0
- ctkfiledialog_plus-2.1.1.dist-info/RECORD +27 -0
- ctkfiledialog_plus-2.1.1.dist-info/WHEEL +5 -0
- ctkfiledialog_plus-2.1.1.dist-info/licenses/LICENSE +21 -0
- ctkfiledialog_plus-2.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Core non-UI logic: filesystem, sorting, search."""
|
|
2
|
+
from .filesystem import create_folder, get_file_info, list_directory, prompt_create_folder
|
|
3
|
+
from .search import filter_by_query
|
|
4
|
+
from .sorting import sort_files
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"list_directory",
|
|
8
|
+
"create_folder",
|
|
9
|
+
"prompt_create_folder",
|
|
10
|
+
"get_file_info",
|
|
11
|
+
"sort_files",
|
|
12
|
+
"filter_by_query",
|
|
13
|
+
]
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Filesystem operations used by the dialogs."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import time
|
|
7
|
+
from typing import Any, Callable, List, Optional
|
|
8
|
+
|
|
9
|
+
from CTkMessagebox import CTkMessagebox
|
|
10
|
+
|
|
11
|
+
from ..system.platform import find_owner
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def list_directory(
|
|
15
|
+
path: str,
|
|
16
|
+
*,
|
|
17
|
+
method: str,
|
|
18
|
+
hidden: bool = False,
|
|
19
|
+
filetypes: Optional[List[str]] = None,
|
|
20
|
+
) -> List[str]:
|
|
21
|
+
"""Return names of entries in *path* filtered by dialog mode and options.
|
|
22
|
+
|
|
23
|
+
Directories are always included (unless hidden). Files are included
|
|
24
|
+
unless the method is a pure directory-picker, and must match *filetypes*
|
|
25
|
+
when provided.
|
|
26
|
+
"""
|
|
27
|
+
dir_only = method in ("askdirectory", "askdirectories")
|
|
28
|
+
result: List[str] = []
|
|
29
|
+
try:
|
|
30
|
+
for entry in os.scandir(path):
|
|
31
|
+
if entry.name.startswith(".") and not hidden:
|
|
32
|
+
continue
|
|
33
|
+
if entry.is_dir():
|
|
34
|
+
result.append(entry.name)
|
|
35
|
+
elif not dir_only and entry.is_file():
|
|
36
|
+
if not filetypes or any(entry.name.endswith(ext) for ext in filetypes):
|
|
37
|
+
result.append(entry.name)
|
|
38
|
+
except (PermissionError, FileNotFoundError, OSError):
|
|
39
|
+
raise
|
|
40
|
+
return result
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def create_folder(parent: str, name: str) -> Optional[str]:
|
|
44
|
+
"""Create a new folder under *parent*.
|
|
45
|
+
|
|
46
|
+
Returns the new path on success, or ``None`` on failure (after showing
|
|
47
|
+
a message box). Raises nothing — errors are reported via UI.
|
|
48
|
+
"""
|
|
49
|
+
name = (name or "").strip()
|
|
50
|
+
if not name:
|
|
51
|
+
return None
|
|
52
|
+
new_path = os.path.join(parent, name)
|
|
53
|
+
try:
|
|
54
|
+
os.makedirs(new_path, exist_ok=False)
|
|
55
|
+
return new_path
|
|
56
|
+
except FileExistsError:
|
|
57
|
+
CTkMessagebox(
|
|
58
|
+
message="A file or folder with that name already exists!",
|
|
59
|
+
title="Error",
|
|
60
|
+
icon="cancel",
|
|
61
|
+
)
|
|
62
|
+
except PermissionError:
|
|
63
|
+
CTkMessagebox(message="Permission denied!", title="Error", icon="cancel")
|
|
64
|
+
except OSError as e:
|
|
65
|
+
CTkMessagebox(
|
|
66
|
+
message=f"Could not create folder: {e}",
|
|
67
|
+
title="Error",
|
|
68
|
+
icon="cancel",
|
|
69
|
+
)
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_file_info(path: str) -> str:
|
|
74
|
+
"""Return a multi-line tooltip string for *path*."""
|
|
75
|
+
try:
|
|
76
|
+
st = os.stat(path)
|
|
77
|
+
owner = find_owner(path)
|
|
78
|
+
fecha = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(st.st_ctime))
|
|
79
|
+
return (
|
|
80
|
+
f"File: {os.path.basename(path)}\n"
|
|
81
|
+
f" creation: {fecha}\n"
|
|
82
|
+
f" owner: {owner}\n"
|
|
83
|
+
f" path: {path}\n"
|
|
84
|
+
f" "
|
|
85
|
+
)
|
|
86
|
+
except Exception as e:
|
|
87
|
+
return f"Error getting info: {e}"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def prompt_create_folder(parent_path: str, on_success: Optional[Callable[[], Any]] = None) -> None:
|
|
91
|
+
"""Show an input dialog, create the folder, then call *on_success*."""
|
|
92
|
+
import customtkinter as ctk
|
|
93
|
+
|
|
94
|
+
dialog = ctk.CTkInputDialog(text="Enter new folder name:", title="New Folder")
|
|
95
|
+
name = dialog.get_input()
|
|
96
|
+
if create_folder(parent_path, name or "") and on_success:
|
|
97
|
+
on_success()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Search / filter helpers."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def filter_by_query(files: List[str], query: str) -> List[str]:
|
|
9
|
+
"""Case-insensitive substring filter on file names."""
|
|
10
|
+
q = (query or "").lower().strip()
|
|
11
|
+
if not q:
|
|
12
|
+
return list(files)
|
|
13
|
+
return [f for f in files if q in f.lower()]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""File sorting helpers."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import List
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def sort_files(files: List[str], current_path: str, sort_by: str = "name") -> List[str]:
|
|
10
|
+
"""Sort *files* (names relative to *current_path*).
|
|
11
|
+
|
|
12
|
+
Directories always sort before files. *sort_by* may be one of
|
|
13
|
+
``name``, ``date``, ``type``, ``size``, ``modified``.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def info(filename: str) -> dict:
|
|
17
|
+
full = os.path.join(current_path, filename)
|
|
18
|
+
try:
|
|
19
|
+
st = os.stat(full)
|
|
20
|
+
return {
|
|
21
|
+
"name": filename.lower(),
|
|
22
|
+
"date": st.st_mtime,
|
|
23
|
+
"modified": st.st_mtime,
|
|
24
|
+
"type": os.path.splitext(filename)[1].lower(),
|
|
25
|
+
"size": st.st_size,
|
|
26
|
+
"is_dir": os.path.isdir(full),
|
|
27
|
+
}
|
|
28
|
+
except OSError:
|
|
29
|
+
return {
|
|
30
|
+
"name": filename.lower(),
|
|
31
|
+
"date": 0,
|
|
32
|
+
"modified": 0,
|
|
33
|
+
"type": "",
|
|
34
|
+
"size": 0,
|
|
35
|
+
"is_dir": os.path.isdir(full),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return sorted(
|
|
39
|
+
files,
|
|
40
|
+
key=lambda f: (not info(f)["is_dir"], info(f).get(sort_by, 0)),
|
|
41
|
+
)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Image and video detection / frame extraction.
|
|
3
|
+
|
|
4
|
+
Architecture is ready for future PDF / audio preview plugins:
|
|
5
|
+
register additional checkers via the same pattern (is_* + extract).
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from PIL import Image
|
|
13
|
+
|
|
14
|
+
_IMAGE_EXTENSIONS = frozenset(
|
|
15
|
+
{
|
|
16
|
+
".bmp",
|
|
17
|
+
".dib",
|
|
18
|
+
".gif",
|
|
19
|
+
".ico",
|
|
20
|
+
".im",
|
|
21
|
+
".jpg",
|
|
22
|
+
".jpeg",
|
|
23
|
+
".jpe",
|
|
24
|
+
".pcx",
|
|
25
|
+
".png",
|
|
26
|
+
".ppm",
|
|
27
|
+
".pbm",
|
|
28
|
+
".pgm",
|
|
29
|
+
".tif",
|
|
30
|
+
".tiff",
|
|
31
|
+
".webp",
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
_VIDEO_EXTENSIONS = frozenset(
|
|
36
|
+
{
|
|
37
|
+
".avi",
|
|
38
|
+
".mp4",
|
|
39
|
+
".mov",
|
|
40
|
+
".mkv",
|
|
41
|
+
".webm",
|
|
42
|
+
".flv",
|
|
43
|
+
".wmv",
|
|
44
|
+
".mpg",
|
|
45
|
+
".mpeg",
|
|
46
|
+
".3gp",
|
|
47
|
+
".m4v",
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def is_image(path: str) -> bool:
|
|
53
|
+
if not path or not os.path.isfile(path):
|
|
54
|
+
return False
|
|
55
|
+
if os.path.splitext(path)[1].lower() not in _IMAGE_EXTENSIONS:
|
|
56
|
+
return False
|
|
57
|
+
try:
|
|
58
|
+
with Image.open(path) as img:
|
|
59
|
+
img.verify()
|
|
60
|
+
return True
|
|
61
|
+
except Exception:
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def is_video(path: str) -> bool:
|
|
66
|
+
if not path or not os.path.isfile(path):
|
|
67
|
+
return False
|
|
68
|
+
if os.path.splitext(path)[1].lower() not in _VIDEO_EXTENSIONS:
|
|
69
|
+
return False
|
|
70
|
+
try:
|
|
71
|
+
import cv2
|
|
72
|
+
|
|
73
|
+
cap = cv2.VideoCapture(path)
|
|
74
|
+
valid = cap.isOpened()
|
|
75
|
+
cap.release()
|
|
76
|
+
return valid
|
|
77
|
+
except Exception:
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_video_frame(path: str, frame_number: int = 0) -> Optional[Image.Image]:
|
|
82
|
+
"""Return a PIL Image of a single video frame, or None."""
|
|
83
|
+
if not path or not os.path.isfile(path):
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
import cv2
|
|
88
|
+
except ImportError:
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
cap = cv2.VideoCapture(path)
|
|
93
|
+
if not cap.isOpened():
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0)
|
|
97
|
+
if total_frames > 0:
|
|
98
|
+
if frame_number < 0:
|
|
99
|
+
frame_number = max(total_frames // 2, 0)
|
|
100
|
+
elif frame_number >= total_frames:
|
|
101
|
+
frame_number = max(total_frames - 1, 0)
|
|
102
|
+
else:
|
|
103
|
+
frame_number = 0
|
|
104
|
+
|
|
105
|
+
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
|
|
106
|
+
ret, frame = cap.read()
|
|
107
|
+
cap.release()
|
|
108
|
+
if not ret or frame is None:
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
|
112
|
+
return Image.fromarray(frame)
|
|
113
|
+
except Exception:
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def thumbnail_image(path: str, size: tuple[int, int] = (32, 32)) -> Optional[Image.Image]:
|
|
118
|
+
"""Open an image and return a thumbnail, or None on failure."""
|
|
119
|
+
if not path or not os.path.isfile(path):
|
|
120
|
+
return None
|
|
121
|
+
try:
|
|
122
|
+
with Image.open(path) as img:
|
|
123
|
+
img.thumbnail(size, Image.Resampling.LANCZOS)
|
|
124
|
+
return img.copy()
|
|
125
|
+
except Exception:
|
|
126
|
+
return None
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Static resources (icons)."""
|
|
2
|
+
from .icons import (
|
|
3
|
+
EXTENSION_ICONS,
|
|
4
|
+
icon_for_extension,
|
|
5
|
+
load_default_icons,
|
|
6
|
+
load_mini_icons,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"EXTENSION_ICONS",
|
|
11
|
+
"load_default_icons",
|
|
12
|
+
"load_mini_icons",
|
|
13
|
+
"icon_for_extension",
|
|
14
|
+
]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Icon loading for Default and Mini dialogs."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict
|
|
7
|
+
|
|
8
|
+
import customtkinter as ctk
|
|
9
|
+
from PIL import Image
|
|
10
|
+
import tkinter as tk
|
|
11
|
+
|
|
12
|
+
# Package root (CTkFileDialog/)
|
|
13
|
+
_PACKAGE_DIR = Path(__file__).resolve().parent.parent
|
|
14
|
+
_ICON_DIR = _PACKAGE_DIR / "icons"
|
|
15
|
+
_MINI_ICON_DIR = _ICON_DIR / "_IconsMini"
|
|
16
|
+
|
|
17
|
+
# Extension → icon key mapping used by Default dialog
|
|
18
|
+
EXTENSION_ICONS: Dict[str, str] = {
|
|
19
|
+
".webp": "webp",
|
|
20
|
+
".awk": "bash",
|
|
21
|
+
".mp4": "video",
|
|
22
|
+
".mvk": "video",
|
|
23
|
+
".sh": "bash",
|
|
24
|
+
".zsh": "bash",
|
|
25
|
+
".py": "python",
|
|
26
|
+
".png": "image",
|
|
27
|
+
".jpg": "image",
|
|
28
|
+
".jpeg": "image",
|
|
29
|
+
".txt": "text",
|
|
30
|
+
".js": "javascript",
|
|
31
|
+
".md": "markdown",
|
|
32
|
+
".php": "php",
|
|
33
|
+
".html": "html",
|
|
34
|
+
".css": "css",
|
|
35
|
+
".ini": "ini",
|
|
36
|
+
".conf": "conf",
|
|
37
|
+
".json": "json",
|
|
38
|
+
".odt": "odt",
|
|
39
|
+
".pdf": "pdf",
|
|
40
|
+
".exe": "exe",
|
|
41
|
+
".gz": "gz",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# Filename (without path) → logical name for Default CTkImage icons
|
|
45
|
+
_DEFAULT_ICON_FILES = {
|
|
46
|
+
"folder": "folder.png",
|
|
47
|
+
"bash": "bash.png",
|
|
48
|
+
"image": "image.png",
|
|
49
|
+
"python": "python.png",
|
|
50
|
+
"text": "text.png",
|
|
51
|
+
"markdown": "markdown.png",
|
|
52
|
+
"javascript": "javascript.png",
|
|
53
|
+
"php": "php.png",
|
|
54
|
+
"html": "html.png",
|
|
55
|
+
"css": "css.png",
|
|
56
|
+
"ini": "ini.png",
|
|
57
|
+
"conf": "conf.png",
|
|
58
|
+
"exe": "exe.png",
|
|
59
|
+
"odt": "odt.png",
|
|
60
|
+
"pdf": "pdf.png",
|
|
61
|
+
"json": "json.png",
|
|
62
|
+
"gz": "gz.png",
|
|
63
|
+
"video": "video.png",
|
|
64
|
+
"awk": "bash.png",
|
|
65
|
+
"webp": "image.png",
|
|
66
|
+
"default": "text.png",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def load_default_icons(size: tuple[int, int] = (40, 40)) -> Dict[str, ctk.CTkImage]:
|
|
71
|
+
"""Load all Default-dialog CTkImage icons."""
|
|
72
|
+
icons: Dict[str, ctk.CTkImage] = {}
|
|
73
|
+
for key, filename in _DEFAULT_ICON_FILES.items():
|
|
74
|
+
path = _ICON_DIR / filename
|
|
75
|
+
icons[key] = ctk.CTkImage(Image.open(path), size=size)
|
|
76
|
+
return icons
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def load_mini_icons() -> tuple[tk.PhotoImage, tk.PhotoImage]:
|
|
80
|
+
"""Return (folder_image, file_image) for Mini dialog Treeview."""
|
|
81
|
+
folder = tk.PhotoImage(file=str(_MINI_ICON_DIR / "folder.png"))
|
|
82
|
+
file_img = tk.PhotoImage(file=str(_MINI_ICON_DIR / "file.png"))
|
|
83
|
+
return folder, file_img
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def icon_for_extension(ext: str, icons: Dict[str, ctk.CTkImage]) -> ctk.CTkImage:
|
|
87
|
+
"""Resolve a CTkImage for a file extension."""
|
|
88
|
+
key = EXTENSION_ICONS.get(ext.lower(), "default")
|
|
89
|
+
return icons.get(key, icons["default"])
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Platform-specific utilities (owner lookup, path helpers)."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import platform
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def find_owner(path) -> str:
|
|
11
|
+
"""Return the owner of *path* (Unix or Windows)."""
|
|
12
|
+
system = platform.system()
|
|
13
|
+
path = str(path)
|
|
14
|
+
|
|
15
|
+
if system == "Windows":
|
|
16
|
+
return _get_windows_owner(path)
|
|
17
|
+
return _get_unix_owner(path)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _get_unix_owner(path: str) -> str:
|
|
21
|
+
try:
|
|
22
|
+
return Path(path).owner()
|
|
23
|
+
except Exception:
|
|
24
|
+
return "unknown:unknown"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _get_windows_owner(path: str) -> str:
|
|
28
|
+
import ctypes
|
|
29
|
+
from ctypes import wintypes
|
|
30
|
+
|
|
31
|
+
GetNamedSecurityInfoW = ctypes.windll.advapi32.GetNamedSecurityInfoW
|
|
32
|
+
LookupAccountSidW = ctypes.windll.advapi32.LookupAccountSidW
|
|
33
|
+
LocalFree = ctypes.windll.kernel32.LocalFree
|
|
34
|
+
|
|
35
|
+
OWNER_SECURITY_INFORMATION = 0x00000001
|
|
36
|
+
SE_FILE_OBJECT = 1
|
|
37
|
+
|
|
38
|
+
pSidOwner = ctypes.c_void_p()
|
|
39
|
+
pSD = ctypes.c_void_p()
|
|
40
|
+
|
|
41
|
+
result = GetNamedSecurityInfoW(
|
|
42
|
+
ctypes.c_wchar_p(path),
|
|
43
|
+
SE_FILE_OBJECT,
|
|
44
|
+
OWNER_SECURITY_INFORMATION,
|
|
45
|
+
ctypes.byref(pSidOwner),
|
|
46
|
+
None, None, None,
|
|
47
|
+
ctypes.byref(pSD),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
if result != 0:
|
|
51
|
+
return "unknown"
|
|
52
|
+
|
|
53
|
+
name = ctypes.create_unicode_buffer(256)
|
|
54
|
+
domain = ctypes.create_unicode_buffer(256)
|
|
55
|
+
name_size = wintypes.DWORD(len(name))
|
|
56
|
+
domain_size = wintypes.DWORD(len(domain))
|
|
57
|
+
sid_name_use = wintypes.DWORD()
|
|
58
|
+
|
|
59
|
+
success = LookupAccountSidW(
|
|
60
|
+
None,
|
|
61
|
+
pSidOwner,
|
|
62
|
+
name,
|
|
63
|
+
ctypes.byref(name_size),
|
|
64
|
+
domain,
|
|
65
|
+
ctypes.byref(domain_size),
|
|
66
|
+
ctypes.byref(sid_name_use),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
LocalFree(pSD)
|
|
70
|
+
|
|
71
|
+
if not success:
|
|
72
|
+
return "unknown"
|
|
73
|
+
|
|
74
|
+
return f"{name.value}"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class System:
|
|
78
|
+
"""Thin path helpers used by both Default and Mini dialogs."""
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def get_path(path=None) -> str:
|
|
82
|
+
if path is None:
|
|
83
|
+
path = os.getcwd()
|
|
84
|
+
return f"{path}" if path == os.getenv("HOME") else path
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def parse_path(path: str) -> str:
|
|
88
|
+
return os.path.abspath(os.path.expanduser(os.path.expandvars(path)))
|