diff --git a/home/lib/wallpaper/buildWallpaper.nix b/home/lib/wallpaper/buildWallpaper.nix index 8afb2a9..44eccb4 100644 --- a/home/lib/wallpaper/buildWallpaper.nix +++ b/home/lib/wallpaper/buildWallpaper.nix @@ -11,10 +11,21 @@ let getWallpaper = wallpaper: let - inherit (wallpaper) name path convertMethod; + inherit (wallpaper) + name + baseImageName + path + convertMethod + effects + ; in { - inherit name convertMethod; + inherit + name + baseImageName + convertMethod + effects + ; } // ( if path == null then @@ -25,23 +36,59 @@ let { inherit path; } ); - getName = - path: - baseNameOf path |> match "(.*)\\..*" |> head |> lib.splitString "-" |> tail |> concatStringsSep "-"; + applyEffect = + { + name, + path, + }: + effect: + if hasAttr effect.name config.lib.wallpapers.effects then + config.lib.wallpapers.effects.${effect.name} { inherit name path; } // effect.passthru + else + path; + + applyEffects = + wallpaper: + let + inherit (wallpaper) + name + baseImageName + path + convertMethod + effects + ; + in + { + inherit name baseImageName convertMethod; + } + // ( + if effects == null then + { inherit path; } + else + { + path = foldl' ( + acc: elem: + applyEffect { + inherit name; + path = acc; + } elem + ) path effects; + } + ); generateWallpaper = wallpaper: let inherit (wallpaper) path convertMethod; - # name = getName path; name = match "(.*)\\..*" wallpaper.name |> head; + baseImageName = if wallpaper.baseImageName == null then name else wallpaper.baseImageName; live = (toString path |> match ".*gif$") != null; thisWallpaper = { inherit name path live; }; in { inherit name live; path = - if lib.strings.hasPrefix name config.lib.stylix.colors.scheme then + if lib.strings.hasPrefix baseImageName config.lib.stylix.colors.scheme then path else if convertMethod == "gonord" then goNord thisWallpaper @@ -85,6 +132,7 @@ in lib.wallpapers = { inherit getWallpaper + applyEffects convertWallpaper generateWallpaper setWallpaper diff --git a/home/lib/wallpaper/default.nix b/home/lib/wallpaper/default.nix index 3976759..0464c4f 100644 --- a/home/lib/wallpaper/default.nix +++ b/home/lib/wallpaper/default.nix @@ -3,5 +3,6 @@ ./goNord.nix ./lutgen.nix ./buildWallpaper.nix + ./effects.nix ]; } diff --git a/home/lib/wallpaper/effects.nix b/home/lib/wallpaper/effects.nix new file mode 100644 index 0000000..9bb622e --- /dev/null +++ b/home/lib/wallpaper/effects.nix @@ -0,0 +1,43 @@ +{ pkgs, config, ... }: +{ + lib.wallpapers.effects = { + hydrogen = + { + name, + path, + cropAspectRatio ? with config.lib.monitors.mainMonitor.mode; "${toString width}x${toString height}", + extraArguments ? "", + }: + pkgs.runCommand name { buildInputs = [ pkgs.imagemagick ]; } '' + ${pkgs.python3}/bin/python ${./hydrogen.py} -i ${path} --aspect-ratio-crop ${cropAspectRatio} -o $out ${extraArguments} + ''; + vignette = + { + name, + path, + extraArguments ? "-d 100 -o 20", + }: + let + # Fred's ImageMagick script + fredVignette = pkgs.stdenv.mkDerivation { + name = "vignette"; + src = pkgs.fetchurl { + url = "http://www.fmwconcepts.com/imagemagick/downloadcounter.php?scriptname=vignette3&dirname=vignette3"; + sha256 = "XlisDt8FTK20UY3R+nqaTP96rHM3fE20wKM1sT6udrM="; + }; + nativeBuildInputs = [ pkgs.bash ]; + dontUnpack = true; + installPhase = '' + install -Dm755 $src $out/bin/vignette + ''; + }; + in + pkgs.runCommand name + { + buildInputs = [ pkgs.imagemagick ]; + } + '' + ${fredVignette}/bin/vignette ${extraArguments} ${path} $out + ''; + }; +} diff --git a/home/lib/wallpaper/hydrogen.py b/home/lib/wallpaper/hydrogen.py new file mode 100644 index 0000000..e51ccb5 --- /dev/null +++ b/home/lib/wallpaper/hydrogen.py @@ -0,0 +1,265 @@ +# Generated by DeepSeek and Gemini +import argparse +import subprocess +import os +import shlex +import re # Import re for parsing the aspect ratio string + + +def get_image_dimensions(image_path): + """Gets the width and height of an image using ImageMagick identify.""" + # Append [0] to the image path to select the first frame, + # which works for both single-frame and multi-frame images. + image_path_with_frame = f"{image_path}[0]" + + # Construct the command list correctly + cmd = ["magick", "identify", "-format", "%wx%h", image_path_with_frame] + # ^-- image_path_with_frame is now a single string element in the list + + try: + process = subprocess.run(cmd, check=True, capture_output=True, text=True) + output = process.stdout.strip() + if "x" in output: + # Handle potential output like '1920x1080+0+0' from some identify versions + # by splitting only on 'x' and taking the first two parts. + dims = output.split("x") + width = int(dims[0]) + height = int(dims[1].split("+")[0]) # Handle potential offset info + return width, height + else: + print( + f"Warning: Could not parse dimensions from identify output: '{output}' for image '{image_path}'" + ) + return None, None + except subprocess.CalledProcessError as e: + print(f"Error running identify: {e}") + # Use cmd here, not the base command without the modified path + print(f"Command failed: {' '.join(shlex.quote(c) for c in cmd)}") + print("ImageMagick Error Output:\n", e.stderr) + return None, None + except FileNotFoundError: + print( + "Error: 'magick' command not found. Is ImageMagick installed and in your PATH?" + ) + return None, None + except ValueError as e: + print(f"Error parsing dimensions '{output}' from identify output: {e}") + return None, None + except Exception as e: + # Catching the specific error from the traceback helps debugging + print(f"An unexpected error occurred during identify ({type(e).__name__}): {e}") + return None, None + + +def calculate_centered_crop(orig_w, orig_h, target_aspect_ratio): + """Calculates the WxH for a centered crop preserving target aspect ratio.""" + orig_aspect_ratio = orig_w / orig_h + + if ( + abs(orig_aspect_ratio - target_aspect_ratio) < 1e-6 + ): # Check for floating point equality + # Already the correct aspect ratio, no crop needed based on ratio + return f"{orig_w}x{orig_h}+0+0" + + if orig_aspect_ratio > target_aspect_ratio: + # Original image is wider than target: crop width + crop_h = orig_h + crop_w = int(round(orig_h * target_aspect_ratio)) + else: + # Original image is taller than target: crop height + crop_w = orig_w + crop_h = int(round(orig_w / target_aspect_ratio)) + + # Return geometry string suitable for -crop with -gravity Center + return f"{crop_w}x{crop_h}+0+0" + + +def main(): + parser = argparse.ArgumentParser( + description="Optionally crop an image (manually or by aspect ratio), then overlay it on its blurred version with a shadow." + ) + parser.add_argument( + "-i", "--input", required=True, type=str, help="Input image file" + ) + parser.add_argument( + "-o", "--output", type=str, default="output.jpg", help="Output image file" + ) + + # --- Cropping Arguments Group --- + crop_group = ( + parser.add_mutually_exclusive_group() + ) # Ensure only one crop method is used + crop_group.add_argument( + "-c", + "--crop", + type=str, + default=None, + help="Crop geometry BEFORE processing (e.g., '600x400+100+50'). See ImageMagick -crop documentation. Incompatible with --aspect-ratio-crop.", + ) + crop_group.add_argument( + "--aspect-ratio-crop", + metavar="WxH", # Indicate expected format in help + type=str, + default=None, + help="Automatically crop to maintain the aspect ratio given by WxH (e.g., '16x9' or '2240x1400'), centering the crop. Incompatible with --crop.", + ) + # --- End of Cropping Arguments --- + + parser.add_argument( + "-s", + "--scale", + type=float, + default=0.8, + help="Scale factor for the overlay (e.g., 0.8 for 80%% of original size)", + ) + parser.add_argument( + "--blur-arguments", + type=str, + default="0x20", + help="Arguments for the blur (e.g., '0x10' for sigma 10)", + ) + parser.add_argument( + "--shadow-arguments", + type=str, + default="80x40+0+0", + help="Arguments for the shadow (e.g., '80x3+0+0')", + ) + + args = parser.parse_args() + + if not os.path.isfile(args.input): + print(f"Error: Input file '{args.input}' not found or is not a file.") + exit(1) + + scale_percent = args.scale * 100 + crop_geometry_to_use = None + use_gravity_center_for_crop = False + + # --- Determine Crop Geometry --- + if args.aspect_ratio_crop: + # Validate and parse the aspect ratio string + match = re.fullmatch(r"(\d+)[x:](\d+)", args.aspect_ratio_crop) + if not match: + print( + f"Error: Invalid format for --aspect-ratio-crop '{args.aspect_ratio_crop}'. Use WxH or W:H (e.g., '16x9')." + ) + exit(1) + try: + target_w = int(match.group(1)) + target_h = int(match.group(2)) + if target_w <= 0 or target_h <= 0: + raise ValueError("Width and height must be positive.") + target_aspect_ratio = target_w / target_h + except (ValueError, ZeroDivisionError) as e: + print( + f"Error: Invalid values in --aspect-ratio-crop '{args.aspect_ratio_crop}': {e}" + ) + exit(1) + + # Get original dimensions + orig_w, orig_h = get_image_dimensions(args.input) + if orig_w is None or orig_h is None: + print( + "Error: Could not determine image dimensions. Cannot perform aspect ratio crop." + ) + exit(1) + + # Calculate the crop needed + crop_geometry_to_use = calculate_centered_crop( + orig_w, orig_h, target_aspect_ratio + ) + use_gravity_center_for_crop = True # Center this type of crop + print( + f"Calculated aspect ratio crop geometry: {crop_geometry_to_use} (centered)" + ) + + elif args.crop: + # Use the manually specified crop geometry + # We assume the user knows what they're doing with offsets etc. + # Manual crop typically defaults to top-left unless gravity is set elsewhere. + crop_geometry_to_use = args.crop + use_gravity_center_for_crop = False # Do not force center for manual crop + print(f"Using manual crop geometry: {crop_geometry_to_use}") + # --- End Determine Crop Geometry --- + + # --- Build the command list dynamically --- + cmd = ["magick"] + + # 1. Add the input file + cmd.append(args.input) + + # 2. Add cropping if specified + if crop_geometry_to_use: + if use_gravity_center_for_crop: + # Aspect ratio crop needs gravity center first + cmd.extend(["-gravity", "Center"]) + cmd.extend(["-crop", crop_geometry_to_use]) + cmd.append("+repage") # Reset page geometry after crop + + # 3. Add the rest of the processing steps + cmd.extend( + [ + # These steps now operate on the (potentially cropped) image + "(", + "+clone", # Clone the (potentially cropped) image for blurring + "-blur", + args.blur_arguments, + ")", # Result: [cropped_image, blurred_background] + "(", + "-clone", + "0", # Clone the original (potentially cropped) image again + "-resize", + f"{scale_percent}%", # Resize this clone + # Result: [cropped_image, blurred_background, resized_overlay] + "(", + "+clone", # Clone the resized overlay to create shadow from + "-background", + "black", + "-shadow", + args.shadow_arguments, + ")", # Result: [cropped_image, blurred_background, resized_overlay, shadow] + "+swap", # Swap shadow and resized overlay + # Result: [cropped_image, blurred_background, shadow, resized_overlay] + "-background", + "none", + "-layers", + "merge", # Merge shadow onto resized overlay + "+repage", # Reset page geometry after merge + ")", # Result: [cropped_image, blurred_background, overlay_with_shadow] + "-delete", + "0", # Delete the original (potentially cropped) image at index 0 + # Result: [blurred_background, overlay_with_shadow] + "-gravity", # <<< This gravity is for the FINAL composite step + "center", + "-compose", + "over", + "-composite", # Composite overlay_with_shadow onto blurred_background + args.output, # Specify the output file + ] + ) + # --- End of command building --- + + print(f"Executing command: {' '.join(shlex.quote(c) for c in cmd)}") + + try: + # Use stderr=subprocess.PIPE to capture errors from magick + process = subprocess.run(cmd, check=True, capture_output=True, text=True) + print(f"Successfully created {args.output}") + if process.stdout: # Print stdout from magick if any + print("ImageMagick Output:\n", process.stdout) + if process.stderr: + print("ImageMagick Warnings/Messages:\n", process.stderr) + except subprocess.CalledProcessError as e: + print(f"Error processing image: {e}") + print(f"Command failed: {' '.join(shlex.quote(c) for c in cmd)}") + print("ImageMagick Error Output:\n", e.stderr) # Print stderr on error + except FileNotFoundError: + print( + "Error: 'magick' command not found. Is ImageMagick installed and in your PATH?" + ) + except Exception as e: + print(f"An unexpected error occurred: {e}") + + +if __name__ == "__main__": + main() diff --git a/home/programs/coding/python.nix b/home/programs/coding/python.nix index 37aaacf..0526e19 100644 --- a/home/programs/coding/python.nix +++ b/home/programs/coding/python.nix @@ -1,12 +1,12 @@ { pkgs, ... }: { home.packages = with pkgs; [ - python312 - python312Packages.pip - python312Packages.virtualenv - python312Packages.numpy - python312Packages.matplotlib - python312Packages.pandas + python3 + python3Packages.pip + python3Packages.virtualenv + python3Packages.numpy + python3Packages.matplotlib + python3Packages.pandas black conda ]; diff --git a/home/tweaks/wallpaper.nix b/home/tweaks/wallpaper.nix index 93427ed..c0c1075 100644 --- a/home/tweaks/wallpaper.nix +++ b/home/tweaks/wallpaper.nix @@ -1,3 +1,4 @@ +{ pkgs, ... }: { wallpapers = [ { @@ -8,6 +9,23 @@ name = "frieren-butterflies.jpg"; convertMethod = "lutgen"; } + { + name = "frieren-butterflies-hydrogen.jpg"; + baseImageName = "frieren-butterflies"; + path = "${pkgs.wallpapers}/frieren-butterflies.jpg"; + convertMethod = "lutgen"; + effects = [ + { + name = "hydrogen"; + passthru = { + extraArguments = "--shadow-arguments '80x50+0+0'"; + }; + } + { + name = "vignette"; + } + ]; + } { name = "frieren-fire.jpg"; convertMethod = "lutgen"; @@ -28,6 +46,17 @@ name = "bangqiaoyan-girl-sky.jpg"; convertMethod = "gonord"; } + { + name = "bangqiaoyan-girl-sky-hydrogen.jpg"; + baseImageName = "bangqiaoyan-girl-sky"; + path = "${pkgs.wallpapers}/bangqiaoyan-girl-sky.jpg"; + convertMethod = "gonord"; + effects = [ + { + name = "hydrogen"; + } + ]; + } { name = "morncolour-pink-landscape.png"; convertMethod = "gonord"; diff --git a/modules/home-manager/monitors.nix b/modules/home-manager/monitors.nix index 8ab7fd7..0d70fbb 100644 --- a/modules/home-manager/monitors.nix +++ b/modules/home-manager/monitors.nix @@ -65,4 +65,5 @@ in |> builtins.head; config.lib.monitors.otherMonitorsNames = builtins.attrNames config.monitors |> builtins.filter (name: !config.monitors.${name}.isMain); + config.lib.monitors.mainMonitor = config.monitors.${config.lib.monitors.mainMonitorName}; } diff --git a/modules/home-manager/wallpaper.nix b/modules/home-manager/wallpaper.nix index 26e656f..6f7b43f 100644 --- a/modules/home-manager/wallpaper.nix +++ b/modules/home-manager/wallpaper.nix @@ -3,12 +3,18 @@ with lib; with types; with config.lib.wallpapers; let + cfg = config.wallpapers; wallpaper = submodule { options = { name = mkOption { type = str; description = "Name of the wallpaper"; }; + baseImageName = mkOption { + type = nullOr str; + description = "Name of the base image"; + default = null; + }; path = mkOption { type = nullOr path; description = "Path to the wallpaper, ${pkgs.wallpapers}/name by default"; @@ -17,7 +23,27 @@ let convertMethod = mkOption { type = str; description = "Method to convert the wallpaper (gonord, lutgen, none)"; - default = "lutgen"; + default = "gonord"; + }; + effects = mkOption { + type = nullOr ( + listOf (submodule { + options = { + name = mkOption { + type = nullOr str; + description = "Name of the effect to apply"; + default = null; + }; + passthru = mkOption { + type = attrs; + description = "Extra arguments to pass to the effect"; + default = { }; + }; + }; + }) + ); + description = "List of effects to apply to the wallpaper"; + default = null; }; }; }; @@ -30,8 +56,9 @@ in config = let - wallpapers = map getWallpaper config.wallpapers; - generatedWallpapers = map generateWallpaper wallpapers; + wallpapers = map getWallpaper cfg; + wallpapersWithEffects = map applyEffects wallpapers; + generatedWallpapers = map generateWallpaper wallpapersWithEffects; normalWallpapers = map setWallpaper generatedWallpapers |> builtins.listToAttrs; blurredWallpapers = map blurWallpaper generatedWallpapers |> builtins.listToAttrs; in