27 Commits

Author SHA1 Message Date
github-actions d1c8c8fc09 [CI] chore: update flake 2026-06-02 04:44:32 +00:00
2 * r + 2 * t ad533a0dd4 fix: only screenshot focused monitor in fs mode 2026-06-01 21:06:04 +10:00
İlyas ccd2712982 fix: Lua dispatcher compat (#112)
* fix: temporary Lua dispatcher compat for workspace dispatchers

* fix(resizer): add Lua dispatcher compat for window resize/move/float/center

* feat(theme): write current.lua for Hyprland Lua config, current.conf for hyprlang

* fix: align gen_lua format with #111

* refactor address review feedback
refactor(hypr,theme): address review feedback

- cache is_lua_config result to avoid redundant socket calls
- remove is_lua_config wrapper, rename _is_lua_config to is_lua_config
- move hypr import to top of theme.py
- use single line syntax in apply_hypr and apply_colours

* restore original specialws behavior and some formatting
2026-05-31 23:48:33 +10:00
github-actions 1ea661859d [CI] chore: update flake 2026-05-31 04:25:32 +00:00
github-actions 64a5507e74 [CI] chore: update flake 2026-05-26 04:08:39 +00:00
github-actions 7fa3fc1bd0 [CI] chore: update flake 2026-05-24 04:20:58 +00:00
github-actions 7f30062670 [CI] chore: update flake 2026-05-23 04:01:11 +00:00
github-actions 04d286eaff [CI] chore: update flake 2026-05-17 04:09:50 +00:00
github-actions 2ce6213698 [CI] chore: update flake 2026-05-13 03:57:27 +00:00
Zynix 4b3ffcd644 fix: defer DynamicScheme annotation evaluation (#110) 2026-05-11 16:48:11 +10:00
github-actions 2621724c55 [CI] chore: update flake 2026-05-10 04:03:00 +00:00
github-actions 7b8a4281aa [CI] chore: update flake 2026-05-07 03:45:02 +00:00
github-actions 7452974dc9 [CI] chore: update flake 2026-05-06 03:53:45 +00:00
github-actions 544b567668 [CI] chore: update flake 2026-05-05 03:33:35 +00:00
github-actions 1f523c7556 [CI] chore: update flake 2026-05-03 04:00:27 +00:00
2 * r + 2 * t a00e71d6b7 docs: add iconTheme options to example conf 2026-05-02 22:51:35 +10:00
2 * r + 2 * t 1ec969d9ec fix: use auto bars for cava 2026-05-02 22:48:41 +10:00
2 * r + 2 * t 5273ed514f feat: add theme postHook 2026-05-02 22:24:26 +10:00
github-actions f3b13affc3 [CI] chore: update flake 2026-05-02 03:39:49 +00:00
Haikal 5c9ce66c03 feat: expose more environment variables in post-hook (#107)
* feat: expose more environment variables in post-hook

* fix: formatted
2026-04-29 23:56:07 +10:00
github-actions c18f749f24 [CI] chore: update flake 2026-04-29 03:43:15 +00:00
2 * r + 2 * t 96fcdf5bce fix: use hypr socket instead of hyprctl 2026-04-28 21:20:40 +10:00
github-actions eddee4deca [CI] chore: update flake 2026-04-25 03:06:00 +00:00
Foxlike Creature 68bc03bc17 feat: allow overriding icon theme via cli.json (#106)
* theme: allow overriding Qt icon theme via cli.json

Papirus colors XDG special folders (Downloads, Pictures, Music, etc.)
differently from regular ones - they end up a different color while
everything else stays neutral. With themes like breeze-dark, all folder
icons share the same style, so everything looks consistent.

Add optional `iconTheme` field to the `theme` section of cli.json.
When set, it replaces the Papirus icon theme in the generated qtengine
config with the specified theme.

Example usage in cli.json:
  "theme": { "iconTheme": "breeze-dark" }

* theme: allow overriding Qt and GTK icon theme via cli.json

Some folders in Dolphin end up with Papirus-style icons while others
use the default theme icons, resulting in two different icon styles
mixed together in the same view. Dolphin's default folder icons take
their color directly from the active color scheme, so they always match
the theme exactly - Papirus has a fixed, limited palette and does not
always match.

Add optional iconThemeDark and iconThemeLight fields to the theme
section of cli.json. When set, they override the Papirus icon theme in
both the generated qtengine config and the GTK dconf setting. A generic
iconTheme field is also supported as a fallback for both modes.

Example usage in cli.json:
  "theme": { "iconThemeDark": "breeze-dark", "iconThemeLight": "breeze" }

---------

Co-authored-by: Foxlike Creature <safonovkirill113@gmail.com>
2026-04-24 14:55:47 +10:00
github-actions 023a30b83c [CI] chore: update flake 2026-04-21 03:27:51 +00:00
github-actions a192efae9c [CI] chore: update flake 2026-04-20 03:35:55 +00:00
2 * r + 2 * t 463f36544a docs: add missing theme.enable* opts to example conf 2026-04-19 16:01:09 +10:00
10 changed files with 135 additions and 30 deletions
+13 -2
View File
@@ -191,17 +191,28 @@ All configuration options are in `~/.config/caelestia/cli.json`.
"extraArgs": [] "extraArgs": []
}, },
"wallpaper": { "wallpaper": {
"postHook": "echo $WALLPAPER_PATH" "postHook": "echo $WALLPAPER_PATH $SCHEME_NAME $SCHEME_FLAVOUR $SCHEME_MODE $SCHEME_VARIANT $SCHEME_COLOURS"
}, },
"theme": { "theme": {
"enableTerm": true, "enableTerm": true,
"enableHypr": true, "enableHypr": true,
"enableDiscord": true, "enableDiscord": true,
"enableSpicetify": true, "enableSpicetify": true,
"enablePandora": true,
"enableFuzzel": true, "enableFuzzel": true,
"enableBtop": true, "enableBtop": true,
"enableNvtop": true,
"enableHtop": true,
"enableGtk": true, "enableGtk": true,
"enableQt": true "enableQt": true,
"enableWarp": true,
"enableChromium": true,
"enableZed": true,
"enableCava": true,
"iconTheme": "Papirus-Dark",
"iconThemeLight": "Papirus-Light",
"iconThemeDark": "Papirus-Dark",
"postHook": "echo $SCHEME_NAME $SCHEME_FLAVOUR $SCHEME_MODE $SCHEME_VARIANT $SCHEME_COLOURS"
}, },
"toggles": { "toggles": {
"communication": { "communication": {
Generated
+10 -10
View File
@@ -9,11 +9,11 @@
"quickshell": "quickshell" "quickshell": "quickshell"
}, },
"locked": { "locked": {
"lastModified": 1775801889, "lastModified": 1780196414,
"narHash": "sha256-q1LGwhQbNOurIAClh5YwKVU2kJ5lTCxRYZf48bAb9IM=", "narHash": "sha256-iXmyWULTZuRd68xRL79e9GyYL9FZ6gfh6zl1PPlWX2A=",
"owner": "caelestia-dots", "owner": "caelestia-dots",
"repo": "shell", "repo": "shell",
"rev": "0e07176ff149d02391531c802b51c28e73185f30", "rev": "63bb82762bb29ac9b7fcd5b97839abae721ce860",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -24,11 +24,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1775710090, "lastModified": 1780243769,
"narHash": "sha256-ar3rofg+awPB8QXDaFJhJ2jJhu+KqN/PRCXeyuXR76E=", "narHash": "sha256-x5UQuRsH3MqI0U9afaXSNqzTPSeZlRLvFAav2Ux1pNw=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "4c1018dae018162ec878d42fec712642d214fdfa", "rev": "331800de5053fcebacf6813adb5db9c9dca22a0c",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -46,11 +46,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1772925576, "lastModified": 1779430452,
"narHash": "sha256-mMoiXABDtkSJxCYDrkhJ/TrrJf5M46oUfIlJvv2gkZ0=", "narHash": "sha256-zTslhsxLqUlRTML506iougTGzyR38Fzhzn7t4KDEuuE=",
"ref": "refs/heads/master", "ref": "refs/heads/master",
"rev": "15a84097653593dd15fad59a56befc2b7bdc270d", "rev": "4b4fca3224ab977dc515ac0bb78d00b3dfa71e00",
"revCount": 750, "revCount": 819,
"type": "git", "type": "git",
"url": "https://git.outfoxxed.me/outfoxxed/quickshell" "url": "https://git.outfoxxed.me/outfoxxed/quickshell"
}, },
-2
View File
@@ -2,8 +2,6 @@
# Optimized for smooth and responsive visualization # Optimized for smooth and responsive visualization
[general] [general]
# Number of bars (20-200) - fewer bars = better performance
bars = 64
# Framerate (1-144) - higher = smoother but more CPU intensive # Framerate (1-144) - higher = smoother but more CPU intensive
framerate = 60 framerate = 60
+2 -1
View File
@@ -7,6 +7,7 @@ from argparse import Namespace
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from caelestia.utils import hypr
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
@@ -36,7 +37,7 @@ class Command:
def start(self) -> None: def start(self) -> None:
args = ["-w"] args = ["-w"]
monitors = json.loads(subprocess.check_output(["hyprctl", "monitors", "-j"])) monitors = hypr.message("monitors")
if self.args.region: if self.args.region:
if self.args.region == "slurp": if self.args.region == "slurp":
region = subprocess.check_output(["slurp", "-f", "%wx%h+%x+%y"], text=True) region = subprocess.check_output(["slurp", "-f", "%wx%h+%x+%y"], text=True)
+25 -5
View File
@@ -26,6 +26,26 @@ class Command:
self.timeout_tracker: dict[str, float] = {} self.timeout_tracker: dict[str, float] = {}
self.window_rules = self._load_window_rules() self.window_rules = self._load_window_rules()
def _make_resize_cmd(self, width: int | str, height: int | str, address: str) -> str:
if hypr.is_lua_config():
return f'dispatch hl.dsp.window.resize({{x = {width}, y = {height}, exact = true, window = "address:{address}"}})'
return f"dispatch resizewindowpixel exact {width} {height},address:{address}"
def _make_move_cmd(self, x: int, y: int, address: str) -> str:
if hypr.is_lua_config():
return f'dispatch hl.dsp.window.move({{x = {x}, y = {y}, window = "address:{address}"}})'
return f"dispatch movewindowpixel exact {x} {y},address:{address}"
def _make_float_cmd(self, address: str) -> str:
if hypr.is_lua_config():
return f'dispatch hl.dsp.window.float({{action = "toggle", window = "address:{address}"}})'
return f"dispatch togglefloating address:{address}"
def _make_center_cmd(self) -> str:
if hypr.is_lua_config():
return "dispatch hl.dsp.window.center()"
return "dispatch centerwindow"
def _load_window_rules(self) -> list[WindowRule]: def _load_window_rules(self) -> list[WindowRule]:
default_rules = [ default_rules = [
WindowRule("(Bitwarden", "titleContains", "20%", "54%", ["float", "center"]), WindowRule("(Bitwarden", "titleContains", "20%", "54%", ["float", "center"]),
@@ -164,8 +184,8 @@ class Command:
move_x = monitor_x + monitor_width - scaled_width - offset move_x = monitor_x + monitor_width - scaled_width - offset
move_y = monitor_y + monitor_height - scaled_height - offset move_y = monitor_y + monitor_height - scaled_height - offset
command1 = f"dispatch resizewindowpixel exact {scaled_width} {scaled_height},address:{address}" command1 = self._make_resize_cmd(scaled_width, scaled_height, address)
command2 = f"dispatch movewindowpixel exact {int(move_x)} {int(move_y)},address:{address}" command2 = self._make_move_cmd(int(move_x), int(move_y), address)
hypr.batch(command1, command2) hypr.batch(command1, command2)
log_message( log_message(
@@ -181,16 +201,16 @@ class Command:
if "float" in actions: if "float" in actions:
window_info = self._get_window_info(window_id) window_info = self._get_window_info(window_id)
if window_info and not window_info.get("floating", False): if window_info and not window_info.get("floating", False):
dispatch_commands.append(f"dispatch togglefloating address:0x{window_id}") dispatch_commands.append(self._make_float_cmd(f"0x{window_id}"))
if "pip" in actions: if "pip" in actions:
self._apply_pip_action(window_id) self._apply_pip_action(window_id)
return True return True
dispatch_commands.append(f"dispatch resizewindowpixel exact {width} {height},address:0x{window_id}") dispatch_commands.append(self._make_resize_cmd(width, height, f"0x{window_id}"))
if "center" in actions: if "center" in actions:
dispatch_commands.append("dispatch centerwindow") dispatch_commands.append(self._make_center_cmd())
try: try:
hypr.batch(*dispatch_commands) hypr.batch(*dispatch_commands)
+7 -1
View File
@@ -2,6 +2,7 @@ import subprocess
from argparse import Namespace from argparse import Namespace
from datetime import datetime from datetime import datetime
from caelestia.utils import hypr
from caelestia.utils.notify import notify from caelestia.utils.notify import notify
from caelestia.utils.paths import screenshots_cache_dir, screenshots_dir from caelestia.utils.paths import screenshots_cache_dir, screenshots_dir
@@ -33,7 +34,12 @@ class Command:
swappy.stdin.close() swappy.stdin.close()
def fullscreen(self) -> None: def fullscreen(self) -> None:
sc_data = subprocess.check_output(["grim", "-"]) cmd = ["grim"]
focused_monitor = next(monitor for monitor in hypr.message("monitors") if monitor["focused"])
if focused_monitor:
cmd += ["-o", focused_monitor["name"]]
cmd += ["-"]
sc_data = subprocess.check_output(cmd)
subprocess.run(["wl-copy"], input=sc_data) subprocess.run(["wl-copy"], input=sc_data)
+30
View File
@@ -7,6 +7,7 @@ socket_base = f"{os.getenv('XDG_RUNTIME_DIR')}/hypr/{os.getenv('HYPRLAND_INSTANC
socket_path = f"{socket_base}/.socket.sock" socket_path = f"{socket_base}/.socket.sock"
socket2_path = f"{socket_base}/.socket2.sock" socket2_path = f"{socket_base}/.socket2.sock"
_lua_config_cache: bool | None = None
def message(msg: str, is_json: bool = True) -> str | dict[str, Any]: def message(msg: str, is_json: bool = True) -> str | dict[str, Any]:
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
@@ -26,7 +27,36 @@ def message(msg: str, is_json: bool = True) -> str | dict[str, Any]:
return json.loads(resp) if is_json else resp return json.loads(resp) if is_json else resp
def is_lua_config() -> bool:
global _lua_config_cache
if _lua_config_cache is not None:
return _lua_config_cache
try:
result = message("systeminfo", is_json=False)
for line in result.splitlines():
if "configProvider:" in line:
_lua_config_cache = "lua" in line.lower()
return _lua_config_cache
_lua_config_cache = False
return False
except Exception:
_lua_config_cache = False
return False
DISPATCHER_MAP_LUA = {
"togglespecialworkspace": lambda *a: f'hl.dsp.workspace.toggle_special("{a[0]}")' if a else 'hl.dsp.workspace.toggle_special()',
"movetoworkspacesilent": lambda *a: (
f'hl.dsp.window.move({{window = "address:{a[0].split(",")[1].replace("address:", "")}", workspace = "{a[0].split(",")[0]}", follow = false}})'
),
"exec": lambda *a: 'hl.dsp.exec_cmd("' + ' '.join(a).replace('\\', '\\\\').replace('"', '\\"') + '")',
}
def dispatch(dispatcher: str, *args: str) -> bool: def dispatch(dispatcher: str, *args: str) -> bool:
if is_lua_config() and dispatcher in DISPATCHER_MAP_LUA:
lua_dispatch = DISPATCHER_MAP_LUA[dispatcher](*args)
return message(f"dispatch {lua_dispatch}", is_json=False) == "ok"
return message(f"dispatch {dispatcher} {' '.join(map(str, args))}".rstrip(), is_json=False) == "ok" return message(f"dispatch {dispatcher} {' '.join(map(str, args))}".rstrip(), is_json=False) == "ok"
+1 -1
View File
@@ -18,7 +18,7 @@ from typing import Protocol, Any
# subclasses in get_scheme() handle that internally. This Protocol tells the type # subclasses in get_scheme() handle that internally. This Protocol tells the type
# checker to expect our specific 3-argument setup instead of the base class signature. # checker to expect our specific 3-argument setup instead of the base class signature.
class SchemeConstructor(Protocol): class SchemeConstructor(Protocol):
def __call__(self, source_color_hct: Any, is_dark: bool, contrast_level: float) -> DynamicScheme: ... def __call__(self, source_color_hct: Any, is_dark: bool, contrast_level: float) -> "DynamicScheme": ...
try: try:
from materialyoucolor.dynamiccolor.dynamic_scheme import DynamicScheme from materialyoucolor.dynamiccolor.dynamic_scheme import DynamicScheme
+37 -7
View File
@@ -1,5 +1,6 @@
import fcntl import fcntl
import json import json
import os
import re import re
import shutil import shutil
import subprocess import subprocess
@@ -17,6 +18,8 @@ from caelestia.utils.paths import (
user_config_path, user_config_path,
user_templates_dir, user_templates_dir,
) )
from caelestia.utils.scheme import get_scheme
from caelestia.utils.hypr import is_lua_config
def gen_conf(colours: dict[str, str]) -> str: def gen_conf(colours: dict[str, str]) -> str:
@@ -25,6 +28,12 @@ def gen_conf(colours: dict[str, str]) -> str:
conf += f"${name} = {colour}\n" conf += f"${name} = {colour}\n"
return conf return conf
def gen_lua(colours: dict[str, str]) -> str:
lua = "return {\n"
for name, colour in colours.items():
lua += f' {name} = "{colour}",\n'
lua += "}"
return lua
def gen_scss(colours: dict[str, str]) -> str: def gen_scss(colours: dict[str, str]) -> str:
scss = "" scss = ""
@@ -142,7 +151,8 @@ def apply_terms(sequences: str) -> None:
@log_exception @log_exception
def apply_hypr(conf: str) -> None: def apply_hypr(conf: str) -> None:
write_file(config_dir / "hypr/scheme/current.conf", conf) ext = "lua" if is_lua_config() else "conf"
write_file(config_dir / f"hypr/scheme/current.{ext}", conf)
@log_exception @log_exception
@@ -302,7 +312,7 @@ def _determine_hue_color(r: int, g: int, b: int, brightness: int, use_pale: bool
@log_exception @log_exception
def apply_gtk(colours: dict[str, str], mode: str) -> None: def apply_gtk(colours: dict[str, str], mode: str, icon_theme: str | None = None) -> None:
gtk_template = gen_replace(colours, templates_dir / "gtk.css", hash=True) gtk_template = gen_replace(colours, templates_dir / "gtk.css", hash=True)
thunar_template = gen_replace(colours, templates_dir / "thunar.css", hash=True) thunar_template = gen_replace(colours, templates_dir / "thunar.css", hash=True)
@@ -313,18 +323,21 @@ 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/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/color-scheme", f"'prefer-{mode}'"])
subprocess.run(["dconf", "write", "/org/gnome/desktop/interface/icon-theme", f"'Papirus-{mode.capitalize()}'"]) gtk_icon_theme = icon_theme if icon_theme is not None else f"Papirus-{mode.capitalize()}"
subprocess.run(["dconf", "write", "/org/gnome/desktop/interface/icon-theme", f"'{gtk_icon_theme}'"])
sync_papirus_colors(colours["primary"]) sync_papirus_colors(colours["primary"])
@log_exception @log_exception
def apply_qt(colours: dict[str, str], mode: str) -> None: def apply_qt(colours: dict[str, str], mode: str, icon_theme: str | None = None) -> None:
colours = 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 / "qtengine/caelestia.colors", colours) write_file(config_dir / "qtengine/caelestia.colors", colours)
config = (templates_dir / "qtengine.json").read_text() config = (templates_dir / "qtengine.json").read_text()
config = config.replace("{{ $mode }}", mode.capitalize()) config = config.replace("{{ $mode }}", mode.capitalize())
if icon_theme is not None:
config = config.replace(f'"iconTheme": "Papirus-{mode.capitalize()}"', f'"iconTheme": "{icon_theme}"')
write_file(config_dir / "qtengine/config.json", config) write_file(config_dir / "qtengine/config.json", config)
@@ -423,7 +436,7 @@ def apply_colours(colours: dict[str, str], mode: str) -> None:
if check("enableTerm"): if check("enableTerm"):
apply_terms(gen_sequences(colours)) apply_terms(gen_sequences(colours))
if check("enableHypr"): if check("enableHypr"):
apply_hypr(gen_conf(colours)) apply_hypr(gen_lua(colours) if is_lua_config() else gen_conf(colours))
if check("enableDiscord"): if check("enableDiscord"):
apply_discord(gen_scss(colours)) apply_discord(gen_scss(colours))
if check("enableSpicetify"): if check("enableSpicetify"):
@@ -438,10 +451,11 @@ def apply_colours(colours: dict[str, str], mode: str) -> None:
apply_nvtop(colours) apply_nvtop(colours)
if check("enableHtop"): if check("enableHtop"):
apply_htop(colours) apply_htop(colours)
icon_theme = cfg.get(f"iconTheme{mode.capitalize()}") or cfg.get("iconTheme")
if check("enableGtk"): if check("enableGtk"):
apply_gtk(colours, mode) apply_gtk(colours, mode, icon_theme)
if check("enableQt"): if check("enableQt"):
apply_qt(colours, mode) apply_qt(colours, mode, icon_theme)
if check("enableWarp"): if check("enableWarp"):
apply_warp(colours, mode) apply_warp(colours, mode)
if check("enableChromium"): if check("enableChromium"):
@@ -452,6 +466,22 @@ def apply_colours(colours: dict[str, str], mode: str) -> None:
apply_cava(colours) apply_cava(colours)
apply_user_templates(colours, mode) apply_user_templates(colours, mode)
if post_hook := cfg.get("postHook"):
scheme = get_scheme()
subprocess.run(
post_hook,
shell=True,
env={
**os.environ,
"SCHEME_NAME": scheme.name,
"SCHEME_FLAVOUR": scheme.flavour,
"SCHEME_MODE": scheme.mode,
"SCHEME_VARIANT": scheme.variant,
"SCHEME_COLOURS": json.dumps(scheme.colours),
},
stderr=subprocess.DEVNULL,
)
finally: finally:
try: try:
lock_file.unlink() lock_file.unlink()
+10 -1
View File
@@ -192,7 +192,16 @@ def set_wallpaper(wall: Path, no_smart: bool) -> None:
subprocess.run( subprocess.run(
post_hook, post_hook,
shell=True, shell=True,
env={**os.environ, "WALLPAPER_PATH": str(wall)}, env={
**os.environ,
"WALLPAPER_PATH": str(wall),
"SCHEME_NAME": scheme.name,
"SCHEME_FLAVOUR": scheme.flavour,
"SCHEME_MODE": scheme.mode,
"SCHEME_VARIANT": scheme.variant,
"SCHEME_COLOURS": json.dumps(scheme.colours),
"THUMBNAIL_PATH": str(thumb),
},
stderr=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
) )
except (FileNotFoundError, json.JSONDecodeError): except (FileNotFoundError, json.JSONDecodeError):