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/__init__.py +1 -0
- hadalized/__main__.py +6 -0
- hadalized/base.py +137 -0
- hadalized/cache.py +121 -0
- hadalized/cli/__init__.py +1 -0
- hadalized/cli/main.py +221 -0
- hadalized/color.py +595 -0
- hadalized/config.py +442 -0
- hadalized/const.py +6 -0
- hadalized/convert.py.bak +1903 -0
- hadalized/homedirs.py +69 -0
- hadalized/options.py +134 -0
- hadalized/palette.py +133 -0
- hadalized/templates/colors.html +21 -0
- hadalized/templates/config.toml +15 -0
- hadalized/templates/generic.txt +1 -0
- hadalized/templates/item.html +1 -0
- hadalized/templates/lua_module.lua +6 -0
- hadalized/templates/model_dump.json +1 -0
- hadalized/templates/neovim.lua +24 -0
- hadalized/templates/neovim_palette.lua +7 -0
- hadalized/templates/palette.html +53 -0
- hadalized/templates/palette_info.json +1 -0
- hadalized/templates/palette_test.toml +10 -0
- hadalized/templates/starship-all.toml +98 -0
- hadalized/templates/starship.toml +233 -0
- hadalized/templates/wezterm.toml +45 -0
- hadalized/web.py +168 -0
- hadalized/writer.py +243 -0
- hadalized-0.4.0.dist-info/METADATA +79 -0
- hadalized-0.4.0.dist-info/RECORD +33 -0
- hadalized-0.4.0.dist-info/WHEEL +4 -0
- hadalized-0.4.0.dist-info/entry_points.txt +4 -0
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
|