20 Commits

Author SHA1 Message Date
2 * r + 2 * t c930bd2604 feat: switch to qtengine 2026-03-09 21:36:44 +11:00
2 * r + 2 * t cc155cf432 fix: format 2026-03-09 21:26:37 +11:00
github-actions 6e59149fbf [CI] chore: update flake 2026-03-09 02:52:41 +00:00
github-actions 8d2b737f15 [CI] chore: update flake 2026-03-08 02:51:20 +00:00
github-actions 4bcd42f482 [CI] chore: update flake 2026-03-06 02:36:22 +00:00
xeisenberg 51cecd481c fix: wall not Path type (#89) 2026-03-04 19:30:11 +11:00
github-actions c9312f3928 [CI] chore: update flake 2026-03-04 02:36:02 +00:00
github-actions bfaf4fc373 [CI] chore: update flake 2026-03-03 02:43:42 +00:00
2 * r + 2 * t 6e711ec289 fix: dynamic scheme import <3.0.0 compat 2026-03-03 01:04:33 +11:00
github-actions 7899f8348f [CI] chore: update flake 2026-03-01 03:00:32 +00:00
github-actions b0d68f0a1c [CI] chore: update flake 2026-02-25 02:51:37 +00:00
github-actions b0325a1898 [CI] chore: update flake 2026-02-22 02:46:26 +00:00
github-actions a6defd2921 [CI] chore: update flake 2026-02-21 02:33:51 +00:00
2 * r + 2 * t 0b9e416175 fix: missing colon 2026-02-21 00:52:12 +11:00
Unrectified 8ce97ea3f5 feat: add GIF files support as wallpaper (#88)
* feat: add GIF files support as wallpaper (not animated tho)

* tweak: simplifying variable use

* fix: git is good but damn it's annoying

* fix

---------

Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>
2026-02-21 00:17:16 +11:00
github-actions 25c473c18e [CI] chore: update flake 2026-02-20 02:40:44 +00:00
github-actions c22916fe45 [CI] chore: update flake 2026-02-19 02:45:57 +00:00
Soramane 011989e3ca fix: dynamic scheme import 2026-02-18 11:18:15 +11:00
Nathachou d88cc7ff79 feat: scheme support for Pandora Minecraft launcher (#87)
* added pandora theme template json

* feat: add support for Pandora theme integration
2026-02-17 11:36:42 +11:00
github-actions a550eb79ed [CI] chore: update flake 2026-02-15 02:56:44 +00:00
10 changed files with 281 additions and 82 deletions
Generated
+10 -10
View File
@@ -9,11 +9,11 @@
"quickshell": "quickshell" "quickshell": "quickshell"
}, },
"locked": { "locked": {
"lastModified": 1770949235, "lastModified": 1772962569,
"narHash": "sha256-OFeud9FjaOk6xHp/9igYl/+Zw6FJDyZNrIDNi47gsG0=", "narHash": "sha256-ctRw4pVgx0IYKfA2hy90Ku37pnVX2T4q57UWp+l69fs=",
"owner": "caelestia-dots", "owner": "caelestia-dots",
"repo": "shell", "repo": "shell",
"rev": "93e8880842b03e251bf59d1ba316f2393c68574f", "rev": "e183599ce9e2c8d30a14631d53eb9947220c0812",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -24,11 +24,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1770841267, "lastModified": 1772773019,
"narHash": "sha256-9xejG0KoqsoKEGp2kVbXRlEYtFFcDTHjidiuX8hGO44=", "narHash": "sha256-E1bxHxNKfDoQUuvriG71+f+s/NT0qWkImXsYZNFFfCs=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "ec7c70d12ce2fc37cb92aff673dcdca89d187bae", "rev": "aca4d95fce4914b3892661bcb80b8087293536c6",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -46,11 +46,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1769593411, "lastModified": 1772925576,
"narHash": "sha256-WW00FaBiUmQyxvSbefvgxIjwf/WmRrEGBbwMHvW/7uQ=", "narHash": "sha256-mMoiXABDtkSJxCYDrkhJ/TrrJf5M46oUfIlJvv2gkZ0=",
"ref": "refs/heads/master", "ref": "refs/heads/master",
"rev": "1e4d804e7f3fa7465811030e8da2bf10d544426a", "rev": "15a84097653593dd15fad59a56befc2b7bdc270d",
"revCount": 732, "revCount": 750,
"type": "git", "type": "git",
"url": "https://git.outfoxxed.me/outfoxxed/quickshell" "url": "https://git.outfoxxed.me/outfoxxed/quickshell"
}, },
+162
View File
@@ -0,0 +1,162 @@
{
"$schema": "https://github.com/longbridge/gpui-component/raw/refs/heads/main/.theme-schema.json",
"name": "Caelestia",
"author": "Unrectified",
"url": "https://github.com/caelestia-dots/cli",
"themes": [
{
"name": "Caelestia",
"mode": "{{ $mode }}",
"colors": {
"accent.background": "{{ $surfaceContainerHigh }}",
"accent.foreground": "{{ $onSurface }}",
"background": "{{ $background }}",
"border": "{{ $outlineVariant }}",
"danger.background": "{{ $error }}",
"foreground": "{{ $onBackground }}",
"input.border": "{{ $outline }}",
"link.active.foreground": "{{ $primary }}",
"link.foreground": "{{ $primary }}",
"link.hover.foreground": "{{ $primaryFixed }}",
"list.active.background": "{{ $secondaryContainer }}",
"list.active.border": "{{ $secondary }}",
"list.even.background": "{{ $surfaceContainerLowest }}",
"muted.background": "{{ $surfaceVariant }}",
"muted.foreground": "{{ $onSurfaceVariant }}",
"panel.background": "{{ $surfaceContainer }}",
"popover.background": "{{ $surfaceContainerHigh }}",
"popover.foreground": "{{ $onSurface }}",
"primary.active.background": "{{ $primaryFixedDim }}",
"primary.background": "{{ $primary }}",
"primary.foreground": "{{ $onPrimary }}",
"primary.hover.background": "{{ $primaryFixed }}",
"scrollbar.background": "{{ $surface }}",
"scrollbar.thumb.background": "{{ $outline }}",
"secondary.background": "{{ $secondaryContainer }}",
"secondary.active.background": "{{ $secondaryFixedDim }}",
"secondary.foreground": "{{ $onSecondary }}",
"secondary.hover.background": "{{ $secondaryFixed }}",
"tab.active.background": "{{ $surface }}",
"tab.active.foreground": "{{ $onSurface }}",
"tab.background": "{{ $surfaceContainerLowest }}",
"tab.foreground": "{{ $onSurfaceVariant }}",
"tab_bar.background": "{{ $surface }}",
"table.background": "{{ $surfaceContainer }}",
"table.head.foreground": "{{ $onSurfaceVariant }}",
"table.row.border": "{{ $outlineVariant }}",
"title_bar.background": "{{ $surfaceDim }}",
"ring": "{{ $primary }}",
"base.red": "{{ $red }}",
"base.red.light": "{{ $peach }}",
"base.green": "{{ $green }}",
"base.green.light": "{{ $teal }}",
"base.blue": "{{ $blue }}",
"base.blue.light": "{{ $sky }}",
"base.cyan": "{{ $teal }}",
"base.cyan.light": "{{ $sky }}",
"base.magenta": "{{ $mauve }}",
"base.magenta.light": "{{ $pink }}",
"base.yellow": "{{ $yellow }}",
"base.yellow.light": "{{ $peach }}"
},
"highlight": {
"editor.foreground": "{{ $onSurface }}",
"editor.background": "{{ $surface }}",
"editor.active_line.background": "{{ $surfaceContainerLow }}",
"editor.line_number": "{{ $onSurfaceVariant }}",
"editor.active_line_number": "{{ $onSurface }}",
"editor.invisible": "{{ $outlineVariant }}",
"conflict": "{{ $red }}",
"created": "{{ $green }}",
"deleted": "{{ $red }}",
"error": "{{ $error }}",
"hidden": "{{ $outline }}",
"hint": "{{ $success }}",
"ignored": "{{ $outline }}",
"info": "{{ $blue }}",
"modified": "{{ $yellow }}",
"predictive": "{{ $overlay1 }}",
"renamed": "{{ $green }}",
"success": "{{ $success }}",
"unreachable": "{{ $outlineVariant }}",
"warning": "{{ $yellow }}",
"syntax": {
"attribute": {
"color": "{{ $yellow }}"
},
"boolean": {
"color": "{{ $green }}"
},
"comment": {
"color": "{{ $subtext0 }}",
"font_style": "italic"
},
"comment.doc": {
"color": "{{ $subtext0 }}",
"font_style": "italic"
},
"constant": {
"color": "{{ $red }}"
},
"constructor": {
"color": "{{ $yellow }}"
},
"embedded": {
"color": "{{ $onSurface }}"
},
"function": {
"color": "{{ $green }}"
},
"keyword": {
"color": "{{ $mauve }}"
},
"link_text": {
"color": "{{ $sky }}",
"font_style": "normal"
},
"link_uri": {
"color": "{{ $klink }}",
"font_style": "italic"
},
"number": {
"color": "{{ $red }}"
},
"string": {
"color": "{{ $green }}"
},
"string.escape": {
"color": "{{ $green }}"
},
"string.regex": {
"color": "{{ $green }}"
},
"string.special": {
"color": "{{ $yellow }}"
},
"string.special.symbol": {
"color": "{{ $yellow }}"
},
"tag": {
"color": "{{ $yellow }}"
},
"text.literal": {
"color": "{{ $red }}"
},
"title": {
"color": "{{ $sky }}",
"font_weight": 600
},
"type": {
"color": "{{ $yellow }}"
},
"property": {
"color": "{{ $onSurface }}"
},
"variable.special": {
"color": "{{ $red }}"
}
}
}
}
]
}
-6
View File
@@ -1,6 +0,0 @@
[Appearance]
color_scheme_path={{ $config }}/colors/caelestia.colors
custom_palette=true
icon_theme=Papirus-{{ $mode }}
standard_dialogs=default
style=Darkly
@@ -0,0 +1,22 @@
{
"theme": {
"colorScheme": "~/.config/qtengine/caelestia.colors",
"iconTheme": "Papirus-{{ $mode }}",
"style": "Darkly",
"font": {
"family": "Sans Serif",
"size": 12,
"weight": -1
},
"fontFixed": {
"family": "Monospace",
"size": 12,
"weight": -1
}
},
"misc": {
"menusHaveIcons": true,
"singleClickActivate": false,
"shortcutsForContextMenus": true
}
}
+1 -1
View File
@@ -1,4 +1,3 @@
from pathlib import Path
import json import json
import re import re
import shutil import shutil
@@ -6,6 +5,7 @@ import subprocess
import time import time
from argparse import Namespace from argparse import Namespace
from datetime import datetime from datetime import datetime
from pathlib import Path
from caelestia.utils.notify import close_notification, notify from caelestia.utils.notify import close_notification, notify
from caelestia.utils.paths import recording_notif_path, recording_path, recordings_dir, user_config_path from caelestia.utils.paths import recording_notif_path, recording_path, recordings_dir, user_config_path
+4 -1
View File
@@ -140,7 +140,10 @@ class Command:
monitor_x = monitor.get("x") monitor_x = monitor.get("x")
monitor_y = monitor.get("y") monitor_y = monitor.get("y")
if not all(isinstance(x, (int, float)) for x in [monitor_height, monitor_width, monitor_scale, monitor_x, monitor_y]): if not all(
isinstance(x, (int, float))
for x in [monitor_height, monitor_width, monitor_scale, monitor_x, monitor_y]
):
return return
monitor_height = monitor_height / monitor_scale monitor_height = monitor_height / monitor_scale
+2
View File
@@ -12,9 +12,11 @@ def log_exception(func):
Used by the `apply_()` functions so that an exception, when applying Used by the `apply_()` functions so that an exception, when applying
a theme, does not prevent the other themes from being applied. a theme, does not prevent the other themes from being applied.
""" """
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
try: try:
func(*args, **kwargs) func(*args, **kwargs)
except Exception as e: except Exception as e:
log_message(f'Error during execution of "{func.__name__}()": {str(e)}') log_message(f'Error during execution of "{func.__name__}()": {str(e)}')
return wrapper return wrapper
+6 -4
View File
@@ -1,8 +1,5 @@
from materialyoucolor.blend import Blend from materialyoucolor.blend import Blend
from materialyoucolor.dynamiccolor.material_dynamic_colors import ( from materialyoucolor.dynamiccolor.material_dynamic_colors import MaterialDynamicColors
DynamicScheme,
MaterialDynamicColors,
)
from materialyoucolor.hct import Hct from materialyoucolor.hct import Hct
from materialyoucolor.scheme.scheme_content import SchemeContent from materialyoucolor.scheme.scheme_content import SchemeContent
from materialyoucolor.scheme.scheme_expressive import SchemeExpressive from materialyoucolor.scheme.scheme_expressive import SchemeExpressive
@@ -15,6 +12,11 @@ from materialyoucolor.scheme.scheme_tonal_spot import SchemeTonalSpot
from materialyoucolor.scheme.scheme_vibrant import SchemeVibrant from materialyoucolor.scheme.scheme_vibrant import SchemeVibrant
from materialyoucolor.utils.math_utils import difference_degrees, rotation_direction, sanitize_degrees_double from materialyoucolor.utils.math_utils import difference_degrees, rotation_direction, sanitize_degrees_double
try:
from materialyoucolor.dynamiccolor.dynamic_scheme import DynamicScheme
except ImportError:
from materialyoucolor.scheme.dynamic_scheme import DynamicScheme
def hex_to_hct(hex: str) -> Hct: def hex_to_hct(hex: str) -> Hct:
return Hct.from_int(int(f"0xFF{hex}", 16)) return Hct.from_int(int(f"0xFF{hex}", 16))
+23 -34
View File
@@ -1,11 +1,10 @@
import fcntl
import json import json
import re import re
import subprocess
from pathlib import Path
import tempfile
import shutil import shutil
import fcntl import subprocess
import sys import tempfile
from pathlib import Path
from caelestia.utils.colour import get_dynamic_colours from caelestia.utils.colour import get_dynamic_colours
from caelestia.utils.logging import log_exception from caelestia.utils.logging import log_exception
@@ -117,6 +116,7 @@ def write_file(path: Path, content: str) -> None:
f.flush() f.flush()
shutil.move(f.name, path) shutil.move(f.name, path)
@log_exception @log_exception
def apply_terms(sequences: str) -> None: def apply_terms(sequences: str) -> None:
state = c_state_dir / "sequences.txt" state = c_state_dir / "sequences.txt"
@@ -129,6 +129,7 @@ def apply_terms(sequences: str) -> None:
try: try:
# Use non-blocking write with timeout to prevent hangs # Use non-blocking write with timeout to prevent hangs
import os import os
fd = os.open(str(pt), os.O_WRONLY | os.O_NONBLOCK | os.O_NOCTTY) fd = os.open(str(pt), os.O_WRONLY | os.O_NONBLOCK | os.O_NOCTTY)
try: try:
os.write(fd, sequences.encode()) os.write(fd, sequences.encode())
@@ -156,6 +157,13 @@ def apply_discord(scss: str) -> None:
write_file(config_dir / client / "themes/caelestia.theme.css", conf) write_file(config_dir / client / "themes/caelestia.theme.css", conf)
@log_exception
def apply_pandora(colours: dict[str, str], mode: str) -> None:
template = gen_replace(colours, templates_dir / "pandora.json", hash=True)
template = template.replace("{{ $mode }}", mode)
write_file(data_dir / "PandoraLauncher/themes/caelestia.json", template)
@log_exception @log_exception
def apply_spicetify(colours: dict[str, str], mode: str) -> None: def apply_spicetify(colours: dict[str, str], mode: str) -> None:
template = gen_replace(colours, templates_dir / f"spicetify-{mode}.ini") template = gen_replace(colours, templates_dir / f"spicetify-{mode}.ini")
@@ -191,11 +199,7 @@ def apply_htop(colours: dict[str, str]) -> None:
def sync_papirus_colors(hex_color: str) -> None: def sync_papirus_colors(hex_color: str) -> None:
"""Sync Papirus folder icon colors using hue/saturation analysis""" """Sync Papirus folder icon colors using hue/saturation analysis"""
try: try:
result = subprocess.run( result = subprocess.run(["which", "papirus-folders"], capture_output=True, check=False)
["which", "papirus-folders"],
capture_output=True,
check=False
)
if result.returncode != 0: if result.returncode != 0:
return return
except Exception: except Exception:
@@ -241,7 +245,7 @@ def sync_papirus_colors(hex_color: str) -> None:
["sudo", "-n", "papirus-folders", "-C", color, "-u"], ["sudo", "-n", "papirus-folders", "-C", color, "-u"],
stderr=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
stdout=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
start_new_session=True start_new_session=True,
) )
except Exception: except Exception:
pass pass
@@ -316,29 +320,12 @@ def apply_gtk(colours: dict[str, str], mode: str) -> None:
@log_exception @log_exception
def apply_qt(colours: dict[str, str], mode: str) -> None: def apply_qt(colours: dict[str, str], mode: str) -> None:
template = gen_replace(colours, templates_dir / f"qt{mode}.colors", hash=True) colours = gen_replace(colours, templates_dir / f"qt{mode}.colors", hash=True)
write_file(config_dir / "qt5ct/colors/caelestia.colors", template) write_file(config_dir / "qtengine/caelestia.colors", colours)
write_file(config_dir / "qt6ct/colors/caelestia.colors", template)
qtct = (templates_dir / "qtct.conf").read_text() config = (templates_dir / "qtengine.json").read_text()
qtct = qtct.replace("{{ $mode }}", mode.capitalize()) config = config.replace("{{ $mode }}", mode.capitalize())
write_file(config_dir / "qtengine/config.json", config)
for ver in 5, 6:
conf = qtct.replace("{{ $config }}", str(config_dir / f"qt{ver}ct"))
if ver == 5:
conf += """
[Fonts]
fixed="Monospace,12,-1,5,50,0,0,0,0,0"
general="Sans Serif,12,-1,5,50,0,0,0,0,0"
"""
else:
conf += """
[Fonts]
fixed="Monospace,12,-1,5,400,0,0,0,0,0,0,0,0,0,0,1"
general="Sans Serif,12,-1,5,400,0,0,0,0,0,0,0,0,0,0,1"
"""
write_file(config_dir / f"qt{ver}ct/qt{ver}ct.conf", conf)
@log_exception @log_exception
@@ -374,7 +361,7 @@ def apply_colours(colours: dict[str, str], mode: str) -> None:
c_state_dir.mkdir(parents=True, exist_ok=True) c_state_dir.mkdir(parents=True, exist_ok=True)
try: try:
with open(lock_file, 'w') as lock_fd: with open(lock_file, "w") as lock_fd:
try: try:
fcntl.flock(lock_fd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) fcntl.flock(lock_fd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError: except BlockingIOError:
@@ -396,6 +383,8 @@ def apply_colours(colours: dict[str, str], mode: str) -> None:
apply_discord(gen_scss(colours)) apply_discord(gen_scss(colours))
if check("enableSpicetify"): if check("enableSpicetify"):
apply_spicetify(colours, mode) apply_spicetify(colours, mode)
if check("enablePandora"):
apply_pandora(colours, mode)
if check("enableFuzzel"): if check("enableFuzzel"):
apply_fuzzel(colours) apply_fuzzel(colours)
if check("enableBtop"): if check("enableBtop"):
+29 -4
View File
@@ -24,7 +24,7 @@ from caelestia.utils.theme import apply_colours
def is_valid_image(path: Path) -> bool: def is_valid_image(path: Path) -> bool:
return path.is_file() and path.suffix in [".jpg", ".jpeg", ".png", ".webp", ".tif", ".tiff"] return path.is_file() and path.suffix in [".jpg", ".jpeg", ".png", ".webp", ".tif", ".tiff", ".gif"]
def check_wall(wall: Path, filter_size: tuple[int, int], threshold: float) -> bool: def check_wall(wall: Path, filter_size: tuple[int, int], threshold: float) -> bool:
@@ -96,9 +96,13 @@ def get_smart_opts(wall: Path, cache: Path) -> str:
def get_colours_for_wall(wall: Path | str, no_smart: bool) -> None: def get_colours_for_wall(wall: Path | str, no_smart: bool) -> None:
wall = Path(wall)
scheme = get_scheme() scheme = get_scheme()
cache = wallpapers_cache_dir / compute_hash(wall) cache = wallpapers_cache_dir / compute_hash(wall)
if wall.suffix.lower() == ".gif":
wall = convert_gif(wall)
name = "dynamic" name = "dynamic"
if not no_smart: if not no_smart:
@@ -122,6 +126,24 @@ def get_colours_for_wall(wall: Path | str, no_smart: bool) -> None:
} }
def convert_gif(wall: Path) -> Path:
cache = wallpapers_cache_dir / compute_hash(wall)
output_path = cache / "first_frame.png"
if not output_path.exists():
output_path.parent.mkdir(parents=True, exist_ok=True)
with Image.open(wall) as img:
try:
img.seek(0)
except EOFError:
pass
img = img.convert("RGB")
img.save(output_path, "PNG")
return output_path
def set_wallpaper(wall: Path | str, no_smart: bool) -> None: def set_wallpaper(wall: Path | str, no_smart: bool) -> None:
# Make path absolute # Make path absolute
wall = Path(wall).resolve() wall = Path(wall).resolve()
@@ -129,6 +151,9 @@ def set_wallpaper(wall: Path | str, no_smart: bool) -> None:
if not is_valid_image(wall): if not is_valid_image(wall):
raise ValueError(f'"{wall}" is not a valid image') raise ValueError(f'"{wall}" is not a valid image')
# Use gif's 1st frame for thumb only
wall_cache = convert_gif(wall) if wall.suffix.lower() == ".gif" else wall
# Update files # Update files
wallpaper_path_path.parent.mkdir(parents=True, exist_ok=True) wallpaper_path_path.parent.mkdir(parents=True, exist_ok=True)
wallpaper_path_path.write_text(str(wall)) wallpaper_path_path.write_text(str(wall))
@@ -136,10 +161,10 @@ def set_wallpaper(wall: Path | str, no_smart: bool) -> None:
wallpaper_link_path.unlink(missing_ok=True) wallpaper_link_path.unlink(missing_ok=True)
wallpaper_link_path.symlink_to(wall) wallpaper_link_path.symlink_to(wall)
cache = wallpapers_cache_dir / compute_hash(wall) cache = wallpapers_cache_dir / compute_hash(wall_cache)
# Generate thumbnail or get from cache # Generate thumbnail or get from cache
thumb = get_thumb(wall, cache) thumb = get_thumb(wall_cache, cache)
wallpaper_thumbnail_path.parent.mkdir(parents=True, exist_ok=True) wallpaper_thumbnail_path.parent.mkdir(parents=True, exist_ok=True)
wallpaper_thumbnail_path.unlink(missing_ok=True) wallpaper_thumbnail_path.unlink(missing_ok=True)
wallpaper_thumbnail_path.symlink_to(thumb) wallpaper_thumbnail_path.symlink_to(thumb)
@@ -148,7 +173,7 @@ def set_wallpaper(wall: Path | str, no_smart: bool) -> None:
# Change mode and variant based on wallpaper colour # Change mode and variant based on wallpaper colour
if scheme.name == "dynamic" and not no_smart: if scheme.name == "dynamic" and not no_smart:
smart_opts = get_smart_opts(wall, cache) smart_opts = get_smart_opts(wall_cache, cache)
scheme.mode = smart_opts["mode"] scheme.mode = smart_opts["mode"]
scheme.variant = smart_opts["variant"] scheme.variant = smart_opts["variant"]