1"""XDG environment variable mixin for Unix and macOS."""
2
3from __future__ import annotations
4
5import os
6import posixpath
7import sys
8from contextlib import suppress
9from typing import Final
10
11from .api import PlatformDirsABC
12
13
14class XDGMixin(PlatformDirsABC):
15 """Mixin that checks XDG environment variables, falling back to platform-specific defaults via ``super()``."""
16
17 @property
18 def user_data_dir(self) -> str:
19 """Data directory tied to the user, from ``$XDG_DATA_HOME`` if set, else platform default."""
20 if path := _xdg_dir("XDG_DATA_HOME"):
21 return self._append_app_name_and_version(path, private=True)
22 return super().user_data_dir
23
24 @property
25 def _site_data_dirs(self) -> list[str]:
26 if xdg_dirs := _xdg_dir_list("XDG_DATA_DIRS"):
27 return [self._join_app_name_and_version(p) for p in xdg_dirs]
28 return super()._site_data_dirs
29
30 @property
31 def site_data_dir(self) -> str:
32 """Data directories shared by users, from ``$XDG_DATA_DIRS`` if set, else platform default."""
33 return self._select_site_dirs(self._site_data_dirs)
34
35 @property
36 def user_config_dir(self) -> str:
37 """Config directory tied to the user, from ``$XDG_CONFIG_HOME`` if set, else platform default."""
38 if path := _xdg_dir("XDG_CONFIG_HOME"):
39 return self._append_app_name_and_version(path, private=True)
40 return super().user_config_dir
41
42 @property
43 def _site_config_dirs(self) -> list[str]:
44 if xdg_dirs := _xdg_dir_list("XDG_CONFIG_DIRS"):
45 return [self._join_app_name_and_version(p) for p in xdg_dirs]
46 return super()._site_config_dirs
47
48 @property
49 def site_config_dir(self) -> str:
50 """Config directories shared by users, from ``$XDG_CONFIG_DIRS`` if set, else platform default."""
51 return self._select_site_dirs(self._site_config_dirs)
52
53 @property
54 def user_cache_dir(self) -> str:
55 """Cache directory tied to the user, from ``$XDG_CACHE_HOME`` if set, else platform default."""
56 if path := _xdg_dir("XDG_CACHE_HOME"):
57 return self._append_app_name_and_version(path, private=True)
58 return super().user_cache_dir
59
60 @property
61 def user_state_dir(self) -> str:
62 """State directory tied to the user, from ``$XDG_STATE_HOME`` if set, else platform default."""
63 if path := _xdg_dir("XDG_STATE_HOME"):
64 return self._append_app_name_and_version(path, private=True)
65 return super().user_state_dir
66
67 @property
68 def user_runtime_dir(self) -> str:
69 """Runtime directory tied to the user, from ``$XDG_RUNTIME_DIR`` if set, else platform default."""
70 if path := _xdg_dir("XDG_RUNTIME_DIR"):
71 return self._append_app_name_and_version(path, private=True)
72 return super().user_runtime_dir
73
74 @property
75 def site_runtime_dir(self) -> str:
76 """Runtime directory shared by users, from ``$XDG_RUNTIME_DIR`` if set, else platform default."""
77 if path := _xdg_dir("XDG_RUNTIME_DIR"):
78 return self._append_app_name_and_version(path, private=False)
79 return super().site_runtime_dir
80
81 @property
82 def user_documents_dir(self) -> str:
83 """Documents directory tied to the user, from ``$XDG_DOCUMENTS_DIR`` if set, else platform default."""
84 return self._xdg_media_dir("XDG_DOCUMENTS_DIR") or super().user_documents_dir
85
86 @property
87 def user_downloads_dir(self) -> str:
88 """Downloads directory tied to the user, from ``$XDG_DOWNLOAD_DIR`` if set, else platform default."""
89 return self._xdg_media_dir("XDG_DOWNLOAD_DIR") or super().user_downloads_dir
90
91 @property
92 def user_pictures_dir(self) -> str:
93 """Pictures directory tied to the user, from ``$XDG_PICTURES_DIR`` if set, else platform default."""
94 return self._xdg_media_dir("XDG_PICTURES_DIR") or super().user_pictures_dir
95
96 @property
97 def user_videos_dir(self) -> str:
98 """Videos directory tied to the user, from ``$XDG_VIDEOS_DIR`` if set, else platform default."""
99 return self._xdg_media_dir("XDG_VIDEOS_DIR") or super().user_videos_dir
100
101 @property
102 def user_music_dir(self) -> str:
103 """Music directory tied to the user, from ``$XDG_MUSIC_DIR`` if set, else platform default."""
104 return self._xdg_media_dir("XDG_MUSIC_DIR") or super().user_music_dir
105
106 @property
107 def user_desktop_dir(self) -> str:
108 """Desktop directory tied to the user, from ``$XDG_DESKTOP_DIR`` if set, else platform default."""
109 return self._xdg_media_dir("XDG_DESKTOP_DIR") or super().user_desktop_dir
110
111 @property
112 def user_projects_dir(self) -> str:
113 """Projects directory tied to the user, from ``$XDG_PROJECTS_DIR`` if set, else platform default."""
114 return self._xdg_media_dir("XDG_PROJECTS_DIR") or super().user_projects_dir
115
116 @property
117 def user_publicshare_dir(self) -> str:
118 """Public share directory tied to the user, from ``$XDG_PUBLICSHARE_DIR`` if set, else platform default."""
119 return self._xdg_media_dir("XDG_PUBLICSHARE_DIR") or super().user_publicshare_dir
120
121 @property
122 def user_templates_dir(self) -> str:
123 """Templates directory tied to the user, from ``$XDG_TEMPLATES_DIR`` if set, else platform default."""
124 return self._xdg_media_dir("XDG_TEMPLATES_DIR") or super().user_templates_dir
125
126 def _xdg_media_dir(self, env_var: str) -> str | None:
127 if path := _xdg_dir(env_var):
128 self._optionally_create_media_directory(path)
129 return path
130
131 @property
132 def user_fonts_dir(self) -> str:
133 """Fonts directory tied to the user, from ``$XDG_DATA_HOME/fonts`` if set, else platform default."""
134 if path := _xdg_dir("XDG_DATA_HOME"):
135 return f"{os.path.expanduser(path)}/fonts" # ruff:ignore[os-path-expanduser] # API returns str, not Path
136 return super().user_fonts_dir
137
138 @property
139 def user_applications_dir(self) -> str:
140 """Applications directory tied to the user, from ``$XDG_DATA_HOME`` if set, else platform default."""
141 if path := _xdg_dir("XDG_DATA_HOME"):
142 return os.path.join(os.path.expanduser(path), "applications") # ruff:ignore[os-path-expanduser, os-path-join]
143 return super().user_applications_dir
144
145 @property
146 def _site_applications_dirs(self) -> list[str]:
147 if xdg_dirs := _xdg_dir_list("XDG_DATA_DIRS"):
148 return [os.path.join(p, "applications") for p in xdg_dirs] # ruff:ignore[os-path-join]
149 return super()._site_applications_dirs
150
151 @property
152 def site_applications_dir(self) -> str:
153 """Applications directories shared by users, from ``$XDG_DATA_DIRS`` if set, else platform default."""
154 dirs = self._site_applications_dirs
155 return os.pathsep.join(dirs) if self.multipath else dirs[0]
156
157
158def _xdg_dir(env_var: str) -> str | None:
159 path: Final = os.environ.get(env_var, "").strip()
160 return path if posixpath.isabs(path) else None
161
162
163def _xdg_dir_list(env_var: str) -> list[str]:
164 return [
165 stripped for path in os.environ.get(env_var, "").split(os.pathsep) if posixpath.isabs(stripped := path.strip())
166 ]
167
168
169def _expand_user(path: str) -> str:
170 """Expand the leading ``~`` of ``path`` like :func:`os.path.expanduser`, treating an empty ``HOME`` as unset.
171
172 :raises RuntimeError: if neither ``HOME`` nor the password database gives a home directory.
173
174 """
175 if "HOME" in os.environ and not os.environ["HOME"]:
176 # expanduser roots the path at / for an empty HOME; Rust's std::env::home_dir treats it as unset too
177 expanded = path if (home := _passwd_home()) is None else (home.rstrip("/") + path[1:] or "/")
178 else:
179 expanded = os.path.expanduser(path) # ruff:ignore[os-path-expanduser] # str(Path) rewrites separators on Windows
180 if expanded.startswith("~"):
181 msg = f"could not determine the home directory for {path!r}, set HOME or an absolute XDG variable"
182 raise RuntimeError(msg)
183 return expanded
184
185
186def _passwd_home() -> str | None:
187 if sys.platform == "win32": # pragma: win32 cover # narrows pwd and os.getuid for ty
188 return None
189 with suppress(ImportError, KeyError):
190 import pwd # ruff:ignore[import-outside-top-level] # missing on WASI
191
192 return pwd.getpwuid(os.getuid()).pw_dir
193 return None
194
195
196__all__ = [
197 "XDGMixin",
198 "_expand_user",
199 "_xdg_dir",
200]