m3wal 1.0.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.
- m3wal/__init__.py +0 -0
- m3wal/m3wal.py +615 -0
- m3wal/templates/colors-alacritty.toml.template +5 -0
- m3wal/templates/colors-nvim.lua.template +199 -0
- m3wal/templates/colors-tty.sh.template +21 -0
- m3wal/templates/colors-waybar.css.template +28 -0
- m3wal/templates/colors.Xresources.template +176 -0
- m3wal/templates/colors.css.template +99 -0
- m3wal/templates/colors.hs.template +128 -0
- m3wal/templates/colors.json.template +71 -0
- m3wal/templates/colors.scss.template +127 -0
- m3wal/templates/colors.sh.template +117 -0
- m3wal/templates/colors.template +16 -0
- m3wal/templates/colors.yml.template +22 -0
- m3wal/templates/dunst-config.template +20 -0
- m3wal/templates/gtk.3.20.template +110 -0
- m3wal/templates/gtk.css.template +93 -0
- m3wal/templates/gtkrc.template +797 -0
- m3wal/templates/kitty.conf.template +29 -0
- m3wal/templates/polybar-config.template +19 -0
- m3wal/templates/rmpc.ron.template +171 -0
- m3wal/templates/rofi.rasi.template +133 -0
- m3wal/templates/sequences.template +1 -0
- m3wal/templates/zathurarc.template +77 -0
- m3wal-1.0.0.dist-info/METADATA +17 -0
- m3wal-1.0.0.dist-info/RECORD +30 -0
- m3wal-1.0.0.dist-info/WHEEL +5 -0
- m3wal-1.0.0.dist-info/entry_points.txt +2 -0
- m3wal-1.0.0.dist-info/licenses/LICENSE.txt +339 -0
- m3wal-1.0.0.dist-info/top_level.txt +1 -0
m3wal/__init__.py
ADDED
|
File without changes
|
m3wal/m3wal.py
ADDED
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
"""
|
|
2
|
+
M3WAL: Wallpaper to Material 3 Color Scheme Generator
|
|
3
|
+
Version: Official Library (material-color-utilities)
|
|
4
|
+
|
|
5
|
+
Install: pip install material-color-utilities Pillow
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from material_color_utilities import Variant, hex_from_argb, theme_from_image
|
|
13
|
+
from PIL import Image
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class M3WAL:
|
|
17
|
+
def __init__(self, wallpaper_path, config=None):
|
|
18
|
+
self.wallpaper_path = wallpaper_path
|
|
19
|
+
self.theme = None
|
|
20
|
+
self.mode = None
|
|
21
|
+
self.source_color = None
|
|
22
|
+
self.config = config if config else self.load_config()
|
|
23
|
+
self.brightness_threshold = int(self.config.get('General', 'brightness_threshold', fallback='128'))
|
|
24
|
+
|
|
25
|
+
def load_config(self):
|
|
26
|
+
"""Load configuration from m3-colors.conf"""
|
|
27
|
+
import configparser
|
|
28
|
+
|
|
29
|
+
config_file = Path.home() / ".config" / "m3-colors" / "m3-colors.conf"
|
|
30
|
+
|
|
31
|
+
# Default config
|
|
32
|
+
defaults = {
|
|
33
|
+
'mode': 'auto',
|
|
34
|
+
'variant': 'CONTENT',
|
|
35
|
+
'brightness_threshold': '128',
|
|
36
|
+
'templates_dir': 'templates',
|
|
37
|
+
'cache_dir': '~/.cache/m3-colors',
|
|
38
|
+
'config_dir': '~/.config/m3-colors',
|
|
39
|
+
'set_wallpaper': 'true',
|
|
40
|
+
'apply_xresources': 'true',
|
|
41
|
+
'generate_palette_preview': 'true',
|
|
42
|
+
'run_post_script': 'true',
|
|
43
|
+
'create_symlink': 'true',
|
|
44
|
+
'script_path': 'm3wal-post.sh'
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
config = configparser.ConfigParser()
|
|
48
|
+
|
|
49
|
+
if config_file.exists():
|
|
50
|
+
config.read(config_file)
|
|
51
|
+
else:
|
|
52
|
+
# make default config
|
|
53
|
+
config['General'] = {
|
|
54
|
+
'mode': defaults['mode'],
|
|
55
|
+
'variant': defaults['variant'],
|
|
56
|
+
'brightness_threshold': defaults['brightness_threshold']
|
|
57
|
+
}
|
|
58
|
+
config['Paths'] = {
|
|
59
|
+
'templates_dir': defaults['templates_dir'],
|
|
60
|
+
'cache_dir': defaults['cache_dir'],
|
|
61
|
+
'config_dir': defaults['config_dir']
|
|
62
|
+
}
|
|
63
|
+
config['Features'] = {
|
|
64
|
+
'set_wallpaper': defaults['set_wallpaper'],
|
|
65
|
+
'apply_xresources': defaults['apply_xresources'],
|
|
66
|
+
'generate_palette_preview': defaults['generate_palette_preview'],
|
|
67
|
+
'run_post_script': defaults['run_post_script'],
|
|
68
|
+
'create_symlink': defaults['create_symlink']
|
|
69
|
+
}
|
|
70
|
+
config['PostScript'] = {
|
|
71
|
+
'script_path': defaults['script_path']
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
config_file.parent.mkdir(parents=True, exist_ok=True)
|
|
75
|
+
with open(config_file, 'w') as f:
|
|
76
|
+
config.write(f)
|
|
77
|
+
|
|
78
|
+
return config
|
|
79
|
+
|
|
80
|
+
def analyze_wallpaper(self):
|
|
81
|
+
"""Extract dominant color & detect brightness"""
|
|
82
|
+
img = Image.open(self.wallpaper_path)
|
|
83
|
+
|
|
84
|
+
# Detect brightness for auto mode
|
|
85
|
+
grayscale = img.convert("L")
|
|
86
|
+
pixels = grayscale.tobytes()
|
|
87
|
+
avg_brightness = sum(pixels) / len(pixels)
|
|
88
|
+
|
|
89
|
+
# Auto select light/dark mode based on threshold
|
|
90
|
+
self.mode = "dark" if avg_brightness < self.brightness_threshold else "light"
|
|
91
|
+
|
|
92
|
+
return {"brightness": avg_brightness, "mode": self.mode}
|
|
93
|
+
|
|
94
|
+
def generate_scheme(self, mode=None, variant="CONTENT"):
|
|
95
|
+
"""Generate Material 3 color scheme"""
|
|
96
|
+
if mode:
|
|
97
|
+
self.mode = mode
|
|
98
|
+
|
|
99
|
+
self.variant = variant
|
|
100
|
+
|
|
101
|
+
# Map string to Variant enum
|
|
102
|
+
variant_map = {
|
|
103
|
+
"TONALSPOT": Variant.TONALSPOT,
|
|
104
|
+
"VIBRANT": Variant.VIBRANT,
|
|
105
|
+
"EXPRESSIVE": Variant.EXPRESSIVE,
|
|
106
|
+
"NEUTRAL": Variant.NEUTRAL,
|
|
107
|
+
"FIDELITY": Variant.FIDELITY,
|
|
108
|
+
"CONTENT": Variant.CONTENT,
|
|
109
|
+
"MONOCHROME": Variant.MONOCHROME,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
variant_enum = variant_map.get(variant.upper(), Variant.CONTENT)
|
|
113
|
+
|
|
114
|
+
# Generate theme from image
|
|
115
|
+
img = Image.open(self.wallpaper_path)
|
|
116
|
+
self.theme = theme_from_image(img, 0, variant_enum)
|
|
117
|
+
|
|
118
|
+
# Get source color
|
|
119
|
+
self.source_color = self.theme.source
|
|
120
|
+
|
|
121
|
+
return self._extract_colors()
|
|
122
|
+
|
|
123
|
+
def _extract_colors(self):
|
|
124
|
+
"""Extract all M3 colors + 16 terminal colors"""
|
|
125
|
+
if not self.theme:
|
|
126
|
+
raise ValueError("Generate scheme first!")
|
|
127
|
+
|
|
128
|
+
# Select scheme based on mode
|
|
129
|
+
scheme = (
|
|
130
|
+
self.theme.schemes.dark if self.mode == "dark" else self.theme.schemes.light
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
m3_colors = {
|
|
134
|
+
# Primary
|
|
135
|
+
"m3primary": scheme.primary,
|
|
136
|
+
"m3onPrimary": scheme.on_primary,
|
|
137
|
+
"m3primaryContainer": scheme.primary_container,
|
|
138
|
+
"m3onPrimaryContainer": scheme.on_primary_container,
|
|
139
|
+
"m3primaryFixed": scheme.primary_fixed,
|
|
140
|
+
"m3primaryFixedDim": scheme.primary_fixed_dim,
|
|
141
|
+
"m3onPrimaryFixed": scheme.on_primary_fixed,
|
|
142
|
+
"m3onPrimaryFixedVariant": scheme.on_primary_fixed_variant,
|
|
143
|
+
# Secondary
|
|
144
|
+
"m3secondary": scheme.secondary,
|
|
145
|
+
"m3onSecondary": scheme.on_secondary,
|
|
146
|
+
"m3secondaryContainer": scheme.secondary_container,
|
|
147
|
+
"m3onSecondaryContainer": scheme.on_secondary_container,
|
|
148
|
+
"m3secondaryFixed": scheme.secondary_fixed,
|
|
149
|
+
"m3secondaryFixedDim": scheme.secondary_fixed_dim,
|
|
150
|
+
"m3onSecondaryFixed": scheme.on_secondary_fixed,
|
|
151
|
+
"m3onSecondaryFixedVariant": scheme.on_secondary_fixed_variant,
|
|
152
|
+
# Tertiary
|
|
153
|
+
"m3tertiary": scheme.tertiary,
|
|
154
|
+
"m3onTertiary": scheme.on_tertiary,
|
|
155
|
+
"m3tertiaryContainer": scheme.tertiary_container,
|
|
156
|
+
"m3onTertiaryContainer": scheme.on_tertiary_container,
|
|
157
|
+
"m3tertiaryFixed": scheme.tertiary_fixed,
|
|
158
|
+
"m3tertiaryFixedDim": scheme.tertiary_fixed_dim,
|
|
159
|
+
"m3onTertiaryFixed": scheme.on_tertiary_fixed,
|
|
160
|
+
"m3onTertiaryFixedVariant": scheme.on_tertiary_fixed_variant,
|
|
161
|
+
# Error
|
|
162
|
+
"m3error": scheme.error,
|
|
163
|
+
"m3onError": scheme.on_error,
|
|
164
|
+
"m3errorContainer": scheme.error_container,
|
|
165
|
+
"m3onErrorContainer": scheme.on_error_container,
|
|
166
|
+
# Surface
|
|
167
|
+
"m3surface": scheme.surface,
|
|
168
|
+
"m3onSurface": scheme.on_surface,
|
|
169
|
+
"m3surfaceVariant": scheme.surface_variant,
|
|
170
|
+
"m3onSurfaceVariant": scheme.on_surface_variant,
|
|
171
|
+
"m3surfaceDim": scheme.surface_dim,
|
|
172
|
+
"m3surfaceBright": scheme.surface_bright,
|
|
173
|
+
"m3surfaceContainerLowest": scheme.surface_container_lowest,
|
|
174
|
+
"m3surfaceContainerLow": scheme.surface_container_low,
|
|
175
|
+
"m3surfaceContainer": scheme.surface_container,
|
|
176
|
+
"m3surfaceContainerHigh": scheme.surface_container_high,
|
|
177
|
+
"m3surfaceContainerHighest": scheme.surface_container_highest,
|
|
178
|
+
# Background (deprecated in M3 but kept for compatibility)
|
|
179
|
+
"m3background": scheme.surface,
|
|
180
|
+
"m3onBackground": scheme.on_surface,
|
|
181
|
+
# Outline
|
|
182
|
+
"m3outline": scheme.outline,
|
|
183
|
+
"m3outlineVariant": scheme.outline_variant,
|
|
184
|
+
# Inverse
|
|
185
|
+
"m3inverseSurface": scheme.inverse_surface,
|
|
186
|
+
"m3inverseOnSurface": scheme.inverse_on_surface,
|
|
187
|
+
"m3inversePrimary": scheme.inverse_primary,
|
|
188
|
+
# Shadow & Scrim
|
|
189
|
+
"m3shadow": scheme.shadow,
|
|
190
|
+
"m3scrim": scheme.scrim,
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
# Generate terminal colors
|
|
194
|
+
terminal_colors = self._generate_terminal_colors(scheme)
|
|
195
|
+
|
|
196
|
+
return {**m3_colors, **terminal_colors}
|
|
197
|
+
|
|
198
|
+
def _generate_terminal_colors(self, scheme):
|
|
199
|
+
"""Generate 16 terminal colors from M3 palette"""
|
|
200
|
+
return {
|
|
201
|
+
# Normal colors
|
|
202
|
+
"term0": scheme.surface_dim, # Black
|
|
203
|
+
"term1": scheme.error, # Red
|
|
204
|
+
"term2": scheme.tertiary, # Green
|
|
205
|
+
"term3": scheme.primary_fixed_dim, # Yellow
|
|
206
|
+
"term4": scheme.primary, # Blue
|
|
207
|
+
"term5": scheme.secondary, # Magenta
|
|
208
|
+
"term6": scheme.tertiary_container, # Cyan
|
|
209
|
+
"term7": scheme.on_surface, # White
|
|
210
|
+
# Bright colors
|
|
211
|
+
"term8": scheme.surface_container_high, # Bright Black
|
|
212
|
+
"term9": scheme.error_container, # Bright Red
|
|
213
|
+
"term10": scheme.tertiary_fixed, # Bright Green
|
|
214
|
+
"term11": scheme.primary_fixed, # Bright Yellow
|
|
215
|
+
"term12": scheme.primary_container, # Bright Blue
|
|
216
|
+
"term13": scheme.secondary_container, # Bright Magenta
|
|
217
|
+
"term14": scheme.tertiary_fixed_dim, # Bright Cyan
|
|
218
|
+
"term15": scheme.surface_bright, # Bright White
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
def apply_all_templates(self, templates_dir=None, output_dir=None):
|
|
222
|
+
"""Apply colors to all templates in templates directory"""
|
|
223
|
+
if not self.theme:
|
|
224
|
+
raise ValueError("Generate scheme first!")
|
|
225
|
+
|
|
226
|
+
if output_dir is None:
|
|
227
|
+
output_dir = Path.home() / ".cache" / "m3-colors"
|
|
228
|
+
|
|
229
|
+
# Use package templates if not specified
|
|
230
|
+
if templates_dir is None:
|
|
231
|
+
try:
|
|
232
|
+
import pkg_resources
|
|
233
|
+
templates_dir = pkg_resources.resource_filename('m3wal', 'templates')
|
|
234
|
+
except:
|
|
235
|
+
# Fallback to config dir
|
|
236
|
+
templates_dir = Path.home() / ".config" / "m3-colors" / "templates"
|
|
237
|
+
|
|
238
|
+
templates_path = Path(templates_dir)
|
|
239
|
+
output_path = Path(output_dir)
|
|
240
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
241
|
+
|
|
242
|
+
# Cari semua file .template
|
|
243
|
+
template_files = list(templates_path.glob("*.template"))
|
|
244
|
+
|
|
245
|
+
if not template_files:
|
|
246
|
+
print(f"No template files found in {templates_dir}/")
|
|
247
|
+
return []
|
|
248
|
+
|
|
249
|
+
generated_files = []
|
|
250
|
+
|
|
251
|
+
print(f"\nApplying colors to templates...")
|
|
252
|
+
for template_file in template_files:
|
|
253
|
+
# Hilangkan ekstensi .template untuk nama output
|
|
254
|
+
output_filename = template_file.stem
|
|
255
|
+
output_file = output_path / output_filename
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
self.apply_template(template_file, output_file)
|
|
259
|
+
generated_files.append(str(output_file))
|
|
260
|
+
print(f"✓ {template_file.name} → {output_file}")
|
|
261
|
+
except Exception as e:
|
|
262
|
+
print(f"✗ {template_file.name}: {e}")
|
|
263
|
+
|
|
264
|
+
return generated_files
|
|
265
|
+
|
|
266
|
+
def create_wallpaper_symlink(self):
|
|
267
|
+
"""Create symlink to current wallpaper in ~/.config/m3-colors"""
|
|
268
|
+
config_dir = Path.home() / ".config" / "m3-colors"
|
|
269
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
270
|
+
|
|
271
|
+
symlink_path = config_dir / "current_wallpaper"
|
|
272
|
+
wallpaper_path = Path(self.wallpaper_path).resolve()
|
|
273
|
+
|
|
274
|
+
# Delete old symlink if exist
|
|
275
|
+
if symlink_path.exists() or symlink_path.is_symlink():
|
|
276
|
+
symlink_path.unlink()
|
|
277
|
+
|
|
278
|
+
# Make new symlink
|
|
279
|
+
symlink_path.symlink_to(wallpaper_path)
|
|
280
|
+
print(f"Created symlink → {symlink_path}")
|
|
281
|
+
|
|
282
|
+
def load_deploy_config(self):
|
|
283
|
+
"""Load deployment mappings from config"""
|
|
284
|
+
config_file = Path.home() / ".config" / "m3-colors" / "deploy.json"
|
|
285
|
+
|
|
286
|
+
default_config = {
|
|
287
|
+
"deployments": [
|
|
288
|
+
{"source": "colors-nvim.lua", "destination": "~/.config/nvim/lua/themes/material3.lua"},
|
|
289
|
+
{"source": "gtkrc", "destination": "~/.local/share/themes/FlatColor/gtk-2.0/gtkrc"},
|
|
290
|
+
{"source": "gtk.css", "destination": "~/.local/share/themes/FlatColor/gtk-3.0/gtk.css"},
|
|
291
|
+
{"source": "gtk.3.20", "destination": "~/.local/share/themes/FlatColor/gtk-3.20/gtk.css"}
|
|
292
|
+
]
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if config_file.exists():
|
|
296
|
+
with open(config_file, 'r') as f:
|
|
297
|
+
return json.load(f)
|
|
298
|
+
else:
|
|
299
|
+
config_file.parent.mkdir(parents=True, exist_ok=True)
|
|
300
|
+
with open(config_file, 'w') as f:
|
|
301
|
+
json.dump(default_config, f, indent=2)
|
|
302
|
+
return default_config
|
|
303
|
+
|
|
304
|
+
def deploy_configs(self):
|
|
305
|
+
"""Deploy configs based on deploy.json"""
|
|
306
|
+
import shutil
|
|
307
|
+
|
|
308
|
+
cache_dir = Path.home() / ".cache" / "m3-colors"
|
|
309
|
+
config = self.load_deploy_config()
|
|
310
|
+
|
|
311
|
+
for item in config.get("deployments", []):
|
|
312
|
+
src = cache_dir / item["source"]
|
|
313
|
+
dest = Path(item["destination"]).expanduser()
|
|
314
|
+
|
|
315
|
+
if src.exists():
|
|
316
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
317
|
+
shutil.copy2(src, dest)
|
|
318
|
+
print(f"{item['source']} → {dest}")
|
|
319
|
+
else:
|
|
320
|
+
print(f"{item['source']} not found")
|
|
321
|
+
|
|
322
|
+
def run_post_script(self, script_path=None):
|
|
323
|
+
"""Run post-generation script"""
|
|
324
|
+
import subprocess
|
|
325
|
+
|
|
326
|
+
if script_path is None:
|
|
327
|
+
script_path = Path.home() / ".config" / "m3-colors" / "m3wal-post.sh"
|
|
328
|
+
|
|
329
|
+
script = Path(script_path).expanduser()
|
|
330
|
+
if script.exists():
|
|
331
|
+
subprocess.run(["bash", str(script)])
|
|
332
|
+
print(f"Executed {script.name}")
|
|
333
|
+
|
|
334
|
+
def apply_xresources(self):
|
|
335
|
+
import subprocess
|
|
336
|
+
|
|
337
|
+
xresources = Path.home() / ".cache" / "m3-colors" / "colors.Xresources"
|
|
338
|
+
if xresources.exists():
|
|
339
|
+
subprocess.run(["xrdb", "-merge", str(xresources)])
|
|
340
|
+
print(f"Applied Xresources")
|
|
341
|
+
|
|
342
|
+
def export_json(self, output_path=None, variant="CONTENT"):
|
|
343
|
+
"""Export scheme to JSON"""
|
|
344
|
+
if not self.theme:
|
|
345
|
+
raise ValueError("Generate scheme first!")
|
|
346
|
+
colors = self._extract_colors()
|
|
347
|
+
|
|
348
|
+
# Save to ~/.config/m3-colors/output only
|
|
349
|
+
config_dir = Path.home() / ".config" / "m3-colors" / "output"
|
|
350
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
351
|
+
wallpaper_name = Path(self.wallpaper_path).stem
|
|
352
|
+
config_path = config_dir / f"{wallpaper_name}_{variant}_scheme.json"
|
|
353
|
+
|
|
354
|
+
output = {
|
|
355
|
+
"wallpaper": self.wallpaper_path,
|
|
356
|
+
"mode": self.mode,
|
|
357
|
+
"variant": variant,
|
|
358
|
+
"source_color": (
|
|
359
|
+
hex_from_argb(self.source_color)
|
|
360
|
+
if isinstance(self.source_color, int)
|
|
361
|
+
else self.source_color
|
|
362
|
+
),
|
|
363
|
+
"colors": colors,
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
with open(config_path, "w") as f:
|
|
367
|
+
json.dump(output, f, indent=2)
|
|
368
|
+
|
|
369
|
+
print(f"Exported to: {config_path}")
|
|
370
|
+
return str(config_path)
|
|
371
|
+
|
|
372
|
+
def set_wallpaper(self):
|
|
373
|
+
"""Set wallpaper using feh"""
|
|
374
|
+
import subprocess
|
|
375
|
+
|
|
376
|
+
wallpaper = Path(self.wallpaper_path).expanduser()
|
|
377
|
+
if wallpaper.exists():
|
|
378
|
+
subprocess.run(["feh", "--bg-fill", str(wallpaper)])
|
|
379
|
+
print(f"Set wallpaper with feh")
|
|
380
|
+
|
|
381
|
+
def apply_template(self, template_path, output_path):
|
|
382
|
+
"""Apply colors to template file"""
|
|
383
|
+
if not self.theme:
|
|
384
|
+
raise ValueError("Generate scheme first!")
|
|
385
|
+
|
|
386
|
+
colors = self._extract_colors()
|
|
387
|
+
|
|
388
|
+
# Add metadata to dictionary colors
|
|
389
|
+
colors["wallpaper_path"] = self.wallpaper_path
|
|
390
|
+
colors["mode"] = self.mode
|
|
391
|
+
colors["source_color"] = (
|
|
392
|
+
hex_from_argb(self.source_color)
|
|
393
|
+
if isinstance(self.source_color, int)
|
|
394
|
+
else self.source_color
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
with open(template_path, "r") as f:
|
|
398
|
+
template = f.read()
|
|
399
|
+
|
|
400
|
+
# Replace {{m3primary}}, {{term0}}, {{wallpaper_path}}, etc.
|
|
401
|
+
for key, value in colors.items():
|
|
402
|
+
template = template.replace(f"{{{{{key}}}}}", str(value))
|
|
403
|
+
|
|
404
|
+
with open(output_path, "w") as f:
|
|
405
|
+
f.write(template)
|
|
406
|
+
|
|
407
|
+
return output_path
|
|
408
|
+
|
|
409
|
+
def generate_palette_preview(self, output_path=None):
|
|
410
|
+
"""Generate color palette preview image"""
|
|
411
|
+
if not self.theme:
|
|
412
|
+
raise ValueError("Generate scheme first!")
|
|
413
|
+
try:
|
|
414
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
415
|
+
except ImportError:
|
|
416
|
+
print("PIL/Pillow not installed. Install: pip install Pillow")
|
|
417
|
+
return None
|
|
418
|
+
|
|
419
|
+
variant = self.variant if hasattr(self, 'variant') else 'CONTENT'
|
|
420
|
+
colors_dict = self._extract_colors()
|
|
421
|
+
|
|
422
|
+
# All sequence M3 color
|
|
423
|
+
all_colors = [
|
|
424
|
+
# Primary colors
|
|
425
|
+
'm3primary', 'm3onPrimary', 'm3primaryContainer', 'm3onPrimaryContainer',
|
|
426
|
+
'm3primaryFixed', 'm3primaryFixedDim', 'm3onPrimaryFixed', 'm3onPrimaryFixedVariant',
|
|
427
|
+
|
|
428
|
+
# Secondary colors
|
|
429
|
+
'm3secondary', 'm3onSecondary', 'm3secondaryContainer', 'm3onSecondaryContainer',
|
|
430
|
+
'm3secondaryFixed', 'm3secondaryFixedDim', 'm3onSecondaryFixed', 'm3onSecondaryFixedVariant',
|
|
431
|
+
|
|
432
|
+
# Tertiary colors
|
|
433
|
+
'm3tertiary', 'm3onTertiary', 'm3tertiaryContainer', 'm3onTertiaryContainer',
|
|
434
|
+
'm3tertiaryFixed', 'm3tertiaryFixedDim', 'm3onTertiaryFixed', 'm3onTertiaryFixedVariant',
|
|
435
|
+
|
|
436
|
+
# Error colors
|
|
437
|
+
'm3error', 'm3onError', 'm3errorContainer', 'm3onErrorContainer',
|
|
438
|
+
|
|
439
|
+
# Surface colors
|
|
440
|
+
'm3surface', 'm3onSurface', 'm3surfaceVariant', 'm3onSurfaceVariant',
|
|
441
|
+
'm3surfaceDim', 'm3surfaceBright', 'm3surfaceContainerLowest', 'm3surfaceContainerLow',
|
|
442
|
+
'm3surfaceContainer', 'm3surfaceContainerHigh', 'm3surfaceContainerHighest',
|
|
443
|
+
|
|
444
|
+
# Outline colors
|
|
445
|
+
'm3outline', 'm3outlineVariant',
|
|
446
|
+
|
|
447
|
+
# Inverse colors
|
|
448
|
+
'm3inverseSurface', 'm3inverseOnSurface', 'm3inversePrimary',
|
|
449
|
+
|
|
450
|
+
# Shadow & Scrim
|
|
451
|
+
'm3shadow', 'm3scrim',
|
|
452
|
+
]
|
|
453
|
+
|
|
454
|
+
# Terminal colors
|
|
455
|
+
all_colors.extend([f'term{i}' for i in range(16)])
|
|
456
|
+
|
|
457
|
+
# Filter
|
|
458
|
+
available_colors = [key for key in all_colors if colors_dict.get(key) is not None]
|
|
459
|
+
|
|
460
|
+
# configuration grid
|
|
461
|
+
MAX_COLS = 16
|
|
462
|
+
cell_width = 80
|
|
463
|
+
cell_height = 60
|
|
464
|
+
|
|
465
|
+
# count how many column/row that we need
|
|
466
|
+
total_colors = len(available_colors)
|
|
467
|
+
num_rows = (total_colors + MAX_COLS - 1) // MAX_COLS # Ceiling division
|
|
468
|
+
|
|
469
|
+
# make image
|
|
470
|
+
img_width = cell_width * MAX_COLS
|
|
471
|
+
img_height = cell_height * num_rows
|
|
472
|
+
img = Image.new('RGB', (img_width, img_height), 'white')
|
|
473
|
+
draw = ImageDraw.Draw(img)
|
|
474
|
+
|
|
475
|
+
# Draw every color
|
|
476
|
+
for idx, color_key in enumerate(available_colors):
|
|
477
|
+
color_value = colors_dict.get(color_key)
|
|
478
|
+
if color_value is None:
|
|
479
|
+
continue
|
|
480
|
+
|
|
481
|
+
# count grid position
|
|
482
|
+
col_idx = idx % MAX_COLS
|
|
483
|
+
row_idx = idx // MAX_COLS
|
|
484
|
+
|
|
485
|
+
# Convert ARGB to hex
|
|
486
|
+
if isinstance(color_value, int):
|
|
487
|
+
hex_color = hex_from_argb(color_value)
|
|
488
|
+
else:
|
|
489
|
+
hex_color = color_value
|
|
490
|
+
|
|
491
|
+
# Remove # if exists
|
|
492
|
+
color_clean = hex_color.replace('#', '')
|
|
493
|
+
# Convert hex to RGB
|
|
494
|
+
rgb = tuple(int(color_clean[j:j+2], 16) for j in (0, 2, 4))
|
|
495
|
+
|
|
496
|
+
# Position
|
|
497
|
+
x = col_idx * cell_width
|
|
498
|
+
y = row_idx * cell_height
|
|
499
|
+
|
|
500
|
+
# Draw rectangle
|
|
501
|
+
draw.rectangle([x, y, x + cell_width, y + cell_height], fill=rgb)
|
|
502
|
+
|
|
503
|
+
# Save
|
|
504
|
+
if output_path is None:
|
|
505
|
+
output_dir = Path.home() / ".config" / "m3-colors" / "sample"
|
|
506
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
507
|
+
wallpaper_name = Path(self.wallpaper_path).stem
|
|
508
|
+
output_path = output_dir / f"{wallpaper_name}_{variant}_palette.png"
|
|
509
|
+
|
|
510
|
+
img.save(output_path)
|
|
511
|
+
print(f"Palette preview saved: {output_path}")
|
|
512
|
+
|
|
513
|
+
return str(output_path)
|
|
514
|
+
|
|
515
|
+
def preview_colors(self):
|
|
516
|
+
"""Print color preview"""
|
|
517
|
+
if not self.theme:
|
|
518
|
+
raise ValueError("Generate scheme first!")
|
|
519
|
+
|
|
520
|
+
colors = self._extract_colors()
|
|
521
|
+
|
|
522
|
+
print(f"\nColor Preview ({self.mode} mode):")
|
|
523
|
+
print(
|
|
524
|
+
f" Source: {hex_from_argb(self.source_color) if isinstance(self.source_color, int) else self.source_color}"
|
|
525
|
+
)
|
|
526
|
+
print(f"\n Primary: {colors['m3primary']}")
|
|
527
|
+
print(f" Secondary: {colors['m3secondary']}")
|
|
528
|
+
print(f" Tertiary: {colors['m3tertiary']}")
|
|
529
|
+
print(f" Surface: {colors['m3surface']}")
|
|
530
|
+
print(f" Error: {colors['m3error']}")
|
|
531
|
+
|
|
532
|
+
print(f"\n Terminal Colors:")
|
|
533
|
+
for i in range(16):
|
|
534
|
+
print(f" term{i}: {colors[f'term{i}']}")
|
|
535
|
+
|
|
536
|
+
def main():
|
|
537
|
+
if len(sys.argv) < 2:
|
|
538
|
+
print("Usage: python m3wal.py <wallpaper_path> [light|dark|auto] [variant]")
|
|
539
|
+
print("\nVariants: TONALSPOT, VIBRANT, EXPRESSIVE, NEUTRAL, FIDELITY, CONTENT, MONOCHROME")
|
|
540
|
+
print("Default: uses m3-colors.conf settings")
|
|
541
|
+
sys.exit(1)
|
|
542
|
+
|
|
543
|
+
wallpaper = sys.argv[1]
|
|
544
|
+
|
|
545
|
+
# Initialize and load config
|
|
546
|
+
m3wal = M3WAL(wallpaper)
|
|
547
|
+
|
|
548
|
+
# Override config with CLI args if provided
|
|
549
|
+
mode = sys.argv[2] if len(sys.argv) > 2 else m3wal.config.get('General', 'mode', fallback='auto')
|
|
550
|
+
variant = sys.argv[3] if len(sys.argv) > 3 else m3wal.config.get('General', 'variant', fallback='CONTENT')
|
|
551
|
+
|
|
552
|
+
# Analyze wallpaper
|
|
553
|
+
print("Analyzing wallpaper...")
|
|
554
|
+
analysis = m3wal.analyze_wallpaper()
|
|
555
|
+
print(f" Brightness: {analysis['brightness']:.1f} (threshold: {m3wal.brightness_threshold})")
|
|
556
|
+
print(f" Auto-detected mode: {analysis['mode']}")
|
|
557
|
+
|
|
558
|
+
# Generate scheme
|
|
559
|
+
if mode == "auto":
|
|
560
|
+
mode = analysis["mode"]
|
|
561
|
+
|
|
562
|
+
print(f"\nGenerating {mode} scheme with {variant} variant...")
|
|
563
|
+
colors = m3wal.generate_scheme(mode, variant)
|
|
564
|
+
print(f"Generated {len(colors)} colors")
|
|
565
|
+
|
|
566
|
+
# Export to JSON
|
|
567
|
+
output = m3wal.export_json(variant=variant)
|
|
568
|
+
|
|
569
|
+
# Apply to all templates
|
|
570
|
+
cache_dir = Path(m3wal.config.get('Paths', 'cache_dir', fallback='~/.cache/m3-colors')).expanduser()
|
|
571
|
+
generated_files = m3wal.apply_all_templates(output_dir=cache_dir)
|
|
572
|
+
|
|
573
|
+
if generated_files:
|
|
574
|
+
print(f"\nGenerated {len(generated_files)} config files")
|
|
575
|
+
|
|
576
|
+
# Deploy configs
|
|
577
|
+
print("\nDeploying configs...")
|
|
578
|
+
m3wal.deploy_configs()
|
|
579
|
+
|
|
580
|
+
# Apply Xresources
|
|
581
|
+
if m3wal.config.getboolean('Features', 'apply_xresources', fallback=True):
|
|
582
|
+
print("\nApplying Xresources...")
|
|
583
|
+
m3wal.apply_xresources()
|
|
584
|
+
|
|
585
|
+
# Set wallpaper
|
|
586
|
+
if m3wal.config.getboolean('Features', 'set_wallpaper', fallback=True):
|
|
587
|
+
print("\nSetting wallpaper...")
|
|
588
|
+
m3wal.set_wallpaper()
|
|
589
|
+
|
|
590
|
+
# Create wallpaper symlink
|
|
591
|
+
if m3wal.config.getboolean('Features', 'create_symlink', fallback=True):
|
|
592
|
+
print("\nCreating wallpaper symlink...")
|
|
593
|
+
m3wal.create_wallpaper_symlink()
|
|
594
|
+
|
|
595
|
+
# Run post script
|
|
596
|
+
if m3wal.config.getboolean('Features', 'run_post_script', fallback=True):
|
|
597
|
+
script_path = m3wal.config.get('PostScript', 'script_path', fallback='m3wal-post.sh')
|
|
598
|
+
# if relative path, merge with config_dir
|
|
599
|
+
if not Path(script_path).is_absolute():
|
|
600
|
+
config_dir = Path(m3wal.config.get('Paths', 'config_dir', fallback='~/.config/m3-colors')).expanduser()
|
|
601
|
+
script_path = config_dir / script_path
|
|
602
|
+
|
|
603
|
+
print("\nRunning post script...")
|
|
604
|
+
m3wal.run_post_script(script_path)
|
|
605
|
+
|
|
606
|
+
# Show preview
|
|
607
|
+
m3wal.preview_colors()
|
|
608
|
+
|
|
609
|
+
# Generate palette preview
|
|
610
|
+
if m3wal.config.getboolean('Features', 'generate_palette_preview', fallback=True):
|
|
611
|
+
print("\nGenerating palette preview...")
|
|
612
|
+
m3wal.generate_palette_preview()
|
|
613
|
+
|
|
614
|
+
if __name__ == "__main__":
|
|
615
|
+
main()
|