hadalized 0.4.0__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.
hadalized/config.py ADDED
@@ -0,0 +1,442 @@
1
+ """Module containing all underlying color definitions and gamut info."""
2
+
3
+ from enum import StrEnum, auto
4
+ from pathlib import Path
5
+ from typing import Any, Self
6
+
7
+ from pydantic import Field, PrivateAttr
8
+ from pydantic_settings import (
9
+ BaseSettings,
10
+ PydanticBaseSettingsSource,
11
+ SettingsConfigDict,
12
+ TomlConfigSettingsSource,
13
+ )
14
+
15
+ from hadalized import homedirs
16
+ from hadalized.base import BaseNode
17
+ from hadalized.color import Bases, ColorFieldType, Hues, Ref
18
+ from hadalized.options import Options
19
+ from hadalized.palette import Palette
20
+
21
+
22
+ def default_palettes() -> dict[str, Palette]:
23
+ """Lazily compute default palette colors.
24
+
25
+ Returns:
26
+ A map of palette.name -> palette.
27
+
28
+ """
29
+ # Palette definitions
30
+ dark: Palette = Palette(
31
+ name="hadalized",
32
+ desc="Main dark theme with blueish solarized inspired backgrounds.",
33
+ mode="dark",
34
+ gamut="srgb",
35
+ aliases=["dark"],
36
+ hue=Hues.dark(),
37
+ base=Bases.dark(),
38
+ )
39
+
40
+ gray: Palette = Palette(
41
+ name="hadalized-gray",
42
+ desc="Dark theme variant with more grayish backgrounds.",
43
+ mode="dark",
44
+ gamut=dark.gamut,
45
+ aliases=["gray"],
46
+ hue=Hues.dark(),
47
+ base=Bases.dark()
48
+ | Bases(
49
+ bg=Ref.w13,
50
+ bg1=Ref.w14,
51
+ bg2=Ref.w16,
52
+ bg3=Ref.w20,
53
+ bg4=Ref.w25,
54
+ bg5=Ref.w30,
55
+ bg6=Ref.w35,
56
+ ),
57
+ )
58
+
59
+ day: Palette = Palette(
60
+ name="hadalized-day",
61
+ desc="Light theme variant with sunny backgrounds.",
62
+ mode="light",
63
+ gamut="srgb",
64
+ aliases=["day"],
65
+ hue=Hues.light(),
66
+ base=Bases.light(),
67
+ )
68
+
69
+ white: Palette = Palette(
70
+ name="hadalized-white",
71
+ desc="Light theme variant with whiter backgrounds.",
72
+ mode="light",
73
+ gamut=day.gamut,
74
+ aliases=["white"],
75
+ hue=Hues.light(),
76
+ base=day.base
77
+ | Bases(
78
+ bg=Ref.w100,
79
+ bg1=Ref.w99,
80
+ bg2=Ref.w95,
81
+ bg3=Ref.w92,
82
+ bg4=Ref.w99,
83
+ bg5=Ref.w85,
84
+ bg6=Ref.w80,
85
+ ),
86
+ )
87
+
88
+ return {
89
+ dark.name: dark,
90
+ gray.name: gray,
91
+ day.name: day,
92
+ white.name: white,
93
+ }
94
+
95
+
96
+ class ANSIMap(BaseNode):
97
+ """A mapping from color hue name to ANSI color index."""
98
+
99
+ red: int = 1
100
+ """Typically represents red."""
101
+ rose: int = 9
102
+ """Typically represents bright red."""
103
+ green: int = 2
104
+ """Typically represents green."""
105
+ lime: int = 10
106
+ """Typically represents bright green."""
107
+ yellow: int = 3
108
+ """Typically represents yellow."""
109
+ orange: int = 11
110
+ """Typically represents bright yellow."""
111
+ blue: int = 4
112
+ """Typically represents blue."""
113
+ azure: int = 12
114
+ """Typically represents bright blue."""
115
+ magenta: int = 5
116
+ """Typically represents magenta or purple."""
117
+ violet: int = 13
118
+ """Typically represents bright magenta or bright purple."""
119
+ cyan: int = 6
120
+ """Typically represents cyan."""
121
+ mint: int = 14
122
+ """Typically represents bright cyan."""
123
+ _idx_to_name: dict[int, str] = PrivateAttr({})
124
+
125
+ def model_post_init(self, context: Any, /) -> None:
126
+ """Model post init."""
127
+ super().model_post_init(context)
128
+ self._idx_to_name = {idx: name for name, idx in self}
129
+
130
+ def get_name(self, idx: int) -> str:
131
+ """Lookup the color name.
132
+
133
+ Returns:
134
+ The field name whose value is in the input.
135
+
136
+ """
137
+ return self._idx_to_name[idx]
138
+
139
+ @property
140
+ def pairing(self) -> list[tuple[str, str]]:
141
+ """A map of a color and it's 'bright' variant."""
142
+ return [(self.get_name(i), self.get_name(i + 8)) for i in range(1, 7)]
143
+
144
+
145
+ class TerminalConfig(BaseNode):
146
+ """Configurations related to terminal emulators."""
147
+
148
+ ansi: ANSIMap = ANSIMap()
149
+
150
+
151
+ class ContextType(StrEnum):
152
+ """Values determine which context expose to template when building a theme."""
153
+
154
+ palette = auto()
155
+ """A single palette will be passed to the `context` variable of a template."""
156
+ full = auto()
157
+ """A full `Config` instance will be passed to the `context` variable."""
158
+
159
+
160
+ class BuiltinThemes(StrEnum):
161
+ """Enumerates the list of themes that are handled by the builder."""
162
+
163
+ neovim = auto()
164
+ wezterm = auto()
165
+ starship = auto()
166
+
167
+
168
+ class BuildConfig(BaseNode):
169
+ """Information about which files should be generatted specific app."""
170
+
171
+ name: str = Field(
172
+ examples=["neovim", "myapp", "html-examples"],
173
+ )
174
+ """Application name or theme category."""
175
+ subdir: Path | None = None
176
+ """Build sub-directory where theme files are placed. Defaults to `name`."""
177
+ template: Path
178
+ """Template filename relative to the templates directory."""
179
+ filename: str | None = Field(
180
+ default=None,
181
+ examples=[
182
+ "{context.name}.{template_ext}", # default
183
+ "starship-alt.toml",
184
+ ],
185
+ )
186
+ """Output file name, including extension. For builds
187
+ that generate palette specific theme files, the default filename is of the
188
+ form `{palette.name}.{template.extension}`. For those that take in
189
+ all palettes into the context, the filename defaults to the underlying
190
+ template name.
191
+ """
192
+ context_type: ContextType = ContextType.palette
193
+ """The underlying context type to pass to the template."""
194
+ color_type: ColorFieldType = ColorFieldType.hex
195
+ """How each Palette should be transformed when presented as context
196
+ to the template."""
197
+ _fname: str = PrivateAttr(default="")
198
+
199
+ def model_post_init(self, context: Any, /) -> None:
200
+ """Construct filename template."""
201
+ filename = self.filename or ""
202
+ if not self.filename:
203
+ if self.context_type == ContextType.palette:
204
+ filename = "{context.name}.{ext}"
205
+ else:
206
+ filename = str(self.template)
207
+
208
+ # Infer extension from template file extension.
209
+ if filename.endswith(".{ext}"):
210
+ filename = filename.replace(".{ext}", self.template.suffix)
211
+ self._fname = filename.rstrip(".")
212
+ return super().model_post_init(context)
213
+
214
+ def format_path(self, context: BaseNode) -> Path:
215
+ """File output path relative to build directory.
216
+
217
+ Returns:
218
+ The absolute path where a file should be written.
219
+
220
+ """
221
+ fname = self._fname.format(context=context).rstrip(".")
222
+ return (self.subdir or Path(self.name)) / fname
223
+
224
+
225
+ def default_builds() -> dict[str, BuildConfig]:
226
+ """Builtin build configs.
227
+
228
+ Returns:
229
+ The default build instructions used to generate theme files.
230
+
231
+ """
232
+ return {
233
+ "neovim": BuildConfig(
234
+ name="neovim",
235
+ template=Path("neovim.lua"),
236
+ ),
237
+ "wezterm": BuildConfig(
238
+ name="wezterm",
239
+ template=Path("wezterm.toml"),
240
+ ),
241
+ "starship": BuildConfig(
242
+ name="starship",
243
+ template=Path("starship.toml"),
244
+ context_type=ContextType.full,
245
+ ),
246
+ "info": BuildConfig(
247
+ name="info",
248
+ template=Path("palette_info.json"),
249
+ color_type=ColorFieldType.info,
250
+ ),
251
+ "html-samples": BuildConfig(
252
+ name="html-samples",
253
+ template=Path("palette.html"),
254
+ color_type=ColorFieldType.css,
255
+ ),
256
+ }
257
+
258
+
259
+ class Config(Options):
260
+ """App configuration.
261
+
262
+ Contains information about which app theme files to generate and where
263
+ to write the build artifacts.
264
+
265
+ This particular Config will not load settings from anything except
266
+ init arguments, and as such serves as a default Config base.
267
+ """
268
+
269
+ builds: dict[str, BuildConfig] = Field(default_factory=default_builds)
270
+ """Build directives specifying how and which theme files are
271
+ generated."""
272
+ palettes: dict[str, Palette] = Field(default_factory=default_palettes)
273
+ """Palette color definitions."""
274
+ terminal: TerminalConfig = TerminalConfig()
275
+ _palette_lu: dict[str, Palette] = PrivateAttr(default={})
276
+ """Lookup for a palette by name or alias."""
277
+ _opts: Options | None = PrivateAttr(default=None)
278
+
279
+ @classmethod
280
+ def settings_customise_sources(
281
+ cls,
282
+ settings_cls: type[BaseSettings],
283
+ init_settings: PydanticBaseSettingsSource,
284
+ env_settings: PydanticBaseSettingsSource,
285
+ dotenv_settings: PydanticBaseSettingsSource,
286
+ file_secret_settings: PydanticBaseSettingsSource,
287
+ ) -> tuple[PydanticBaseSettingsSource, ...]:
288
+ """Set source loading priority.
289
+
290
+ Returns:
291
+ Priority order in which config settings are loaded.
292
+
293
+ """
294
+ return (init_settings,)
295
+
296
+ def model_post_init(self, context, /) -> None:
297
+ """Set lookups."""
298
+ for key, palette in self.palettes.items():
299
+ self._palette_lu[key] = palette
300
+ for alias in palette.aliases:
301
+ self._palette_lu[alias] = palette
302
+
303
+ return super().model_post_init(context)
304
+
305
+ @property
306
+ def opt(self) -> Options:
307
+ """Access just the runtime options from the configuration."""
308
+ if self._opts is None:
309
+ fields = set(Options.model_fields)
310
+ opts = {k: v for k, v in self if k in fields and k in self.model_fields_set}
311
+ self._opts = Options.model_construct(**opts)
312
+ return self._opts
313
+
314
+ def get_palette(self, name: str) -> Palette:
315
+ """Get Palette by name or alias.
316
+
317
+ Returns:
318
+ Palette instance.
319
+
320
+ """
321
+ return self._palette_lu[name]
322
+
323
+ def to(self, color_type: str | ColorFieldType) -> Self:
324
+ """Transform the ColorFields to the specified type.
325
+
326
+ Use to render themes that require the entire context (e.g., all palettes),
327
+ but where specific color representations (e.g., hex)
328
+ are required.
329
+
330
+ Returns:
331
+ A new Config instance whose ColorFields match the input type.
332
+
333
+ """
334
+ return self.replace(
335
+ palettes={k: v.parse().to(color_type) for k, v in self.palettes.items()}
336
+ )
337
+
338
+ def parse_palettes(self) -> Self:
339
+ """Parse each Palette to contain full ColorInfo.
340
+
341
+ Returns:
342
+ A new instance with each Palette a ParsedPalette instance.
343
+
344
+ """
345
+ return self.replace(palettes={k: v.parse() for k, v in self.palettes.items()})
346
+
347
+ def __hash__(self) -> int:
348
+ """Hash of the main config contents, excluding runtime options.
349
+
350
+ Returns:
351
+ The hash of the json dump of the instance.
352
+
353
+ """
354
+ if self._hash is None:
355
+ include = {"palettes", "build", "terminal"}
356
+ self._hash = hash(self.model_dump_json(include=include))
357
+ return self._hash
358
+
359
+
360
+ class UserConfig(Config):
361
+ """User configuration settings.
362
+
363
+ While schematically identical to the base ``Config`` parent class, when
364
+ a UserConfig is instantiated a selection of settings locations are
365
+ additionally scanned. The priority of settings is
366
+
367
+ - init params, e.g., those passed from the CLI
368
+ - environment variables prefixed with `HADALIZED_`
369
+ - environment variables in `./hadalized.env` prefixxed with `HADALIZED_`
370
+ - environment variables in `./.env` prefixxed with `HADALIZED_`
371
+ - settings in `./hadalized.toml`
372
+ - settings in `$XDG_CONFIG_DIR/hadalized/config.toml`
373
+ """
374
+
375
+ model_config = SettingsConfigDict(
376
+ frozen=True,
377
+ env_file=[".env", "hadalized.env"],
378
+ env_file_encoding="utf-8",
379
+ # The env_nested_delimiter=_ and max_split=1 means
380
+ # HADALIZED_OPTS_CACHE_DIR == Config.opts.cache_dir
381
+ # otherwise with delimiter=__ we would need to pass
382
+ # HADALIZED_OPTS__CACHE_DIR
383
+ env_nested_delimiter="_",
384
+ env_nested_max_split=1,
385
+ env_prefix="hadalized_",
386
+ env_parse_none_str="null",
387
+ env_parse_enums=True,
388
+ # env_ignore_empty=True,
389
+ extra="forbid",
390
+ nested_model_default_partial_update=True,
391
+ toml_file=[homedirs.config() / "config.toml", "hadalized.toml"],
392
+ )
393
+
394
+ @classmethod
395
+ def settings_customise_sources(
396
+ cls,
397
+ settings_cls: type[BaseSettings],
398
+ init_settings: PydanticBaseSettingsSource,
399
+ env_settings: PydanticBaseSettingsSource,
400
+ dotenv_settings: PydanticBaseSettingsSource,
401
+ file_secret_settings: PydanticBaseSettingsSource,
402
+ ) -> tuple[PydanticBaseSettingsSource, ...]:
403
+ """Set source loading priority.
404
+
405
+ Returns:
406
+ Priority order in which config settings are loaded.
407
+
408
+ """
409
+ return (
410
+ init_settings,
411
+ env_settings,
412
+ dotenv_settings,
413
+ file_secret_settings,
414
+ TomlConfigSettingsSource(settings_cls),
415
+ )
416
+
417
+
418
+ def load_config(opt: Options | None = None) -> Config:
419
+ """Load a configuration instance with the cli options merged in.
420
+
421
+ Handles the cases when a user specifies a specific user config file
422
+ or when only the default configuration should be used.
423
+
424
+ Args:
425
+ opt: Options that determine which configuration sources are utilized.
426
+
427
+ Returns:
428
+ A Config or UserConfig instance.
429
+
430
+ """
431
+ if opt is None:
432
+ config = UserConfig()
433
+ elif opt.config_file is not None:
434
+ import tomllib
435
+
436
+ data = opt.config_file.read_text()
437
+ config = Config.model_validate(tomllib.loads(data)) | opt
438
+ elif opt.no_config:
439
+ config = Config() | opt
440
+ else:
441
+ config = UserConfig() | opt
442
+ return config.parse_palettes() if config.parse else config
hadalized/const.py ADDED
@@ -0,0 +1,6 @@
1
+ """Constants."""
2
+
3
+ from importlib.metadata import version
4
+
5
+ APP_NAME: str = "hadalized"
6
+ APP_VERSION = version("hadalized")