8 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
8 changed files with 78 additions and 75 deletions
Generated
+10 -10
View File
@@ -9,11 +9,11 @@
"quickshell": "quickshell"
},
"locked": {
"lastModified": 1772330657,
"narHash": "sha256-cWblprYsDUeAWA57xAqxIjNxXvDI/rqYn6TFp2OPi/k=",
"lastModified": 1772962569,
"narHash": "sha256-ctRw4pVgx0IYKfA2hy90Ku37pnVX2T4q57UWp+l69fs=",
"owner": "caelestia-dots",
"repo": "shell",
"rev": "278fd4a4ed1bfb42c3fe197ff38b587539c012aa",
"rev": "e183599ce9e2c8d30a14631d53eb9947220c0812",
"type": "github"
},
"original": {
@@ -24,11 +24,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1772198003,
"narHash": "sha256-I45esRSssFtJ8p/gLHUZ1OUaaTaVLluNkABkk6arQwE=",
"lastModified": 1772773019,
"narHash": "sha256-E1bxHxNKfDoQUuvriG71+f+s/NT0qWkImXsYZNFFfCs=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "dd9b079222d43e1943b6ebd802f04fd959dc8e61",
"rev": "aca4d95fce4914b3892661bcb80b8087293536c6",
"type": "github"
},
"original": {
@@ -46,11 +46,11 @@
]
},
"locked": {
"lastModified": 1771926182,
"narHash": "sha256-QbXuSLhiSxOq6ydBL3+KGe1aiYWBW+e3J6qjJZaRMq0=",
"lastModified": 1772925576,
"narHash": "sha256-mMoiXABDtkSJxCYDrkhJ/TrrJf5M46oUfIlJvv2gkZ0=",
"ref": "refs/heads/master",
"rev": "cddb4f061bab495f4473ca5f2c571b6c710efef7",
"revCount": 744,
"rev": "15a84097653593dd15fad59a56befc2b7bdc270d",
"revCount": 750,
"type": "git",
"url": "https://git.outfoxxed.me/outfoxxed/quickshell"
},
-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 re
import shutil
@@ -6,6 +5,7 @@ import subprocess
import time
from argparse import Namespace
from datetime import datetime
from pathlib import Path
from caelestia.utils.notify import close_notification, notify
from caelestia.utils.paths import recording_notif_path, recording_path, recordings_dir, user_config_path
+13 -10
View File
@@ -140,9 +140,12 @@ class Command:
monitor_x = monitor.get("x")
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
monitor_height = monitor_height / monitor_scale
monitor_width = monitor_width / monitor_scale
@@ -232,7 +235,7 @@ class Command:
window_id = event.split(">>>")[1].split(",")[0]
else:
window_id = event.split(">>")[1].split(",")[0]
# Remove any leading > characters
window_id = window_id.lstrip(">")
@@ -268,9 +271,9 @@ class Command:
data = event[13:] # Remove "openwindow>>>"
else:
data = event[12:] # Remove "openwindow>>"
window_id, workspace, window_class, title = data.split(",", 3)
# Remove any leading > characters
window_id = window_id.lstrip(">")
@@ -348,19 +351,19 @@ class Command:
# Find all windows that match the pattern
matching_windows = self._find_matching_windows(temp_rule)
if not matching_windows:
print(f"No windows found matching pattern '{temp_rule.name}' with match type '{temp_rule.match_type}'")
return
print(f"Found {len(matching_windows)} matching window(s)")
# Apply rule to all matching windows
success_count = 0
for window in matching_windows:
window_id = window["address"][2:] # Remove "0x" prefix
window_title = window.get("title", "")
print(f"Applying rule to window 0x{window_id}: '{window_title}'")
success = self._apply_window_actions(window_id, temp_rule.width, temp_rule.height, temp_rule.actions)
if success:
@@ -386,7 +389,7 @@ class Command:
return
window_id = address[2:] # Remove "0x" prefix
print(f"Applying rule to active window 0x{window_id}: '{window_title}'")
success = self._apply_window_actions(window_id, temp_rule.width, temp_rule.height, temp_rule.actions)
if success:
@@ -411,7 +414,7 @@ class Command:
window_title = window.get("title", "")
initial_title = window.get("initialTitle", "")
# Check if window matches the pattern
matches = False
if temp_rule.match_type == "initialTitle":
+2
View File
@@ -12,9 +12,11 @@ def log_exception(func):
Used by the `apply_()` functions so that an exception, when applying
a theme, does not prevent the other themes from being applied.
"""
def wrapper(*args, **kwargs):
try:
func(*args, **kwargs)
except Exception as e:
log_message(f'Error during execution of "{func.__name__}()": {str(e)}')
return wrapper
+28 -47
View File
@@ -1,11 +1,10 @@
import fcntl
import json
import re
import subprocess
from pathlib import Path
import tempfile
import shutil
import fcntl
import sys
import subprocess
import tempfile
from pathlib import Path
from caelestia.utils.colour import get_dynamic_colours
from caelestia.utils.logging import log_exception
@@ -60,7 +59,7 @@ def gen_replace_dynamic(colours: dict[str, str], template: Path, mode: str) -> s
colours_dyn = get_dynamic_colours(colours)
template_content = template.read_text()
template_filled = re.sub(dotField, fill_colour, template_content)
template_filled = re.sub(dotField, fill_colour, template_content)
template_filled = re.sub(modeField, mode, template_filled)
return template_filled
@@ -117,6 +116,7 @@ def write_file(path: Path, content: str) -> None:
f.flush()
shutil.move(f.name, path)
@log_exception
def apply_terms(sequences: str) -> None:
state = c_state_dir / "sequences.txt"
@@ -129,6 +129,7 @@ def apply_terms(sequences: str) -> None:
try:
# Use non-blocking write with timeout to prevent hangs
import os
fd = os.open(str(pt), os.O_WRONLY | os.O_NONBLOCK | os.O_NOCTTY)
try:
os.write(fd, sequences.encode())
@@ -155,6 +156,7 @@ def apply_discord(scss: str) -> None:
for client in "Equicord", "Vencord", "BetterDiscord", "equibop", "vesktop", "legcord":
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)
@@ -197,36 +199,32 @@ def apply_htop(colours: dict[str, str]) -> None:
def sync_papirus_colors(hex_color: str) -> None:
"""Sync Papirus folder icon colors using hue/saturation analysis"""
try:
result = subprocess.run(
["which", "papirus-folders"],
capture_output=True,
check=False
)
result = subprocess.run(["which", "papirus-folders"], capture_output=True, check=False)
if result.returncode != 0:
return
except Exception:
return
papirus_paths = [
Path("/usr/share/icons/Papirus"),
Path("/usr/share/icons/Papirus-Dark"),
Path.home() / ".local/share/icons/Papirus",
Path.home() / ".icons/Papirus",
]
if not any(p.exists() for p in papirus_paths):
return
r = int(hex_color[0:2], 16)
g = int(hex_color[2:4], 16)
b = int(hex_color[4:6], 16)
# Brightness and saturation
max_val = max(r, g, b)
min_val = min(r, g, b)
brightness = max_val
saturation = 0 if max_val == 0 else ((max_val - min_val) * 100) // max_val
# Low saturation = grayscale
if saturation < 20:
if brightness < 85:
@@ -241,13 +239,13 @@ def sync_papirus_colors(hex_color: str) -> None:
color = _determine_hue_color(r, g, b, brightness, use_pale)
else:
color = _determine_hue_color(r, g, b, brightness, False)
try:
subprocess.Popen(
["sudo", "-n", "papirus-folders", "-C", color, "-u"],
stderr=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
start_new_session=True
start_new_session=True,
)
except Exception:
pass
@@ -259,7 +257,7 @@ def _determine_hue_color(r: int, g: int, b: int, brightness: int, use_pale: bool
r_ratio = (r * 100) // b if b > 0 else 0
g_ratio = (g * 100) // b if b > 0 else 0
rg_diff = abs(r - g)
if r_ratio > 70 and g_ratio > 70:
# Both R and G high relative to B = light blue/periwinkle
if rg_diff < 15:
@@ -307,7 +305,7 @@ def _determine_hue_color(r: int, g: int, b: int, brightness: int, use_pale: bool
def apply_gtk(colours: dict[str, str], mode: str) -> None:
gtk_template = gen_replace(colours, templates_dir / "gtk.css", hash=True)
thunar_template = gen_replace(colours, templates_dir / "thunar.css", hash=True)
for gtk_version in ["gtk-3.0", "gtk-4.0"]:
gtk_config_dir = config_dir / gtk_version
write_file(gtk_config_dir / "gtk.css", gtk_template)
@@ -316,35 +314,18 @@ def apply_gtk(colours: dict[str, str], mode: str) -> None:
subprocess.run(["dconf", "write", "/org/gnome/desktop/interface/gtk-theme", "'adw-gtk3-dark'"])
subprocess.run(["dconf", "write", "/org/gnome/desktop/interface/color-scheme", f"'prefer-{mode}'"])
subprocess.run(["dconf", "write", "/org/gnome/desktop/interface/icon-theme", f"'Papirus-{mode.capitalize()}'"])
sync_papirus_colors(colours["primary"])
@log_exception
def apply_qt(colours: dict[str, str], mode: str) -> None:
template = gen_replace(colours, templates_dir / f"qt{mode}.colors", hash=True)
write_file(config_dir / "qt5ct/colors/caelestia.colors", template)
write_file(config_dir / "qt6ct/colors/caelestia.colors", template)
colours = gen_replace(colours, templates_dir / f"qt{mode}.colors", hash=True)
write_file(config_dir / "qtengine/caelestia.colors", colours)
qtct = (templates_dir / "qtct.conf").read_text()
qtct = qtct.replace("{{ $mode }}", mode.capitalize())
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)
config = (templates_dir / "qtengine.json").read_text()
config = config.replace("{{ $mode }}", mode.capitalize())
write_file(config_dir / "qtengine/config.json", config)
@log_exception
@@ -378,14 +359,14 @@ def apply_colours(colours: dict[str, str], mode: str) -> None:
# Use file-based lock to prevent concurrent theme changes
lock_file = c_state_dir / "theme.lock"
c_state_dir.mkdir(parents=True, exist_ok=True)
try:
with open(lock_file, 'w') as lock_fd:
with open(lock_file, "w") as lock_fd:
try:
fcntl.flock(lock_fd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError:
return
try:
cfg = json.loads(user_config_path.read_text())["theme"]
except (FileNotFoundError, json.JSONDecodeError, KeyError):
@@ -421,7 +402,7 @@ def apply_colours(colours: dict[str, str], mode: str) -> None:
if check("enableCava"):
apply_cava(colours)
apply_user_templates(colours, mode)
finally:
try:
lock_file.unlink()
+2 -1
View File
@@ -96,6 +96,7 @@ def get_smart_opts(wall: Path, cache: Path) -> str:
def get_colours_for_wall(wall: Path | str, no_smart: bool) -> None:
wall = Path(wall)
scheme = get_scheme()
cache = wallpapers_cache_dir / compute_hash(wall)
@@ -124,6 +125,7 @@ def get_colours_for_wall(wall: Path | str, no_smart: bool) -> None:
"colours": get_colours_for_image(get_thumb(wall, cache), scheme),
}
def convert_gif(wall: Path) -> Path:
cache = wallpapers_cache_dir / compute_hash(wall)
output_path = cache / "first_frame.png"
@@ -142,7 +144,6 @@ def convert_gif(wall: Path) -> Path:
return output_path
def set_wallpaper(wall: Path | str, no_smart: bool) -> None:
# Make path absolute
wall = Path(wall).resolve()