From bc17bc796ca656597cc5c143318483813695134c Mon Sep 17 00:00:00 2001 From: Valentine Omonya Date: Sun, 2 Nov 2025 03:42:45 +0300 Subject: [PATCH 001/106] My local changes --- src/caelestia/parser.py | 211 +++++++++++++++++++----- src/caelestia/subcommands/record.py | 240 ++++++++++++++++++++++------ 2 files changed, 363 insertions(+), 88 deletions(-) diff --git a/src/caelestia/parser.py b/src/caelestia/parser.py index 840ead5c..4f5afca2 100644 --- a/src/caelestia/parser.py +++ b/src/caelestia/parser.py @@ -1,32 +1,61 @@ import argparse -from caelestia.subcommands import clipboard, emoji, record, resizer, scheme, screenshot, shell, toggle, wallpaper +from caelestia.subcommands import ( + clipboard, + emoji, + record, + resizer, + scheme, + screenshot, + shell, + toggle, + wallpaper, +) from caelestia.utils.paths import wallpapers_dir from caelestia.utils.scheme import get_scheme_names, scheme_variants from caelestia.utils.wallpaper import get_wallpaper def parse_args() -> (argparse.ArgumentParser, argparse.Namespace): - parser = argparse.ArgumentParser(prog="caelestia", description="Main control script for the Caelestia dotfiles") - parser.add_argument("-v", "--version", action="store_true", help="print the current version") + parser = argparse.ArgumentParser( + prog="caelestia", description="Main control script for the Caelestia dotfiles" + ) + parser.add_argument( + "-v", "--version", action="store_true", help="print the current version" + ) # Add subcommand parsers command_parser = parser.add_subparsers( - title="subcommands", description="valid subcommands", metavar="COMMAND", help="the subcommand to run" + title="subcommands", + description="valid subcommands", + metavar="COMMAND", + help="the subcommand to run", ) # Create parser for shell opts shell_parser = command_parser.add_parser("shell", help="start or message the shell") shell_parser.set_defaults(cls=shell.Command) - shell_parser.add_argument("message", nargs="*", help="a message to send to the shell") - shell_parser.add_argument("-d", "--daemon", action="store_true", help="start the shell detached") - shell_parser.add_argument("-s", "--show", action="store_true", help="print all shell IPC commands") - shell_parser.add_argument("-l", "--log", action="store_true", help="print the shell log") - shell_parser.add_argument("-k", "--kill", action="store_true", help="kill the shell") + shell_parser.add_argument( + "message", nargs="*", help="a message to send to the shell" + ) + shell_parser.add_argument( + "-d", "--daemon", action="store_true", help="start the shell detached" + ) + shell_parser.add_argument( + "-s", "--show", action="store_true", help="print all shell IPC commands" + ) + shell_parser.add_argument( + "-l", "--log", action="store_true", help="print the shell log" + ) + shell_parser.add_argument( + "-k", "--kill", action="store_true", help="kill the shell" + ) shell_parser.add_argument("--log-rules", metavar="RULES", help="log rules to apply") # Create parser for toggle opts - toggle_parser = command_parser.add_parser("toggle", help="toggle a special workspace") + toggle_parser = command_parser.add_parser( + "toggle", help="toggle a special workspace" + ) toggle_parser.set_defaults(cls=toggle.Command) toggle_parser.add_argument("workspace", help="the workspace to toggle") @@ -34,66 +63,162 @@ def parse_args() -> (argparse.ArgumentParser, argparse.Namespace): scheme_parser = command_parser.add_parser("scheme", help="manage the colour scheme") scheme_command_parser = scheme_parser.add_subparsers(title="subcommands") - list_parser = scheme_command_parser.add_parser("list", help="list available schemes") + list_parser = scheme_command_parser.add_parser( + "list", help="list available schemes" + ) list_parser.set_defaults(cls=scheme.List) - list_parser.add_argument("-n", "--names", action="store_true", help="list scheme names") - list_parser.add_argument("-f", "--flavours", action="store_true", help="list scheme flavours") - list_parser.add_argument("-m", "--modes", action="store_true", help="list scheme modes") - list_parser.add_argument("-v", "--variants", action="store_true", help="list scheme variants") + list_parser.add_argument( + "-n", "--names", action="store_true", help="list scheme names" + ) + list_parser.add_argument( + "-f", "--flavours", action="store_true", help="list scheme flavours" + ) + list_parser.add_argument( + "-m", "--modes", action="store_true", help="list scheme modes" + ) + list_parser.add_argument( + "-v", "--variants", action="store_true", help="list scheme variants" + ) get_parser = scheme_command_parser.add_parser("get", help="get scheme properties") get_parser.set_defaults(cls=scheme.Get) - get_parser.add_argument("-n", "--name", action="store_true", help="print the current scheme name") - get_parser.add_argument("-f", "--flavour", action="store_true", help="print the current scheme flavour") - get_parser.add_argument("-m", "--mode", action="store_true", help="print the current scheme mode") - get_parser.add_argument("-v", "--variant", action="store_true", help="print the current scheme variant") + get_parser.add_argument( + "-n", "--name", action="store_true", help="print the current scheme name" + ) + get_parser.add_argument( + "-f", "--flavour", action="store_true", help="print the current scheme flavour" + ) + get_parser.add_argument( + "-m", "--mode", action="store_true", help="print the current scheme mode" + ) + get_parser.add_argument( + "-v", "--variant", action="store_true", help="print the current scheme variant" + ) set_parser = scheme_command_parser.add_parser("set", help="set the current scheme") set_parser.set_defaults(cls=scheme.Set) - set_parser.add_argument("--notify", action="store_true", help="send a notification on error") - set_parser.add_argument("-r", "--random", action="store_true", help="switch to a random scheme") - set_parser.add_argument("-n", "--name", choices=get_scheme_names(), help="the name of the scheme to switch to") + set_parser.add_argument( + "--notify", action="store_true", help="send a notification on error" + ) + set_parser.add_argument( + "-r", "--random", action="store_true", help="switch to a random scheme" + ) + set_parser.add_argument( + "-n", + "--name", + choices=get_scheme_names(), + help="the name of the scheme to switch to", + ) set_parser.add_argument("-f", "--flavour", help="the flavour to switch to") - set_parser.add_argument("-m", "--mode", choices=["dark", "light"], help="the mode to switch to") - set_parser.add_argument("-v", "--variant", choices=scheme_variants, help="the variant to switch to") + set_parser.add_argument( + "-m", "--mode", choices=["dark", "light"], help="the mode to switch to" + ) + set_parser.add_argument( + "-v", "--variant", choices=scheme_variants, help="the variant to switch to" + ) # Create parser for screenshot opts - screenshot_parser = command_parser.add_parser("screenshot", help="take a screenshot") + screenshot_parser = command_parser.add_parser( + "screenshot", help="take a screenshot" + ) screenshot_parser.set_defaults(cls=screenshot.Command) - screenshot_parser.add_argument("-r", "--region", nargs="?", const="slurp", help="take a screenshot of a region") screenshot_parser.add_argument( - "-f", "--freeze", action="store_true", help="freeze the screen while selecting a region" + "-r", "--region", nargs="?", const="slurp", help="take a screenshot of a region" + ) + screenshot_parser.add_argument( + "-f", + "--freeze", + action="store_true", + help="freeze the screen while selecting a region", ) # Create parser for record opts record_parser = command_parser.add_parser("record", help="start a screen recording") record_parser.set_defaults(cls=record.Command) - record_parser.add_argument("-r", "--region", nargs="?", const="slurp", help="record a region") - record_parser.add_argument("-s", "--sound", action="store_true", help="record audio") - record_parser.add_argument("-p", "--pause", action="store_true", help="pause/resume the recording") + + # Recording mode options - separate video and audio modes + record_parser.add_argument( + "-m", + "--mode", + choices=["fullscreen", "region", "window"], + default="fullscreen", + help="video recording mode (default: fullscreen)", + ) + + record_parser.add_argument( + "-a", + "--audio", + choices=["none", "mic", "system", "combined"], + default="none", + help="Audio recording mode", + ) + + # Region option (only used with region mode) + record_parser.add_argument( + "-r", + "--region", + nargs="?", + const="slurp", + help="record a region (only for region mode)", + ) + + # Control options + record_parser.add_argument( + "-p", "--pause", action="store_true", help="pause/resume the recording" + ) + record_parser.add_argument( + "-s", "--stop", action="store_true", help="stop the current recording" + ) + record_parser.add_argument( + "--status", action="store_true", help="check recording status" + ) # Create parser for clipboard opts - clipboard_parser = command_parser.add_parser("clipboard", help="open clipboard history") + clipboard_parser = command_parser.add_parser( + "clipboard", help="open clipboard history" + ) clipboard_parser.set_defaults(cls=clipboard.Command) - clipboard_parser.add_argument("-d", "--delete", action="store_true", help="delete from clipboard history") + clipboard_parser.add_argument( + "-d", "--delete", action="store_true", help="delete from clipboard history" + ) # Create parser for emoji-picker opts emoji_parser = command_parser.add_parser("emoji", help="emoji/glyph utilities") emoji_parser.set_defaults(cls=emoji.Command) - emoji_parser.add_argument("-p", "--picker", action="store_true", help="open the emoji/glyph picker") - emoji_parser.add_argument("-f", "--fetch", action="store_true", help="fetch emoji/glyph data from remote") + emoji_parser.add_argument( + "-p", "--picker", action="store_true", help="open the emoji/glyph picker" + ) + emoji_parser.add_argument( + "-f", "--fetch", action="store_true", help="fetch emoji/glyph data from remote" + ) # Create parser for wallpaper opts - wallpaper_parser = command_parser.add_parser("wallpaper", help="manage the wallpaper") + wallpaper_parser = command_parser.add_parser( + "wallpaper", help="manage the wallpaper" + ) wallpaper_parser.set_defaults(cls=wallpaper.Command) wallpaper_parser.add_argument( - "-p", "--print", nargs="?", const=get_wallpaper(), metavar="PATH", help="print the scheme for a wallpaper" + "-p", + "--print", + nargs="?", + const=get_wallpaper(), + metavar="PATH", + help="print the scheme for a wallpaper", + ) + wallpaper_parser.add_argument( + "-r", + "--random", + nargs="?", + const=wallpapers_dir, + metavar="DIR", + help="switch to a random wallpaper", ) wallpaper_parser.add_argument( - "-r", "--random", nargs="?", const=wallpapers_dir, metavar="DIR", help="switch to a random wallpaper" + "-f", "--file", help="the path to the wallpaper to switch to" + ) + wallpaper_parser.add_argument( + "-n", "--no-filter", action="store_true", help="do not filter by size" ) - wallpaper_parser.add_argument("-f", "--file", help="the path to the wallpaper to switch to") - wallpaper_parser.add_argument("-n", "--no-filter", action="store_true", help="do not filter by size") wallpaper_parser.add_argument( "-t", "--threshold", @@ -110,7 +235,9 @@ def parse_args() -> (argparse.ArgumentParser, argparse.Namespace): # Create parser for resizer opts resizer_parser = command_parser.add_parser("resizer", help="window resizer daemon") resizer_parser.set_defaults(cls=resizer.Command) - resizer_parser.add_argument("-d", "--daemon", action="store_true", help="start the resizer daemon") + resizer_parser.add_argument( + "-d", "--daemon", action="store_true", help="start the resizer daemon" + ) resizer_parser.add_argument( "pattern", nargs="?", @@ -125,6 +252,8 @@ def parse_args() -> (argparse.ArgumentParser, argparse.Namespace): ) resizer_parser.add_argument("width", nargs="?", help="width to resize to") resizer_parser.add_argument("height", nargs="?", help="height to resize to") - resizer_parser.add_argument("actions", nargs="?", help="comma-separated actions to apply (float,center,pip)") + resizer_parser.add_argument( + "actions", nargs="?", help="comma-separated actions to apply (float,center,pip)" + ) return parser, parser.parse_args() diff --git a/src/caelestia/subcommands/record.py b/src/caelestia/subcommands/record.py index 867eb1b5..e771fe4a 100644 --- a/src/caelestia/subcommands/record.py +++ b/src/caelestia/subcommands/record.py @@ -7,10 +7,21 @@ from datetime import datetime 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, +) RECORDER = "gpu-screen-recorder" +AUDIO_MODES = { + "mic": "default_input", + "system": "default_output", + "combined": "CombinedSink.monitor", +} + class Command: args: Namespace @@ -19,50 +30,172 @@ def __init__(self, args: Namespace) -> None: self.args = args def run(self) -> None: - if self.args.pause: - subprocess.run(["pkill", "-USR2", "-f", RECORDER], stdout=subprocess.DEVNULL) + if hasattr(self.args, "status") and self.args.status: + self.status() + elif hasattr(self.args, "stop") and self.args.stop: + self.stop() + elif self.args.pause: + subprocess.run( + ["pkill", "-USR2", "-f", RECORDER], stdout=subprocess.DEVNULL + ) elif self.proc_running(): self.stop() else: self.start() + def status(self) -> None: + """Check and display recording status""" + if self.proc_running(): + print("Recording: RUNNING") + else: + print("Recording: STOPPED") + def proc_running(self) -> bool: - return subprocess.run(["pidof", RECORDER], stdout=subprocess.DEVNULL).returncode == 0 + return ( + subprocess.run(["pidof", RECORDER], stdout=subprocess.DEVNULL).returncode + == 0 + ) + + def intersects( + self, a: tuple[int, int, int, int], b: tuple[int, int, int, int] + ) -> bool: + return ( + a[0] < b[0] + b[2] + and a[0] + a[2] > b[0] + and a[1] < b[1] + b[3] + and a[1] + a[3] > b[1] + ) - def intersects(self, a: tuple[int, int, int, int], b: tuple[int, int, int, int]) -> bool: - return a[0] < b[0] + b[2] and a[0] + a[2] > b[0] and a[1] < b[1] + b[3] and a[1] + a[3] > b[1] + def get_audio_device(self, audio_mode: str) -> str: + """Get the appropriate audio device for the given mode with fallback handling.""" + if audio_mode == "none" or not audio_mode: + return "" + + device = AUDIO_MODES.get(audio_mode, "") + + # Check if the device is available + if audio_mode in ["mic", "system", "combined"]: + try: + result = subprocess.run( + ["pactl", "list", "sources", "short"], + capture_output=True, + text=True, + check=True, + ) + available_devices = [ + line.split("\t")[1] + for line in result.stdout.strip().split("\n") + if line + ] + if device and device not in available_devices: + print( + f"Warning: Audio device '{device}' not available, falling back to default" + ) + + if audio_mode == "mic": + input_devices = [ + d + for d in available_devices + if "input" in d.lower() or "mic" in d.lower() + ] + device = input_devices[0] if input_devices else "" + elif audio_mode == "system": + output_devices = [ + d + for d in available_devices + if "output" in d.lower() or "monitor" in d.lower() + ] + device = output_devices[0] if output_devices else "" + except (subprocess.CalledProcessError, FileNotFoundError): + print( + "Warning: Could not check audio devices, audio recording may fail" + ) + device = "" + + return device def start(self) -> None: args = ["-w"] + # Get video mode and audio mode from args + video_mode = getattr(self.args, "mode", "fullscreen") + audio_mode = getattr(self.args, "audio", "none") + monitors = json.loads(subprocess.check_output(["hyprctl", "monitors", "-j"])) - if self.args.region: - if self.args.region == "slurp": - region = subprocess.check_output(["slurp", "-f", "%wx%h+%x+%y"], text=True) + + # Handle video modes + if video_mode == "region" or self.args.region: + if self.args.region == "slurp" or not self.args.region: + region = subprocess.check_output( + ["slurp", "-f", "%wx%h+%x+%y"], text=True + ).strip() else: region = self.args.region.strip() args += ["region", "-region", region] - m = re.match(r"(\d+)x(\d+)\+(\d+)\+(\d+)", region) if not m: raise ValueError(f"Invalid region: {region}") - w, h, x, y = map(int, m.groups()) r = x, y, w, h max_rr = 0 for monitor in monitors: - if self.intersects((monitor["x"], monitor["y"], monitor["width"], monitor["height"]), r): + if self.intersects( + (monitor["x"], monitor["y"], monitor["width"], monitor["height"]), r + ): rr = round(monitor["refreshRate"]) max_rr = max(max_rr, rr) args += ["-f", str(max_rr)] + + elif video_mode == "window": + try: + window_info = subprocess.check_output( + ["slurp", "-w", "-f", "%wx%h+%x+%y"], text=True + ).strip() + args += ["region", "-region", window_info] + m = re.match(r"(\d+)x(\d+)\+(\d+)\+(\d+)", window_info) + if not m: + raise ValueError(f"Invalid window region: {window_info}") + w, h, x, y = map(int, m.groups()) + r = x, y, w, h + max_rr = 0 + for monitor in monitors: + if self.intersects( + ( + monitor["x"], + monitor["y"], + monitor["width"], + monitor["height"], + ), + r, + ): + rr = round(monitor["refreshRate"]) + max_rr = max(max_rr, rr) + args += ["-f", str(max_rr)] + except subprocess.CalledProcessError: + print("Window selection canceled") + return else: - focused_monitor = next(monitor for monitor in monitors if monitor["focused"]) + # fullscreen + focused_monitor = next( + (monitor for monitor in monitors if monitor["focused"]), None + ) if focused_monitor: - args += [focused_monitor["name"], "-f", str(round(focused_monitor["refreshRate"]))] + args += [ + focused_monitor["name"], + "-f", + str(round(focused_monitor["refreshRate"])), + ] + + # Handle audio modes + audio_device = self.get_audio_device(audio_mode) + if audio_device: + args += ["-a", audio_device, "-ac", "opus", "-ab", "192k"] + print(f"Recording with audio: {audio_device} ({audio_mode})") + else: + print("Recording without audio") - if self.args.sound: - args += ["-a", "default_output"] + # Load extra args from config try: config = json.loads(user_config_path.read_text()) if "record" in config and "extraArgs" in config["record"]: @@ -70,12 +203,18 @@ def start(self) -> None: except (json.JSONDecodeError, FileNotFoundError): pass except TypeError as e: - raise ValueError(f"Config option 'record.extraArgs' should be an array: {e}") + raise ValueError( + f"Config option 'record.extraArgs' should be an array: {e}" + ) recording_path.parent.mkdir(parents=True, exist_ok=True) - proc = subprocess.Popen([RECORDER, *args, "-o", str(recording_path)], start_new_session=True) + proc = subprocess.Popen( + [RECORDER, *args, "-o", str(recording_path)], start_new_session=True + ) - notif = notify("-p", "Recording started", "Recording...") + # Show notification with mode info + mode_text = f"{video_mode} with {audio_mode if audio_device else 'no'} audio" + notif = notify("-p", "Recording started", f"Recording {mode_text}...") recording_notif_path.write_text(notif) try: @@ -84,7 +223,7 @@ def start(self) -> None: notify( "Recording failed", "An error occurred attempting to start recorder. " - f"Command `{' '.join(proc.args)}` failed with exit code {proc.returncode}", + f"Command {' '.join(proc.args)} failed with exit code {proc.returncode}", ) except subprocess.TimeoutExpired: pass @@ -94,11 +233,26 @@ def stop(self) -> None: subprocess.run(["pkill", "-f", RECORDER], stdout=subprocess.DEVNULL) # Wait for recording to finish to avoid corrupted video file - while self.proc_running(): + max_wait = 50 # Max 5 seconds + wait_count = 0 + while self.proc_running() and wait_count < max_wait: time.sleep(0.1) + wait_count += 1 + + # Check if file exists before trying to move it + if not recording_path.exists(): + print("Warning: No recording file found") + try: + close_notification(recording_notif_path.read_text()) + except IOError: + pass + return # Move to recordings folder - new_path = recordings_dir / f"recording_{datetime.now().strftime('%Y%m%d_%H-%M-%S')}.mp4" + new_path = ( + recordings_dir + / f"recording_{datetime.now().strftime('%Y%m%d_%H-%M-%S')}.mp4" + ) recordings_dir.mkdir(exist_ok=True, parents=True) shutil.move(recording_path, new_path) @@ -108,30 +262,22 @@ def stop(self) -> None: except IOError: pass - action = notify( - "--action=watch=Watch", - "--action=open=Open", - "--action=delete=Delete", - "Recording stopped", - f"Recording saved in {new_path}", - ) - - if action == "watch": - subprocess.Popen(["app2unit", "-O", new_path], start_new_session=True) - elif action == "open": - p = subprocess.run( + # Show completion notification in background (non-blocking) + try: + subprocess.Popen( [ - "dbus-send", - "--session", - "--dest=org.freedesktop.FileManager1", - "--type=method_call", - "/org/freedesktop/FileManager1", - "org.freedesktop.FileManager1.ShowItems", - f"array:string:file://{new_path}", - "string:", - ] + "notify-send", + "-a", + "caelestia-cli", + "--action=watch=Watch", + "--action=open=Open", + "--action=delete=Delete", + "Recording stopped", + f"Recording saved in {new_path}", + ], + start_new_session=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, ) - if p.returncode != 0: - subprocess.Popen(["app2unit", "-O", new_path.parent], start_new_session=True) - elif action == "delete": - new_path.unlink() + except Exception as e: + print(f"Could not show notification: {e}") From 46c589b8f12f040d5de41be779f2620facb53bb9 Mon Sep 17 00:00:00 2001 From: Valentine Omonya Date: Sun, 14 Dec 2025 03:07:33 +0300 Subject: [PATCH 002/106] Added my screen recorder config --- src/caelestia/subcommands/record.py | 43 ++++++++++++++++------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/src/caelestia/subcommands/record.py b/src/caelestia/subcommands/record.py index e771fe4a..442e7621 100644 --- a/src/caelestia/subcommands/record.py +++ b/src/caelestia/subcommands/record.py @@ -87,25 +87,25 @@ def get_audio_device(self, audio_mode: str) -> str: for line in result.stdout.strip().split("\n") if line ] + if device and device not in available_devices: print( f"Warning: Audio device '{device}' not available, falling back to default" ) - - if audio_mode == "mic": - input_devices = [ - d - for d in available_devices - if "input" in d.lower() or "mic" in d.lower() - ] - device = input_devices[0] if input_devices else "" - elif audio_mode == "system": - output_devices = [ - d - for d in available_devices - if "output" in d.lower() or "monitor" in d.lower() - ] - device = output_devices[0] if output_devices else "" + if audio_mode == "mic": + input_devices = [ + d + for d in available_devices + if "input" in d.lower() or "mic" in d.lower() + ] + device = input_devices[0] if input_devices else "" + elif audio_mode == "system": + output_devices = [ + d + for d in available_devices + if "output" in d.lower() or "monitor" in d.lower() + ] + device = output_devices[0] if output_devices else "" except (subprocess.CalledProcessError, FileNotFoundError): print( "Warning: Could not check audio devices, audio recording may fail" @@ -132,9 +132,11 @@ def start(self) -> None: else: region = self.args.region.strip() args += ["region", "-region", region] + m = re.match(r"(\d+)x(\d+)\+(\d+)\+(\d+)", region) if not m: raise ValueError(f"Invalid region: {region}") + w, h, x, y = map(int, m.groups()) r = x, y, w, h max_rr = 0 @@ -152,9 +154,11 @@ def start(self) -> None: ["slurp", "-w", "-f", "%wx%h+%x+%y"], text=True ).strip() args += ["region", "-region", window_info] + m = re.match(r"(\d+)x(\d+)\+(\d+)\+(\d+)", window_info) if not m: raise ValueError(f"Invalid window region: {window_info}") + w, h, x, y = map(int, m.groups()) r = x, y, w, h max_rr = 0 @@ -174,8 +178,8 @@ def start(self) -> None: except subprocess.CalledProcessError: print("Window selection canceled") return - else: - # fullscreen + + else: # fullscreen focused_monitor = next( (monitor for monitor in monitors if monitor["focused"]), None ) @@ -191,10 +195,11 @@ def start(self) -> None: if audio_device: args += ["-a", audio_device, "-ac", "opus", "-ab", "192k"] print(f"Recording with audio: {audio_device} ({audio_mode})") + elif self.args.sound: # Legacy support for --sound flag + args += ["-a", "default_output"] else: print("Recording without audio") - # Load extra args from config try: config = json.loads(user_config_path.read_text()) @@ -223,7 +228,7 @@ def start(self) -> None: notify( "Recording failed", "An error occurred attempting to start recorder. " - f"Command {' '.join(proc.args)} failed with exit code {proc.returncode}", + f"Command `{' '.join(proc.args)}` failed with exit code {proc.returncode}", ) except subprocess.TimeoutExpired: pass From 79c613e4893eddc80dd40c564c25db53c39d27f6 Mon Sep 17 00:00:00 2001 From: Valentine Omonya Date: Sun, 14 Dec 2025 03:21:59 +0300 Subject: [PATCH 003/106] Updated audio options --- src/caelestia/subcommands/record.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/caelestia/subcommands/record.py b/src/caelestia/subcommands/record.py index 442e7621..94a95a13 100644 --- a/src/caelestia/subcommands/record.py +++ b/src/caelestia/subcommands/record.py @@ -68,8 +68,8 @@ def intersects( def get_audio_device(self, audio_mode: str) -> str: """Get the appropriate audio device for the given mode with fallback handling.""" - if audio_mode == "none" or not audio_mode: - return "" + if not audio_mode: + return "none" device = AUDIO_MODES.get(audio_mode, "") @@ -195,7 +195,7 @@ def start(self) -> None: if audio_device: args += ["-a", audio_device, "-ac", "opus", "-ab", "192k"] print(f"Recording with audio: {audio_device} ({audio_mode})") - elif self.args.sound: # Legacy support for --sound flag + elif hasattr(self.args, "sound") and self.args.sound: # Legacy support for --sound flag args += ["-a", "default_output"] else: print("Recording without audio") From 7f0860e0c531340251423580b0d2e83ada81439e Mon Sep 17 00:00:00 2001 From: Valentine Omonya Date: Sun, 14 Dec 2025 11:26:17 +0300 Subject: [PATCH 004/106] Fixed window option --- src/caelestia/subcommands/record.py | 306 +++++++++++++++++++--------- 1 file changed, 209 insertions(+), 97 deletions(-) diff --git a/src/caelestia/subcommands/record.py b/src/caelestia/subcommands/record.py index 94a95a13..3a834ec9 100644 --- a/src/caelestia/subcommands/record.py +++ b/src/caelestia/subcommands/record.py @@ -115,124 +115,236 @@ def get_audio_device(self, audio_mode: str) -> str: return device def start(self) -> None: - args = ["-w"] - - # Get video mode and audio mode from args - video_mode = getattr(self.args, "mode", "fullscreen") - audio_mode = getattr(self.args, "audio", "none") - - monitors = json.loads(subprocess.check_output(["hyprctl", "monitors", "-j"])) + args = ["-w"] + # Get video mode and audio mode from args + video_mode = getattr(self.args, "mode", "fullscreen") + audio_mode = getattr(self.args, "audio", "none") + monitors = json.loads(subprocess.check_output(["hyprctl", "monitors", "-j"])) + # Handle video modes + if video_mode == "region" or self.args.region: + if self.args.region == "slurp" or not self.args.region: + region = subprocess.check_output( + ["slurp", "-f", "%wx%h+%x+%y"], text=True + ).strip() + else: + region = self.args.region.strip() + args += ["region", "-region", region] + m = re.match(r"(\d+)x(\d+)\+(\d+)\+(\d+)", region) + if not m: + raise ValueError(f"Invalid region: {region}") + w, h, x, y = map(int, m.groups()) + r = x, y, w, h + max_rr = 0 + for monitor in monitors: + if self.intersects( + (monitor["x"], monitor["y"], monitor["width"], monitor["height"]), r + ): + rr = round(monitor["refreshRate"]) + max_rr = max(max_rr, rr) + args += ["-f", str(max_rr)] + elif video_mode == "window": + try: + # Get active window info from Hyprland + active_window = json.loads( + subprocess.check_output(["hyprctl", "activewindow", "-j"]) + ) - # Handle video modes - if video_mode == "region" or self.args.region: - if self.args.region == "slurp" or not self.args.region: - region = subprocess.check_output( - ["slurp", "-f", "%wx%h+%x+%y"], text=True - ).strip() + # Extract window geometry + x = active_window["at"][0] + y = active_window["at"][1] + w = active_window["size"][0] + h = active_window["size"][1] + + window_region = f"{w}x{h}+{x}+{y}" + args += ["region", "-region", window_region] + + # Calculate max refresh rate for the window region + r = x, y, w, h + max_rr = 0 + for monitor in monitors: + if self.intersects( + ( + monitor["x"], + monitor["y"], + monitor["width"], + monitor["height"], + ), + r, + ): + rr = round(monitor["refreshRate"]) + max_rr = max(max_rr, rr) + args += ["-f", str(max_rr)] + except subprocess.CalledProcessError as e: + print(f"Window selection failed: {e}") + return + except (KeyError, json.JSONDecodeError) as e: + print(f"Could not parse window info: {e}") + return + else: # fullscreen + focused_monitor = next( + (monitor for monitor in monitors if monitor["focused"]), None + ) + if focused_monitor: + args += [ + focused_monitor["name"], + "-f", + str(round(focused_monitor["refreshRate"])), + ] + # Handle audio modes + audio_device = self.get_audio_device(audio_mode) + if audio_device: + args += ["-a", audio_device, "-ac", "opus", "-ab", "192k"] + print(f"Recording with audio: {audio_device} ({audio_mode})") + elif hasattr(self.args, "sound") and self.args.sound: # Legacy support for --sound flag + args += ["-a", "default_output"] else: - region = self.args.region.strip() - args += ["region", "-region", region] - - m = re.match(r"(\d+)x(\d+)\+(\d+)\+(\d+)", region) - if not m: - raise ValueError(f"Invalid region: {region}") - - w, h, x, y = map(int, m.groups()) - r = x, y, w, h - max_rr = 0 - for monitor in monitors: - if self.intersects( - (monitor["x"], monitor["y"], monitor["width"], monitor["height"]), r - ): - rr = round(monitor["refreshRate"]) - max_rr = max(max_rr, rr) - args += ["-f", str(max_rr)] - - elif video_mode == "window": + print("Recording without audio") + # Load extra args from config + try: + config = json.loads(user_config_path.read_text()) + if "record" in config and "extraArgs" in config["record"]: + args += config["record"]["extraArgs"] + except (json.JSONDecodeError, FileNotFoundError): + pass + except TypeError as e: + raise ValueError( + f"Config option 'record.extraArgs' should be an array: {e}" + ) + recording_path.parent.mkdir(parents=True, exist_ok=True) + proc = subprocess.Popen( + [RECORDER, *args, "-o", str(recording_path)], start_new_session=True + ) + # Show notification with mode info + mode_text = f"{video_mode} with {audio_mode if audio_device else 'no'} audio" + notif = notify("-p", "Recording started", f"Recording {mode_text}...") + recording_notif_path.write_text(notif) try: - window_info = subprocess.check_output( - ["slurp", "-w", "-f", "%wx%h+%x+%y"], text=True - ).strip() - args += ["region", "-region", window_info] + if proc.wait(1) != 0: + close_notification(notif) + notify( + "Recording failed", + "An error occurred attempting to start recorder. " + f"Command `{' '.join(proc.args)}` failed with exit code {proc.returncode}", + ) + except subprocess.TimeoutExpired: + pass + args = ["-w"] + + # Get video mode and audio mode from args + video_mode = getattr(self.args, "mode", "fullscreen") + audio_mode = getattr(self.args, "audio", "none") - m = re.match(r"(\d+)x(\d+)\+(\d+)\+(\d+)", window_info) + monitors = json.loads(subprocess.check_output(["hyprctl", "monitors", "-j"])) + + # Handle video modes + if video_mode == "region" or self.args.region: + if self.args.region == "slurp" or not self.args.region: + region = subprocess.check_output( + ["slurp", "-f", "%wx%h+%x+%y"], text=True + ).strip() + else: + region = self.args.region.strip() + args += ["region", "-region", region] + + m = re.match(r"(\d+)x(\d+)\+(\d+)\+(\d+)", region) if not m: - raise ValueError(f"Invalid window region: {window_info}") + raise ValueError(f"Invalid region: {region}") w, h, x, y = map(int, m.groups()) r = x, y, w, h max_rr = 0 for monitor in monitors: if self.intersects( - ( - monitor["x"], - monitor["y"], - monitor["width"], - monitor["height"], - ), - r, + (monitor["x"], monitor["y"], monitor["width"], monitor["height"]), r ): rr = round(monitor["refreshRate"]) max_rr = max(max_rr, rr) args += ["-f", str(max_rr)] - except subprocess.CalledProcessError: - print("Window selection canceled") - return - else: # fullscreen - focused_monitor = next( - (monitor for monitor in monitors if monitor["focused"]), None - ) - if focused_monitor: - args += [ - focused_monitor["name"], - "-f", - str(round(focused_monitor["refreshRate"])), - ] + elif video_mode == "window": + try: + window_info = subprocess.check_output( + ["slurp", "-w", "-f", "%wx%h+%x+%y"], text=True + ).strip() + args += ["region", "-region", window_info] + + m = re.match(r"(\d+)x(\d+)\+(\d+)\+(\d+)", window_info) + if not m: + raise ValueError(f"Invalid window region: {window_info}") + + w, h, x, y = map(int, m.groups()) + r = x, y, w, h + max_rr = 0 + for monitor in monitors: + if self.intersects( + ( + monitor["x"], + monitor["y"], + monitor["width"], + monitor["height"], + ), + r, + ): + rr = round(monitor["refreshRate"]) + max_rr = max(max_rr, rr) + args += ["-f", str(max_rr)] + except subprocess.CalledProcessError: + print("Window selection canceled") + return + + else: # fullscreen + focused_monitor = next( + (monitor for monitor in monitors if monitor["focused"]), None + ) + if focused_monitor: + args += [ + focused_monitor["name"], + "-f", + str(round(focused_monitor["refreshRate"])), + ] + + # Handle audio modes + audio_device = self.get_audio_device(audio_mode) + if audio_device: + args += ["-a", audio_device, "-ac", "opus", "-ab", "192k"] + print(f"Recording with audio: {audio_device} ({audio_mode})") + elif hasattr(self.args, "sound") and self.args.sound: # Legacy support for --sound flag + args += ["-a", "default_output"] + else: + print("Recording without audio") - # Handle audio modes - audio_device = self.get_audio_device(audio_mode) - if audio_device: - args += ["-a", audio_device, "-ac", "opus", "-ab", "192k"] - print(f"Recording with audio: {audio_device} ({audio_mode})") - elif hasattr(self.args, "sound") and self.args.sound: # Legacy support for --sound flag - args += ["-a", "default_output"] - else: - print("Recording without audio") + # Load extra args from config + try: + config = json.loads(user_config_path.read_text()) + if "record" in config and "extraArgs" in config["record"]: + args += config["record"]["extraArgs"] + except (json.JSONDecodeError, FileNotFoundError): + pass + except TypeError as e: + raise ValueError( + f"Config option 'record.extraArgs' should be an array: {e}" + ) - # Load extra args from config - try: - config = json.loads(user_config_path.read_text()) - if "record" in config and "extraArgs" in config["record"]: - args += config["record"]["extraArgs"] - except (json.JSONDecodeError, FileNotFoundError): - pass - except TypeError as e: - raise ValueError( - f"Config option 'record.extraArgs' should be an array: {e}" + recording_path.parent.mkdir(parents=True, exist_ok=True) + proc = subprocess.Popen( + [RECORDER, *args, "-o", str(recording_path)], start_new_session=True ) - recording_path.parent.mkdir(parents=True, exist_ok=True) - proc = subprocess.Popen( - [RECORDER, *args, "-o", str(recording_path)], start_new_session=True - ) - - # Show notification with mode info - mode_text = f"{video_mode} with {audio_mode if audio_device else 'no'} audio" - notif = notify("-p", "Recording started", f"Recording {mode_text}...") - recording_notif_path.write_text(notif) - - try: - if proc.wait(1) != 0: - close_notification(notif) - notify( - "Recording failed", - "An error occurred attempting to start recorder. " - f"Command `{' '.join(proc.args)}` failed with exit code {proc.returncode}", - ) - except subprocess.TimeoutExpired: - pass + # Show notification with mode info + mode_text = f"{video_mode} with {audio_mode if audio_device else 'no'} audio" + notif = notify("-p", "Recording started", f"Recording {mode_text}...") + recording_notif_path.write_text(notif) + try: + if proc.wait(1) != 0: + close_notification(notif) + notify( + "Recording failed", + "An error occurred attempting to start recorder. " + f"Command `{' '.join(proc.args)}` failed with exit code {proc.returncode}", + ) + except subprocess.TimeoutExpired: + pass def stop(self) -> None: # Start killing recording process subprocess.run(["pkill", "-f", RECORDER], stdout=subprocess.DEVNULL) From 6ad4fc5d0b21767a6f8f9c42e43413fffe23866d Mon Sep 17 00:00:00 2001 From: Valentine Omonya Date: Sun, 14 Dec 2025 11:47:27 +0300 Subject: [PATCH 005/106] Fixed window option --- src/caelestia/subcommands/record.py | 370 +++++++++++----------------- 1 file changed, 147 insertions(+), 223 deletions(-) diff --git a/src/caelestia/subcommands/record.py b/src/caelestia/subcommands/record.py index 3a834ec9..17271d63 100644 --- a/src/caelestia/subcommands/record.py +++ b/src/caelestia/subcommands/record.py @@ -68,7 +68,7 @@ def intersects( def get_audio_device(self, audio_mode: str) -> str: """Get the appropriate audio device for the given mode with fallback handling.""" - if not audio_mode: + if not audio_mode: return "none" device = AUDIO_MODES.get(audio_mode, "") @@ -114,237 +114,161 @@ def get_audio_device(self, audio_mode: str) -> str: return device + def get_window_region(self) -> str | None: + """Get window region using Hyprland's client list and slurp for selection.""" + try: + # Get all windows from Hyprland + clients = json.loads(subprocess.check_output(["hyprctl", "clients", "-j"])) + + if not clients: + print("No windows found") + return None + + # Create slurp format strings for each window + slurp_regions = [] + for client in clients: + x = client["at"][0] + y = client["at"][1] + w = client["size"][0] + h = client["size"][1] + slurp_regions.append(f"{x},{y} {w}x{h}") + + # Use slurp with predefined regions to pick a window + slurp_input = "\n".join(slurp_regions) + result = subprocess.run( + ["slurp", "-f", "%wx%h+%x+%y"], + input=slurp_input, + capture_output=True, + text=True, + ) + + if result.returncode != 0: + return None + + return result.stdout.strip() + + except (subprocess.CalledProcessError, json.JSONDecodeError, KeyError) as e: + print(f"Error getting window region: {e}") + return None + def start(self) -> None: - args = ["-w"] - # Get video mode and audio mode from args - video_mode = getattr(self.args, "mode", "fullscreen") - audio_mode = getattr(self.args, "audio", "none") - monitors = json.loads(subprocess.check_output(["hyprctl", "monitors", "-j"])) - # Handle video modes - if video_mode == "region" or self.args.region: - if self.args.region == "slurp" or not self.args.region: - region = subprocess.check_output( - ["slurp", "-f", "%wx%h+%x+%y"], text=True - ).strip() - else: - region = self.args.region.strip() - args += ["region", "-region", region] - m = re.match(r"(\d+)x(\d+)\+(\d+)\+(\d+)", region) - if not m: - raise ValueError(f"Invalid region: {region}") - w, h, x, y = map(int, m.groups()) - r = x, y, w, h - max_rr = 0 - for monitor in monitors: - if self.intersects( - (monitor["x"], monitor["y"], monitor["width"], monitor["height"]), r - ): - rr = round(monitor["refreshRate"]) - max_rr = max(max_rr, rr) - args += ["-f", str(max_rr)] - elif video_mode == "window": - try: - # Get active window info from Hyprland - active_window = json.loads( - subprocess.check_output(["hyprctl", "activewindow", "-j"]) - ) + args = ["-w"] - # Extract window geometry - x = active_window["at"][0] - y = active_window["at"][1] - w = active_window["size"][0] - h = active_window["size"][1] - - window_region = f"{w}x{h}+{x}+{y}" - args += ["region", "-region", window_region] - - # Calculate max refresh rate for the window region - r = x, y, w, h - max_rr = 0 - for monitor in monitors: - if self.intersects( - ( - monitor["x"], - monitor["y"], - monitor["width"], - monitor["height"], - ), - r, - ): - rr = round(monitor["refreshRate"]) - max_rr = max(max_rr, rr) - args += ["-f", str(max_rr)] - except subprocess.CalledProcessError as e: - print(f"Window selection failed: {e}") - return - except (KeyError, json.JSONDecodeError) as e: - print(f"Could not parse window info: {e}") - return - else: # fullscreen - focused_monitor = next( - (monitor for monitor in monitors if monitor["focused"]), None - ) - if focused_monitor: - args += [ - focused_monitor["name"], - "-f", - str(round(focused_monitor["refreshRate"])), - ] - # Handle audio modes - audio_device = self.get_audio_device(audio_mode) - if audio_device: - args += ["-a", audio_device, "-ac", "opus", "-ab", "192k"] - print(f"Recording with audio: {audio_device} ({audio_mode})") - elif hasattr(self.args, "sound") and self.args.sound: # Legacy support for --sound flag - args += ["-a", "default_output"] + # Get video mode and audio mode from args + video_mode = getattr(self.args, "mode", "fullscreen") + audio_mode = getattr(self.args, "audio", "none") + + monitors = json.loads(subprocess.check_output(["hyprctl", "monitors", "-j"])) + + # Handle video modes + if video_mode == "region" or self.args.region: + if self.args.region == "slurp" or not self.args.region: + region = subprocess.check_output( + ["slurp", "-f", "%wx%h+%x+%y"], text=True + ).strip() else: - print("Recording without audio") - # Load extra args from config - try: - config = json.loads(user_config_path.read_text()) - if "record" in config and "extraArgs" in config["record"]: - args += config["record"]["extraArgs"] - except (json.JSONDecodeError, FileNotFoundError): - pass - except TypeError as e: - raise ValueError( - f"Config option 'record.extraArgs' should be an array: {e}" - ) - recording_path.parent.mkdir(parents=True, exist_ok=True) - proc = subprocess.Popen( - [RECORDER, *args, "-o", str(recording_path)], start_new_session=True + region = self.args.region.strip() + args += ["region", "-region", region] + + m = re.match(r"(\d+)x(\d+)\+(\d+)\+(\d+)", region) + if not m: + raise ValueError(f"Invalid region: {region}") + + w, h, x, y = map(int, m.groups()) + r = x, y, w, h + max_rr = 0 + for monitor in monitors: + if self.intersects( + (monitor["x"], monitor["y"], monitor["width"], monitor["height"]), r + ): + rr = round(monitor["refreshRate"]) + max_rr = max(max_rr, rr) + args += ["-f", str(max_rr)] + + elif video_mode == "window": + window_info = self.get_window_region() + if not window_info: + print("Window selection canceled") + return + + args += ["region", "-region", window_info] + m = re.match(r"(\d+)x(\d+)\+(\d+)\+(\d+)", window_info) + if not m: + raise ValueError(f"Invalid window region: {window_info}") + + w, h, x, y = map(int, m.groups()) + r = x, y, w, h + + # Calculate max refresh rate for the window region + max_rr = 0 + for monitor in monitors: + if self.intersects( + ( + monitor["x"], + monitor["y"], + monitor["width"], + monitor["height"], + ), + r, + ): + rr = round(monitor["refreshRate"]) + max_rr = max(max_rr, rr) + args += ["-f", str(max_rr)] + + else: # fullscreen + focused_monitor = next( + (monitor for monitor in monitors if monitor["focused"]), None ) - # Show notification with mode info - mode_text = f"{video_mode} with {audio_mode if audio_device else 'no'} audio" - notif = notify("-p", "Recording started", f"Recording {mode_text}...") - recording_notif_path.write_text(notif) - try: - if proc.wait(1) != 0: - close_notification(notif) - notify( - "Recording failed", - "An error occurred attempting to start recorder. " - f"Command `{' '.join(proc.args)}` failed with exit code {proc.returncode}", - ) - except subprocess.TimeoutExpired: - pass - args = ["-w"] - - # Get video mode and audio mode from args - video_mode = getattr(self.args, "mode", "fullscreen") - audio_mode = getattr(self.args, "audio", "none") - - monitors = json.loads(subprocess.check_output(["hyprctl", "monitors", "-j"])) - - # Handle video modes - if video_mode == "region" or self.args.region: - if self.args.region == "slurp" or not self.args.region: - region = subprocess.check_output( - ["slurp", "-f", "%wx%h+%x+%y"], text=True - ).strip() - else: - region = self.args.region.strip() - args += ["region", "-region", region] - - m = re.match(r"(\d+)x(\d+)\+(\d+)\+(\d+)", region) - if not m: - raise ValueError(f"Invalid region: {region}") - - w, h, x, y = map(int, m.groups()) - r = x, y, w, h - max_rr = 0 - for monitor in monitors: - if self.intersects( - (monitor["x"], monitor["y"], monitor["width"], monitor["height"]), r - ): - rr = round(monitor["refreshRate"]) - max_rr = max(max_rr, rr) - args += ["-f", str(max_rr)] - - elif video_mode == "window": - try: - window_info = subprocess.check_output( - ["slurp", "-w", "-f", "%wx%h+%x+%y"], text=True - ).strip() - args += ["region", "-region", window_info] - - m = re.match(r"(\d+)x(\d+)\+(\d+)\+(\d+)", window_info) - if not m: - raise ValueError(f"Invalid window region: {window_info}") - - w, h, x, y = map(int, m.groups()) - r = x, y, w, h - max_rr = 0 - for monitor in monitors: - if self.intersects( - ( - monitor["x"], - monitor["y"], - monitor["width"], - monitor["height"], - ), - r, - ): - rr = round(monitor["refreshRate"]) - max_rr = max(max_rr, rr) - args += ["-f", str(max_rr)] - except subprocess.CalledProcessError: - print("Window selection canceled") - return - - else: # fullscreen - focused_monitor = next( - (monitor for monitor in monitors if monitor["focused"]), None - ) - if focused_monitor: - args += [ - focused_monitor["name"], - "-f", - str(round(focused_monitor["refreshRate"])), - ] - - # Handle audio modes - audio_device = self.get_audio_device(audio_mode) - if audio_device: - args += ["-a", audio_device, "-ac", "opus", "-ab", "192k"] - print(f"Recording with audio: {audio_device} ({audio_mode})") - elif hasattr(self.args, "sound") and self.args.sound: # Legacy support for --sound flag - args += ["-a", "default_output"] - else: - print("Recording without audio") + if focused_monitor: + args += [ + focused_monitor["name"], + "-f", + str(round(focused_monitor["refreshRate"])), + ] - # Load extra args from config - try: - config = json.loads(user_config_path.read_text()) - if "record" in config and "extraArgs" in config["record"]: - args += config["record"]["extraArgs"] - except (json.JSONDecodeError, FileNotFoundError): - pass - except TypeError as e: - raise ValueError( - f"Config option 'record.extraArgs' should be an array: {e}" - ) + # Handle audio modes + audio_device = self.get_audio_device(audio_mode) + if audio_device: + args += ["-a", audio_device, "-ac", "opus", "-ab", "192k"] + print(f"Recording with audio: {audio_device} ({audio_mode})") + elif hasattr(self.args, "sound") and self.args.sound: + args += ["-a", "default_output"] + else: + print("Recording without audio") - recording_path.parent.mkdir(parents=True, exist_ok=True) - proc = subprocess.Popen( - [RECORDER, *args, "-o", str(recording_path)], start_new_session=True + # Load extra args from config + try: + config = json.loads(user_config_path.read_text()) + if "record" in config and "extraArgs" in config["record"]: + args += config["record"]["extraArgs"] + except (json.JSONDecodeError, FileNotFoundError): + pass + except TypeError as e: + raise ValueError( + f"Config option 'record.extraArgs' should be an array: {e}" ) - # Show notification with mode info - mode_text = f"{video_mode} with {audio_mode if audio_device else 'no'} audio" - notif = notify("-p", "Recording started", f"Recording {mode_text}...") - recording_notif_path.write_text(notif) + recording_path.parent.mkdir(parents=True, exist_ok=True) + proc = subprocess.Popen( + [RECORDER, *args, "-o", str(recording_path)], start_new_session=True + ) + + # Show notification with mode info + mode_text = f"{video_mode} with {audio_mode if audio_device else 'no'} audio" + notif = notify("-p", "Recording started", f"Recording {mode_text}...") + recording_notif_path.write_text(notif) + + try: + if proc.wait(1) != 0: + close_notification(notif) + notify( + "Recording failed", + "An error occurred attempting to start recorder. " + f"Command `{' '.join(proc.args)}` failed with exit code {proc.returncode}", + ) + except subprocess.TimeoutExpired: + pass - try: - if proc.wait(1) != 0: - close_notification(notif) - notify( - "Recording failed", - "An error occurred attempting to start recorder. " - f"Command `{' '.join(proc.args)}` failed with exit code {proc.returncode}", - ) - except subprocess.TimeoutExpired: - pass def stop(self) -> None: # Start killing recording process subprocess.run(["pkill", "-f", RECORDER], stdout=subprocess.DEVNULL) From 4edc0173881a047ad65bb801a9cd570e3e717190 Mon Sep 17 00:00:00 2001 From: Devalentine_ <147273781+devalentineomonya@users.noreply.github.com> Date: Tue, 6 Jan 2026 21:21:07 +0300 Subject: [PATCH 006/106] Update src/caelestia/subcommands/record.py Co-authored-by: Mateus --- src/caelestia/subcommands/record.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/caelestia/subcommands/record.py b/src/caelestia/subcommands/record.py index 17271d63..c63b0e91 100644 --- a/src/caelestia/subcommands/record.py +++ b/src/caelestia/subcommands/record.py @@ -19,7 +19,7 @@ AUDIO_MODES = { "mic": "default_input", "system": "default_output", - "combined": "CombinedSink.monitor", + "combined": "default_output|default_input", } From 6c89b83a56b72f3e2a5005d39d3ed5a94dfbbb57 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 14 Dec 2025 02:13:56 +0000 Subject: [PATCH 007/106] [CI] chore: update flake --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index b1c4bf68..46162398 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1765071049, - "narHash": "sha256-HIJtxkYaGxUFZ03wOzF4pWhKWAvFuYBN9jAdhCzZvnI=", + "lastModified": 1765675511, + "narHash": "sha256-o8Ok+UaDRDUhONI9JqytO//RlWOf6LAPnEQ7vA8f+qw=", "owner": "caelestia-dots", "repo": "shell", - "rev": "982d64d5e5b9295d12dec37d45442ed6a05fe284", + "rev": "d819bf2fc3ded72ad4fa116c4f06c3afb7d59153", "type": "github" }, "original": { @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1765186076, - "narHash": "sha256-hM20uyap1a0M9d344I692r+ik4gTMyj60cQWO+hAYP8=", + "lastModified": 1765472234, + "narHash": "sha256-9VvC20PJPsleGMewwcWYKGzDIyjckEz8uWmT0vCDYK0=", "owner": "nixos", "repo": "nixpkgs", - "rev": "addf7cf5f383a3101ecfba091b98d0a1263dc9b8", + "rev": "2fbfb1d73d239d2402a8fe03963e37aab15abe8b", "type": "github" }, "original": { From 94344511f95dd6b885f282d779a895d2def46f49 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 02:07:04 +0000 Subject: [PATCH 008/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 46162398..60650c07 100644 --- a/flake.lock +++ b/flake.lock @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1765472234, - "narHash": "sha256-9VvC20PJPsleGMewwcWYKGzDIyjckEz8uWmT0vCDYK0=", + "lastModified": 1765779637, + "narHash": "sha256-KJ2wa/BLSrTqDjbfyNx70ov/HdgNBCBBSQP3BIzKnv4=", "owner": "nixos", "repo": "nixpkgs", - "rev": "2fbfb1d73d239d2402a8fe03963e37aab15abe8b", + "rev": "1306659b587dc277866c7b69eb97e5f07864d8c4", "type": "github" }, "original": { From a94dccf63c5df17edf5b4a32fa57d3d9fa9076c7 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 20 Dec 2025 01:59:18 +0000 Subject: [PATCH 009/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 60650c07..723b2803 100644 --- a/flake.lock +++ b/flake.lock @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1765779637, - "narHash": "sha256-KJ2wa/BLSrTqDjbfyNx70ov/HdgNBCBBSQP3BIzKnv4=", + "lastModified": 1766070988, + "narHash": "sha256-G/WVghka6c4bAzMhTwT2vjLccg/awmHkdKSd2JrycLc=", "owner": "nixos", "repo": "nixpkgs", - "rev": "1306659b587dc277866c7b69eb97e5f07864d8c4", + "rev": "c6245e83d836d0433170a16eb185cefe0572f8b8", "type": "github" }, "original": { From 8eb805b5ce4fec672ab45486447a61d03f82d354 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 21 Dec 2025 02:13:41 +0000 Subject: [PATCH 010/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 723b2803..d8e6b878 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1765675511, - "narHash": "sha256-o8Ok+UaDRDUhONI9JqytO//RlWOf6LAPnEQ7vA8f+qw=", + "lastModified": 1766280311, + "narHash": "sha256-Ty4GT6EaQFaL9E/dKcJB1Q30ByGkxXV6U1eu8+PS5Dc=", "owner": "caelestia-dots", "repo": "shell", - "rev": "d819bf2fc3ded72ad4fa116c4f06c3afb7d59153", + "rev": "66e509ae488b2c0468f2c803fd34c2625ca725dc", "type": "github" }, "original": { From 182b92e35af08886356b282776fe5bb394814a12 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 23 Dec 2025 02:07:06 +0000 Subject: [PATCH 011/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index d8e6b878..45591c14 100644 --- a/flake.lock +++ b/flake.lock @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1766070988, - "narHash": "sha256-G/WVghka6c4bAzMhTwT2vjLccg/awmHkdKSd2JrycLc=", + "lastModified": 1766309749, + "narHash": "sha256-3xY8CZ4rSnQ0NqGhMKAy5vgC+2IVK0NoVEzDoOh4DA4=", "owner": "nixos", "repo": "nixpkgs", - "rev": "c6245e83d836d0433170a16eb185cefe0572f8b8", + "rev": "a6531044f6d0bef691ea18d4d4ce44d0daa6e816", "type": "github" }, "original": { From 3dbfe79aaed6e41b07615d64ac107b163193e835 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 26 Dec 2025 02:06:29 +0000 Subject: [PATCH 012/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 45591c14..3b8b0dc6 100644 --- a/flake.lock +++ b/flake.lock @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1766309749, - "narHash": "sha256-3xY8CZ4rSnQ0NqGhMKAy5vgC+2IVK0NoVEzDoOh4DA4=", + "lastModified": 1766651565, + "narHash": "sha256-QEhk0eXgyIqTpJ/ehZKg9IKS7EtlWxF3N7DXy42zPfU=", "owner": "nixos", "repo": "nixpkgs", - "rev": "a6531044f6d0bef691ea18d4d4ce44d0daa6e816", + "rev": "3e2499d5539c16d0d173ba53552a4ff8547f4539", "type": "github" }, "original": { From 60b80ade96fdf64699d0fa9dca29c0272685c8d8 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 28 Dec 2025 02:30:51 +0000 Subject: [PATCH 013/106] [CI] chore: update flake --- flake.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/flake.lock b/flake.lock index 3b8b0dc6..aab5aa52 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1766280311, - "narHash": "sha256-Ty4GT6EaQFaL9E/dKcJB1Q30ByGkxXV6U1eu8+PS5Dc=", + "lastModified": 1766885627, + "narHash": "sha256-Ux+D5mjfLEMQfCaG9g4TGKgBsQ/oVjo1613/m7Uk/5Y=", "owner": "caelestia-dots", "repo": "shell", - "rev": "66e509ae488b2c0468f2c803fd34c2625ca725dc", + "rev": "dfa28d7f85215bd42d02717255c45b8126aeb8da", "type": "github" }, "original": { @@ -46,11 +46,11 @@ ] }, "locked": { - "lastModified": 1764663772, - "narHash": "sha256-sHqLmm0wAt3PC4vczJeBozI1/f4rv9yp3IjkClHDXDs=", + "lastModified": 1766725085, + "narHash": "sha256-O2aMFdDUYJazFrlwL7aSIHbUSEm3ADVZjmf41uBJfHs=", "ref": "refs/heads/master", - "rev": "26531fc46ef17e9365b03770edd3fb9206fcb460", - "revCount": 713, + "rev": "41828c4180fb921df7992a5405f5ff05d2ac2fff", + "revCount": 715, "type": "git", "url": "https://git.outfoxxed.me/outfoxxed/quickshell" }, From 66295a47c5d6f02d52c2496b2bf3520aa7903f26 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 29 Dec 2025 02:28:26 +0000 Subject: [PATCH 014/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index aab5aa52..ca34dc87 100644 --- a/flake.lock +++ b/flake.lock @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1766651565, - "narHash": "sha256-QEhk0eXgyIqTpJ/ehZKg9IKS7EtlWxF3N7DXy42zPfU=", + "lastModified": 1766902085, + "narHash": "sha256-coBu0ONtFzlwwVBzmjacUQwj3G+lybcZ1oeNSQkgC0M=", "owner": "nixos", "repo": "nixpkgs", - "rev": "3e2499d5539c16d0d173ba53552a4ff8547f4539", + "rev": "c0b0e0fddf73fd517c3471e546c0df87a42d53f4", "type": "github" }, "original": { From f6cae21abbfd08e5a7f74b3dd99ea0675f5b51a2 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 2 Jan 2026 02:16:30 +0000 Subject: [PATCH 015/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index ca34dc87..b126ee7c 100644 --- a/flake.lock +++ b/flake.lock @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1766902085, - "narHash": "sha256-coBu0ONtFzlwwVBzmjacUQwj3G+lybcZ1oeNSQkgC0M=", + "lastModified": 1767116409, + "narHash": "sha256-5vKw92l1GyTnjoLzEagJy5V5mDFck72LiQWZSOnSicw=", "owner": "nixos", "repo": "nixpkgs", - "rev": "c0b0e0fddf73fd517c3471e546c0df87a42d53f4", + "rev": "cad22e7d996aea55ecab064e84834289143e44a0", "type": "github" }, "original": { From a8ccaaf50217eb07790b658708d05fffa47c7bd5 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 4 Jan 2026 02:27:36 +0000 Subject: [PATCH 016/106] [CI] chore: update flake --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index b126ee7c..6447e87d 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1766885627, - "narHash": "sha256-Ux+D5mjfLEMQfCaG9g4TGKgBsQ/oVjo1613/m7Uk/5Y=", + "lastModified": 1767490542, + "narHash": "sha256-NkwDCzDC5soGuAE4k8YuvdzYOi7ugrBjUxavKwmFoUM=", "owner": "caelestia-dots", "repo": "shell", - "rev": "dfa28d7f85215bd42d02717255c45b8126aeb8da", + "rev": "1b4b90a3ad9532f7002ef2593d8efb68443f21f3", "type": "github" }, "original": { @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1767116409, - "narHash": "sha256-5vKw92l1GyTnjoLzEagJy5V5mDFck72LiQWZSOnSicw=", + "lastModified": 1767379071, + "narHash": "sha256-EgE0pxsrW9jp9YFMkHL9JMXxcqi/OoumPJYwf+Okucw=", "owner": "nixos", "repo": "nixpkgs", - "rev": "cad22e7d996aea55ecab064e84834289143e44a0", + "rev": "fb7944c166a3b630f177938e478f0378e64ce108", "type": "github" }, "original": { From 6c756aa26a0a6c597618dd541f7670a923a3822a Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 7 Jan 2026 02:09:52 +0000 Subject: [PATCH 017/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 6447e87d..8639a812 100644 --- a/flake.lock +++ b/flake.lock @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1767379071, - "narHash": "sha256-EgE0pxsrW9jp9YFMkHL9JMXxcqi/OoumPJYwf+Okucw=", + "lastModified": 1767640445, + "narHash": "sha256-UWYqmD7JFBEDBHWYcqE6s6c77pWdcU/i+bwD6XxMb8A=", "owner": "nixos", "repo": "nixpkgs", - "rev": "fb7944c166a3b630f177938e478f0378e64ce108", + "rev": "9f0c42f8bc7151b8e7e5840fb3bd454ad850d8c5", "type": "github" }, "original": { From c57314e215453672b24137145d9624c6a1b1509e Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 8 Jan 2026 02:10:20 +0000 Subject: [PATCH 018/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 8639a812..ca43455b 100644 --- a/flake.lock +++ b/flake.lock @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1767640445, - "narHash": "sha256-UWYqmD7JFBEDBHWYcqE6s6c77pWdcU/i+bwD6XxMb8A=", + "lastModified": 1767767207, + "narHash": "sha256-Mj3d3PfwltLmukFal5i3fFt27L6NiKXdBezC1EBuZs4=", "owner": "nixos", "repo": "nixpkgs", - "rev": "9f0c42f8bc7151b8e7e5840fb3bd454ad850d8c5", + "rev": "5912c1772a44e31bf1c63c0390b90501e5026886", "type": "github" }, "original": { From 076a09887694c0bfeed8e641fbc856a12ce1d32b Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 10 Jan 2026 02:06:38 +0000 Subject: [PATCH 019/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index ca43455b..dca532df 100644 --- a/flake.lock +++ b/flake.lock @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1767767207, - "narHash": "sha256-Mj3d3PfwltLmukFal5i3fFt27L6NiKXdBezC1EBuZs4=", + "lastModified": 1767892417, + "narHash": "sha256-dhhvQY67aboBk8b0/u0XB6vwHdgbROZT3fJAjyNh5Ww=", "owner": "nixos", "repo": "nixpkgs", - "rev": "5912c1772a44e31bf1c63c0390b90501e5026886", + "rev": "3497aa5c9457a9d88d71fa93a4a8368816fbeeba", "type": "github" }, "original": { From f7fed17e4b485a362e66dbe81e716615613362a3 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 11 Jan 2026 02:32:22 +0000 Subject: [PATCH 020/106] [CI] chore: update flake --- flake.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/flake.lock b/flake.lock index dca532df..ca5628b2 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1767490542, - "narHash": "sha256-NkwDCzDC5soGuAE4k8YuvdzYOi7ugrBjUxavKwmFoUM=", + "lastModified": 1768095294, + "narHash": "sha256-KfuZxu5LOYeFq3s2qQKJDvVhxB2qhxHYYj2TL5MoaPU=", "owner": "caelestia-dots", "repo": "shell", - "rev": "1b4b90a3ad9532f7002ef2593d8efb68443f21f3", + "rev": "15e637c98059dea3642b845f48d3d6d6597f1379", "type": "github" }, "original": { @@ -46,11 +46,11 @@ ] }, "locked": { - "lastModified": 1766725085, - "narHash": "sha256-O2aMFdDUYJazFrlwL7aSIHbUSEm3ADVZjmf41uBJfHs=", + "lastModified": 1768080170, + "narHash": "sha256-HYKRNShQe5YnnxLazQajB9JkAPGpVcUt9jQ3KwilITQ=", "ref": "refs/heads/master", - "rev": "41828c4180fb921df7992a5405f5ff05d2ac2fff", - "revCount": 715, + "rev": "bcc3d4265e8b3ed2b17b801923905b60a3927823", + "revCount": 722, "type": "git", "url": "https://git.outfoxxed.me/outfoxxed/quickshell" }, From 285048ba015895a61a329740c21844fae7368c68 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 02:23:10 +0000 Subject: [PATCH 021/106] [CI] chore: update flake --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index ca5628b2..fbe10d82 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1768095294, - "narHash": "sha256-KfuZxu5LOYeFq3s2qQKJDvVhxB2qhxHYYj2TL5MoaPU=", + "lastModified": 1768132764, + "narHash": "sha256-MSU1F4RzcNeeW3R0hxBYsci+MxCxzet6uHh3pUsPdVQ=", "owner": "caelestia-dots", "repo": "shell", - "rev": "15e637c98059dea3642b845f48d3d6d6597f1379", + "rev": "1097c7fb37367be448c53d8aa5c9cd6d49418622", "type": "github" }, "original": { @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1767892417, - "narHash": "sha256-dhhvQY67aboBk8b0/u0XB6vwHdgbROZT3fJAjyNh5Ww=", + "lastModified": 1768127708, + "narHash": "sha256-1Sm77VfZh3mU0F5OqKABNLWxOuDeHIlcFjsXeeiPazs=", "owner": "nixos", "repo": "nixpkgs", - "rev": "3497aa5c9457a9d88d71fa93a4a8368816fbeeba", + "rev": "ffbc9f8cbaacfb331b6017d5a5abb21a492c9a38", "type": "github" }, "original": { From 8054f7e25cf4c75afe95d4073d09150521667482 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 02:08:26 +0000 Subject: [PATCH 022/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index fbe10d82..2e42ff69 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1768132764, - "narHash": "sha256-MSU1F4RzcNeeW3R0hxBYsci+MxCxzet6uHh3pUsPdVQ=", + "lastModified": 1768218535, + "narHash": "sha256-hu8b8JW28QGvH3lkVmIPltr5S8U6/hkRoJDlWA9kdcE=", "owner": "caelestia-dots", "repo": "shell", - "rev": "1097c7fb37367be448c53d8aa5c9cd6d49418622", + "rev": "3bb58e6da8f9bf465e6cd5517c46c4a0667c0e07", "type": "github" }, "original": { From c8326c1a992af8d06786a457c11e4219e1214755 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Wed, 14 Jan 2026 19:18:36 +1100 Subject: [PATCH 023/106] theme: update discord theme template Fixes caelestia-dots/caelestia#206 --- src/caelestia/data/templates/discord.scss | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/caelestia/data/templates/discord.scss b/src/caelestia/data/templates/discord.scss index 91d0cfb1..5582741f 100644 --- a/src/caelestia/data/templates/discord.scss +++ b/src/caelestia/data/templates/discord.scss @@ -15,8 +15,10 @@ @import url("https://refact0r.github.io/midnight-discord/build/midnight.css"); body { - /* font, change to '' for default discord font */ - --font: "figtree"; + /* font options */ + --font: "figtree"; /* change to '' for default discord font */ + --code-font: "JetBrainsMono NF"; /* change to '' for default discord font */ + font-weight: 400; /* normal text font weight. DOES NOT AFFECT BOLD TEXT */ /* sizes */ --gap: 12px; /* spacing between panels */ @@ -27,13 +29,14 @@ body { --animations: on; /* turn off to disable all midnight animations/transitions */ --list-item-transition: 0.2s ease; /* transition for list items */ --dms-icon-svg-transition: 0.4s ease; /* transition for the dms icon */ + --border-hover-transition: 0.2s ease; /* transition for borders when hovered */ /* top bar options */ --top-bar-height: var( --gap ); /* height of the titlebar/top bar (discord default is 36px, 24px recommended if moving/hiding top bar buttons) */ - --top-bar-button-position: hide; /* off: default position, hide: hide inbox/support buttons completely, serverlist: move inbox button to server list, titlebar: move inbox button to titlebar (will hide title) */ - --top-bar-title-position: hide; /* off: default centered position, hide: hide title completely, left: left align title (like old discord) */ + --top-bar-button-position: titlebar; /* off: default position, hide: hide inbox/support buttons completely, serverlist: move inbox button to server list, titlebar: move inbox button to titlebar (will hide title) */ + --top-bar-title-position: off; /* off: default centered position, hide: hide title completely, left: left align title (like old discord) */ --subtle-top-bar-title: off; /* off: default, on: hide the icon and use subtle text color (like old discord) */ /* window controls */ @@ -42,9 +45,9 @@ body { /* dms button icon options */ --custom-dms-icon: custom; /* off: use default discord icon, hide: remove icon entirely, custom: use custom icon */ - --dms-icon-svg-url: url("https://upload.wikimedia.org/wikipedia/commons/c/c4/Font_Awesome_5_solid_moon.svg"); /* icon svg url. MUST BE A SVG. */ + --dms-icon-svg-url: url("https://refact0r.github.io/midnight-discord/assets/Font_Awesome_5_solid_moon.svg"); /* icon svg url. MUST BE A SVG. */ --dms-icon-svg-size: 90%; /* size of the svg (css mask-size) */ - --dms-icon-color-before: var(--icon-secondary); /* normal icon color */ + --dms-icon-color-before: var(--icon-subtle); /* normal icon color */ --dms-icon-color-after: var(--white); /* icon color when button is hovered/selected */ /* dms button background options */ @@ -71,12 +74,11 @@ body { --bg-floating: #{c.$surface}; /* you can set this to a more opaque color if floating panels look too transparent */ /* chatbar options */ - --custom-chatbar: aligned; /* off: default chatbar, aligned: chatbar aligned with the user panel, separated: chatbar separated from chat */ - --chatbar-height: 47px; /* height of the chatbar (52px by default, 47px recommended for aligned, 56px recommended for separated) */ - --chatbar-padding: 8px; /* padding of the chatbar. only applies in aligned mode. */ + --custom-chatbar: off; /* off: default chatbar, separated: chatbar separated from chat */ + --chatbar-height: 47px; /* height of the chatbar (56px by default, 47px to align with user panel, 56px recommended for separated) */ /* other options */ - --small-user-panel: off; /* turn on to make the user panel smaller like in old discord */ + --small-user-panel: off; /* off: default user panel, on: smaller user panel like in old discord */ } /* color options */ From 8a48ae8fe20dedaaf94a55024dd2a1ae673974fa Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 02:12:09 +0000 Subject: [PATCH 024/106] [CI] chore: update flake --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 2e42ff69..2bd66aaf 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1768218535, - "narHash": "sha256-hu8b8JW28QGvH3lkVmIPltr5S8U6/hkRoJDlWA9kdcE=", + "lastModified": 1768490533, + "narHash": "sha256-Crf752Mpg956GHHUphH+WJn6yXfRCr9HY4jeNMSQSXE=", "owner": "caelestia-dots", "repo": "shell", - "rev": "3bb58e6da8f9bf465e6cd5517c46c4a0667c0e07", + "rev": "2d000c1052e50c6a1c36db91d998bc6fa4b1ba59", "type": "github" }, "original": { @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1768127708, - "narHash": "sha256-1Sm77VfZh3mU0F5OqKABNLWxOuDeHIlcFjsXeeiPazs=", + "lastModified": 1768305791, + "narHash": "sha256-AIdl6WAn9aymeaH/NvBj0H9qM+XuAuYbGMZaP0zcXAQ=", "owner": "nixos", "repo": "nixpkgs", - "rev": "ffbc9f8cbaacfb331b6017d5a5abb21a492c9a38", + "rev": "1412caf7bf9e660f2f962917c14b1ea1c3bc695e", "type": "github" }, "original": { From acfb6b0b4920b57d6b658f3bd6d6d58b2b0e7a39 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 17 Jan 2026 02:06:07 +0000 Subject: [PATCH 025/106] [CI] chore: update flake --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 2bd66aaf..0a142d59 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1768490533, - "narHash": "sha256-Crf752Mpg956GHHUphH+WJn6yXfRCr9HY4jeNMSQSXE=", + "lastModified": 1768561833, + "narHash": "sha256-WE5uqQTUtGDh199h/mLUdSN1rl9ui+dbWkjvQgtu/gg=", "owner": "caelestia-dots", "repo": "shell", - "rev": "2d000c1052e50c6a1c36db91d998bc6fa4b1ba59", + "rev": "d2c06587f5321ccf12feedd08d84fa11b9a2ff68", "type": "github" }, "original": { @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1768305791, - "narHash": "sha256-AIdl6WAn9aymeaH/NvBj0H9qM+XuAuYbGMZaP0zcXAQ=", + "lastModified": 1768564909, + "narHash": "sha256-Kell/SpJYVkHWMvnhqJz/8DqQg2b6PguxVWOuadbHCc=", "owner": "nixos", "repo": "nixpkgs", - "rev": "1412caf7bf9e660f2f962917c14b1ea1c3bc695e", + "rev": "e4bae1bd10c9c57b2cf517953ab70060a828ee6f", "type": "github" }, "original": { From adbc89329e646f3600e7e0d5022aa7f09ecaa5fd Mon Sep 17 00:00:00 2001 From: mj0x0 <93428389+mj0x0@users.noreply.github.com> Date: Sat, 17 Jan 2026 15:11:13 +0200 Subject: [PATCH 026/106] theme: ensure atomic writes for configuration files (#76) * fix: ensure atomic writes for configuration files * requested changes --- src/caelestia/utils/theme.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/caelestia/utils/theme.py b/src/caelestia/utils/theme.py index d8e648d7..a137a57c 100644 --- a/src/caelestia/utils/theme.py +++ b/src/caelestia/utils/theme.py @@ -2,6 +2,8 @@ import re import subprocess from pathlib import Path +import tempfile +import shutil from caelestia.utils.colour import get_dynamic_colours from caelestia.utils.logging import log_exception @@ -101,8 +103,11 @@ def gen_sequences(colours: dict[str, str]) -> str: def write_file(path: Path, content: str) -> None: path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(content) + with tempfile.NamedTemporaryFile("w") as f: + f.write(content) + f.flush() + shutil.move(f.name, path) @log_exception def apply_terms(sequences: str) -> None: From 6a8c5cfeb6628b837cd0b3480439cba3593609d8 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 18 Jan 2026 02:32:10 +0000 Subject: [PATCH 027/106] [CI] chore: update flake --- flake.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/flake.lock b/flake.lock index 0a142d59..d66ea6a2 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1768561833, - "narHash": "sha256-WE5uqQTUtGDh199h/mLUdSN1rl9ui+dbWkjvQgtu/gg=", + "lastModified": 1768700084, + "narHash": "sha256-G/RtxgpF4OHRWy82/MHmEClOq9sBn8tki6K6vCuPZvU=", "owner": "caelestia-dots", "repo": "shell", - "rev": "d2c06587f5321ccf12feedd08d84fa11b9a2ff68", + "rev": "408c523d257f5e22fd95229dd36e76f4b90439a2", "type": "github" }, "original": { @@ -46,11 +46,11 @@ ] }, "locked": { - "lastModified": 1768080170, - "narHash": "sha256-HYKRNShQe5YnnxLazQajB9JkAPGpVcUt9jQ3KwilITQ=", + "lastModified": 1768689040, + "narHash": "sha256-Tlnr5BulJcMers/cb+YvmBQW4nKHjdKo9loInJkyO2k=", "ref": "refs/heads/master", - "rev": "bcc3d4265e8b3ed2b17b801923905b60a3927823", - "revCount": 722, + "rev": "7a427ce1979ce7447e885c4f30129b40f3d466f5", + "revCount": 729, "type": "git", "url": "https://git.outfoxxed.me/outfoxxed/quickshell" }, From 7e7f74386eed4a17c48d1d0845ea05615e09bbd3 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 20 Jan 2026 02:12:10 +0000 Subject: [PATCH 028/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index d66ea6a2..9e66ba38 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1768700084, - "narHash": "sha256-G/RtxgpF4OHRWy82/MHmEClOq9sBn8tki6K6vCuPZvU=", + "lastModified": 1768799057, + "narHash": "sha256-6VLKEkFVZt1hbugt0WfvFFM9u0N/3uQ8vSNMtDBQ6T4=", "owner": "caelestia-dots", "repo": "shell", - "rev": "408c523d257f5e22fd95229dd36e76f4b90439a2", + "rev": "a17fe72be5acdd049f005c3809332192bf649af2", "type": "github" }, "original": { From 3354dc5c7b746ab9cb6f13c0a5c216bc6cb87b2d Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 21 Jan 2026 02:21:30 +0000 Subject: [PATCH 029/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 9e66ba38..46d0ff6f 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1768799057, - "narHash": "sha256-6VLKEkFVZt1hbugt0WfvFFM9u0N/3uQ8vSNMtDBQ6T4=", + "lastModified": 1768914728, + "narHash": "sha256-HITJhpTIF9SiWvsb9XjeFMrkYZNdahe/HgY3puxYvcA=", "owner": "caelestia-dots", "repo": "shell", - "rev": "a17fe72be5acdd049f005c3809332192bf649af2", + "rev": "2ddc367e4e12c13fc9499550fab62772408a6b47", "type": "github" }, "original": { From e6e9742126dd4ec07faf611f456372d594aee413 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 02:21:06 +0000 Subject: [PATCH 030/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 46d0ff6f..bbd2d45c 100644 --- a/flake.lock +++ b/flake.lock @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1768564909, - "narHash": "sha256-Kell/SpJYVkHWMvnhqJz/8DqQg2b6PguxVWOuadbHCc=", + "lastModified": 1768886240, + "narHash": "sha256-C2TjvwYZ2VDxYWeqvvJ5XPPp6U7H66zeJlRaErJKoEM=", "owner": "nixos", "repo": "nixpkgs", - "rev": "e4bae1bd10c9c57b2cf517953ab70060a828ee6f", + "rev": "80e4adbcf8992d3fd27ad4964fbb84907f9478b0", "type": "github" }, "original": { From b01aaec456f5415a0b5fafe45cae6e3527b4162e Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 23 Jan 2026 02:11:56 +0000 Subject: [PATCH 031/106] [CI] chore: update flake --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index bbd2d45c..f61b9fd3 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1768914728, - "narHash": "sha256-HITJhpTIF9SiWvsb9XjeFMrkYZNdahe/HgY3puxYvcA=", + "lastModified": 1769073714, + "narHash": "sha256-vppHLOKWw3ygroSlQ2oZ/evNIeXrBDl7cOPOyXZAh90=", "owner": "caelestia-dots", "repo": "shell", - "rev": "2ddc367e4e12c13fc9499550fab62772408a6b47", + "rev": "617f7a19f335be9e975dd001e262794636a6716f", "type": "github" }, "original": { @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1768886240, - "narHash": "sha256-C2TjvwYZ2VDxYWeqvvJ5XPPp6U7H66zeJlRaErJKoEM=", + "lastModified": 1769018530, + "narHash": "sha256-MJ27Cy2NtBEV5tsK+YraYr2g851f3Fl1LpNHDzDX15c=", "owner": "nixos", "repo": "nixpkgs", - "rev": "80e4adbcf8992d3fd27ad4964fbb84907f9478b0", + "rev": "88d3861acdd3d2f0e361767018218e51810df8a1", "type": "github" }, "original": { From 7718720ccb3636c3edc1c5547a5b6a727f445cff Mon Sep 17 00:00:00 2001 From: Kalagmitan <121934419+Kalagmitan@users.noreply.github.com> Date: Sat, 24 Jan 2026 11:45:32 +0800 Subject: [PATCH 032/106] theme: inject mode into user templates (#77) * theme: apply_user_templates accepts mode. * Some themes like those in NvChad require a mode to be supplied to work. Added a minimal change that makes apply_user_templates accept a mode parameter and replaces any {{ mode }} placeholder in a file with the actual mode. * theme: mode replace integrated with gen_replace_dynamic + Moved the {{ mode }} replacement logic to the gen_replace_dynamic function. * refactor: adjusted comment --- src/caelestia/utils/theme.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/caelestia/utils/theme.py b/src/caelestia/utils/theme.py index a137a57c..91d48a94 100644 --- a/src/caelestia/utils/theme.py +++ b/src/caelestia/utils/theme.py @@ -39,7 +39,7 @@ def gen_replace(colours: dict[str, str], template: Path, hash: bool = False) -> return template -def gen_replace_dynamic(colours: dict[str, str], template: Path) -> str: +def gen_replace_dynamic(colours: dict[str, str], template: Path, mode: str) -> str: def fill_colour(match: re.Match) -> str: data = match.group(1).strip().split(".") if len(data) != 2: @@ -50,10 +50,16 @@ def fill_colour(match: re.Match) -> str: return getattr(colours_dyn[col], form) # match atomic {{ . }} pairs - field = r"\{\{((?:(?!\{\{|\}\}).)*)\}\}" + dotField = r"\{\{((?:(?!\{\{|\}\}).)*)\}\}" + + # match {{ mode }} + modeField = r"\{\{\s*mode\s*\}\}" + colours_dyn = get_dynamic_colours(colours) template_content = template.read_text() - template_filled = re.sub(field, fill_colour, template_content) + + template_filled = re.sub(dotField, fill_colour, template_content) + template_filled = re.sub(modeField, mode, template_content) return template_filled @@ -229,13 +235,13 @@ def apply_cava(colours: dict[str, str]) -> None: @log_exception -def apply_user_templates(colours: dict[str, str]) -> None: +def apply_user_templates(colours: dict[str, str], mode: str) -> None: if not user_templates_dir.is_dir(): return for file in user_templates_dir.iterdir(): if file.is_file(): - content = gen_replace_dynamic(colours, file) + content = gen_replace_dynamic(colours, file, mode) write_file(theme_dir / file.name, content) @@ -272,4 +278,4 @@ def check(key: str) -> bool: apply_warp(colours, mode) if check("enableCava"): apply_cava(colours) - apply_user_templates(colours) + apply_user_templates(colours, mode) From 1a6eb203ca4d30884472398fa68d9da5b69a99d1 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 25 Jan 2026 02:35:40 +0000 Subject: [PATCH 033/106] [CI] chore: update flake --- flake.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/flake.lock b/flake.lock index f61b9fd3..1ac05d3e 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1769073714, - "narHash": "sha256-vppHLOKWw3ygroSlQ2oZ/evNIeXrBDl7cOPOyXZAh90=", + "lastModified": 1769305032, + "narHash": "sha256-Of4+4pnT+EmAl/DM1GYkpceX6yXw+QLU30WTec0ZypQ=", "owner": "caelestia-dots", "repo": "shell", - "rev": "617f7a19f335be9e975dd001e262794636a6716f", + "rev": "b33440684950acf0b3f089b789da370685d9396c", "type": "github" }, "original": { @@ -46,11 +46,11 @@ ] }, "locked": { - "lastModified": 1768689040, - "narHash": "sha256-Tlnr5BulJcMers/cb+YvmBQW4nKHjdKo9loInJkyO2k=", + "lastModified": 1768985439, + "narHash": "sha256-qkU4r+l+UPz4dutMMRZSin64HuVZkEv9iFpu9yMWVY0=", "ref": "refs/heads/master", - "rev": "7a427ce1979ce7447e885c4f30129b40f3d466f5", - "revCount": 729, + "rev": "191085a8821b35680bba16ce5411fc9dbe912237", + "revCount": 731, "type": "git", "url": "https://git.outfoxxed.me/outfoxxed/quickshell" }, From 0b78da66e3ae4c4e4631a79912a292d1be62df5e Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 02:35:07 +0000 Subject: [PATCH 034/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 1ac05d3e..b23be6d3 100644 --- a/flake.lock +++ b/flake.lock @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1769018530, - "narHash": "sha256-MJ27Cy2NtBEV5tsK+YraYr2g851f3Fl1LpNHDzDX15c=", + "lastModified": 1769170682, + "narHash": "sha256-oMmN1lVQU0F0W2k6OI3bgdzp2YOHWYUAw79qzDSjenU=", "owner": "nixos", "repo": "nixpkgs", - "rev": "88d3861acdd3d2f0e361767018218e51810df8a1", + "rev": "c5296fdd05cfa2c187990dd909864da9658df755", "type": "github" }, "original": { From 34f7e880ffb8b82e34800fd7a2f3d43f8decf235 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 30 Jan 2026 02:37:13 +0000 Subject: [PATCH 035/106] [CI] chore: update flake --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index b23be6d3..a16c85fa 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1769305032, - "narHash": "sha256-Of4+4pnT+EmAl/DM1GYkpceX6yXw+QLU30WTec0ZypQ=", + "lastModified": 1769733078, + "narHash": "sha256-Cu5b3bq8MoU8cZNjmjYM5mGF0G3CY2JmCfWj+gmNOqQ=", "owner": "caelestia-dots", "repo": "shell", - "rev": "b33440684950acf0b3f089b789da370685d9396c", + "rev": "39e38e8e76bd1f60970775d112f77dfc9781f82d", "type": "github" }, "original": { @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1769170682, - "narHash": "sha256-oMmN1lVQU0F0W2k6OI3bgdzp2YOHWYUAw79qzDSjenU=", + "lastModified": 1769461804, + "narHash": "sha256-msG8SU5WsBUfVVa/9RPLaymvi5bI8edTavbIq3vRlhI=", "owner": "nixos", "repo": "nixpkgs", - "rev": "c5296fdd05cfa2c187990dd909864da9658df755", + "rev": "bfc1b8a4574108ceef22f02bafcf6611380c100d", "type": "github" }, "original": { From 237252519dee8537b0c6ee0c09d6481c72a18a17 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 03:04:44 +0000 Subject: [PATCH 036/106] [CI] chore: update flake --- flake.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/flake.lock b/flake.lock index a16c85fa..a3c5c1e9 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1769733078, - "narHash": "sha256-Cu5b3bq8MoU8cZNjmjYM5mGF0G3CY2JmCfWj+gmNOqQ=", + "lastModified": 1769911585, + "narHash": "sha256-fc/o0hiTc16e53he7ytDaNsCI71xhJ9hkYl+zHOxp6o=", "owner": "caelestia-dots", "repo": "shell", - "rev": "39e38e8e76bd1f60970775d112f77dfc9781f82d", + "rev": "45b87645e20d9b472d0449415cd9f277dce21364", "type": "github" }, "original": { @@ -46,11 +46,11 @@ ] }, "locked": { - "lastModified": 1768985439, - "narHash": "sha256-qkU4r+l+UPz4dutMMRZSin64HuVZkEv9iFpu9yMWVY0=", + "lastModified": 1769593411, + "narHash": "sha256-WW00FaBiUmQyxvSbefvgxIjwf/WmRrEGBbwMHvW/7uQ=", "ref": "refs/heads/master", - "rev": "191085a8821b35680bba16ce5411fc9dbe912237", - "revCount": 731, + "rev": "1e4d804e7f3fa7465811030e8da2bf10d544426a", + "revCount": 732, "type": "git", "url": "https://git.outfoxxed.me/outfoxxed/quickshell" }, From a7d12d8496db64f3cb0d095b26c9337a95516ba7 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 02:53:33 +0000 Subject: [PATCH 037/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index a3c5c1e9..804bf5ed 100644 --- a/flake.lock +++ b/flake.lock @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1769461804, - "narHash": "sha256-msG8SU5WsBUfVVa/9RPLaymvi5bI8edTavbIq3vRlhI=", + "lastModified": 1769789167, + "narHash": "sha256-kKB3bqYJU5nzYeIROI82Ef9VtTbu4uA3YydSk/Bioa8=", "owner": "nixos", "repo": "nixpkgs", - "rev": "bfc1b8a4574108ceef22f02bafcf6611380c100d", + "rev": "62c8382960464ceb98ea593cb8321a2cf8f9e3e5", "type": "github" }, "original": { From 0b025b8a0b0bab6d474b629a8a7a3a0d22c38d73 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 02:42:43 +0000 Subject: [PATCH 038/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 804bf5ed..9b533ea4 100644 --- a/flake.lock +++ b/flake.lock @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1769789167, - "narHash": "sha256-kKB3bqYJU5nzYeIROI82Ef9VtTbu4uA3YydSk/Bioa8=", + "lastModified": 1770019141, + "narHash": "sha256-VKS4ZLNx4PNrABoB0L8KUpc1fE7CLpQXQs985tGfaCU=", "owner": "nixos", "repo": "nixpkgs", - "rev": "62c8382960464ceb98ea593cb8321a2cf8f9e3e5", + "rev": "cb369ef2efd432b3cdf8622b0ffc0a97a02f3137", "type": "github" }, "original": { From af15c4faed6e882353ffbfdc259b680b6fd2c823 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Feb 2026 02:38:06 +0000 Subject: [PATCH 039/106] [CI] chore: update flake --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 9b533ea4..30224954 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1769911585, - "narHash": "sha256-fc/o0hiTc16e53he7ytDaNsCI71xhJ9hkYl+zHOxp6o=", + "lastModified": 1770122420, + "narHash": "sha256-SWFov0EDEZIjFMMNKiwOpTIsbiKO4jE7LSO7L2Bv3zE=", "owner": "caelestia-dots", "repo": "shell", - "rev": "45b87645e20d9b472d0449415cd9f277dce21364", + "rev": "4c72e3e06bd58a31e16cc1588d94543069fbd00a", "type": "github" }, "original": { @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1770019141, - "narHash": "sha256-VKS4ZLNx4PNrABoB0L8KUpc1fE7CLpQXQs985tGfaCU=", + "lastModified": 1770115704, + "narHash": "sha256-KHFT9UWOF2yRPlAnSXQJh6uVcgNcWlFqqiAZ7OVlHNc=", "owner": "nixos", "repo": "nixpkgs", - "rev": "cb369ef2efd432b3cdf8622b0ffc0a97a02f3137", + "rev": "e6eae2ee2110f3d31110d5c222cd395303343b08", "type": "github" }, "original": { From 2bf40972ad0f3adf21ba0b7c3756e0d715156eae Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 5 Feb 2026 02:40:10 +0000 Subject: [PATCH 040/106] [CI] chore: update flake --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 30224954..285d2a95 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1770122420, - "narHash": "sha256-SWFov0EDEZIjFMMNKiwOpTIsbiKO4jE7LSO7L2Bv3zE=", + "lastModified": 1770192250, + "narHash": "sha256-L2aKAiPfm6REIQyoHxAu0BXuiR07MH523byykjUE2EA=", "owner": "caelestia-dots", "repo": "shell", - "rev": "4c72e3e06bd58a31e16cc1588d94543069fbd00a", + "rev": "7a41a85954a40366bd25ed4e33d1cd9146507ad4", "type": "github" }, "original": { @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1770115704, - "narHash": "sha256-KHFT9UWOF2yRPlAnSXQJh6uVcgNcWlFqqiAZ7OVlHNc=", + "lastModified": 1770181073, + "narHash": "sha256-ksTL7P9QC1WfZasNlaAdLOzqD8x5EPyods69YBqxSfk=", "owner": "nixos", "repo": "nixpkgs", - "rev": "e6eae2ee2110f3d31110d5c222cd395303343b08", + "rev": "bf922a59c5c9998a6584645f7d0de689512e444c", "type": "github" }, "original": { From 03035f1f1640184a519bcec18e6e3fbf5cedcaef Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 6 Feb 2026 02:39:29 +0000 Subject: [PATCH 041/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 285d2a95..e8c14200 100644 --- a/flake.lock +++ b/flake.lock @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1770181073, - "narHash": "sha256-ksTL7P9QC1WfZasNlaAdLOzqD8x5EPyods69YBqxSfk=", + "lastModified": 1770197578, + "narHash": "sha256-AYqlWrX09+HvGs8zM6ebZ1pwUqjkfpnv8mewYwAo+iM=", "owner": "nixos", "repo": "nixpkgs", - "rev": "bf922a59c5c9998a6584645f7d0de689512e444c", + "rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", "type": "github" }, "original": { From e97388f46d5ecd6e462e643f3f89486627ef26cd Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 8 Feb 2026 03:19:32 +0000 Subject: [PATCH 042/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index e8c14200..d1879841 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1770192250, - "narHash": "sha256-L2aKAiPfm6REIQyoHxAu0BXuiR07MH523byykjUE2EA=", + "lastModified": 1770516618, + "narHash": "sha256-wsLmha3769FTTPt5A7npa7lbln6tkmnps5ptugyxjNM=", "owner": "caelestia-dots", "repo": "shell", - "rev": "7a41a85954a40366bd25ed4e33d1cd9146507ad4", + "rev": "6ad2c9bbf9151265d6cbda4c34a2397776a3fd5b", "type": "github" }, "original": { From d62f643d6748f6e326ba24d529208afe9fa4f640 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 9 Feb 2026 02:54:39 +0000 Subject: [PATCH 043/106] [CI] chore: update flake --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index d1879841..a42ce350 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1770516618, - "narHash": "sha256-wsLmha3769FTTPt5A7npa7lbln6tkmnps5ptugyxjNM=", + "lastModified": 1770527631, + "narHash": "sha256-QFZAXF80GUrgdmgjMLPzhGOqjiRk4ukCEkpr0dehdnk=", "owner": "caelestia-dots", "repo": "shell", - "rev": "6ad2c9bbf9151265d6cbda4c34a2397776a3fd5b", + "rev": "5b2e1a6231af24472fb8ff3bfa7183a4f63c6ba7", "type": "github" }, "original": { @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1770197578, - "narHash": "sha256-AYqlWrX09+HvGs8zM6ebZ1pwUqjkfpnv8mewYwAo+iM=", + "lastModified": 1770562336, + "narHash": "sha256-ub1gpAONMFsT/GU2hV6ZWJjur8rJ6kKxdm9IlCT0j84=", "owner": "nixos", "repo": "nixpkgs", - "rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", + "rev": "d6c71932130818840fc8fe9509cf50be8c64634f", "type": "github" }, "original": { From 775aaf255ba877fc4d665add4dba9d5094235012 Mon Sep 17 00:00:00 2001 From: Chea Vuthearith <159639139+chea-vuthearith@users.noreply.github.com> Date: Tue, 10 Feb 2026 16:53:27 +0700 Subject: [PATCH 044/106] record: add arg to copy screen recording to clipboard (#83) --- completions/caelestia.fish | 1 + src/caelestia/subcommands/record.py | 1 + 2 files changed, 2 insertions(+) diff --git a/completions/caelestia.fish b/completions/caelestia.fish index 5257f3f6..80f177cb 100644 --- a/completions/caelestia.fish +++ b/completions/caelestia.fish @@ -105,6 +105,7 @@ complete -c caelestia -n "$seen screenshot" -s 'f' -l 'freeze' -d 'Freeze while # Record complete -c caelestia -n "$seen record" -s 'r' -l 'region' -d 'Capture region' complete -c caelestia -n "$seen record" -s 's' -l 'sound' -d 'Capture sound' +complete -c caelestia -n "$seen record" -s 'c' -l 'clipboard' -d 'Copy recording path to clipboard' # Clipboard complete -c caelestia -n "$seen clipboard" -s 'd' -l 'delete' -d 'Delete from cliboard history' diff --git a/src/caelestia/subcommands/record.py b/src/caelestia/subcommands/record.py index c63b0e91..b9d27973 100644 --- a/src/caelestia/subcommands/record.py +++ b/src/caelestia/subcommands/record.py @@ -1,3 +1,4 @@ +from pathlib import Path import json import re import shutil From 44a21714a94b870b28da480372a621bcb8b54dd1 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Tue, 10 Feb 2026 21:32:49 +1100 Subject: [PATCH 045/106] fix: xxx_paletteKeyColor rename in myc 3.0.0 Fixes caelestia-dots/shell#1112 --- src/caelestia/utils/material/generator.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/caelestia/utils/material/generator.py b/src/caelestia/utils/material/generator.py index 200fa7f1..fb7b75b1 100644 --- a/src/caelestia/utils/material/generator.py +++ b/src/caelestia/utils/material/generator.py @@ -172,10 +172,15 @@ def gen_scheme(scheme, primary: Hct) -> dict[str, str]: # Material colours primary_scheme = get_scheme(scheme.variant)(primary, not light, 0) - for colour in vars(MaterialDynamicColors).keys(): - colour_name = getattr(MaterialDynamicColors, colour) - if hasattr(colour_name, "get_hct"): - colours[colour] = colour_name.get_hct(primary_scheme) + dyn_colours = MaterialDynamicColors() + for colour in dyn_colours.all_colors: + colours[colour.name] = colour.get_hct(primary_scheme) + + # Backwards compatibility with old colour names + if "primaryPaletteKeyColor" in colours: # materialyoucolor-python >= 3.0.0 + for colour in "primary", "secondary", "tertiary", "neutral": + colours[f"{colour}_paletteKeyColor"] = colours[f"{colour}PaletteKeyColor"] + colours["neutral_variant_paletteKeyColor"] = colours["neutralVariantPaletteKeyColor"] # Harmonize terminal colours for i, hct in enumerate(light_gruvbox if light else dark_gruvbox): From 9228232ea4cd6e6356d861a1fd8ede5558aaf86e Mon Sep 17 00:00:00 2001 From: AteebXYZ <130910481+AteebXYZ@users.noreply.github.com> Date: Tue, 10 Feb 2026 13:37:58 +0300 Subject: [PATCH 046/106] fix: apply mode substitution after dynamic template fill (#84) --- src/caelestia/utils/theme.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/caelestia/utils/theme.py b/src/caelestia/utils/theme.py index 91d48a94..66665a89 100644 --- a/src/caelestia/utils/theme.py +++ b/src/caelestia/utils/theme.py @@ -58,8 +58,8 @@ def fill_colour(match: re.Match) -> str: colours_dyn = get_dynamic_colours(colours) template_content = template.read_text() - template_filled = re.sub(dotField, fill_colour, template_content) - template_filled = re.sub(modeField, mode, template_content) + template_filled = re.sub(dotField, fill_colour, template_content) + template_filled = re.sub(modeField, mode, template_filled) return template_filled From ff5a2f3c9ad79e6e0d181653bb69b73a3a73bc4e Mon Sep 17 00:00:00 2001 From: Nick <145419280+nicklany01@users.noreply.github.com> Date: Tue, 10 Feb 2026 21:02:18 +1000 Subject: [PATCH 047/106] nix: fix todoist command replacement in default.nix (#86) Target the command list specifically ["todoist"] instead of a global replace to avoid breaking the configuration key structure. --- default.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/default.nix b/default.nix index e8ba844d..dafab0d5 100644 --- a/default.nix +++ b/default.nix @@ -68,7 +68,7 @@ python3.pkgs.buildPythonApplication { # Use config bin instead of discord + fix todoist + fix app2unit substituteInPlace src/caelestia/subcommands/toggle.py \ --replace-fail 'discord' ${discordBin} \ - --replace-fail 'todoist' 'todoist.desktop'\ + --replace-fail '["todoist"]' '["todoist.desktop"]'\ --replace-fail 'app2unit' ${app2unit}/bin/app2unit # Use config style instead of darkly From 4daa4a63e7cb7607d74408996b98fe0b9947563a Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 02:54:26 +0000 Subject: [PATCH 048/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index a42ce350..45938b60 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1770527631, - "narHash": "sha256-QFZAXF80GUrgdmgjMLPzhGOqjiRk4ukCEkpr0dehdnk=", + "lastModified": 1770770663, + "narHash": "sha256-Vs+Pwzos8IRtlvkeGpdCeJi5rfmcpqajOqtk7eW7k9c=", "owner": "caelestia-dots", "repo": "shell", - "rev": "5b2e1a6231af24472fb8ff3bfa7183a4f63c6ba7", + "rev": "2e22a21defc26b7a24fb0a01a0882f8d33e344be", "type": "github" }, "original": { From d319feb8b4ddb676877bd6f35addfd41b2fbcf36 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 13 Feb 2026 02:50:31 +0000 Subject: [PATCH 049/106] [CI] chore: update flake --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 45938b60..d166dd7d 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1770770663, - "narHash": "sha256-Vs+Pwzos8IRtlvkeGpdCeJi5rfmcpqajOqtk7eW7k9c=", + "lastModified": 1770949235, + "narHash": "sha256-OFeud9FjaOk6xHp/9igYl/+Zw6FJDyZNrIDNi47gsG0=", "owner": "caelestia-dots", "repo": "shell", - "rev": "2e22a21defc26b7a24fb0a01a0882f8d33e344be", + "rev": "93e8880842b03e251bf59d1ba316f2393c68574f", "type": "github" }, "original": { @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1770562336, - "narHash": "sha256-ub1gpAONMFsT/GU2hV6ZWJjur8rJ6kKxdm9IlCT0j84=", + "lastModified": 1770841267, + "narHash": "sha256-9xejG0KoqsoKEGp2kVbXRlEYtFFcDTHjidiuX8hGO44=", "owner": "nixos", "repo": "nixpkgs", - "rev": "d6c71932130818840fc8fe9509cf50be8c64634f", + "rev": "ec7c70d12ce2fc37cb92aff673dcdca89d187bae", "type": "github" }, "original": { From 9e20fcf9974278539fb8d30817dca9a74b884917 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 14 Feb 2026 23:22:30 +1100 Subject: [PATCH 050/106] fix: compat for python-materialyoucolor < 3.0.0 --- src/caelestia/utils/material/generator.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/caelestia/utils/material/generator.py b/src/caelestia/utils/material/generator.py index fb7b75b1..93372577 100644 --- a/src/caelestia/utils/material/generator.py +++ b/src/caelestia/utils/material/generator.py @@ -172,9 +172,15 @@ def gen_scheme(scheme, primary: Hct) -> dict[str, str]: # Material colours primary_scheme = get_scheme(scheme.variant)(primary, not light, 0) - dyn_colours = MaterialDynamicColors() - for colour in dyn_colours.all_colors: - colours[colour.name] = colour.get_hct(primary_scheme) + if hasattr(MaterialDynamicColors, "all_colors"): # materialyoucolor-python >= 3.0.0 + dyn_colours = MaterialDynamicColors() + for colour in dyn_colours.all_colors: + colours[colour.name] = colour.get_hct(primary_scheme) + else: + for colour in vars(MaterialDynamicColors).keys(): + colour_name = getattr(MaterialDynamicColors, colour) + if hasattr(colour_name, "get_hct"): + colours[colour] = colour_name.get_hct(primary_scheme) # Backwards compatibility with old colour names if "primaryPaletteKeyColor" in colours: # materialyoucolor-python >= 3.0.0 From 2ab351ab576225e4542c5e4b81016d6eeb6a969c Mon Sep 17 00:00:00 2001 From: Robin Seger Date: Sat, 14 Feb 2026 13:23:33 +0100 Subject: [PATCH 051/106] feat: thunar & papirus-folders theming + new schemes (#80) * feat: GTK app theming system - Implemented custom.css import for user-managed app themes - process_app_themes() to dynamically update colors in imported CSS files - Inline comment markers for color replacement (e.g, /* accent-color */) - Papirus icon color syncing with weighted hue/saturation algorithm This allows users to create modular app themes that automatically update when the scheme/wallpaper changes Example usage: .app .element { color: #24BD5C; /* accent-color */ } .app .element:hover { background: rgba(36, 189, 92, 0.15); /* accent-color with 15% opacity */ } * feat: atomic theme changes with locking and mode-specific CSS - Implemented locking to prevent concurrent theme changes - Added mode-light/mode-dark CSS markers for dynamic property reordering - Made terminal writes and Papirus sync non-blocking to prevent hangs - Only save scheme.json after successful theme application Fixes race conditions during rapid theme switching and ensures Shell and GTK apps scheme stay in sync. * theme: added to color mapping for custom theming, new schemes * theme: quick fixes, cleanup * theme: include thunar.css as template, with new theming system * theme: modified GTK theming approach - Dropped comment targeted theming in favor for existing {{ }} replacement - [app].css.template file created for customization, bypassing built in default if present - Handling *.template for added templates to be parsed and added to import * theme: fixes for thunar.css * theme: remove .template file use * theme: path button color adjustment, non-active hover * fixes & cleanup * thunar css fixes * more css fixes * format * fix tab vert spacing --------- Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> --- README.md | 26 +++ .../data/schemes/darkgreen/hard/dark.txt | 12 +- .../data/schemes/darkgreen/medium/dark.txt | 12 +- .../data/schemes/dracula/medium/dark.txt | 110 +++++++++ .../data/schemes/everblush/medium/dark.txt | 110 +++++++++ .../data/schemes/everforest/hard/dark.txt | 110 +++++++++ .../data/schemes/everforest/medium/dark.txt | 110 +++++++++ .../data/schemes/everforest/medium/light.txt | 110 +++++++++ .../data/schemes/everforest/soft/dark.txt | 110 +++++++++ .../data/schemes/nord/medium/dark.txt | 110 +++++++++ .../data/schemes/solarized/medium/dark.txt | 110 +++++++++ .../data/schemes/tokyonight/medium/dark.txt | 110 +++++++++ src/caelestia/data/templates/gtk.css | 4 + src/caelestia/data/templates/thunar.css | 202 ++++++++++++++++ src/caelestia/utils/theme.py | 218 ++++++++++++++---- 15 files changed, 1423 insertions(+), 41 deletions(-) create mode 100644 src/caelestia/data/schemes/dracula/medium/dark.txt create mode 100644 src/caelestia/data/schemes/everblush/medium/dark.txt create mode 100644 src/caelestia/data/schemes/everforest/hard/dark.txt create mode 100644 src/caelestia/data/schemes/everforest/medium/dark.txt create mode 100644 src/caelestia/data/schemes/everforest/medium/light.txt create mode 100644 src/caelestia/data/schemes/everforest/soft/dark.txt create mode 100644 src/caelestia/data/schemes/nord/medium/dark.txt create mode 100644 src/caelestia/data/schemes/solarized/medium/dark.txt create mode 100644 src/caelestia/data/schemes/tokyonight/medium/dark.txt create mode 100644 src/caelestia/data/templates/thunar.css diff --git a/README.md b/README.md index 5c9e9608..7239cc34 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,32 @@ The main control script for the Caelestia dotfiles. +
Optional dependencies + +- [`papirus-folders`](https://github.com/PapirusDevelopmentTeam/papirus-folders) - automatic folder icon color syncing with theme + +> [!NOTE] +> For automatic Papirus folder icon color syncing, `papirus-folders` needs to be able to run with `sudo` without a password prompt. +> +> **Recommended** - Create a sudoers file: +> ```fish +> # Fish shell +> echo "$USER ALL=(ALL) NOPASSWD: "(which papirus-folders) | sudo tee /etc/sudoers.d/papirus-folders +> sudo chmod 440 /etc/sudoers.d/papirus-folders +> ``` +> ```sh +> # Bash/other shells +> echo "$USER ALL=(ALL) NOPASSWD: $(which papirus-folders)" | sudo tee /etc/sudoers.d/papirus-folders +> sudo chmod 440 /etc/sudoers.d/papirus-folders +> ``` +> +> **Alternatively** - Edit the main sudoers file by running `sudo visudo` and adding at the end: +> ``` +> your_username ALL=(ALL) NOPASSWD: /usr/bin/papirus-folders +> ``` + +
+ ## Installation ### Arch linux diff --git a/src/caelestia/data/schemes/darkgreen/hard/dark.txt b/src/caelestia/data/schemes/darkgreen/hard/dark.txt index 45ab558d..dea54c48 100644 --- a/src/caelestia/data/schemes/darkgreen/hard/dark.txt +++ b/src/caelestia/data/schemes/darkgreen/hard/dark.txt @@ -4,7 +4,7 @@ tertiary_paletteKeyColor 376942 neutral_paletteKeyColor 1E1E26 neutral_variant_paletteKeyColor 23252D background 23262D -onBackground 02200B +onBackground F5F5F6 surface 050505 surfaceDim 1E1E24 surfaceBright 1E1E24 @@ -82,6 +82,16 @@ sky cefb97 sapphire 85ef77 blue 65eea0 lavender 90f79e +klink 65eea0 +klinkSelection 65eea0 +kvisited 73fa90 +kvisitedSelection 73fa90 +knegative 8affab +knegativeSelection 8affab +kneutral d0f9f4 +kneutralSelection d0f9f4 +kpositive 8af797 +kpositiveSelection 8af797 text e0e3e4 subtext1 bec8cc subtext0 889296 diff --git a/src/caelestia/data/schemes/darkgreen/medium/dark.txt b/src/caelestia/data/schemes/darkgreen/medium/dark.txt index 30723ba0..1caff9e4 100644 --- a/src/caelestia/data/schemes/darkgreen/medium/dark.txt +++ b/src/caelestia/data/schemes/darkgreen/medium/dark.txt @@ -4,7 +4,7 @@ tertiary_paletteKeyColor 376942 neutral_paletteKeyColor 1E1E26 neutral_variant_paletteKeyColor 23252D background 23262D -onBackground 02200B +onBackground F5F5F6 surface 1E1E24 surfaceDim 1E1E24 surfaceBright 1E1E24 @@ -82,6 +82,16 @@ sky cefb97 sapphire 85ef77 blue 65eea0 lavender 90f79e +klink 65eea0 +klinkSelection 65eea0 +kvisited 73fa90 +kvisitedSelection 73fa90 +knegative 8affab +knegativeSelection 8affab +kneutral d0f9f4 +kneutralSelection d0f9f4 +kpositive 8af797 +kpositiveSelection 8af797 text e0e3e4 subtext1 bec8cc subtext0 889296 diff --git a/src/caelestia/data/schemes/dracula/medium/dark.txt b/src/caelestia/data/schemes/dracula/medium/dark.txt new file mode 100644 index 00000000..4195f452 --- /dev/null +++ b/src/caelestia/data/schemes/dracula/medium/dark.txt @@ -0,0 +1,110 @@ +primary_paletteKeyColor BD93F9 +secondary_paletteKeyColor 50FA7B +tertiary_paletteKeyColor FF79C6 +neutral_paletteKeyColor 282A36 +neutral_variant_paletteKeyColor 44475A +background 282A36 +onBackground F8F8F2 +surface 343746 +surfaceDim 21222C +surfaceBright 4D4F66 +surfaceContainerLowest 191A21 +surfaceContainerLow 3C3F4E +surfaceContainer 3E4153 +surfaceContainerHigh 4D4F66 +surfaceContainerHighest 565970 +onSurface F8F8F2 +surfaceVariant 3E4153 +onSurfaceVariant F8F8F2 +inverseSurface F8F8F2 +inverseOnSurface 282A36 +outline 6272A4 +outlineVariant 4D4F66 +shadow 000000 +scrim 000000 +surfaceTint BD93F9 +primary BD93F9 +onPrimary 282A36 +primaryContainer 4D4F66 +onPrimaryContainer BD93F9 +inversePrimary 9D73D9 +secondary 50FA7B +onSecondary 282A36 +secondaryContainer 4D4F66 +onSecondaryContainer 50FA7B +tertiary FF79C6 +onTertiary 282A36 +tertiaryContainer 4D4F66 +onTertiaryContainer FF79C6 +error FF5555 +onError 282A36 +errorContainer 4C3743 +onErrorContainer FF5555 +primaryFixed BD93F9 +primaryFixedDim 9D73D9 +onPrimaryFixed 282A36 +onPrimaryFixedVariant 3E4153 +secondaryFixed 50FA7B +secondaryFixedDim 30DA5B +onSecondaryFixed 282A36 +onSecondaryFixedVariant 3E4153 +tertiaryFixed FF79C6 +tertiaryFixedDim DF59A6 +onTertiaryFixed 282A36 +onTertiaryFixedVariant 3E4153 +term0 282A36 +term1 FF5555 +term2 50FA7B +term3 F1FA8C +term4 BD93F9 +term5 FF79C6 +term6 8BE9FD +term7 F8F8F2 +term8 6272A4 +term9 FF6E6E +term10 69FF94 +term11 FFFFA5 +term12 D6ACFF +term13 FF92DF +term14 A4FFFF +term15 FFFFFF +rosewater F8F8F2 +flamingo FFB86C +pink FF79C6 +mauve BD93F9 +red FF5555 +maroon FF6E6E +peach FFB86C +yellow F1FA8C +green 50FA7B +teal 8BE9FD +sky 8BE9FD +sapphire 8BE9FD +blue BD93F9 +lavender BD93F9 +klink BD93F9 +klinkSelection BD93F9 +kvisited FF79C6 +kvisitedSelection FF79C6 +knegative FF5555 +knegativeSelection FF5555 +kneutral F1FA8C +kneutralSelection F1FA8C +kpositive 50FA7B +kpositiveSelection 50FA7B +text F8F8F2 +subtext1 F8F8F2 +subtext0 E6E6E6 +overlay2 A0A0A0 +overlay1 8A8A8A +overlay0 6272A4 +surface2 3E4153 +surface1 343746 +surface0 282A36 +base 282A36 +mantle 21222C +crust 191A21 +success 50FA7B +onSuccess 282A36 +successContainer 4D4F66 +onSuccessContainer F8F8F2 diff --git a/src/caelestia/data/schemes/everblush/medium/dark.txt b/src/caelestia/data/schemes/everblush/medium/dark.txt new file mode 100644 index 00000000..fefd5ac4 --- /dev/null +++ b/src/caelestia/data/schemes/everblush/medium/dark.txt @@ -0,0 +1,110 @@ +primary_paletteKeyColor 8CCFB0 +secondary_paletteKeyColor E5C76B +tertiary_paletteKeyColor E5A5C5 +neutral_paletteKeyColor 2D3139 +neutral_variant_paletteKeyColor 3A3F4B +background 141B1E +onBackground E8E8E8 +surface 232A2D +surfaceDim 0F1416 +surfaceBright 3A4145 +surfaceContainerLowest 0A0E10 +surfaceContainerLow 2A3235 +surfaceContainer 2E3538 +surfaceContainerHigh 3A4145 +surfaceContainerHighest 434A4E +onSurface E8E8E8 +surfaceVariant 2E3538 +onSurfaceVariant B3B9BE +inverseSurface E8E8E8 +inverseOnSurface 141B1E +outline 8A8F94 +outlineVariant 3A4145 +shadow 000000 +scrim 000000 +surfaceTint 8CCFB0 +primary 8CCFB0 +onPrimary 141B1E +primaryContainer 3A4145 +onPrimaryContainer 8CCFB0 +inversePrimary 6FA98C +secondary E5C76B +onSecondary 141B1E +secondaryContainer 3A4145 +onSecondaryContainer E5C76B +tertiary E5A5C5 +onTertiary 141B1E +tertiaryContainer 3A4145 +onTertiaryContainer E5A5C5 +error E57474 +onError 141B1E +errorContainer 4A2C2C +onErrorContainer E57474 +primaryFixed 8CCFB0 +primaryFixedDim 6FA98C +onPrimaryFixed 141B1E +onPrimaryFixedVariant 3A3F4B +secondaryFixed E5C76B +secondaryFixedDim C4A855 +onSecondaryFixed 141B1E +onSecondaryFixedVariant 3A3F4B +tertiaryFixed E5A5C5 +tertiaryFixedDim C888A4 +onTertiaryFixed 141B1E +onTertiaryFixedVariant 3A3F4B +term0 141B1E +term1 E57474 +term2 8CCFB0 +term3 E5C76B +term4 67B0E8 +term5 C47FD5 +term6 6CBFBF +term7 E8E8E8 +term8 8A8F94 +term9 E57474 +term10 8CCFB0 +term11 E5C76B +term12 67B0E8 +term13 C47FD5 +term14 6CBFBF +term15 E8E8E8 +rosewater E8E8E8 +flamingo E5A5C5 +pink E5A5C5 +mauve C47FD5 +red E57474 +maroon E57474 +peach E59A84 +yellow E5C76B +green 8CCFB0 +teal 6CBFBF +sky 67B0E8 +sapphire 67B0E8 +blue 67B0E8 +lavender 67B0E8 +klink 67B0E8 +klinkSelection 67B0E8 +kvisited C47FD5 +kvisitedSelection C47FD5 +knegative E57474 +knegativeSelection E57474 +kneutral E5C76B +kneutralSelection E5C76B +kpositive 8CCFB0 +kpositiveSelection 8CCFB0 +text E8E8E8 +subtext1 B3B9BE +subtext0 8A8F94 +overlay2 7A7F84 +overlay1 6A6F74 +overlay0 5A5F64 +surface2 2E3538 +surface1 232A2D +surface0 1A2023 +base 141B1E +mantle 0F1416 +crust 0A0E10 +success 8CCFB0 +onSuccess 141B1E +successContainer 3A4145 +onSuccessContainer E8E8E8 diff --git a/src/caelestia/data/schemes/everforest/hard/dark.txt b/src/caelestia/data/schemes/everforest/hard/dark.txt new file mode 100644 index 00000000..d4823504 --- /dev/null +++ b/src/caelestia/data/schemes/everforest/hard/dark.txt @@ -0,0 +1,110 @@ +primary_paletteKeyColor 7FBBB3 +secondary_paletteKeyColor 83C092 +tertiary_paletteKeyColor A7C080 +neutral_paletteKeyColor 2E383C +neutral_variant_paletteKeyColor 374145 +background 1E2326 +onBackground D3C6AA +surface 252B2E +surfaceDim 15191C +surfaceBright 343E43 +surfaceContainerLowest 11161A +surfaceContainerLow 2A3338 +surfaceContainer 2E383C +surfaceContainerHigh 343E43 +surfaceContainerHighest 3A4448 +onSurface D3C6AA +surfaceVariant 374145 +onSurfaceVariant 9DA9A0 +inverseSurface D3C6AA +inverseOnSurface 1E2326 +outline 859289 +outlineVariant 414B50 +shadow 000000 +scrim 000000 +surfaceTint 7FBBB3 +primary 7FBBB3 +onPrimary 1E2326 +primaryContainer 414B50 +onPrimaryContainer A7C080 +inversePrimary 5A9A8F +secondary 83C092 +onSecondary 1E2326 +secondaryContainer 414B50 +onSecondaryContainer A7C080 +tertiary A7C080 +onTertiary 1E2326 +tertiaryContainer 414B50 +onTertiaryContainer D3C6AA +error E67E80 +onError 1E2326 +errorContainer 4C3743 +onErrorContainer E67E80 +primaryFixed 7FBBB3 +primaryFixedDim 5A9A8F +onPrimaryFixed 1E2326 +onPrimaryFixedVariant 374145 +secondaryFixed 83C092 +secondaryFixedDim 5F8C6F +onSecondaryFixed 1E2326 +onSecondaryFixedVariant 374145 +tertiaryFixed A7C080 +tertiaryFixedDim 7F9D5F +onTertiaryFixed 1E2326 +onTertiaryFixedVariant 374145 +term0 1E2326 +term1 E67E80 +term2 A7C080 +term3 DBBC7F +term4 7FBBB3 +term5 D699B6 +term6 83C092 +term7 D3C6AA +term8 859289 +term9 E67E80 +term10 A7C080 +term11 DBBC7F +term12 7FBBB3 +term13 D699B6 +term14 83C092 +term15 D3C6AA +rosewater D3C6AA +flamingo D699B6 +pink D699B6 +mauve D699B6 +red E67E80 +maroon E67E80 +peach E69875 +yellow DBBC7F +green A7C080 +teal 83C092 +sky 7FBBB3 +sapphire 7FBBB3 +blue 7FBBB3 +lavender 7FBBB3 +klink 7FBBB3 +klinkSelection 7FBBB3 +kvisited 83C092 +kvisitedSelection 83C092 +knegative E67E80 +knegativeSelection E67E80 +kneutral DBBC7F +kneutralSelection DBBC7F +kpositive A7C080 +kpositiveSelection A7C080 +text D3C6AA +subtext1 9DA9A0 +subtext0 859289 +overlay2 7A8478 +overlay1 6F7A6F +overlay0 5F6A5F +surface2 2E383C +surface1 252B2E +surface0 1E2326 +base 1E2326 +mantle 15191C +crust 11161A +success A7C080 +onSuccess 1E2326 +successContainer 414B50 +onSuccessContainer D3C6AA diff --git a/src/caelestia/data/schemes/everforest/medium/dark.txt b/src/caelestia/data/schemes/everforest/medium/dark.txt new file mode 100644 index 00000000..86167436 --- /dev/null +++ b/src/caelestia/data/schemes/everforest/medium/dark.txt @@ -0,0 +1,110 @@ +primary_paletteKeyColor 7FBBB3 +secondary_paletteKeyColor 83C092 +tertiary_paletteKeyColor A7C080 +neutral_paletteKeyColor 2E383C +neutral_variant_paletteKeyColor 374145 +background 2D353B +onBackground D3C6AA +surface 343F44 +surfaceDim 232A2E +surfaceBright 475258 +surfaceContainerLowest 1E2326 +surfaceContainerLow 3B474E +surfaceContainer 3D484D +surfaceContainerHigh 475258 +surfaceContainerHighest 4C5258 +onSurface D3C6AA +surfaceVariant 3D484D +onSurfaceVariant 9DA9A0 +inverseSurface D3C6AA +inverseOnSurface 2D353B +outline 859289 +outlineVariant 475258 +shadow 000000 +scrim 000000 +surfaceTint 7FBBB3 +primary 7FBBB3 +onPrimary 2D353B +primaryContainer 475258 +onPrimaryContainer A7C080 +inversePrimary 5A9A8F +secondary 83C092 +onSecondary 2D353B +secondaryContainer 475258 +onSecondaryContainer A7C080 +tertiary A7C080 +onTertiary 2D353B +tertiaryContainer 475258 +onTertiaryContainer D3C6AA +error E67E80 +onError 2D353B +errorContainer 4C3743 +onErrorContainer E67E80 +primaryFixed 7FBBB3 +primaryFixedDim 5A9A8F +onPrimaryFixed 2D353B +onPrimaryFixedVariant 374145 +secondaryFixed 83C092 +secondaryFixedDim 5F8C6F +onSecondaryFixed 2D353B +onSecondaryFixedVariant 374145 +tertiaryFixed A7C080 +tertiaryFixedDim 7F9D5F +onTertiaryFixed 2D353B +onTertiaryFixedVariant 374145 +term0 2D353B +term1 E67E80 +term2 A7C080 +term3 DBBC7F +term4 7FBBB3 +term5 D699B6 +term6 83C092 +term7 D3C6AA +term8 859289 +term9 E67E80 +term10 A7C080 +term11 DBBC7F +term12 7FBBB3 +term13 D699B6 +term14 83C092 +term15 D3C6AA +rosewater D3C6AA +flamingo D699B6 +pink D699B6 +mauve D699B6 +red E67E80 +maroon E67E80 +peach E69875 +yellow DBBC7F +green A7C080 +teal 83C092 +sky 7FBBB3 +sapphire 7FBBB3 +blue 7FBBB3 +lavender 7FBBB3 +klink 7FBBB3 +klinkSelection 7FBBB3 +kvisited 83C092 +kvisitedSelection 83C092 +knegative E67E80 +knegativeSelection E67E80 +kneutral DBBC7F +kneutralSelection DBBC7F +kpositive A7C080 +kpositiveSelection A7C080 +text D3C6AA +subtext1 9DA9A0 +subtext0 859289 +overlay2 7A8478 +overlay1 6F7A6F +overlay0 5F6A5F +surface2 3D484D +surface1 343F44 +surface0 2D353B +base 2D353B +mantle 232A2E +crust 1E2326 +success A7C080 +onSuccess 2D353B +successContainer 475258 +onSuccessContainer D3C6AA diff --git a/src/caelestia/data/schemes/everforest/medium/light.txt b/src/caelestia/data/schemes/everforest/medium/light.txt new file mode 100644 index 00000000..92fd1dc1 --- /dev/null +++ b/src/caelestia/data/schemes/everforest/medium/light.txt @@ -0,0 +1,110 @@ +primary_paletteKeyColor 3A94C5 +secondary_paletteKeyColor 35A77C +tertiary_paletteKeyColor 8DA101 +neutral_paletteKeyColor E6E2CC +neutral_variant_paletteKeyColor E0DCC7 +background FDF6E3 +onBackground 5C6A72 +surface F3EAD3 +surfaceDim FDF6E3 +surfaceBright FFFBF0 +surfaceContainerLowest FFFBF0 +surfaceContainerLow FDF6E3 +surfaceContainer F3EAD3 +surfaceContainerHigh EAE4CA +surfaceContainerHighest E0DCC7 +onSurface 5C6A72 +surfaceVariant EAE4CA +onSurfaceVariant 6F7C84 +inverseSurface 5C6A72 +inverseOnSurface FDF6E3 +outline 939F91 +outlineVariant E0DCC7 +shadow 000000 +scrim 000000 +surfaceTint 3A94C5 +primary 3A94C5 +onPrimary FFFBF0 +primaryContainer E0DCC7 +onPrimaryContainer 8DA101 +inversePrimary 5FAFD7 +secondary 35A77C +onSecondary FFFBF0 +secondaryContainer E0DCC7 +onSecondaryContainer 8DA101 +tertiary 8DA101 +onTertiary FFFBF0 +tertiaryContainer E0DCC7 +onTertiaryContainer 5C6A72 +error F85552 +onError FFFBF0 +errorContainer E6E2CC +onErrorContainer F85552 +primaryFixed 3A94C5 +primaryFixedDim 5FAFD7 +onPrimaryFixed FFFBF0 +onPrimaryFixedVariant E0DCC7 +secondaryFixed 35A77C +secondaryFixedDim 5FC198 +onSecondaryFixed FFFBF0 +onSecondaryFixedVariant E0DCC7 +tertiaryFixed 8DA101 +tertiaryFixedDim A7C080 +onTertiaryFixed FFFBF0 +onTertiaryFixedVariant E0DCC7 +term0 5C6A72 +term1 F85552 +term2 8DA101 +term3 DFA000 +term4 3A94C5 +term5 DF69BA +term6 35A77C +term7 5C6A72 +term8 939F91 +term9 F85552 +term10 8DA101 +term11 DFA000 +term12 3A94C5 +term13 DF69BA +term14 35A77C +term15 5C6A72 +rosewater 5C6A72 +flamingo DF69BA +pink DF69BA +mauve DF69BA +red F85552 +maroon F85552 +peach E66868 +yellow DFA000 +green 8DA101 +teal 35A77C +sky 3A94C5 +sapphire 3A94C5 +blue 3A94C5 +lavender 3A94C5 +klink 3A94C5 +klinkSelection 3A94C5 +kvisited 35A77C +kvisitedSelection 35A77C +knegative F85552 +knegativeSelection F85552 +kneutral DFA000 +kneutralSelection DFA000 +kpositive 8DA101 +kpositiveSelection 8DA101 +text 5C6A72 +subtext1 6F7C84 +subtext0 939F91 +overlay2 A6B0A0 +overlay1 B9C0B0 +overlay0 CCD3C2 +surface2 EAE4CA +surface1 F3EAD3 +surface0 FDF6E3 +base FDF6E3 +mantle FFFBF0 +crust FFFEF9 +success 8DA101 +onSuccess FFFBF0 +successContainer E0DCC7 +onSuccessContainer 5C6A72 diff --git a/src/caelestia/data/schemes/everforest/soft/dark.txt b/src/caelestia/data/schemes/everforest/soft/dark.txt new file mode 100644 index 00000000..817425b6 --- /dev/null +++ b/src/caelestia/data/schemes/everforest/soft/dark.txt @@ -0,0 +1,110 @@ +primary_paletteKeyColor 7FBBB3 +secondary_paletteKeyColor 83C092 +tertiary_paletteKeyColor A7C080 +neutral_paletteKeyColor 2E383C +neutral_variant_paletteKeyColor 374145 +background 323C41 +onBackground D3C6AA +surface 3A454A +surfaceDim 282F34 +surfaceBright 4D585D +surfaceContainerLowest 232A2E +surfaceContainerLow 414D54 +surfaceContainer 434E53 +surfaceContainerHigh 4D585D +surfaceContainerHighest 525C61 +onSurface D3C6AA +surfaceVariant 434E53 +onSurfaceVariant 9DA9A0 +inverseSurface D3C6AA +inverseOnSurface 323C41 +outline 859289 +outlineVariant 4D585D +shadow 000000 +scrim 000000 +surfaceTint 7FBBB3 +primary 7FBBB3 +onPrimary 323C41 +primaryContainer 4D585D +onPrimaryContainer A7C080 +inversePrimary 5A9A8F +secondary 83C092 +onSecondary 323C41 +secondaryContainer 4D585D +onSecondaryContainer A7C080 +tertiary A7C080 +onTertiary 323C41 +tertiaryContainer 4D585D +onTertiaryContainer D3C6AA +error E67E80 +onError 323C41 +errorContainer 4C3743 +onErrorContainer E67E80 +primaryFixed 7FBBB3 +primaryFixedDim 5A9A8F +onPrimaryFixed 323C41 +onPrimaryFixedVariant 374145 +secondaryFixed 83C092 +secondaryFixedDim 5F8C6F +onSecondaryFixed 323C41 +onSecondaryFixedVariant 374145 +tertiaryFixed A7C080 +tertiaryFixedDim 7F9D5F +onTertiaryFixed 323C41 +onTertiaryFixedVariant 374145 +term0 323C41 +term1 E67E80 +term2 A7C080 +term3 DBBC7F +term4 7FBBB3 +term5 D699B6 +term6 83C092 +term7 D3C6AA +term8 859289 +term9 E67E80 +term10 A7C080 +term11 DBBC7F +term12 7FBBB3 +term13 D699B6 +term14 83C092 +term15 D3C6AA +rosewater D3C6AA +flamingo D699B6 +pink D699B6 +mauve D699B6 +red E67E80 +maroon E67E80 +peach E69875 +yellow DBBC7F +green A7C080 +teal 83C092 +sky 7FBBB3 +sapphire 7FBBB3 +blue 7FBBB3 +lavender 7FBBB3 +klink 7FBBB3 +klinkSelection 7FBBB3 +kvisited 83C092 +kvisitedSelection 83C092 +knegative E67E80 +knegativeSelection E67E80 +kneutral DBBC7F +kneutralSelection DBBC7F +kpositive A7C080 +kpositiveSelection A7C080 +text D3C6AA +subtext1 9DA9A0 +subtext0 859289 +overlay2 7A8478 +overlay1 6F7A6F +overlay0 5F6A5F +surface2 434E53 +surface1 3A454A +surface0 323C41 +base 323C41 +mantle 282F34 +crust 232A2E +success A7C080 +onSuccess 323C41 +successContainer 4D585D +onSuccessContainer D3C6AA diff --git a/src/caelestia/data/schemes/nord/medium/dark.txt b/src/caelestia/data/schemes/nord/medium/dark.txt new file mode 100644 index 00000000..9e0cc02d --- /dev/null +++ b/src/caelestia/data/schemes/nord/medium/dark.txt @@ -0,0 +1,110 @@ +primary_paletteKeyColor 88C0D0 +secondary_paletteKeyColor 81A1C1 +tertiary_paletteKeyColor 5E81AC +neutral_paletteKeyColor 3B4252 +neutral_variant_paletteKeyColor 434C5E +background 2E3440 +onBackground ECEFF4 +surface 3B4252 +surfaceDim 242933 +surfaceBright 4C566A +surfaceContainerLowest 1F232C +surfaceContainerLow 424A5E +surfaceContainer 434C5E +surfaceContainerHigh 4C566A +surfaceContainerHighest 55606E +onSurface ECEFF4 +surfaceVariant 434C5E +onSurfaceVariant D8DEE9 +inverseSurface ECEFF4 +inverseOnSurface 2E3440 +outline 616E88 +outlineVariant 4C566A +shadow 000000 +scrim 000000 +surfaceTint 88C0D0 +primary 88C0D0 +onPrimary 2E3440 +primaryContainer 4C566A +onPrimaryContainer 88C0D0 +inversePrimary 6FA3B3 +secondary 81A1C1 +onSecondary 2E3440 +secondaryContainer 4C566A +onSecondaryContainer 81A1C1 +tertiary 5E81AC +onTertiary 2E3440 +tertiaryContainer 4C566A +onTertiaryContainer 5E81AC +error BF616A +onError 2E3440 +errorContainer 4C3743 +onErrorContainer BF616A +primaryFixed 88C0D0 +primaryFixedDim 6FA3B3 +onPrimaryFixed 2E3440 +onPrimaryFixedVariant 434C5E +secondaryFixed 81A1C1 +secondaryFixedDim 6A84A4 +onSecondaryFixed 2E3440 +onSecondaryFixedVariant 434C5E +tertiaryFixed 5E81AC +tertiaryFixedDim 4A6A8F +onTertiaryFixed 2E3440 +onTertiaryFixedVariant 434C5E +term0 3B4252 +term1 BF616A +term2 A3BE8C +term3 EBCB8B +term4 81A1C1 +term5 B48EAD +term6 88C0D0 +term7 E5E9F0 +term8 4C566A +term9 BF616A +term10 A3BE8C +term11 EBCB8B +term12 81A1C1 +term13 B48EAD +term14 8FBCBB +term15 ECEFF4 +rosewater ECEFF4 +flamingo B48EAD +pink B48EAD +mauve B48EAD +red BF616A +maroon BF616A +peach D08770 +yellow EBCB8B +green A3BE8C +teal 8FBCBB +sky 88C0D0 +sapphire 81A1C1 +blue 5E81AC +lavender 5E81AC +klink 88C0D0 +klinkSelection 88C0D0 +kvisited 81A1C1 +kvisitedSelection 81A1C1 +knegative BF616A +knegativeSelection BF616A +kneutral EBCB8B +kneutralSelection EBCB8B +kpositive A3BE8C +kpositiveSelection A3BE8C +text ECEFF4 +subtext1 D8DEE9 +subtext0 616E88 +overlay2 5A677E +overlay1 4F5B73 +overlay0 434C5E +surface2 434C5E +surface1 3B4252 +surface0 2E3440 +base 2E3440 +mantle 242933 +crust 1F232C +success A3BE8C +onSuccess 2E3440 +successContainer 4C566A +onSuccessContainer ECEFF4 diff --git a/src/caelestia/data/schemes/solarized/medium/dark.txt b/src/caelestia/data/schemes/solarized/medium/dark.txt new file mode 100644 index 00000000..7000788f --- /dev/null +++ b/src/caelestia/data/schemes/solarized/medium/dark.txt @@ -0,0 +1,110 @@ +primary_paletteKeyColor 268BD2 +secondary_paletteKeyColor 2AA198 +tertiary_paletteKeyColor 6C71C4 +neutral_paletteKeyColor 002B36 +neutral_variant_paletteKeyColor 073642 +background 002B36 +onBackground FDF6E3 +surface 073642 +surfaceDim 001F29 +surfaceBright 0D4250 +surfaceContainerLowest 00151D +surfaceContainerLow 0A404E +surfaceContainer 094B59 +surfaceContainerHigh 0D4250 +surfaceContainerHighest 11505E +onSurface FDF6E3 +surfaceVariant 094B59 +onSurfaceVariant 93A1A1 +inverseSurface FDF6E3 +inverseOnSurface 002B36 +outline 586E75 +outlineVariant 0D4250 +shadow 000000 +scrim 000000 +surfaceTint 268BD2 +primary 268BD2 +onPrimary 002B36 +primaryContainer 0D4250 +onPrimaryContainer 268BD2 +inversePrimary 2075B2 +secondary 2AA198 +onSecondary 002B36 +secondaryContainer 0D4250 +onSecondaryContainer 2AA198 +tertiary 6C71C4 +onTertiary 002B36 +tertiaryContainer 0D4250 +onTertiaryContainer 6C71C4 +error DC322F +onError 002B36 +errorContainer 4C3743 +onErrorContainer DC322F +primaryFixed 268BD2 +primaryFixedDim 2075B2 +onPrimaryFixed 002B36 +onPrimaryFixedVariant 094B59 +secondaryFixed 2AA198 +secondaryFixedDim 228178 +onSecondaryFixed 002B36 +onSecondaryFixedVariant 094B59 +tertiaryFixed 6C71C4 +tertiaryFixedDim 5C61A4 +onTertiaryFixed 002B36 +onTertiaryFixedVariant 094B59 +term0 002B36 +term1 DC322F +term2 859900 +term3 B58900 +term4 268BD2 +term5 D33682 +term6 2AA198 +term7 EEE8D5 +term8 586E75 +term9 CB4B16 +term10 859900 +term11 B58900 +term12 268BD2 +term13 6C71C4 +term14 2AA198 +term15 FDF6E3 +rosewater FDF6E3 +flamingo EEE8D5 +pink D33682 +mauve 6C71C4 +red DC322F +maroon CB4B16 +peach CB4B16 +yellow B58900 +green 859900 +teal 2AA198 +sky 2AA198 +sapphire 268BD2 +blue 268BD2 +lavender 6C71C4 +klink 268BD2 +klinkSelection 268BD2 +kvisited 6C71C4 +kvisitedSelection 6C71C4 +knegative DC322F +knegativeSelection DC322F +kneutral B58900 +kneutralSelection B58900 +kpositive 859900 +kpositiveSelection 859900 +text FDF6E3 +subtext1 93A1A1 +subtext0 839496 +overlay2 657B83 +overlay1 586E75 +overlay0 073642 +surface2 094B59 +surface1 073642 +surface0 002B36 +base 002B36 +mantle 001F29 +crust 00151D +success 859900 +onSuccess 002B36 +successContainer 0D4250 +onSuccessContainer FDF6E3 diff --git a/src/caelestia/data/schemes/tokyonight/medium/dark.txt b/src/caelestia/data/schemes/tokyonight/medium/dark.txt new file mode 100644 index 00000000..0fbb87ac --- /dev/null +++ b/src/caelestia/data/schemes/tokyonight/medium/dark.txt @@ -0,0 +1,110 @@ +primary_paletteKeyColor 7AA2F7 +secondary_paletteKeyColor 9ECE6A +tertiary_paletteKeyColor BB9AF7 +neutral_paletteKeyColor 1A1B26 +neutral_variant_paletteKeyColor 292E42 +background 1A1B26 +onBackground C0CAF5 +surface 24283B +surfaceDim 16161E +surfaceBright 3B4261 +surfaceContainerLowest 0F0F14 +surfaceContainerLow 2B3048 +surfaceContainer 2A2F41 +surfaceContainerHigh 3B4261 +surfaceContainerHighest 414868 +onSurface C0CAF5 +surfaceVariant 2A2F41 +onSurfaceVariant A9B1D6 +inverseSurface C0CAF5 +inverseOnSurface 1A1B26 +outline 565F89 +outlineVariant 3B4261 +shadow 000000 +scrim 000000 +surfaceTint 7AA2F7 +primary 7AA2F7 +onPrimary 1A1B26 +primaryContainer 3B4261 +onPrimaryContainer 7AA2F7 +inversePrimary 5A7FD7 +secondary 9ECE6A +onSecondary 1A1B26 +secondaryContainer 3B4261 +onSecondaryContainer 9ECE6A +tertiary BB9AF7 +onTertiary 1A1B26 +tertiaryContainer 3B4261 +onTertiaryContainer BB9AF7 +error F7768E +onError 1A1B26 +errorContainer 4C3743 +onErrorContainer F7768E +primaryFixed 7AA2F7 +primaryFixedDim 5A7FD7 +onPrimaryFixed 1A1B26 +onPrimaryFixedVariant 2A2F41 +secondaryFixed 9ECE6A +secondaryFixedDim 7EAE4A +onSecondaryFixed 1A1B26 +onSecondaryFixedVariant 2A2F41 +tertiaryFixed BB9AF7 +tertiaryFixedDim 9B7AD7 +onTertiaryFixed 1A1B26 +onTertiaryFixedVariant 2A2F41 +term0 1A1B26 +term1 F7768E +term2 9ECE6A +term3 E0AF68 +term4 7AA2F7 +term5 BB9AF7 +term6 7DCFFF +term7 C0CAF5 +term8 565F89 +term9 F7768E +term10 9ECE6A +term11 E0AF68 +term12 7AA2F7 +term13 BB9AF7 +term14 7DCFFF +term15 C0CAF5 +rosewater C0CAF5 +flamingo BB9AF7 +pink F7768E +mauve BB9AF7 +red F7768E +maroon E0AF68 +peach FF9E64 +yellow E0AF68 +green 9ECE6A +teal 1ABC9C +sky 7DCFFF +sapphire 2AC3DE +blue 7AA2F7 +lavender 7DCFFF +klink 7AA2F7 +klinkSelection 7AA2F7 +kvisited BB9AF7 +kvisitedSelection BB9AF7 +knegative F7768E +knegativeSelection F7768E +kneutral E0AF68 +kneutralSelection E0AF68 +kpositive 9ECE6A +kpositiveSelection 9ECE6A +text C0CAF5 +subtext1 A9B1D6 +subtext0 9AA5CE +overlay2 787C99 +overlay1 696D85 +overlay0 565F89 +surface2 2A2F41 +surface1 24283B +surface0 1A1B26 +base 1A1B26 +mantle 16161E +crust 0F0F14 +success 9ECE6A +onSuccess 1A1B26 +successContainer 3B4261 +onSuccessContainer C0CAF5 diff --git a/src/caelestia/data/templates/gtk.css b/src/caelestia/data/templates/gtk.css index bc9e5581..35d16a11 100644 --- a/src/caelestia/data/templates/gtk.css +++ b/src/caelestia/data/templates/gtk.css @@ -15,3 +15,7 @@ @define-color sidebar_fg_color @window_fg_color; @define-color sidebar_border_color @window_bg_color; @define-color sidebar_backdrop_color @window_bg_color; +@define-color theme_selected_bg_color alpha(@accent_color, 0.15); +@define-color theme_selected_fg_color @primary; + +@import "thunar.css"; diff --git a/src/caelestia/data/templates/thunar.css b/src/caelestia/data/templates/thunar.css new file mode 100644 index 00000000..548a9e0e --- /dev/null +++ b/src/caelestia/data/templates/thunar.css @@ -0,0 +1,202 @@ +/* Thunar theme */ + +/* ============================================================================= + Global Resets + ============================================================================= */ + +.thunar * { + outline: none; + border: none; +} + +/* ============================================================================= + Window & Background + ============================================================================= */ + +.thunar.background { + background: {{ $surface }}; +} + +.thunar .titlebar { + background: inherit; +} + +.thunar .titlebutton.close { + margin: 0 15px 0 0; +} + +/* ============================================================================= + Layout Containers + ============================================================================= */ + +/* Paned separator between sidebar and main view */ +.thunar paned > separator { + min-width: 4px; + margin-right: -7px; + margin-left: -7px; + background: none; + background-image: none; + box-shadow: none; +} + + +/* Main file view frame */ +.thunar .frame.standard-view { + padding: 10px; + margin: 10px 15px 0 0; + border-radius: 15px; + background-color: {{ $surfaceContainerLow }}; + animation: fading 400ms ease forwards; + opacity: 0; + animation-delay: 250ms; +} + +.thunar .frame.standard-view .view:not(.rubberband), +.thunar .frame.standard-view .view *:not(.rubberband) { + background-color: transparent; +} + +.thunar .frame.standard-view .view *:selected { + color: {{ $primary }}; +} + +.thunar .rubberband { + background-color: alpha({{ $primary }}, 0.15); + border: 1px solid alpha({{ $primary }}, 0.15); +} + + +/* Tabs */ +.thunar header.top { + background: none; + padding: 0 10px 0 0; + margin: 3px 0 -3px -2px; +} + +.thunar header.top tabs .reorderable-page { + margin: 0; + transition: all ease 300ms; +} +.thunar header.top tabs .reorderable-page + .reorderable-page { + margin: 0 0 0 10px; +} + +.thunar header.top tabs .reorderable-page:hover { + background-color: alpha({{ $primary }}, 0.08); + +} +.thunar header.top tabs .reorderable-page:checked { + color: {{ $primary }}; + background-color: alpha({{ $primary }}, 0.15); + +} + +/* ============================================================================= + Sidebar Navigation + ============================================================================= */ + +.thunar .sidebar { + padding: 0 20px; + background: none; + animation: fading 600ms ease forwards; + animation-delay: 100ms; + opacity: 0; +} + +.thunar .sidebar .view { + padding: 8px 4px; + border-radius: 10px; + background: none; + transition: all ease 300ms; +} + +.thunar .sidebar .view:hover { + background: alpha({{ $onSurface }}, 0.1); +} + +.thunar .sidebar .view:selected { + background: alpha({{ $primary }}, 0.15); + color: {{ $primary }}; +} + +/* ============================================================================= + Path Bar & Location Buttons + ============================================================================= */ + +.thunar .path-bar-button { + margin: 0; + padding: 8px 5px; + transition: all ease 0.4s; +} + +.thunar .location-button.toggle:checked, +.thunar .path-bar-button.toggle:checked { + padding: 8px 25px; + background: alpha({{ $primary }}, 0.15); + color: {{ $primary }}; + box-shadow: none; +} + +.thunar .location-button.path-bar-button:not(:checked) { + background-color: {{ $surfaceContainerLow }}; +} + +.thunar .location-button.path-bar-button:not(:checked):hover { + background: alpha({{ $primary }}, 0.08); + color: alpha({{ $primary }}, 0.8); +} + +.thunar .location-button.toggle+.location-button.toggle:checked { + margin-left: 0px; + padding: 0 25px; +} + +.thunar .titlebar { + padding: 15px 0 5px 0; +} + +/* ============================================================================= + Buttons + ============================================================================= */ + +.thunar button.toggle:checked { + color: {{ $primary }}; +} + +.thunar .image-button { + padding: 8px; + margin: 0 0 0 8px; + transition: all ease 0.4s; +} + +/* ============================================================================= + Status Bar + ============================================================================= */ + +.thunar statusbar { + background-color: {{ $surfaceContainerLow }}; + border-radius: 15px; + padding: 10px 10px; + margin: 15px 5px 15px -10px; + color: {{ $onSurface }}; +} + + +/* ============================================================================= + Image preview + ============================================================================= */ + +.thunar box.vertical .image { + margin: 15px; +} + + +/* ============================================================================= + Animation + ============================================================================= */ + +@keyframes fading { + to { + opacity: 1; + } +} diff --git a/src/caelestia/utils/theme.py b/src/caelestia/utils/theme.py index 66665a89..dc4db628 100644 --- a/src/caelestia/utils/theme.py +++ b/src/caelestia/utils/theme.py @@ -4,6 +4,8 @@ from pathlib import Path import tempfile import shutil +import fcntl +import sys from caelestia.utils.colour import get_dynamic_colours from caelestia.utils.logging import log_exception @@ -58,7 +60,7 @@ def fill_colour(match: re.Match) -> str: 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 @@ -125,9 +127,15 @@ def apply_terms(sequences: str) -> None: for pt in pts_path.iterdir(): if pt.name.isdigit(): try: - with pt.open("a") as f: - f.write(sequences) - except PermissionError: + # 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()) + finally: + os.close(fd) + except (PermissionError, OSError, BlockingIOError): + # Skip terminals that are busy, closed, or inaccessible pass @@ -180,15 +188,130 @@ def apply_htop(colours: dict[str, str]) -> None: subprocess.run(["killall", "-USR2", "htop"], stderr=subprocess.DEVNULL) +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 + ) + 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: + color = "black" + elif brightness < 170: + color = "grey" + else: + color = "white" + # Medium-low saturation with high brightness = pale variants + elif saturation < 60 and brightness > 180: + use_pale = True + 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 + ) + except Exception: + pass + + +def _determine_hue_color(r: int, g: int, b: int, brightness: int, use_pale: bool) -> str: + if b > r and b > g: + # Blue dominant + 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: + return "blue" + elif r > g: + return "violet" + else: + return "cyan" + elif r_ratio > 60 and r > g: + return "violet" + elif g_ratio > 60 and g > r: + return "cyan" + else: + return "blue" + elif r > g and r > b: + # Red dominant + if g > b + 30: + # Orange/yellow-ish/brown + rg_ratio = (g * 100) // r if r > 0 else 0 + if use_pale: + if rg_ratio > 70 and brightness < 220: + return "palebrown" + else: + return "paleorange" + else: + if rg_ratio > 70 and brightness < 180: + return "brown" + else: + return "orange" + elif b > g + 20: + return "pink" + else: + return "pink" if use_pale else "red" + elif g > r and g > b: + # Green dominant + if r > b + 30: + return "yellow" + else: + return "green" + else: + return "grey" + + @log_exception def apply_gtk(colours: dict[str, str], mode: str) -> None: - template = gen_replace(colours, templates_dir / "gtk.css", hash=True) - write_file(config_dir / "gtk-3.0/gtk.css", template) - write_file(config_dir / "gtk-4.0/gtk.css", template) + 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) + write_file(gtk_config_dir / "thunar.css", thunar_template) 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 @@ -246,36 +369,53 @@ def apply_user_templates(colours: dict[str, str], mode: str) -> None: 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: - cfg = json.loads(user_config_path.read_text())["theme"] - except (FileNotFoundError, json.JSONDecodeError, KeyError): - cfg = {} - - def check(key: str) -> bool: - return cfg[key] if key in cfg else True - - if check("enableTerm"): - apply_terms(gen_sequences(colours)) - if check("enableHypr"): - apply_hypr(gen_conf(colours)) - if check("enableDiscord"): - apply_discord(gen_scss(colours)) - if check("enableSpicetify"): - apply_spicetify(colours, mode) - if check("enableFuzzel"): - apply_fuzzel(colours) - if check("enableBtop"): - apply_btop(colours) - if check("enableNvtop"): - apply_nvtop(colours) - if check("enableHtop"): - apply_htop(colours) - if check("enableGtk"): - apply_gtk(colours, mode) - if check("enableQt"): - apply_qt(colours, mode) - if check("enableWarp"): - apply_warp(colours, mode) - if check("enableCava"): - apply_cava(colours) - apply_user_templates(colours, mode) + 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): + cfg = {} + + def check(key: str) -> bool: + return cfg[key] if key in cfg else True + + if check("enableTerm"): + apply_terms(gen_sequences(colours)) + if check("enableHypr"): + apply_hypr(gen_conf(colours)) + if check("enableDiscord"): + apply_discord(gen_scss(colours)) + if check("enableSpicetify"): + apply_spicetify(colours, mode) + if check("enableFuzzel"): + apply_fuzzel(colours) + if check("enableBtop"): + apply_btop(colours) + if check("enableNvtop"): + apply_nvtop(colours) + if check("enableHtop"): + apply_htop(colours) + if check("enableGtk"): + apply_gtk(colours, mode) + if check("enableQt"): + apply_qt(colours, mode) + if check("enableWarp"): + apply_warp(colours, mode) + if check("enableCava"): + apply_cava(colours) + apply_user_templates(colours, mode) + + finally: + try: + lock_file.unlink() + except FileNotFoundError: + pass From 1d4fdfca976a51fe484cd5ae290a436880529557 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 15 Feb 2026 00:24:14 +1100 Subject: [PATCH 052/106] feat: add dynamic hard flavour Someone requested idk who tho --- src/caelestia/utils/material/__init__.py | 2 +- src/caelestia/utils/material/generator.py | 20 +++++++++++++++++++- src/caelestia/utils/scheme.py | 4 +++- src/caelestia/utils/wallpaper.py | 4 ++-- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/caelestia/utils/material/__init__.py b/src/caelestia/utils/material/__init__.py index 88e855fc..b9aabab8 100644 --- a/src/caelestia/utils/material/__init__.py +++ b/src/caelestia/utils/material/__init__.py @@ -31,7 +31,7 @@ def get_colours_for_image(image: Path | str = wallpaper_thumbnail_path, scheme=N scheme = get_scheme() cache_base = scheme_cache_dir / compute_hash(image) - cache = (cache_base / scheme.variant / scheme.mode).with_suffix(".json") + cache = (cache_base / scheme.variant / scheme.flavour / scheme.mode).with_suffix(".json") try: with cache.open("r") as f: diff --git a/src/caelestia/utils/material/generator.py b/src/caelestia/utils/material/generator.py index 93372577..a8de4e2c 100644 --- a/src/caelestia/utils/material/generator.py +++ b/src/caelestia/utils/material/generator.py @@ -142,7 +142,7 @@ def lighten(colour: Hct, amount: float) -> Hct: def darken(colour: Hct, amount: float) -> Hct: diff = colour.tone * amount - return Hct.from_hct(colour.hue, colour.chroma + diff / 5, colour.tone - diff) + return Hct.from_hct(colour.hue, colour.chroma - diff / 5, colour.tone - diff) def get_scheme(scheme: str) -> DynamicScheme: @@ -216,6 +216,12 @@ def gen_scheme(scheme, primary: Hct) -> dict[str, str]: for name, hct in colours.items(): colours[name].chroma -= 15 + # Darken surfaces for hard flavour + if scheme.flavour == "hard": + for colour in "background", *(k for k in colours.keys() if k.startswith("surface")): + colours[colour] = lighten(colours[colour], 0.4) if light else darken(colours[colour], 0.8) + colours["term0"] = lighten(colours["term0"], 0.4) if light else darken(colours["term0"], 0.9) + # FIXME: deprecated stuff colours["text"] = colours["onBackground"] colours["subtext1"] = colours["onSurfaceVariant"] @@ -230,6 +236,18 @@ def gen_scheme(scheme, primary: Hct) -> dict[str, str]: colours["mantle"] = darken(colours["surface"], 0.03) colours["crust"] = darken(colours["surface"], 0.05) + # More darkening if hard flavour + if scheme.flavour == "hard": + for colour in "base", "mantle", "crust": + colours[colour] = lighten(colours[colour], 0.4) if light else darken(colours[colour], 0.9) + for i in range(3): + colours[f"overlay{i}"] = ( + lighten(colours[f"overlay{i}"], 0.4) if light else darken(colours[f"overlay{i}"], 0.8) + ) + colours[f"surface{i}"] = ( + lighten(colours[f"surface{i}"], 0.4) if light else darken(colours[f"surface{i}"], 0.8) + ) + # For debugging # print("\n".join(["{}: \x1b[48;2;{};{};{}m \x1b[0m".format(n, *c.to_rgba()[:3]) for n, c in colours.items()])) diff --git a/src/caelestia/utils/scheme.py b/src/caelestia/utils/scheme.py index 31fb77a4..57a5e0b6 100644 --- a/src/caelestia/utils/scheme.py +++ b/src/caelestia/utils/scheme.py @@ -229,7 +229,9 @@ def get_scheme_flavours(name: str = None) -> list[str]: if name is None: name = get_scheme().name - return ["default"] if name == "dynamic" else [f.name for f in (scheme_data_dir / name).iterdir() if f.is_dir()] + return ( + ["default", "hard"] if name == "dynamic" else [f.name for f in (scheme_data_dir / name).iterdir() if f.is_dir()] + ) def get_scheme_modes(name: str = None, flavour: str = None) -> list[str]: diff --git a/src/caelestia/utils/wallpaper.py b/src/caelestia/utils/wallpaper.py index 327da5c4..5564db65 100644 --- a/src/caelestia/utils/wallpaper.py +++ b/src/caelestia/utils/wallpaper.py @@ -106,7 +106,7 @@ def get_colours_for_wall(wall: Path | str, no_smart: bool) -> None: scheme = Scheme( { "name": name, - "flavour": "default", + "flavour": scheme.flavour, "mode": smart_opts["mode"], "variant": smart_opts["variant"], "colours": scheme.colours, @@ -115,7 +115,7 @@ def get_colours_for_wall(wall: Path | str, no_smart: bool) -> None: return { "name": name, - "flavour": "default", + "flavour": scheme.flavour, "mode": scheme.mode, "variant": scheme.variant, "colours": get_colours_for_image(get_thumb(wall, cache), scheme), From 93041e7bf6931bab5e4b4784379a915f97d0339b Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 15 Feb 2026 02:56:44 +0000 Subject: [PATCH 053/106] [CI] chore: update flake --- flake.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/flake.lock b/flake.lock index d166dd7d..f0890491 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1770949235, - "narHash": "sha256-OFeud9FjaOk6xHp/9igYl/+Zw6FJDyZNrIDNi47gsG0=", + "lastModified": 1771120934, + "narHash": "sha256-CnE8v42+SU7uLjTit453knxuwsMxZixEPU4s/6JANjs=", "owner": "caelestia-dots", "repo": "shell", - "rev": "93e8880842b03e251bf59d1ba316f2393c68574f", + "rev": "3a7309294cd4575e60aeb5e153d346313b16f7d9", "type": "github" }, "original": { @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1770841267, - "narHash": "sha256-9xejG0KoqsoKEGp2kVbXRlEYtFFcDTHjidiuX8hGO44=", + "lastModified": 1771008912, + "narHash": "sha256-gf2AmWVTs8lEq7z/3ZAsgnZDhWIckkb+ZnAo5RzSxJg=", "owner": "nixos", "repo": "nixpkgs", - "rev": "ec7c70d12ce2fc37cb92aff673dcdca89d187bae", + "rev": "a82ccc39b39b621151d6732718e3e250109076fa", "type": "github" }, "original": { @@ -46,11 +46,11 @@ ] }, "locked": { - "lastModified": 1769593411, - "narHash": "sha256-WW00FaBiUmQyxvSbefvgxIjwf/WmRrEGBbwMHvW/7uQ=", + "lastModified": 1770693276, + "narHash": "sha256-ngXnN5YXu+f45+QGYNN/VEBMQmcBCYGRCqwaK8cxY1s=", "ref": "refs/heads/master", - "rev": "1e4d804e7f3fa7465811030e8da2bf10d544426a", - "revCount": 732, + "rev": "dacfa9de829ac7cb173825f593236bf2c21f637e", + "revCount": 735, "type": "git", "url": "https://git.outfoxxed.me/outfoxxed/quickshell" }, From 2021bd7d975096536c06117495d1684a72e7648f Mon Sep 17 00:00:00 2001 From: Nathachou <83581717+HANTALE-59@users.noreply.github.com> Date: Tue, 17 Feb 2026 01:36:42 +0100 Subject: [PATCH 054/106] feat: scheme support for Pandora Minecraft launcher (#87) * added pandora theme template json * feat: add support for Pandora theme integration --- src/caelestia/data/templates/pandora.json | 162 ++++++++++++++++++++++ src/caelestia/utils/theme.py | 8 ++ 2 files changed, 170 insertions(+) create mode 100644 src/caelestia/data/templates/pandora.json diff --git a/src/caelestia/data/templates/pandora.json b/src/caelestia/data/templates/pandora.json new file mode 100644 index 00000000..c105c482 --- /dev/null +++ b/src/caelestia/data/templates/pandora.json @@ -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 }}" + } + } + } + } + ] +} diff --git a/src/caelestia/utils/theme.py b/src/caelestia/utils/theme.py index dc4db628..7d174330 100644 --- a/src/caelestia/utils/theme.py +++ b/src/caelestia/utils/theme.py @@ -155,6 +155,12 @@ 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) + template = template.replace("{{ $mode }}", mode) + write_file(data_dir / "PandoraLauncher/themes/caelestia.json", template) + @log_exception def apply_spicetify(colours: dict[str, str], mode: str) -> None: @@ -396,6 +402,8 @@ def check(key: str) -> bool: apply_discord(gen_scss(colours)) if check("enableSpicetify"): apply_spicetify(colours, mode) + if check("enablePandora"): + apply_pandora(colours, mode) if check("enableFuzzel"): apply_fuzzel(colours) if check("enableBtop"): From 6f713c6070cd9502ae7ee88f688143c2e3d03faf Mon Sep 17 00:00:00 2001 From: Soramane <61896496+soramanew@users.noreply.github.com> Date: Wed, 18 Feb 2026 11:18:15 +1100 Subject: [PATCH 055/106] fix: dynamic scheme import --- src/caelestia/utils/material/generator.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/caelestia/utils/material/generator.py b/src/caelestia/utils/material/generator.py index a8de4e2c..f67febdf 100644 --- a/src/caelestia/utils/material/generator.py +++ b/src/caelestia/utils/material/generator.py @@ -1,8 +1,6 @@ from materialyoucolor.blend import Blend -from materialyoucolor.dynamiccolor.material_dynamic_colors import ( - DynamicScheme, - MaterialDynamicColors, -) +from materialyoucolor.dynamiccolor.dynamic_scheme import DynamicScheme +from materialyoucolor.dynamiccolor.material_dynamic_colors import MaterialDynamicColors from materialyoucolor.hct import Hct from materialyoucolor.scheme.scheme_content import SchemeContent from materialyoucolor.scheme.scheme_expressive import SchemeExpressive From bec59b85209bb0e38fd82e1e26838dab90be4fb8 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 02:45:57 +0000 Subject: [PATCH 056/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index f0890491..9bd4e847 100644 --- a/flake.lock +++ b/flake.lock @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1771008912, - "narHash": "sha256-gf2AmWVTs8lEq7z/3ZAsgnZDhWIckkb+ZnAo5RzSxJg=", + "lastModified": 1771369470, + "narHash": "sha256-0NBlEBKkN3lufyvFegY4TYv5mCNHbi5OmBDrzihbBMQ=", "owner": "nixos", "repo": "nixpkgs", - "rev": "a82ccc39b39b621151d6732718e3e250109076fa", + "rev": "0182a361324364ae3f436a63005877674cf45efb", "type": "github" }, "original": { From 12683d0024fff515aa1cb0b9c52db53e5100bae6 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 02:40:44 +0000 Subject: [PATCH 057/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 9bd4e847..82278630 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1771120934, - "narHash": "sha256-CnE8v42+SU7uLjTit453knxuwsMxZixEPU4s/6JANjs=", + "lastModified": 1771502402, + "narHash": "sha256-w7AWGc+t+XpzrpMUuJpPAJ6Ihj7MfxdW4xoS4EMwlik=", "owner": "caelestia-dots", "repo": "shell", - "rev": "3a7309294cd4575e60aeb5e153d346313b16f7d9", + "rev": "1313b899ad9e0aba73aadedb249da1e5dfbf1486", "type": "github" }, "original": { From 97a3942f30c4676d1b1e45187d05e604b2e78a8c Mon Sep 17 00:00:00 2001 From: Unrectified <83581717+Unrectified@users.noreply.github.com> Date: Fri, 20 Feb 2026 14:17:16 +0100 Subject: [PATCH 058/106] 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> --- src/caelestia/utils/wallpaper.py | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/caelestia/utils/wallpaper.py b/src/caelestia/utils/wallpaper.py index 5564db65..c470b9c6 100644 --- a/src/caelestia/utils/wallpaper.py +++ b/src/caelestia/utils/wallpaper.py @@ -24,7 +24,7 @@ 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: @@ -99,6 +99,9 @@ def get_colours_for_wall(wall: Path | str, no_smart: bool) -> None: scheme = get_scheme() cache = wallpapers_cache_dir / compute_hash(wall) + if wall.suffix.lower() == ".gif" + wall = convert_gif(wall) + name = "dynamic" if not no_smart: @@ -121,6 +124,24 @@ 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" + + 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: # Make path absolute @@ -129,6 +150,9 @@ def set_wallpaper(wall: Path | str, no_smart: bool) -> None: if not is_valid_image(wall): 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 wallpaper_path_path.parent.mkdir(parents=True, exist_ok=True) wallpaper_path_path.write_text(str(wall)) @@ -136,10 +160,10 @@ def set_wallpaper(wall: Path | str, no_smart: bool) -> None: wallpaper_link_path.unlink(missing_ok=True) 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 - thumb = get_thumb(wall, cache) + thumb = get_thumb(wall_cache, cache) wallpaper_thumbnail_path.parent.mkdir(parents=True, exist_ok=True) wallpaper_thumbnail_path.unlink(missing_ok=True) wallpaper_thumbnail_path.symlink_to(thumb) @@ -148,7 +172,7 @@ def set_wallpaper(wall: Path | str, no_smart: bool) -> None: # Change mode and variant based on wallpaper colour 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.variant = smart_opts["variant"] From 999aad81389b1765b8acda3389e701bf3d6d02c7 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 21 Feb 2026 00:52:12 +1100 Subject: [PATCH 059/106] fix: missing colon --- src/caelestia/utils/wallpaper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/caelestia/utils/wallpaper.py b/src/caelestia/utils/wallpaper.py index c470b9c6..6699823b 100644 --- a/src/caelestia/utils/wallpaper.py +++ b/src/caelestia/utils/wallpaper.py @@ -99,7 +99,7 @@ def get_colours_for_wall(wall: Path | str, no_smart: bool) -> None: scheme = get_scheme() cache = wallpapers_cache_dir / compute_hash(wall) - if wall.suffix.lower() == ".gif" + if wall.suffix.lower() == ".gif": wall = convert_gif(wall) name = "dynamic" From e8333123db1135a26084c5219c32491d572422e6 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 21 Feb 2026 02:33:51 +0000 Subject: [PATCH 060/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 82278630..dccdb992 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1771502402, - "narHash": "sha256-w7AWGc+t+XpzrpMUuJpPAJ6Ihj7MfxdW4xoS4EMwlik=", + "lastModified": 1771569129, + "narHash": "sha256-YPWfRQOt/exrM994a7ijasPtUKBumWFnUU3iTFbghLk=", "owner": "caelestia-dots", "repo": "shell", - "rev": "1313b899ad9e0aba73aadedb249da1e5dfbf1486", + "rev": "4be8fc9693e439c487f091413289b782d78130e7", "type": "github" }, "original": { From 7e43dc7dd7677e398bcd0ea49b1370c2a7619417 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 22 Feb 2026 02:46:26 +0000 Subject: [PATCH 061/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index dccdb992..0fbea26f 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1771569129, - "narHash": "sha256-YPWfRQOt/exrM994a7ijasPtUKBumWFnUU3iTFbghLk=", + "lastModified": 1771725071, + "narHash": "sha256-70QlV4vXdJfp+xBzM+JyzZLz9XFTBIWP369lb8YDn9Y=", "owner": "caelestia-dots", "repo": "shell", - "rev": "4be8fc9693e439c487f091413289b782d78130e7", + "rev": "dc7af39c909e47b19ac582a1eec39ee60f0dcce1", "type": "github" }, "original": { From a6d4c74ea3593d16f9e76a97ab979b8432b576b4 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 02:51:37 +0000 Subject: [PATCH 062/106] [CI] chore: update flake --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 0fbea26f..52ace6f3 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1771725071, - "narHash": "sha256-70QlV4vXdJfp+xBzM+JyzZLz9XFTBIWP369lb8YDn9Y=", + "lastModified": 1771916719, + "narHash": "sha256-ciqlUWBoW63bep7WnpFgsBnhGDf/nTm3FVoQABdu68A=", "owner": "caelestia-dots", "repo": "shell", - "rev": "dc7af39c909e47b19ac582a1eec39ee60f0dcce1", + "rev": "71f291f79bf7c35ad7db2c0061efc80cf768426a", "type": "github" }, "original": { @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1771369470, - "narHash": "sha256-0NBlEBKkN3lufyvFegY4TYv5mCNHbi5OmBDrzihbBMQ=", + "lastModified": 1771848320, + "narHash": "sha256-0MAd+0mun3K/Ns8JATeHT1sX28faLII5hVLq0L3BdZU=", "owner": "nixos", "repo": "nixpkgs", - "rev": "0182a361324364ae3f436a63005877674cf45efb", + "rev": "2fc6539b481e1d2569f25f8799236694180c0993", "type": "github" }, "original": { From c3d9f2dde65f64c12f82c4179de2861af782a314 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 1 Mar 2026 03:00:32 +0000 Subject: [PATCH 063/106] [CI] chore: update flake --- flake.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/flake.lock b/flake.lock index 52ace6f3..6abc7162 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1771916719, - "narHash": "sha256-ciqlUWBoW63bep7WnpFgsBnhGDf/nTm3FVoQABdu68A=", + "lastModified": 1772330657, + "narHash": "sha256-cWblprYsDUeAWA57xAqxIjNxXvDI/rqYn6TFp2OPi/k=", "owner": "caelestia-dots", "repo": "shell", - "rev": "71f291f79bf7c35ad7db2c0061efc80cf768426a", + "rev": "278fd4a4ed1bfb42c3fe197ff38b587539c012aa", "type": "github" }, "original": { @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1771848320, - "narHash": "sha256-0MAd+0mun3K/Ns8JATeHT1sX28faLII5hVLq0L3BdZU=", + "lastModified": 1772198003, + "narHash": "sha256-I45esRSssFtJ8p/gLHUZ1OUaaTaVLluNkABkk6arQwE=", "owner": "nixos", "repo": "nixpkgs", - "rev": "2fc6539b481e1d2569f25f8799236694180c0993", + "rev": "dd9b079222d43e1943b6ebd802f04fd959dc8e61", "type": "github" }, "original": { @@ -46,11 +46,11 @@ ] }, "locked": { - "lastModified": 1770693276, - "narHash": "sha256-ngXnN5YXu+f45+QGYNN/VEBMQmcBCYGRCqwaK8cxY1s=", + "lastModified": 1771926182, + "narHash": "sha256-QbXuSLhiSxOq6ydBL3+KGe1aiYWBW+e3J6qjJZaRMq0=", "ref": "refs/heads/master", - "rev": "dacfa9de829ac7cb173825f593236bf2c21f637e", - "revCount": 735, + "rev": "cddb4f061bab495f4473ca5f2c571b6c710efef7", + "revCount": 744, "type": "git", "url": "https://git.outfoxxed.me/outfoxxed/quickshell" }, From 0238e15cf92707f08a3b7f581c1c31a9958d7ae3 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Tue, 3 Mar 2026 00:59:38 +1100 Subject: [PATCH 064/106] fix: dynamic scheme import <3.0.0 compat --- src/caelestia/utils/material/generator.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/caelestia/utils/material/generator.py b/src/caelestia/utils/material/generator.py index f67febdf..384fdd1a 100644 --- a/src/caelestia/utils/material/generator.py +++ b/src/caelestia/utils/material/generator.py @@ -1,5 +1,4 @@ from materialyoucolor.blend import Blend -from materialyoucolor.dynamiccolor.dynamic_scheme import DynamicScheme from materialyoucolor.dynamiccolor.material_dynamic_colors import MaterialDynamicColors from materialyoucolor.hct import Hct from materialyoucolor.scheme.scheme_content import SchemeContent @@ -13,6 +12,11 @@ from materialyoucolor.scheme.scheme_vibrant import SchemeVibrant 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: return Hct.from_int(int(f"0xFF{hex}", 16)) From 71931aaaa282c40c908106d2663db4952f2f61ee Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 02:43:42 +0000 Subject: [PATCH 065/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 6abc7162..97e44441 100644 --- a/flake.lock +++ b/flake.lock @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1772198003, - "narHash": "sha256-I45esRSssFtJ8p/gLHUZ1OUaaTaVLluNkABkk6arQwE=", + "lastModified": 1772433332, + "narHash": "sha256-izhTDFKsg6KeVBxJS9EblGeQ8y+O8eCa6RcW874vxEc=", "owner": "nixos", "repo": "nixpkgs", - "rev": "dd9b079222d43e1943b6ebd802f04fd959dc8e61", + "rev": "cf59864ef8aa2e178cccedbe2c178185b0365705", "type": "github" }, "original": { From cf37f6a71624cf750fa431af744fe7785ec14f28 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 02:36:02 +0000 Subject: [PATCH 066/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 97e44441..fa8db716 100644 --- a/flake.lock +++ b/flake.lock @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1772433332, - "narHash": "sha256-izhTDFKsg6KeVBxJS9EblGeQ8y+O8eCa6RcW874vxEc=", + "lastModified": 1772542754, + "narHash": "sha256-WGV2hy+VIeQsYXpsLjdr4GvHv5eECMISX1zKLTedhdg=", "owner": "nixos", "repo": "nixpkgs", - "rev": "cf59864ef8aa2e178cccedbe2c178185b0365705", + "rev": "8c809a146a140c5c8806f13399592dbcb1bb5dc4", "type": "github" }, "original": { From 67a3e8f522b6f0dbd9b4e2cc2bf654f4dd89cbb9 Mon Sep 17 00:00:00 2001 From: xeisenberg <94274140+xxeisenberg@users.noreply.github.com> Date: Wed, 4 Mar 2026 14:00:11 +0530 Subject: [PATCH 067/106] fix: wall not Path type (#89) --- src/caelestia/utils/wallpaper.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/caelestia/utils/wallpaper.py b/src/caelestia/utils/wallpaper.py index 6699823b..cb3efafd 100644 --- a/src/caelestia/utils/wallpaper.py +++ b/src/caelestia/utils/wallpaper.py @@ -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) From 80f9cb811d53070d4d12fb19ff402f24e00cfd11 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 6 Mar 2026 02:36:22 +0000 Subject: [PATCH 068/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index fa8db716..42e34f90 100644 --- a/flake.lock +++ b/flake.lock @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1772542754, - "narHash": "sha256-WGV2hy+VIeQsYXpsLjdr4GvHv5eECMISX1zKLTedhdg=", + "lastModified": 1772624091, + "narHash": "sha256-QKyJ0QGWBn6r0invrMAK8dmJoBYWoOWy7lN+UHzW1jc=", "owner": "nixos", "repo": "nixpkgs", - "rev": "8c809a146a140c5c8806f13399592dbcb1bb5dc4", + "rev": "80bdc1e5ce51f56b19791b52b2901187931f5353", "type": "github" }, "original": { From af0452398f769d238ed0dfc779e80b03c5b54a9c Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 02:51:20 +0000 Subject: [PATCH 069/106] [CI] chore: update flake --- flake.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/flake.lock b/flake.lock index 42e34f90..fb3eb55a 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1772330657, - "narHash": "sha256-cWblprYsDUeAWA57xAqxIjNxXvDI/rqYn6TFp2OPi/k=", + "lastModified": 1772934947, + "narHash": "sha256-CCZKZAa7uaRBY5TgKG59fOpmDkBiGkY78mbcJ68A9Vw=", "owner": "caelestia-dots", "repo": "shell", - "rev": "278fd4a4ed1bfb42c3fe197ff38b587539c012aa", + "rev": "658e09f89664978497a81f744a8f9186ee32c518", "type": "github" }, "original": { @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1772624091, - "narHash": "sha256-QKyJ0QGWBn6r0invrMAK8dmJoBYWoOWy7lN+UHzW1jc=", + "lastModified": 1772773019, + "narHash": "sha256-E1bxHxNKfDoQUuvriG71+f+s/NT0qWkImXsYZNFFfCs=", "owner": "nixos", "repo": "nixpkgs", - "rev": "80bdc1e5ce51f56b19791b52b2901187931f5353", + "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" }, From 95fdf10033a9cce23a1dc3dbf2e49deae469d283 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 02:52:41 +0000 Subject: [PATCH 070/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index fb3eb55a..871d5181 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1772934947, - "narHash": "sha256-CCZKZAa7uaRBY5TgKG59fOpmDkBiGkY78mbcJ68A9Vw=", + "lastModified": 1772962569, + "narHash": "sha256-ctRw4pVgx0IYKfA2hy90Ku37pnVX2T4q57UWp+l69fs=", "owner": "caelestia-dots", "repo": "shell", - "rev": "658e09f89664978497a81f744a8f9186ee32c518", + "rev": "e183599ce9e2c8d30a14631d53eb9947220c0812", "type": "github" }, "original": { From 8b66cbdaa4dd835a58296ef1692cff9a04cd9eae Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 9 Mar 2026 21:25:33 +1100 Subject: [PATCH 071/106] fix: format --- src/caelestia/subcommands/record.py | 2 +- src/caelestia/subcommands/resizer.py | 23 ++++++++------ src/caelestia/utils/logging.py | 2 ++ src/caelestia/utils/theme.py | 46 +++++++++++++--------------- src/caelestia/utils/wallpaper.py | 2 +- 5 files changed, 39 insertions(+), 36 deletions(-) diff --git a/src/caelestia/subcommands/record.py b/src/caelestia/subcommands/record.py index b9d27973..3585ea99 100644 --- a/src/caelestia/subcommands/record.py +++ b/src/caelestia/subcommands/record.py @@ -1,4 +1,3 @@ -from pathlib import Path import json import re import shutil @@ -6,6 +5,7 @@ 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 ( diff --git a/src/caelestia/subcommands/resizer.py b/src/caelestia/subcommands/resizer.py index ece8a884..02822657 100644 --- a/src/caelestia/subcommands/resizer.py +++ b/src/caelestia/subcommands/resizer.py @@ -140,9 +140,12 @@ def _apply_pip_action(self, window_id: str) -> None: 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 @@ def _handle_title_event(self, event: str) -> None: 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 @@ def _handle_open_event(self, event: str) -> None: 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 @@ def _run_active_mode(self) -> None: # 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 @@ def _apply_to_active_window(self, temp_rule: WindowRule) -> None: 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 @@ def _find_matching_windows(self, temp_rule: WindowRule) -> list: window_title = window.get("title", "") initial_title = window.get("initialTitle", "") - + # Check if window matches the pattern matches = False if temp_rule.match_type == "initialTitle": diff --git a/src/caelestia/utils/logging.py b/src/caelestia/utils/logging.py index 20964078..228936e5 100644 --- a/src/caelestia/utils/logging.py +++ b/src/caelestia/utils/logging.py @@ -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 diff --git a/src/caelestia/utils/theme.py b/src/caelestia/utils/theme.py index 7d174330..c36de9b2 100644 --- a/src/caelestia/utils/theme.py +++ b/src/caelestia/utils/theme.py @@ -1,11 +1,10 @@ +import fcntl import json import re +import shutil import subprocess -from pathlib import Path import tempfile -import shutil -import fcntl -import sys +from pathlib import Path from caelestia.utils.colour import get_dynamic_colours from caelestia.utils.logging import log_exception @@ -60,7 +59,7 @@ def fill_colour(match: re.Match) -> str: 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,7 +314,7 @@ 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"]) @@ -378,14 +376,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 +419,7 @@ def check(key: str) -> bool: if check("enableCava"): apply_cava(colours) apply_user_templates(colours, mode) - + finally: try: lock_file.unlink() diff --git a/src/caelestia/utils/wallpaper.py b/src/caelestia/utils/wallpaper.py index cb3efafd..77dcb382 100644 --- a/src/caelestia/utils/wallpaper.py +++ b/src/caelestia/utils/wallpaper.py @@ -125,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" @@ -143,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() From 198675fc1b15eb3229bb1026650ed14e52a2ce73 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 9 Mar 2026 21:36:44 +1100 Subject: [PATCH 072/106] feat: switch to qtengine --- src/caelestia/data/templates/qtct.conf | 6 ----- src/caelestia/data/templates/qtengine.json | 22 ++++++++++++++++ src/caelestia/utils/theme.py | 29 +++++----------------- 3 files changed, 28 insertions(+), 29 deletions(-) delete mode 100644 src/caelestia/data/templates/qtct.conf create mode 100644 src/caelestia/data/templates/qtengine.json diff --git a/src/caelestia/data/templates/qtct.conf b/src/caelestia/data/templates/qtct.conf deleted file mode 100644 index 578085a8..00000000 --- a/src/caelestia/data/templates/qtct.conf +++ /dev/null @@ -1,6 +0,0 @@ -[Appearance] -color_scheme_path={{ $config }}/colors/caelestia.colors -custom_palette=true -icon_theme=Papirus-{{ $mode }} -standard_dialogs=default -style=Darkly diff --git a/src/caelestia/data/templates/qtengine.json b/src/caelestia/data/templates/qtengine.json new file mode 100644 index 00000000..a340a7cf --- /dev/null +++ b/src/caelestia/data/templates/qtengine.json @@ -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 + } +} diff --git a/src/caelestia/utils/theme.py b/src/caelestia/utils/theme.py index c36de9b2..c5ced3a3 100644 --- a/src/caelestia/utils/theme.py +++ b/src/caelestia/utils/theme.py @@ -320,29 +320,12 @@ def apply_gtk(colours: dict[str, str], mode: str) -> None: @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) - - 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) + colours = gen_replace(colours, templates_dir / f"qt{mode}.colors", hash=True) + write_file(config_dir / "qtengine/caelestia.colors", colours) + + config = (templates_dir / "qtengine.json").read_text() + config = config.replace("{{ $mode }}", mode.capitalize()) + write_file(config_dir / "qtengine/config.json", config) @log_exception From cb1f3fe6e8e714b4f4a64e68340db6c0e7310e2d Mon Sep 17 00:00:00 2001 From: Kalagmitan <121934419+Kalagmitan@users.noreply.github.com> Date: Sun, 15 Mar 2026 19:56:05 +0800 Subject: [PATCH 073/106] refactor: enforce stricter type hints (#91) LSP was screaming at me so I decided to just address it to get it off my screen. + Fixed the type hints := Modified and added type hints for certain functions and variables in most of the files in the utils/ folder (and some in the subcommands/ folder) for clarity and so pyright's type checker wouldn't cry. :+ To resolve certain type issues, I had to add a bit more tiny additional code such as, additional checks if a variable is None, a tiny class in utils/material/generator.py to resolve the constructor usage mismatch between what the DynamicScheme accepts and what the code actually passes, and etc. - Renamed certain functions and variables for clarity and also for some to not collide with pre-existing definitions from well-known library imports. + PIL has reorganized their code a bit, so the code is made to reflect their new definitions. = Reorganized the single import statement for "colourfulness" in utils/wallpaper.py to be close to the top. (I think that's it) Side Effects?: Everything should work the same as no logic change was done whatsover (unless we consider the added if statements for type checking as a logic change). I've tested it, everything seems to be in urdir. --- src/caelestia/subcommands/screenshot.py | 7 ++- src/caelestia/subcommands/shell.py | 11 +++-- src/caelestia/subcommands/toggle.py | 28 ++++++----- src/caelestia/utils/colourfulness.py | 5 +- src/caelestia/utils/hypr.py | 24 +++++---- src/caelestia/utils/material/generator.py | 42 +++++++++------- src/caelestia/utils/notify.py | 2 +- src/caelestia/utils/paths.py | 59 ++++++++++++----------- src/caelestia/utils/scheme.py | 27 ++++++----- src/caelestia/utils/theme.py | 56 ++++++++++----------- src/caelestia/utils/wallpaper.py | 28 ++++++----- 11 files changed, 159 insertions(+), 130 deletions(-) diff --git a/src/caelestia/subcommands/screenshot.py b/src/caelestia/subcommands/screenshot.py index 6b4c00ad..9a8cd607 100644 --- a/src/caelestia/subcommands/screenshot.py +++ b/src/caelestia/subcommands/screenshot.py @@ -26,8 +26,11 @@ def region(self) -> None: else: sc_data = subprocess.check_output(["grim", "-l", "0", "-g", self.args.region.strip(), "-"]) swappy = subprocess.Popen(["swappy", "-f", "-"], stdin=subprocess.PIPE, start_new_session=True) - swappy.stdin.write(sc_data) - swappy.stdin.close() + + # Ensure stdin is not None for the type checker + if swappy.stdin: + swappy.stdin.write(sc_data) + swappy.stdin.close() def fullscreen(self) -> None: sc_data = subprocess.check_output(["grim", "-"]) diff --git a/src/caelestia/subcommands/shell.py b/src/caelestia/subcommands/shell.py index b9e81edb..48a71c21 100644 --- a/src/caelestia/subcommands/shell.py +++ b/src/caelestia/subcommands/shell.py @@ -33,11 +33,14 @@ def run(self) -> None: subprocess.run(args) else: shell = subprocess.Popen(args, stdout=subprocess.PIPE, universal_newlines=True) - for line in shell.stdout: - if self.filter_log(line): - print(line, end="") - def shell(self, *args: list[str]) -> str: + # Ensure stdout is not None for the type checker + if shell.stdout: + for line in shell.stdout: + if self.filter_log(line): + print(line, end="") + + def shell(self, *args: str) -> str: return subprocess.check_output(["qs", "-c", "caelestia", *args], text=True) def filter_log(self, line: str) -> bool: diff --git a/src/caelestia/subcommands/toggle.py b/src/caelestia/subcommands/toggle.py index ba5a351f..56565f37 100644 --- a/src/caelestia/subcommands/toggle.py +++ b/src/caelestia/subcommands/toggle.py @@ -3,6 +3,7 @@ import shutil from argparse import Namespace from collections import ChainMap +from typing import Any, Callable, cast from caelestia.utils import hypr from caelestia.utils.paths import user_config_path @@ -52,8 +53,8 @@ def __repr__(self): class Command: args: Namespace - cfg: dict[str, dict[str, dict[str, any]]] | DeepChainMap - clients: list[dict[str, any]] = None + cfg: dict[str, dict[str, dict[str, Any]]] | DeepChainMap + clients: list[dict[str, Any]] | None = None def __init__(self, args: Namespace) -> None: self.args = args @@ -120,27 +121,27 @@ def run(self) -> None: if not spawned: hypr.dispatch("togglespecialworkspace", self.args.workspace) - def get_clients(self) -> list[dict[str, any]]: + def get_clients(self) -> list[dict[str, Any]]: if self.clients is None: - self.clients = hypr.message("clients") - + self.clients = cast(list[dict[str, Any]], hypr.message("clients")) return self.clients - def move_client(self, selector: callable, workspace: str) -> None: + def move_client(self, selector: Callable, workspace: str) -> None: for client in self.get_clients(): if selector(client) and client["workspace"]["name"] != f"special:{workspace}": hypr.dispatch("movetoworkspacesilent", f"special:{workspace},address:{client['address']}") - def spawn_client(self, selector: callable, spawn: list[str]) -> bool: + def spawn_client(self, selector: Callable, spawn: list[str]) -> bool: if (spawn[0].endswith(".desktop") or shutil.which(spawn[0])) and not any( selector(client) for client in self.get_clients() ): hypr.dispatch("exec", f"[workspace special:{self.args.workspace}] app2unit -- {shlex.join(spawn)}") return True - return False + else: + return False - def handle_client_config(self, client: dict[str, any]) -> bool: - def selector(c: dict[str, any]) -> bool: + def handle_client_config(self, client: dict[str, Any]) -> bool: + def selector(c: dict[str, Any]) -> bool: # Each match is or, inside matches is and for match in client["match"]: if is_subset(c, match): @@ -156,5 +157,8 @@ def selector(c: dict[str, any]) -> bool: return spawned def specialws(self) -> None: - special = next(m for m in hypr.message("monitors") if m["focused"])["specialWorkspace"]["name"] - hypr.dispatch("togglespecialworkspace", special[8:] or "special") + monitors = cast(list[dict[str, Any]], hypr.message("monitors")) + target = next((m for m in monitors if m.get("focused")), None) + if target: + special = target.get("specialWorkspace", {}).get("name", "")[8:] or "special" + hypr.dispatch("togglespecialworkspace", special) diff --git a/src/caelestia/utils/colourfulness.py b/src/caelestia/utils/colourfulness.py index f76c1b63..5e06eb3d 100644 --- a/src/caelestia/utils/colourfulness.py +++ b/src/caelestia/utils/colourfulness.py @@ -11,8 +11,7 @@ def stddev(values: list[float], mean_val: float) -> float: return math.sqrt(sum((x - mean_val) ** 2 for x in values) / len(values)) if values else 0 -def calc_colourfulness(image: Image) -> float: - width, height = image.size +def calc_colourfulness(image: Image.Image) -> float: pixels = list(image.getdata()) # List of (R, G, B) tuples rg_diffs = [] @@ -32,7 +31,7 @@ def calc_colourfulness(image: Image) -> float: return math.sqrt(std_rg**2 + std_yb**2) + 0.3 * math.sqrt(mean_rg**2 + mean_yb**2) -def get_variant(image: Image) -> str: +def get_variant(image: Image.Image) -> str: colourfulness = calc_colourfulness(image) if colourfulness < 10: diff --git a/src/caelestia/utils/hypr.py b/src/caelestia/utils/hypr.py index 81bc1231..d251b987 100644 --- a/src/caelestia/utils/hypr.py +++ b/src/caelestia/utils/hypr.py @@ -1,17 +1,18 @@ -import json as j +import json import os import socket +from typing import Any socket_base = f"{os.getenv('XDG_RUNTIME_DIR')}/hypr/{os.getenv('HYPRLAND_INSTANCE_SIGNATURE')}" socket_path = f"{socket_base}/.socket.sock" socket2_path = f"{socket_base}/.socket2.sock" -def message(msg: str, 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: sock.connect(socket_path) - if json: + if is_json: msg = f"j/{msg}" sock.send(msg.encode()) @@ -22,14 +23,17 @@ def message(msg: str, json: bool = True) -> str | dict[str, any]: break resp += new_resp.decode() - return j.loads(resp) if json else resp + return json.loads(resp) if is_json else resp -def dispatch(dispatcher: str, *args: list[any]) -> bool: - return message(f"dispatch {dispatcher} {' '.join(map(str, args))}".rstrip(), json=False) == "ok" +def dispatch(dispatcher: str, *args: str) -> bool: + return message(f"dispatch {dispatcher} {' '.join(map(str, args))}".rstrip(), is_json=False) == "ok" -def batch(*msgs: list[str], json: bool = False) -> str | dict[str, any]: - if json: - msgs = (f"j/{m.strip()}" for m in msgs) - return message(f"[[BATCH]]{';'.join(msgs)}", json=False) +def batch(*msgs: str, is_json: bool = False) -> str | dict[str, Any]: + formatted_msgs = msgs + + if is_json: + formatted_msgs = [f"j/{m.strip()}" for m in msgs] + + return message(f"[[BATCH]]{';'.join(formatted_msgs)}", is_json=False) diff --git a/src/caelestia/utils/material/generator.py b/src/caelestia/utils/material/generator.py index 384fdd1a..cf241e99 100644 --- a/src/caelestia/utils/material/generator.py +++ b/src/caelestia/utils/material/generator.py @@ -11,6 +11,14 @@ from materialyoucolor.scheme.scheme_tonal_spot import SchemeTonalSpot from materialyoucolor.scheme.scheme_vibrant import SchemeVibrant from materialyoucolor.utils.math_utils import difference_degrees, rotation_direction, sanitize_degrees_double +from typing import Protocol, Any + + +# The base DynamicScheme class requires a 'variant' argument, but the specific +# 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. +class SchemeConstructor(Protocol): + def __call__(self, source_color_hct: Any, is_dark: bool, contrast_level: float) -> DynamicScheme: ... try: from materialyoucolor.dynamiccolor.dynamic_scheme import DynamicScheme @@ -147,7 +155,7 @@ def darken(colour: Hct, amount: float) -> Hct: return Hct.from_hct(colour.hue, colour.chroma - diff / 5, colour.tone - diff) -def get_scheme(scheme: str) -> DynamicScheme: +def get_scheme(scheme: str) -> SchemeConstructor: if scheme == "content": return SchemeContent if scheme == "expressive": @@ -168,12 +176,12 @@ def get_scheme(scheme: str) -> DynamicScheme: def gen_scheme(scheme, primary: Hct) -> dict[str, str]: - light = scheme.mode == "light" + is_light = scheme.mode == "light" colours = {} # Material colours - primary_scheme = get_scheme(scheme.variant)(primary, not light, 0) + primary_scheme = get_scheme(scheme.variant)(source_color_hct=primary, is_dark=not is_light, contrast_level=0.0) if hasattr(MaterialDynamicColors, "all_colors"): # materialyoucolor-python >= 3.0.0 dyn_colours = MaterialDynamicColors() for colour in dyn_colours.all_colors: @@ -191,28 +199,28 @@ def gen_scheme(scheme, primary: Hct) -> dict[str, str]: colours["neutral_variant_paletteKeyColor"] = colours["neutralVariantPaletteKeyColor"] # Harmonize terminal colours - for i, hct in enumerate(light_gruvbox if light else dark_gruvbox): + for i, hct in enumerate(light_gruvbox if is_light else dark_gruvbox): if scheme.variant == "monochrome": - colours[f"term{i}"] = grayscale(hct, light) + colours[f"term{i}"] = grayscale(hct, is_light) else: colours[f"term{i}"] = harmonize( - hct, colours["primary_paletteKeyColor"], (0.35 if i < 8 else 0.2) * (-1 if light else 1) + hct, colours["primary_paletteKeyColor"], (0.35 if i < 8 else 0.2) * (-1 if is_light else 1) ) # Harmonize named colours - for i, hct in enumerate(light_catppuccin if light else dark_catppuccin): + for i, hct in enumerate(light_catppuccin if is_light else dark_catppuccin): if scheme.variant == "monochrome": - colours[colour_names[i]] = grayscale(hct, light) + colours[colour_names[i]] = grayscale(hct, is_light) else: - colours[colour_names[i]] = harmonize(hct, colours["primary_paletteKeyColor"], (-0.2 if light else 0.05)) + colours[colour_names[i]] = harmonize(hct, colours["primary_paletteKeyColor"], (-0.2 if is_light else 0.05)) # KColours for colour in kcolours: colours[colour["name"]] = harmonize(colour["hct"], colours["primary"], 0.1) colours[f"{colour['name']}Selection"] = harmonize(colour["hct"], colours["onPrimaryFixedVariant"], 0.1) if scheme.variant == "monochrome": - colours[colour["name"]] = grayscale(colours[colour["name"]], light) - colours[f"{colour['name']}Selection"] = grayscale(colours[f"{colour['name']}Selection"], light) + colours[colour["name"]] = grayscale(colours[colour["name"]], is_light) + colours[f"{colour['name']}Selection"] = grayscale(colours[f"{colour['name']}Selection"], is_light) if scheme.variant == "neutral": for name, hct in colours.items(): @@ -221,8 +229,8 @@ def gen_scheme(scheme, primary: Hct) -> dict[str, str]: # Darken surfaces for hard flavour if scheme.flavour == "hard": for colour in "background", *(k for k in colours.keys() if k.startswith("surface")): - colours[colour] = lighten(colours[colour], 0.4) if light else darken(colours[colour], 0.8) - colours["term0"] = lighten(colours["term0"], 0.4) if light else darken(colours["term0"], 0.9) + colours[colour] = lighten(colours[colour], 0.4) if is_light else darken(colours[colour], 0.8) + colours["term0"] = lighten(colours["term0"], 0.4) if is_light else darken(colours["term0"], 0.9) # FIXME: deprecated stuff colours["text"] = colours["onBackground"] @@ -241,13 +249,13 @@ def gen_scheme(scheme, primary: Hct) -> dict[str, str]: # More darkening if hard flavour if scheme.flavour == "hard": for colour in "base", "mantle", "crust": - colours[colour] = lighten(colours[colour], 0.4) if light else darken(colours[colour], 0.9) + colours[colour] = lighten(colours[colour], 0.4) if is_light else darken(colours[colour], 0.9) for i in range(3): colours[f"overlay{i}"] = ( - lighten(colours[f"overlay{i}"], 0.4) if light else darken(colours[f"overlay{i}"], 0.8) + lighten(colours[f"overlay{i}"], 0.4) if is_light else darken(colours[f"overlay{i}"], 0.8) ) colours[f"surface{i}"] = ( - lighten(colours[f"surface{i}"], 0.4) if light else darken(colours[f"surface{i}"], 0.8) + lighten(colours[f"surface{i}"], 0.4) if is_light else darken(colours[f"surface{i}"], 0.8) ) # For debugging @@ -256,7 +264,7 @@ def gen_scheme(scheme, primary: Hct) -> dict[str, str]: colours = {k: hex(v.to_int())[4:] for k, v in colours.items()} # Extended material - if light: + if is_light: colours["success"] = "4F6354" colours["onSuccess"] = "FFFFFF" colours["successContainer"] = "D1E8D5" diff --git a/src/caelestia/utils/notify.py b/src/caelestia/utils/notify.py index ee862369..c88dece6 100644 --- a/src/caelestia/utils/notify.py +++ b/src/caelestia/utils/notify.py @@ -1,7 +1,7 @@ import subprocess -def notify(*args: list[str]) -> str: +def notify(*args: str) -> str: return subprocess.check_output(["notify-send", "-a", "caelestia-cli", *args], text=True).strip() diff --git a/src/caelestia/utils/paths.py b/src/caelestia/utils/paths.py index 14eb5505..3223b900 100644 --- a/src/caelestia/utils/paths.py +++ b/src/caelestia/utils/paths.py @@ -4,41 +4,42 @@ import shutil import tempfile from pathlib import Path +from typing import Any -config_dir = Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config")) -data_dir = Path(os.getenv("XDG_DATA_HOME", Path.home() / ".local/share")) -state_dir = Path(os.getenv("XDG_STATE_HOME", Path.home() / ".local/state")) -cache_dir = Path(os.getenv("XDG_CACHE_HOME", Path.home() / ".cache")) -pictures_dir = Path(os.getenv("XDG_PICTURES_DIR", Path.home() / "Pictures")) -videos_dir = Path(os.getenv("XDG_VIDEOS_DIR", Path.home() / "Videos")) +config_dir: Path = Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config")) +data_dir: Path = Path(os.getenv("XDG_DATA_HOME", Path.home() / ".local/share")) +state_dir: Path = Path(os.getenv("XDG_STATE_HOME", Path.home() / ".local/state")) +cache_dir: Path = Path(os.getenv("XDG_CACHE_HOME", Path.home() / ".cache")) +pictures_dir: Path = Path(os.getenv("XDG_PICTURES_DIR", Path.home() / "Pictures")) +videos_dir: Path = Path(os.getenv("XDG_VIDEOS_DIR", Path.home() / "Videos")) -c_config_dir = config_dir / "caelestia" -c_data_dir = data_dir / "caelestia" -c_state_dir = state_dir / "caelestia" -c_cache_dir = cache_dir / "caelestia" +c_config_dir: Path = config_dir / "caelestia" +c_data_dir: Path = data_dir / "caelestia" +c_state_dir: Path = state_dir / "caelestia" +c_cache_dir: Path = cache_dir / "caelestia" -user_config_path = c_config_dir / "cli.json" -cli_data_dir = Path(__file__).parent.parent / "data" -templates_dir = cli_data_dir / "templates" -user_templates_dir = c_config_dir / "templates" -theme_dir = c_state_dir / "theme" +user_config_path: Path = c_config_dir / "cli.json" +cli_data_dir: Path = Path(__file__).parent.parent / "data" +templates_dir: Path = cli_data_dir / "templates" +user_templates_dir: Path = c_config_dir / "templates" +theme_dir: Path = c_state_dir / "theme" -scheme_path = c_state_dir / "scheme.json" -scheme_data_dir = cli_data_dir / "schemes" -scheme_cache_dir = c_cache_dir / "schemes" +scheme_path: Path = c_state_dir / "scheme.json" +scheme_data_dir: Path = cli_data_dir / "schemes" +scheme_cache_dir: Path = c_cache_dir / "schemes" -wallpapers_dir = os.getenv("CAELESTIA_WALLPAPERS_DIR", pictures_dir / "Wallpapers") -wallpaper_path_path = c_state_dir / "wallpaper/path.txt" -wallpaper_link_path = c_state_dir / "wallpaper/current" -wallpaper_thumbnail_path = c_state_dir / "wallpaper/thumbnail.jpg" -wallpapers_cache_dir = c_cache_dir / "wallpapers" +wallpapers_dir: Path = Path(os.getenv("CAELESTIA_WALLPAPERS_DIR", pictures_dir / "Wallpapers")) +wallpaper_path_path: Path = c_state_dir / "wallpaper/path.txt" +wallpaper_link_path: Path = c_state_dir / "wallpaper/current" +wallpaper_thumbnail_path: Path = c_state_dir / "wallpaper/thumbnail.jpg" +wallpapers_cache_dir: Path = c_cache_dir / "wallpapers" -screenshots_dir = os.getenv("CAELESTIA_SCREENSHOTS_DIR", pictures_dir / "Screenshots") -screenshots_cache_dir = c_cache_dir / "screenshots" +screenshots_dir: Path = Path(os.getenv("CAELESTIA_SCREENSHOTS_DIR", pictures_dir / "Screenshots")) +screenshots_cache_dir: Path = c_cache_dir / "screenshots" -recordings_dir = os.getenv("CAELESTIA_RECORDINGS_DIR", videos_dir / "Recordings") -recording_path = c_state_dir / "record/recording.mp4" -recording_notif_path = c_state_dir / "record/notifid.txt" +recordings_dir: Path = Path(os.getenv("CAELESTIA_RECORDINGS_DIR", videos_dir / "Recordings")) +recording_path: Path = c_state_dir / "record/recording.mp4" +recording_notif_path: Path = c_state_dir / "record/notifid.txt" def compute_hash(path: Path | str) -> str: @@ -51,7 +52,7 @@ def compute_hash(path: Path | str) -> str: return sha.hexdigest() -def atomic_dump(path: Path, content: dict[str, any]) -> None: +def atomic_dump(path: Path, content: dict[str, Any]) -> None: with tempfile.NamedTemporaryFile("w") as f: json.dump(content, f) f.flush() diff --git a/src/caelestia/utils/scheme.py b/src/caelestia/utils/scheme.py index 57a5e0b6..e08d777e 100644 --- a/src/caelestia/utils/scheme.py +++ b/src/caelestia/utils/scheme.py @@ -1,6 +1,7 @@ import json import random from pathlib import Path +from typing import Any from caelestia.utils.notify import notify from caelestia.utils.paths import atomic_dump, scheme_data_dir, scheme_path @@ -14,19 +15,19 @@ class Scheme: _colours: dict[str, str] notify: bool - def __init__(self, json: dict[str, any] | None) -> None: - if json is None: + def __init__(self, scheme_json: dict[str, Any] | None) -> None: + if scheme_json is None: self._name = "catppuccin" self._flavour = "mocha" self._mode = "dark" self._variant = "tonalspot" self._colours = read_colours_from_file(self.get_colours_path()) else: - self._name = json["name"] - self._flavour = json["flavour"] - self._mode = json["mode"] - self._variant = json["variant"] - self._colours = json["colours"] + self._name = scheme_json["name"] + self._flavour = scheme_json["flavour"] + self._mode = scheme_json["mode"] + self._variant = scheme_json["variant"] + self._colours = scheme_json["colours"] self.notify = False @property @@ -196,7 +197,7 @@ def __str__(self) -> str: "content", ] -scheme: Scheme = None +scheme: Scheme | None = None def read_colours_from_file(path: Path) -> dict[str, str]: @@ -225,7 +226,7 @@ def get_scheme_names() -> list[str]: return [*(f.name for f in scheme_data_dir.iterdir() if f.is_dir()), "dynamic"] -def get_scheme_flavours(name: str = None) -> list[str]: +def get_scheme_flavours(name: str | None = None) -> list[str]: if name is None: name = get_scheme().name @@ -234,11 +235,11 @@ def get_scheme_flavours(name: str = None) -> list[str]: ) -def get_scheme_modes(name: str = None, flavour: str = None) -> list[str]: - if name is None: +def get_scheme_modes(name: str | None = None, flavour: str | None = None) -> list[str]: + if name is None or flavour is None: scheme = get_scheme() - name = scheme.name - flavour = scheme.flavour + name = name or scheme.name + flavour = flavour or scheme.flavour if name == "dynamic": return ["light", "dark"] diff --git a/src/caelestia/utils/theme.py b/src/caelestia/utils/theme.py index c5ced3a3..30e73f81 100644 --- a/src/caelestia/utils/theme.py +++ b/src/caelestia/utils/theme.py @@ -4,6 +4,8 @@ import shutil import subprocess import tempfile +import shutil +import fcntl from pathlib import Path from caelestia.utils.colour import get_dynamic_colours @@ -34,10 +36,10 @@ def gen_scss(colours: dict[str, str]) -> str: def gen_replace(colours: dict[str, str], template: Path, hash: bool = False) -> str: - template = template.read_text() + new_template = template.read_text() for name, colour in colours.items(): - template = template.replace(f"{{{{ ${name} }}}}", f"#{colour}" if hash else colour) - return template + new_template = new_template.replace(f"{{{{ ${name} }}}}", f"#{colour}" if hash else colour) + return new_template def gen_replace_dynamic(colours: dict[str, str], template: Path, mode: str) -> str: @@ -65,7 +67,7 @@ def fill_colour(match: re.Match) -> str: return template_filled -def c2s(c: str, *i: list[int]) -> str: +def hex_to_ansi(c: str, *i: int) -> str: """Hex to ANSI sequence (e.g. ffffff, 11 -> \x1b]11;rgb:ff/ff/ff\x1b\\)""" return f"\x1b]{';'.join(map(str, i))};rgb:{c[0:2]}/{c[2:4]}/{c[4:6]}\x1b\\" @@ -82,29 +84,29 @@ def gen_sequences(colours: dict[str, str]) -> str: 16+: 256 colours """ return ( - c2s(colours["onSurface"], 10) - + c2s(colours["surface"], 11) - + c2s(colours["secondary"], 12) - + c2s(colours["secondary"], 17) - + c2s(colours["term0"], 4, 0) - + c2s(colours["term1"], 4, 1) - + c2s(colours["term2"], 4, 2) - + c2s(colours["term3"], 4, 3) - + c2s(colours["term4"], 4, 4) - + c2s(colours["term5"], 4, 5) - + c2s(colours["term6"], 4, 6) - + c2s(colours["term7"], 4, 7) - + c2s(colours["term8"], 4, 8) - + c2s(colours["term9"], 4, 9) - + c2s(colours["term10"], 4, 10) - + c2s(colours["term11"], 4, 11) - + c2s(colours["term12"], 4, 12) - + c2s(colours["term13"], 4, 13) - + c2s(colours["term14"], 4, 14) - + c2s(colours["term15"], 4, 15) - + c2s(colours["primary"], 4, 16) - + c2s(colours["secondary"], 4, 17) - + c2s(colours["tertiary"], 4, 18) + hex_to_ansi(colours["onSurface"], 10) + + hex_to_ansi(colours["surface"], 11) + + hex_to_ansi(colours["secondary"], 12) + + hex_to_ansi(colours["secondary"], 17) + + hex_to_ansi(colours["term0"], 4, 0) + + hex_to_ansi(colours["term1"], 4, 1) + + hex_to_ansi(colours["term2"], 4, 2) + + hex_to_ansi(colours["term3"], 4, 3) + + hex_to_ansi(colours["term4"], 4, 4) + + hex_to_ansi(colours["term5"], 4, 5) + + hex_to_ansi(colours["term6"], 4, 6) + + hex_to_ansi(colours["term7"], 4, 7) + + hex_to_ansi(colours["term8"], 4, 8) + + hex_to_ansi(colours["term9"], 4, 9) + + hex_to_ansi(colours["term10"], 4, 10) + + hex_to_ansi(colours["term11"], 4, 11) + + hex_to_ansi(colours["term12"], 4, 12) + + hex_to_ansi(colours["term13"], 4, 13) + + hex_to_ansi(colours["term14"], 4, 14) + + hex_to_ansi(colours["term15"], 4, 15) + + hex_to_ansi(colours["primary"], 4, 16) + + hex_to_ansi(colours["secondary"], 4, 17) + + hex_to_ansi(colours["tertiary"], 4, 18) ) diff --git a/src/caelestia/utils/wallpaper.py b/src/caelestia/utils/wallpaper.py index 77dcb382..02d3f4e8 100644 --- a/src/caelestia/utils/wallpaper.py +++ b/src/caelestia/utils/wallpaper.py @@ -2,8 +2,10 @@ import os import random import subprocess + from argparse import Namespace from pathlib import Path +from typing import cast from materialyoucolor.hct import Hct from materialyoucolor.utils.color_utils import argb_from_rgb @@ -11,6 +13,7 @@ from caelestia.utils.hypr import message from caelestia.utils.material import get_colours_for_image +from caelestia.utils.colourfulness import get_variant from caelestia.utils.paths import ( compute_hash, user_config_path, @@ -33,7 +36,7 @@ def check_wall(wall: Path, filter_size: tuple[int, int], threshold: float) -> bo return width >= filter_size[0] * threshold and height >= filter_size[1] * threshold -def get_wallpaper() -> str: +def get_wallpaper() -> str | None: try: return wallpaper_path_path.read_text() except IOError: @@ -41,16 +44,16 @@ def get_wallpaper() -> str: def get_wallpapers(args: Namespace) -> list[Path]: - dir = Path(args.random) - if not dir.is_dir(): + directory = Path(args.random) + if not directory.is_dir(): return [] - walls = [f for f in dir.rglob("*") if is_valid_image(f)] + walls = [f for f in directory.rglob("*") if is_valid_image(f)] if args.no_filter: return walls - monitors = message("monitors") + monitors = cast(list[dict[str, int]], message("monitors")) filter_size = min(m["width"] for m in monitors), min(m["height"] for m in monitors) return [f for f in walls if check_wall(f, filter_size, args.threshold)] @@ -62,14 +65,14 @@ def get_thumb(wall: Path, cache: Path) -> Path: if not thumb.exists(): with Image.open(wall) as img: img = img.convert("RGB") - img.thumbnail((128, 128), Image.NEAREST) + img.thumbnail((128, 128), Image.Resampling.NEAREST) thumb.parent.mkdir(parents=True, exist_ok=True) img.save(thumb, "JPEG") return thumb -def get_smart_opts(wall: Path, cache: Path) -> str: +def get_smart_opts(wall: Path, cache: Path) -> dict: opts_cache = cache / "smart.json" try: @@ -77,15 +80,16 @@ def get_smart_opts(wall: Path, cache: Path) -> str: except (IOError, json.JSONDecodeError): pass - from caelestia.utils.colourfulness import get_variant - opts = {} with Image.open(get_thumb(wall, cache)) as img: opts["variant"] = get_variant(img) + img.thumbnail((1, 1), Image.Resampling.LANCZOS) + + # Cast the pixel to a tuple of 3 integers to safely unpack it + pixel = cast(tuple[int, int, int], img.getpixel((0, 0))) + hct = Hct.from_int(argb_from_rgb(*pixel)) - img.thumbnail((1, 1), Image.LANCZOS) - hct = Hct.from_int(argb_from_rgb(*img.getpixel((0, 0)))) opts["mode"] = "light" if hct.tone > 60 else "dark" opts_cache.parent.mkdir(parents=True, exist_ok=True) @@ -144,7 +148,7 @@ def convert_gif(wall: Path) -> Path: return output_path -def set_wallpaper(wall: Path | str, no_smart: bool) -> None: +def set_wallpaper(wall: Path, no_smart: bool) -> None: # Make path absolute wall = Path(wall).resolve() From ef516afd9f6ccb5426980fa1a67645efdf53039d Mon Sep 17 00:00:00 2001 From: Samiyel Frazier Date: Sun, 15 Mar 2026 20:14:46 -0700 Subject: [PATCH 074/106] nix: fix qtct sub (#98) --- default.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/default.nix b/default.nix index dafab0d5..22fb0b64 100644 --- a/default.nix +++ b/default.nix @@ -72,7 +72,7 @@ python3.pkgs.buildPythonApplication { --replace-fail 'app2unit' ${app2unit}/bin/app2unit # Use config style instead of darkly - substituteInPlace src/caelestia/data/templates/qtct.conf \ + substituteInPlace src/caelestia/data/templates/qtengine.json \ --replace-fail 'Darkly' '${qtctStyle}' ''; From 3f85e7fa93b87dbdf238662f3543c147ec1f23c3 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 02:42:09 +0000 Subject: [PATCH 075/106] [CI] chore: update flake --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 871d5181..910f4ba9 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1772962569, - "narHash": "sha256-ctRw4pVgx0IYKfA2hy90Ku37pnVX2T4q57UWp+l69fs=", + "lastModified": 1773574600, + "narHash": "sha256-RsBgiq93SjlYVHSYtz/ESALa7WgbLu+xcH0oTJRjztQ=", "owner": "caelestia-dots", "repo": "shell", - "rev": "e183599ce9e2c8d30a14631d53eb9947220c0812", + "rev": "377778596acf90451d1bd19f0c03b5f1c0467958", "type": "github" }, "original": { @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1772773019, - "narHash": "sha256-E1bxHxNKfDoQUuvriG71+f+s/NT0qWkImXsYZNFFfCs=", + "lastModified": 1773646010, + "narHash": "sha256-iYrs97hS7p5u4lQzuNWzuALGIOdkPXvjz7bviiBjUu8=", "owner": "nixos", "repo": "nixpkgs", - "rev": "aca4d95fce4914b3892661bcb80b8087293536c6", + "rev": "5b2c2d84341b2afb5647081c1386a80d7a8d8605", "type": "github" }, "original": { From c3b168b6a7f5878bf9655dc47d334c15a1e3953a Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 02:48:20 +0000 Subject: [PATCH 076/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 910f4ba9..06ef80d1 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1773574600, - "narHash": "sha256-RsBgiq93SjlYVHSYtz/ESALa7WgbLu+xcH0oTJRjztQ=", + "lastModified": 1773732413, + "narHash": "sha256-ScUMKWQF2hmHjPyAP269Q7BgrVVgEA7Y6u2V2+gOHTY=", "owner": "caelestia-dots", "repo": "shell", - "rev": "377778596acf90451d1bd19f0c03b5f1c0467958", + "rev": "60de5a129a6ec7ba1d43d97600ee0ca993204616", "type": "github" }, "original": { From 31ad3ad57b1f2a402ed9e22facf89a9c87569c9b Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 02:48:41 +0000 Subject: [PATCH 077/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 06ef80d1..255267bc 100644 --- a/flake.lock +++ b/flake.lock @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1773646010, - "narHash": "sha256-iYrs97hS7p5u4lQzuNWzuALGIOdkPXvjz7bviiBjUu8=", + "lastModified": 1773821835, + "narHash": "sha256-TJ3lSQtW0E2JrznGVm8hOQGVpXjJyXY2guAxku2O9A4=", "owner": "nixos", "repo": "nixpkgs", - "rev": "5b2c2d84341b2afb5647081c1386a80d7a8d8605", + "rev": "b40629efe5d6ec48dd1efba650c797ddbd39ace0", "type": "github" }, "original": { From 7b7d2de6d588c6ca889375b4a326a8e94d0a0865 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 02:40:16 +0000 Subject: [PATCH 078/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 255267bc..a6e38499 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1773732413, - "narHash": "sha256-ScUMKWQF2hmHjPyAP269Q7BgrVVgEA7Y6u2V2+gOHTY=", + "lastModified": 1773930260, + "narHash": "sha256-HBP08tt5bTvD/nvhOlkHNVs0V6fVlIDl0T71mJmFohU=", "owner": "caelestia-dots", "repo": "shell", - "rev": "60de5a129a6ec7ba1d43d97600ee0ca993204616", + "rev": "344ec92cc5bc146b8b30a76fedc2f444348f366d", "type": "github" }, "original": { From 01466901d2f912daf042366fc7601739c691d585 Mon Sep 17 00:00:00 2001 From: AteebXYZ <130910481+AteebXYZ@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:56:12 +0300 Subject: [PATCH 079/106] readme: document user templates (#99) * document user_templates in README * move user_template documentation to Usage * format --------- Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> --- README.md | 51 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 7239cc34..ad3af240 100644 --- a/README.md +++ b/README.md @@ -4,40 +4,43 @@ The main control script for the Caelestia dotfiles.
External dependencies -- [`libnotfy`](https://gitlab.gnome.org/GNOME/libnotify) - sending notifications -- [`swappy`](https://github.com/jtheoof/swappy) - screenshot editor -- [`grim`](https://gitlab.freedesktop.org/emersion/grim) - taking screenshots -- [`dart-sass`](https://github.com/sass/dart-sass) - discord theming -- [`app2unit`](https://github.com/Vladimir-csp/app2unit) - launching apps -- [`wl-clipboard`](https://github.com/bugaevc/wl-clipboard) - copying to clipboard -- [`slurp`](https://github.com/emersion/slurp) - selecting an area -- [`gpu-screen-recorder`](https://git.dec05eba.com/gpu-screen-recorder/about) - screen recording -- `glib2` - closing notifications -- [`cliphist`](https://github.com/sentriz/cliphist) - clipboard history -- [`fuzzel`](https://codeberg.org/dnkl/fuzzel) - clipboard history/emoji picker +- [`libnotfy`](https://gitlab.gnome.org/GNOME/libnotify) - sending notifications +- [`swappy`](https://github.com/jtheoof/swappy) - screenshot editor +- [`grim`](https://gitlab.freedesktop.org/emersion/grim) - taking screenshots +- [`dart-sass`](https://github.com/sass/dart-sass) - discord theming +- [`app2unit`](https://github.com/Vladimir-csp/app2unit) - launching apps +- [`wl-clipboard`](https://github.com/bugaevc/wl-clipboard) - copying to clipboard +- [`slurp`](https://github.com/emersion/slurp) - selecting an area +- [`gpu-screen-recorder`](https://git.dec05eba.com/gpu-screen-recorder/about) - screen recording +- `glib2` - closing notifications +- [`cliphist`](https://github.com/sentriz/cliphist) - clipboard history +- [`fuzzel`](https://codeberg.org/dnkl/fuzzel) - clipboard history/emoji picker
Optional dependencies -- [`papirus-folders`](https://github.com/PapirusDevelopmentTeam/papirus-folders) - automatic folder icon color syncing with theme +- [`papirus-folders`](https://github.com/PapirusDevelopmentTeam/papirus-folders) - automatic folder icon color syncing with theme > [!NOTE] > For automatic Papirus folder icon color syncing, `papirus-folders` needs to be able to run with `sudo` without a password prompt. -> +> > **Recommended** - Create a sudoers file: +> > ```fish > # Fish shell > echo "$USER ALL=(ALL) NOPASSWD: "(which papirus-folders) | sudo tee /etc/sudoers.d/papirus-folders > sudo chmod 440 /etc/sudoers.d/papirus-folders > ``` +> > ```sh > # Bash/other shells > echo "$USER ALL=(ALL) NOPASSWD: $(which papirus-folders)" | sudo tee /etc/sudoers.d/papirus-folders > sudo chmod 440 /etc/sudoers.d/papirus-folders > ``` -> +> > **Alternatively** - Edit the main sudoers file by running `sudo visudo` and adding at the end: +> > ``` > your_username ALL=(ALL) NOPASSWD: /usr/bin/papirus-folders > ``` @@ -148,6 +151,24 @@ subcommands: resizer window resizer daemon ``` +### User Templates + +Custom user templates can be defined in `~/.config/caelestia/templates/`. + +#### Template syntax + +`{{ . }}` + +- `` is a theme color role derived from the Material You color system (e.g. `primary`, `secondary`, `background`) +- `` is the output format: `hex` or `rgb` + +#### Examples + +- `{{ primary.hex }}` outputs `3f4ba2` +- `{{ primary.rgb }}` outputs `rgb(193, 132, 207)` + +Output files are written to `~/.local/state/caelestia/theme/`. You can symlink them to your desired locations. + ## Configuring All configuration options are in `~/.config/caelestia/cli.json`. @@ -160,7 +181,7 @@ All configuration options are in `~/.config/caelestia/cli.json`. "extraArgs": [] }, "wallpaper": { - "postHook": "echo $WALLPAPER_PATH" + "postHook": "echo $WALLPAPER_PATH" }, "theme": { "enableTerm": true, From 838c86c8e91b2f7f51bc01fcb95c76ebe64cb1f7 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 21 Mar 2026 02:34:07 +0000 Subject: [PATCH 080/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index a6e38499..d8a82f98 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1773930260, - "narHash": "sha256-HBP08tt5bTvD/nvhOlkHNVs0V6fVlIDl0T71mJmFohU=", + "lastModified": 1774000277, + "narHash": "sha256-3IWTAr4x5HonjWzMyBG+rmH3+Lncxo1y7nYY+59NT+Q=", "owner": "caelestia-dots", "repo": "shell", - "rev": "344ec92cc5bc146b8b30a76fedc2f444348f366d", + "rev": "d81d471740e3e6bfb6359d9cdc4875a7e0412975", "type": "github" }, "original": { From 3e3a539166943625a93c5bfbd10aa8234cbb7d5a Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 22 Mar 2026 02:50:36 +0000 Subject: [PATCH 081/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index d8a82f98..c388c4b4 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1774000277, - "narHash": "sha256-3IWTAr4x5HonjWzMyBG+rmH3+Lncxo1y7nYY+59NT+Q=", + "lastModified": 1774106929, + "narHash": "sha256-5fyfoxMjPCYhvHvaRUbfjJq94Tg9XsIjzM9p3NnYjLU=", "owner": "caelestia-dots", "repo": "shell", - "rev": "d81d471740e3e6bfb6359d9cdc4875a7e0412975", + "rev": "6068e9f08a16580da1300acf1d869be37fbfa984", "type": "github" }, "original": { From 05c5f10e201fa4c7a0ae00b47b0276e6b737941a Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 02:50:37 +0000 Subject: [PATCH 082/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index c388c4b4..40d1d505 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1774106929, - "narHash": "sha256-5fyfoxMjPCYhvHvaRUbfjJq94Tg9XsIjzM9p3NnYjLU=", + "lastModified": 1774156137, + "narHash": "sha256-3zv9eQj3rKJO0SRdQNyeIX2GT1UmTzdQOPGj8Pkxldw=", "owner": "caelestia-dots", "repo": "shell", - "rev": "6068e9f08a16580da1300acf1d869be37fbfa984", + "rev": "25515fd32b84ab7320bac305a7c050933747c8c3", "type": "github" }, "original": { From 82a8313ba5d6225d89b4aef05b11ca0fc09c2792 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 02:49:18 +0000 Subject: [PATCH 083/106] [CI] chore: update flake --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 40d1d505..bf7be911 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1774156137, - "narHash": "sha256-3zv9eQj3rKJO0SRdQNyeIX2GT1UmTzdQOPGj8Pkxldw=", + "lastModified": 1774280005, + "narHash": "sha256-Sj28gSYkCgN0ZXpTMYpnpAu4lK51ieCMngOGedB82So=", "owner": "caelestia-dots", "repo": "shell", - "rev": "25515fd32b84ab7320bac305a7c050933747c8c3", + "rev": "501a14bd2a8ab7703aac6a224824a23818552554", "type": "github" }, "original": { @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1773821835, - "narHash": "sha256-TJ3lSQtW0E2JrznGVm8hOQGVpXjJyXY2guAxku2O9A4=", + "lastModified": 1774106199, + "narHash": "sha256-US5Tda2sKmjrg2lNHQL3jRQ6p96cgfWh3J1QBliQ8Ws=", "owner": "nixos", "repo": "nixpkgs", - "rev": "b40629efe5d6ec48dd1efba650c797ddbd39ace0", + "rev": "6c9a78c09ff4d6c21d0319114873508a6ec01655", "type": "github" }, "original": { From 36a0c7f77f58dfd6c1f22cfc0a5168ca3e2b9142 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 02:52:54 +0000 Subject: [PATCH 084/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index bf7be911..32eae885 100644 --- a/flake.lock +++ b/flake.lock @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1774106199, - "narHash": "sha256-US5Tda2sKmjrg2lNHQL3jRQ6p96cgfWh3J1QBliQ8Ws=", + "lastModified": 1774386573, + "narHash": "sha256-4hAV26quOxdC6iyG7kYaZcM3VOskcPUrdCQd/nx8obc=", "owner": "nixos", "repo": "nixpkgs", - "rev": "6c9a78c09ff4d6c21d0319114873508a6ec01655", + "rev": "46db2e09e1d3f113a13c0d7b81e2f221c63b8ce9", "type": "github" }, "original": { From 42db8853b09531c5a6af36cffa6d44d218ef0baf Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 02:56:29 +0000 Subject: [PATCH 085/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 32eae885..d7ba19be 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1774280005, - "narHash": "sha256-Sj28gSYkCgN0ZXpTMYpnpAu4lK51ieCMngOGedB82So=", + "lastModified": 1774505637, + "narHash": "sha256-jXYVt7+4erkMrtM7wLV6ZpJbTVkXAL4iyzetTifgDPU=", "owner": "caelestia-dots", "repo": "shell", - "rev": "501a14bd2a8ab7703aac6a224824a23818552554", + "rev": "6fa80bd85778cff5a9f7c716a9bff4648884919f", "type": "github" }, "original": { From 0302d67863f2dd81cb5005fe9497559f478b0051 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 28 Mar 2026 02:46:03 +0000 Subject: [PATCH 086/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index d7ba19be..2709d737 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1774505637, - "narHash": "sha256-jXYVt7+4erkMrtM7wLV6ZpJbTVkXAL4iyzetTifgDPU=", + "lastModified": 1774597625, + "narHash": "sha256-KV9J2WOCC4bayPhGCb33I8O1nu/WMW+8GyfxJpA/bb8=", "owner": "caelestia-dots", "repo": "shell", - "rev": "6fa80bd85778cff5a9f7c716a9bff4648884919f", + "rev": "49695f8fd0163041e125c7ac72d0c9740676e9fa", "type": "github" }, "original": { From ad203da417084d20aa0211654933e8f7313d13cd Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 29 Mar 2026 03:19:20 +0000 Subject: [PATCH 087/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 2709d737..fc2247f8 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1774597625, - "narHash": "sha256-KV9J2WOCC4bayPhGCb33I8O1nu/WMW+8GyfxJpA/bb8=", + "lastModified": 1774713398, + "narHash": "sha256-tUqYokDDYkozQsHMHwdL/q+1TmDBRxzMSaX5s4D/DS4=", "owner": "caelestia-dots", "repo": "shell", - "rev": "49695f8fd0163041e125c7ac72d0c9740676e9fa", + "rev": "b5f761666db40b71b15291956fc3ba3e283bd781", "type": "github" }, "original": { From a76f595176cace0e36278b0c12796759ad6e4187 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 03:22:12 +0000 Subject: [PATCH 088/106] [CI] chore: update flake --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index fc2247f8..e4f1c088 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1774713398, - "narHash": "sha256-tUqYokDDYkozQsHMHwdL/q+1TmDBRxzMSaX5s4D/DS4=", + "lastModified": 1774796046, + "narHash": "sha256-133xpO5rGVSjEae6k7Iw/GcSodBd35qMNWJrXfhPXRo=", "owner": "caelestia-dots", "repo": "shell", - "rev": "b5f761666db40b71b15291956fc3ba3e283bd781", + "rev": "3bdffca061f0a2b55728f15f20334b5740858f08", "type": "github" }, "original": { @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1774386573, - "narHash": "sha256-4hAV26quOxdC6iyG7kYaZcM3VOskcPUrdCQd/nx8obc=", + "lastModified": 1774709303, + "narHash": "sha256-D3Q07BbIA2KnTcSXIqqu9P586uWxN74zNoCH3h2ESHg=", "owner": "nixos", "repo": "nixpkgs", - "rev": "46db2e09e1d3f113a13c0d7b81e2f221c63b8ce9", + "rev": "8110df5ad7abf5d4c0f6fb0f8f978390e77f9685", "type": "github" }, "original": { From e9d7a5ad82cb9fbcadba6d420504b9562e6feec2 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 02:55:25 +0000 Subject: [PATCH 089/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index e4f1c088..3a127497 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1774796046, - "narHash": "sha256-133xpO5rGVSjEae6k7Iw/GcSodBd35qMNWJrXfhPXRo=", + "lastModified": 1774841756, + "narHash": "sha256-TmyM698bzIaPRSZ5OzgGmQNFHWNwRIfxugcMy/igFdQ=", "owner": "caelestia-dots", "repo": "shell", - "rev": "3bdffca061f0a2b55728f15f20334b5740858f08", + "rev": "3fe54c5be808e702903925df501a62cabfe73a13", "type": "github" }, "original": { From faf996f4776e5c0c7ad1d2f654fc705415ca668f Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 03:26:37 +0000 Subject: [PATCH 090/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 3a127497..0d8d727d 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1774841756, - "narHash": "sha256-TmyM698bzIaPRSZ5OzgGmQNFHWNwRIfxugcMy/igFdQ=", + "lastModified": 1774934956, + "narHash": "sha256-RDbsQKmp0h6zI3N/JzxbCLQG48ic/YITnh6oIAs/6OU=", "owner": "caelestia-dots", "repo": "shell", - "rev": "3fe54c5be808e702903925df501a62cabfe73a13", + "rev": "36aac876cca81c992b7d412d3639ac56fd25bd02", "type": "github" }, "original": { From 39ba0ee97e4f6f4b9f63379b2a6647240ebff5b8 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 02:51:37 +0000 Subject: [PATCH 091/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 0d8d727d..03f82a74 100644 --- a/flake.lock +++ b/flake.lock @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1774709303, - "narHash": "sha256-D3Q07BbIA2KnTcSXIqqu9P586uWxN74zNoCH3h2ESHg=", + "lastModified": 1775036866, + "narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=", "owner": "nixos", "repo": "nixpkgs", - "rev": "8110df5ad7abf5d4c0f6fb0f8f978390e77f9685", + "rev": "6201e203d09599479a3b3450ed24fa81537ebc4e", "type": "github" }, "original": { From d2eeabf9ae986aa2909fd19e82e20c39d1d74a23 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 02:54:20 +0000 Subject: [PATCH 092/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 03f82a74..2eca9fe2 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1774934956, - "narHash": "sha256-RDbsQKmp0h6zI3N/JzxbCLQG48ic/YITnh6oIAs/6OU=", + "lastModified": 1775144602, + "narHash": "sha256-KxnqudOZ/dLPZdMDIuP07eQUVVOVYdFP/m49sbsPCyU=", "owner": "caelestia-dots", "repo": "shell", - "rev": "36aac876cca81c992b7d412d3639ac56fd25bd02", + "rev": "dfc5bcf8b9511fde1fee7de15cdfa79c5f5aeeb6", "type": "github" }, "original": { From acaf95b66c93c1b144f1add5ec087c6e694410de Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 4 Apr 2026 02:45:02 +0000 Subject: [PATCH 093/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 2eca9fe2..9a787858 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1775144602, - "narHash": "sha256-KxnqudOZ/dLPZdMDIuP07eQUVVOVYdFP/m49sbsPCyU=", + "lastModified": 1775190720, + "narHash": "sha256-Mrwi2tNpgYm8SdhzzsOQntOexOqeWNQaJ6nsN+71NYk=", "owner": "caelestia-dots", "repo": "shell", - "rev": "dfc5bcf8b9511fde1fee7de15cdfa79c5f5aeeb6", + "rev": "f924dbfbbb8f1a0fe8c36ccca254063f1bbe0e1b", "type": "github" }, "original": { From e99e65889eda653c06589f9a64b3c89eca4e54f4 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 02:57:24 +0000 Subject: [PATCH 094/106] [CI] chore: update flake --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 9a787858..9e1eb76b 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1775190720, - "narHash": "sha256-Mrwi2tNpgYm8SdhzzsOQntOexOqeWNQaJ6nsN+71NYk=", + "lastModified": 1775480688, + "narHash": "sha256-0Bs5Z39ziq/ZSF3TJIQePVhdy3CjdKvQbbq/gLTRCqw=", "owner": "caelestia-dots", "repo": "shell", - "rev": "f924dbfbbb8f1a0fe8c36ccca254063f1bbe0e1b", + "rev": "612f828b9f84ca0dda76ce009c3ff64f75c5733e", "type": "github" }, "original": { @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1775036866, - "narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=", + "lastModified": 1775423009, + "narHash": "sha256-vPKLpjhIVWdDrfiUM8atW6YkIggCEKdSAlJPzzhkQlw=", "owner": "nixos", "repo": "nixpkgs", - "rev": "6201e203d09599479a3b3450ed24fa81537ebc4e", + "rev": "68d8aa3d661f0e6bd5862291b5bb263b2a6595c9", "type": "github" }, "original": { From 84a364f692d75eb4ebf6a4663f56b14ed3c81491 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 9 Apr 2026 02:53:38 +0000 Subject: [PATCH 095/106] [CI] chore: update flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 9e1eb76b..4789eb7e 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1775480688, - "narHash": "sha256-0Bs5Z39ziq/ZSF3TJIQePVhdy3CjdKvQbbq/gLTRCqw=", + "lastModified": 1775660122, + "narHash": "sha256-qMKB06TE0MY1anDQKBrzZEpktNPyvMxQQzTEEwWAA6I=", "owner": "caelestia-dots", "repo": "shell", - "rev": "612f828b9f84ca0dda76ce009c3ff64f75c5733e", + "rev": "aa2b08dd45963dc9558de94dbff5e1615e347d02", "type": "github" }, "original": { From f9e5dc82ea3c28b76203753d6278d676c2690f0f Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 10 Apr 2026 21:24:51 +1000 Subject: [PATCH 096/106] feat: add caelestia scheme Closes #100 --- .../data/schemes/caelestia/default/dark.txt | 120 ++++++++++++++++++ .../data/schemes/caelestia/default/light.txt | 120 ++++++++++++++++++ 2 files changed, 240 insertions(+) create mode 100644 src/caelestia/data/schemes/caelestia/default/dark.txt create mode 100644 src/caelestia/data/schemes/caelestia/default/light.txt diff --git a/src/caelestia/data/schemes/caelestia/default/dark.txt b/src/caelestia/data/schemes/caelestia/default/dark.txt new file mode 100644 index 00000000..26c30dcd --- /dev/null +++ b/src/caelestia/data/schemes/caelestia/default/dark.txt @@ -0,0 +1,120 @@ +background 0a0f0f +onBackground dce8e6 +surface 0a0f0f +surfaceDim 0a0f0f +surfaceBright 242e2d +surfaceContainerLowest 000000 +surfaceContainerLow 0e1514 +surfaceContainer 131b1a +surfaceContainerHigh 192120 +surfaceContainerHighest 1d2827 +onSurface dce8e6 +surfaceVariant 1d2827 +onSurfaceVariant a2adac +outline 6d7876 +outlineVariant 3f4a49 +inverseSurface f6faf9 +inverseOnSurface 515655 +shadow 000000 +scrim 000000 +surfaceTint 9bd0cc +primary 9bd0cc +primaryDim 8ec2bf +onPrimary 0d4845 +primaryContainer 255b58 +onPrimaryContainer b8ede9 +inversePrimary 336764 +primaryFixed b7ede9 +primaryFixedDim a9deda +onPrimaryFixed 0c4744 +onPrimaryFixedVariant 306461 +secondary b0ccc9 +secondaryDim a3bebc +onSecondary 2c4543 +secondaryContainer 27403e +onSecondaryContainer a9c5c2 +secondaryFixed cce8e5 +secondaryFixedDim bedad7 +onSecondaryFixed 2b4442 +onSecondaryFixedVariant 47605e +tertiary d5efff +tertiaryDim b6e3fe +onTertiary 2e5c72 +tertiaryContainer b6e3fe +onTertiaryContainer 255369 +tertiaryFixed b6e3fe +tertiaryFixedDim a8d5ef +onTertiaryFixed 0b4156 +onTertiaryFixedVariant 2f5d73 +error fa746f +errorDim c54d4a +onError 490006 +errorContainer 871f21 +onErrorContainer ff9993 +primaryPaletteKeyColor 4c807d +secondaryPaletteKeyColor 627c7a +tertiaryPaletteKeyColor 517d94 +neutralPaletteKeyColor 737877 +neutralVariantPaletteKeyColor 6e7978 +errorPaletteKeyColor c84f4c +primary_paletteKeyColor 4c807d +secondary_paletteKeyColor 627c7a +tertiary_paletteKeyColor 517d94 +neutral_paletteKeyColor 737877 +neutral_variant_paletteKeyColor 6e7978 +term0 343434 +term1 769e00 +term2 56e2c0 +term3 81fcce +term4 76b6b3 +term5 7aaee9 +term6 83d8c9 +term7 cddcd3 +term8 9aa59e +term9 85b900 +term10 41f7d0 +term11 cdffe9 +term12 a3c8c3 +term13 a2c0f7 +term14 8bedd9 +term15 ffffff +rosewater f1f3e5 +flamingo e3e4c5 +pink bae2ff +mauve 60cfe8 +red 8ab5ff +maroon abbef0 +peach a9daac +yellow d3fae8 +green 8df1df +teal 9feee7 +sky 93eae9 +sapphire 70d7db +blue 57cdda +lavender 86d9e7 +klink 00969e +klinkSelection 00969e +kvisited 008ca9 +kvisitedSelection 008ca9 +knegative 838f00 +knegativeSelection 838f00 +kneutral 34c359 +kneutralSelection 34c359 +kpositive 00beab +kpositiveSelection 00beab +text dce8e6 +subtext1 a2adac +subtext0 6d7876 +overlay2 5f6967 +overlay1 505958 +overlay0 434b4a +surface2 353d3c +surface1 282e2e +surface0 191f1e +base 0a0f0f +mantle 0a0f0f +crust 090e0e +success B5CCBA +onSuccess 213528 +successContainer 374B3E +onSuccessContainer D1E9D6 diff --git a/src/caelestia/data/schemes/caelestia/default/light.txt b/src/caelestia/data/schemes/caelestia/default/light.txt new file mode 100644 index 00000000..9e2a5b9d --- /dev/null +++ b/src/caelestia/data/schemes/caelestia/default/light.txt @@ -0,0 +1,120 @@ +background f6faf9 +onBackground 2a3433 +surface f6faf9 +surfaceDim d1dcdb +surfaceBright f6faf9 +surfaceContainerLowest ffffff +surfaceContainerLow eef5f3 +surfaceContainer e7f0ee +surfaceContainerHigh e1eae8 +surfaceContainerHighest d9e5e3 +onSurface 2a3433 +surfaceVariant d9e5e3 +onSurfaceVariant 566160 +outline 727d7c +outlineVariant a9b4b3 +inverseSurface 0a0f0f +inverseOnSurface 999e9d +shadow 000000 +scrim 000000 +surfaceTint 1c6a66 +primary 1c6a66 +primaryDim 045d5a +onPrimary e1fffc +primaryContainer a8f0eb +onPrimaryContainer 015c59 +inversePrimary b0f8f3 +primaryFixed a8f0eb +primaryFixedDim 9ae1dc +onPrimaryFixed 004845 +onPrimaryFixedVariant 166663 +secondary 4a6462 +secondaryDim 3e5856 +onSecondary e2fffc +secondaryContainer cce8e5 +onSecondaryContainer 3d5654 +secondaryFixed cce8e5 +secondaryFixedDim bedad7 +onSecondaryFixed 2b4442 +onSecondaryFixedVariant 47605e +tertiary 37647b +tertiaryDim 2a586e +onTertiary f4faff +tertiaryContainer b6e3fe +onTertiaryContainer 255369 +tertiaryFixed b6e3fe +tertiaryFixedDim a8d5ef +onTertiaryFixed 0b4156 +onTertiaryFixedVariant 2f5d73 +error a83836 +errorDim 67040d +onError fff7f6 +errorContainer fa746f +onErrorContainer 6e0a12 +primaryPaletteKeyColor 3a827e +secondaryPaletteKeyColor 627c7a +tertiaryPaletteKeyColor 517d94 +neutralPaletteKeyColor 737877 +neutralVariantPaletteKeyColor 6e7978 +errorPaletteKeyColor c84f4c +primary_paletteKeyColor 3a827e +secondary_paletteKeyColor 627c7a +tertiary_paletteKeyColor 517d94 +neutral_paletteKeyColor 737877 +neutral_variant_paletteKeyColor 6e7978 +term0 9a9b99 +term1 005bcc +term2 00907c +term3 427d3b +term4 269a7a +term5 0071a3 +term6 128f8d +term7 1f2324 +term8 0f0f0f +term9 0071fa +term10 00b49c +term11 5d9954 +term12 52be9c +term13 008cca +term14 45b0ae +term15 25292a +rosewater 6b8647 +flamingo 6f7c1e +pink 0085c0 +mauve 005d6c +red 515900 +maroon 606c00 +peach 198900 +yellow 008f67 +green 007d6d +teal 007573 +sky 00878d +sapphire 008080 +blue 00636d +lavender 007e8b +klink 00969d +klinkSelection 00969e +kvisited 008ca9 +kvisitedSelection 008ca9 +knegative 838f00 +knegativeSelection 838f00 +kneutral 34c359 +kneutralSelection 34c359 +kpositive 00beab +kpositiveSelection 00beac +text 2a3433 +subtext1 566160 +subtext0 727d7c +overlay2 828c8b +overlay1 949d9c +overlay0 a5aead +surface2 b8bfbe +surface1 cbd1d0 +surface0 e1e6e5 +base f6faf9 +mantle eef1f0 +crust e9eceb +success 4F6354 +onSuccess FFFFFF +successContainer D1E8D5 +onSuccessContainer 0C1F13 From a46f26840578f28d37afbc6fd256788f0ab69b9b Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 11 Apr 2026 02:50:24 +0000 Subject: [PATCH 097/106] [CI] chore: update flake --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 4789eb7e..fc8df74f 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1775660122, - "narHash": "sha256-qMKB06TE0MY1anDQKBrzZEpktNPyvMxQQzTEEwWAA6I=", + "lastModified": 1775801889, + "narHash": "sha256-q1LGwhQbNOurIAClh5YwKVU2kJ5lTCxRYZf48bAb9IM=", "owner": "caelestia-dots", "repo": "shell", - "rev": "aa2b08dd45963dc9558de94dbff5e1615e347d02", + "rev": "0e07176ff149d02391531c802b51c28e73185f30", "type": "github" }, "original": { @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1775423009, - "narHash": "sha256-vPKLpjhIVWdDrfiUM8atW6YkIggCEKdSAlJPzzhkQlw=", + "lastModified": 1775710090, + "narHash": "sha256-ar3rofg+awPB8QXDaFJhJ2jJhu+KqN/PRCXeyuXR76E=", "owner": "nixos", "repo": "nixpkgs", - "rev": "68d8aa3d661f0e6bd5862291b5bb263b2a6595c9", + "rev": "4c1018dae018162ec878d42fec712642d214fdfa", "type": "github" }, "original": { From f0ecb6b889977fcdb58ac1435013511e2b789b8d Mon Sep 17 00:00:00 2001 From: Yuka <128908897+yukazakiri@users.noreply.github.com> Date: Sun, 12 Apr 2026 15:28:44 +0800 Subject: [PATCH 098/106] theme: add zed editor theme support (#102) - Add Zed theme template (zed.json) with full Material You syntax highlighting, UI colors, and terminal ANSI palette - Apply theme via file overwrite instead of restarting Zed, so Zed's file watcher picks up changes automatically - Resolve symlinks in apply_zed() only before writing, since Zed's file watcher does not detect changes through symlinks (other themed apps that rely on symlinks are unaffected) --- src/caelestia/data/templates/zed.json | 457 ++++++++++++++++++++++++++ src/caelestia/utils/theme.py | 14 + 2 files changed, 471 insertions(+) create mode 100644 src/caelestia/data/templates/zed.json diff --git a/src/caelestia/data/templates/zed.json b/src/caelestia/data/templates/zed.json new file mode 100644 index 00000000..58eb4117 --- /dev/null +++ b/src/caelestia/data/templates/zed.json @@ -0,0 +1,457 @@ +{ + "$schema": "https://zed.dev/schema/themes/v0.2.0.json", + "name": "Caelestia", + "author": "Caelestia", + "themes": [ + { + "name": "Caelestia", + "appearance": "{{ mode }}", + "style": { + "background": "#{{ surface.hex }}", + "border": "#{{ outlineVariant.hex }}40", + "border.variant": "#{{ outlineVariant.hex }}60", + "border.focused": "#{{ primary.hex }}", + "border.selected": "#{{ primary.hex }}80", + "border.transparent": "#00000000", + "border.disabled": "#{{ outlineVariant.hex }}30", + + "elevated_surface.background": "#{{ surfaceContainerHigh.hex }}", + "surface.background": "#{{ surface.hex }}", + + "element.background": "#{{ outlineVariant.hex }}40", + "element.hover": "#{{ outlineVariant.hex }}60", + "element.active": "#{{ primary.hex }}30", + "element.selected": "#{{ primary.hex }}20", + "element.disabled": "#{{ outlineVariant.hex }}20", + + "drop_target.background": "#{{ primary.hex }}20", + + "ghost_element.background": "#00000000", + "ghost_element.hover": "#{{ outlineVariant.hex }}40", + "ghost_element.active": "#{{ primary.hex }}30", + "ghost_element.selected": "#{{ primary.hex }}20", + "ghost_element.disabled": "#{{ outlineVariant.hex }}20", + + "text": "#{{ onSurface.hex }}", + "text.muted": "#{{ onSurfaceVariant.hex }}", + "text.placeholder": "#{{ outline.hex }}", + "text.disabled": "#{{ outline.hex }}80", + "text.accent": "#{{ primary.hex }}", + + "icon": "#{{ onSurface.hex }}", + "icon.muted": "#{{ onSurfaceVariant.hex }}", + "icon.disabled": "#{{ outlineVariant.hex }}60", + "icon.placeholder": "#{{ onSurfaceVariant.hex }}", + "icon.accent": "#{{ primary.hex }}", + + "status_bar.background": "#{{ surface.hex }}", + "title_bar.background": "#{{ surface.hex }}", + "title_bar.inactive_background": "#{{ surface.hex }}", + "toolbar.background": "#{{ surface.hex }}", + "tab_bar.background": "#{{ surface.hex }}", + "tab.inactive_background": "#{{ surface.hex }}", + "tab.active_background": "#{{ surfaceContainerHigh.hex }}", + + "search.match_background": "#{{ yellow.hex }}40", + + "panel.background": "#{{ surface.hex }}", + "panel.focused_border": "#{{ primary.hex }}", + + "pane.focused_border": "#{{ primary.hex }}", + + "scrollbar.thumb.background": "#{{ outlineVariant.hex }}30", + "scrollbar.thumb.hover_background": "#{{ outlineVariant.hex }}60", + "scrollbar.thumb.border": "#{{ outlineVariant.hex }}20", + "scrollbar.track.background": "#00000000", + "scrollbar.track.border": "#00000000", + + "editor.foreground": "#{{ onSurface.hex }}", + "editor.background": "#{{ surface.hex }}", + "editor.gutter.background": "#{{ surface.hex }}", + "editor.subheader.background": "#{{ surfaceContainer.hex }}", + "editor.active_line.background": "#{{ surfaceContainerHigh.hex }}60", + "editor.highlighted_line.background": "#{{ primary.hex }}15", + "editor.line_number": "#{{ onSurfaceVariant.hex }}", + "editor.active_line_number": "#{{ onSurface.hex }}", + "editor.invisible": "#{{ outlineVariant.hex }}40", + "editor.wrap_guide": "#{{ outlineVariant.hex }}30", + "editor.active_wrap_guide": "#{{ outlineVariant.hex }}60", + "editor.document_highlight.read_background": "#{{ primary.hex }}20", + "editor.document_highlight.write_background": "#{{ primary.hex }}30", + + "terminal.background": "#{{ surface.hex }}", + "terminal.foreground": "#{{ onSurface.hex }}", + "terminal.bright_foreground": "#{{ onSurface.hex }}", + "terminal.dim_foreground": "#{{ onSurfaceVariant.hex }}", + "terminal.ansi.black": "#{{ surface.hex }}", + "terminal.ansi.bright_black": "#{{ onSurfaceVariant.hex }}", + "terminal.ansi.dim_black": "#{{ surface.hex }}80", + "terminal.ansi.red": "#{{ red.hex }}", + "terminal.ansi.bright_red": "#{{ maroon.hex }}", + "terminal.ansi.dim_red": "#{{ red.hex }}80", + "terminal.ansi.green": "#{{ green.hex }}", + "terminal.ansi.bright_green": "#{{ teal.hex }}", + "terminal.ansi.dim_green": "#{{ green.hex }}80", + "terminal.ansi.yellow": "#{{ yellow.hex }}", + "terminal.ansi.bright_yellow": "#{{ peach.hex }}", + "terminal.ansi.dim_yellow": "#{{ yellow.hex }}80", + "terminal.ansi.blue": "#{{ blue.hex }}", + "terminal.ansi.bright_blue": "#{{ sapphire.hex }}", + "terminal.ansi.dim_blue": "#{{ blue.hex }}80", + "terminal.ansi.magenta": "#{{ mauve.hex }}", + "terminal.ansi.bright_magenta": "#{{ pink.hex }}", + "terminal.ansi.dim_magenta": "#{{ mauve.hex }}80", + "terminal.ansi.cyan": "#{{ teal.hex }}", + "terminal.ansi.bright_cyan": "#{{ sky.hex }}", + "terminal.ansi.dim_cyan": "#{{ teal.hex }}80", + "terminal.ansi.white": "#{{ onSurface.hex }}", + "terminal.ansi.bright_white": "#{{ onSurface.hex }}", + "terminal.ansi.dim_white": "#{{ onSurface.hex }}80", + + "link_text.hover": "#{{ primary.hex }}", + + "conflict": "#{{ yellow.hex }}", + "conflict.background": "#{{ yellow.hex }}15", + "conflict.border": "#{{ yellow.hex }}", + + "created": "#{{ green.hex }}", + "created.background": "#{{ green.hex }}15", + "created.border": "#{{ green.hex }}", + + "deleted": "#{{ red.hex }}", + "deleted.background": "#{{ red.hex }}15", + "deleted.border": "#{{ red.hex }}", + + "error": "#{{ error.hex }}", + "error.background": "#{{ error.hex }}15", + "error.border": "#{{ error.hex }}", + + "hidden": "#{{ outline.hex }}", + "hidden.background": "#{{ outline.hex }}15", + "hidden.border": "#{{ outline.hex }}", + + "hint": "#{{ success.hex }}", + "hint.background": "#{{ success.hex }}15", + "hint.border": "#{{ success.hex }}", + + "ignored": "#{{ outline.hex }}", + "ignored.background": "#{{ outline.hex }}15", + "ignored.border": "#{{ outline.hex }}", + + "info": "#{{ blue.hex }}", + "info.background": "#{{ blue.hex }}15", + "info.border": "#{{ blue.hex }}", + + "modified": "#{{ peach.hex }}", + "modified.background": "#{{ peach.hex }}15", + "modified.border": "#{{ peach.hex }}", + + "predictive": "#{{ onSurfaceVariant.hex }}", + "predictive.background": "#{{ onSurfaceVariant.hex }}15", + "predictive.border": "#{{ outlineVariant.hex }}40", + + "renamed": "#{{ teal.hex }}", + "renamed.background": "#{{ teal.hex }}15", + "renamed.border": "#{{ teal.hex }}", + + "success": "#{{ success.hex }}", + "success.background": "#{{ success.hex }}15", + "success.border": "#{{ success.hex }}", + + "unreachable": "#{{ outline.hex }}", + "unreachable.background": "#{{ outline.hex }}15", + "unreachable.border": "#{{ outline.hex }}", + + "warning": "#{{ yellow.hex }}", + "warning.background": "#{{ yellow.hex }}15", + "warning.border": "#{{ yellow.hex }}", + + "players": [ + { + "cursor": "#{{ onSurface.hex }}", + "selection": "#{{ onSurface.hex }}60", + "background": "#{{ primary.hex }}" + }, + { + "cursor": "#{{ teal.hex }}", + "selection": "#{{ teal.hex }}40", + "background": "#{{ teal.hex }}" + }, + { + "cursor": "#{{ pink.hex }}", + "selection": "#{{ pink.hex }}40", + "background": "#{{ pink.hex }}" + }, + { + "cursor": "#{{ yellow.hex }}", + "selection": "#{{ yellow.hex }}40", + "background": "#{{ yellow.hex }}" + }, + { + "cursor": "#{{ green.hex }}", + "selection": "#{{ green.hex }}40", + "background": "#{{ green.hex }}" + }, + { + "cursor": "#{{ red.hex }}", + "selection": "#{{ red.hex }}40", + "background": "#{{ red.hex }}" + }, + { + "cursor": "#{{ blue.hex }}", + "selection": "#{{ blue.hex }}40", + "background": "#{{ blue.hex }}" + }, + { + "cursor": "#{{ maroon.hex }}", + "selection": "#{{ maroon.hex }}40", + "background": "#{{ maroon.hex }}" + } + ], + + "syntax": { + "attribute": { + "color": "#{{ yellow.hex }}", + "font_style": "italic", + "font_weight": null + }, + "boolean": { + "color": "#{{ peach.hex }}", + "font_style": null, + "font_weight": null + }, + "comment": { + "color": "#{{ subtext0.hex }}", + "font_style": "italic", + "font_weight": null + }, + "comment.doc": { + "color": "#{{ subtext0.hex }}", + "font_style": "italic", + "font_weight": null + }, + "constant": { + "color": "#{{ peach.hex }}", + "font_style": null, + "font_weight": null + }, + "constructor": { + "color": "#{{ yellow.hex }}", + "font_style": null, + "font_weight": null + }, + "embedded": { + "color": "#{{ onSurface.hex }}", + "font_style": null, + "font_weight": null + }, + "emphasis": { + "color": "#{{ red.hex }}", + "font_style": "italic", + "font_weight": null + }, + "emphasis.strong": { + "color": "#{{ red.hex }}", + "font_style": null, + "font_weight": 700 + }, + "enum": { + "color": "#{{ yellow.hex }}", + "font_style": null, + "font_weight": null + }, + "function": { + "color": "#{{ blue.hex }}", + "font_style": null, + "font_weight": null + }, + "function.builtin": { + "color": "#{{ teal.hex }}", + "font_style": null, + "font_weight": null + }, + "function.definition": { + "color": "#{{ blue.hex }}", + "font_style": null, + "font_weight": null + }, + "function.method": { + "color": "#{{ blue.hex }}", + "font_style": null, + "font_weight": null + }, + "function.special.definition": { + "color": "#{{ blue.hex }}", + "font_style": null, + "font_weight": null + }, + "hint": { + "color": "#{{ onSurfaceVariant.hex }}", + "font_style": "italic", + "font_weight": null + }, + "keyword": { + "color": "#{{ pink.hex }}", + "font_style": null, + "font_weight": null + }, + "label": { + "color": "#{{ yellow.hex }}", + "font_style": null, + "font_weight": null + }, + "link_text": { + "color": "#{{ blue.hex }}", + "font_style": null, + "font_weight": null + }, + "link_uri": { + "color": "#{{ teal.hex }}", + "font_style": "underline", + "font_weight": null + }, + "number": { + "color": "#{{ peach.hex }}", + "font_style": null, + "font_weight": null + }, + "operator": { + "color": "#{{ sapphire.hex }}", + "font_style": null, + "font_weight": null + }, + "predictive": { + "color": "#{{ onSurfaceVariant.hex }}", + "font_style": "italic", + "font_weight": null + }, + "preproc": { + "color": "#{{ teal.hex }}", + "font_style": null, + "font_weight": null + }, + "primary": { + "color": "#{{ onSurface.hex }}", + "font_style": null, + "font_weight": null + }, + "property": { + "color": "#{{ teal.hex }}", + "font_style": null, + "font_weight": null + }, + "punctuation": { + "color": "#{{ subtext1.hex }}", + "font_style": null, + "font_weight": null + }, + "punctuation.bracket": { + "color": "#{{ subtext1.hex }}", + "font_style": null, + "font_weight": null + }, + "punctuation.delimiter": { + "color": "#{{ subtext1.hex }}", + "font_style": null, + "font_weight": null + }, + "punctuation.list_marker": { + "color": "#{{ teal.hex }}", + "font_style": null, + "font_weight": null + }, + "punctuation.special": { + "color": "#{{ sapphire.hex }}", + "font_style": null, + "font_weight": null + }, + "string": { + "color": "#{{ green.hex }}", + "font_style": null, + "font_weight": null + }, + "string.escape": { + "color": "#{{ pink.hex }}", + "font_style": null, + "font_weight": null + }, + "string.regex": { + "color": "#{{ sky.hex }}", + "font_style": null, + "font_weight": null + }, + "string.special": { + "color": "#{{ green.hex }}", + "font_style": null, + "font_weight": null + }, + "string.special.symbol": { + "color": "#{{ teal.hex }}", + "font_style": null, + "font_weight": null + }, + "tag": { + "color": "#{{ yellow.hex }}", + "font_style": null, + "font_weight": null + }, + "text.literal": { + "color": "#{{ green.hex }}", + "font_style": null, + "font_weight": null + }, + "title": { + "color": "#{{ blue.hex }}", + "font_style": null, + "font_weight": 700 + }, + "type": { + "color": "#{{ yellow.hex }}", + "font_style": null, + "font_weight": null + }, + "type.builtin": { + "color": "#{{ onSurface.hex }}", + "font_style": null, + "font_weight": null + }, + "type.interface": { + "color": "#{{ yellow.hex }}", + "font_style": null, + "font_weight": null + }, + "type.super": { + "color": "#{{ yellow.hex }}", + "font_style": "italic", + "font_weight": null + }, + "variable": { + "color": "#{{ onSurface.hex }}", + "font_style": null, + "font_weight": null + }, + "variable.member": { + "color": "#{{ teal.hex }}", + "font_style": null, + "font_weight": null + }, + "variable.parameter": { + "color": "#{{ teal.hex }}", + "font_style": "italic", + "font_weight": null + }, + "variable.special": { + "color": "#{{ onSurface.hex }}", + "font_style": "italic", + "font_weight": null + }, + "variant": { + "color": "#{{ peach.hex }}", + "font_style": null, + "font_weight": null + } + } + } + } + ] +} diff --git a/src/caelestia/utils/theme.py b/src/caelestia/utils/theme.py index 30e73f81..32e5a025 100644 --- a/src/caelestia/utils/theme.py +++ b/src/caelestia/utils/theme.py @@ -339,6 +339,18 @@ def apply_warp(colours: dict[str, str], mode: str) -> None: write_file(data_dir / "warp-terminal/themes/caelestia.yaml", template) +@log_exception +def apply_zed(colours: dict[str, str], mode: str) -> None: + theme_path = config_dir / "zed/themes/caelestia.json" + # Zed's file watcher does not detect changes through symlinks, + # so resolve to a regular file before writing + if theme_path.is_symlink(): + theme_path.unlink() + + content = gen_replace_dynamic(colours, templates_dir / "zed.json", mode) + write_file(theme_path, content) + + @log_exception def apply_cava(colours: dict[str, str]) -> None: template = gen_replace(colours, templates_dir / "cava.conf", hash=True) @@ -401,6 +413,8 @@ def check(key: str) -> bool: apply_qt(colours, mode) if check("enableWarp"): apply_warp(colours, mode) + if check("enableZed"): + apply_zed(colours, mode) if check("enableCava"): apply_cava(colours) apply_user_templates(colours, mode) From bb2fcede51848564471e5abe8f9e272fd9440abc Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Wed, 15 Apr 2026 02:18:08 +1000 Subject: [PATCH 099/106] fix: remove troublesome resizer rules --- src/caelestia/subcommands/resizer.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/caelestia/subcommands/resizer.py b/src/caelestia/subcommands/resizer.py index 02822657..c9d8fc0d 100644 --- a/src/caelestia/subcommands/resizer.py +++ b/src/caelestia/subcommands/resizer.py @@ -29,8 +29,6 @@ def __init__(self, args: Namespace) -> None: def _load_window_rules(self) -> list[WindowRule]: default_rules = [ WindowRule("(Bitwarden", "titleContains", "20%", "54%", ["float", "center"]), - WindowRule("Sign in - Google Accounts", "titleContains", "35%", "65%", ["float", "center"]), - WindowRule("oauth", "titleContains", "30%", "60%", ["float", "center"]), WindowRule("^[Pp]icture(-| )in(-| )[Pp]icture$", "titleRegex", "", "", ["pip"]), ] From f2e06b3fbc81ce071e652a88d21cf6f14d6eedbb Mon Sep 17 00:00:00 2001 From: Yuka <128908897+yukazakiri@users.noreply.github.com> Date: Wed, 15 Apr 2026 14:20:15 +0800 Subject: [PATCH 100/106] theme: add live theming for chromium-based browsers (#103) * theme: add live theming for chromium-based browsers Writes the surface colour as an RGB triplet to chromium.theme and applies it as a managed browser policy for chromium, brave, and google-chrome-stable using the --refresh-platform-policy flag (introduced in Chrome 142+). Also fixes write_file to set 0644 permissions so browser processes can read the policy files. * feat: use sudo tee not chmod * chore: update readme --------- Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> --- README.md | 70 ++++++++++++++++++++---------------- src/caelestia/utils/theme.py | 37 +++++++++++++++++-- 2 files changed, 75 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index ad3af240..64cfd0d0 100644 --- a/README.md +++ b/README.md @@ -18,35 +18,6 @@ The main control script for the Caelestia dotfiles.
-
Optional dependencies - -- [`papirus-folders`](https://github.com/PapirusDevelopmentTeam/papirus-folders) - automatic folder icon color syncing with theme - -> [!NOTE] -> For automatic Papirus folder icon color syncing, `papirus-folders` needs to be able to run with `sudo` without a password prompt. -> -> **Recommended** - Create a sudoers file: -> -> ```fish -> # Fish shell -> echo "$USER ALL=(ALL) NOPASSWD: "(which papirus-folders) | sudo tee /etc/sudoers.d/papirus-folders -> sudo chmod 440 /etc/sudoers.d/papirus-folders -> ``` -> -> ```sh -> # Bash/other shells -> echo "$USER ALL=(ALL) NOPASSWD: $(which papirus-folders)" | sudo tee /etc/sudoers.d/papirus-folders -> sudo chmod 440 /etc/sudoers.d/papirus-folders -> ``` -> -> **Alternatively** - Edit the main sudoers file by running `sudo visudo` and adding at the end: -> -> ``` -> your_username ALL=(ALL) NOPASSWD: /usr/bin/papirus-folders -> ``` - -
- ## Installation ### Arch linux @@ -122,6 +93,45 @@ sudo python -m installer dist/*.whl sudo cp completions/caelestia.fish /usr/share/fish/vendor_completions.d/caelestia.fish ``` +### Additional steps + +#### Auto folder colour theming + +For automatic Papirus folder icon colour syncing, you must have [`papirus-folders`](https://github.com/PapirusDevelopmentTeam/papirus-folders) +installed, and `papirus-folders` must to be able to run with `sudo` without a password prompt. + +You can allow this by creating a sudoers file: + +```sh +echo "$USER ALL=(ALL) NOPASSWD: $(which papirus-folders)" | sudo tee /etc/sudoers.d/papirus-folders +sudo chmod 440 /etc/sudoers.d/papirus-folders +``` + +#### Chromium-based browser theming + +For live Chromium-based browser theming, the CLI must be allowed to create certain directories in `/etc` +and write to them via `sudo` without a password prompt. + +You can allow this by creating a sudoers file: + +```fish +# Fish shell +for dir in /etc/chromium/policies/managed /etc/brave/policies/managed /etc/opt/chrome/policies/managed + echo "$USER ALL=(ALL) NOPASSWD: $(which mkdir) -p $dir" | sudo tee -a /etc/sudoers.d/caelestia-chromium + echo "$USER ALL=(ALL) NOPASSWD: $(which tee) $dir/caelestia.json" | sudo tee -a /etc/sudoers.d/caelestia-chromium +end +sudo chmod 440 /etc/sudoers.d/caelestia-chromium +``` + +```sh +# Bash/other shells +for dir in /etc/chromium/policies/managed /etc/brave/policies/managed /etc/opt/chrome/policies/managed; do + echo "$USER ALL=(ALL) NOPASSWD: $(which mkdir) -p $dir" | sudo tee -a /etc/sudoers.d/caelestia-chromium + echo "$USER ALL=(ALL) NOPASSWD: $(which tee) $dir/caelestia.json" | sudo tee -a /etc/sudoers.d/caelestia-chromium +done +sudo chmod 440 /etc/sudoers.d/caelestia-chromium +``` + ## Usage All subcommands/options can be explored via the help flag. @@ -151,7 +161,7 @@ subcommands: resizer window resizer daemon ``` -### User Templates +### User templates Custom user templates can be defined in `~/.config/caelestia/templates/`. diff --git a/src/caelestia/utils/theme.py b/src/caelestia/utils/theme.py index 32e5a025..0addb831 100644 --- a/src/caelestia/utils/theme.py +++ b/src/caelestia/utils/theme.py @@ -4,8 +4,6 @@ import shutil import subprocess import tempfile -import shutil -import fcntl from pathlib import Path from caelestia.utils.colour import get_dynamic_colours @@ -340,6 +338,39 @@ def apply_warp(colours: dict[str, str], mode: str) -> None: @log_exception +def apply_chromium(colours: dict[str, str]) -> None: + surface_hex = colours["surface"] + theme_color = f"#{surface_hex}" + browsers = [ + ("chromium", Path("/etc/chromium/policies/managed")), + ("brave", Path("/etc/brave/policies/managed")), + ("google-chrome-stable", Path("/etc/opt/chrome/policies/managed")), + ] + + for cmd, policy_dir in browsers: + if shutil.which(cmd) is None: + continue + if not policy_dir.is_dir(): + subprocess.run(["sudo", "-n", "mkdir", "-p", str(policy_dir)], stderr=subprocess.DEVNULL) + if not policy_dir.is_dir(): + print(f"Unable to create {policy_dir} directory") + continue + + # Use tee instead of write_file cause we need sudo + subprocess.run( + ["sudo", "-n", "tee", str(policy_dir / "caelestia.json")], + input=json.dumps({"BrowserThemeColor": theme_color, "BrowserColorScheme": "device"}), + text=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + subprocess.run( + [cmd, "--refresh-platform-policy", "--no-startup-window"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + + def apply_zed(colours: dict[str, str], mode: str) -> None: theme_path = config_dir / "zed/themes/caelestia.json" # Zed's file watcher does not detect changes through symlinks, @@ -413,6 +444,8 @@ def check(key: str) -> bool: apply_qt(colours, mode) if check("enableWarp"): apply_warp(colours, mode) + if check("enableChromium"): + apply_chromium(colours) if check("enableZed"): apply_zed(colours, mode) if check("enableCava"): From c5eae87e49646d421db79175892258910a34c611 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 19 Apr 2026 16:01:09 +1000 Subject: [PATCH 101/106] docs: add missing theme.enable* opts to example conf --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 64cfd0d0..82dcb1dd 100644 --- a/README.md +++ b/README.md @@ -198,10 +198,17 @@ All configuration options are in `~/.config/caelestia/cli.json`. "enableHypr": true, "enableDiscord": true, "enableSpicetify": true, + "enablePandora": true, "enableFuzzel": true, "enableBtop": true, + "enableNvtop": true, + "enableHtop": true, "enableGtk": true, - "enableQt": true + "enableQt": true, + "enableWarp": true, + "enableChromium": true, + "enableZed": true, + "enableCava": true }, "toggles": { "communication": { From 20669564a4b679bee3cc25cc4397abcc22520db1 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 20 Apr 2026 03:35:55 +0000 Subject: [PATCH 102/106] [CI] chore: update flake --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index fc8df74f..718b29f7 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1775801889, - "narHash": "sha256-q1LGwhQbNOurIAClh5YwKVU2kJ5lTCxRYZf48bAb9IM=", + "lastModified": 1776610813, + "narHash": "sha256-izquCGM5xB4rxvvtRQsgvX483TuWrd5RwkXPcVwUTl0=", "owner": "caelestia-dots", "repo": "shell", - "rev": "0e07176ff149d02391531c802b51c28e73185f30", + "rev": "f77ba920ead8b8e82753c1f66ad417cc734631a3", "type": "github" }, "original": { @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1775710090, - "narHash": "sha256-ar3rofg+awPB8QXDaFJhJ2jJhu+KqN/PRCXeyuXR76E=", + "lastModified": 1776169885, + "narHash": "sha256-l/iNYDZ4bGOAFQY2q8y5OAfBBtrDAaPuRQqWaFHVRXM=", "owner": "nixos", "repo": "nixpkgs", - "rev": "4c1018dae018162ec878d42fec712642d214fdfa", + "rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9", "type": "github" }, "original": { From c0dae189abe7a58b2b650d5c8e62f0a5b886772f Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 21 Apr 2026 03:27:51 +0000 Subject: [PATCH 103/106] [CI] chore: update flake --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 718b29f7..3d815362 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1776610813, - "narHash": "sha256-izquCGM5xB4rxvvtRQsgvX483TuWrd5RwkXPcVwUTl0=", + "lastModified": 1776670101, + "narHash": "sha256-VmPWtG6H+k2tgGnpYwNO5YueHOBdOXXTiBTrjXqcHag=", "owner": "caelestia-dots", "repo": "shell", - "rev": "f77ba920ead8b8e82753c1f66ad417cc734631a3", + "rev": "b94ee8d41bad1ea59395d6184425036fa7121bc5", "type": "github" }, "original": { @@ -24,11 +24,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1776169885, - "narHash": "sha256-l/iNYDZ4bGOAFQY2q8y5OAfBBtrDAaPuRQqWaFHVRXM=", + "lastModified": 1776548001, + "narHash": "sha256-ZSK0NL4a1BwVbbTBoSnWgbJy9HeZFXLYQizjb2DPF24=", "owner": "nixos", "repo": "nixpkgs", - "rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9", + "rev": "b12141ef619e0a9c1c84dc8c684040326f27cdcc", "type": "github" }, "original": { From 03ae9c1aff94cdf1c99c4a4e72f1648cce74f2cc Mon Sep 17 00:00:00 2001 From: Valentine Omonya Date: Tue, 21 Apr 2026 23:05:35 +0300 Subject: [PATCH 104/106] UPdated recording util combined sink --- .envrc | 1 + src/caelestia/subcommands/record.py | 258 +++++++++++++--------------- 2 files changed, 116 insertions(+), 143 deletions(-) diff --git a/.envrc b/.envrc index 612703d4..a04e6171 100644 --- a/.envrc +++ b/.envrc @@ -3,3 +3,4 @@ if has nix; then fi PATH_add bin +cls diff --git a/src/caelestia/subcommands/record.py b/src/caelestia/subcommands/record.py index 3585ea99..8bda3e08 100644 --- a/src/caelestia/subcommands/record.py +++ b/src/caelestia/subcommands/record.py @@ -23,6 +23,10 @@ "combined": "default_output|default_input", } +# Maximum time (in seconds) to wait for the recorder process to exit cleanly +STOP_TIMEOUT = 5.0 +STOP_POLL_INTERVAL = 0.1 + class Command: args: Namespace @@ -31,9 +35,9 @@ def __init__(self, args: Namespace) -> None: self.args = args def run(self) -> None: - if hasattr(self.args, "status") and self.args.status: + if getattr(self.args, "status", False): self.status() - elif hasattr(self.args, "stop") and self.args.stop: + elif getattr(self.args, "stop", False): self.stop() elif self.args.pause: subprocess.run( @@ -45,7 +49,7 @@ def run(self) -> None: self.start() def status(self) -> None: - """Check and display recording status""" + """Print the current recording status.""" if self.proc_running(): print("Recording: RUNNING") else: @@ -67,195 +71,167 @@ def intersects( and a[1] + a[3] > b[1] ) - def get_audio_device(self, audio_mode: str) -> str: - """Get the appropriate audio device for the given mode with fallback handling.""" - if not audio_mode: - return "none" + def get_audio_device(self, audio_mode: str | None) -> str: + """Return the audio device string for the given mode, with fallback handling. + + Returns an empty string when no audio should be recorded. + """ + if not audio_mode or audio_mode == "none": + return "" device = AUDIO_MODES.get(audio_mode, "") - # Check if the device is available - if audio_mode in ["mic", "system", "combined"]: - try: - result = subprocess.run( - ["pactl", "list", "sources", "short"], - capture_output=True, - text=True, - check=True, - ) - available_devices = [ - line.split("\t")[1] - for line in result.stdout.strip().split("\n") - if line - ] - - if device and device not in available_devices: - print( - f"Warning: Audio device '{device}' not available, falling back to default" - ) - if audio_mode == "mic": - input_devices = [ - d - for d in available_devices - if "input" in d.lower() or "mic" in d.lower() - ] - device = input_devices[0] if input_devices else "" - elif audio_mode == "system": - output_devices = [ - d - for d in available_devices - if "output" in d.lower() or "monitor" in d.lower() - ] - device = output_devices[0] if output_devices else "" - except (subprocess.CalledProcessError, FileNotFoundError): + try: + result = subprocess.run( + ["pactl", "list", "sources", "short"], + capture_output=True, + text=True, + check=True, + ) + available_devices = [ + line.split("\t")[1] + for line in result.stdout.strip().split("\n") + if line + ] + + if device and device not in available_devices: print( - "Warning: Could not check audio devices, audio recording may fail" + f"Warning: audio device '{device}' not available, falling back to default" ) - device = "" + if audio_mode == "mic": + candidates = [ + d + for d in available_devices + if "input" in d.lower() or "mic" in d.lower() + ] + device = candidates[0] if candidates else "" + elif audio_mode == "system": + candidates = [ + d + for d in available_devices + if "output" in d.lower() or "monitor" in d.lower() + ] + device = candidates[0] if candidates else "" + + except (subprocess.CalledProcessError, FileNotFoundError): + print("Warning: could not check audio devices, audio recording may fail") return device def get_window_region(self) -> str | None: - """Get window region using Hyprland's client list and slurp for selection.""" + """Select a window via slurp and return its region string.""" try: - # Get all windows from Hyprland clients = json.loads(subprocess.check_output(["hyprctl", "clients", "-j"])) if not clients: print("No windows found") return None - # Create slurp format strings for each window - slurp_regions = [] - for client in clients: - x = client["at"][0] - y = client["at"][1] - w = client["size"][0] - h = client["size"][1] - slurp_regions.append(f"{x},{y} {w}x{h}") - - # Use slurp with predefined regions to pick a window - slurp_input = "\n".join(slurp_regions) + slurp_regions = [ + f"{c['at'][0]},{c['at'][1]} {c['size'][0]}x{c['size'][1]}" + for c in clients + ] + result = subprocess.run( ["slurp", "-f", "%wx%h+%x+%y"], - input=slurp_input, + input="\n".join(slurp_regions), capture_output=True, text=True, ) - if result.returncode != 0: - return None - - return result.stdout.strip() + return result.stdout.strip() if result.returncode == 0 else None except (subprocess.CalledProcessError, json.JSONDecodeError, KeyError) as e: print(f"Error getting window region: {e}") return None + def _max_refresh_rate_for_region( + self, + monitors: list[dict], + region: tuple[int, int, int, int], + ) -> int: + """Return the highest refresh rate among monitors that overlap *region*.""" + max_rr = 0 + for monitor in monitors: + if self.intersects( + (monitor["x"], monitor["y"], monitor["width"], monitor["height"]), + region, + ): + max_rr = max(max_rr, round(monitor["refreshRate"])) + return max_rr + + def _parse_region(self, region_str: str) -> tuple[int, int, int, int]: + """Parse a ``WxH+X+Y`` region string into ``(x, y, w, h)``.""" + m = re.match(r"(\d+)x(\d+)\+(\d+)\+(\d+)", region_str) + if not m: + raise ValueError(f"Invalid region format: {region_str!r}") + w, h, x, y = map(int, m.groups()) + return x, y, w, h + def start(self) -> None: args = ["-w"] - # Get video mode and audio mode from args video_mode = getattr(self.args, "mode", "fullscreen") - audio_mode = getattr(self.args, "audio", "none") + audio_mode = getattr(self.args, "audio", None) monitors = json.loads(subprocess.check_output(["hyprctl", "monitors", "-j"])) - # Handle video modes + # --- Video mode --- if video_mode == "region" or self.args.region: if self.args.region == "slurp" or not self.args.region: - region = subprocess.check_output( + region_str = subprocess.check_output( ["slurp", "-f", "%wx%h+%x+%y"], text=True ).strip() else: - region = self.args.region.strip() - args += ["region", "-region", region] - - m = re.match(r"(\d+)x(\d+)\+(\d+)\+(\d+)", region) - if not m: - raise ValueError(f"Invalid region: {region}") - - w, h, x, y = map(int, m.groups()) - r = x, y, w, h - max_rr = 0 - for monitor in monitors: - if self.intersects( - (monitor["x"], monitor["y"], monitor["width"], monitor["height"]), r - ): - rr = round(monitor["refreshRate"]) - max_rr = max(max_rr, rr) - args += ["-f", str(max_rr)] + region_str = self.args.region.strip() + + x, y, w, h = self._parse_region(region_str) + max_rr = self._max_refresh_rate_for_region(monitors, (x, y, w, h)) + args += ["region", "-region", region_str, "-f", str(max_rr)] elif video_mode == "window": window_info = self.get_window_region() if not window_info: - print("Window selection canceled") + print("Window selection cancelled") return - args += ["region", "-region", window_info] - m = re.match(r"(\d+)x(\d+)\+(\d+)\+(\d+)", window_info) - if not m: - raise ValueError(f"Invalid window region: {window_info}") - - w, h, x, y = map(int, m.groups()) - r = x, y, w, h - - # Calculate max refresh rate for the window region - max_rr = 0 - for monitor in monitors: - if self.intersects( - ( - monitor["x"], - monitor["y"], - monitor["width"], - monitor["height"], - ), - r, - ): - rr = round(monitor["refreshRate"]) - max_rr = max(max_rr, rr) - args += ["-f", str(max_rr)] + x, y, w, h = self._parse_region(window_info) + max_rr = self._max_refresh_rate_for_region(monitors, (x, y, w, h)) + args += ["region", "-region", window_info, "-f", str(max_rr)] else: # fullscreen - focused_monitor = next( - (monitor for monitor in monitors if monitor["focused"]), None - ) - if focused_monitor: - args += [ - focused_monitor["name"], - "-f", - str(round(focused_monitor["refreshRate"])), - ] - - # Handle audio modes + focused = next((m for m in monitors if m["focused"]), None) + if focused: + args += [focused["name"], "-f", str(round(focused["refreshRate"]))] + + # --- Audio mode --- audio_device = self.get_audio_device(audio_mode) if audio_device: args += ["-a", audio_device, "-ac", "opus", "-ab", "192k"] print(f"Recording with audio: {audio_device} ({audio_mode})") - elif hasattr(self.args, "sound") and self.args.sound: + elif getattr(self.args, "sound", False): args += ["-a", "default_output"] else: print("Recording without audio") - # Load extra args from config + # --- Extra args from user config --- try: config = json.loads(user_config_path.read_text()) - if "record" in config and "extraArgs" in config["record"]: - args += config["record"]["extraArgs"] + extra = config.get("record", {}).get("extraArgs", []) + if not isinstance(extra, list): + raise ValueError("Config option 'record.extraArgs' must be an array") + args += extra except (json.JSONDecodeError, FileNotFoundError): pass - except TypeError as e: - raise ValueError( - f"Config option 'record.extraArgs' should be an array: {e}" - ) + # --- Launch recorder --- recording_path.parent.mkdir(parents=True, exist_ok=True) proc = subprocess.Popen( [RECORDER, *args, "-o", str(recording_path)], start_new_session=True ) - # Show notification with mode info - mode_text = f"{video_mode} with {audio_mode if audio_device else 'no'} audio" + audio_label = audio_mode if audio_device else "no audio" + mode_text = f"{video_mode} with {audio_label}" notif = notify("-p", "Recording started", f"Recording {mode_text}...") recording_notif_path.write_text(notif) @@ -268,37 +244,33 @@ def start(self) -> None: f"Command `{' '.join(proc.args)}` failed with exit code {proc.returncode}", ) except subprocess.TimeoutExpired: - pass + pass # Still running — good def stop(self) -> None: - # Start killing recording process subprocess.run(["pkill", "-f", RECORDER], stdout=subprocess.DEVNULL) - # Wait for recording to finish to avoid corrupted video file - max_wait = 50 # Max 5 seconds - wait_count = 0 - while self.proc_running() and wait_count < max_wait: - time.sleep(0.1) - wait_count += 1 + # Wait up to STOP_TIMEOUT seconds for a clean exit + max_polls = int(STOP_TIMEOUT / STOP_POLL_INTERVAL) + for _ in range(max_polls): + if not self.proc_running(): + break + time.sleep(STOP_POLL_INTERVAL) - # Check if file exists before trying to move it if not recording_path.exists(): - print("Warning: No recording file found") + print("Warning: no recording file found") try: close_notification(recording_notif_path.read_text()) except IOError: pass return - # Move to recordings folder - new_path = ( - recordings_dir - / f"recording_{datetime.now().strftime('%Y%m%d_%H-%M-%S')}.mp4" - ) + # Move to recordings folder with a timestamped name + timestamp = datetime.now().strftime("%Y%m%d_%H-%M-%S") + new_path = recordings_dir / f"recording_{timestamp}.mp4" recordings_dir.mkdir(exist_ok=True, parents=True) shutil.move(recording_path, new_path) - # Close start notification + # Dismiss the "recording started" notification try: close_notification(recording_notif_path.read_text()) except IOError: From c2fa4150418a2779dac40f59580466e820299abc Mon Sep 17 00:00:00 2001 From: Valentine Omonya Date: Wed, 22 Apr 2026 18:50:44 +0300 Subject: [PATCH 105/106] Updated screenrecorder to re-encord audio to aac --- src/caelestia/subcommands/record.py | 111 ++++++++++++++++++++-------- 1 file changed, 80 insertions(+), 31 deletions(-) diff --git a/src/caelestia/subcommands/record.py b/src/caelestia/subcommands/record.py index 8bda3e08..03295d6b 100644 --- a/src/caelestia/subcommands/record.py +++ b/src/caelestia/subcommands/record.py @@ -23,7 +23,11 @@ "combined": "default_output|default_input", } -# Maximum time (in seconds) to wait for the recorder process to exit cleanly +# PipeWire/PulseAudio symbolic aliases — never appear literally in `pactl list +# sources short` but are always valid; skip availability checks for these. +SYMBOLIC_DEFAULTS = {"default_input", "default_output", "default_output|default_input"} + +# Maximum time (in seconds) to wait for the recorder process to exit cleanly. STOP_TIMEOUT = 5.0 STOP_POLL_INTERVAL = 0.1 @@ -94,21 +98,21 @@ def get_audio_device(self, audio_mode: str | None) -> str: if line ] - if device and device not in available_devices: + # Symbolic defaults are PipeWire aliases — skip the availability + # check for them since they'll never appear in the source list. + if device and device not in SYMBOLIC_DEFAULTS and device not in available_devices: print( f"Warning: audio device '{device}' not available, falling back to default" ) if audio_mode == "mic": candidates = [ - d - for d in available_devices + d for d in available_devices if "input" in d.lower() or "mic" in d.lower() ] device = candidates[0] if candidates else "" elif audio_mode == "system": candidates = [ - d - for d in available_devices + d for d in available_devices if "output" in d.lower() or "monitor" in d.lower() ] device = candidates[0] if candidates else "" @@ -145,6 +149,14 @@ def get_window_region(self) -> str | None: print(f"Error getting window region: {e}") return None + def _parse_region(self, region_str: str) -> tuple[int, int, int, int]: + """Parse a ``WxH+X+Y`` region string into ``(x, y, w, h)``.""" + m = re.match(r"(\d+)x(\d+)\+(\d+)\+(\d+)", region_str) + if not m: + raise ValueError(f"Invalid region format: {region_str!r}") + w, h, x, y = map(int, m.groups()) + return x, y, w, h + def _max_refresh_rate_for_region( self, monitors: list[dict], @@ -160,14 +172,6 @@ def _max_refresh_rate_for_region( max_rr = max(max_rr, round(monitor["refreshRate"])) return max_rr - def _parse_region(self, region_str: str) -> tuple[int, int, int, int]: - """Parse a ``WxH+X+Y`` region string into ``(x, y, w, h)``.""" - m = re.match(r"(\d+)x(\d+)\+(\d+)\+(\d+)", region_str) - if not m: - raise ValueError(f"Invalid region format: {region_str!r}") - w, h, x, y = map(int, m.groups()) - return x, y, w, h - def start(self) -> None: args = ["-w"] @@ -270,28 +274,73 @@ def stop(self) -> None: recordings_dir.mkdir(exist_ok=True, parents=True) shutil.move(recording_path, new_path) + # Re-encode audio to AAC for compatibility with Premiere, WhatsApp, etc. + # gpu-screen-recorder outputs Opus audio, which many apps don't support. + # -c:v copy means video is never re-encoded, so this only takes ~10-30s + # even for multi-hour recordings. + if shutil.which("ffmpeg") is None: + print("Warning: ffmpeg not found — skipping audio re-encode. " + "Install ffmpeg for Premiere/WhatsApp compatibility.") + else: + fixed_path = recordings_dir / f"recording_{timestamp}_aac.mp4" + result = subprocess.run( + [ + "ffmpeg", "-i", str(new_path), + "-c:v", "copy", # copy video stream — no quality loss + "-c:a", "aac", # re-encode audio to AAC + "-b:a", "192k", + "-movflags", "+faststart", # better compatibility for apps/web + str(fixed_path), + ], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + if result.returncode == 0: + new_path.unlink() # delete the original Opus file + new_path = fixed_path.rename(recordings_dir / f"recording_{timestamp}.mp4") + else: + print("Warning: ffmpeg audio re-encode failed, keeping original file") + # Dismiss the "recording started" notification try: close_notification(recording_notif_path.read_text()) except IOError: pass - # Show completion notification in background (non-blocking) - try: - subprocess.Popen( + # Copy to clipboard if requested + if self.args.clipboard: + file_uri = Path(new_path).resolve().as_uri() + "\n" + subprocess.run( + ["wl-copy", "--type", "text/uri-list"], input=file_uri.encode() + ) + + # Show completion notification and handle user action + action = notify( + "--action=watch=Watch", + "--action=open=Open", + "--action=delete=Delete", + "Recording stopped", + f"Recording saved in {new_path}", + ) + + if action == "watch": + subprocess.Popen(["app2unit", "-O", new_path], start_new_session=True) + elif action == "open": + p = subprocess.run( [ - "notify-send", - "-a", - "caelestia-cli", - "--action=watch=Watch", - "--action=open=Open", - "--action=delete=Delete", - "Recording stopped", - f"Recording saved in {new_path}", - ], - start_new_session=True, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, + "dbus-send", + "--session", + "--dest=org.freedesktop.FileManager1", + "--type=method_call", + "/org/freedesktop/FileManager1", + "org.freedesktop.FileManager1.ShowItems", + f"array:string:file://{new_path}", + "string:", + ] ) - except Exception as e: - print(f"Could not show notification: {e}") + if p.returncode != 0: + subprocess.Popen( + ["app2unit", "-O", new_path.parent], start_new_session=True + ) + elif action == "delete": + new_path.unlink() \ No newline at end of file From 120074034e521750acd52e786e5feeb21bea29ad Mon Sep 17 00:00:00 2001 From: Valentine Omonya Date: Tue, 12 May 2026 18:59:48 +0300 Subject: [PATCH 106/106] Added logging for screenshot --- src/caelestia/subcommands/screenshot.py | 79 ++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 9 deletions(-) diff --git a/src/caelestia/subcommands/screenshot.py b/src/caelestia/subcommands/screenshot.py index 9a8cd607..873616b7 100644 --- a/src/caelestia/subcommands/screenshot.py +++ b/src/caelestia/subcommands/screenshot.py @@ -1,10 +1,19 @@ import subprocess +import time from argparse import Namespace from datetime import datetime - +from pathlib import Path from caelestia.utils.notify import notify from caelestia.utils.paths import screenshots_cache_dir, screenshots_dir +LOG_FILE = Path.home() / ".local" / "share" / "caelestia" / "screenshot.log" + + +def log(msg: str) -> None: + LOG_FILE.parent.mkdir(exist_ok=True, parents=True) + with LOG_FILE.open("a") as f: + f.write(f"[{datetime.now().isoformat()}] {msg}\n") + class Command: args: Namespace @@ -13,30 +22,81 @@ def __init__(self, args: Namespace) -> None: self.args = args def run(self) -> None: + log(f"run() called — args.region={self.args.region!r}") if self.args.region: self.region() else: self.fullscreen() def region(self) -> None: + log(f"region() called — region={self.args.region!r}") if self.args.region == "slurp": - subprocess.run( - ["qs", "-c", "caelestia", "ipc", "call", "picker", "openFreeze" if self.args.freeze else "open"] - ) + cmd = ["qs", "-c", "caelestia", "ipc", "call", "picker", "openFreeze" if self.args.freeze else "open"] + log(f"Firing IPC: {cmd}") + result = subprocess.run(cmd, capture_output=True, text=True) + log(f"IPC returned: returncode={result.returncode} stdout={result.stdout!r} stderr={result.stderr!r}") else: - sc_data = subprocess.check_output(["grim", "-l", "0", "-g", self.args.region.strip(), "-"]) - swappy = subprocess.Popen(["swappy", "-f", "-"], stdin=subprocess.PIPE, start_new_session=True) + self._capture_region(self.args.region) - # Ensure stdin is not None for the type checker + def _capture_region(self, region: str) -> None: + log(f"_capture_region() called — region={region!r}") + try: + sc_data = subprocess.check_output( + ["grim", "-l", "0", "-g", region.strip(), "-"], + stderr=subprocess.PIPE, + ) + log(f"grim succeeded — {len(sc_data)} bytes captured") + except subprocess.CalledProcessError as e: + msg = e.stderr.decode().strip() + log(f"grim CalledProcessError: {msg}") + notify("Screenshot failed", f"grim error: {msg}") + return + except FileNotFoundError: + log("grim not found") + notify("Screenshot failed", "grim not found") + return + + try: + swappy = subprocess.Popen( + ["swappy", "-f", "-"], + stdin=subprocess.PIPE, + start_new_session=True, + stderr=subprocess.PIPE, + ) + log(f"swappy launched — pid={swappy.pid}") if swappy.stdin: swappy.stdin.write(sc_data) swappy.stdin.close() + time.sleep(0.2) + poll = swappy.poll() + if poll is not None: + _, err = swappy.communicate() + msg = err.decode().strip() + log(f"swappy exited early with code {poll}: {msg}") + notify("Screenshot failed", f"swappy exited early: {msg}") + else: + log("swappy running OK") + except FileNotFoundError: + log("swappy not found") + notify("Screenshot failed", "swappy not found") + def fullscreen(self) -> None: - sc_data = subprocess.check_output(["grim", "-"]) + log("fullscreen() called") + try: + sc_data = subprocess.check_output(["grim", "-"], stderr=subprocess.PIPE) + log(f"grim succeeded — {len(sc_data)} bytes") + except subprocess.CalledProcessError as e: + msg = e.stderr.decode().strip() + log(f"grim CalledProcessError: {msg}") + notify("Screenshot failed", f"grim error: {msg}") + return + except FileNotFoundError: + log("grim not found") + notify("Screenshot failed", "grim not found") + return subprocess.run(["wl-copy"], input=sc_data) - dest = screenshots_cache_dir / datetime.now().strftime("%Y%m%d%H%M%S") screenshots_cache_dir.mkdir(exist_ok=True, parents=True) dest.write_bytes(sc_data) @@ -51,6 +111,7 @@ def fullscreen(self) -> None: "Screenshot taken", f"Screenshot stored in {dest} and copied to clipboard", ) + log(f"notify action: {action!r}") if action == "open": subprocess.Popen(["swappy", "-f", dest], start_new_session=True)