diff --git a/flake.lock b/flake.lock index 194a51f..a196916 100644 --- a/flake.lock +++ b/flake.lock @@ -44,6 +44,29 @@ "type": "github" } }, + "chaotic": { + "inputs": { + "fenix": "fenix", + "flake-schemas": "flake-schemas", + "home-manager": "home-manager", + "jovian": "jovian", + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1739212779, + "narHash": "sha256-7U7fOAOVy/AaOtw3HflnwEeXZJ9+ldxVU/Mx5tGN9A4=", + "owner": "chaotic-cx", + "repo": "nyx", + "rev": "175a7f545d07bd08c14709f0d0849a8cddaaf460", + "type": "github" + }, + "original": { + "owner": "chaotic-cx", + "ref": "nyxpkgs-unstable", + "repo": "nyx", + "type": "github" + } + }, "crane": { "locked": { "lastModified": 1731098351, @@ -59,6 +82,28 @@ "type": "github" } }, + "fenix": { + "inputs": { + "nixpkgs": [ + "chaotic", + "nixpkgs" + ], + "rust-analyzer-src": "rust-analyzer-src" + }, + "locked": { + "lastModified": 1739082714, + "narHash": "sha256-cylMa750pId3Hqvzyurd86qJIYyyMWB0M7Gbh7ZB2tY=", + "owner": "nix-community", + "repo": "fenix", + "rev": "e84058a7fe56aa01f2db19373cce190098494698", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "fenix", + "type": "github" + } + }, "flake-compat": { "flake": false, "locked": { @@ -145,6 +190,20 @@ "type": "github" } }, + "flake-schemas": { + "locked": { + "lastModified": 1721999734, + "narHash": "sha256-G5CxYeJVm4lcEtaO87LKzOsVnWeTcHGKbKxNamNWgOw=", + "rev": "0a5c42297d870156d9c57d8f99e476b738dcd982", + "revCount": 75, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/DeterminateSystems/flake-schemas/0.1.5/0190ef2f-61e0-794b-ba14-e82f225e55e6/source.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://flakehub.com/f/DeterminateSystems/flake-schemas/%3D0.1.5.tar.gz" + } + }, "flake-utils": { "inputs": { "systems": [ @@ -209,7 +268,8 @@ "home-manager": { "inputs": { "nixpkgs": [ - "nixpkgs-unstable" + "chaotic", + "nixpkgs" ] }, "locked": { @@ -247,6 +307,26 @@ "type": "github" } }, + "home-manager_2": { + "inputs": { + "nixpkgs": [ + "nixpkgs-unstable" + ] + }, + "locked": { + "lastModified": 1738610386, + "narHash": "sha256-yb6a5efA1e8xze1vcdN2HBxqYr340EsxFMrDUHL3WZM=", + "owner": "nix-community", + "repo": "home-manager", + "rev": "066ba0c5cfddbc9e0dddaec73b1561ad38aa8abe", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "home-manager", + "type": "github" + } + }, "impermanence": { "locked": { "lastModified": 1737831083, @@ -265,7 +345,29 @@ "jovian": { "inputs": { "nix-github-actions": "nix-github-actions_2", - "nixpkgs": "nixpkgs" + "nixpkgs": [ + "chaotic", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1738875499, + "narHash": "sha256-P3VbO2IkEW+0d0pJU7CuX8e+obSoiDw/YCVL1mnA26w=", + "owner": "Jovian-Experiments", + "repo": "Jovian-NixOS", + "rev": "4642ec1073a7417e6303484d8f2e7d29dc24a50f", + "type": "github" + }, + "original": { + "owner": "Jovian-Experiments", + "repo": "Jovian-NixOS", + "type": "github" + } + }, + "jovian_2": { + "inputs": { + "nix-github-actions": "nix-github-actions_3", + "nixpkgs": "nixpkgs_2" }, "locked": { "lastModified": 1739640234, @@ -310,7 +412,7 @@ "manyfold": { "inputs": { "flake-utils": "flake-utils_2", - "nixpkgs": "nixpkgs_2" + "nixpkgs": "nixpkgs_3" }, "locked": { "lastModified": 1735525498, @@ -354,7 +456,7 @@ }, "nix-darwin": { "inputs": { - "nixpkgs": "nixpkgs_3" + "nixpkgs": "nixpkgs_4" }, "locked": { "lastModified": 1739548217, @@ -393,6 +495,29 @@ } }, "nix-github-actions_2": { + "inputs": { + "nixpkgs": [ + "chaotic", + "jovian", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1729697500, + "narHash": "sha256-VFTWrbzDlZyFHHb1AlKRiD/qqCJIripXKiCSFS8fAOY=", + "owner": "zhaofengli", + "repo": "nix-github-actions", + "rev": "e418aeb728b6aa5ca8c5c71974e7159c2df1d8cf", + "type": "github" + }, + "original": { + "owner": "zhaofengli", + "ref": "matrix-name", + "repo": "nix-github-actions", + "type": "github" + } + }, + "nix-github-actions_3": { "inputs": { "nixpkgs": [ "jovian", @@ -417,7 +542,7 @@ "nixos-apple-silicon": { "inputs": { "flake-compat": "flake-compat_3", - "nixpkgs": "nixpkgs_4", + "nixpkgs": "nixpkgs_5", "rust-overlay": "rust-overlay_2" }, "locked": { @@ -527,6 +652,22 @@ } }, "nixpkgs_2": { + "locked": { + "lastModified": 1738546358, + "narHash": "sha256-nLivjIygCiqLp5QcL7l56Tca/elVqM9FG1hGd9ZSsrg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "c6e957d81b96751a3d5967a0fd73694f303cc914", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_3": { "locked": { "lastModified": 1735291276, "narHash": "sha256-NYVcA06+blsLG6wpAbSPTCyLvxD/92Hy4vlY9WxFI1M=", @@ -542,7 +683,7 @@ "type": "github" } }, - "nixpkgs_3": { + "nixpkgs_4": { "locked": { "lastModified": 1736241350, "narHash": "sha256-CHd7yhaDigUuJyDeX0SADbTM9FXfiWaeNyY34FL1wQU=", @@ -558,7 +699,7 @@ "type": "github" } }, - "nixpkgs_4": { + "nixpkgs_5": { "locked": { "lastModified": 1738410390, "narHash": "sha256-xvTo0Aw0+veek7hvEVLzErmJyQkEcRk6PSR4zsRQFEc=", @@ -635,10 +776,11 @@ "root": { "inputs": { "authentik-nix": "authentik-nix", - "home-manager": "home-manager", + "chaotic": "chaotic", + "home-manager": "home-manager_2", "home-manager-stable": "home-manager-stable", "impermanence": "impermanence", - "jovian": "jovian", + "jovian": "jovian_2", "lanzaboote": "lanzaboote", "manyfold": "manyfold", "nix-darwin": "nix-darwin", @@ -649,6 +791,23 @@ "sops-nix": "sops-nix" } }, + "rust-analyzer-src": { + "flake": false, + "locked": { + "lastModified": 1738997488, + "narHash": "sha256-jeNdFVtEDLypGIbNqBjURovfw9hMkVtlLR7j/5fRh54=", + "owner": "rust-lang", + "repo": "rust-analyzer", + "rev": "208bc52b5dc177badc081c64eb0584a313c73242", + "type": "github" + }, + "original": { + "owner": "rust-lang", + "ref": "nightly", + "repo": "rust-analyzer", + "type": "github" + } + }, "rust-overlay": { "inputs": { "nixpkgs": [ diff --git a/flake.nix b/flake.nix index c74e9d0..53604be 100644 --- a/flake.nix +++ b/flake.nix @@ -19,7 +19,7 @@ }; # Chaotic-nix - # chaotic.url = "github:chaotic-cx/nyx/nyxpkgs-unstable"; + chaotic.url = "github:chaotic-cx/nyx/nyxpkgs-unstable"; # Impermenance impermanence.url = "github:nix-community/impermanence"; @@ -72,7 +72,7 @@ nixpkgs-unstable, # nixpkgs-unstable-small, nixpkgs-stable, - # chaotic, + chaotic, lanzaboote, impermanence, home-manager, @@ -211,6 +211,8 @@ sops-nix.nixosModules.sops jovian.nixosModules.jovian + + chaotic.nixosModules.default ]; }; }; diff --git a/hosts/deck/boot.nix b/hosts/deck/boot.nix index 25fd2f7..ce478f2 100644 --- a/hosts/deck/boot.nix +++ b/hosts/deck/boot.nix @@ -1,6 +1,6 @@ { pkgs, ... }: let - kernel = pkgs.linuxPackages_latest; + kernel = pkgs.linuxPackages_cachyos; in { # Configure bootloader with lanzaboot and secureboot diff --git a/hosts/deck/configuration.nix b/hosts/deck/configuration.nix index c223f3c..a7d8082 100644 --- a/hosts/deck/configuration.nix +++ b/hosts/deck/configuration.nix @@ -9,6 +9,8 @@ [ # Include the results of the hardware scan. ./boot.nix ./jovian.nix + ./steam-shortcuts.nix + # ./steam-game ./hardware-configuration.nix ]; @@ -134,6 +136,8 @@ }; }; + chaotic.mesa-git.enable = true; + # List packages installed in system profile. To search, run: # $ nix search wget environment = { @@ -141,6 +145,7 @@ fuse jq newt + maliit-keyboard onlyoffice-bin python3 rsync @@ -160,6 +165,27 @@ }; }; + # services.steamRomManager = { + # enable = true; + # user = "deck"; + + # romSources = [ + # { + # name = "Nintendo Switch ROMs"; + # path = "/home/deck/Emulation/roms/switch"; + # platform = "nintendoswitch"; + # emulator = "ryujinx"; + # } + # ]; + + # emulators = [ + # { + # name = "Ryujinx"; + # executable = "${pkgs.ryujinx-greemdev}/bin/ryujinx"; + # } + # ]; + # }; + # Some programs need SUID wrappers, can be configured further or are # started in user sessions. # programs.mtr.enable = true; diff --git a/hosts/deck/steam-games.nix b/hosts/deck/steam-games.nix new file mode 100644 index 0000000..bfaffd4 --- /dev/null +++ b/hosts/deck/steam-games.nix @@ -0,0 +1,35 @@ +{ pkgs, ... }: +{ + # You'll need to get an API key from https://www.steamgriddb.com/profile/preferences/api + steamGridDbApiKey = "ada6729c6cd321a4e3792ddd0b83c1ba"; + # List your non-Steam games here + games = [ + { + name = "Ryujinx"; + path = "${pkgs.ryujinx-greemdev}/bin/ryujinx"; + extraLaunchOptions = ""; + steamGridDbIds = { + grid = "121085"; + hero = "63924"; + logo = "29735"; + list = "5517"; + }; + } + { + name = "Prismlauncher"; + path = "${pkgs.prismlauncher}/bin/prismlauncher"; + extraLaunchOptions = ""; + steamGridDbIds = { + grid = "267141"; + hero = "64303"; + logo = "68299"; + list = "23582"; + }; + } + { + name = "Mario Kart 8"; + path = "${pkgs.ryujinx-greemdev}/bin/ryujinx"; + extraLaunchOptions = "--fullscreen '/home/deck/Emulation/roms/switch/Mario Kart 8 Deluxe [B+U720896].nsp'"; + } + ]; +} \ No newline at end of file diff --git a/hosts/deck/steam-shortcuts.nix b/hosts/deck/steam-shortcuts.nix new file mode 100644 index 0000000..a39b410 --- /dev/null +++ b/hosts/deck/steam-shortcuts.nix @@ -0,0 +1,371 @@ +{ config, lib, pkgs, ... }: + +let + # Import the games configuration + gamesConfig = import ./steam-games.nix { inherit pkgs; }; + + # Create the VDF generator script + vdfGeneratorScript = pkgs.writeText "generate_vdf.py" '' + #!/usr/bin/env python3 + import sys + import json + import struct + import time + import hashlib + + def generate_app_id(name): + """Generate Steam app ID for a game using Steam's apparent format.""" + # Get the current timestamp + timestamp = int(time.time()) + + # Create a hash of the name and timestamp + hash_input = f"{name}{timestamp}".encode('utf-8') + hash_obj = hashlib.md5(hash_input) + + # Take first 6 bytes of hash and combine with a fixed pattern + hash_bytes = hash_obj.digest()[:6] + + # Create a 64-bit number where: + # - First 6 bytes come from hash + # - Last 2 bytes are zeros (matching Steam's pattern) + full_bytes = hash_bytes + b'\x00\x00' + + # Convert to 64-bit integer + app_id = struct.unpack(">Q", full_bytes)[0] + + # Ensure the number is in the correct range by setting specific bits + app_id |= (1 << 62) # Set the second-to-highest bit + app_id &= ~(1 << 63) # Clear the highest bit + + return app_id + + def write_vdf(shortcuts): + """Write VDF format matching the exact byte sequence.""" + output = bytearray() + + # Header + output.extend(b'\x00shortcuts\x00') + + for i, shortcut in enumerate(shortcuts): + # Index + output.extend(f'\x00{i}\x00'.encode('utf-8')) + + # AppName + output.extend(b'\x01appname\x00') + output.extend(shortcut['AppName'].encode('utf-8') + b'\x00') + + # App ID (this is the key addition) + output.extend(b'\x02appid\x00') + output.extend(f'"{shortcut["Id"]}"'.encode('utf-8') + b'\x00') + + # Exe + output.extend(b'\x01exe\x00') + output.extend(f'"{shortcut["ExePath"]}"'.encode('utf-8') + b'\x00') + + # StartDir + output.extend(b'\x01StartDir\x00') + output.extend(b'"/home/deck"\x00') + + # Icon + output.extend(b'\x01icon\x00\x00') + + # ShortcutPath + output.extend(b'\x01ShortcutPath\x00\x00') + + # LaunchOptions + output.extend(b'\x01LaunchOptions\x00') + output.extend(f'"{shortcut["LaunchOptions"]}"'.encode('utf-8') + b'\x00') + + # IsHidden + output.extend(b'\x02IsHidden\x00\x00\x00\x00\x00') + + # AllowDesktopConfig + output.extend(b'\x02AllowDesktopConfig\x00\x01\x00\x00\x00') + + # AllowOverlay + output.extend(b'\x02AllowOverlay\x00\x01\x00\x00\x00') + + # OpenVR + output.extend(b'\x02OpenVR\x00\x00\x00\x00\x00') + + # Tags + output.extend(b'\x00tags\x00\x08') + + # Entry terminator + output.extend(b'\x08') + + # Final terminators + output.extend(b'\x08\x08') + + return output + + def main(): + try: + shortcuts = json.load(sys.stdin) + vdf_data = write_vdf(shortcuts) + sys.stdout.buffer.write(vdf_data) + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + if __name__ == '__main__': + main() + ''; + + # Create the SteamGridDB image downloader script + steamGridDbScript = pkgs.writeText "fetch_steamgriddb.py" '' + #!/usr/bin/env python3 + import sys + import json + import requests + import time + import hashlib + import struct + from pathlib import Path + + def generate_app_id(name): + """Generate Steam app ID for a game using Steam's apparent format.""" + # Get the current timestamp + timestamp = int(time.time()) + + # Create a hash of the name and timestamp + hash_input = f"{name}{timestamp}".encode('utf-8') + hash_obj = hashlib.md5(hash_input) + + # Take first 6 bytes of hash and combine with a fixed pattern + hash_bytes = hash_obj.digest()[:6] + + # Create a 64-bit number where: + # - First 6 bytes come from hash + # - Last 2 bytes are zeros (matching Steam's pattern) + full_bytes = hash_bytes + b'\x00\x00' + + # Convert to 64-bit integer + app_id = struct.unpack(">Q", full_bytes)[0] + + # Ensure the number is in the correct range by setting specific bits + app_id |= (1 << 62) # Set the second-to-highest bit + app_id &= ~(1 << 63) # Clear the highest bit + + return app_id + + def download_image(url, path, headers=None): + """Download an image from a URL to a specified path.""" + if url: + try: + response = requests.get(url) # Don't use auth headers for actual download + if response.status_code == 200: + path.write_bytes(response.content) + return True + else: + pass + except Exception as e: + print(f"Error downloading: {e}") + return False + + def fetch_images(api_key, game_name, grid_ids, output_dir): + """Fetch images for a game from SteamGridDB.""" + print(f"\nProcessing images for {game_name}") + headers = {"Authorization": f"Bearer {api_key}"} + base_url = "https://www.steamgriddb.com/api/v2" + app_id = generate_app_id(game_name) + + # Create output directories + grid_dir = output_dir / "config/grid" + hero_dir = output_dir / "config/heroes" + logo_dir = output_dir / "config/logos" + + print(f"Using directories:") + print(f"Grid: {grid_dir}") + print(f"Hero: {hero_dir}") + print(f"Logo: {logo_dir}") + + grid_dir.mkdir(parents=True, exist_ok=True) + hero_dir.mkdir(parents=True, exist_ok=True) + logo_dir.mkdir(parents=True, exist_ok=True) + + # Test API connection + test_response = requests.get(f"{base_url}/grids/game/1", headers=headers) + print(f"API test response: {test_response.status_code}") + if test_response.status_code == 401: + print("API authentication failed. Check your API key.") + return + + image_types = { + "grid": (grid_dir / f"{app_id}.jpg", grid_ids.get("grid"), "grids"), + "hero": (hero_dir / f"{app_id}.jpg", grid_ids.get("hero"), "heroes"), + "logo": (logo_dir / f"{app_id}.png", grid_ids.get("logo"), "logos"), + "list": (grid_dir / f"{app_id}_list.jpg", grid_ids.get("list"), "grids") + } + + for img_type, (output_path, specific_id, asset_type) in image_types.items(): + if specific_id: + # Get direct file URL from the API + url = f"{base_url}/{asset_type}/{specific_id}" + try: + response = requests.get(url, headers=headers) + if response.status_code == 200: + data = response.json() + if data.get("success"): + image_url = data["data"]["url"] + if download_image(image_url, output_path): + print(f"Successfully downloaded {img_type} image") + else: + print(f"Failed to download {img_type} image") + else: + print(f"API error: {data}") + else: + print(f"API request failed: status {response.status_code}") + if response.headers.get('content-type', "").startswith('application/json'): + print(f"Response: {response.json()}") + except Exception as e: + print(f"Error processing {img_type}: {e}") + time.sleep(1) # Rate limiting + + def main(): + if len(sys.argv) != 4: + print("Usage: script.py API_KEY game_data.json output_dir", file=sys.stderr) + sys.exit(1) + + api_key = sys.argv[1] + game_data_file = sys.argv[2] + output_dir = Path(sys.argv[3]) + + print(f"Loading game data from: {game_data_file}") + print(f"Output directory: {output_dir}") + + try: + with open(game_data_file) as f: + games = json.load(f) + print(f"Loaded {len(games)} games") + + for game in games: + if "steamGridDbIds" in game: + fetch_images(api_key, game["name"], game["steamGridDbIds"], output_dir) + else: + print(f"No steamGridDbIds for {game['name']}, skipping") + + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + if __name__ == "__main__": + main() + ''; + + # Python environment with required packages + pythonEnv = pkgs.python3.withPackages (ps: with ps; [ + requests + ]); + + # Helper function to convert hex char to decimal + hexCharToInt = c: + let + hexChars = { + "0" = 0; "1" = 1; "2" = 2; "3" = 3; "4" = 4; + "5" = 5; "6" = 6; "7" = 7; "8" = 8; "9" = 9; + "a" = 10; "b" = 11; "c" = 12; "d" = 13; "e" = 14; "f" = 15; + "A" = 10; "B" = 11; "C" = 12; "D" = 13; "E" = 14; "F" = 15; + }; + in hexChars.${c}; + + # Convert hex string to decimal number + hexToInt = str: + let + len = builtins.stringLength str; + chars = lib.stringToCharacters str; + go = pos: nums: + if pos == len then 0 + else + let + char = builtins.elemAt chars pos; + num = hexCharToInt char; + exp = len - pos - 1; + factor = builtins.foldl' (a: b: a * 16) 1 (lib.range 1 exp); + in + (num * factor) + (go (pos + 1) nums); + in go 0 chars; + + # Helper function for the ID generation - handling large numbers + generateAppId = name: + let + hash = builtins.hashString "md5" name; + # Instead of trying to create a massive number, we'll generate a unique pattern + # that will create a consistent large number when interpreted by Python + sixteenHex = builtins.substring 0 16 hash; + # Format it as a string that Python will interpret as a large number + shortcutAppId = "0x${sixteenHex}0000"; + in shortcutAppId; + + # Convert games to shortcuts format + gameToShortcut = game: { + Id = generateAppId game.name; + AppName = game.name; + ExePath = game.path; + LaunchOptions = game.extraLaunchOptions; + }; + + # Script to update shortcuts and fetch images + updateSteamShortcuts = pkgs.writeShellScriptBin "update-steam-shortcuts" '' + STEAM_DIR="$HOME/.local/share/Steam" + USERDATA_DIR="$STEAM_DIR/userdata" + CACHE_DIR="$HOME/.cache/steam-shortcuts" + + if [ ! -d "$USERDATA_DIR" ]; then + echo "Steam userdata directory not found!" + exit 1 + fi + + # Make sure Steam is not running + if pgrep -x "steam" > /dev/null; then + echo "Please close Steam before updating shortcuts" + exit 1 + fi + + mkdir -p "$CACHE_DIR" + + # Process each Steam user directory + for USER_DIR in "$USERDATA_DIR"/*; do + if [ -d "$USER_DIR" ]; then + USER_ID="$(basename "$USER_DIR")" + CONFIG_DIR="$USER_DIR/config" + mkdir -p "$CONFIG_DIR" + + echo "Processing Steam user $USER_ID..." + + # Create games JSON file + GAMES_JSON="$CACHE_DIR/games.json" + echo '${builtins.toJSON (map gameToShortcut gamesConfig.games)}' > "$GAMES_JSON" + + # Generate shortcuts.vdf + cat "$GAMES_JSON" | ${pythonEnv}/bin/python3 ${vdfGeneratorScript} > "$CONFIG_DIR/shortcuts.vdf" + + # Download images if API key is set + if [ "${gamesConfig.steamGridDbApiKey}" != "YOUR_API_KEY_HERE" ]; then + echo '${builtins.toJSON gamesConfig.games}' > "$CACHE_DIR/games_full.json" + ${pythonEnv}/bin/python3 ${steamGridDbScript} "${gamesConfig.steamGridDbApiKey}" "$CACHE_DIR/games_full.json" "$USER_DIR" + fi + + echo "Updated shortcuts and images for $USER_ID" + fi + done + + echo "Done! You can now start Steam." + ''; + +in { + # Add the update script to system packages + environment.systemPackages = [ updateSteamShortcuts ]; + + # Optional: Create a systemd user service to update shortcuts on login + systemd.user.services.update-steam-shortcuts = { + enable = true; + description = "Update Steam shortcuts"; + wantedBy = [ "graphical-session.target" ]; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${updateSteamShortcuts}/bin/update-steam-shortcuts"; + RemainAfterExit = true; + }; + }; +} \ No newline at end of file